Reputation: 3970
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.
Upvotes: 1
Views: 2023
Reputation: 22187
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>');
Upvotes: 1
Reputation: 22187
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;
Upvotes: 2
Reputation: 13006
to replace all Bar node
s 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")')
Upvotes: 1