select returns only one item from XML - arrays

I have an xml array. when using select it only returns the first value. Here is my code . Whats wrong with my code.
DECLARE #xml xml;
SET #xml =N'<root>
<Id>68890</Id>
<Id>68900</Id>
</root>';
SELECT
replicateIdXml.replicateIds.value('Id[1]','bigint') as id
FROM #xml.nodes('/root') AS replicateIdXml (replicateIds)
It only returns the first row .

You were very close. Your .nodes() returns all root element of the first level row-wise. But there is only one root-element... Than you pick the first Id-element, which is the one you see.
You have to let .nodes() return all Id-elements row-wise:
DECLARE #xml xml;
SET #xml =
N'<root>
<Id>68890</Id>
<Id>68900</Id>
</root>';
SELECT
replicateIdXml.replicateIds.value('.','bigint') as id
FROM #xml.nodes('/root/Id') AS replicateIdXml (replicateIds)

Related

T-SQL parse XML data into single line

I have XML saved into a column in a table as type nvarchar. Now I need to parse data from that xml. I do
SELECT
CONVERT(XML, columnX).value('(chatTranscript/message/msgText/text())[1]', 'nvarchar(max)')
as chat but I get only first value. How do I extract all into single line? XML can be long, depends on chat length.
I need to get userNick and then msgText and loop it till the end. Something like this:
userX:Hello<>userY:How are you;
XML:
<?xml version="1.0"?>
<chatTranscript startAt="2020-07-30T11:00:12Z" sessionId="......">
<newParty userId="......" timeShift="0" visibility="ALL" eventId="1">
<userInfo personId="" userNick="userX"/>
</newParty>
<message userId="..." timeShift="12" visibility="ALL" eventId="9">
<msgText msgType="text">Hello</msgText>
</message>
<newParty userId="..." timeShift="15" visibility="ALL" eventId="10">
<userInfo userNick="userY"/>
</newParty>
<message userId="..." timeShift="29" visibility="ALL" eventId="12">
<msgText treatAs="NORMAL">how are you?</msgText>
</message>
<partyLeft userId="..." timeShift="36" visibility="ALL" eventId="13" askerId="...">
<reason code="1">left with request to close if no agents</reason>
</partyLeft>
<partyLeft userId="..." timeShift="36" visibility="ALL" eventId="14" askerId="...">
<reason code="4">removed by other party</reason>
</partyLeft>
</chatTranscript>
You need code to do this cleanly. Trying to do what you are asking will be super messy T-SQL. I'd recommend parsing the xml in code to generate what you want based on that xml. You could also create a CLR function using code so that you can create a SQL function to do this. You can do some amazing things with XQuery and T-SQL, but sometimes it just gets to messy. For xml manipulation all within the database, CLR functions are perfect.
Here is my solution:
ALTER FUNCTION [dbo].[fn_parse_chat_xml] (#xml XML)
RETURNS NVARCHAR(MAX)
BEGIN
DECLARE #n INT, #content NVARCHAR(MAX), #userId NVARCHAR(200), #userNick1 NVARCHAR(200), #userNick2 NVARCHAR(200), #userNickX NVARCHAR(200)
SET #n = 1
SET #userId = #xml.value('(chatTranscript/newParty/#userId)[1]', 'nvarchar(max)')
SET #userNick1 = #xml.value('(chatTranscript/newParty/userInfo/#userNick)[1]', 'nvarchar(max)')
SET #userNick2 = #xml.value('(chatTranscript/newParty/userInfo/#userNick)[2]', 'nvarchar(max)')
WHILE DATALENGTH(#xml.value('(chatTranscript/message/msgText/text())[sql:variable("#n")][1]', 'nvarchar(max)'))>0
BEGIN
IF #userId = #xml.value('(chatTranscript/message/#userId)[sql:variable("#n")][1]', 'nvarchar(max)')
SET #userNickX = #userNick1
else
SET #userNickX = #userNick2
SET #content = concat(#content, ' <> ', #userNickX, ': ', #xml.value('(chatTranscript/message/msgText/text())[sql:variable("#n")][1]', 'nvarchar(max)'))
SET #n = #n + 1
END
RETURN #content
END

SQL Server: how to delete xml node by sql variable?

My first question on Stack Overflow :)
I have XML:
DECLARE #xml XML = '<root><tag1 /><tag2 /></root>';
I need to remove node, but, path to node is variable "#path".
DECLARE #path XML = '/root/tag2';
My query is:
SET #xml.[modify]('delete sql:variable("#path")');
But, I get error:
Msg 9342, Level 16, State 1, Line 9
XQuery [modify()]: An XML instance is only supported as the direct source of an insert using sql:column/sql:variable.
So my question is: how can I delete xml node by sql parameter?
There is no general recipie...
Just some ideas:
If you know the node's name
DECLARE #xml XML = '<root><tag1 /><tag2 /></root>';
DECLARE #nodeToDelete VARCHAR(100)='tag2';
SET #xml.modify('delete (/root/*[local-name()=sql:variable("#nodeToDelete")])[1]');
SELECT #xml;
If you know the node's name with FLWOR-query
DECLARE #xml XML = '<root><tag1 /><tag2 /></root>';
DECLARE #nodeToDelete VARCHAR(100)='tag2';
SET #xml=#xml.query('<root>
{
for $nd in /root/*[local-name()!=sql:variable("#nodeToDelete")]
return $nd
}
</root>');
SELECT #xml;
dynamically created
DECLARE #xpath VARCHAR(100)='/root/tag2';
DECLARE #command VARCHAR(MAX)=
'DECLARE #xml XML = ''<root><tag1 /><tag2 /></root>'';
SELECT #xml;
SET #xml.modify(''delete ' + #xpath + ''');
SELECT #xml';
PRINT #command;
EXEC(#command);

Find and replace just a part of a xml value using XQuery?

I have an XML in one of my columns, that is looking something like this:
<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path3/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path21/anothertest/second.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path15/test123/file.doc</VorlagenHistorie>
</BenutzerEinstellungen>
I would like to replace all test123 occurances (there can be more than one) in VorlagenHistorie with another test, that all paths direct to test123 after my update.
I know, how you can check and replace all values with an equality-operator, I saw it in this answer:
Dynamically replacing the value of a node in XML DML
But is there a CONTAINS Operator and is it possible to replace INSIDE of a value, I mean only replace a part of the value?
Thanks in advance!
I would not suggest a string based approach normally. But in this case it might be easiest to do something like this
declare #xml XML=
'<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
SELECT CAST(REPLACE(CAST(#xml AS nvarchar(MAX)),'/test123/','/anothertest/') AS xml);
UPDATE
If this approach is to global you might try something like this:
I read the XML as derived table and write it back as XML. In this case you can be sure, that only Nodes with VorlageHistorie will be touched...
SELECT #xml.value('(/BenutzerEinstellungen/State)[1]','nvarchar(max)') AS [State]
,(
SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
FROM #xml.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
FOR XML PATH('VorlagenHistorie'),TYPE
)
FOR XML PATH('BenutzerEinstellungen');
UPDATE 2
Try this. It will read all nodes, which are not called VorlagenHistorie as is and will then add the VorlageHistorie nodes with replaced values. The only draw back might be, that the order of your file will be different, if there are other nodes after the VorlagenHistorie elements. But this should not really touch the validity of your XML...
declare #xml XML=
'<BenutzerEinstellungen>
<State>Original</State>
<Unknown>Original</Unknown>
<UnknownComplex>
<A>Test</A>
</UnknownComplex>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
SELECT #xml.query('/BenutzerEinstellungen/*[local-name(.)!="VorlagenHistorie"]') AS [node()]
,(
SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
FROM #xml.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
FOR XML PATH('VorlagenHistorie'),TYPE
)
FOR XML PATH('BenutzerEinstellungen');
UPDATE 3
Use an updateable CTE to first get the values and then set them in one single go:
declare #tbl TABLE(ID INT IDENTITY,xmlColumn XML);
INSERT INTO #tbl VALUES
(
'<BenutzerEinstellungen>
<State>Original</State>
<Unknown>Original</Unknown>
<UnknownComplex>
<A>Test</A>
</UnknownComplex>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>')
,('<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>');
WITH NewData AS
(
SELECT ID
,xmlColumn AS OldData
,(
SELECT t.xmlColumn.query('/BenutzerEinstellungen/*[local-name(.)!="VorlagenHistorie"]') AS [node()]
,(
SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
FROM t.xmlColumn.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
FOR XML PATH('VorlagenHistorie'),TYPE
)
FOR XML PATH('BenutzerEinstellungen'),TYPE
) AS NewXML
FROM #tbl AS t
)
UPDATE NewData
SET OldData=NewXml;
SELECT * FROM #tbl;
A weird solution, but it worked well:
DECLARE #xml XML = '
<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path5/test123/third.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
DECLARE #Counter int = 1,
#newValue nvarchar(max),
#old nvarchar(max) = N'test123',
#new nvarchar(max) = N'anothertest';
WHILE #Counter <= #xml.value('fn:count(//*//*)','int')
BEGIN
SET #newValue = REPLACE(CONVERT(nvarchar(100), #xml.query('((/*/*)[position()=sql:variable("#Counter")]/text())[1]')), #old, #new)
SET #xml.modify('replace value of ((/*/*)[position()=sql:variable("#Counter")]/text())[1] with sql:variable("#newValue")');
SET #Counter = #Counter + 1;
END
SELECT #xml;
Output:
<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/anothertest/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path5/anothertest/third.doc</VorlagenHistorie>
</BenutzerEinstellungen>
If #shnugo's answer does not fit your needs, you can use XML/XQuery approach:
DECLARE #xml xml = '<BenutzerEinstellungen>
<State>Original</State>
<VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
<VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';
DECLARE #from nvarchar(20) = N'test123';
DECLARE #to nvarchar(20) = N'another test';
DECLARE #newValue nvarchar(100) = REPLACE(CONVERT(nvarchar(100), #xml.query('(/BenutzerEinstellungen/VorlagenHistorie/text()[contains(.,sql:variable("#from"))])[1]')), #from, #to)
SET #xml.modify('
replace value of (/BenutzerEinstellungen/VorlagenHistorie/text()[contains(.,sql:variable("#from"))])[1]
with sql:variable("#newValue")')
SELECT #xml
gofr1's answer might be enhanced by using more specific XPath expressions:
DECLARE #Counter int = 1,
#newValue nvarchar(max),
#old nvarchar(max) = N'test123',
#new nvarchar(max) = N'anothertest';
WHILE #Counter <= #xml.value('fn:count(/BenutzerEinstellungen/VorlagenHistorie)','int')
BEGIN
SET #newValue = REPLACE(CONVERT(nvarchar(100), #xml.value('(/BenutzerEinstellungen/VorlagenHistorie)[sql:variable("#Counter")][1]','nvarchar(max)')), #old, #new)
SET #xml.modify('replace value of (/BenutzerEinstellungen/VorlagenHistorie[sql:variable("#Counter")]/text())[1] with sql:variable("#newValue")');
SET #Counter = #Counter + 1;
END
SELECT #xml;

SQL Server XML modify with no result

I have an xml document like
<xsd:form-definition ...>
<xsd:page ..></xsd:page>
<xsd:page ..></xsd:page>
...
</xsd:form-definition>
and I want to insert a new node in the first <xsd:page> element
DECLARE #res XML = '<Subject>English</Subject>'
SET #myXmlContent.modify('insert sql:variable("#res") as first into (/form-definition/page)[1]')
but nothing changes, why ?
The XQuery for your modify needs to declare and reference the xsd namespace:
SET #myXmlContent.modify('declare namespace xsd="..."; insert sql:variable("#res") as first into (/xsd:form-definition/xsd:page)[1]')
Try this one -
DECLARE #XML XML
SELECT #XML =
'<xsd_form-definition>
<xsd_page></xsd_page>
<xsd_page></xsd_page>
</xsd_form-definition>'
DECLARE #res XML = '<VehicleManufacturerID>abc</VehicleManufacturerID>'
SELECT #XML.modify('insert sql:variable("#res") into (xsd_form-definition/xsd_page)[1]')
SELECT #XML

Inserting an attribute in multiple XML Nodes using XML.modify() in SQL 2005

I have an #XML document created from a single select statement.
<root>
<node>
<node1>
<targetNode>
</targetNode>
</node1>
<node1>
<targetNode>
</targetNode>
</node1>
<node1>
<targetNode>
</targetNode>
</node1>
</node>
<node>
......
</node>
</root>
I want to insert the xsi:nil as an attribute of 'targetNode' for this document.
#XML.modify( 'insert attribute xsi:nil {"true"} into (root/node/node1/targetNode) [1]')
The above will insert the attribute into the first occurance of the targetNode in the #XML document. The insert statement however will only work on a single node. Is there any way I can insert this attribute into all instances of targetNode in the #XML document.
I found a simple and elegant solution in DML operations on multiple nodes
http://blogs.msdn.com/b/denisruc/archive/2005/09/19/471562.aspx
The idea is to count how many nodes and modify them one by one:
DECLARE #iCount int
SET #iCount = #var.value('count(root/node/node1/targetNode)','int')
DECLARE #i int
SET #i = 1
WHILE (#i <= #iCount)
BEGIN
#xml.modify('insert attribute xsi:nil {"true"} into (root/node/node1/targetNode)[sql:variable("#i")][1]')
SET #i = #i + 1
END
That's not possible with the modify-function. It only works on a single node.
You can manipulate it as string, although that is definitely ugly and possibly wrong in some cases, depending on the actual structure of your XML.
Like this:
declare #xml as xml
set #xml = '<root>
<node>
<node1>
<targetNode>
</targetNode>
</node1>
<node1>
<targetNode>
</targetNode>
</node1>
<node1>
<targetNode>
</targetNode>
</node1>
</node>
</root>
'
set #xml = replace(cast(#xml as nvarchar(max)), '<targetNode/>', '<targetNode xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" />')
select #xml
you can do this in the select, that you are using to create your xml, using the XSINILL parameter.
http://msdn.microsoft.com/en-us/library/ms178079.aspx
(here is a very rough example)
--create 2 tables and put some data in them
create table node
(
id int identity(1,1) primary key,
node int
)
GO
create table node1
(
id int identity(1,1) primary key,
nodeid int foreign key references node(id),
targetnode int
)
GO
insert into node
select 1
GO 5
insert into node1
select 1,2
union
select 2,null
union
select 3,2
union
select 4,null
--
--select statement to generate the xml
SELECT TOP(1)
(SELECT
( SELECT targetnode
FROM node1
WHERE nodeid = node.id
FOR XML AUTO,
ELEMENTS XSINIL,
TYPE
)
FROM node FOR XML AUTO,
ELEMENTS,
TYPE
)
FROM node FOR XML RAW('root'),
ELEMENTS

Resources