Chapter 24. Maps, Arrays, and JSON

This chapter covers maps and arrays, which are new to the data model in version 3.1. It describes how to construct, manipulate, and query them. Because the support for maps and arrays fits neatly with JSON objects and arrays, this chapter also describes support for parsing and serializing JSON.

Maps

A map is a collection of key/value pairs that can be constructed, manipulated, and queried in XQuery 3.1. Each key/value pair is known as an entry. Within a map, each key is unique and the order of the entries has no particular significance.

Maps are full-fledged items in the XQuery data model and can be constructed using map constructors. Special operators are provided for looking up values in maps, and they can also be queried and manipulated by a number of built-in functions, many of which are in the namespace http://www.w3.org/2005/xpath-functions/map, which is typically bound to the prefix map.

Constructing Maps

Maps can be constructed in queries by using either a map constructor or a built-in function, as described in this section. They can also be created by parsing a JSON object, as described later in the chapter.

Using a map constructor

Example 24-1 shows a simple map constructor that creates a map containing three entries.

Example 24-1. A simple map constructor
xquery version "3.1";
map {
  "ACC": "Accessories",
  "WMN": "Women's",
  "MEN": "Men's"
}

Figure 24-1 shows the syntax for a map constructor.

Figure 24-1. Syntax of a map constructor

The map constructor contains a map keyword followed by curly braces that surround zero or more pairs of expressions that identify the key and the value. The key and value expressions are separated by a colon, and the entries are separated by commas.

Before the colon, the key expression can be any XQuery expression that results in a single atomic value, after atomization. In Example 24-1, the key expressions are just string literals, but it could be a more complex expression, such as a function call or a path expression. The keys can have any atomic data type, and it is even possible (but fairly unusual) to have keys in the same map that have a variety of data types. Because the keys must be unique, a map cannot contain two key values that are equal when taking into account their data types, or error XQDY0137 is raised.

After the colon, the value expression can be any XQuery expression and there is no limitation that it must return an atomic value. The value associated with a key can be a node, an array, a sequence of multiple items, or even another map. Example 24-2 shows a map constructor where the map values are nested maps.

Example 24-2. Nested maps
xquery version "3.1";
map {
    "ACC": map {
              "name": "Accessories",
              "code": 300 },
    "WMN": map {
              "name": "Women's",
              "code": 310 },
    "MEN": map {
              "name": "Men's",
              "code": 320 } 
    }

Using map:entry and map:merge to create maps

The map constructor syntax works well for a map whose size and basic structure is known at the time the query is written. However, sometimes it is necessary to create a map with a variable number of entries, based, for example, on an input document. For this use case, two built-in functions come in handy: map:entry and map:merge. The map:entry function is used to create a map with a single entry, given a key and value. The map:merge function can be used to merge the maps created by map:entry into a single map. Example 24-3 creates a single map with four entries, one per product, where the key is the product number and the value is the product name.

Example 24-3. Creating maps with map:entry and map:merge
xquery version "3.1";
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
map:merge(for $p in doc("catalog.xml")//product
          return map:entry(string($p/number), string($p/name)) )

Looking Up Map Values

Given a map, there are several ways to look up the value for a particular key. For the purposes of this section, assume that the global variables shown in Example 24-4 are declared.

Example 24-4. Map variables used in examples
xquery version "3.1";
declare variable $deptnames := map {
  "ACC": "Accessories",
  "WMN": "Women's",
  "MEN": "Men's"
};
declare variable $deptinfo := map {
  "ACC": map {
            "deptname": "Accessories",
            "deptnum": 300 },
  "WMN": map {
            "deptname": "Women's",
            "deptnum": 310 },
  "MEN": map {
            "deptname": "Men's",
            "deptnum": 320 } 
};

Using the map:get function

One straightforward way to look up the value in a map is to use the built-in map:get function. This function takes as arguments the map and a key, and it returns the value associated with that key. If that key is not in the map, it returns the empty sequence. For example, map:get($deptnames, "ACC") returns the string Accessories.

Invoking maps as functions

In the XQuery data model, a map is actually a specific subtype of function, one which accepts a key and returns the value associated with that key. Any given map can be thought of as an anonymous function that has one parameter (the key), whose sequence type is xs:anyAtomicType. The return type of this function is the generic item()*, since any value can be associated with a key in a map.

To retrieve a value from a map, you can treat it like a function, passing the key value of interest to the function. To do this, you could bind the map to a variable, and treat that variable name like the function name. For example, $deptnames("ACC") will return the string Accessories. This is similar to calling functions dynamically, described in “Constructing Functions and Calling Them Dynamically”.

Of course, the parameter is not required to be a literal value. In Example 24-5, the query refers to the $deptnames map in a FLWOR expression, passing the dept attribute to the map function.

Example 24-5. Looking up a map value with a dynamic parameter

Query

xquery version "3.1";
declare variable $deptnames := map {
  "ACC": "Accessories",
  "WMN": "Women's",
  "MEN": "Men's"
};
for $prod in doc("catalog.xml")//product
return <product num="{$prod/number}" 
                dept-name="{$deptnames($prod/@dept)}"/>

Results

<product num="557" dept-name="Women's"/>
<product num="563" dept-name="Accessories"/>
<product num="443" dept-name="Accessories"/>
<product num="784" dept-name="Men's"/>

When treating the map like a function, normal function rules apply. In the example shown, the value passed as a parameter is an attribute node, but it is atomized into xs:anyAtomicType according to function conversion rules. Since the type of the parameter is xs:anyAtomicType, the empty sequence cannot be the argument, so the example would raise an error if there were a product element with no dept attribute. It is not an error to pass a key that is not in the map; in that case, like the map:get function, it will return the empty sequence.

You can chain function calls together if a map contains another map as a value. Example 24-6 chains lookups to get down two levels into the nested map $deptinfo. To fill in the dept-name attribute, it gets the map function associated with that particular department by using the expression $deptinfo($prod/@dept). It immediately follows that with a call to that retrieved map function by using the expression ("deptname"), which looks up the value for the deptname key in that second-level map.

Example 24-6. Chaining map function calls

Query

xquery version "3.1";
declare variable $deptinfo := map {
  "ACC": map {
            "deptname": "Accessories",
            "deptnum": 300 },
  "WMN": map {
            "deptname": "Women's",
            "deptnum": 310 },
  "MEN": map {
            "deptname": "Men's",
            "deptnum": 320 } 
};
for $prod in doc("catalog.xml")//product
return <product num="{$prod/number}"
                dept-name="{$deptinfo($prod/@dept)("deptname")}"
                dept-code="{$deptinfo($prod/@dept)("deptnum")}"/>

Results

<product num="557" dept-name="Women's" dept-code="310"/>
<product num="563" dept-name="Accessories" dept-code="300"/>
<product num="443" dept-name="Accessories" dept-code="300"/>
<product num="784" dept-name="Men's" dept-code="320"/>

Using the lookup operator on maps

The lookup operator is a more terse syntax for looking up values in a map. It uses a question mark (?) followed by an expression that evaluates to zero or more keys. For example:

$deptnames?("ACC")

retrieves the string Accessories. Multiple keys can be provided, so:

$deptnames?("ACC", "MEN")

retrieves a sequence of the strings Accessories and Men's. This is the equivalent of the expression:

for $d in ("ACC", "MEN") return $deptnames($d)

The expression in parentheses can be any XQuery expression, so for example:

$deptnames?(doc("catalog.xml")//@dept)

returns all the values in $deptnames that have a key equivalent to a value in a dept attribute. It would actually return the string Accessories twice because the value ACC appears twice in the results of the parenthesized expression.

There are several other syntax options for the lookup operator. Instead of a parenthesized expression, it is possible to use a literal integer, which works if a map key is an integer. For example, if a map were bound to a variable as follows:

declare variable $map-with-integer-keys := map{ 10:"a", 20:"b"};

The expression $map-with-integer-keys?20 returns the string b.

It is also possible to specify a name (without quotes) after the question mark, in which case it is treated like a key that is a string. For example:

$deptnames?ACC

is equivalent to $deptnames?("ACC"), which retrieves the string Accessories.

Finally, it is possible to use an asterisk (*) as a wildcard to retrieve a sequence containing every value in a map. For example, $deptnames?* will return a sequence of three strings: ("Accessories", "Women's", "Men's"). Because the order of entries in a map is not significant, the implementation may return these in any order.

You can chain lookups together if a map contains another map as a value. Example 24-7 is equivalent to Example 24-6 except that it uses the lookup operator instead of function calls.

Example 24-7. Chaining map lookups
xquery version "3.1";
declare variable $deptinfo := map {
  "ACC": map {
            "deptname": "Accessories",
            "deptnum": 300 },
  "WMN": map {
            "deptname": "Women's",
            "deptnum": 310 },
  "MEN": map {
            "deptname": "Men's",
            "deptnum": 320 } 
};
for $prod in doc("catalog.xml")//product
return <product num="{$prod/number}"
                dept-name="{$deptinfo?($prod/@dept)?deptname}"
                dept-code="{$deptinfo?($prod/@dept)?deptnum}"/>

In all of the examples so far in this section, we have set the context item for the lookup operator directly before the question mark. For example, in the expression $deptnames?ACC, the map bound to $deptnames is established as the context item for the lookup. This is known as a postfix lookup. It is also possible to use the lookup operator without immediately preceding it with the context expression, in cases where the context item is already a map or array. This is known as a unary lookup, and can be useful inside a predicate, or after a simple map operator. For example:

$deptinfo?*[?deptname = "Accessories"]

In this case, the expression $deptinfo?* uses a wildcard to get all the values in the $deptinfo map, which happen to all be maps themselves. The predicate [?deptname = "Accessories"] is applied to that sequence of maps in order to only select the map whose key deptname is equal to Accessories. Inside the predicate, the unary lookup ?deptname applies to whatever map is the current context item. That query will return the entire map associated with the key ACC in the $deptinfo map. This could be taken further by chaining another lookup onto the expression, as in:

$deptinfo?*[?deptname = "Accessories"]?deptnum

This will return the integer value 300, because that is the value associated with the key deptnum in the ACC map.

Querying Maps

In addition to being able to look up values in a map based on keys, there are several built-in functions that allow you to query other aspects of maps.

map:size

Returns the number of entries in a map. For example, map:size($deptnames) returns the integer 3.

map:contains

Tests whether a map contains an entry with a particular key. For example, map:contains($deptnames, "ACC") returns true, and map:contains($deptnames, "FOO") returns false.

map:find

Recursively searches a sequence for map entries with a particular key and returns their values as an array. For example, map:find($deptinfo, "deptnum") returns [300, 310, 320].

map:keys

Returns a sequence of all the keys in the map, in no particular order. For example, map:keys($deptnames) returns the sequence of three strings ("ACC", "MEN", "WMN").

deep-equal

Compares the values of two items, which can include maps. If the two maps have the same number of entries with the same keys and values (regardless of the order of the entries), this function will return true.

Changing Maps

Built-in functions are provided to make changes to maps, for example, adding and removing entries, or merging entries. As with all XQuery functions, the original map(s) passed to these functions are unchanged; the “changed” map is an entirely new map returned by the function.

map:put

Adds an entry to a map, or replaces an entry if that key already exists in the map. For example, map:put($deptnames, "SHO", "Shoes") returns a map that is identical to $deptnames except that it has an additional entry whose key is SHO and whose value is Shoes. The function call map:put($deptnames, "ACC", "Other") returns a map that is identical to $deptnames except that the entry whose key is ACC has a value of Other instead of Accessories.

map:remove

Removes an entry from a map. For example, map:remove($deptnames, "ACC") returns a map that is identical to $deptnames except that it no longer has an entry whose key is ACC.

map:merge

Merges the entries in multiple maps. It was described earlier in the chapter as a way to create new variable-size maps in conjunction with the map:entry function. This function is also useful in general any time two or more maps need to be merged. For example, map:merge( ($deptnames, $deptnames2) ) returns a union of all the entries in $deptnames and $deptnames2. A second $options parameter can be used to specify what to do if there are duplicate keys in the maps.

Iterating over Entries in a Map

The map:for-each function is useful if you want to iterate over all the entries in a map, and perform the same function on each entry. The first argument is the map itself, and the second argument is the function. The provided function must accept two arguments, representing the key and the value. For example, if you wanted to create a sequence of strings listing all the key/value pairs in a map, you could use the query shown in Example 24-8.

Example 24-8. Using map:for-each

Query

xquery version "3.1";
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare variable $deptnames := map {
  "ACC": "Accessories",
  "WMN": "Women's",
  "MEN": "Men's"
};
let $f := function($k, $v) 
  {concat('Key: ', $k, ', value: ', $v)}
return map:for-each($deptnames, $f)

Results

("Key: ACC, value: Accessories",
 "Key: WMN, value: Women's",
 "Key: MEN, value: Men's")

Maps and Sequence Types

Sequence types can be used to describe maps. This is useful if, for example, you would like to write a user-defined function that accepts a map as a parameter. The syntax for a map test is shown in Figure 24-2.

Figure 24-2. Syntax of a map test

The generic test map(*) can be used to specify any map, regardless of the types of the keys or values. A more specific sequence type specifies the types of the keys and values allowed in the map. For example, map(xs:integer, xs:string) matches a map where every key matches the sequence type xs:integer and every value matches the sequence type xs:string. For the type of the key, the only allowed sequence type is the name of an atomic or union type, because keys must be atomic values. For the type of the entry, any sequence type can be used, including the very generic item()*. The sequence type could even be another map test. For example, the $deptinfo map defined in Example 24-4 matches the map test:

map(xs:string, map(xs:string, xs:anyAtomicType))

because each value in the map is itself a map whose keys are strings and whose values are either strings or integers.

Sequence types, which are discussed in detail in “Sequence Types”, are used in a variety of XQuery expressions to indicate allowed values. The most common use is in the signatures of user-defined functions. For example, to write a function that takes a map and returns those keys that are greater than 50, you could use the function shown in Example 24-9. The use of the map(xs:integer, item()*) sequence type in the function signature ensures that only a single map whose keys are all integers are passed to this function as the $maparg argument.

Example 24-9. Function that takes a map as an argument

Query

xquery version "3.1";
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare function local:large-keys
  ($maparg as map(xs:integer, item()*)) as xs:integer* {
   map:keys($maparg)[. > 50]
};
local:large-keys(map {10:"a", 55:"b", 60:"c"})

Results

(55, 60)

Because a map is a special kind of function, it will also match function tests whose syntax is shown in Figure 23-1. It will match the generic function test function(*) as well as a more specific one that matches the function signature of a map, namely function(xs:anyAtomicType) as item()*. This means that a map can be passed as an argument to higher-order functions that expect a function item as one of their arguments. A map will also match the generic item() sequence type.

Arrays

An array is simply an ordered list of values. The values in an array are called its members, and they can be retrieved based on their position number.

Like maps, arrays are full-fledged items in the XQuery data model and can be constructed using array constructors. Special operators are provided for looking up values in array, and they can also be queried and manipulated by a number of built-in functions, many of which are in the namespace http://www.w3.org/2005/xpath-functions/array, which is typically bound to the prefix array.

Constructing Arrays

An array constructor is used to create an array. There are two distinct varieties of array constructors: the square array constructor and the curly array constructor. For example, a simple square array constructor that creates an array containing three entries looks like this:

[ "a", "b", "c" ]

It simply consists of square brackets surrounding zero or more expressions that are separated by commas. An equivalent curly array constructor looks like this:

array { "a", "b", "c" }

It uses the keyword array followed by curly braces surrounding the expressions. It is important to note that the curly array constructor differs from the square array constructor in the way it determines the members. With a square array constructor, the comma is considered a hard delimiter between members, where each expression returns the value for a single member. The following square array constructor creates an array with two members:

[("a", "b"), "c"]

The first is a sequence of the strings a and b, and the second is the string c. By contrast, the following curly array constructor creates an array with three members (the strings a, b, and c):

array { ("a", "b"), "c" }

The difference is that the expression within curly braces is resolved into a flat sequence of three items first, and then a member is created for each item.

The previous examples use strings as array values, but any expression is valid, so you can have an array of values of other atomic types, or of nodes, function items, maps, or other arrays. There is no requirement that all the members of the array have the same type, so you could create a mixture, for example:

[$myitems, doc("catalog.xml")//product, 
  12, xs:date('2015-01-15'), <foo>bar</foo>]

Using either syntax, you can create an array that contains an array, as in:

array { ["a", "b"], "c" }

or:

[ ["a", "b"], "c" ]

In both of these cases, the outer array has two members: an array, and the string c.

Arrays Versus Sequences

In some ways, it may seem that arrays and sequences are the same thing: they are both ordered lists of items. The main difference is that sequences of items are automatically flattened in XQuery. There is no such thing as sequences of sequences in the data model. Arrays, by contrast, can have members that are other arrays, or sequences of multiple items. For example, the following array constructor creates an array with two members, the first is a sequence of three strings and the second is the string d:

[ ("a", "b", "c"), "d" ]

The items are not flattened to create an array with four members. In contrast, suppose you construct a sequence using similar syntax, but with parentheses instead of square brackets, as in:

( ("a", "b", "c"), "d" )

In this case, the inner sequence is flattened so you end up with a sequence of four strings. However, an array within a sequence is retained, for example in the following:

( ["a", "b", "c"], "d" )

The result is a sequence of two items, the first being an array with three members, and the second being the string d.

Some XQuery expressions that iterate over sequences will give surprising results when used with arrays, because arrays are considered items whereas sequences are not. For example:

for $x in (1,2) return $x + 1

will return a sequence of integers (2,3), but:

for $x in [1,2] return $x + 1

will raise a type error because $x is bound to the entire array, not each member in turn. The return clause, which is evaluated only once, is trying to add 1 to the entire array. Similarly, count([1,2]) will return 1 because the argument is a single item, the array itself. (You can use the array:size function instead.) This also means that positional predicates do not work as expected on arrays. For example:

let $a := [1,2] return $a[1]

will return the entire array, not the first member.

Arrays and Atomization

During atomization, an array is flattened and converted into a sequence of the atomized values of its members. This is most common when calling functions that expect atomic values. For example, it is possible to pass an array to the distinct-values function, as in:

distinct-values( [1,2,3,1] )

This will return the distinct values of the members of the array. The flattening process is recursive, so the following will work as well:

distinct-values( [1,2, [3,1] ] )

returning the sequence (1,2,3). Similarly, sum( [1,2] ) will work as you might expect (returning the value 3) because the sum function expects a sequence of atomic values as its argument, so the array is atomized into a sequence of integers 1 and 2. This works differently from the count example in the previous section because the count function accepts a sequence of any items, not just atomic values, so no atomization takes place.

Looking Up Array Values

Given an array, there are several ways to retrieve the member in a particular position (index) in the array. Using all of these methods, specifying a position that is larger than the number of members in the array will raise error FOAY0001. In XQuery, the first member is in position 1, not 0 as in some programming languages. For the purposes of this section, assume that the global variables shown in Example 24-10 are declared.

Example 24-10. Array variables used in examples
xquery version "3.1";
declare variable $array-of-ints := [10, 20, 30];
declare variable $array-of-arrays := [ ["a", "b", "c"], ["d", "e", "f"] ];

Using the array:get function

One way to get a value from an array is to use the built-in array:get function. This function takes as arguments the array and an index, and it returns the value in that position. For example, array:get($array-of-ints, 2) returns the integer 20.

Invoking arrays as functions

In the XQuery data model, an array is actually a specific subtype of function, one which accepts an index and returns the member in that position. Any given array can be thought of as an anonymous function that has one parameter (representing the index), whose sequence type is xs:integer. The return type of this function is the generic item()*, since any value can be a member of an array.

To retrieve a member from an array, you can treat it like a function, passing the index to the function. To do this, you could assign the array to a variable, and treat that variable name like the function name. For example, $array-of-ints(2) will return the integer 20. This is similar to calling functions dynamically, described in “Constructing Functions and Calling Them Dynamically”. As with any function, the parameter does not have to be a literal; it could be, for example, a variable as in $array-of-ints($pos), assuming a $pos variable has been declared and it is of type xs:integer or a derived type.

When treating the array like a function, normal function rules apply. In the previous example, if $pos were bound to an untyped node, that node would be atomized and the value cast to xs:integer according to normal function conversion rules. Since the type of the parameter is xs:integer, the empty sequence cannot be the argument, so the example would raise an error if $pos were bound to the empty sequence.

You can chain function calls together if an array contains another array as a member. For example, the expression $array-of-arrays(2)(1) will return the string d. The expression $array-of-arrays(2) gets the second member of the array, which happens to itself be an array. It immediately follows that with a call to that retrieved function via the expression (1), which gets the first member in that second-level array.

Using the lookup operator on arrays

The lookup operator is a more terse syntax for retrieving members in an array. It uses a question mark (?) followed by an integer, or an expression that evaluates to zero or more integers. For example:

$array-of-ints?(2)

retrieves the integer 20. Multiple keys can be provided, so:

$array-of-ints?(2, 3)

retrieves a sequence of the integers 20 and 30. This is the equivalent of the expression:

for $i in (2, 3) return $array-of-ints($i)

The expression in parentheses can be any XQuery expression that returns a sequence of zero or more integers, so for example:

$array-of-ints?(1 to 2)

returns the first two members of the array.

There are two other syntax options for the lookup operator. Instead of a parenthesized expression, it is possible to use a literal integer. For example, the expression $array-of-ints?2 returns the integer 20.

You can use an asterisk (*) as a wildcard to retrieve a sequence containing every member in the array. For example, $array-of-ints?* will return a sequence of three integers (10, 20, 30).

You can chain lookups together if an array contains another array as a member. For example, $array-of-arrays?2?1 is the equivalent of $array-of-arrays(2)(1) and returns the same value (the string d).

In all the examples so far in this section, we have set the context item for the lookup operator directly before the question mark. For example, in the expression $array-of-ints?2, the array bound to $array-of-ints is established as the context item for the lookup. As with maps, it is also possible to use the lookup operator without immediately preceding it with the context expression, in cases where the context item is already a map or array. For example $array-of-arrays?*[?2 = "b"] will return the members of $array-of-arrays that are themselves arrays whose second member is b, namely the first member of $array-of-arrays, the array ["a", "b", "c"].

In this case, the expression $array-of-arrays?* uses a wildcard to get all the members in the $array-of-arrays array, which happen to all be arrays themselves. The predicate [?2 = "b"] is applied to that sequence of arrays in order to only select the array whose second member is equal to "b". Inside the predicate, the unary lookup ?2 applies to whatever array is the current context item. If any of the members of $array-of-arrays is not an array (or map with integer keys), or is an array with fewer than two items, an error will be raised.

Querying Arrays

In addition to being able to retrieve a value in an array based on its position, there are several built-in functions that allow you to query other aspects of arrays.

array:size

Returns the number of members in an array. For example, array:size($array-of-ints) returns the integer 3.

array:head

Returns the first member of an array. For example, array:head($array-of-ints) returns the integer 10.

array:tail

Returns an array without its first member. For example, array:tail($array-of-ints) returns the array [20, 30].

deep-equal

Compares two items, which can include arrays. If the two arrays have the same number of members with equal values (taking into account their data types), in the same order, this function will return true.

Changing Arrays

Built-in functions are provided to make changes to arrays, for example adding and removing members, or joining arrays. As with all XQuery functions, the original array(s) passed to these functions are unchanged; the “changed” array is an entirely new array returned by the function.

array:append

Adds one member to the end of the array. For example, array:append($array-of-ints, 40) returns the array [10, 20, 30, 40].

array:insert-before

Inserts one member into a specified position in an array. For example, array:insert-before($array-of-ints, 2, 40) returns the array [10, 40, 20, 30].

array:put

Replaces one member at a specified position in an array. For example, array:put($array-of-ints, 2, 40) returns the array [10, 40, 30].

array:remove

Removes a member from an array. For example, array:remove($array-of-ints, 2) returns the array [10, 30].

array:subarray

Returns a subset of an array based on a starting position and length. For example, array:subarray($array-of-ints, 2, 2) returns two members from $array-of-ints, starting with position 2, so the result is the array [20, 30].

array:filter

Applies a function to each member in an array and returns an array containing those members for which the function returns true. For example, array:filter($array-of-ints, function($n) {$n > 15}) returns [20, 30] because those are the members of $array-of-ints that are greater than 15.

array:flatten

Turns arrays into sequences of items, recursively flattening any arrays that are within arrays. For example, array:flatten($array-of-arrays) returns a sequence of six strings: "a", "b", "c", "d", "e", "f".

array:join

Merges the members in multiple arrays into a single array, retaining the order. For example, array:join( ($array-of-ints, ["a", "b", "c"]) ) returns an array with six members: [10, 20, 30, "a", "b", "c"].

array:sort

Sorts the members of an array, optionally allowing you to pass it a function that determines the sort key. For example, array:sort([6, 2, -4], (), abs#1) returns [2, -4, 6] because the members are sorted on the result of the abs (absolute value) function.

array:reverse

Reverses the order of members of an array. For example, array:reverse($array-of-ints) returns [30, 20, 10].

Arrays and Sequence Types

Sequence types can be used to describe arrays. This is useful if, for example, you would like to write a user-defined function that accepts an array as a parameter. The syntax for an array test is shown in Figure 24-3.

Figure 24-3. Syntax of an array test

The generic test array(*) can be used to specify any array, regardless of the types of the members. A more specific sequence type specifies the types of the members allowed in the array. For example, array(xs:integer) matches an array where every member matches the sequence type xs:integer. Any sequence type is allowed, so it could, for example, match a sequence of multiple nodes, as in array(node()*), where each member can be the empty sequence or one or more nodes of any kind. The sequence type could even be another array test. For example, the $array-of-arrays array defined in Example 24-10 matches the array test array(array(xs:string)) because each member in the array is itself an array whose members are strings.

Sequence types, which are discussed in detail in “Sequence Types”, are used in a variety of XQuery expressions to indicate allowed values. The most common use is in the signatures of user-defined functions. For example, to write a function that takes an array and returns those members that are greater than 50 as a sequence, you could use the function shown in Example 24-11. The use of the array(xs:integer) sequence type in the function signature ensures that only a single array whose members are all integers are passed to this function as the $arrayarg argument.

Example 24-11. Function that takes an array as an argument
xquery version "3.1";
declare namespace array = "http://www.w3.org/2005/xpath-functions/array";
declare function local:larger-values
  ($arrayarg as array(xs:integer))as xs:integer* {
   array:flatten($arrayarg)[. > 15]
};
local:larger-values([10, 20, 30])

Because an array is a special kind of function, it will also match function tests whose syntax is shown in Figure 23-1. It will match the generic function test function(*) as well as a more specific one that matches the function signature of an array, namely function(xs:integer) as item()*. This means that an array can be passed as an argument to higher-order functions that expect a function item as one of their arguments. An array will also match the generic item() sequence type.

JSON

Although maps and arrays are useful on their own as programming constructs, one of the important reasons for adding them to the XQuery specification was to support the querying of JSON documents, and the ability to return results as JSON.

XQuery maps correspond nicely to JSON objects in that they contain name/value pairs that have to have unique keys/names, their order doesn’t matter, and they can have complex structures. Table 24-1 provides a general mapping between JSON and the XQuery data model.

Table 24-1. Mapping from JSON syntax to the XQuery data model
JSON constructXQuery equivalent
ObjectMap, with an entry for each name/value pair
ArrayArray
StringAtomic xs:string value
NumberAtomic xs:double value
true tokenAtomic xs:boolean value true
false tokenAtomic xs:boolean value false
null tokenEmpty sequence

Parsing JSON

Using XQuery, it is possible to parse a JSON document to retrieve an instance of the XQuery data model that can then be queried and manipulated. This is accomplished using the json-doc function, which parses a document associated with the supplied URI, and returns it as a sequence of items. Typically those items will be maps, but they could also be, for example, strings or arrays. The mapping shown in Table 24-1 tells you what you can expect as output of this function.

Given the following input document called product.json:

{
   "number": 557,
   "name": "Fleece Pullover",
   "colorChoices": ["navy", "black"],
   "is-current": true,
   "other": null
}

The function call json-doc("product.json") will return a map equivalent to the document, as in:

map {
   "number": xs:double(557),
   "name": "Fleece Pullover",
   "colorChoices": ["navy", "black"],
   "is-current": true(),
   "other": ()
}

Another function, parse-json, will convert a string that is in JSON syntax into a sequence of items. It generally has the same behavior as json-doc. In fact, the function call json-doc("uri") is the same as the function call parse-json(unparsed-text("uri")).

Serializing JSON

You can serialize the results of your query as a JSON document by choosing the serialization method json. Generally, the mapping is the same as shown in Table 24-1, in the opposite direction. For example, map items are serialized as JSON objects, and boolean values are serialized using the JSON tokens true and false.

However, you may have some items to serialize that are not easily mapped to the JSON model. When you attempt to serialize XML nodes as JSON, they are turned into strings. By default, the string will look like an XML document, but you can use the json-node-output-method serialization parameter to make it look like text or HTML.

In Example 24-12, the map has an entry, properties, whose value is an XML element. When serializing this node to JSON, it is turned into a string as shown by the quotes around the props element in the result.

Example 24-12. Serializing an XML element as JSON

Query

xquery version "3.1";
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization"; 
declare option output:method "json";
declare option output:indent "yes";
map {
   "number": 557,
   "props":
     <props>
       <length>31</length>
     </props>
}

Results

{
   "number": 557,
   "props":
     "<props>
  <length>31</length>
</props>"
}

Some instances of the XQuery data model cannot be serialized as JSON at all. One of these is a sequence of multiple items, which will raise error SERE0023. This error can be avoided by turning the sequence into an array before serialization. Function items that are not maps or arrays also cannot be serialized as JSON; attempting to do so will raise error SERE0021.

Converting Between JSON and XML

Two functions, json-to-xml and xml-to-json, allow conversion between JSON and an XML structure that is analogous to JSON, with elements like map and string. Example 24-13 shows a JSON document and a corresponding XML document.

Example 24-13. Comparing JSON to XML

JSON

{   "number": 557,
   "name": "Fleece Pullover",
   "colorChoices": ["navy", "black"],
   "is-current": true,
   "other": null,
   "priceInfo": {
       "price": 19.99,
       "discount": 10.00
       }
}

Equivalent XML

<map xmlns="http://www.w3.org/2005/xpath-functions">
   <number key="number">557</number>
   <string key="name">Fleece Pullover</string>
   <array key="colorChoices">
      <string>navy</string>
      <string>black</string>
   </array>
   <boolean key="is-current">true</boolean>
   <null key="other"/>
   <map key="priceInfo">
      <number key="price">19.99</number>
      <number key="discount">10.00</number>
   </map>
</map>

json-to-xml will take JSON, as a string, and create the equivalent XML shown in the example. xml-to-json will do the opposite: convert the XML shown into the JSON string.

Each JSON construct is represented by a different element name. Table 24-2 provides a list of these elements and their allowed content. For name/value pairs within an object, a key attribute is used to hold the name. The XML elements are in the namespace http://www.w3.org/2005/xpath-functions.

Table 24-2. Mapping from JSON syntax to XML elements used by json-to-xml and xml-to-json
JSON constructXML element nameContents/type of XML element
Object map Any number of any of the elements listed, each with a key attribute containing the name part of the name/value pair
Array array Any number of any of the elements listed
String string xs:string
Number number xs:double
true, false tokens boolean xs:boolean
null token null Empty content

Two additional attributes, related to escaping, are allowed in the XML. The escaped attribute is used on a string element if the contents of the string are JSON-escaped. The escaped-key attribute appears on any element that has a key attribute, to indicate that the value of the key attribute is JSON-escaped.

Converting JSON to this XML structure by using json-to-xml is preferable to converting it to a map (as is done by the parse-json function) in cases where sophisticated queries need to be performed on that data. If you just need to retrieve the price for a particular product, a map will work fine, because you can chain together map lookups to retrieve the necessary data.

However, sometimes it is useful to have the full power of XPath to query the data. If the JSON structure is flexible, you may need to look for price values at any level in the hierarchy, using // in your path expression. Another case is when you need to use other axes, like parent:: or preceding-sibling::.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.174.195