Passing a sequence to xdmp:eval()

Author: Dave Cassel  |  Category: Software Development

When using MarkLogic Server, you’ll sometimes need to execute a command against a different database than the one associated with your application. Enter the xdmp:eval() function. eval() lets you execute any code you can put into a string. Among its several options is <database/>, allowing you to execute a command against a different database.

As an example, suppose you want to create a new role. This function must be executed against the security database. We can do this with the eval function:

let $cmd := fn:concat(
    'import module namespace sec="http://marklogic.com/xdmp/security" at
      "/MarkLogic/security.xqy";',
    'sec:create-role("my-role", "This is my new role", (), (), ())'
  )
return xdmp:eval($cmd, (),
  <options xmlns="xdmp:eval">
    <database>{xdmp:security-database()}</database>
  </options>)

This will create a simple (albeit not very interesting) role.

But let’s suppose that we want to do something a little more interesting. Suppose we’re writing a script that will initialize the database for an application, and that as part of that initialization, you want to set up some roles. For each role, you want to pass in a sequence of default collections. There are a few ways to do it. First, let’s review xdmp:eval()’s mechanism for passing in variable information:

let $collections := ("http://marklogic.com/abc", "http://marklogic.com/def")
let $cmd := fn:concat(
    'import module namespace sec="http://marklogic.com/xdmp/security" at
       "/MarkLogic/security.xqy";
     declare variable $role external;
     declare variable $desc external;
     sec:create-role($role, $desc, (), (), ())'
)
return xdmp:eval($cmd, (xs:QName("role"), "my-role", xs:QName("desc"), "This is my new role"),
  <options xmlns="xdmp:eval">
    <database>{xdmp:security-database()}</database>
  </options>
)

Here we pass the role and description in to the eval() expression. We could, of course, just build the string with the values embedded. Why not do that? A couple of reasons:

  1. Reuse: if you’re going to execute this command a few times, it’s better to build the string once, then pass in the parts that change;
  2. Security: in some cases, embedding values directly into the command string could expose you to the XQuery injection problem, analogous to SQL injection. IBM has a write up of XPath injection. Disclaimer: I know that I need to read up more on XQuery injection to understand when it can bite you, and how the passing variable technique protects you. Maybe a future post. The usual case to worry about is when you use input from the user.
  3. Readability: okay, this is subjective, but I think your code will be easier to read if the command string just declares the external variables and isn’t broken up with sticking values in. This may depend on whether you’re putting in a scalar or a sequence. In our example, we’ll do a sequence.

As you can see, the 2nd parameter to eval() is a sequence of variable names and values. Here’s the trick: I want to get my sequence of collection strings into the mix. But if I put a sequence into a sequence, it will get flattened. So I can’t simply add “xs:QName(‘collections’), $collections”, because eval() will complain that the 2nd collection in the list isn’t a QName. Here’s how we can handle that:

let $collections := ("http://marklogic.com/abc", "http://marklogic.com/def")
let $cmd := fn:concat(
    'import module namespace sec="http://marklogic.com/xdmp/security" at
       "/MarkLogic/security.xqy";
     declare variable $role external;
     declare variable $desc external;
     declare variable $collections external;
     sec:create-role($role, $desc, (), (), fn:tokenize($collections, "~"))'
)
return xdmp:eval($cmd,
  (xs:QName("role"), "my-role",
   xs:QName("desc"), "This is my new role",
   xs:QName("collections"), fn:string-join($collections, "~")),
  <options xmlns="xdmp:eval">
    <database>{xdmp:security-database()}</database>
  </options>
)

In order to pass a sequence to eval(), we have to make it something other than a sequence. What I did here was join the collection names with a character that I’m sure isn’t being used in the collection names, then split them again inside the eval().

Another technique that was suggested to me was to build some XML around the sequence values, then use XPath to break them down within the eval(), but my testing found strings to be both simpler and more concise to write and faster to run.

Tags: ,

One Response to “Passing a sequence to xdmp:eval()”

  1. Geert Says:

    If you can’t pass your sequence through as joined-string, you can also put things in a map:map, and pass that through. You can use the same map:map for multiple sequence arguments. Another new option is json arrays, which came available with one of latest MarkLogic versions.

    Both these options are type safe, no need for explicit type casting.

    Kind regards,
    Geert

Leave a Reply