Stored procedure parameter with XML query in MSSQL gives "argument must be string literal" - sql

I'm trying to query a table with a column which is xml data with the query and value functions. When using regular string literals it's all okay, but if I put that in a stored procedure and try to use variables it doesn't work.
I suppose I'm not using the correct datatype, but after some searching I can't figure out what datatype the query function wants.
Example:
table contains
| Id | xmldata |
| 1 | <data><node>value</node></data> |
now, using the select query
select id
from table
where xmldata.query('/data/node').value('.', 'VARCHAR(50)') = 'value'
gets me the data I want. But, if I use this in a stored procedure and use a parameter #xpath varchar(100) and pass that to the query method as xmldata.query(#xpath)
i get the error
The argument 1 of the xml data type method "query" must be a string literal.
I guess varchar(100) is not correct, but what datatype can I use that would make MSSQL happy?
Update:
Okay, so. Apparently you can't pass a parameter to the query method "just like that", but one can use the sql:variable in conjunction with local-name to work a part of it out. So, for instance, this will work
declare #xpath VarChar(100)
set #xpath='node'
select objectData.query('/data/*[local-name() = sql:variable("#xpath")]')
.value('.', 'varchar(100)') as xmldata
from table
and value is selected in the column xmldata. But(!) it requires that the root node is the first value in the query function. The following will not work
declare #xpath VarChar(100)
set #xpath='/data/node'
select objectData.query('*[local-name() = sql:variable("#xpath")]')
.value('.', 'varchar(100)') as xmldata
from table
notice how the query path is "moved up" to the variable. I will continue my investigations..

A literal is the opposite of a variable. The message means that you cannot pass a variable as the first argument to query.
One way around that is dynamic SQL:
declare #sql varchar(max)
set #sql = 'select id from table where xmldata.query(''' + #path +
''').value(''.'', ''VARCHAR(50)'') = ''value'''
exec #sql
As you can see, dynamic SQL does not result in very readable code. I would certainly investigate alternatives.
EDIT: Your suggestion of local-name() works for node names.
declare #nodename varchar(max)
set #nodename = 'node'
...
where xmldata.query('//*[local-name()=sql:variable("#nodename")]')
.value('.', 'varchar(50)') = 'value'
There doesn't seem to be an equivalent for paths.

Related

read from xml string in sql

i want create stored proc to display last name of xml string ''
so i tried..... following script
Declare #xml_string xml
set #xml_string='<Contacts LastName="jonson"/>'
select Lastname =#xml_string.value('Contacts[1]/LastName[1]','varchar(50)')
select statement display NULL here .please help any one ....
This is the correct way on getting the attribute value from the xml.
Declare #xml_string xml set #xml_string='<Contacts LastName="jonson"/>'
select Lastname =#xml_string.value('Contacts[1]/#LastName[1]','varchar(50)')

SQL Server xml variable is not by reference

the following query should append node to an exist xml.
based on the output xml variable is actually new node and not reference of the exists one.
can I use reference variable
please advise:
declare #a_bo_key_xml xml='<r><items><item><key>k1</key><value>v1</value></item></items></r>'
Declare #rowsBOK xml=#a_bo_key_xml.query('/r/items/item')
select #rowsBOK.value('(*/key)[1]','varchar(100)'), #rowsBOK.value('(*/value)[1]','varchar(100)')
set #rowsBOK.modify('insert <added>aaa</added> as first into (*)[1]')
select cast(#a_bo_key_xml as varchar(max))
select cast(#rowsBOK as varchar(max))
output:
<r><items><item><key>k1</key><value>v1</value></item></items></r>
<item><added>aaa</added><key>k1</key><value>v1</value></item>
expected:
<r><items><item><added>aaa</added><key>k1</key><value>v1</value></item></items></r>
<item><added>aaa</added><key>k1</key><value>v1</value></item>
XML variables take a new identity when re-assigned, there is no connection between one and the other.
Just modify the #a_bo_key_xml variable directly:
declare #a_bo_key_xml xml='<r><items><item><key>k1</key><value>v1</value></item></items></r>';
set #a_bo_key_xml.modify('insert <added>aaa</added> as first into (/r/items/item)[1]');
select #a_bo_key_xml;
db<>fiddle

XML Query with a variable in WHERE

I have this code that works well. But I want to have a variable to change its value in run time. I want the output xml file in C#.
declare #MySearch nvarchar(50)
set #MySearch='بودان'
SELECT ID, Family
FROM Table_1
where freetext(Family, #MySearch, LANGUAGE 1025)
FOR XML RAW ('Employee'), ROOT ('Employees'), ELEMENTS XSINIL;
EDITED:
I ran this code and got this error:
Null or empty full-text predicate.
I want the output xml file in C#.
declare #MySearch nvarchar(50)
--set #MySearch=N'بودان'
SELECT ID, Family
FROM Table_1
where freetext(Family, #MySearch, LANGUAGE 1025)
FOR XML RAW ('Employee'), ROOT ('Employees'), ELEMENTS XSINIL;
The "N" makes the difference, and you need it twice...
Try this:
DECLARE #v1 VARCHAR(10)='بودان';
DECLARE #v2 VARCHAR(10)=N'بودان';
DECLARE #nv1 NVARCHAR(10)='بودان';
DECLARE #nv2 NVARCHAR(10)=N'بودان';
SELECT #v1,#v2,#nv1,#nv2;
The result
????? ????? ????? بودان
Only the very last attempt will show what you need...
The reason: A normal string SET #v='MyString' is not handled as UNICODE
It is neither enough to declare the variable as NVARCHAR nor is it enough to store a unicode literal like N'SomeText'.
EDIT:
After long disucussion in comments the final sentence to solve the issue was this:
Of course you must be connected with the DB to do this!
What ever you want to with your database, first you need an opened connection.

Escape SQL function string parameter within query

I have a SQL view that calls a scalar function with a string parameter. The problem is that the string occasionally has special characters which causes the function to fail.
The view query looks like this:
SELECT TOP (100) PERCENT
Id, Name, StartDate, EndDate
,dbo.[fnGetRelatedInfo] (Name) as Information
FROM dbo.Session
The function looks like this:
ALTER FUNCTION [dbo].[fnGetRelatedInfo]( #Name varchar(50) )
RETURNS varchar(200)
AS
BEGIN
DECLARE #Result varchar(200)
SELECT #Result = ''
SELECT #Result = #Result + Info + CHAR(13)+CHAR(10)
FROM [SessionInfo]
WHERE SessionName = #Name
RETURN #Result
END
How do I escape the name value so it will work when passed to the function?
I am guessing that the problem is non-unicode characters in dbo.Session.Name. Since the parameter to the function is VARCHAR, it will only hold unicode characters, so the non-unicode characters are lost when being passed to the function. The solution for this would be to change the parameter to be NVARCHAR(50).
However, if you care about performance, and more importantly consistent, reliable results stop using this function immediately. Alter your view to simply be:
SELECT s.ID,
s.Name,
s.StartDate,
s.EndDate,
( SELECT si.Info + CHAR(13)+CHAR(10)
FROM SessionInfo AS si
WHERE si.SessionName = s.Name
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)') AS Information
FROM dbo.Session AS s;
Using variable concatenation can lead to unexpected results which are dependent on the internal pathways of the execution plan. So I would rule this out as a solution immediately. Not only this, the RBAR nature of a scalar UDF means that this will not scale well at all.
Various ways of doing this grouped concatenation have been benchmarked here, where CLR is actually the winner, but this is not always an option.

How do I make a function in SQL Server that accepts a column of data?

I made the following function in SQL Server 2008 earlier this week that takes two parameters and uses them to select a column of "detail" records and returns them as a single varchar list of comma separated values. Now that I get to thinking about it, I would like to take this table and application-specific function and make it more generic.
I am not well-versed in defining SQL functions, as this is my first. How can I change this function to accept a single "column" worth of data, so that I can use it in a more generic way?
Instead of calling:
SELECT ejc_concatFormDetails(formuid, categoryName)
I would like to make it work like:
SELECT concatColumnValues(SELECT someColumn FROM SomeTable)
Here is my function definition:
FUNCTION [DNet].[ejc_concatFormDetails](#formuid AS int, #category as VARCHAR(75))
RETURNS VARCHAR(1000) AS
BEGIN
DECLARE #returnData VARCHAR(1000)
DECLARE #currentData VARCHAR(75)
DECLARE dataCursor CURSOR FAST_FORWARD FOR
SELECT data FROM DNet.ejc_FormDetails WHERE formuid = #formuid AND category = #category
SET #returnData = ''
OPEN dataCursor
FETCH NEXT FROM dataCursor INTO #currentData
WHILE (##FETCH_STATUS = 0)
BEGIN
SET #returnData = #returnData + ', ' + #currentData
FETCH NEXT FROM dataCursor INTO #currentData
END
CLOSE dataCursor
DEALLOCATE dataCursor
RETURN SUBSTRING(#returnData,3,1000)
END
As you can see, I am selecting the column data within my function and then looping over the results with a cursor to build my comma separated varchar.
How can I alter this to accept a single parameter that is a result set and then access that result set with a cursor?
Others have answered your main question - but let me point out another problem with your function - the terrible use of a CURSOR!
You can easily rewrite this function to use no cursor, no WHILE loop - nothing like that. It'll be tons faster, and a lot easier, too - much less code:
FUNCTION DNet.ejc_concatFormDetails
(#formuid AS int, #category as VARCHAR(75))
RETURNS VARCHAR(1000)
AS
RETURN
SUBSTRING(
(SELECT ', ' + data
FROM DNet.ejc_FormDetails
WHERE formuid = #formuid AND category = #category
FOR XML PATH('')
), 3, 1000)
The trick is to use the FOR XML PATH('') - this returns a concatenated list of your data columns and your fixed ', ' delimiters. Add a SUBSTRING() on that and you're done! As easy as that..... no dogged-slow CURSOR, no messie concatenation and all that gooey code - just one statement and that's all there is.
You can use table-valued parameters:
CREATE FUNCTION MyFunction(
#Data AS TABLE (
Column1 int,
Column2 nvarchar(50),
Column3 datetime
)
)
RETURNS NVARCHAR(MAX)
AS BEGIN
/* here you can do what you want */
END
You can use Table Valued Parameters as of SQL Server 2008, which would allow you to pass a TABLE variable in as a parameter. The limitations and examples for this are all in that linked article.
However, I'd also point out that using a cursor could well be painful for performance.
You don't need to use a cursor, as you can do it all in 1 SELECT statement:
SELECT #MyCSVString = COALESCE(#MyCSVString + ', ', '') + data
FROM DNet.ejc_FormDetails
WHERE formuid = #formuid AND category = #category
No need for a cursor
Your question is a bit unclear. In your first SQL statement it looks like you're trying to pass columns to the function, but there is no WHERE clause. In the second SQL statement you're passing a collection of rows (results from a SELECT). Can you supply some sample data and expected outcome?
Without fully understanding your goal, you could look into changing the parameter to be a table variable. Fill a table variable local to the calling code and pass that into the function. You could do that as a stored procedure though and wouldn't need a function.