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

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

Related

T-SQL: How to avoid string manipulation on this XML?

I need to move a node from one place to another in some XML, but after becoming frustrated, I used string manipulation.
I'm trying to move <ReaderTypeID>5</ReaderTypeID> from under <SCPReplyMessage> to be under <SCPReplyMessage><tran>
The section of code where I take a node from outside tran and move it inside tran became troubling and I had to get it working, so I resorted to a more comfortable (but inefficient) approach: string manipulation.
-- move ReaderTypeID from outside <tran> to be inside <tran>
DECLARE #rtidXml VARCHAR(100)
SELECT #rtidXml = CONVERT(VARCHAR(100),#ReplyMessageXml.query('/SCPReplyMessage/ReaderTypeID'))
DECLARE #st NVARCHAR(max)
SET #st = CONVERT(NVARCHAR(MAX),#tranXml)
SET #st = REPLACE(#st,'</tran>',#rtidXml + '</tran>')
SET #tranXml.modify('delete /SCPReplyMessage/ReaderTypeID')
I'd like to accomplish the same result without the CONVERT to and from XML.
Thanks!
the function:
CREATE FUNCTION dbo.udf_mTranAddl (#ReplyMessageXml XML)
returns XML
AS
BEGIN
DECLARE #tranXml XML
SELECT #tranXml = #ReplyMessageXml.query('/SCPReplyMessage/tran')
-- Discard extraneous tran elements
SET #tranXml.modify('delete /tran/ser_num')
SET #tranXml.modify('delete /tran/time')
SET #tranXml.modify('delete /tran/sys')
SET #tranXml.modify('delete /tran/sys_comm')
-- move ReaderTypeID from outside <tran> to be inside <tran>
DECLARE #rtidXml VARCHAR(100)
SELECT #rtidXml = CONVERT(VARCHAR(100),#ReplyMessageXml.query('/SCPReplyMessage/ReaderTypeID'))
DECLARE #st NVARCHAR(max)
SET #st = CONVERT(NVARCHAR(MAX),#tranXml)
SET #st = REPLACE(#st,'</tran>',#rtidXml + '</tran>')
SET #tranXml.modify('delete /SCPReplyMessage/ReaderTypeID')
RETURN CONVERT(xml, #st)
END
Input #ReplyMessageXml:
<SCPReplyMessage>
<ContDeviceID>5974</ContDeviceID>
<LocalTime>2019-08-29T12:35:43</LocalTime>
<Priority>false</Priority>
<ReaderTypeID>5</ReaderTypeID>
<Deferred>false</Deferred>
<tran>
<ser_num>147</ser_num>
<time>1567096543</time>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<sys>
<error_code>4</error_code>
</sys>
<sys_comm>
<current_primary_comm>123</current_primary_comm>
<current_alternate_comm>4</current_alternate_comm>
</sys_comm>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
</tran>
<SCPId>99</SCPId>
<ReplyType>7</ReplyType>
<ChannelNo>-1</ChannelNo>
</SCPReplyMessage>
output (which is correct):
<tran>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
<ReaderTypeID>5</ReaderTypeID>
</tran>
FINAL RESULT:
Thanks to #PeterHe
CREATE FUNCTION dbo.udf_mTranAddl (#ReplyMessageXml XML)
returns XML
AS
BEGIN
DECLARE #tranXml XML
SELECT #tranXml = #ReplyMessageXml.query('/SCPReplyMessage/tran')
-- Discard extraneous tran elements
SET #tranXml.modify('delete /tran/ser_num')
SET #tranXml.modify('delete /tran/time')
SET #tranXml.modify('delete /tran/sys')
SET #tranXml.modify('delete /tran/sys_comm')
-- move ReaderTypeID from outside <tran> to be inside <tran>
DECLARE #x1 xml;
SELECT #x1=#ReplyMessageXml.query('SCPReplyMessage/ReaderTypeID');
SET #tranXml.modify('insert sql:variable("#x1") into (/tran)[1]')
SET #tranXml.modify('delete /SCPReplyMessage/ReaderTypeID')
RETURN #tranXml
END
GO
YOu can do it using xquery:
DECLARE #x xml = '<SCPReplyMessage>
<ContDeviceID>5974</ContDeviceID>
<LocalTime>2019-08-29T12:35:43</LocalTime>
<Priority>false</Priority>
<ReaderTypeID>5</ReaderTypeID>
<Deferred>false</Deferred>
<tran>
<ser_num>147</ser_num>
<time>1567096543</time>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<sys>
<error_code>4</error_code>
</sys>
<sys_comm>
<current_primary_comm>123</current_primary_comm>
<current_alternate_comm>4</current_alternate_comm>
</sys_comm>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
</tran>
<SCPId>99</SCPId>
<ReplyType>7</ReplyType>
<ChannelNo>-1</ChannelNo>
</SCPReplyMessage>'
DECLARE #output xml;
SELECT #output = #x.query('/SCPReplyMessage/tran');
SET #Output.modify('delete(/tran/ser_num)');
SET #Output.modify('delete(/tran/time)');
SET #Output.modify('delete(/tran/sys)');
SET #Output.modify('delete(/tran/sys_comm)');
DECLARE #x1 xml;
SELECT #x1=#x.query('SCPReplyMessage/ReaderTypeID');
SET #output.modify('insert sql:variable("#x1") into (/tran)[1]')
SELECT #output;
Here is a much easier way by using XQuery FLWOR expression. The main idea is to construct what you need in one single statement instead of moving, deleting, inserting, etc.
SQL
DECLARE #xml XML =
N'<SCPReplyMessage>
<ContDeviceID>5974</ContDeviceID>
<LocalTime>2019-08-29T12:35:43</LocalTime>
<Priority>false</Priority>
<ReaderTypeID>5</ReaderTypeID>
<Deferred>false</Deferred>
<tran>
<ser_num>147</ser_num>
<time>1567096543</time>
<source_type>9</source_type>
<source_number>0</source_number>
<tran_type>6</tran_type>
<tran_code>13</tran_code>
<sys>
<error_code>4</error_code>
</sys>
<sys_comm>
<current_primary_comm>123</current_primary_comm>
<current_alternate_comm>4</current_alternate_comm>
</sys_comm>
<c_id>
<format_number>4</format_number>
<cardholder_id>123</cardholder_id>
<floor_number>4</floor_number>
</c_id>
<oal>
<nData>AAAAAA==</nData>
</oal>
</tran>
<SCPId>99</SCPId>
<ReplyType>7</ReplyType>
<ChannelNo>-1</ChannelNo>
</SCPReplyMessage>';
SELECT #xml.query('<tran>{
for $x in /SCPReplyMessage/tran
return ($x/source_type,
$x/source_number,
$x/tran_type,
$x/tran_code,
$x/c_id,
$x/oal,
$x/../ReaderTypeID)
}</tran>');

select returns only one item from XML

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)

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

Parameterizing XPath for modify() in SQL Server XML Processing

Just like the title suggests, I'm trying to parameterize the XPath for a modify() method for an XML data column in SQL Server, but running into some problems.
So far I have:
DECLARE #newVal varchar(50)
DECLARE #xmlQuery varchar(50)
SELECT #newVal = 'features'
SELECT #xmlQuery = 'settings/resources/type/text()'
UPDATE [dbo].[Users]
SET [SettingsXml].modify('
replace value of (sql:variable("#xmlQuery"))[1]
with sql:variable("#newVal")')
WHERE UserId = 1
with the following XML Structure:
<settings>
...
<resources>
<type> ... </type>
...
</resources>
...
</settings>
which is then generating this error:
XQuery [dbo.Users.NewSettingsXml.modify()]: The target of 'replace' must be at most one node, found 'xs:string ?'
Now I realize that the modify method must not be capable of accepting a string as a path, but is there a way to accomplish this short of using dynamic SQL?
Oh, by the way, I'm using SQL Server 2008 Standard 64-bit, but any queries I write need to be compatible back to 2005 Standard.
Thanks!
In case anyone was interested, I came up with a pretty decent solution myself using a dynamic query:
DECLARE #newVal nvarchar(max)
DECLARE #xmlQuery nvarchar(max)
DECLARE #id int
SET #newVal = 'foo'
SET #xmlQuery = '/root/node/leaf/text()'
SET #id = 1
DECLARE #query nvarchar(max)
SET #query = '
UPDATE [Table]
SET [XmlColumn].modify(''
replace value of (' + #xmlQuery + '))[1]
with sql:variable("#newVal")'')
WHERE Id = #id'
EXEC sp_executesql #query,
N'#newVal nvarchar(max) #id int',
#newVal, #id
Using this, the only unsafe part of the dynamic query is the xPath, which, in my case, is controlled entirely by my code and so shouldn't be exploitable.
The best I could figure out was this:
declare #Q1 varchar(50)
declare #Q2 varchar(50)
declare #Q3 varchar(50)
set #Q1 = 'settings'
set #Q2 = 'resources'
set #Q3 = 'type'
UPDATE [dbo].[Users]
SET [SettingsXml].modify('
replace value of (for $n1 in /*,
$n2 in $n1/*,
$n3 in $n2/*
where $n1[local-name(.) = sql:variable("#Q1")] and
$n2[local-name(.) = sql:variable("#Q2")] and
$n3[local-name(.) = sql:variable("#Q3")]
return $n3/text())[1]
with sql:variable("#newVal")')
WHERE UserId = 1
Node names are parameters but the level/number of nodes is sadly not.
Here is the solution we found for parameterizing both the property name to be replaced and the new value. It needs a specific xpath, and the parameter name can be an sql variable or table column.
SET Bundle.modify
(
'replace value of(//config-entry-metadata/parameter-name[text() = sql:column("BTC.Name")]/../..//value/text())[1] with sql:column("BTC.Value") '
)
This is the hard coded x path: //config-entry-metadata/parameter-name ... /../..//value/text()
The name of the parameter is dynamic: [text() = sql:column("BTC.Name")]
The new value is also dynamic: with sql:column("BTC.Value")

Resources