How to write a while loop in MarkLogic XQuery - xquery

Is there any recognized idiom for writing the equivalent of a while loop in MarkLogic XQuery? I know that I could write a tail-recursive routine, but MarkLogic XQuery does not optimize tail-recursion and I'm getting a stack overflow (I have to go around my loop ~20000 times).
Editorial note: as of MarkLogic 6, tail recursion is optimized in MarkLogic.

Recursion is the usual way. Another is to use a FLWOR with a try-catch and throw an exception with a known code to exit early.
try {
for $x in 1 to count($tokens)
return tok($x) }
catch ($ex) {
if ($x/error:code eq 'BREAK') then ()
else xdmp:rethrow() }
The tok function would call error((), 'BREAK') to exit the parent FLWOR expression. If needed you could multiply the token count by some factor, or use an arbitrary large number.
https://github.com/robwhitby/xray/blob/coverage/src/coverage.xqy has a more complex example, in the cover:actual-via-debug function.

The usual way in XQuery is by using a FLWOR expression. How to write this highly depends on your loop and data as it is not an excact mappoing to a while loop known from other languages.
So what you can do is e.g.
let $c := doc('my.doc')/root/get-all-my-elements-using-xpath
return process-result($c)
Please be more specific what you actually want to do if you need further advice.

Related

How to include multiple function calls or assignments in an if-then statement

I am new to xquery and have a hard time finding more than just the basics online. I have existing code I want to change but I don't know how to include multiple function calls or variable assignments in a single if-then statement.
Say I have this block
if (fn:namespace-uri(.) = 'http://xmlns.oracle.com/communications/sce/dictionary/testNamespace'
and //tns:CreateSalesOrder/tns:ListOfSWIOrderIO/tns:SWIOrder/tns:OrderTypeCode/text() = 'Test Order'
and $isDefaultVersion)
then true()
else false()
How can I include a function call after true()?
Something like this but I dont know the exact syntax:
if (fn:namespace-uri(.) = 'http://xmlns.oracle.com/communications/sce/dictionary/testNamespace'
and //tns:CreateSalesOrder/tns:ListOfSWIOrderIO/tns:SWIOrder/tns:OrderTypeCode/text() = 'Test Order'
and $isDefaultVersion)
then true(), util:write-record('123','johndoe')
else false()
You can write:
if (...) then (true(), util:write-record('123','johndoe')) else ...
Note the parentheses.
I suspect from the name that util:write-record() is a function that has side-effects. Functions with side-effects are very tricky in XQuery and you need to understand how they are handled by your particular implementation. There is always a risk that the query optimizer will change the order of evaluation so side-effects happen in an unexpected order, or that particular calls will not happen at all because the optimizer decides the result is not needed.

Execute function in FLWOR without using 'let'

Let's say I create a map:
let $map := map:map()
How can I put something in that map without using let? Usually I have to do something like
let $map := map:map()
let $useless-var := map:put($map, $key, $value)
Seems strange that if I want to execute something and I don't care about the return value, I still have to store the result. What am I missing here?
Note: The important part is not map(), but the fact that I can't run a function without storing the result in some pointless variable.
One approach is to execute the functions as items in a sequence where only one item (typically, the first or last) in the sequence supplies the real value for an assignment or return, as in:
let $roundedX := (
math:floor($x),
local:function1(...),
local:function2(...)[false()],
...
)
...
return (
local:functionA(...),
local:functionB(...)[false()],
...,
$roundedX * 10
)
If the function returns a value that you want to throw away, just use a false predicate, as with two of the functions above.
Of course, this approach is only useful for functions with side effects.
Don't use a FLWOR unless you need it. In my opinion FLWOR expressions are somewhat overused. I often see expressions like:
let $a := current-time()
return $a
...when it would work just as well to write:
current-time()
See also: http://blakeley.com/blogofile/2012/03/19/let-free-style-and-streaming/
In MarkLogic 7 you can use the map constructor to generate maps recursively. I think this is probably what you want:
let $map := map:new(
(1 to 10) ! map:entry(., .)
)
Or you execute map:put as part of another sequence, or in the return statement before you return the map:
let $map := map:map()
let $not-useless-var := ...
return (map:put($map, string($not-useless-var), $not-useless-var), $map)
In plain XQuery (ignoring extensions like the XQuery scripting extension) there are no side-effects, so calling a function without using its return value is meaningless.
What you may be missing here is that map:put() returns a new map with an extra item added, it does not mutate the original map. So your $useless-var is not actually useless.
EDIT: Actually I'm not sure if MarkLogic's map:put() mutates the map. (If it does, that is really gross.) I was thinking of the proposed XQuery 3.1 maps (which I've used in BaseX) which definitely are immutable.
Since the focus of your question is about running a function without storing intermediate results, you might find the map operator (!) helpful:
local:build-sequence() ! local:do-something-to-each(.)
That's good for processing sequences. If you're thinking more about processing the result of something, the answer is likely in embracing the functional nature of XQuery:
local:produce-result(
local:build-parameter(),
local:retrieve-config()
)
Not sure exactly what you're looking for, but hopefully those help.

XQuery concat not outputting document XML in expected way

let $d := doc('foo.xml')
return concat('let $d := 

', $d)
returns
let $d :=
bar
I need it to return:
let $d :=
<foo>bar</foo>
Reading the function signature for fn:concat, there is no reason to expect it to output XML. From http://www.w3.org/TR/xpath-functions/#func-concat
fn:concat(
$arg1 as xs:anyAtomicType?,
$arg2 as xs:anyAtomicType?,
...)
as xs:string
That is, it takes a variable number of atomic items and returns a string. So if you pass it an XML node, it will attempt to atomize that and return a string result. If you haven't run into atomization yet, try string(doc($uri)) to see what happens.
Ignoring that, it looks like you're trying to build an XQuery expression using string manipulation - perhaps for use with xdmp:eval? That's fine, but don't pass in the XML using xdmp:quote. For correctness, performance, and security reasons, the right tool for this job is an external variable.
xdmp:eval('
declare variable $INPUT external ;
let $d := $INPUT
return xdmp:describe($d)',
(xs:QName('INPUT'), $d))
Better yet, write the string part as a separate XQuery module and xdmp:invoke it with the same external variable parameters.
Why do it this way? Correctness, efficiency and security. If you get into the habit of blindly evaluating strings, you are setting yourself up for problems. Passing a node reference is more efficient than quoting a large node. When you quote XML as a string you may end up with XQuery-significant characters, such as {. Then you have to escape them (or switch to using xdmp:unquote, but that makes it even less efficient). Any escaping will be error-prone. In SQL the classic way to handle this is with a bind variable, and with XQuery it is an external variable. As with bind variables in SQL, external variables handle escaping. This also makes injection attacks much more difficult.
Use pipes to concatenate.
Eg: $a||$b
If your variables are of atomic type, then you can use a string-join.
eg:, $a||fn:string-join(($b),",")
let $d := xdmp:quote(doc('foo.xml'))
return concat('let $d := 

', $d)

print out xquery sequence and exit

Is there a way to "die" in execution flow in an xquery file and output a nicely formatted printout of a sequence variable?
I'm trying something like:
return { fn:error(xs:QName("ERROR"), $xml) }
but that doesn't quite seem to work.
Thanks!
Based on your comment (you need it for debugging) I guess you are looking for the fn:trace function, described here http://www.xqueryfunctions.com/xq/fn_trace.html
If you want to abort the execution flow and output an error in your application you should in fact use the XQuery exception handling.
Try something like this, omitting the return if this isn't part of a FLWOR expression.
...
return fn:error((), "DEBUG", $xml)
There's no need for curly braces unless you're enclosing an expression, for example <x>{ current-time() }</x>. The return expression is not enclosed.
With MarkLogic it's best to leave the first parameter of fn:error empty. That way you don't have to worry about a QName, and anyway some folks believe that it's reserved for predefined errors. MarkLogic uses the second parameter to fill in error:code, and the third parameter for data.
For more on fn:error, see http://docs.marklogic.com/fn:error and https://github.com/robwhitby/xray/pull/11

how to build string iteratively in xquery

I need the xquery structure which is the same with java code
string temp
for(int i=0,i<string[].length,i++)
temp=temp+string[i]
for example, in xquery, I have string /a/b/c I need to something like
let $temp:=""
for $x in tokenize(string,'/')
$temp=concat($temp,$x)
return $temp
and it should return the following at each iterate
a
ab
abc
but somehow it seams that this statement $temp=concat($temp,$x) is not working. so what's the right syntax to do this? Thanks in advance
I think, you need to get the notion of declarative programming. You are trying to tell the processor what to do (like you would do in java) instead of describing the overall result. For example, if you don't use the scripting extension (which is only supported by some processors, e.g. zorba) you cannot use assignments the way you would use them in java. Think of it as the complete query describing one resulting document.
This stuff is hard to get in the beginning, but it brings huge benefits in the end (productivity, robustness, performance).
I would translate your imperative pseudo code into this one-liner:
string-join(tokenize("/a/b/c",'/'))
You can test it on try.zorba-xquery.com. I really hope this helps. Sorry, if this is not the answer you were looking for...
The $temp=conct($temp, $x) doesn't accumulate because in XQuery, that's a new variable each time through the loop. Try the following (tested in MarkLogic but uses all standard syntax):
declare function local:build($prefix, $tokens)
{
if (fn:exists($tokens)) then
let $str := fn:concat($prefix, $tokens[1])
return (
$str,
local:build($str, fn:subsequence($tokens, 2))
)
else ()
};
let $string := "/a/b/c"
return local:build("", fn:tokenize($string, "/"))

Resources