T-SQL: How to avoid string manipulation on this XML? - sql-server

I need to move a node from one place to another in some XML, but after becoming frustrated, I used string manipulation.
I'm trying to move <ReaderTypeID>5</ReaderTypeID> from under <SCPReplyMessage> to be under <SCPReplyMessage><tran>
The section of code where I take a node from outside tran and move it inside tran became troubling and I had to get it working, so I resorted to a more comfortable (but inefficient) approach: string manipulation.
-- move ReaderTypeID from outside <tran> to be inside <tran>
DECLARE #rtidXml VARCHAR(100)
SELECT #rtidXml = CONVERT(VARCHAR(100),#ReplyMessageXml.query('/SCPReplyMessage/ReaderTypeID'))
DECLARE #st NVARCHAR(max)
SET #st = CONVERT(NVARCHAR(MAX),#tranXml)
SET #st = REPLACE(#st,'</tran>',#rtidXml + '</tran>')
SET #tranXml.modify('delete /SCPReplyMessage/ReaderTypeID')
I'd like to accomplish the same result without the CONVERT to and from XML.
Thanks!
the function:
CREATE FUNCTION dbo.udf_mTranAddl (#ReplyMessageXml XML)
returns XML
AS
BEGIN
DECLARE #tranXml XML
SELECT #tranXml = #ReplyMessageXml.query('/SCPReplyMessage/tran')
-- Discard extraneous tran elements
SET #tranXml.modify('delete /tran/ser_num')
SET #tranXml.modify('delete /tran/time')
SET #tranXml.modify('delete /tran/sys')
SET #tranXml.modify('delete /tran/sys_comm')
-- move ReaderTypeID from outside <tran> to be inside <tran>
DECLARE #rtidXml VARCHAR(100)
SELECT #rtidXml = CONVERT(VARCHAR(100),#ReplyMessageXml.query('/SCPReplyMessage/ReaderTypeID'))
DECLARE #st NVARCHAR(max)
SET #st = CONVERT(NVARCHAR(MAX),#tranXml)
SET #st = REPLACE(#st,'</tran>',#rtidXml + '</tran>')
SET #tranXml.modify('delete /SCPReplyMessage/ReaderTypeID')
RETURN CONVERT(xml, #st)
END
Input #ReplyMessageXml:
<SCPReplyMessage>
<ContDeviceID>5974</ContDeviceID>
<LocalTime>2019-08-29T12:35:43</LocalTime>
<Priority>false</Priority>
<ReaderTypeID>5</ReaderTypeID>
<Deferred>false</Deferred>
<tran>
<ser_num>147</ser_num>
<time>1567096543</time>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<sys>
<error_code>4</error_code>
</sys>
<sys_comm>
<current_primary_comm>123</current_primary_comm>
<current_alternate_comm>4</current_alternate_comm>
</sys_comm>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
</tran>
<SCPId>99</SCPId>
<ReplyType>7</ReplyType>
<ChannelNo>-1</ChannelNo>
</SCPReplyMessage>
output (which is correct):
<tran>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
<ReaderTypeID>5</ReaderTypeID>
</tran>
FINAL RESULT:
Thanks to #PeterHe
CREATE FUNCTION dbo.udf_mTranAddl (#ReplyMessageXml XML)
returns XML
AS
BEGIN
DECLARE #tranXml XML
SELECT #tranXml = #ReplyMessageXml.query('/SCPReplyMessage/tran')
-- Discard extraneous tran elements
SET #tranXml.modify('delete /tran/ser_num')
SET #tranXml.modify('delete /tran/time')
SET #tranXml.modify('delete /tran/sys')
SET #tranXml.modify('delete /tran/sys_comm')
-- move ReaderTypeID from outside <tran> to be inside <tran>
DECLARE #x1 xml;
SELECT #x1=#ReplyMessageXml.query('SCPReplyMessage/ReaderTypeID');
SET #tranXml.modify('insert sql:variable("#x1") into (/tran)[1]')
SET #tranXml.modify('delete /SCPReplyMessage/ReaderTypeID')
RETURN #tranXml
END
GO

YOu can do it using xquery:
DECLARE #x xml = '<SCPReplyMessage>
<ContDeviceID>5974</ContDeviceID>
<LocalTime>2019-08-29T12:35:43</LocalTime>
<Priority>false</Priority>
<ReaderTypeID>5</ReaderTypeID>
<Deferred>false</Deferred>
<tran>
<ser_num>147</ser_num>
<time>1567096543</time>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<sys>
<error_code>4</error_code>
</sys>
<sys_comm>
<current_primary_comm>123</current_primary_comm>
<current_alternate_comm>4</current_alternate_comm>
</sys_comm>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
</tran>
<SCPId>99</SCPId>
<ReplyType>7</ReplyType>
<ChannelNo>-1</ChannelNo>
</SCPReplyMessage>'
DECLARE #output xml;
SELECT #output = #x.query('/SCPReplyMessage/tran');
SET #Output.modify('delete(/tran/ser_num)');
SET #Output.modify('delete(/tran/time)');
SET #Output.modify('delete(/tran/sys)');
SET #Output.modify('delete(/tran/sys_comm)');
DECLARE #x1 xml;
SELECT #x1=#x.query('SCPReplyMessage/ReaderTypeID');
SET #output.modify('insert sql:variable("#x1") into (/tran)[1]')
SELECT #output;

Here is a much easier way by using XQuery FLWOR expression. The main idea is to construct what you need in one single statement instead of moving, deleting, inserting, etc.
SQL
DECLARE #xml XML =
N'<SCPReplyMessage>
<ContDeviceID>5974</ContDeviceID>
<LocalTime>2019-08-29T12:35:43</LocalTime>
<Priority>false</Priority>
<ReaderTypeID>5</ReaderTypeID>
<Deferred>false</Deferred>
<tran>
<ser_num>147</ser_num>
<time>1567096543</time>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<sys>
<error_code>4</error_code>
</sys>
<sys_comm>
<current_primary_comm>123</current_primary_comm>
<current_alternate_comm>4</current_alternate_comm>
</sys_comm>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
</tran>
<SCPId>99</SCPId>
<ReplyType>7</ReplyType>
<ChannelNo>-1</ChannelNo>
</SCPReplyMessage>';
SELECT #xml.query('<tran>{
for $x in /SCPReplyMessage/tran
return ($x/source_type,
$x/source_number,
$x/tran_type,
$x/tran_code,
$x/c_id,
$x/oal,
$x/../ReaderTypeID)
}</tran>');

Related

Return and Store string from an execute statement to be used later

I've got a procedure call that is used by several groups/processes etc.
The call works as follows:
EXEC LWP_PAYMENT_URL #order_no, #dept
and it returns a string like this
NzI2NzU4NabNzEyMj24Ny1zYQ=
I'm given the assignment to create a url path as follows
DECLARE #url_path VARCHAR(4000)
SET #url_path = 'https://www.website.com/payment?code='
DECLARE #ReturnValue VARCHAR(4000) = ''
EXEC #ReturnValue = LWP_PAYMENT_URL #order_no, #dept
SET #url_path = #url_path + #ReturnValue
SELECT #ReturnValue, #url_path
My goal is to take the hard coded url_path and get the encoded string from the execute and save it in a variable and concatenate it to the url_path.
What I'm seeing is that the string is returned part of the execute call instead of setting it to #ReturnValue and then looks like I get a zero value being saved and concatenated.
Added these are the final two lines of the LWP_PAYMENT_URL procedure.
DECLARE #Encoded VARCHAR(500) = CONVERT(VARCHAR(500), (SELECT CONVERT(VARBINARY, #string) FOR XML PATH(''), BINARY BASE64))
SELECT #Encoded AS [Encoded]
Thank you
Your stored procedure should be doing this instead:
CREATE OR ALTER PROCEDURE dbo.LWP_PAYMENT_URL
...#input parameters...,
#encoded varchar(500) = NULL OUTPUT
AS
BEGIN
SET NOCOUNT ON;
...
SET #Encoded = CONVERT(varchar(500),
(SELECT CONVERT(VARBINARY, #string) FOR XML PATH(''), BINARY BASE64));
END
And then the caller says:
DECLARE #ReturnValue varchar(500);
EXEC dbo.LWP_PAYMENT_URL #order_no, #dept,
#Encoded = #ReturnValue output;
If you can't change the stored procedure, create a separate one, or a table-valued UDF as suggested in the comments, or (assuming there are no other SELECTs in the procedure we can't see):
CREATE TABLE #foo(ReturnValue varchar(500));
INSERT #foo EXEC dbo.LWP_PAYMENT_URL ...;
DECLARE #ReturnValue varchar(500);
SELECT #ReturnValue = ReturnValue FROM #foo;
That's gross, though, and basically an abuse of how data sharing should work in SQL Server.
Ideally what you should do is, if the logic is the same for all uses, put that logic in some type of module that is much easier to reuse (e.g. a table-valued function). Then this existing stored procedure can maintain the current behavior (except it would call the function instead of performing the calculation locally), and you can create a different stored procedure (or just call the function directly, if this is all your code is doing), and the logic doesn't have to be duplicated, and you don't have to trample on their stored procedure.

Why a scalar function call for string splitting can lead to session block?

Recently, we monitored a very strange issue about SQL Server session block, there was a scalar function for string splitting process which used in a loop code block within the stored procedure, we've checked the code, there was no any operations on database tables, why other sessions were blocked by the session that invoking the function?
here is the function definitions:
CREATE function [dbo].[splits](#SourceSql varchar(max), #StrSeprate varchar(10), #y int)
returns varchar(max) as
begin
declare #i int
declare #idx int
declare #s varchar(max)
if(right(#SourceSql,1)!=#StrSeprate)begin
set #SourceSql=#SourceSql+#StrSeprate
end
set #idx=0
set #i=charindex(#StrSeprate,#SourceSql)
while #i>=1
begin
set #s=left(#SourceSql,#i-1)
set #SourceSql=substring(#SourceSql,#i+1,len(#SourceSql)-#i)
set #i=charindex(#StrSeprate,#SourceSql)
set #idx=#idx+1
if (#idx=#y) begin
break
end
set #s=null
end
return #s
end
This function can not cause the blocking on your instance. You can check other previous operation over same Session\Request.
But, I can help you with other option, instead of this function. Below one is for Splitting Integer values but you can use for VARCHAR as well with changing the datatype.
DECLARE #ids NVARCHAR(MAX) = N'115676,115678,115679,115680,115681'
DECLARE #input_xml XML
SELECT #input_xml = Cast('<root><x>'+ Replace(#ids, ',', '</x><x>')+ '</x></root>' AS XML)
SELECT f.x.value('.', 'BIGINT') AS Id
FROM #input_xml.nodes('/root/x') f(x)

SQL Server while loop not printing last value

I have below code, why i am not trying to get the last value in the temp table for #waitlistitmeID
10011 is not getting stored, though its len is 19 and it should be inserted.
Need help in the query.
DECLARE #pos INT
DECLARE #len INT
DECLARE #value VARCHAR(MAX)
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011'
declare #MovingWaitListItems table ( WaitlistItemID int, WaitlistItemGUID numeric(16,0))
set #pos = 0
set #len = 0
while CHARINDEX(',', #WaitlistItemID, #pos+1)>0
BEGIN
set #len = CHARINDEX(',', #WaitlistItemID, #pos+1) - #pos
set #value = SUBSTRING(#WaitlistItemID, #pos, #len)
print #pos
print #len
print #value
INSERT INTO #MovingWaitListItems
SELECT WaitlistItemID, WaitlistItemGUID
FROM SXAAMWLWaitList
Where WaitlistItemID = #value
select * from #MovingWaitListItems
set #pos = CHARINDEX(',', #WaitlistItemID, #pos+#len) +1
print #pos
END
For above, i would like to use of xml node method instead of use of loops with substring(), CHARINDEX() function
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011'
declare #MovingWaitListItems table ( WaitlistItemID int, WaitlistItemGUID numeric(16,0))
;with cte as
(
select
a.value('.', 'varchar(max)') [WaitlistItemID] from
(
select CAST('<m>'+REPLACE(#WaitlistItemID, ',', '</m><m>')+'</m>' as xml) as waitlist
) w cross apply waitlist.nodes ('/m') as split(a)
)
INSERT INTO #MovingWaitListItems
select S.WaitlistItemID, S.WaitlistItemGUID from SXAAMWLWaitList S
JOIN CTE C ON C.[WaitlistItemID] = S.WaitlistItemID
select * from #MovingWaitListItems
For the moment I'll give you the benefit of the doubt and assume that there is some good reason for #WaitlistItemID to be a comma-delimited string. Your while loop condition says that processing should continue as long as a comma exists somewhere in the part of the string you haven't parsed yet. Of course, as Juan Carlos Oropeza points out in his answer, this will always be false for the last item in your list, because you haven't placed a comma after the last value (10011).
However, I would not recommend repeating the insert statement after the loop as in Juan's answer, because there's an easy way to avoid the duplication: just reformulate the loop so its condition instead asks whether there is any part of the string you haven't processed yet. For instance:
declare #cursorPosition int = 1;
declare #commaPosition int;
declare #valueLength int;
declare #value varchar(max);
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011';
declare #MovingWaitListItems table (WaitlistItemID int, WaitlistItemGUID numeric(16,0));
while #cursorPosition < len(#WaitlistItemID)
begin
set #commaPosition = charindex(',', #WaitlistItemID, #cursorPosition);
set #valueLength = case
when #commaPosition = 0 then len(#WaitlistItemID) - #cursorPosition + 1
else #commaPosition - #cursorPosition
end;
set #value = substring(#WaitlistItemID, #cursorPosition, #valueLength);
set #cursorPosition = #cursorPosition + #valueLength + 1;
insert #MovingWaitListItems
select WaitlistItemID, WaitlistItemGUID
from SXAAMWLWaitList
where WaitlistItemID = #value;
end;
select * from #MovingWaitListItems;
Hopefully this answers the question of why your loop isn't performing as you expect. The question I have for you is why you're resorting to parsing a delimited string in SQL in the first place. If #WaitlistItemID is a table of identifiers instead of a delimited string, then the equivalent logic is:
declare #WaitlistItemID table (ID int);
insert #WaitlistItemID values (10008),(10009),(10010),(10011);
declare #MovingWaitListItems table (WaitlistItemID int, WaitlistItemGUID numeric(16,0));
insert #MovingWaitListItems
select W.WaitlistItemID, W.WaitlistItemGUID
from
SXAAMWLWaitList W
inner join #WaitlistItemID ID on W.WaitlistItemID = ID.ID;
select * from #MovingWaitListItems;
Aside from being much easier to understand, this is also going to have vastly better performance than the equivalent iterative solution; set-based logic like this is SQL's forte. while loops in SQL are something you should fall back on only when it's absolutely necessary.
If, in your actual application, #MovingWaitListItems needs to be a parameter to a stored procedure (or similar), you can enable that functionality by creating a custom type. For instance:
create type dbo.IdentifierList as table (ID int);
You can use this type for parameters to stored procedures and functions like so:
create procedure DoSomething(#WaitlistItemID dbo.IdentifierList readonly)
as
select W.WaitlistItemID, W.WaitlistItemGUID
from
SXAAMWLWaitList W
inner join #WaitlistItemID ID on W.WaitlistItemID = ID.ID;
And invoke it like this:
declare #WaitlistItemID dbo.IdentifierList;
insert #WaitlistItemID values (10008),(10009),(10010),(10011);
exec DoSomething #WaitlistItemID;
Perhaps you're already aware of all this and there is a good reason for #WaitlistItemID to be a delimited string. For instance, maybe you're maintaining a stored procedure that was written to accept a delimited string as a parameter and you're not able to refactor the caller(s) to use a table-type parameter instead. In that case, I would still recommend a change to your original design: instead of writing the parsing logic directly into this specific application, consider writing yourself a generic function that takes a delimited string and outputs a table, as described in questions like this one, so you can reuse it the next time a similar scenario comes up instead of having to struggle with the parsing logic again.
Imagine if you have a #WaitlistItemID with a single element 10100 you will also skip it because your loop condition will be false.
while CHARINDEX(',', #WaitlistItemID, #pos+1)>0
Same is true for the last element. So you have to add another interaction at the end of the loop
while ...
begin
end
set #len = LEN(#WaitlistItemID)
if #len > 0
BEGIN
set #value = SUBSTRING(#WaitlistItemID, #pos, #len)
INSERT INTO #MovingWaitListItems
SELECT WaitlistItemID, WaitlistItemGUID
FROM SXAAMWLWaitList
Where WaitlistItemID = #value
END
Problem is related to the number of commas i think.
You can add one more item as dummy, you will see 10011 when you add a comma like this:
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011,'
or
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011,0'

Find and replace just a part of a xml value using XQuery?

I have an XML in one of my columns, that is looking something like this:
<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path3/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path21/anothertest/second.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path15/test123/file.doc</VorlagenHistorie>
</BenutzerEinstellungen>
I would like to replace all test123 occurances (there can be more than one) in VorlagenHistorie with another test, that all paths direct to test123 after my update.
I know, how you can check and replace all values with an equality-operator, I saw it in this answer:
Dynamically replacing the value of a node in XML DML
But is there a CONTAINS Operator and is it possible to replace INSIDE of a value, I mean only replace a part of the value?
Thanks in advance!
I would not suggest a string based approach normally. But in this case it might be easiest to do something like this
declare #xml XML=
'<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
SELECT CAST(REPLACE(CAST(#xml AS nvarchar(MAX)),'/test123/','/anothertest/') AS xml);
UPDATE
If this approach is to global you might try something like this:
I read the XML as derived table and write it back as XML. In this case you can be sure, that only Nodes with VorlageHistorie will be touched...
SELECT #xml.value('(/BenutzerEinstellungen/State)[1]','nvarchar(max)') AS [State]
,(
SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
FROM #xml.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
FOR XML PATH('VorlagenHistorie'),TYPE
)
FOR XML PATH('BenutzerEinstellungen');
UPDATE 2
Try this. It will read all nodes, which are not called VorlagenHistorie as is and will then add the VorlageHistorie nodes with replaced values. The only draw back might be, that the order of your file will be different, if there are other nodes after the VorlagenHistorie elements. But this should not really touch the validity of your XML...
declare #xml XML=
'<BenutzerEinstellungen>
<State>Original</State>
<Unknown>Original</Unknown>
<UnknownComplex>
<A>Test</A>
</UnknownComplex>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
SELECT #xml.query('/BenutzerEinstellungen/*[local-name(.)!="VorlagenHistorie"]') AS [node()]
,(
SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
FROM #xml.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
FOR XML PATH('VorlagenHistorie'),TYPE
)
FOR XML PATH('BenutzerEinstellungen');
UPDATE 3
Use an updateable CTE to first get the values and then set them in one single go:
declare #tbl TABLE(ID INT IDENTITY,xmlColumn XML);
INSERT INTO #tbl VALUES
(
'<BenutzerEinstellungen>
<State>Original</State>
<Unknown>Original</Unknown>
<UnknownComplex>
<A>Test</A>
</UnknownComplex>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>')
,('<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>');
WITH NewData AS
(
SELECT ID
,xmlColumn AS OldData
,(
SELECT t.xmlColumn.query('/BenutzerEinstellungen/*[local-name(.)!="VorlagenHistorie"]') AS [node()]
,(
SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
FROM t.xmlColumn.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
FOR XML PATH('VorlagenHistorie'),TYPE
)
FOR XML PATH('BenutzerEinstellungen'),TYPE
) AS NewXML
FROM #tbl AS t
)
UPDATE NewData
SET OldData=NewXml;
SELECT * FROM #tbl;
A weird solution, but it worked well:
DECLARE #xml XML = '
<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path5/test123/third.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
DECLARE #Counter int = 1,
#newValue nvarchar(max),
#old nvarchar(max) = N'test123',
#new nvarchar(max) = N'anothertest';
WHILE #Counter <= #xml.value('fn:count(//*//*)','int')
BEGIN
SET #newValue = REPLACE(CONVERT(nvarchar(100), #xml.query('((/*/*)[position()=sql:variable("#Counter")]/text())[1]')), #old, #new)
SET #xml.modify('replace value of ((/*/*)[position()=sql:variable("#Counter")]/text())[1] with sql:variable("#newValue")');
SET #Counter = #Counter + 1;
END
SELECT #xml;
Output:
<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/anothertest/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path5/anothertest/third.doc</VorlagenHistorie>
</BenutzerEinstellungen>
If #shnugo's answer does not fit your needs, you can use XML/XQuery approach:
DECLARE #xml xml = '<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
DECLARE #from nvarchar(20) = N'test123';
DECLARE #to nvarchar(20) = N'another test';
DECLARE #newValue nvarchar(100) = REPLACE(CONVERT(nvarchar(100), #xml.query('(/BenutzerEinstellungen/VorlagenHistorie/text()[contains(.,sql:variable("#from"))])[1]')), #from, #to)
SET #xml.modify('
replace value of (/BenutzerEinstellungen/VorlagenHistorie/text()[contains(.,sql:variable("#from"))])[1]
with sql:variable("#newValue")')
SELECT #xml
gofr1's answer might be enhanced by using more specific XPath expressions:
DECLARE #Counter int = 1,
#newValue nvarchar(max),
#old nvarchar(max) = N'test123',
#new nvarchar(max) = N'anothertest';
WHILE #Counter <= #xml.value('fn:count(/BenutzerEinstellungen/VorlagenHistorie)','int')
BEGIN
SET #newValue = REPLACE(CONVERT(nvarchar(100), #xml.value('(/BenutzerEinstellungen/VorlagenHistorie)[sql:variable("#Counter")][1]','nvarchar(max)')), #old, #new)
SET #xml.modify('replace value of (/BenutzerEinstellungen/VorlagenHistorie[sql:variable("#Counter")]/text())[1] with sql:variable("#newValue")');
SET #Counter = #Counter + 1;
END
SELECT #xml;

how to use bulk collect in db2 cursor

Here is my procedure, I don't know how to use bulk collecton in cursor, that we can batch process the cursor data. Please help me, thanks!
CREATE PROCEDURE PROC_AUTOACTIVE
BEGIN ATOMIC
DECLARE v_sql VARCHAR(800);
DECLARE v_customer_id BIGINT;
DECLARE v_cardnum varchar(500);
DECLARE v_cardtype varchar(20);
DECLARE v_status varchar(10);
DECLARE v_lastname varchar(200);
DECLARE v_email varchar(150);
DECLARE v_mobile varchar(30);
DECLARE v_phone varchar(30);
DECLARE v_zipcode varchar(20);
DECLARE v_crm_mobile varchar(30);
DECLARE v_address varchar(500);
DECLARE v_order_count BIGINT;
DECLARE v_order_no varchar(500);
DECLARE not_found CONDITION FOR SQLSTATE '02000';
DECLARE at_end INT DEFAULT 0;
DECLARE c_customers CURSOR FOR s_cardsinfo;
DECLARE CONTINUE HANDLER FOR not_found SET at_end = 1;
SET v_sql = 'select t.customer_id, v.CUSTOMER_ID, v.CARD_TYPE, v.STATUS
from customer_tempcard t,
vip_fields v
where t.tempcard_num=v.CUSTOMER_ID
and t.status=1
and v.STATUS=1
and exists (select id
from orders o
where o.FK_CUSTOMER=t.CUSTOMER_ID
and o.FK_ORDER_STATUS in (3,4,6)) ';
PREPARE s_cardsinfo FROM v_sql;
OPEN c_customers;
--fetch card info
LOOP_CUSTOMER_INFO:
LOOP
FETCH c_customers INTO v_customer_id,v_cardnum,v_cardtype,v_status;
IF at_end <> 0 THEN
SET at_end = 0;
LEAVE LOOP_CUSTOMER_INFO;
END IF;
select c.LOGON_ID, o.DEV_CUSTOMER_NAME,
o.DEV_MOBILE, o.DEV_PHONE, o.DEV_ZIP, o.DEV_ADDRESS, o.ORDER_NO
into v_email, v_lastname,
v_mobile, v_phone, v_zipcode, v_address, v_order_no
from orders o,customer c
where o.FK_CUSTOMER=c.ID
and o.FK_CUSTOMER=v_customer_id
and o.FK_ORDER_STATUS in (3,4,6)
order by o.ID desc
fetch first 1 rows only;
IF v_mobile <> null THEN
SET v_crm_mobile = v_mobile;
ELSE
SET v_crm_mobile = v_phone;
END IF;
update customer_tempcard ct
set ct.STATUS='0',
ct.UPDATE_TIME=current_timestamp
where ct.CUSTOMER_ID=v_customer_id;
update card_store cs
set cs.STATUS='0',
cs.UPDATE_TIME=current_timestamp
where cs.CARD_NUM=v_cardnum;
update vip_fields v
set v.LAST_NAME=v_lastname,
v.EMAIL=v_email, v.MOBILE=v_crm_mobile,
v.CUSTOMER_UPDATE_TIME=current_timestamp,
v.UPDATE_TIME=current_timestamp,
v.OPERATION_TYPE='2',
v.CREATE_SOURCE='2',
v.STATUS='0',
v.ZIP_CODE=v_zipcode,
v.ADDRESS=v_address
where customer_id = v_cardnum;
update customer c
set c.VIP_CARD_NUMBER=v_cardnum,
c.VIP_CARD_NAME=v_lastname,
c.VIP_EMAIL=v_email,
c.VIP_CARD_TYPE=v_cardtype,
c.LEVEL=v_cardtype,
c.VIP_ZIP=v_zipcode,
c.VIP_MOBILE=v_crm_mobile,
c.VIP_ADDRESS=v_address,
c.FK_CUSTOMER_GRADE='1'
where c.id=v_customer_id;
insert into beactiveinfo
values (default,v_cardnum,v_order_no,current_timestamp);
END LOOP;
CLOSE c_customers;
END
BULK COLLECT is part of the Oracle compatibility feature in DB2, so, firstly, you cannot use it in the DB2 SQL PL native context, which you are using in your procedure. Secondly, you don't use BULK COLLECT in a cursor. You use SELECT ... BULK COLLECT INTO an_array_variable ... to populate a PL/SQL array. If you intend then to loop over that array, you won't get any performance benefit over the cursor, while incurring the memory overhead for storing the entire result set in the application memory.

Resources