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

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>');

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 can we update some nodes in an xml column?

I have the below in an xml column of a table. How can I write a query that replaces only part of the value for all ? The text REPLACE will be replaced with another value.
<Root>
<Response xmlns:ns1="urn:names:tc:legalxml-message1:schema:xsd:Message-4.0">
<Message xmlns:ns2="urn:names:tc:legalxml-message2:schema:xsd:Types-4.0">
<Response1>
<ns1:Name>REPLACE name1</Name>
</Response1>
<Response2>
<ns1:Name>REPLACE name2</Name>
</Response2>
<Response3>
<ns1:Name>REPLACE name3</Name>
</Response3>
<Response4>
<ns1:Name>REPLACE name4</Name>
</Response4>
</Message>
</Response>
</Root>
I tried the below query and received an error message.
XQuery [r.x.modify()]: The target of 'replace value of' must be a non-metadata attribute or an element with simple typed content, found 'element(Name,xdt:untyped) ?'
I was following this link.
declare #SearchString varchar(100),#ReplaceString varchar(100)
SELECT #SearchString = 'REPLACE',#ReplaceString = 'NEWVALUE'
UPDATE r
SET x.modify('replace value of (/Root/Response/Message/Response1/Name)[1] with sql:column("y")')
FROM (SELECT xmlColumn,REPLACE(t.u.value('Name[1]','varchar(100)'),#SearchString,#ReplaceString) as y
FROM tblMessage
CROSS APPLY tblMessage.nodes('/Root/Response/Message/Response1/Name')t(u)
)r
Given the following sample data:
DECLARE #xml XML =
'<Root>
<Response xmlns:ns1="urn:names:tc:legalxml-message1:schema:xsd:Message-4.0">
<Message xmlns:ns2="urn:names:tc:legalxml-message2:schema:xsd:Types-4.0">
<Response1>
<ns1:Name>REPLACE name1</ns1:Name>
</Response1>
<Response2>
<ns1:Name>REPLACE name2</ns1:Name>
</Response2>
<Response3>
<ns1:Name>REPLACE name3</ns1:Name>
</Response3>
<Response4>
<ns1:Name>REPLACE name4</ns1:Name>
</Response4>
</Message>
</Response>
</Root>';
DECLARE #replaceText VARCHAR(100) = 'Something New Here';
You can use the XML modify method to update the values. To do so explicitly by node name you could do this:
SET #xml.modify('replace value of (/Root/Response/Message/Response4/*:Name/text())[1]
with sql:variable("#replaceText")');
In this ^^ example I'm updating the Response4 node. You can also use the node position to update the XML. The example below will update Response1:
SET #xml.modify('replace value of (/Root/Response/Message//*:Name/text())[1]
with sql:variable("#replaceText")');
To update Response2 you would do change the [1] to [2] like so:
SET #xml.modify('replace value of (/Root/Response/Message//*:Name/text())[2]
with sql:variable("#replaceText")');

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

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

Casting a field to XML, querying it returns NULL records

I've a field on my table that is nvarchar(max) and contains XML document. Don't ask why it is nvarchar(max) instead of XML because I don't know it.
By the way, here is an extraction of a sample XML:
<?xml version="1.0" encoding="utf-16"?>
<ItemType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<AutoPay xmlns="urn:ebay:apis:eBLBaseComponents">true</AutoPay>
<Country xmlns="urn:ebay:apis:eBLBaseComponents">IT</Country>
<Currency xmlns="urn:ebay:apis:eBLBaseComponents">EUR</Currency>
<HitCounter xmlns="urn:ebay:apis:eBLBaseComponents">BasicStyle</HitCounter>
<ListingDuration xmlns="urn:ebay:apis:eBLBaseComponents">GTC</ListingDuration>
<ListingType xmlns="urn:ebay:apis:eBLBaseComponents">FixedPriceItem</ListingType>
<Location xmlns="urn:ebay:apis:eBLBaseComponents">Italy</Location>
<PaymentMethods xmlns="urn:ebay:apis:eBLBaseComponents">PayPal</PaymentMethods>
<PayPalEmailAddress xmlns="urn:ebay:apis:eBLBaseComponents">email#paypal.com</PayPalEmailAddress>
<PrimaryCategory xmlns="urn:ebay:apis:eBLBaseComponents">
<CategoryID>137084</CategoryID>
</PrimaryCategory>
<ShippingDetails xmlns="urn:ebay:apis:eBLBaseComponents">
<ShippingServiceOptions>
<ShippingService>StandardShippingFromOutsideUS</ShippingService>
<ShippingServiceCost currencyID="EUR">0</ShippingServiceCost>
<ShippingServiceAdditionalCost currencyID="EUR">0</ShippingServiceAdditionalCost>
<FreeShipping>true</FreeShipping>
</ShippingServiceOptions>
<InternationalShippingServiceOption>
<ShippingService>StandardInternational</ShippingService>
<ShippingServiceCost currencyID="EUR">0</ShippingServiceCost>
<ShippingServiceAdditionalCost currencyID="EUR">0</ShippingServiceAdditionalCost>
<ShippingServicePriority>1</ShippingServicePriority>
<ShipToLocation>Americas</ShipToLocation>
<ShipToLocation>Europe</ShipToLocation>
</InternationalShippingServiceOption>
<ShippingType>Flat</ShippingType>
<InsuranceDetails>
<InsuranceFee currencyID="EUR">0</InsuranceFee>
<InsuranceOption>NotOffered</InsuranceOption>
</InsuranceDetails>
<InternationalInsuranceDetails>
<InsuranceFee currencyID="EUR">0</InsuranceFee>
<InsuranceOption>NotOffered</InsuranceOption>
</InternationalInsuranceDetails>
</ShippingDetails>
<Site xmlns="urn:ebay:apis:eBLBaseComponents">US</Site>
<Storefront xmlns="urn:ebay:apis:eBLBaseComponents">
<StoreCategoryID>2947535016</StoreCategoryID>
<StoreCategory2ID>0</StoreCategory2ID>
</Storefront>
<DispatchTimeMax xmlns="urn:ebay:apis:eBLBaseComponents">4</DispatchTimeMax>
<ReturnPolicy xmlns="urn:ebay:apis:eBLBaseComponents">
<ReturnsAcceptedOption>ReturnsAccepted</ReturnsAcceptedOption>
<Description>Accepted</Description>
<ShippingCostPaidByOption>Buyer</ShippingCostPaidByOption>
</ReturnPolicy>
<ConditionID xmlns="urn:ebay:apis:eBLBaseComponents">1000</ConditionID>
</ItemType>
I would love to query the table on that field, for example to extract CategoryID field.
I tried everything I knew, like casting to ntext, removing utf-16, replacing it with utf-8, adding namespaces and stuff like that, but the result is always a NULL record.
Here is one of the queries I tried:
;WITH XMLNAMESPACES('urn:ebay:apis:eBLBaseComponents' AS ns,
'http://www.w3.org/2001/XMLSchema-instance' as xsi,
'http://www.w3.org/2001/XMLSchema' as xsd)
select CategoryVal = CONVERT(xml, [Template]).value('(/ItemType/PrimaryCategory/CategoryID)[1]', 'nvarchar(max)') FROM Templates where ID = 1
Thanks, Marco
with xmlnamespaces('urn:ebay:apis:eBLBaseComponents' as n)
select cast(Template as xml).value('(/ItemType/n:PrimaryCategory/n:CategoryID)[1]', 'nvarchar(max)')
from Templates
where ID = 1
You need to prefix the elements in your xpath expression.
I've done that once, but without the use of namespaces.
I had my input as varchar(max) (nvarchar should work too)
#Text AS varchar(MAX)
Then i used an XML type variable and the conversion was as simple as this:
DECLARE #XML XML
SELECT #XML = #Text
To query your CategoryID value you would use:
SELECT itemtype.item.value('(/ItemType/PrimaryCategory/CategoryID)[1]', 'nvarchar(max)')
FROM #XML.nodes('/ItemType') AS itemtype(item);

Read value in XML Node - T-SQL

This is my code.......
DECLARE #XML AS XML;
SET #XML = CAST('<Session id="ID969138672" realTimeID="4300815712">
<VarValues>
<varValue id="ID123" source="Internal" name="DisconnectedBy">VisitorClosedWindow</varValue>
<varValue id="ID1234" source="PreChat" name="email">1234#mail.ru</varValue>
</VarValues>
</Session>
' AS XML)
SELECT
xmlData.Col.value('#id','varchar(max)')
,xmlData.Col.value('#source','varchar(max)')
,xmlData.Col.value('#name','varchar(max)')
FROM #XML.nodes('//Session/VarValues/varValue') xmlData(Col);
This is the output.....
How can I include the actual values of the varValue?
I need to read the values VisistorClosedWindow and 1234#mail.ru values as well
You can get that by doing this:
xmlData.Col.value('.','varchar(max)')
So the select would be:
SELECT
xmlData.Col.value('#id','varchar(max)')
,xmlData.Col.value('#source','varchar(max)')
,xmlData.Col.value('#name','varchar(max)')
,xmlData.Col.value('.','varchar(max)')
FROM #XML.nodes('//Session/VarValues/varValue') xmlData(Col);
Just use the .value('.', 'varchar(50)) line for that:
SELECT
xmlData.Col.value('#id','varchar(25)'),
xmlData.Col.value('#source','varchar(50)'),
xmlData.Col.value('#name','varchar(50)'),
xmlData.Col.value('.','varchar(50)') -- <== this gets your the element's value
FROM #XML.nodes('//Session/VarValues/varValue') xmlData(Col);

Resources