Mauricio Ortega
Mauricio Ortega

Reputation: 335

Get xml-path from a node selected from a document

Given this scenario, I selected the nodes with xpath //client-agent that matches three nodes:

begin
    declare @Xml xml

    set @Xml = convert(xml,'<root><other><client-agent type="a" /><other><client-agent type="b" /><client-agent type="c" ><some-info /></client-agent></other></other></root>')

    select @Xml

    select t.n.value('@type','varchar(max)') as [client-agent@type]
         , t.n.query('.') OuterXml
         , 'Is there a "t.n.SomeXmlFunction(WithTheRequiredParameters)" expression to get the node path' as [the-path-i-want-to-get]
      from @Xml.nodes('//client-agent') as t(n)
end

Is there an expression ( like native .query() or .value('','') ) on the node t.n to get the xpath on the column [the-path-i-want-to-get] as I described in the list above the code?

Upvotes: 2

Views: 51

Answers (1)

Charlieface
Charlieface

Reputation: 72060

Unfortunately there is no easy way to get this in SQL Server.

You can use some XQuery to descend through all the nodes in the XML, checking for the context node, and return node names as you go. This is likely to be very inefficient for a large number of nodes.

This would have been much easier had SQL Server supported the path-to-node-with-pos function, or even the ancestor:: axis.

The logic is:

  • Set $current to the context node.
  • Loop through all descendant nodes from the top of the XML.
    • If that node has any descendant node (incl. itself) which...
      • ... is the identical node as $current
  • Then return / plus the name of that node.
  • We put that into a constructed text{} node, otherwise a space is added.
  • We need to use .query to return all those text nodes, then .value to munge them all into one string.
select
  t.n.value('@type','varchar(max)') as [client-agent@type],
  t.n.query('.') OuterXml,
  t.n.query('
    let $current := .
    for $i in //* [ descendant-or-self::node() [. is $current ] ]
    return text{ concat("/", local-name($i)) }
  ').value('text()[1]', 'nvarchar(max)') as [the-path-i-want-to-get]
from @Xml.nodes('//client-agent') as t(n);

This doesn't return the position of each of those nodes. For that you need an even more complex XQuery, which can't use the position(.) function either. So we need to count all previous matching nodes.

The logic of the count() is:

  • From the $i loop variable, ascend to the .. parent, then get all descendant nodes where...
  • ... the name of that node is the same as the name of $i
  • ... and it is earlier << in the document than $i.
  • And add 1.
select
  t.n.value('@type','varchar(max)') as [client-agent@type],
  t.n.query('.') OuterXml,
  t.n.query('
    let $current := .
    for $i in //* [ descendant-or-self::node() [. is $current ] ]
    let $name := local-name($i)
    let $c := count( $i/../* [local-name(.) eq $name and . << $i] ) + 1
    return text{ concat("/", $name, "[", string($c), "]") }
  ').value('text()[1]', 'nvarchar(max)') as [the-path-i-want-to-get]
from @Xml.nodes('//client-agent') as t(n);

db<>fiddle

On very large documents it may be faster to use some kind of recursive CTE.

Upvotes: 3

Related Questions