1

I am relatively new to xQuery and don't use it very often, and I have what's likely a relatively simple question that I just don't know the answer to. How do you apply a function recursively when you have to compare against a parent/child combination rather than a single element in a for loop?

I have a set of data where I have several parent/child element sets with @xml:id attributes

<root>
     <something>
     </something>
     <somethingElse>
         <parent @xml:id="p.1">
            <child @xml:id="c.1">
                <grandchild/>
            </child>
            <child @xml:id="c.2">
                <grandchild/>
            </child>
        </parent>
        <parent @xml:id ="p.2">
            <child @xml:id="c.1">
                <grandchild/>
            </child>
        </parent>
   </somethingElse>
</root>

I need to be able to add an attribute to a specific child of a specific parent, like so

<root>
     <something>
     </something>
     <somethingElse>
         <parent @xml:id="p.1">
            <child @xml:id="c.1">
                <grandchild/>
            </child>
            <child @xml:id="c.2" active="yes">
                <grandchild/>
            </child>
        </parent>
        <parent @xml:id ="p.2">
            <child @xml:id="c.1">
                <grandchild/>
            </child>
        </parent>
   </somethingElse>
</root>

In looking at what's already been done the functx library function add-attributes will do this

declare function functx:add-attributes
  ( $elements as element()* ,
    $attrNames as xs:QName* ,
    $attrValues as xs:anyAtomicType* )  as element()? {

   for $element in $elements
   return element { node-name($element)}
                  { for $attrName at $seq in $attrNames
                    return if ($element/@*[node-name(.) = $attrName])
                           then ()
                           else attribute {$attrName}
                                          {$attrValues[$seq]},
                    $element/@*,
                    $element/node() }
 } ;

but when I apply this to my data(as $body) via the let statement let $bodynew := functx:add-attributes($body//parent[@xml:id='p.1']/child[@xml:id='c.2'], xs:QName('active'), 'yes')

I only get the following:

        <child @xml:id="c.2" active="yes">
            <grandchild/>
        </child>

I understand why I'm only getting the single child element back, but I'm not sure how to return all of the XML, with the change made by the function, when I'm checking against a parent/child combination as I am here since I can't just apply the function to a single element in a for loop. Any help that could be given would be great.

line-o
  • 1,885
  • 3
  • 16
  • 33
medievalmatt
  • 427
  • 2
  • 12
  • Which XQuery processor is that? BaseX or eXist-db support XQuery update so for such tasks it might be easier to use that. – Martin Honnen Aug 04 '22 at 18:42
  • It's eXist-db, so Saxon. My understanding, though, is that update is for altering the document permanently and I don't want to do that, just the local copy I'm working from. – medievalmatt Aug 04 '22 at 19:25
  • eXist-db does not use Saxon for its XQuery functionality. BaseX supports working with XQuery update on local copies with the `copy` expression https://docs.basex.org/wiki/XQuery_Update#copy.2Fmodify.2Freturn, I don't know eXist-db to tell whether they also support that, but check their documentation or wait for some eXist-db aficionados to show up (let's hope they are not all busy attending Balisage). – Martin Honnen Aug 04 '22 at 19:51
  • 1
    Ok, I checked and 'update' doesn't work with stuff in memory in eXist. I found what (to my eyes) is a kludgey solution, though. – medievalmatt Aug 05 '22 at 07:14
  • I have edited my answer with a pure recursive XQuery 3.1 function taking a root node, a sequence of elements to which you want to attributes plus a sequence of attributes to be added. – Martin Honnen Aug 05 '22 at 07:23

3 Answers3

2

In BaseX

copy $d1 := document {
    <root>
     <something>
     </something>
     <somethingElse>
         <parent xml:id="p.1">
            <child xml:id="c.1">
                <grandchild/>
            </child>
            <child xml:id="c.2">
                <grandchild/>
            </child>
        </parent>
        <parent xml:id ="p.2">
            <child xml:id="c.1">
                <grandchild/>
            </child>
        </parent>
   </somethingElse>
</root>
}
modify insert node attribute { 'active' } { 'yes' } into $d1//parent[@xml:id='p.1']/child[@xml:id='c.2']
return $d1

works to return e.g.

<root>
  <something/>
  <somethingElse>
    <parent xml:id="p.1">
      <child xml:id="c.1">
        <grandchild/>
      </child>
      <child active="yes" xml:id="c.2">
        <grandchild/>
      </child>
    </parent>
    <parent xml:id="p.2">
      <child xml:id="c.1">
        <grandchild/>
      </child>
    </parent>
  </somethingElse>
</root>

I haven't been able to identify whether eXist-db supports that or something similar.

In pure, recursive XQuery 3.1 you can use

declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";

declare option output:method 'xml';
declare option output:indent 'yes';

declare function local:add-attributes($root as node(), $elements as element()*, $attributes as attribute()*) as node()
{
  typeswitch ($root) 
    case document-node() 
      return document { 
        $root ! node() ! local:add-attributes(., $elements, $attributes) 
      }
    case element() 
      return 
        if ($root intersect $elements) 
        then element { node-name($root) } { $root/@*, $attributes, $root ! node() ! local:add-attributes(., $elements, $attributes) } 
        else element { node-name($root) } { $root/@*, $root ! node() ! local:add-attributes(., $elements, $attributes) }
    case text() return $root
    case comment() return $root
    case processing-instruction() return $root
    default return error(QName("", "unknown node"))
};

local:add-attributes(/, //parent/child[@xml:id = 'c.2'], attribute { 'active' } { 'yes' })
Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
1

A working and tested example of recursive processing of nodes in eXist-db.

Tested on the current develop HEAD (v6.1.0-SNAPSHOT) but should also work in earlier versions.

xquery version "3.1";

declare function local:set-active-by-id ($node, $id) {
    element { node-name($node) } {
        $node/@*,
        if ($node/@xml:id = $id)
        then attribute active { "yes" }
        else (),
        $node/node() ! local:set-active-by-id(., $id)
    }
};

let $data :=
<root>
     <something>
     </something>
     <somethingElse>
         <parent xml:id="p.1">
            <child xml:id="c.1">
                <grandchild/>
            </child>
            <child xml:id="c.2">
                <grandchild/>
            </child>
        </parent>
        <parent xml:id ="p.2">
            <child xml:id="c.1">
                <grandchild/>
            </child>
        </parent>
   </somethingElse>
</root>

return local:set-active-by-id($data, "c.2")

NOTE: Since an xml:id attribute must be unique within an XML-document, there is really no need to check for the parent if the id is known.

line-o
  • 1,885
  • 3
  • 16
  • 33
0

I got it working, but man is it an ugly solution.

The update/modify et al. functions don't seem to be available to in-memory nodes in eXist-db. It throws an error when I try to use them. Likewise, Martin's much more elegant solution above doesn't work (or at least I haven't been able to get it to do so). What I ended up doing is writing a function that finds each instance of child with the appropriate xml:id, then checks to see if it has the correct parent. This does work, but I'm sure it's inefficient as all get out and needs optimization. If it turns out I'm wrong and the copy function will handle in-memory nodes in eXist then I suggest you go with something akin to what Martin has.

Anyway, here is the function.

declare function local:activeChange($element as element(), $parentAttr as xs:string, $childAttr as xs:string)
{
    element {node-name($element)}
    {$element/@*,
        for $item in $element/node()
            return
                if ($item instance of element())
                then if ($item/self::child[@xml:id=$childAttr])
                     then if ($item/parent::parent[@xml:id=$parentAttr])
                            then local:copy(functx:add-attributes($item, xs:QName('active'), 'yes'))
                            else local:activeChange($item, $parentAttr,$childAttr)
                     else local:activeChange($item, $parentAttr,$childAttr)
                else $item
    }
};
medievalmatt
  • 427
  • 2
  • 12