Xquery Top Function - xquery

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

Related

XQuery cannot parse function argument of type item()*

I have a function which returns item()*. The output of this function is something like the following:
<Root>
<Value>
<Year>1999</Year>
<A>50</A>
<B>100</B>
</Value>
<Value>
<Year>2000</Year>
<A>50</A>
<B>100</B>
<Value>
</Root>
This output is supposed to be passed to a second function. I want to be able to parse this and return a different output. I've tried converting the output to a string using fn:parse-xml($output) but I get an error saying No text allowed before root element. However, if I declare a local variable inside the second function and assign it the string output it works.
This works:
declare function function2($outputFromFunction1 as item()*) item()*{
let sToX := fn:parse-xml(" <Root>
<Value>
<Year>1999</Year>
<A>50</A>
<B>100</B>
</Value>
<Value>
<Year>2000</Year>
<A>50</A>
<B>100</B>
<Value>
</Root>")...
};
I am able to parse this variable (for $Value in $sToX/Value...).
Why does it work in this case and not when I use the argument that is passed in?
When the function returns xml-elements as item() you do not need to parse them. They already are in parsed form.
Just use:
for $Value in $outputFromFunction/Value
(If you call parse-xml on this, it is first converted to a string, i.e. "199950100200050100")

Prioritizing data while sorting in DbXML

Using XQuery in DBXML I want to prioritize some elements depending on multiple nodes set to certain values.
I want to be able to show three of this elements at the top and the rest below.
<properties>
<property>
<zip_code>5550</zip_code>
<agency>ABC</agency>
</property>
<property>
<zip_code>5550</zip_code>
<agency>DEF</agency>
</property>
<property>
<zip_code>5550</zip_code>
<agency>DEF</agency>
</property>
<property>
<zip_code>XYZ</zip_code>
<agency>ABC</agency>
</property>
</properties>
We are getting this XML in a property search page. Real search results will be having hundreds of records but we are only taking the first 10 records to display on the first page. Here we need to apply a sorting order which will show properties of "ABC" agency followed by zip code "XYZ" always on top. If the total result set does not have these agencies we can show them in the normal sorting order.
XQuery's flwor-expressions know order by, which can order by arbitrary values which can also be computed. Use an expression which decides if some product is a "top product" or not (resulting in a boolean value).
Afterwards split up result sequence to highlight only a number of results and limit to a total results.
let $highlighted := 3
let $total := 10
let $sorted :=
for $p in //property
(: order by highlighting predicate :)
order by $p/agency eq "ABC" and $p/zip_code eq "XYZ" descending
return $p
return (
(: first $highlighted elements as defined by predicates above :)
$sorted[ position() = (1 to $highlighted) ],
(: the other elements, `/.` forces sorting back to document order :)
$sorted[ position() = ($highlighted + 1 to $total) ]/.
)
The boolean expression can get arbitrary complex for being more precise on top products, like limiting to TVs or defining some minimum price.

XQuery fn:deep-equal - comparing text from XPath with string literal

I'm trying to use XQuery function fn:deep-equal to compare sections of XML documents and I'm getting unexpected behaviour. When comparing XPath value with string literal, function returns false.
For example following code
let $doc :=
<root>
<child><message>Hello</message></child>
</root>
let $message := <message>Hello</message>
let $value := $doc/child/message/text()
let $compareDirectly := fn:deep-equal($value, "Hello") (: -> false :)
let $compareAsString := fn:deep-equal(fn:concat($value, ""), "Hello") (: -> true :)
let $comparePath := fn:deep-equal($value, $message/text()) (: -> true :)
return
<results>
<value>{$value}</value>
<directly>{$compareDirectly}</directly>
<asString>{$compareAsString}</asString>
<path>{$comparePath}</path>
</results>
Executed using Saxon, XQuery program generates following XML
<?xml version="1.0" encoding="UTF-8"?>
<results>
<value>Hello</value>
<directly>false</directly>
<asString>true</asString>
<path>true</path>
</results>
I'd expect $compareDirectly to be true (same as two other examples), but fn:deep-equal does not seem to work as I would intuitively expect. I'm wondering whether this is correct behaviour.
Is there any better wah how to compare two XML nodes?
I'm lookig for some generic solution which could be used for both XML snippets (like values of $doc or $message in example) and also for this special case with string literal.
From the spec:
To be deep-equal, they must contain items that are pairwise deep-equal; and for two items to be deep-equal, they must either be atomic values that compare equal, or nodes of the same kind, with the same name, whose children are deep-equal.
So this is why it doesn't return true when comparing a text node to an atomic type. In your other two examples you are comparing 2 string atomic types. It looks as if you don't need deep-equal, which compares nodes recursively. If that's the case, then you can just compare the strings:
$doc/child/message/string() eq $message/string()
=> true()
If there are other requirements, then you may need to update your example to demonstrate those more clearly.

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.

How to indicate 'missing' tags in XQuery?

I have an XML file:
$xml := <xml>
<element>
<text>blahblah</text>
</element>
<element>
</element>
<element>
<text>blahblah</text>
</element>
</xml>
I can use the query
for $x in $xml/xml/element/text return string($x)
This gives me a list
blahblah
blahblah
with no indication that there is an element which has no element. What I'd like to do is use a query which, if there is no such element, returns, say "missing". How do I do this?
For a sequence of strings (slightly modified version of the first answer):
for $e in $xml/xml/element
return
if ($e/text)
then string($e/text)
else "missing"
or using a let (which seems a little cleaner to me... but it's probably just 6 of one and half dozen of the other):
for $e in $xml/xml/element
let $text := string($e/text)
return
if ($text)
then $text
else "missing"
Hope that helps.
Are you trying to return the "element" elements that don't have any children? (In your example, it's the second occurrence of "element" as the first and last contain "text" elements.)
If so, you can use a predicate in an XPath expression:
/xml/element[not(*)]
This should work:
for $x in $xml/xml/element
return
if (text)
then string(text)
else "missing"
in MarkLogic
for $e in $xml/xml/element
return ($e/text,"missing")
$xml/element/string((text,"missing")[1])
functions are allowed in XPath expressions, so an explicit loop is not needed here.
the expression (text,"missing")[1]
returns the first non-null item in the sequence of the text element followed by the string "missing"
you can use the eXist sandbox to execute code snippets:-
http://demo.exist-db.org/exist/sandbox/sandbox.xql

Resources