XQUERY - modify using multiple variables - sql

I am writing a stored procedure where I can add/modify any XML nodes with a supplied value on a XML column. #XML xml, #NODENAME nvarchar(100) = NULL, #NODEVALUE nvarchar(max) are being passed as parameters. The procedure should insert a value of #NODENAME as a node name with #NODEVALUE as a value into #XML string.
For example: if #XML is <XML></XML>, #NODENAME is 'DISTRICT' and #NODEVALUE is '123', after executing SP, you get <XML><DISTRICT>123</DISTRICT></XML>. Pretty straightforward.
I've got most of the use cases completed like:
when node exists with non-empty non-null value:
SET #XML.modify('replace value of (/XML/*[local-name()=sql:variable("#NodeName")]/text())[1] with (sql:variable("#myVar"))')
when node exists with an empty or null value:
SET #XML.modify('insert text{sql:variable("#myVar")} into (/XML/*[local-name()=sql:variable("#NodeName")])[1]')
But I can't figure out how to get the following use case:
when node does not exist
I have a hardcoded version that works:
SET #XML.modify('insert <DISTRICT>{sql:variable("#myVar")}</DISTRICT> into (/XML)[1]')
But I need to use something that does NOT hardcode the node (in this case DIVISION).
I tried this but it does not work:
SET #XML.modify('insert <{sql:variable("#myVar")}>{sql:variable("#myVar")}</{sql:variable("#myVar")}> into (/XML)[1]')
I get XQuery [modify()]: Syntax error near '{'
I've tried various different methods that I could find with no luck.
How do I use a variable to represent inserting a NODE into XML? I use microsoft server.

I also couldn't figure out a solution with just one operation for every case.
But one way of doing it would be first assuring the node exists, then proceeding to replace its value:
declare #xml xml, #NodeName sysname, #NodeValue nvarchar(max)
set #NodeName = N'DISTRICT'
set #NodeValue = N'New value'
set #xml = N'<XML></XML>'
-- Make sure the node exists
if #xml.exist(N'/XML/*[local-name(.)[1] = sql:variable("#NodeName")]') = 0
declare #new_node xml = N'<'+#NodeName+N'></'+#NodeName+N'>'
set #XML.modify(N'insert sql:variable("#new_node") into (/XML[1])')
-- And make sure it has something
SET #xml.modify(N'insert text{"X"} into (/XML/*[local-name()=sql:variable("#NodeName")])[1]')
-- Then replace it's contents
set #xml.modify(N'replace value of (/XML/*[local-name()=sql:variable("#NodeName")]/text())[1] with (sql:variable("#NodeValue"))')
print cast(#xml as nvarchar(max))
I've created this as a scalar function on this DB Fiddle, showing it works for every scenario described.

I have a solution, not perfect, but still a solution. Instead of trying to have a solution in one line, I split it into two:
DECLARE #NewNode XML=CAST('<'+#NodeName+'>'+#myVar + '</' + #NodeName + '>' as XML)
SET #XML.modify('insert sql:variable("#NewNode") into (/XML)[1]')
I still don't know if it is possible to have a single line solution, if it is and someone posts it, I will mark that as an asnwer.

Related

SQL Server Xquery sql:variable usage

I need to use a dynamic string for an xquery path but .query/.nodes methods require a literal string as parameter. So I decided to try sql:variable
DECLARE #xmlData XML, #node varchar(max)
SET #xmlData = 'Some XML Here'
SET #node = '/path1/path2/path3'
When I query with
select #xmlData.query('/path1/path2/path3')
It returns the intended result
But when I query with
select #xmlData.query('sql:variable("#node")')
It returns the variable value itself as "/path1/path2/path3"
What is wrong here?
This should do the trick:
select #xmlData.query('/*[local-name()=sql:variable("#node")]')
It matches any node with a wildcard *, but there is an extra predicate that the name has to match the variable
For performance reasons, you should preferably use /text() to get inner text, and use .values to get a single value.
select #xmlData.value('(/*[local-name()=sql:variable("#node")]/text())[1]', 'nvarchar(100)')
sql:variable is used to substitute a single scalar variable into an XPath expression, so can't be used to define a full path. You can use it to test against a single node's name, though, e.g.:
declare #xmlData XML = '<path1><path2><path3>foo</path3></path2></path1>'
select [example1] = #xmlData.query('/path1/path2/path3')
--example1
--<path3>foo</path3>
declare #node1 varchar(max) = 'path1'
declare #node2 varchar(max) = 'path2'
declare #node3 varchar(max) = 'path3'
select [example2] = #xmlData.query('//*[local-name()= sql:variable("#node1")]/*[local-name()= sql:variable("#node2")]/*[local-name()= sql:variable("#node3")]');
--example2
--<path3>foo</path3>

XML DML Replace with a Function Result

I would like to dynamically replace the value of an XML Element, in selected XML value(s) being retrieved from a table. This replacement needs to be performed by a function that I would like to call. I can't even begin to find any clues to point me in the right direction...
I have the follow code snippet (incomplete) that I believe is the correct structure for using XPath to get to the element I want, and to qualify by XPath which records I am interested in (one or more), from there I have no idea how to take the result of the called function, and ideally place the result in a sql:column:
UPDATE [database].[dbo].[table_with_xml]
SET table_with_xml.modify('replace value of (//PrimaryApplicant/FName/text())[1] with sql:column("")')
FROM [database].[dbo].[table_with_xml] AS [XML_Table]
WHERE [XML_Table].XML_Field.value('(//HeaderData/CountryCode/text())[1]', 'varchar(100)') = '123'
First of all, if you want to update table data from a function, you aren't allowed to do it. You can use a stored proc though. You can send 'replace value of' statement (from you syntax above) to it dynamically and send the value with which you want to replace. Here's some code to demonstrate what I'm trying to say:
Create PROC changeXML
#xmlPath nvarchar(max), -- the statement 'replace value of'
#value nvarchar(max) -- the new value
AS
BEGIN
DECLARE #pathString nvarchar(max), #sql NVARCHAR(MAX);
-- create full statement dynamically
SET #pathString = 'replace value of ('+#xmlPath+'text())[1] with ("'+#value+'")';
SELECT #pathString;
-- create dynamic sql and pass pathstring to it.
SET #sql = 'UPDATE [dbo].[tableWithXmlData] SET a.modify(''#p'')';
-- execute dynamic sql
EXEC sp_executesql #sql, N'#p nvarchar(max)', #p = #pathString
END
Then run proc something like this:
EXEC [dbo].[chnageXML] #xmlPath = N'path to your xml value',
#value = N'new value'

SQL XML XQUERY - Looks right, NULL/empty value returned

I'm newish to XML XQuery and such but this sure looks like it ought to work.
XML fragment
<Contacts>
<Contact ContactID="7" FileType="MS">
<ID>7</ID>
....
<Comments ContactID="7" />
<CompanyID ContactID="88">Some New Value</CompanyID>
<CompanyName ContactID="7">Some New Value</CompanyName>
</Contact>
</Contacts>
I pass in #SourceField to the stored procedure. In this case its value is CompanyID. I then create a variable I call #xmlPath.
SET #XmlPath = '/Contacts/Contact/' + #SourceField
I retrieve the XML from the table into a variable named #ContactData (see fragment above)
I then create a variable #SqlCmd and execute it.
SET #SqlCmd = 'SET #ContactID = #ContactData.value(''(' + #xmlPath + '/#ContactID)[1]'', ''nvarchar(max)'')'
EXECUTE sp_executesql #SqlCmd, N'#ContactID nvarchar(20), #ContactData XML',#ContactID, #ContactData
I issue three SQL commands to test the values.
SELECT #ContactData AS ContactData
SELECT #SqlCmd AS SqlCmd
SELECT #ContactID AS ContactID
The first returns the XML from the table. (see fragment above)
The second returns the following;
SET #ContactID = #ContactData.value('(/Contacts/Contact/CompanyID/#ContactID)[1]', 'nvarchar(max)')
which looks correct but...
and the third returns NULL when I would expect 88. Also, when I initialize #ContactID = '' I receive the empty string.
So, my conclusion is that I am missing an assignment process somewhere but the code looks like every example I've seen online that does work. I MUST be missing something simple.
Any thoughts?
Thanks!
You need to declare the parameter #ContactID as an output parameter.
Try this:
EXECUTE sp_executesql #SqlCmd, N'#ContactID nvarchar(20) out, #ContactData XML',#ContactID out, #ContactData

Extracting nvarchar value from XML in T-SQL: only one character returned

In my T-SQL procedure I'm trying to extract a string value from the XML node using the .value() method, like this:
declare #criteria xml;
set #criteria = N'<criterion id="DocName"><value>abcd</value></criterion>';
declare #val nvarchar;
set #val = #criteria.value('(criterion[#id="DocName"]/value)[1]', 'nvarchar');
select #val;
I expected to get 'abcd' as a result, but I surprisingly got just 'a'.
So, the value method returns only the 1st character of the string. Can anybody tell me, what am I doing wrong? Thanks a lot.
P.S. I'm using MS SQL Server 2012
Don't use nvarchar without size. From documentation:
When n is not specified in a data definition or variable declaration
statement, the default length is 1. When n is not specified with the
CAST function, the default length is 30.
If you don't know exact length, you always can use nvarchar(max):
declare #criteria xml;
set #criteria = N'<criterion id="DocName"><value>abcd</value></criterion>';
declare #val nvarchar(max);
set #val = #criteria.value('(criterion[#id="DocName"]/value)[1]', 'nvarchar(max)');
select #val;
sql fiddle demo

Retrieve parameter value from parameter name as a string in SQL

I am trying to use some dynamic sql. I have generated over 100 parameters, and to speed things up I am trying to use dynamic sql to only insert values into a table that I have to based off of information retrieved from other tables. I have tried many things like adding a cast etc.
Example of what I mean:
DECLARE #var1 NVARCHAR(MAX)
-- Loop through and add various values
SET #var1 = #var1 + #parameterName
-- The parameter name is retrieved from a table that holds this information
the problem is that when I add the parameter name which would be like "#myFirstParameter" into my final expression so something like this:
DECLARE #finalString NVARCHAR(MAX)
SET #finalString = 'INSERT INTO myTableName ([myFirstParameter]) VALUES (#myFirstParameter)'
EXEC(#finalString)
The "#myFirstParameter" does not get replaced by it's value and I get the following error:
Must declare the scalar variable "#myFirstParameter".
Does anyone know of a way to go from the string name of a parameter to the actual value? I am trying to avoid hardcoding all the values and any work around I have attempted has failed and given me errors which appear to be much worse than what I have stated above.
The first way is to add the parameter's value, instead of its name, to the SQL string:
SET #finalString = 'INSERT INTO myTableName ([myFirstParameter]) VALUES (' +
#myFirstParameter + ')'
This assumes the parameter has a string value. If not, you have to cast it, like cast(#myFirstParameter as varchar(128)).
The second way is to use sp_executesql instead of exec:
exec sp_executesql
N'INSERT INTO myTableName ([myFirstParameter]) VALUES (#myFirstParameter)',
N'#myFirstParameter varchar(128)',
#myFirstParameter = #myFirstParameter;