Where clause to determine if element value is in a sequence (XQ) - xquery

I am trying to pull some data from our XML-native db, IF the value of one element is in contained sequence of values. Don't think I worded that right. For example,
Given the following XML:
<root>
<a>1</a>
<root>
Seems there should be a way to do something like (excuse syntax, treat as pseudo)
where root/a in (1,2,3,4)
From all of my searching, that doesn't seem to be possible. At best, I've gotten:
where root/a = 1 or root/1 = 2 or root/a = 3 etc
is there a better way to do this?

You can simply use = operator :
where root/a = (1,2,3,4)
= works on set values similar to contains, which is what you needed exactly. Contrast = with eq which requires atomic values for comparison.
Below is a complete example.
XML:
<root>
<parent>
<a>1</a>
</parent>
<parent>
<a>4</a>
</parent>
<parent>
<a>10</a>
</parent>
</root>
XQuery :
for $p in /root/parent
where $p/a = (1,2,3,4)
return $p
Output :
<parent>
<a>1</a>
</parent>
<parent>
<a>4</a>
</parent>
demo : xpathtester

Related

Distinct attribute names

With XQuery I want to select a special value from every article within a product.
What I currently have:
Input XML (extract):
<product type="product" id="2246091">
<product type="article">
<attribute identifier="EXAMPLE1" type="BOOLEAN">0</attribute>
<attribute identifier="EXAMPLE2" type="BOOLEAN">1</attribute>
</product>
<product type="article">
<attribute identifier="EXAMPLE1" type="BOOLEAN">1</attribute>
<attribute identifier="EXAMPLE2" type="BOOLEAN">1</attribute>
</product>
<product type="article">
<attribute identifier="EXAMPLE1" type="BOOLEAN">0</attribute>
<attribute identifier="EXAMPLE2" type="BOOLEAN">1</attribute>
</product>
</product>
XQuery:
for $i in //product
[#type = 'product'
and #id = '2246091']
//attribute
[#type='BOOLEAN'
and #identifier= ('EXAMPLE1', 'EXAMPLE2') ]
where $i = '1'
return $i
This returns me every attribute element from every article under a product where the content is '1' and its identifier is EXAMPLE1 or EXAMPLE2.
It could be, that in article 1 there is the same attribute identifier (e.g. EXAMPLE1) as in article 2.
What I get:
<?xml version="1.0" encoding="UTF-8"?>
<attribute identifier="EXAMPLE2" type="BOOLEAN">1</attribute>
<attribute identifier="EXAMPLE1" type="BOOLEAN">1</attribute>
<attribute identifier="EXAMPLE2" type="BOOLEAN">1</attribute>
<attribute identifier="EXAMPLE2" type="BOOLEAN">1</attribute>
I tried to add a distinct-values around my for loop, but this will return me only '1'.
What I would like is to get every attribute only once:
<attribute identifier="EXAMPLE2" type="BOOLEAN">1</attribute>
<attribute identifier="EXAMPLE1" type="BOOLEAN">1</attribute>
It sounds as if what you want is to see one attribute element for each distinct value of the identifier attribute found among the attribute elements whose content is 1. (Or, slightly more challengingly, one attribute element for each set of equivalent attribute elements, where equivalence is defined by deep-equals().)
The distinct-values() function isn't helping you here, because it coerces any input nodes into simple values (here, 1).
If matching on the identifier attribute suffices
If the identifier attribute suffices to establish equivalence among the elements, then something like the following should suffice (not tested):
let $ones := //product[#type = 'product'
and #id = '2246091']
//attribute[#type='BOOLEAN'
and #identifier =
('EXAMPLE1', 'EXAMPLE2') ],
$ids := distinct-values($ones/#identifier)
for $id in $ids
return ($ones[#identifier = $id])[1]
If a more general equivalence test is needed
If #identifier does not suffice to establish equivalence for your purposes, you will have to do something more complicated; in the general case one way to do it would be to write a function of two arguments (I'll call it local:equivalent()) which returns true iff the two arguments are equivalent for your purposes. Then write a second function to accept a sequence of items and remove duplicates from the sequence (where 'being a duplicate' means 'returning true on local:equivalent()). Something like this might work as a first approximation (not tested):
(: dedup#1: remove duplicates from a sequence :)
declare function local:dedup(
$items as item()*
) as xs:boolean {
local:dedup($items, ())
};
(: dedup#2: work through the input sequence one
by one, removing duplicates and accumulating
non-duplicates. Cost is n^2 / 2. :)
declare function local:dedup(
$in as item()*,
$out as item()*
) as xs:boolean {
if (empty($in))
then $out
else let $car := head($in)
return if (some $i in $in
satisfies
local:equivalent($i, $car))
then local:dedup(tail($in), $out)
else local:dedup(tail($in), ($car, $out))
};
(: equivalent#2: true iff arguments are equivalent :)
declare function local:equivalent(
$x, $y : item()
) as xs:boolean {
// determine application-specific equivalence
// however you like ...
deep-equal($x, $y)
};
(: Now do the work :)
let $ones := //product[#type = 'product'
and #id = '2246091']
//attribute[#type='BOOLEAN'
and #identifier =
('EXAMPLE1', 'EXAMPLE2') ]
return local:dedup($ones)
Those comfortable with higher-order functions will want to go a step further and remove the dependency on having a function named local:equivalent by allowing both local:dedup functions to accept an additional argument providing the equivalence function.

How do I get BaseX to return multiple elements in a nested XQuery?

BaseX is complaining about a nested query of mine. I do not understand why it cannot return multiple lines like it did in the first query. The error says, "Expecting }, found >" and the > it is referring to is the > after name under trips. It works fine if the } is after the close-bracket for id, but obviously, that's not what I want. Here is the query:
for $u in doc("export.xml")/database/USERS/tuple
return
<user>
<login>{$u/USERNAME/text()}</login>
<email></email>
<name></name>
<affiliation></affiliation>
<friend></friend>
<trip>
{for $t in doc("export.xml")/database/TRIPS/tuple
where $t/ADMIN/text() = $u/USERNAME/text()
return
<id> {$t/ID/text()} </id>
<name> {$t/NAME/text()} </name> (: Error is here with <name> :)
<feature> {$t/FEATURE/text()} </feature>
<privacyFlag> {$t/PRIVACY/text() </privacyFlag>)
}
</trip>
</user>
If you want to return multiple items, you need to encapsulate them in a sequence ($item1, $item2, ..., $itemnN). In your case:
for $t in doc("export.xml")/database/TRIPS/tuple
where $t/ADMIN/text() = $u/USERNAME/text()
return (
<id> {$t/ID/text()} </id>,
<name> {$t/NAME/text()} </name>,
<feature> {$t/FEATURE/text()} </feature>,
<privacyFlag> {$t/PRIVACY/text() </privacyFlag>
)
But I'm unsure whether this will do what you expected, or if you actually want to create one element set per trip. Then, you'd also have a single trip element for result and are not required to return a sequence (this is also what's the case in the outer flwor-loop, here the <user/> element encapsulates to a single element):
for $t in doc("export.xml")/database/TRIPS/tuple
where $t/ADMIN/text() = $u/USERNAME/text()
return
<trip>
<id> {$t/ID/text()} </id>
<name> {$t/NAME/text()} </name>
<feature> {$t/FEATURE/text()} </feature>
<privacyFlag> {$t/PRIVACY/text() </privacyFlag>
</trip>

XQuery Match assertion on SOAPUI

I have a soap testStep in SOAPUI with an XQuery match.
The XML (simplified) look as follows:
<root>
<element>
<a>a</a>
<b>b</b>
<c>c</c>
<d>d</d>
</element>
</root>
I want to make an XQuery to get all child nodes from <element> removing a child element depending on his node name. My XQuery looks like:
for $x in //root/element/element()
return
if (name($x) != 'a') then $x
else ""
I expect the next result:
<b>b</b>
<c>c</c>
<d>d</d>
I think that my XQuery is correct, I tested with an XQuery online evaluator and looks ok, you can try with the follow link
However when I use this expression in a XQuery Match assertion in SOAPUI I get the following message: More than one match in current response. How can achieve this with SOAPUI?
Thanks,
Doing some tries finally I found the solution, the way to do this XQuery in SOAPUI is specifying a root node in the XQuery expression i.e:
<MyResult>
{
for $x in //root/element/element()
return
if (name($x) != 'a') then $x
else ""
}
</MyResult>

Find the total number of child elements?

<?xml version="1.0" encoding="UTF-8"?>
<root>
<author>
<name>A</name>
<book>Book1</book>
<book>Book2</book>
</author>
<author>
<name>B</name>
<age>45</age>
<book>Book3</book>
</author>
</root>
How do I write a XQuery to display the total number of books by an author?
One approach is:
let $max-books = max(/root/author/count(book))
return /root/author[count(book) = $max-books]
which will return all authors who have authored a maximum number of books.
As a one liner, this can be simplified to:
/root/author[count(book) = max(/root/author/count(book))]
Another way to do this is:
(for $author in /root/author
order by count($author/book) descending
return $author/name)[1]
which will return an author with the maximum number of books.
#Fox: should you not tag this question with "homework"? ;-)
#Oliver Hallam: IMHO, #Fox wants to list each author together with the amount of books by that author and not the author with the highest amount of books.
Your first query
let $max-books = max(/root/author/count(book))
return /root/author[count(book) = $max-books]
Contains a syntax error. You should use ":=" instead of only "=". Furthermore
#Fox: to find the solution, have a look at FLWOR expressions. You can use the "for" part to select each of the book nodes with an XPath expression and bind each node to a $book variable. Then use the "let" part to define two variables ($authorName and $amountOfBooks) using the $book as "starting point". Last, use the "return" part to define the output format you need for the resulting XML.

Xquery Top Function

How can I achieve something similar to the TOP function is SQL using Xquery? In other words, how can I select the top 5 elements of something with ties? This should be simple but I'm having trouble finding it with Google.
An example of some data I might want to format looks like this:
<?xml version="1.0"?>
<root>
<value>
<a>first</a>
<b>1</b>
</value>
<value>
<a>third</a>
<b>3</b>
</value>
<value>
<a>second</a>
<b>2</b>
</value>
<value>
<a>2nd</a>
<b>2</b>
</value>
</root>
I want to sort by b for all of the values and return a. To illustrate my problem, say I want to return the top two values with ties.
Thanks
For the provided source XML document:
<root>
<value>
<a>first</a>
<b>1</b>
</value>
<value>
<a>third</a>
<b>3</b>
</value>
<value>
<a>second</a>
<b>2</b>
</value>
<value>
<a>2nd</a>
<b>2</b>
</value>
</root>
To get the first two results "with ties" use:
let $vals :=
for $k in distinct-values(/*/*/b/xs:integer(.))
order by $k
return $k
return
for $a in /*/value[index-of($vals,xs:integer(b)) le 2]/a
order by $a/../b/xs:integer(.)
return $a
When this expression is evaluated, the wanted, correct result is produced:
<a>first</a>
<a>second</a>
<a>2nd</a>
Explanation:
We specify in $vals the sorted sequence of all distinct values of /*/*/b, used as integers. This is necessary, because the function distinct-values() is not guaranteed to produce its result sequence in any predefined order. Also, if we do not convert the values to xs:integer before sorting, they would be sorted as strings and this would generally produce incorrect results.
Then we select only those /*/value/a whose b-sibling's index in the sorted sequence of distinct integer b-values is less or equal to 2.
Finally, we need to sort the results by their b-sibling's integer values, because otherwise they will be selected in document order
Do note:
Only this solution at present produces correctly sorted results for any integer values of /*/*/b.
To filter a sequence to the first 5 items you use the fn:position() function:
$sequence[position() le 5]
Do note that when the sequence to filter is a node set resultion from an / step operation, the predicate works againts the last axis. So, maybe you would need to wrap that expression between parentesis.
But, to filter a "calculated sequence" (like sorting or tuples filter conditions), you need to use the full power of the FLWOR expression.
This XQuery:
(for $value in /root/value
order by $value/b
return $value/a)[position() le 2]
Output:
<a>first</a><a>second</a>
Note: This is a simple sort. The filter is the outer most expression because this allows lazy avaluation.
This XQuery:
for $key in (for $val in distinct-values(/root/value/b)
order by xs:integer($val)
return $val)[position() le 2]
return /root/value[b=$key]/a
Output:
<a>first</a><a>second</a><a>2nd</a>
Note: This order the keys first an then return all the result for the first two keys.
Edit: Added explicit integer casting.
You can use XPath on the Node with top limit as indexes
<one>
<two>a</two>
<two>b</two>
<two>c</two>
<two>d</two>
<two>e</two>
<two>f</two>
<two>g</two>
<two>h</two>
</one>
Then
$xml_data/one/two[ 1 to 5 ]
$xml_data/one/two[ some_number to fn:last() ]
With sample input
<one>
<two>a</two>
<two>b</two>
<two>c</two>
<two>d</two>
<two>e</two>
<two>f</two>
<two>g</two>
<two>h</two>
</one>
The following is one way to get the first five rows:
for $two at $index in /one/two
where $index <= 5
return $two
In the MySQL dialect it's not called TOP, but LIMIT. When you google for "limit xquery" you will find:
http://osdir.com/ml/text.xml.exist/2004-02/msg00214.html
and
http://osdir.com/ml/text.xml.exist/2004-08/msg00115.html

Resources