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.
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
.
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.
Example 24-1 shows a simple map constructor that creates a map containing three entries.
xquery
version
"3.1"
;
map
{
"ACC"
:
"Accessories"
,
"WMN"
:
"Women's"
,
"MEN"
:
"Men's"
}
Figure 24-1 shows the syntax for 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.
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
}
}
map:entry
and map:merge
to create mapsThe 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.
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
))
)
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.
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
}
};
map:get
functionOne 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
.
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.
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.
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"
/>
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.
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.
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
.
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.
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.
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")
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.
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.
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.
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
.
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
.
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.
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.
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.
xquery
version
"3.1"
;
declare
variable
$
array-of-ints
:=
[
10
,
20
,
30
];
declare
variable
$
array-of-arrays
:=
[
[
"a"
,
"b"
,
"c"
],
[
"d"
,
"e"
,
"f"
]
];
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.
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.
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
.
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]
.
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.
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.
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.
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.
JSON construct | XQuery equivalent |
---|---|
Object | Map, with an entry for each name/value pair |
Array | Array |
String | Atomic xs:string value |
Number | Atomic xs:double value |
true token | Atomic xs:boolean value true |
false token | Atomic xs:boolean value false |
null token | Empty sequence |
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"))
.
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.
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
.
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.
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
.
JSON construct | XML element name | Contents/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::
.