calvinXS calvinXS - 1 month ago 19
SQL Question

SQL - Using a variable as the entire XQuery path in XML.value()

Is it possible to use a variable to define the entire XQuery path when using

.value()
on an XML field?

Using
[local-name()=sql:variable("@FilterA")]
, I can define the filter I want to apply as a pair of variables, but I can't get a valid syntax for the entire XQuery path to work.

Example

DECLARE @myData AS TABLE (myID INT, Parameter XML)
INSERT INTO @myData VALUES
(1, '<paramdata><Date>19/06/15</Date><term>1</term></paramdata>'),
(2, '<paramdata><Date>19/06/15</Date><term>5</term></paramdata>')

--This works as expected
SELECT * FROM @myData
WHERE Parameter.value('/paramdata/term=5','bit') = 1


--Two variables is possible
DECLARE @FilterA VARCHAR(255) = 'term'
DECLARE @FilterB VARCHAR(255) = '5'

SELECT * FROM @myData
WHERE Parameter.value('(/paramdata/*[local-name()=sql:variable("@FilterA")])[1]','int') = @FilterB

--but a single variable isn't
DECLARE @Filter1 VARCHAR(255) = '/paramdata/term=5'
SELECT * FROM @myData
WHERE Parameter.value('[local-name()=sql:variable("@Filter1")]','bit') = 1


I'm aware that casting the whole query to a string and "EXEC"-ing it should work, but this likely not suitable for the larger case where I want to apply this.

EDIT

After reading about the XY problem this question should be rephrased as:

"is there a way to use an argument of the format
SomeNode=SomeValue
to filter on XML columns?"

Answer

As pointed out, it is absolutely impossible to use a variable path other than with dynamic sql and EXEC.

But you might do something like this:

DECLARE @myData AS TABLE (myID INT, Parameter XML)
INSERT INTO @myData VALUES
(1, '<paramdata><Date>19/06/15</Date><term>1</term></paramdata>'),
(2, '<paramdata><Date>19/06/15</Date><term>5</term></paramdata>')

DECLARE @Filter1 VARCHAR(255) = 'term=5';

WITH Splitted AS
(
    SELECT LEFT(@Filter1,CHARINDEX('=',@Filter1)-1) AS NodeName
          ,RIGHT(@Filter1,CHARINDEX('=',REVERSE(@Filter1))-1) AS SearchValue
)
SELECT md.myID
      ,md.Parameter
      ,md.Parameter.value('(/paramdata/*[local-name()=sql:column("NodeName")])[1]','nvarchar(max)')
FROM Splitted
CROSS APPLY @myData AS md

(You can use this expression in a WHEREclause as well

Another approach might be this:

DECLARE @myData AS TABLE (myID INT, Parameter XML)
INSERT INTO @myData VALUES
(1, '<paramdata><Date>19/06/15</Date><term>1</term></paramdata>'),
(2, '<paramdata><Date>19/06/15</Date><term>5</term></paramdata>')

DECLARE @Filter1 VARCHAR(255) = 'term=5';

WITH TheRightID AS
(
    SELECT md.myID
    FROM  @myData AS md     
    CROSS APPLY md.Parameter.nodes('/paramdata/*') AS A(Nd)
    WHERE Nd.value('local-name(.)','nvarchar(max)') + N'=' + Nd.value('.','nvarchar(max)')=@Filter1
)
SELECT * FROM @myData WHERE myID IN(SELECT x.myID FROM TheRightID AS x)

EDIT

You might even use this to fully answer your initial question:

    WHERE N'/' + Nd.value('local-name(..)','nvarchar(max)') 
        + N'/' +Nd.value('local-name(.)','nvarchar(max)') 
        + N'=' + Nd.value('.','nvarchar(max)')=@Filter1

Advise

But my advise was to read about the XY-problem :-)