SQL Server Xquery sql:variable usage - sql

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>

Related

XQUERY - modify using multiple variables

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.

How to separate string by comma using SQL Server?

I wants to separate string by comma. Actually I wants to filter in WHERE Clause like
CREATE PROC spGetRecords
#class varchar(50)
AS
BEGIN
SELECT *
FROM SampleTable
WHERE class in (#class) --in Database class is an integer
END
So, I want to pass the parameter when execute query like below
spGetRecords #class = '12,22,45,66'
I think it's not possible to pass multiple value in single parameter in my case.
So, If I remove separate the string by ',' then I can run my query in while loop that will bring the record correct?
So, How can I separate the string by comma
You can try splitting using xml path as below:
Declare #delimiter nvarchar(max) = ','
Declare #str nvarchar(max) = '12,22,45,66'
Declare #xml as xml
Select #xml = CAST('<x>' + REPLACE((SELECT REPLACE(#str,#delimiter,'$$$SSText$$$') AS [*] FOR XML PATH('')),'$$$SSText$$$','</x><x>')+ '</x>' AS XML)
--select #xml
Select y.value(N'text()[1]', N'nvarchar(MAX)') as value
FROM #xml.nodes(N'x') as x(y)
If you are in SQL Server >= 2016 you can use STRING_SPLIT() as below:
Select * from string_split('12,22,45,66',',')
Use dynamic SQL
exec 'SELECT * FROM SampleTable WHERE class in (' + #class + ')'
which will patch the strings together and then execute it like this:
SELECT * FROM SampleTable WHERE class in (12,22,45,66)
Here is a good solution - Converting String List into Int List in SQL . Idea here is to send in an int list to a stored procedure as an XML parameter.
Here is another quick but dirty solution - t-sql Convert comma separated string into int, without using user created function . Here the passed in value is used within a dynamic sql statement.
More ideas - http://sql-articles.com/scripts/function-to-split-comma-separated-string-to-integer/

How to reference JSON list value with a variable

I am trying to do this:
JSON_VALUE(#jsondata, CONCAT('$.[', #count, '].Name')
such that I can reference a value in my json list using a variable, however the JSON_VALUE function requires a string literal for the 2nd argument (JSON Path).
Is there a way to either make the function read a string as a string literal, or another way to reference a list with a variable?
Thanks.
You can address a list with a variable by destructuring it into a table using OPENJSON.
For example, given:
declare #jsondata nvarchar(max) = '[
{"Name":"zero","Value":"cero"},
{"Name":"one","Value":"uno"},
{"Name":"two","Value":"dos"},
{"Name":"three","Value":"tres"}
]'
declare #count int set #count = 1
declare #val nvarchar(50)
set #val = (select JSON_VALUE(value, '$.Name') from OPENJSON(#JsonData) d where d."key" = #count)
select #count, #val
For a more advanced method you can query JSON with a schema that describes the structure and elements you are interested in:
https://learn.microsoft.com/en-us/sql/relational-databases/json/use-openjson-with-an-explicit-schema-sql-server

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

Evaluating expression in SQL with XQuery (without stored procedure)

I am trying to evaluate an expression in a function, and it happens that XQuery is not able to get string from the SQL variable
DECLARE #XML AS XML = ''
DECLARE #Formula AS NVARCHAR(MAX) = '1+1'
SELECT #XML.query(#Formula)
Using ...
SELECT #XML.query('sql:variable("#Formula")')
... just returns a string 1+1, and not the sum.
Is there a way to make this work in SQL (without using stored procedures, because those will not run form within the function)?
Thanks in advance!