Logic to modify multiple nodes of xml variable in sql server [duplicate] - sql-server

I want to replace the value in an element tag, depending on its value, as well as the value of another element(in the same level as said element), both elements being inside the same parent element tag everywhere(Every parent tag is unique due to its own ID attribute). I want to do the change at various locations of this XML variable, in a stored procedure.
Being a first timer at this, I am stuck with how i could modify the elements throughout the xml. Both elements are present in the same parent element all over the document, and every one of these parent tags has a unique ID attribute.
Any suggestions would be of great help.
Links to documents on how to mix and match "value()", "modify()", etc will also help.
DECLARE #xml xml = '
<SemanticModel xmlns="schemas.microsoft.com/sqlserver/2004/10/semanticmodeling"; xmlns:xsi="w3.org/2001/XMLSchema-instance"; xmlns:xsd="w3.org/2001/XMLSchema"; ID="G1">
<Entities>
<Entity ID="E1">
<Fields>
<Attribute ID="A1">
<Name>AAAA</Name>
<DataType>Float</DataType>
<Format>n0</Format>
<Column Name="AAAA_ID" />
</Attribute>
<Attribute ID="A2">
<Name>BBBB</Name>
<DataType>Integer</DataType>
<Format>n0</Format>
<Column Name="BBBB_ID" />
</Attribute>
<Attribute ID="A3">
<Name>KKKK</Name>
<Variations>
<Attribute ID="A4">
<Name>CCCC</Name>
<DataType>Float</DataType>
<Format>n0</Format>
</Attribute>
<Attribute ID="A5">
<Name>AAAA</Name>
<DataType>Float</DataType>
<Format>n0</Format>
</Attribute>
</Variations>
<Name>AAAA</Name>
<DataType>Float</DataType>
<Format>n0</Format>
</Attribute>
</Fields>
</Entity>
</Entities>'
DECLARE #i int = 0
;WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/10/semanticmodeling' as dm, 'http://schemas.microsoft.com/analysisservices/2003/engine' as dsv, 'http://w3.org/2001/XMLSchema' as xs )
select #i = #xml.value('count(//dm:Attribute[dm:DataType="Float" and dm:Format="n0"]/dm:Format)', 'int')
select #i
while #i > 0
begin
set #xml.modify('
replace value of
(//dm:Attribute[dm:DataType="Float" and dm:Format="n0"]/dm:Format/text())[1]
with "f2"
')
set #i = #i - 1
end
select #xml
I want to replace Format value "n0" to "f2" for all attributes whose Format value is"n0" and DataType is "Float".
-Thankyou

it's not possible to replace multiple values at once in xml in SQL Server, there're a several options:
use loop and update attributes one by one
split data into temporary table/table variable, update and then merge into one xml
use xquery to rebuild xml
I think correct way for you would be loop solution:
select #i = #data.value('count(//Attribute[DataType="Float" and Format="n0"]/Format)', 'int')
while #i > 0
begin
set #data.modify('
replace value of
(//Attribute[DataType="Float" and Format="n0"]/Format/text())[1]
with "f2"
')
set #i = #i - 1
end
sql fiddle demo
If your xml contains namepaces, simplest way to update I found would be to declare default namespace:
;with xmlnamespaces(default 'schemas.microsoft.com/sqlserver/2004/10/semanticmodeling')
select #i = #xml.value('count(//Attribute[DataType="Float" and Format="n0"]/Format)', 'int')
while #i > 0
begin
set #xml.modify('
declare default element namespace "schemas.microsoft.com/sqlserver/2004/10/semanticmodeling";
replace value of
(//Attribute[DataType="Float" and Format="n0"]/Format/text())[1]
with "f2"
')
set #i = #i - 1
end
select #xml

Related

Find all xml elements by attribute and change their value using t-sql

How can I update multiple XML elements within a single document?
For example, if I have the XML below and I want to change any elements with an attribute Store_ID="13" to instead have Store_ID="99".
declare #x xml
select #x = N'
<Games>
<Game>
<Place City="LAS" State="NV" />
<Place City="ATL" State="GA" />
<Store Store_ID="12" Price_ID="162" Description="Doom" />
<Store Store_ID="12" Price_ID="575" Description="Pac-man" />
<Store Store_ID="13" Price_ID="167" Description="Demons v3" />
<Store Store_ID="13" Price_ID="123" Description="Whatever" />
</Game>
</Games>
'
select #x
I can find all the elements with SQL like this:
select t.c.query('.')
from #x.nodes('.//*[#Store_ID="13"]') as t(c)
To update only the first element I could do an update like this (or change '1' to '2' to update the 2nd element, etc):
SET #x.modify('
replace value of (.//*[#Store_ID="13"]/#Store_ID)[1]
with "99"
');
SELECT #x;
The docs for replace value of say I can only update one node at a time:
It must identify only a single node ... When multiple nodes are selected, an error is raised.
So how do I update multiple elements? I can imagine querying first to find how many elements there are, then looping through and calling #x.modify() once for each element, passing an index parameter... but a) that feels wrong and b) when I try it I get an error
-- Find how many elements there are with the attribute to update
declare #numberOfElements int
select #numberOfElements = count(*)
from (
select element = t.c.query('.')
from #x.nodes('.//*[#Store_ID="13"]') as t(c)
) x
declare #i int = 1
declare #query nvarchar(max)
-- loop through and update each one
while #i <= #numberOfElements begin
SET #x.modify('
replace value of (.//*[#Store_ID="13"]/#Store_ID)[sql:variable("#i")]
with "99"
');
set #i = #i + 1 ;
end
SELECT #x;
Running the sql above gives me the error:
Msg 2337, Level 16, State 1, Line 31
XQuery [modify()]: The target of 'replace' must be at most one node, found 'attribute(Store_ID,xdt:untypedAtomic) *'
Furthermore, if I'm wanting to run this against many rows of a table with XML data stored in a column, it becomes very procedural.
Otherwise I could cast to nvarchar(max) and do string manipulation on it and then cast back to xml. Again, this feels icky, but also means I don't get the power of xml expressions to find the elements to update.
If the XML structure is defined and well-known ahead of time then you could avoid the XML.modify() limitations by just recreating the XML with the edits applied inline.
Example 1: reconstruct the XML with .nodes(), .values() and FOR XML
update dbo.Example
set x = (
select
(
select Place.value(N'#City', N'nvarchar(max)') as [#City],
Place.value(N'#State', N'nvarchar(max)') as [#State]
from Game.nodes('Place') as n(Place)
for xml path(N'Place'), type
),
(
select
case when (StoreID = 13) then 99 else StoreID end as [#Store_ID],
PriceID as [#Price_ID],
[Description] as [#Description]
from Game.nodes('Store') as n(Store)
cross apply (
select Store.value(N'#Store_ID', N'int'),
Store.value(N'#Price_ID', N'int'),
Store.value(N'#Description', N'nvarchar(max)')
) attributes(StoreID, PriceID, [Description])
for xml path('Store'), type
)
from x.nodes(N'/Games/Game') Games(Game)
for xml path(N'Game'), root(N'Games')
);
Example 2: reconstruct the XML with XQuery
update dbo.Example
set x = (
select x.query('
<Games>
<Game>
{
for $place in /Games/Game/Place
return $place
}
{
for $store in /Games/Game/Store
let $PriceID := $store/#Price_ID
let $StoreID_Input := xs:integer($store/#Store_ID)
let $StoreID := if ($StoreID_Input != 13) then $StoreID_Input else 99
let $Description := $store/#Description
return <Store Store_ID="{$StoreID}" Price_ID="{$PriceID}" Description="{$Description}"/>
}
</Game>
</Games>')
);
Here is another relatively generic method by using XQuery.
SQL
DECLARE #x XML =
N'<Games>
<Game>
<Place City="LAS" State="NV"/>
<Place City="ATL" State="GA"/>
<Store Store_ID="12" Price_ID="162" Description="Doom"/>
<Store Store_ID="12" Price_ID="575" Description="Pac-man"/>
<Store Store_ID="13" Price_ID="167" Description="Demons v3"/>
<Store Store_ID="13" Price_ID="123" Description="Whatever"/>
</Game>
</Games>';
DECLARE #oldId int = 13
, #newId int = 99;
SET #x = #x.query('<Games><Game>
{
for $i in /Games/Game/*
return if ($i[(local-name()="Store") and #Store_ID=sql:variable("#oldId")]) then
element Store { attribute Store_ID {sql:variable("#newId")}, $i/#*[not(local-name()="Store_ID")]}
else $i
}
</Game></Games>');
-- test
SELECT #x;
Output
<Games>
<Game>
<Place City="LAS" State="NV" />
<Place City="ATL" State="GA" />
<Store Store_ID="12" Price_ID="162" Description="Doom" />
<Store Store_ID="12" Price_ID="575" Description="Pac-man" />
<Store Store_ID="99" Price_ID="167" Description="Demons v3" />
<Store Store_ID="99" Price_ID="123" Description="Whatever" />
</Game>
</Games>
As per this answer it's not possible to update multiple times in a single statement, so to use xml you need to loop and call modify() repeatedly until all values are updated.
My initial attempt to loop was misfounded: simply do the modify() on the first matching element until there's no more:
while #x.exist('.//*[#Store_ID="13"]')=1
begin
SET #x.modify('
replace value of (.//*[#Store_ID="13"]/#Store_ID)[1]
with "99"
');
end
In my trivial examples I've used hardcoded values "13" and "99" but for real code you can use sql:variable("#variableName") (ref) and sql:column("tableNameOrAlias.colName") (ref), e.g.
declare #oldId int = 13
, #newId int = 99
while #x.exist('.//*[#Store_ID=sql:variable("#oldId")]')=1
begin
SET #x.modify('
replace value of (.//*[#Store_ID=sql:variable("#oldId")]/#Store_ID)[1]
with sql:variable("#newId")
');
end

How to modify an xml variable with a conditional/where clause

I'm trying to figure out the syntax for modifying the value of an xml variable conditionally. If this were a table, it would be easy because I would just use a WHERE clause to specify which of multiple nodes I want to update. But when all I have is a variable, I use the SET command to do the modify, and that doesn't allow a WHERE clause.
Example:
DECLARE #xml xml = '
<Container>
<Collection>
<foo>One</foo>
<bar>true</bar>
<baz>false</baz>
</Collection>
<Collection>
<foo>Two</foo>
<bar>true</bar>
<baz>true</baz>
</Collection>
<Collection>
<foo>Three</foo>
<bar>true</bar>
<baz>true</baz>
</Collection>
</Container>
'
SELECT node.value('(foo/text())[1]', 'varchar(10)') AS Item,
node.value('(bar/text())[1]', 'varchar(10)') AS IsBar,
node.value('(baz/text())[1]', 'varchar(10)') AS IsBaz
FROM #xml.nodes('/*/Collection') t(node)
So I have two questions I can't seem to figure out the syntax for:
1) I want to modify JUST the 'two' mode so that 'IsBar' is false, while not touching the value of 'IsBar' for the other nodes.
2) I want to, in one statement, update all "IsBar" values to "false".
I can't find the right magic incantation for (1), and for (2) if I try the obvious, I get an error that replace can only update at most one node.
For (1) I've tried this, and it doesn't modify anything (though it doesn't give me any error), so I'm clearly missing something obvious in my pathing:
SET #xml.modify('replace value of ((/*/Collection)[(foo/text())[1] = "Two"]/bar/text())[0] with "false"')
For (2), I want something like this, but it just gives an error:
SET #xml.modify('replace value of (/*/Collection/bar/text()) with "false"')
XQuery [modify()]: The target of 'replace' must be at most one node, found 'text *'
I googled around and simply couldn't find anyone trying to update an xml variable conditionally (or all nodes at once). And frankly, I'm clearly doing something wrong because none of my attempts have ever modified the #xml variable values, so I just need another set of eyes to tell me what I'm getting wrong.
Unfortunately, the replace value of statement only updates one node at a time. And for a single update the position [0] is wrong, it should be [1].
Check it out a solution below. SQL Server XQuery native out-of-the-box FLWOR expression is a way to do it.
SQL
-- DDL and sample data population, start
DECLARE #xml xml = N'<Container>
<Collection>
<foo>One</foo>
<bar>true</bar>
<baz>false</baz>
</Collection>
<Collection>
<foo>Two</foo>
<bar>true</bar>
<baz>true</baz>
</Collection>
<Collection>
<foo>Three</foo>
<bar>true</bar>
<baz>true</baz>
</Collection>
</Container>';
-- DDL and sample data population, end
-- before
SELECT #xml;
SET #xml.modify('replace value of ((/Container/Collection)[(foo/text())[1] = "Two"]/bar/text())[1] with "false"')
-- after
SELECT #xml;
DECLARE #bar VARCHAR(10) = 'false';
SET #xml = #xml.query('<Container>
{
for $y in /Container/Collection
return <Collection>
{
for $z in $y/*
return
if (not(local-name($z) = ("bar"))) then $z
else
(
element bar {sql:variable("#bar")}
)
}
</Collection>
}
</Container>');
-- after bulk update
SELECT #xml;
to replace all Bar nodes with text() = false, you can try this loop.
declare #ctr int
select #ctr = max(#xml.value('count(//Collection/bar)', 'int'))
while #ctr > 0
begin
set #xml.modify('replace value of ((//Collection/bar)[sql:variable("#ctr")]/text())[1] with ("false")')
set #ctr = #ctr - 1
end
to replace the first 2 nodes.
set #xml.modify('replace value of ((//Collection/bar)[1]/text())[1] with ("false")')
set #xml.modify('replace value of ((//Collection/bar)[2]/text())[1] with ("false")')
Here is the answer for the "..REAL LIFE case...". I modified the input XML by adding some additional elements. The XQuery was adjusted accordingly.
SQL
-- DDL and sample data population, start
DECLARE #xml xml = N'<Container>
<city>Miami</city>
<state>FL</state>
<Collection>
<foo>One</foo>
<bar>true</bar>
<baz>false</baz>
</Collection>
<Collection>
<foo>Two</foo>
<bar>true</bar>
<baz>true</baz>
</Collection>
<Collection>
<foo>Three</foo>
<bar>true</bar>
<baz>true</baz>
</Collection>
</Container>';
-- DDL and sample data population, end
-- before
SELECT #xml AS [before];
-- update single element
SET #xml.modify('replace value of (/Container/Collection[upper-case((foo/text())[1]) = "TWO"]/bar/text())[1] with "false"')
-- after
SELECT #xml AS [After];
-- Method #1, via FLWOR expression
-- update all <bar> elements with the false' value
DECLARE #bar VARCHAR(10) = 'false';
SET #xml = #xml.query('<Container>
{
for $x in /Container/*[not(local-name(.)=("Collection"))]
return $x
}
{
for $y in /Container/Collection
return <Collection>
{
for $z in $y/*
return
if (not(local-name($z) = ("bar"))) then $z
else
(
element bar {sql:variable("#bar")}
)
}
</Collection>
}
</Container>');

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.

Cursor for spliting t-sql #xml variable on elements level

I need to define some cursor for spliting t-sql #xml variable on elements level into different #xml(s).
for example:
<root>
<element id=10/>
<element id=11/>
<element id=12/>
<element id=13/>
</root>
so that get the following values inside of tsql cursor:
<root><element id=10/><element id=11/></root>
then
<root><element id=12/><element id=13/></root>
and so on where n number of elements pro cursor loop.
Well, you can use the build-in functions for manipulating XML. For example, the following statement:
DECLARE #XML XML = N'<root><element id="10"/><element id="11"/><element id="12"/><element id="13"/></root>'
SELECT ROW_NUMBER() OVER (ORDER BY T.c)
,T.c.query('.')
FROM #XML.nodes('root/element') T(c)
will give you all elements preserving the order they have in the XML structure:
Then you can stored this result and build separate smaller XML variables.
For different elements you can use * like this:
DECLARE #XML XML = N'<root><element1 id="10"/><element2 id="11"/><element3 id="12"/><element4 id="13"/></root>'
SELECT ROW_NUMBER() OVER (ORDER BY T.c)
,T.c.query('.')
FROM #XML.nodes('root/*') T(c)

join multiple 'for XML' outputs in an efficient manner

In the code below, I am join the xml output of two independent select clauses into one XML.
create procedure Test
as
declare #result nvarchar(max)
set #result = '<data>'
set #result = #result + (select FieldA from Table1 for xml raw('a'), root('table1'))
set #result = #result + (select FieldB from Table2 for xml raw('b'), root('table2'))
set #result = #result + '/<data>'
return #result
Output would look like:
<data>
<table1>
<a FieldA="01"/>
<a FieldA="02"/>
</table1>
<table2>
<b FieldB="aa"/>
<b FieldB="bb"/>
</table2>
</data>
I would like to know if there is a much better way to achieve the above output with greater Performance.
I believe 'for xml' output (xml data type) is streamed while the nvarchar is not. So, is it beneficial to output as XML data type? or, does Xml datatype overheads (parsing, wellformedness checks, etc) overweight the streaming benefit over nvarchar datatype?
Edit The procedure will be called by an ASP.Net application and results are read through the ADO.Net classes
It is possible to combine xml from subqueries into a bigger xml result. The key is to use the sqlserver xml type.
with foodata as (
select 1 as n
union all
select n+1 from foodata where n < 20
)
select
(select n as 'text()' from foodata where n between 10 and 13 for xml path('row'), root('query1'), type),
(select n as 'text()' from foodata where n between 15 and 18 for xml path('row'), root('query2'), type)
for xml path('root'), type
The query above will generate the following output:
<root>
<query1>
<row>10</row>
<row>11</row>
<row>12</row>
<row>13</row>
</query1>
<query2>
<row>15</row>
<row>16</row>
<row>17</row>
<row>18</row>
</query2>
</root>
I can't say which is faster though. You will just have to do some benchmarks.

Resources