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)
Related
I am writing Xquery function for BaseX which gets one of arguments as name of the element node. This name is then used in Xpath, but in general I cannot convert string to element.
This is how the method looks like
declare function prefix:getElementWithValue($root as document-node()?, $elem as xs:string?, $minVal as xs:float?, $maxVal as xs:float?)
as element()*
{
let $e := element {$elem} {""}
for $x in $root//SUBELEM
return if ($x//$e/#ATTRIB>=$minVal and $x//$e/#ATTRIB<=$maxVal) then ($x)
};
and the call
return prefix:getElementWithValue($db, "SomeElem", 10.0, 10.0)
and I am getting empty response from that. If I replace the $x//$e with $x//SomeElem it returns proper response. From the QueryPlan I see that the $e is treated as literal value. XPATH is not $x//SomeElem/#ATTRIB but $x//$e/#ATTRIB
So my question is how to covert string to type that can be used in XPATH?
XQuery does not have a standard function to evaluate a dynamically-constructed XPath expression.
Many XQuery processors offer some kind of extension function that does this, however. For example, BaseX offers query:eval():
https://docs.basex.org/wiki/XQuery_Module#xquery:eval
Note that variables in XQuery represent values, not fragments of expression text. Your expression $x//$e/#ATTRIB is equivalent to $x//"SomeElem"/#ATTRIB, which is quite different from $x//SomeElem/#ATTRIB.
If you know that $elem will always be an element name, then you can write $x//*[name()=$e]/#ATTRIB. But take care over namespaces.
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.
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"}
};
I want to succed with following functionality, and it works if you harcoded it:
declare function local:sort($collection as node()*, $filter as xs:SUBPATH?) as node()*{
for $element in $collection
order by
if ($filter) then ($element/$filter) (: OR SOME KIND OF fn:eval($filter) IF WE DEFINED $filter AS AN xs:string :)
else ($element/name()) (: Default :)
descending
return $element
};
And it could be called like that:
for $element in local:sort(doc('data')/Data/*,'/#myAttr')
return $element
or
for $element in local:sort(doc('data')/Data/*,'/subnode/subnode/name()')
return $element
or
for $element in local:sort(doc('data')/Data/*,()) (: This is the default of function = own elmentĀ“s name :)
return $element
Mx problem is passing the subpath. Either I need to know some kind of way to send a relative XPATH as an argument and type of node, or I need some kind of eval to pass from xs:string to a runtime valid code
Any help?
You could consider (a) generating a query in which the filter is hard-coded, or (b) using an eval() function specific to your XQuery vendor, or (c) using XQuery 3.0 higher-order functions if your chosen XQuery engine supports them yet.
I ended up doing the following, thanks to this idea How can I solve this autoncremental var case in XQUERY 1.0 FLOWR?:
(: This is a workaround solution as xquery:eval() is not working with var bindings until BaseX 7.3 version :)
(: It evals $context node + literal subpath with a pattern of '(/)lit/lit/lit/#attr' or '(/)lit/lit/lit' representing (/)lit/lit/lit/name(). If subpath null, it returns $context/name().:)
declare function u:eval-path($context as node()*, $subnodes as xs:string*) as item()* {
if(empty($subnodes)) then $context/name()
else(
if (count($subnodes) eq 1) then ( (: Last Element :)
if (starts-with($subnodes[1],'#')) then $context/#*[name()=substring-after($subnodes[1],'#')]
else $context/*[name()=$subnodes[1]]/name()
)
else if ($subnodes[1] eq '') then u:eval-path($context, $subnodes[position() gt 1])
else u:eval-path($context/*[name()=$subnodes[1]],$subnodes[position() gt 1])
)
};
(: Sorts the given collection by given criteria, which should be a pattern '(/)lit/lit/lit/#attr' or '(/)lit/lit/lit' representing (/)lit/lit/lit/name() :)
(: If criteria is null, everything is ordered by $elements/name(). Theres no way to filter intermediate nodes as in /lit/*[name()='X']/lit :)
declare function u:sort($collection as node()*, $criteria as xs:string?) as node()*{
for $element in $collection
order by u:eval-path($element,tokenize($criteria,'/'))
ascending
return $element
};
When using the MongoDB shell, how do I use a guid datatype (which I have used as the _id in my collection).
The following format doesn't work:
>db.person.find({"_id","E3E45566-AFE4-A564-7876-AEFF6745FF"});
Thanks.
You can use easily:
.find({ "_id" : CSUUID("E3E45566-AFE4-A564-7876-AEFF6745FF")})
You have to compare the _id value against an instance of BinData (not against a string). Unfortunately the BinData constructor takes a Base64 string instead of a hex string.
Your GUID value is missing two hex digits at the end, so for the purposes of this example I will assume they are "00". The following values are equivalent:
hex: "E3E45566-AFE4-A564-7876-AEFF6745FF00" (ignoring dashes)
base64: "ZlXk4+SvZKV4dq7/Z0X/AA=="
So your query should be:
>db.person.find({_id : new BinData(3, "ZlXk4+SvZKV4dq7/Z0X/AA==")})
I am assuming that the binary subtype was correctly set to 3. If not, what driver was used to create the data?
You could use the following js function in front of your query like so:
function LUUID(uuid) {
var hex = uuid.replace(/[{}-]/g, ""); // removes extra characters
return new UUID(hex); //creates new UUID
}
db.person.find({"_id" : LUUID("E3E45566-AFE4-A564-7876-AEFF6745FF"});
You could save the function in .js file and load it or open it before you make your query and if you copy the value from your results you should rename the function with:
LUUID for Legacy UUID
JUUID for Java encoding
NUUID for .net encoding
CSUUID for c# encoding
PYUUID for python encoding
I know it's an old issue, but without any additional needs you can use this one:
find({_id:UUID('af64ab4f-1098-458a-a0a3-f0f6c93530b7')})
You can fix this issue by using split() and join() workaround:
for instance if I use "E3E45566-AFE4-A564-7876-AEFF6745FF" hex value with - inside UUID() function, it does not return BinData in mongo so please try removing all the - before passing to UUID function.
db.person.find({"_id":UUID("E3E45566-AFE4-A564-7876-AEFF6745FF".split("-").join(''))});
Or by defining a variable to do it in multiple line:
var uuid = UUID("E3E45566-AFE4-A564-7876-AEFF6745FF".split("-").join(''))
db.person.find({"_id":uuid});
or by creating a simple function:
function BUUID(uuid){
var str = uuid.split("-").join('');
return new UUID(str);
}
db.person.find({"_id": BUUID("E3E45566-AFE4-A564-7876-AEFF6745FF")}).pretty();