Convert String To XML - sql-server

How to convert the below string to XML,
SET #string = '<Field>
<Field Name="'+#Cname+'">
<DataField>'+#Cname+'</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>'
I tried SET #xmlstring = CONVERT(XML,#string) but it displays below error
Msg 9459, Level 16, State 1, Line 17
XML parsing: line 4, character 13, undeclared prefix

Oh no! Never create XML via string concatenation! Just imagine, your variable comes with a value like this -> value or Tim, Tom & Fred... This might work perfectly, pass all tests and break after rollout with undefined errors.
Always use SQL-Servers support to create XML:
DECLARE #cname VARCHAR(100)='Some Name';
DECLARE #xml XML=
(
SELECT #cname AS [Field/#Name]
,#cname AS [DataField]
,'System.String' AS [TypeName]
FOR XML PATH('Field')
);
SELECT #xml;
The result
<Field>
<Field Name="Some Name" />
<DataField>Some Name</DataField>
<TypeName>System.String</TypeName>
</Field>
And here with the namespace:
WITH XMLNAMESPACES('Some.namespace.url' AS rd)
SELECT #xml=
(
SELECT #cname AS [Field/#Name]
,#cname AS [DataField]
,'System.String' AS [rd:TypeName]
FOR XML PATH('Field')
);
SELECT #xml
The result
<Field xmlns:rd="Some.namespace.url">
<Field Name="Some Name" />
<DataField>Some Name</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>

Your xml need formatted correctly.
try to close tag Field with prop Name using '/' and take out prefix rd in TypeName.
SET #string = '<Field>
<Field Name="'+#Cname+'" />
<DataField>'+#Cname+'</DataField>
<TypeName>System.String</TypeName>
</Field>'
http://sqlfiddle.com/#!18/9eecb/8282/0

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

I need to set up xml tags in some order

I have xml like this:
<tt>
<cpost/>
<cpost/>
<gx2tov />
<kodp/>
<Sertif1/>
<Sertif1/>
<slmat/>
</tt>
But I need to change it to:
<tt>
<slmat/>
<cpost/>
<cpost/>
<kodp/>
<gx2tov />
<Sertif1/>
<Sertif1/>
</tt>
How can I do that using T SQL?
There is an accepted answer already but it will work with empty nodes only. In this case you could just type in the XML as you need it...
Some more approaches:
I added some id-attributes to distinguish the identical elements
DECLARE #xml XML=
'<tt>
<cpost id="1"/>
<cpost id="2"/>
<gx2tov/>
<kodp/>
<Sertif1 id="1"/>
<Sertif1 id="2"/>
<slmat/>
</tt>';
Place <slmat> at the top
You can use .query() to get a full node, which you can use AS [node()] in a SELECT ... FOR XML PATH(). This query will place <slmat> at the top, and will place all with a different name behind (original order):
SELECT #xml.query(N'/tt/slmat') AS [node()]
,#xml.query(N'/tt/*[local-name()!="slmat"]') AS [node()]
FOR XML PATH(N'tt');
That's the same as above, but as one single XQuery
SELECT #xml.query
('
<tt>
{tt/slmat}
{tt/*[local-name()!="slmat"]}
</tt>
');
Set a specific order for all nodes explicitly
If you want to set a specific order for your elements, you might place them explicitly (now I use .nodes(), but you could go without too - as above):
SELECT tt.query(N'slmat') AS [node()]
,tt.query(N'cpost') AS [node()]
,tt.query(N'kodp') AS [node()]
,tt.query(N'gx2tov') AS [node()]
,tt.query(N'Sertif1') AS [node()]
FROM #xml.nodes(N'/tt') AS A(tt)
FOR XML PATH(N'tt');
As XQuery (now with a variable, but you could use the root directly - as above)
SELECT #xml.query
('
let $t:=/tt
return
<tt>
{$t/slmat}
{$t/cpost}
{$t/kodp}
{$t/gx2tov}
{$t/Sertif1}
</tt>
');
All have the same result...
You can delete and insert nodes:
DECLARE #xml XML = N'<tt>
<cpost/>
<cpost/>
<gx2tov />
<kodp/>
<Sertif1/>
<Sertif1/>
<slmat/>
</tt>'
SET #xml.modify('delete tt/slmat[1]')
SET #xml.modify('insert <slmat /> as first into tt[1]')
SELECT #xml
Or, you can reorder all your nodes:
DECLARE #xml XML = N'<tt>
<cpost/>
<cpost/>
<gx2tov />
<kodp/>
<Sertif1/>
<Sertif1/>
<slmat/>
</tt>'
SELECT
CONVERT(xml, '<' +t.value('local-name(.)','nvarchar(max)') + ' />')
FROM #xml.nodes('*/*') AS t(t)
ORDER BY t.value('local-name(.)','nvarchar(max)')
FOR XML PATH(''), TYPE, ROOT('tt')

get xml data from differents nodes and tags

Im trying to insert XML data in a temporary table
DEPARTMENT name="Administration" Revision="" IsRevision="False" Configuration="">
<FIELD name="Name" value="Jean" type="char" />
<FIELD name="LastName" value="Dupont" type="char" />
<FIELD name="EmployeeID" value="5" type="float" />
<ATTACHED_DOCUMENTS>
<DOCUMENT FileName="contract.pdf" Directory="D:/Contracts"/>
</ATTACHED_DOCUMENTS>
</DEPARTMENT>
insert #t_employee
(t_department,
t_name,
t_value,
t_type)
select *
from openxml (#doc, 'DEPARTMENT/FIELD')
with (t_department varchar(255) '../#name',
t_name varchar(255) '#name',
t_value varchar(255) '#value',
t_type varchar(max) '#type');
This code works.
But now what I want is to add a column in my table and get the information of the document in the "Attached_documents" tag.
I tried to change the path like this to add it but it didnt work
select *
from openxml (#doc, 'DEPARTMENT)
with (t_department varchar(255) '#name',
t_name varchar(255) 'FIELD/#name',
t_value varchar(255) 'FIELD/#value',
t_type varchar(max) 'FIELD/#type',
t_document varchar(max) 'ATTACHED_DOCUMENTS/#FileName');
How can I insert the document info in my table?
Thanks for help
Since I don't have access to your XML doc I need to use a variable; hopefully this makes enough sense for you to make the required changes.
-- your XML doc
DECLARE #doc XML =
'<DEPARTMENT name="Administration" Revision="" IsRevision="False" Configuration="">
<FIELD name="Name" value="Jean" type="char" />
<FIELD name="LastName" value="Dupont" type="char" />
<FIELD name="EmployeeID" value="5" type="float" />
<ATTACHED_DOCUMENTS>
<DOCUMENT FileName="contract.pdf" Directory="D:/Contracts"/>
</ATTACHED_DOCUMENTS>
</DEPARTMENT>'
-- Solution
select
DepartmentName = FIELD.value('(../#name)[1]', 'varchar(100)'),
FieldName = FIELD.value('(#name)[1]', 'varchar(100)'),
FieldTxt = FIELD.value('(#value)[1]', 'varchar(100)'),
FieldType = FIELD.value('(#type)[1]', 'varchar(100)'),
[FileName] = info.value('(DOCUMENT/#FileName)[1]', 'varchar(100)'),
FileDirectory = info.value('(DOCUMENT/#Directory)[1]', 'varchar(100)')
FROM (VALUES (#doc)) t(x)
CROSS APPLY x.nodes('/DEPARTMENT/FIELD') FIELDS(FIELD)
CROSS APPLY (VALUES (#doc.query('(//DOCUMENT)'))) doc(info);
Results
DepartmentName FieldName FieldTxt FieldType FileName FileDirectory
-------------- -------------- -------------- -------------- -------------- --------------
Administration Name Jean char contract.pdf D:/Contracts
Administration LastName Dupont char contract.pdf D:/Contracts
Administration EmployeeID 5 float contract.pdf D:/Contracts

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