Replace value of one node in array of nodes in T-SQL - sql-server

If I have a column in a table as xml with the following node structure:
<ArrayOfPickListObjectBaseModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<PickListObjectBaseModel>
<MasterObjectId>3964405</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>405716</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>5872525</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
</ArrayOfPickListObjectBaseModel>
I can successfully retrieve all records that have a particular MasterObjectId
SELECT p.MyPrimaryKey,
m.value('(MasterObjectId)[1]', 'INT') as MasterObjectID,
p.MasterObjectIDs
FROM PickList p
CROSS APPLY MasterObjectIDs.nodes('//PickListObjectBaseModel') AS t1(m)
WHERE m.value('(MasterObjectId)[1]', 'INT') = 3964405
But how would I replace that number in all matching records if needed?
So in the above example, replace 3964405 with 999
This is bad syntax, but I think it is similar to:
DECLARE #oldId INT = 13131180;
DECLARE #newId INT = 99999;
UPDATE PickList
SET MasterObjectIDs.modify('replace value of
(//PickListObjectBaseModel/MasterObjectId/text()=sql:variable("#oldId"))[1] with sql:variable("#newId")');

Here is the complete solution to replace text nodes based on value
The FROM part will speed it up as it reduces the number of rows that are affected, otherwise it performs the action on all rows
DECLARE #oldId INT = 13131180;
DECLARE #newId INT = 99999;
UPDATE PickList
SET MasterObjectIDs.modify('replace value of
(//PickListObjectBaseModel/MasterObjectId[text() = sql:variable("#oldId")]/text())[1]
with sql:variable("#newId")')
FROM PickList p
CROSS APPLY MasterObjectIDs.nodes('//PickListObjectBaseModel') AS t1(m)
WHERE m.value('(MasterObjectId)[1]', 'INT') = #oldId

Great, that you found a solution (voted it up), just some hints:
A dummy table with three rows:
DECLARE #DummyTable TABLE(ID INT IDENTITY, Comment VARCHAR(100), MasterObjectIDs XML);
INSERT INTO #DummyTable VALUES
('Your example', N'<ArrayOfPickListObjectBaseModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<PickListObjectBaseModel>
<MasterObjectId>3964405</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>405716</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>5872525</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
</ArrayOfPickListObjectBaseModel>'
)
,('More than one target', N'<ArrayOfPickListObjectBaseModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<PickListObjectBaseModel>
<MasterObjectId>3964405</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>405716</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>3964405</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
</ArrayOfPickListObjectBaseModel>'
),
('no target', N'<ArrayOfPickListObjectBaseModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<PickListObjectBaseModel>
<MasterObjectId>1111</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>405716</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
<PickListObjectBaseModel>
<MasterObjectId>5872525</MasterObjectId>
<IsSelected>false</IsSelected>
</PickListObjectBaseModel>
</ArrayOfPickListObjectBaseModel>');
--The variables
DECLARE #oldId INT = 3964405;
DECLARE #newId INT = 99999;
--I use xml.exist(), which is even faster, than your approach to filter target rows:
UPDATE #DummyTable
SET MasterObjectIDs.modify('replace value of
(//PickListObjectBaseModel/MasterObjectId[text() = sql:variable("#oldId")]/text())[1]
with sql:variable("#newId")')
FROM #DummyTable p
WHERE p.MasterObjectIDs.exist('/ArrayOfPickListObjectBaseModel/PickListObjectBaseModel/MasterObjectId[text()=sql:variable("#oldId")]') = 1;
--the result
SELECT ##ROWCOUNT; --good the filter hits only 2 rows
SELECT * FROM #DummyTable; --bad, the XML more than one occurances is not edited everywhere...
If the id you are looking for, might occur more than once in one XML, your approach will not work for all of them, just for the first...
You can either run your statement in a loop, until exist() returns 0, or you might shredd the XML and rebuild it from scratch like here:
WITH UpdateableCTE AS
(
SELECT p.MasterObjectIds AS Original
,B.NewXML
FROM #DummyTable AS p
CROSS APPLY
(
SELECT
(
SELECT CASE WHEN plo.value(N'(MasterObjectId/text())[1]','int')=#oldId THEN #newId ELSE plo.value(N'(MasterObjectId/text())[1]','int') END AS MasterObjectId
,plo.value(N'(IsSelected/text())[1]','nvarchar(max)') AS IsSelected
FROM MasterObjectIDs.nodes(N'/ArrayOfPickListObjectBaseModel/PickListObjectBaseModel') AS A(plo)
FOR XML PATH(N'PickListObjectBaseMode'),ROOT(N'ArrayOfPickListObjectBaseModel'),TYPE
)
) AS B(NewXML)
WHERE p.MasterObjectIDs.exist('/ArrayOfPickListObjectBaseModel/PickListObjectBaseModel/MasterObjectId[text()=sql:variable("#oldId")]') = 1
)
UPDATE UpdateableCTE SET Original=NewXML;
--Only 2 rows are affected
SELECT ##ROWCOUNT; --good the filter hits only 2 rows
SELECT * FROM #DummyTable; --all occurances are switched...

Related

How to filter xml data type column in SQL Server

I have a table with two xml columns and the data inside the table is large. I would like to filter on the column which is of xml type to check if it contains a ID.
My sample xml column IncomingXML and value looks like this:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sws="abc">
<soapenv:Header>
<To xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none" soapenv:mustUnderstand="1">abc</To>
<Action xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none" soapenv:mustUnderstand="1">student</Action>
</soapenv:Header>
<soapenv:Body>
<sws:ProvisionStudent>
<sws:provisionStudentInput>
<sws:Operation>U</sws:Operation>
<sws:ADAccount>SU123456789</sws:ADAccount>
<sws:ADPassword>abcde</sws:ADPassword>
<sws:Prefix />
<sws:FirstName>ancd</sws:FirstName>
<sws:LastName>xyz</sws:LastName>
<sws:MiddleName />
<sws:Suffix />
<sws:Email>abc#yahoo.com</sws:Email>
<sws:EmplId>123456789</sws:EmplId>
<sws:CampusCode />
<sws:CompletionYear>0</sws:CompletionYear>
<sws:CurrentCumulativeGpa>0</sws:CurrentCumulativeGpa>
<sws:GraduationGpa />
<sws:GraduationProgramCode />
<sws:ProgramCode />
<sws:UserType />
</sws:provisionStudentInput>
</sws:ProvisionStudent>
</soapenv:Body>
</soapenv:Envelope>
Please help me with a query like
select *
from table
where IncomingXML like '%SU123456789%'
I tried the following but did not have any luck.
select *
from table
where cast(IncomingXML as nvarchar(max)) like '%SU123456789%'
You can use XQuery for this
DECLARE #toSearch nvarchar(100) = 'SU123456789';
WITH XMLNAMESPACES(
'http://schemas.xmlsoap.org/soap/envelope/' AS soapenv,
DEFAULT 'abc'
)
SELECT *
FROM YourTable t
WHERE t.IncomingXML.exist('/soapenv:Envelope/soapenv:Body/ProvisionStudent/provisionStudentInput/ADAccount[text() = sql:variable("#toSearch")]') = 1;
To search in any node, at the cost of efficiency
DECLARE #toSearch nvarchar(100) = 'SU123456789';
SELECT *
FROM YourTable t
WHERE t.IncomingXML.exist('//*[text() = sql:variable("#toSearch")]') = 1;
db<>fiddle
You can obviously also embed a literal instead of a variable in the XQuery.

XML SQL Optimization needed

Hi I been trying to insert into two tables (groups and fields) from XML in SQL. But the solution either doesn't fix my problem or performance is slow as Groups and Fields can number in hundreds of thousands.
A sample of the XML:
<?xml version="1.0" encoding="utf-16"?>
<FB_Flow
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" id="1">
<groups>
<FB_FlowGroup counter="1125" position="2" positionparent="0" id="0">
<fields>
<FB_FlowField>
<value>TEST1</value>
<counter>111</counter>
<lineposition>1</lineposition>
</FB_FlowField>
<FB_FlowField>
<value>TEST2</value>
<counter>222</counter>
<lineposition>2</lineposition>
<groupid>0</groupid>
</FB_FlowField>
<FB_FlowField>
<value>TEST3</value>
<counter>333</counter>
<lineposition>3</lineposition>
</FB_FlowField>
</fields>
</FB_FlowGroup>
<FB_FlowGroup counter="1126" position="3" positionparent="2" id="0">
<fields>
<FB_FlowField>
<value>TEST1</value>
<counter>18</counter>
<lineposition>1</lineposition>
</FB_FlowField>
</fields>
</FB_FlowGroup>
</groups>
</FB_Flow>
The first part works fine (To get a list of all groups)
insert into #Groups (intGroupCounter,intGroupPosition,intGroupPositionParent)
SELECT
gcounter = Groups.value('#counter[1]', 'int'),
gposition = Groups.value('#position[1]', 'int'),
gpositionparent = Groups.value('#positionparent[1]', 'int')
FROM
#FlowXML.nodes('/FB_Flow/groups/FB_FlowGroup') AS XTbl(Groups)
This second part fails for the most part (To get all fields with the parent group position):
insert into #Fields (intGroupPosition,vFieldValue,intFieldCounter,intFieldPosition)
SELECT
gposition = XTbl.Groups.value('#position', 'int'),
fValue = XTbl2.Fields.value('value[1]', 'varchar(max)'),
fcounter = XTbl2.Fields.value('counter[1]', 'int'),
fposition = XTbl2.Fields.value('lineposition[1]', 'int')
FROM
#FlowXML.nodes('/FB_Flow/groups/FB_FlowGroup') AS XTbl(Groups)
cross APPLY
Groups.nodes('fields/FB_FlowField') AS XTbl2(Fields)
I have been getting around this by using a cursor and selecting the group by the position attribute but the performance is very poor.
DECLARE #GroupCounter int,
#GroupPosition int,
#GroupPositionParent int,
#GroupID int
DECLARE #Groups table
(
intGroupCounter int not null,
intGroupPosition int not null,
intGroupPositionParent int null default 0
)
insert into #Groups (intGroupCounter,intGroupPosition,intGroupPositionParent)
SELECT
gcounter = Groups.value('#counter[1]', 'int'),
gposition = Groups.value('#position[1]', 'int'),
gpositionparent = Groups.value('#positionparent[1]', 'int')
FROM
#FlowXML.nodes('/FB_Flow/groups/FB_FlowGroup') AS XTbl(Groups)
DECLARE cur cursor for
SELECT
intGroupCounter,
intGroupPosition,
intGroupPositionParent
FROM
#Groups
OPEN cur
FETCH NEXT FROM cur INTO #GroupCounter, #GroupPosition, #GroupPositionParent
WHILE ##FETCH_STATUS = 0
BEGIN
insert into FB_T_FlowGroups (FH_ID,DTC_GroupCounter,Position,PositionParent)
values (#FlowHeaderID,#GroupCounter,#GroupPosition,#GroupPositionParent)
select #GroupID = ##IDENTITY
--declare #Path varchar(max) = '/FB_Flow/groups/FB_FlowGroup[#position="sql:variable("#GroupPosition")"]/fields/FB_FlowField'
insert into FB_T_FlowGroupField (FlowGroupID,ItemValue,DTC_ItemCounter)
SELECT
#GroupID,
XTbl.Fields.value('value[1]', 'varchar(max)'),
XTbl.Fields.value('counter[1]', 'int')
FROM
#FlowXML.nodes('/FB_Flow/groups/FB_FlowGroup[#position=sql:variable("#GroupPosition")]/fields/FB_FlowField') AS XTbl(Fields)
FETCH NEXT FROM cur INTO #GroupCounter, #GroupPosition, #GroupPositionParent
END
CLOSE cur
DEALLOCATE cur
Any Ideas?
What is your SQL Server version (SELECT ##VERSION;)?
Please try the following approach without a cursor. It should give you a tremendous performance improvement:
XML attributes don't need [1] position. Attributes are always unique.
XML elements need an adjustment in the XPath expression - text().
SQL
DECLARE #FlowXML XML =
N'<FB_Flow xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" id="1">
<groups>
<FB_FlowGroup counter="1125" position="2" positionparent="0" id="0">
<fields>
<FB_FlowField>
<value>TEST1</value>
<counter>111</counter>
<lineposition>1</lineposition>
</FB_FlowField>
<FB_FlowField>
<value>TEST2</value>
<counter>222</counter>
<lineposition>2</lineposition>
<groupid>0</groupid>
</FB_FlowField>
<FB_FlowField>
<value>TEST3</value>
<counter>333</counter>
<lineposition>3</lineposition>
</FB_FlowField>
</fields>
</FB_FlowGroup>
<FB_FlowGroup counter="1126" position="3" positionparent="2" id="0">
<fields>
<FB_FlowField>
<value>TEST1</value>
<counter>18</counter>
<lineposition>1</lineposition>
</FB_FlowField>
</fields>
</FB_FlowGroup>
</groups>
</FB_Flow>';
-- insert into #Groups (intGroupCounter,intGroupPosition,intGroupPositionParent)
SELECT gcounter = Groups.value('#counter', 'INT')
, gposition = Groups.value('#position', 'INT')
, gpositionparent = Groups.value('#positionparent', 'INT')
FROM #FlowXML.nodes('/FB_Flow/groups/FB_FlowGroup') AS XTbl(Groups);
--insert into #Fields (intGroupPosition,vFieldValue,intFieldCounter,intFieldPosition)
SELECT gposition = XTbl.Groups.value('#position', 'INT')
, fValue = XTbl2.Fields.value('(value/text())[1]', 'VARCHAR(MAX)')
, fcounter = XTbl2.Fields.value('(counter/text())[1]', 'INT')
, fposition = XTbl2.Fields.value('(lineposition/text())[1]', 'INT')
FROM #FlowXML.nodes('/FB_Flow/groups/FB_FlowGroup') AS XTbl(Groups)
CROSS APPLY Groups.nodes('fields/FB_FlowField') AS XTbl2(Fields);

Rename xml element with child elements in MS SQL

I try to rename element <Visible> to <IsVisible>, but this SELECT returns element Visible without child elements, how can I get Visible with UserId and RoleId elements?
DECLARE #xml XML =
N'<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<FieldId>2200</FieldId>
<Visible xsi:type="UserRole">
<UserId xsi:type="CurrentUserId" />
<RoleId>26</RoleId>
</Visible>
</Root>';
SELECT #xml.query(N'let $nd:=(//*[local-name()="Visible"])[1]
return
<IsVisible> {$nd/#*}
{$nd/text()}
</IsVisible>
')
Try this.
DECLARE #xml XML =
N'<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<FieldId>2200</FieldId>
<Visible xsi:type="UserRole">
<UserId xsi:type="CurrentUserId" />
<RoleId>26</RoleId>
</Visible>
</Root>';
SELECT #xml.query(N'let $nd:=(//*[local-name()="Visible"])[1]
return
<IsVisible> {$nd/#*}
{$nd/*}
</IsVisible>
')
You can either use XQuery (FLWOR) or a simple replace
DECLARE #xml XML =
N'<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<FieldId>2200</FieldId>
<Visible xsi:type="UserRole">
<UserId xsi:type="CurrentUserId" />
<RoleId>26</RoleId>
</Visible>
</Root>';
--Works, but will reorganise your namespace declarations
WITH XMLNAMESPACES('http://www.w3.org/2001/XMLSchema' AS xsd
,'http://www.w3.org/2001/XMLSchema-instance' AS xsi)
SELECT #xml.query(N'<Root>
{
for $nd in /Root/*
return
if(local-name($nd)!="Visible") then
$nd
else
<IsVisible>{$nd/#*}
{$nd/*}
</IsVisible>
}
</Root>
');
--Might be easier here
SELECT CAST(
REPLACE(REPLACE(
CAST(#xml AS NVARCHAR(MAX))
,'<Visible ','<IsVisible ')
,'</Visible>','</IsVisible>')
AS XML)

Relocate node with value in XML if not exists in mssql

I've got table in mssql, and one column of it contains XML. Most of XML in this column looks like this:
<AuthenticationParams xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<MyParams>
<Username>AmaryllisAPITest</Username>
<ApplicationId>3</ApplicationId>
</MyParams>
<AlsoParams>
<AuthBehavior>Authorization</AuthBehavior>
<SecretKey>MVHXAQA5kF4Ab9siV4vPA4aVPn1EKhbqIBrpCZx2Hg</SecretKey>
</AlsoParams>
</AuthenticationParams>
I want to relocate AuthBehavior node right after AuthenticationParams node, so it will look like this:
<AuthenticationParams xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<AuthBehavior>Authorization</AuthBehavior>
<MyParams>
<Username>AmaryllisAPITest</Username>
<ApplicationId>3</ApplicationId>
</MyParams>
<AlsoParams>
<SecretKey>MVHXAQA5kF4Ab9siV4vPA4aVPn1EKhbqIBrpCZx2Hg</SecretKey>
</AlsoParams>
</AuthenticationParams>
How can I do that? Thanks for any help.
Try this code:
DECLARE #xml xml = '<AuthenticationParams xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<MyParams>
<Username>AmaryllisAPITest</Username>
<ApplicationId>3</ApplicationId>
</MyParams>
<AlsoParams>
<AuthBehavior>Authorization</AuthBehavior>
<SecretKey>MVHXAQA5kF4Ab9siV4vPA4aVPn1EKhbqIBrpCZx2Hg</SecretKey>
</AlsoParams>
</AuthenticationParams>';
DECLARE #temp TABLE (XmlData xml);
INSERT #temp VALUES (#xml);
UPDATE t
SET XmlData.modify('insert /AuthenticationParams/AlsoParams/AuthBehavior
as first
into (/AuthenticationParams)[1]')
FROM #temp t;
UPDATE t
SET XmlData.modify('delete /AuthenticationParams/AlsoParams/AuthBehavior')
FROM #temp t;
SELECT * FROM #temp;

Retrieving an XML node value with TSQL?

What am I not getting here? I can't get any return except NULL...
DECLARE #xml xml
SELECT #xml = '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Body>
<webregdataResponse>
<result>0</result>
<regData />
<errorFlag>99</errorFlag>
<errorResult>Not Processed</errorResult>
</webregdataResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>'
DECLARE #nodeVal int
SELECT #nodeVal = #xml.value('(errorFlag)[1]', 'int')
SELECT #nodeVal
Here is the solution:
DECLARE #xml xml
SELECT #xml = '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Body>
<webregdataResponse>
<result>0</result>
<regData />
<errorFlag>99</errorFlag>
<errorResult>Not Processed</errorResult>
</webregdataResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>'
declare #table table (data xml);
insert into #table values (#xml);
WITH xmlnamespaces (
'http://schemas.xmlsoap.org/soap/envelope/' as [soap])
SELECT Data.value('(/soap:Envelope/soap:Body/webregdataResponse/errorFlag)[1]','int') AS ErrorFlag
FROM #Table ;
Running the above SQL will return 99.
Snapshot of the result is given below,
That's because errorFlag is not the root element of your XML document. You can either specify full path from root element to errorFlag, for example* :
SELECT #nodeVal = #xml.value('(/*/*/*/errorFlag)[1]', 'int')
or you can use descendant-or-self axis (//) to get element by name regardless of it's location in the XML document, for example :
SELECT #nodeVal = #xml.value('(//errorFlag)[1]', 'int')
*: I'm using * instead of actual element name just to simplify the expression. You can also use actual element names along with the namespaces, like demonstrated in the other answer.

Resources