How can we update some nodes in an xml column? - sql-server

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

Related

XML output in SQL help needed

Hi I have the following code in SQL that outputs xml between 2 tables. The output is 90% correct but if possible add to the output and then also remove some of the output text.
I am not sure if SQL has the ability to code a type of a element in the output. Please see the code below with the output. Currently if possible I would like to make 2 changes to my current output. The changes are list at the end of the port
DECLARE #ID_Rechnung int = 1978,
#XMLData xml;
WITH XMLNAMESPACES ('?xml version="1.0" encoding="UTF-8"?' as ext)
SELECT
#XMLData = xmldat.xmldataCol
FROM
(
SELECT (
SELECT
-- HIER XML Daten generieren
[InvoiceHeader].[InvoiceDate] AS 'invoice-date',
([InvoiceHeader].[InvoiceNumber]) AS 'invoice-number',
cast(replace([InvoiceHeader].[GrossValue],' ','') as decimal(18,2)) AS 'gross-total',
cast(replace([InvoiceHeader].[NetValue],' ','') as decimal(18,2)) AS 'amount-due',
[InvoiceHeader].[VatRate] AS 'tax-rate',
cast(replace([InvoiceHeader].[VatValue],' ','') as decimal(18,2)) AS 'tax-amount',
[ImagePath] AS 'image-scan-url',
[InvoiceType] AS 'document-type',
[LegalEntityVATNo] AS 'account-type/id',
[LegalEntityName] AS 'account-type/name',
[SupplierCode] as 'supplier/number',
[Currency] as 'currency/code',
(
SELECT rtrim([InvoiceLines].[LineNumber]) AS [order-line-num]
, [PONumber] as [po-number],
CAST([InvoiceLines].[UnitPrice] AS decimal(18,2)) AS Price ,
[Quantity] as quantity,
[TaxAmount] as [tax-amount],
[LineTotal] as [total],
[Decsription] as description
FROM [InvoiceLines] WHERE [InvoiceLines].[DOCID] = #id_Rechnung
FOR XML PATH('Invoice-line'), ROOT('invoice-lines'), TYPE
)
FROM [InvoiceHeader]
WHERE [InvoiceHeader].[DOCID] = #ID_Rechnung
FOR XML PATH(''), TYPE, ROOT('invoice-header')
) AS xmldataCol
) AS xmldat;
SELECT #XMLData
.query('<invoice-header>
{
for $x in /invoice-header/*[local-name()!="root"]
return $x,
for $x in /invoice-header/root/r
return <invoice-lines>/<invoice-line>{$x/*}</invoice-line></invoice-lines>
}
</invoice-header>');
Output:
<invoice-header>
<invoice-date>20180509</invoice-date>
<invoice-number>1075440</invoice-number>
<gross-total>1376.67</gross-total>
<amount-due>1197.10</amount-due>
<tax-rate>15.00%</tax-rate>
<tax-amount>179.57</tax-amount>
<image-scan-url>\\INTEL-SQL01\Attachment\2018-06-20\7e0dd165-81d6-445a-95d1-8aac686d44ed\f9a1179c-2a54-480e-b97a-ce6ac7327ae0.000</image-scan-url>
<account-type>
<id>4010112052</id>
<name>CONSOLIDATEDPOWERPROJECTS</name>
</account-type>
<supplier>
<number>12345</number>
</supplier>
<currency>
<code>ZAR</code>
</currency>
<invoice-lines xmlns:ext="?xml version="1.0" encoding="UTF-8"?">
<Invoice-line>
<order-line-num>4</order-line-num>
<po-number>120934861</po-number>
<Price>50.00</Price>
<quantity>1.000000</quantity>
<tax-amount>7.500000</tax-amount>
<total>50.00</total>
<description>Test1</description>
</Invoice-line>
<Invoice-line>
<order-line-num>2</order-line-num>
<po-number>120934861</po-number>
<Price>10.00</Price>
<quantity>2.000000</quantity>
<tax-amount>4.500000</tax-amount>
<total>20.00</total>
<description>Test2</description>
</Invoice-line>
</invoice-lines>
</invoice-header>
1.How do I get rid of the following xmlns:ext="?xml version="1.0" encoding="UTF-8&in the line: "<invoice-lines xmlns:ext="?xml version="1.0" encoding="UTF-8"?">"
How would I code "<tax-amount>7.500000</tax-amount> to get the output": "<tax-amount type="decimal">7.500000</tax-amount>"
Without a minimal reproducible example it is not possible to give you a full working answer.
(1) As #JeroenMostert already pointed out, the
'?xml version="1.0" encoding="UTF-8"?'
is an XML prolog declaration. Just delete the following line:
WITH XMLNAMESPACES ('?xml version="1.0" encoding="UTF-8"?' as ext)
(2) Here is a conceptual example how to add an attribute to an XML element. What is important here is a sequential order of adding, i.e. attribute shall be first, element itself is 2nd.
SQL
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, [tax-amount] VARCHAR(20));
INSERT INTO #tbl ([tax-amount]) VALUES
(N'7.500000'),
(N'18.000000');
SELECT
'decimal' AS [tax-amount/#type]
, [tax-amount]
FROM #tbl
FOR XML PATH('r'), TYPE, ROOT('root');
Output
<root>
<r>
<tax-amount type="decimal">7.500000</tax-amount>
</r>
<r>
<tax-amount type="decimal">18.000000</tax-amount>
</r>
</root>

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.

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

Variable usage in XML query

Please check below query.
declare #xmlRoot as xml
set #xmlRoot= '<Root>
<table1 col1="2012-03-02T16:42:55.777">
<table2Array>
<Table2 col2="abc">
</Table2>
<Table2 col2="def">
</Table2>
</table2Array>
</table1>
<table1 col1="2012-03-02T17:42:55.777">
<table2Array>
<Table2 col2="abc1">
</Table2>
<Table2 col2="def1">
</Table2>
</table2Array>
</table1>
</Root>'
declare #a as varchar(1)
set #a= '1'
SELECT
col1 = item.value('./#col2', 'varchar(10)')
FROM #xmlRoot.nodes('Root/table1[1]/table2Array/Table2' ) AS T(item);
--The above query return expected output
SELECT
col1 = item.value('./#col2', 'varchar(10)')
FROM #xmlRoot.nodes('Root/table1[*[local-name()=sql:variable("#a")]]/table2Array/Table2' )
AS T(item);
--The above query doesn't return expected output
what am I doing wrong here?
Since I dont have a key value in parent node to identify child node. I have to parse through index.
This worked for me:
DECLARE #a INT; -- data type is probably important!
SET #a = 1;
SELECT col1 = item.value('./#col2', 'varchar(10)')
FROM #xmlRoot.nodes('Root/table1[sql:variable("#a")]/table2Array/Table2') AS T(item);

Resources