xquery efficient predicates versus for...where - xpath

I have a series of xml nodes that look like this:
let $x :=
<works>
<work xml:id="W1">
<author corresp="AU08"/>
<group corresp="GR03"/>
</work>
<work xml:id="W2">
<author corresp="AU09"/>
<group corresp="GR10"/>
</work>
<work xml:id="W3">
<author corresp="AU08"/>
<group corresp="GR05"/>
</work>
....
</works>
I have a search form that may or may not offer sequence parameters against work/#xml:id , work/author/#corresp, and work/affiliation/$corresp, for example:
let $xmlids := ("W1")
let $authors := ()
let $groups := ("GR05","GR08")
or
let $xmlids := ()
let $authors := ("AU01","AU08")
let $groups := ("GR05")
I am trying to create an efficient query in Xquery 3.1 (eXist 4.7) to account for the different permutations of parameters, while outputting work/. This has lead me to build an ugly series of nested if statements which trying to favour predicates over for ...where like the following:
if (count($xmlids) gt 0 and count($authors) gt 0 and count($groups) gt 0)
then
$x/id($xmlids)/author[#corresp=$authors]/parent::work/group[#corresp=$groups]/parent::work
else if (count($xmlids) gt 0 and count($authors) gt 0 and count($groups) eq 0)
then
$x/id($xmlids)/author[#corresp=$authors]/parent::work
else if ...
Is there a more efficient way to build an Xquery accounting for variable present/absent parameters?
Many thanks in advance.

I think for the predicates you just want #corresp=$authors or not(exists($authors)) and #corresp=$groups or not(exists($groups)).
For the id call I think you need
let $work-items := if ($x/id($xmlids)) then $x/id($xmlids) else $x/work
return $work-items[author[#corresp=$authors or not(exists($authors))] and group[#corresp=$groups or not(exists($groups))]]

Related

XQuery - How can I count and save informations in a map

I want to safe IDs in a map. If the ID occurs again, I want to set the count ($value) from 1 to 2 and so on.
Following you find my code:
declare namespace functx = "http://www.functx.com";
declare variable $idMap := map{};
declare function functx:uniqueID ($entityID as xs:string) as xs:integer {
let $idMap := map:merge(($idMap, if(not(map:contains($idMap, $entityID))) then map:entry($entityID, 1) else map:entry(entityID, map:get($idMap, $entityID)+1)))
return map:get($idMap, $entityID)
};
declare variable $map := map:merge((
map:entry("Sheff", "85246525"),
map:entry("Peter", "85246454"),
map:entry("Marcel", "85246525"),
map:entry("Lion", "85244565"),
map:entry("Klaus", "85241234")
));
map:for-each($map,
function($key, $value) {
functx:uniqueID($value)
}
)
Result:
1
1
1
1
1
Expected Result
1
1
2 (: Because it is the second time, that 85246525 occurs. :)
1
1
Edited 23.03.2020 - 17:45:
I have a complex xquery, which functions. But the target system need unique IDs per line. I have a map, which hold my information like the upper one. I need to add something behind the IDs like (001, 002, 003) to have different IDs.
Best practice would be, that only douplicate IDs get a added number.
Do you understand or what do you need more from me?
One way to construct a new map with an "index" added to duplicated values is to use grouping:
map:merge(
for $key in map:keys($map)
group by $value := $map($key)
for $group-key at $pos in $key
return map:entry($group-key, $value || '-' || format-integer($pos, '000'))
)
At https://xqueryfiddle.liberty-development.net/6qVSgeT that gives
{
"Peter": "85246454-001",
"Marcel": "85246525-001",
"Sheff": "85246525-002",
"Lion": "85244565-001",
"Klaus": "85241234-001"
}
You've written your code as if $idMap is mutable, and as if calling functx:uniqueID() has the side-effect of modifying the map. That isn't going to work in a functional language.
You need a completely different approach; and to help you with that, we need to look at the problem you are trying to solve, not at your existing approach to a solution.

eXist-DB / XQuery StringValue cannot be cast to AnyURIValue (using compression:zip)

In eXist 4.4/XQuery 3.1, I am building a function to compress a number of xml files into a zip using compression:zip.
I have one function which collects all the URIs for the documents to be compressed, schedule:get-document-uris-for-zip(xmlid as xs:string). This function returns lists of documents like the following:
/db/apps/deheresi/data/MS609-0001.xml
/db/apps/deheresi/data/MS609-0002.xml
/db/apps/deheresi/data/MS609-0003.xml
/db/apps/deheresi/data/MS609-0004.xml
/db/apps/deheresi/data/MS609-0005.xml
/db/apps/deheresi/data/MS609-0006.xml
/db/apps/deheresi/data/MS609-0007.xml
/db/apps/deheresi/data/MS609-0008.xml
/db/apps/deheresi/data/MS609-0009.xml
/db/apps/deheresi/data/MS609-0010.xml
This function is called by the compression function as follows
declare function schedule:create-zip-by-batch()
{
let $batch := doc(concat($globalvar:URIdocuments,"document_collections.xml"))
for $entry in $batch//collection[#compile="y"]
let $zipobject := compression:zip(schedule:get-document-uris-for-zip($entry/string(#xml:id)),false())
let $zipstore := xmldb:store("/db/apps/deheresi/documents",
"MS609_tei.zip",
$zipobject)
return $zipstore
};
This is throwing a cast error as follows, but I can't identify how to resolve this...
org.exist.xquery.value.StringValue cannot be cast to org.exist.xquery.value.AnyURIValue
Many thanks in advance.
Edit - I'm adding here the part of the function schedule:get-document-uris-for-zip(xmlid as xs:string) which outputs the list of URIs. The URIs are built through string concatenation:
(: get names of documents which meet criteria :)
let $list := xmldb:get-child-resources("/db/apps/deheresi/data")[starts-with(., $y/string(#filename)) and ends-with(., $y/string(#ext))]
(: create URI for each document :)
return
for $n in $list
return concat("/db/apps/deheresi/data/",$n)
You're right to find this function a bit confusing. The (eXist-specific) compression:zip() function $sources parameter is typed as if it is quite flexible way, as xs:anyType()+. But really it is quite strict about the two types of item it accepts: a sequence of URIs (i.e., of type xs:anyURI), or a sequence of <entry> elements:
<entry name="filename.ext"
type="collection|uri|binary|xml|text"
method="deflate|store"
>data</entry>
See https://exist-db.org/exist/apps/fundocs/view.html?uri=http://exist-db.org/xquery/compression#zip.2.
The problem with your code is that you are passing strings in your $sources parameter, and have not cast these strings as xs:anyURI.
Here is sample working code:
xquery version "3.1";
let $prepare :=
(
xmldb:create-collection("/db", "test"),
xmldb:store("/db/test", "test.xml", <test/>)
)
let $zip := compression:zip("/db/test/test.xml" cast as xs:anyURI, false())
return
xmldb:store("/db/test", "test.zip", $zip)

How to pass attribute for parameter using XQUERY

Hi team,
<cars>
<car id="1">Ford</car>
<car num="2">Mazda</car>
<car vol="3">Toyota</car>
<car soid="4">Lexus</car>
</cars>
XQuery:
let $val := 1
let $attr := id
return //car[$attr=$val]
I dont know how to pass attribute name in parameter can you please help?
As an alternative, from XQuery 3.0 you have higher-order functions:
let $val := 1
let $attr := function($x){$x!#id}
return //car[$attr(.)=$val]]
XQuery is pretty static and doesn't much like dynamic path components. You can leverage local-name():
let $val := 1
let $attr := "id"
return //car[#*[local-name()=$attr and data(.)=$val]]
This is not particularly efficient, though. Work with static expressions when you can, even if this means a repeating a few conditional expressions here and there.

xPath to get parent node position using count

My xml looks like the following. The focus is at the third level: <name>,<address> etc. I'd like to find the position of parent <customer>.
I use "count(../preceding-sibling::*) + 1" but it doesn't return a count when the node is empty. How do I fix this? Thanks.
<record>
<customer>
<name>Sue A</name> -- return 1
<address>123 Main St</address> -- return 1
<phone></phone> -- missing
<status>A</status> -- return 1
</customer>
<customer>
<name>John B</name> -- return 2
<address></address> -- missing
<phone>123-456-7890</phone> -- return 2
<status></status> -- missing
</customer>
…
</record>
While the question is lacking a lot of information, using the following xquery It looks like your logic is correct, however you are probably using the text nodes as current nodes instead of the element node.
This query returns the same wrong results, because some text nodes are missing:
let $x :=
for $grandchild in //customer/*/text()
return count($grandchild/../../preceding-sibling::*) + 1
return $x
This query returns correct results, because every element exists:
let $x :=
for $child in //customer/*
return count($child/../preceding-sibling::*) + 1
return $x
Selecting the element node as current node will probably fix any issue.

xs:string as element() in XQuery

Let's assume I have a XML files, one like this:
<SampleF>
<FirstNode AAA="Something" BBB="Something"></FirstNode>
<SecondNode CCC="Random" DDD="Random"></SecondNode>
</SampleF>
And second one like this:
<SampleF2>
<FirstNode>
<AAA>Something</AAA>
<BBB>Random</BBB>
</FirstNode>
</SampleF2>
And I would like to obtain from both (AAA="Something"/Something) of them as element(). How do I convert it? When in first case I get xs:string and in the second document-node().
I made something like this for the first example but I'm 100% certain there is a better way of doing this
declare function getElementFirstExample($message) as element() {
let $name := "AAA"
let $value := $message/*:SampleF1/*:FirstNode/#AAA
return element {$name} {"$value"}
};
Thank you in advance for your help and advices.
As I understand, you want the value of <FirstNodes/>s AAA attribute or child element, no matter whether it is in the element or attribute.
Use an alternative axis step for accessing both the attribute and element, and data(...) to retrieve the string value.
data(//FirstNode/(#AAA, AAA))
Putting this together for your function and explicit use case:
declare function getElementFirstExample($message) as element() {
let $name := "AAA"
let $value := data($message/*:SampleF1/*:FirstNode/(#AAA, *:AAA))
return element {$name} {"$value"}
};

Resources