![]() XQuery from the Experts: A Guide to the W3C XML Query Language
In XQuery from the Experts, select members of the W3C's XML Query working group come together to discuss every facet of XQuery. From Jonathan Robie's introductory "XQuery: A Guided Tour" to Mary Mary Fernández, Jérôme Siméon, and Philip Wadler's "Introduction to the Formal Semantics," XQuery is revealed in a way that both novice programmers and industry experts can appreciate.
Edited by long-time XML expert and programmer Howard Katz, coverage ranges from strictly technical chapters to comparative essays such as Michael Kay's "XQuery, XPath, and XSLT," which explores the common ancestry of all three languages, and Don Chamberlin's "Influences on the Design of XQuery," which details the process behind XQuery's design. Click here to buy this book! |
XQuery: A Guided TourXML (Extensible Markup Language) is an extremely versatile data format that has been used to represent many different kinds of data, including web pages, web messages, books, business and accounting data, XML representations of relational database tables, programming interfaces, objects, financial transactions, chess games, vector graphics, multimedia presentations, credit applications, system logs, and textual variants in ancient Greek manuscripts.In addition, some systems offer XML views of non-XML data sources such as relational databases, allowing XML-based processing of data that is not physically represented as XML. An XML document can represent almost anything, and users of an XML query language expect it to perform useful queries on whatever they have stored in XML. Examples illustrating the variety of XML documents and queries that operate on them appear in [XQ-UC]. However complex the data stored in XML may be, the structure of XML itself is simple. An XML document is essentially an outline in which order and hierarchy are the two main structural units. XQuery is based on the structure of XML and leverages this structure to provide query capabilities for the same range of data that XML stores. To be more precise, XQuery is defined in terms of the XQuery 1.0 and XPath 2.0 Data Model [XQ-DM], which represents the parsed structure of an XML document as an ordered, labeled tree in which nodes have identity and may be associated with simple or complex types. XQuery can be used to query XML data that has no schema at all, or that is governed by a World Wide Web Consortium (W3C) XML Schema or by a Document Type Definition (DTD). Note that the data model used by XQuery is quite different from the classical relational model, which has no hierarchy, treats order as insignificant, and does not support identity. XQuery is a functional language—instead of executing commands as procedural languages do, every query is an expression to be evaluated, and expressions can be combined quite flexibly with other expressions to create new expressions. This chapter gives a high-level introduction to the XQuery language by presenting a series of examples, each of which illustrates an important feature of the language and shows how it is used in practice. Some of the examples are drawn from [XQ-UC]. We cover most of the language features of XQuery, but also focus on teaching the idioms used to solve specific kinds of problems with XQuery. We start with a discussion of the structure of XML documents as input and output to queries and then present basic operations on XML—locating nodes in XML structures using path expressions, constructing XML structures with element constructors, and combining and restructuring information from XML documents using FLWOR expressions, sorting, conditional expressions, and quantified expressions. After that, we explore operators and functions, discussing arithmetic operators, comparisons, some of the common functions in the XQuery function library, and how to write and call user-defined functions. Finally, we discuss how to import and use XML Schema types in queries. Many users will learn best if they have access to a working implementation of XQuery. Several good implementations can be downloaded for free from the Internet; a list of these appears on the W3C XML Query Working Group home page, which is found at http://www.w3.org/xml/Query.html. This chapter is based on the May 2003 Working Draft of the XQuery language. XQuery is still under development, and some aspects of the language discussed in this chapter may change. Sample Data: A BibliographyThis chapter uses bibliography data to illustrate the basic features of XQuery. The data used is taken from the XML Query Use Cases, Use Case "XMP," and originally appeared in [EXEMPLARS]. We have modified the data slightly to illustrate some of the points to be made. The data used appears in Listing 1.1.
<bib> <book year="1994"> <title>TCP/IP Illustrated</title> <author> <last>Stevens</last> <first>W.</first> </author> <publisher>Addison-Wesley</publisher> <price>65.95</price> </book> <book year="1992"> <title>Advanced Programming in the UNIX Environment</title> <author> <last>Stevens</last> <first>W.</first> </author> <publisher>Addison-Wesley</publisher> <price>65.95</price> </book> <book year="2000"> <title>Data on the Web</title> <author> <last>Abiteboul</last> <first>Serge</first> </author> <author> <last>Buneman</last> <first>Peter</first> </author> <author> <last>Suciu</last> <first>Dan</first> </author> <publisher>Morgan Kaufmann Publishers</publisher> <price>65.95</price> </book> <book year="1999"> <title>The Economics of Technology and Content for Digital TV</title> <editor> <last>Gerbarg</last> <first>Darcy</first> <affiliation>CITI</affiliation> </editor> <publisher>Kluwer Academic Publishers</publisher> <price>129.95</price> </book> </bib> The data for this example was created using a DTD, which specifies that a bibliography is a sequence of books, each book has a title, publication year (as an attribute), an author or an editor, a publisher, and a price, and each author or editor has a first and a last name, and an editor has an affiliation. Listing 1.2 provides the DTD for our example.
<!ELEMENT bib (book* )> <!ELEMENT book (title, (author+ | editor+ ), publisher, price )> <!ATTLIST book year CDATA #REQUIRED > <!ELEMENT author (last, first )> <!ELEMENT editor (last, first, affiliation )> <!ELEMENT title (#PCDATA )> <!ELEMENT last (#PCDATA )> <!ELEMENT first (#PCDATA )> <!ELEMENT affiliation (#PCDATA )> <!ELEMENT publisher (#PCDATA )> <!ELEMENT price (#PCDATA )>
|
Entity Reference | Character Represented |
< | < |
> | > |
& | & |
" | " |
' | ' |
doc("books.xml")
A dynamic error is raised if the doc() function is not able to locate the specified document or the collection() function is not able to locate the specified collection.
doc("books.xml")/bib/book
This expression opens books.xml using the doc() function and returns its document node, uses /bib to select the bib element at the top of the document, and uses /book to select the book elements within the bib element. This path expression contains three steps. The same books could have been found by the following query, which uses the double slash, //, to select all of the book elements contained in the document, regardless of the level at which they are found:
doc("books.xml")//book
Predicates are Boolean conditions that select a subset of the nodes computed by a step expression. XQuery uses square brackets around predicates. For instance, the following query returns only authors for which last="Stevens" is true:
doc("books.xml")/bib/book/author[last="Stevens"]
If a predicate contains a single numeric value, it is treated like a subscript. For instance, the following expression returns the first author of each book:
doc("books.xml")/bib/book/author[1]
Note that the expression author[1] will be evaluated for each book. If you want the first author in the entire document, you can use parentheses to force the desired precedence:
(doc("books.xml")/bib/book/author)[1]
Now let’s explore how path expressions are evaluated in terms of the data model. The steps in a path expression are evaluated from left to right. The first step identifies a sequence of nodes using an input function, a variable that has been bound to a sequence of nodes, or a function that returns a sequence of nodes. Some XQuery implementations also allow a path expression to start with a / or //.doc("books.xml")/bib/book[1]
Working from left to right, XQuery first evaluates the input function, doc("books.xml"), returning the document node, which becomes the context node for evaluating the expression on the right side of the first slash. This right-hand expression is bib, a NameTest that returns all elements named bib that are children of the context node. There is only one bib element, and it becomes the context node for evaluating the expression book, which first selects all book elements that are children of the context node and then filters them to return only the first book element.doc("books.xml")/bib//author[1]
The first step returns the document node, the second step returns the bib element, the third step—which is not visible in the original query evaluates descendant-or-self::node()to return the bib element and all nodes descended from it, and the fourth step selects the first author element for each context node from the third step. Since only book elements contain author elements, this means that the first author of each book will be returned.<book year="1994" xmlns:dcx="http://purl.org/dc/elements/1.1/">
<dcx:title>TCP/IP Illustrated</dcx:title>
<author><last>Stevens</last><first>W.</first></author>
</book>
In this data, xmlns:dcx="http://purl.org/dc/elements/1.1/" declares the prefix "dcx" as a synonym for the full namespace, and the element name dcx:title uses the prefix to indicate this is a title element as defined in the Dublin Core. The following query finds Dublin Core titles:
declare namespace dc="http://purl.org/dc/elements/1.1/"
doc("books.xml")//dc:title
The first line declares the namespace dc as a synonym for the Dublin Core namespace. Note that the prefix used in the document differs from the prefix used in the query. In XQuery, the name used for comparisons consists of the namespace URI and the “local part,” which is title for this element.doc("books.xml")//book[1]/*
The * wildcard matches any element, whether or not it is in a namespace. To match any attribute, use @*. To match any name in the namespace associated with the dc prefix, use dc:*. To match any title element, regardless of namespace, use *:title.
<book year="1977">
<title>Harold and the Purple Crayon</title>
<author><last>Johnson</last><first>Crockett</first></author>
<publisher>HarperCollins Juvenile Books</publisher>
<price>14.95</price>
</book>
As we have mentioned previously, the document node does not have explicit syntax in XML, but XQuery provides an explicit document node constructor. The query document { } creates an empty document node. Let’s use a document node constructor together with other constructors to create an entire document, including the document node, a processing instruction for stylesheet linking, and an XML comment:
document {
<?xml-stylesheet type="text/xsl" href="c:\temp\double-slash.xslt"?>,
<!—I love this book! —>,
<book year="1977">
<title>Harold and the Purple Crayon</title>
<author><last>Johnson</last><first>Crockett</first></author>
<publisher>HarperCollins Juvenile Books</publisher>
<price>14.95</price>
</book>
}
Constructors can be combined with other XQuery expressions to generate content dynamically. In an element constructor, curly braces, { }, delimit enclosed expressions, which are evaluated to create open content. Enclosed expressions may occur in the content of an element or the value of an attribute. For instance, the following query might be used in an interactive XQuery tutorial to teach how element constructors work:
<example>
<p> Here is a query. </p>
<eg> doc("books.xml")//book[1]/title </eg>
<p> Here is the result of the above query.</p>
<eg>{ doc("books.xml")//book[1]/title }</eg>
</example>
Here is the result of executing the above query for our sample data:
<example>
<p> Here is a query. </p>
<eg> doc("books.xml")//book[1]/title </eg>
<p> Here is the result of the above query.</p>
<eg><title>TCP/IP Illustrated</title></eg>
</example>
Enclosed expressions in element constructors permit new XML values to be created by restructuring existing XML values. Here is a query that creates a list of book titles from the bibliography:
<titles count="{ count(doc('books.xml')//title) }">
{
doc("books.xml")//title
}
</titles>
The output of this query follows:
<titles count = "4">
<title>TCP/IP Illustrated</title>
<title>Advanced Programming in the Unix Environment</title>
<title>Data on the Web</title>
<title>The Economics of Technology and Content for Digital TV</title>
</titles>
Namespace declaration attributes in element constructors have the same meaning they have in XML. We previously showed the following Dublin Core example as XML text—but it is equally valid as an XQuery element constructor, and it treats the namespace declaration the same way:
<book year="1994" xmlns:dcx="http://purl.org/dc/elements/1.1/">
<dcx:title>TCP/IP Illustrated</dcx:title>
<author><last>Stevens</last><first>W.</first></author>
</book>
Computed element and attribute constructors are an alternative syntax that can be used as the XML-style constructors are, but they offer additional functionality that is discussed in this section. Here is a computed element constructor that creates an element named title, with the content "Harold and the Purple Crayon". Inside the curly braces, constants are represented using XQuery’s native syntax, in which strings are delimited by double or single quotes.
element title {
"Harold and the Purple Crayon"
}
Here is a slightly more complex constructor that creates nested elements and attributes using the computed constructor syntax:
element book
{
attribute year { 1977 },
element author
{
element first { "Crockett" },
element last { "Johnson" }
},
element publisher {"HarperCollins Juvenile Books"},
element price { 14.95 }
}
The preceding example uses literals for the names of elements. In a computed element or attribute constructor, the name can also be an enclosed expression that must have the type QName, which represents an element or attribute name. For instance, suppose the user has written a function that takes two parameters, an element name in English and a language, and returns a QName that has been translated to the desired language. This function could be used in a computed element constructor as follows:
element { translate-element-name("publisher", "German") }
{ "HarperCollins Juvenile Books" }
The result of the above query is
<Verlag>HarperCollins Juvenile Books</Verlag>
In constructors, if sequences of whitespace characters occur in the boundaries between tags or enclosed expressions, with no intervening non-whitespace characters, then the whitespace is known as boundary whitespace. Implementations may discard boundary whitespace unless the query specifically declares that space must be preserved using the xmlspace declaration, a declaration that can occur in the prolog. The following query declares that all whitespace in element constructors must be preserved:
declare xmlspace = preserve
<author>
<last>Stevens</last>
<first>W.</first>
</author>
The output of the above query is
<author>
<last>Stevens</last>
<first>W.</first>
</author>
If the xmlspace declaration is absent, or is set to strip, then boundary whitespace is stripped:
<author><last>Stevens</last><first>W.</first></author>
for $b in doc("books.xml")//book
where $b/@year = "2000"
return $b/title
This query binds the variable $b to each book, one at a time, to create a series of tuples. Each tuple contains one variable binding in which $b is bound to a single book. The where clause tests each tuple to see if $b/@year is equal to “2000,” and the return clause is evaluated for each tuple that satisfies the conditions expressed in the where clause. In our sample data, only Data on the Web was written in 2000, so the result of this query is
<title>Data on the Web</title>
The name FLWOR is an acronym, standing for the first letter of the clauses that may occur in a FLWOR expression:
for $i in (1, 2, 3)
return
<tuple><i>{ $i }</i></tuple>
In this example, we bind $i to the expression (1, 2, 3), which constructs a sequence of integers. XQuery has a very general syntax, and for clauses or let clauses can be bound to any XQuery expression. Here is the result of the above query, showing how the variable $i is bound in each tuple:
<tuple><i>1</i></tuple>
<tuple><i>2</i></tuple>
<tuple><i>3</i></tuple>
Note that the order of the items bound in the tuple is the same as the order of the items in the original expression (1, 2, 3). A for clause preserves order when it creates tuples.let $i := (1, 2, 3)
return
<tuple><i>{ $i }</i></tuple>
The result of this query contains only one tuple, in which the variable $i is bound to the entire sequence of integers:
<tuple><i>1 2 3</i></tuple>
If a let clause is used in a FLWOR expression that has one or more for clauses, the variable bindings of let clauses are added to the tuples generated by the for clauses. This is demonstrated by the following query:
for $i in (1, 2, 3)
let $j := (1, 2, 3)
return
<tuple><i>{ $i }</i><j>{ $j }</j></tuple>
If a let clause is used in a FLWOR expression that has one or more for clauses, the variable bindings from let clauses are added to the tuples generated by the for clauses:
<tuple><i>1</i><j>1 2 3</j></tuple>
<tuple><i>2</i><j>1 2 3</j></tuple>
<tuple><i>3</i><j>1 2 3</j></tuple>
Here is a query that combines for and let clauses in the same way as the previous query:
for $b in doc("books.xml")//book
let $c := $b/author
return <book>{ $b/title, <count>{ count($c) }</count>}</book>
This query lists the title of each book together with the number of authors. Listing 1.3 shows the result when we apply it to our bibliography data.
Listing 1.3 Query Results
<book> <title>TCP/IP Illustrated</title> <count>1</count> </book> <book> <title>Advanced Programming in the UNIX Environment</title> <count>1</count> </book> <book> <title>Data on the Web</title> <count>3</count> </book> <book> <title>The Economics of Technology and Content for Digital TV</title> <count>0</count> </book>
If more than one variable is bound in the for clauses of a FLWORexpression, then the tuples contain all possible combinations of the items to which these variables are bound. For instance, the following query shows all combinations that include 1, 2, or 3 combined with 4, 5, or 6:
for $i in (1, 2, 3),
$j in (4, 5, 6)
return
<tuple><i>{ $i }</i><j>{ $j }</j></tuple>
Here is the result of the above query:
<tuple><i>1</i><j>4</j></tuple>
<tuple><i>1</i><j>5</j></tuple>
<tuple><i>1</i><j>6</j></tuple>
<tuple><i>2</i><j>4</j></tuple>
<tuple><i>2</i><j>5</j></tuple>
<tuple><i>2</i><j>6</j></tuple>
<tuple><i>3</i><j>4</j></tuple>
<tuple><i>3</i><j>5</j></tuple>
<tuple><i>3</i><j>6</j></tuple>
A combination of all possible combinations of sets of values is called a Cartesian cross-product. The tuples preserve the order of the original sequences, in the order in which they are bound. In the previous example, note that the tuples reflect the values of each $i in the original order; for a given value of $i, the values of $j occur in the original order. In mathematical terms, the tuples generated in a FLWOR expression are drawn from the ordered Cartesian cross-product of the items to which the for variables are bound.for $b in doc("books.xml")//book
where $b/price < 50.00
return $b/title
Here is the result of this query:
<title>Data on the Web</title>
A where clause can contain any expression that evaluates to a Boolean value. In SQL, a WHERE clause can only test single values, but there is no such restriction on where clauses in XQuery. The following query returns the title of books that have more than two authors:
for $b in doc("books.xml")//book
let $c := $b//author
where count($c) > 2
return $b/title
Here is the result of the above query:
<title>Data on the Web</title>
for $t in doc("books.xml")//title
order by $t
return $t
The for clause generates a sequence of tuples, with one title node in each tuple. The order by clause sorts these tuples according to the value of the title elements in the tuples, and the return clause returns the title elements in the same order as the sorted tuples. The result of this query is
<title>Advanced Programming in the Unix Environment</title>
<title>Data on the Web</title>
<title>TCP/IP Illustrated</title>
<title>The Economics of Technology and Content for Digital TV</title>
The order by clause allows one or more orderspecs, each of which specifies one expression used to sort the tuples. An orderspec may also specify whether to sort in ascending or descending order, how expressions that evaluate to empty sequences should be sorted, a specific collation to be used, and whether stable sorting should be used (stable sorting preserves the relative order of two items if their values are equal). Here is a query that returns authors, sorting in reverse order by the last name, then the first name
for $a in doc("books.xml")//author
order by $a/last descending, $a/first descending
return $a
The result of this query is shown in Listing 1.4.
Listing 1.4 Results of Query for Authors Sorted by Last Name
<author> <last>Suciu</last> <first>Dan</first> </author> <author> <last>Stevens</last> <first>W.</first> </author> <author> <last>Stevens</last> <first>W.</first> </author> <author> <last>Buneman</last> <first>Peter</first> </author> <author> <last>Abiteboul</last> <first>Serge</first> </author>
The order by clause may specify conditions based on data that is not used in the return clause, so there is no need for an expression to return data in order to use it to sort. Here is an example that returns the titles of books, sorted by the name of the first author:
let $b := doc("books.xml")//book
for $t in distinct-values($b/title)
let $a1 := $b[title=$t]/author[1]
order by $a1/last, $a1/first
return $b/title
The result of this query is
<title>The Economics of Technology and Content for Digital TV</title>
<title>Data on the Web</title>
<title>Advanced Programming in the UNIX Environment</title>
<title>TCP/IP Illustrated</title>
The first book in this list has editors, but no authors. For this book, $a1/last and $a1/first will both return empty sequences. Some XQuery implementations always sort empty sequences as the greatest possible value; others always sort empty sequences as the least possible value. The XML Query Working Group decided to allow vendors to choose which of these orders to implement because many XQuery implementations present views of relational data, and relational databases differ in their sorting of nulls. To guarantee that an XQuery uses the same sort order across implementations, specify “empty greatest” or “empty least” in an orderspec if its expression can evaluate to an empty sequence.let $b := doc("books.xml")//book
for $t in distinct-values($b/title)
let $a1 := $b[title=$t]/author[1]
stable order by $a1/last empty least, $a1/first empty least
return $b/title
This query returns the same result as the previous one, but is guaranteed to do so across all implementations.for $t in doc("books.xml")//title
order by $t collation "http://www.example.com/collations/eng-us"
return $t
Most queries use the same collation for all comparisons, and it is generally too tedious to specify a collation for every orderspec. XQuery allows a default collation to be specified in the prolog. The default collation is used when the orderspec does not specify a collation. Here is a query that sets http://www.example.com/collations/eng-us as the default collation; it returns the same results as the previous query:
default collation = "http://www.example.com/collations/eng-us"
for $t in doc("books.xml")//title
order by $t
return $t
When sorting expressions in queries, it is important to remember that the / and // operators sort in document order. That means that an order established with an order by clause can be changed by expressions that use these operators. For instance, consider the following query:
let $authors := for $a in doc("books.xml")//author
order by $a/last, $a/first
return $a
return $authors/last
This query does not return the author’s last names in alphabetical order, because the / in $authors/last sorts the last elements in document order. This kind of error generally occurs with let bindings, not with for bindings, because a for clause binds each variable to a single value in a given tuple, and returning children or descendents of a single node does not lead to surprises. The following query returns author’s last names in alphabetical order:
for $a in doc("books.xml")//author
order by $a/last, $a/first
return $a/last
for $b in doc("books.xml")//book
return
<quote>{ $b/title, $b/price }</quote>
Listing 1.5 shows the result of the above query.
Listing 1.5 Results of Query for Price Quotes
<quote> <title>TCP/IP Illustrated</title> <price>65.95</price> </quote> <quote> <title>Advanced Programming in the UNIX Environment</title> <price>65.95</price> </quote> <quote> <title>Data on the Web</title> <price>39.95</price> </quote> <quote> <title>The Economics of Technology and Content for Digital TV</title> <price>129.95</price> </quote>
Element constructors can be used in a return clause to change the hierarchy of data. For instance, we might want to represent an author’s name as a string in a single element, which we can do with the following query:
for $a in doc("books.xml")//author
return
<author>{ string($a/first), " ", string($a/last) }</author>
Here is the result of the above query:
<author>W. Stevens</author>
<author>W. Stevens</author>
<author>Serge Abiteboul</author>
<author>Peter Buneman</author>
<author>Dan Suciu</author>
Another application might want to insert a name element to hold the first and last name of the author—after all, an author does not consist of a first and a last! Here is a query that adds a level to the hierarchy for names:
for $a in doc("books.xml")//author
return
<author>
<name>{ $a/first, $a/last }</name>
</author>
Here is one author’s name taken from the output of the above query:
<author>
<name>
<first>Serge</first>
<last>Abiteboul</last>
</name>
</author>
This section has discussed the most straightforward use of for and return clauses, and it has shown how to combine FLWOR expressions with other expressions to perform common tasks. More complex uses of for clauses are explored later in separate sections on joins and positional variables.
for $t at $i in doc("books.xml")//title
return <title pos="{$i}">{string($t)}</title>
Here is the result of this query:
<title pos="1">TCP/IP Illustrated</title>
<title pos="2">Advanced Programming in the Unix Environment</title>
<title pos="3">Data on the Web</title>
<title pos="4">The Economics of Technology and Content for Digital TV</title>
In some data, position conveys meaning. In tables, for instance, the row and column in which an item is found often determine its meaning. For instance, suppose we wanted to create data from an XHTML web page that contains the table shown in Table 1.2.
Title | Price | Publisher | Year |
TCP/IP Illustrated | Addison-Wesley | 65.95 | 1994 |
Advanced Programming in the UNIX Environment | Addison-Wesley | 65.95 | 1992 |
Data on the Web | Morgan Kaufmann Publishers | 39.95 | 2000 |
The Economics of Technology and Content for Digital TV | Kluwer Academic Publishers | 129.95 | 1999 |
<table border="1" ID="Table2">
<thead>
<tr>
<td>Title</td>
<td>Publisher</td>
<td>Price</td>
<td>Year</td>
</tr>
</thead>
<tbody>
<tr>
<td>TCP/IP Illustrated</td>
<td>Addison-Wesley</td>
<td>65.95</td>
<td>1994</td>
</tr>
<tr>
<td>Advanced Programming in the UNIX Environment</td>
<td>Addison-Wesley</td>
<td>65.95</td>
<td>1992</td>
</tr>
<!— Additional rows omitted to save space —>
</tbody>
</table>
In this table, every entry in the same column as the Title header is a title, every entry in the same column as the Publisher header is a publisher, and so forth. In other words, we can determine the purpose of an entry if we can determine its position as a column of the table, and relate it to the position of a column header. Positional variables make this possible. Since XHTML is XML, it can be queried using XQuery. Listing 1.7 shows a query that produces meaningful XML from the above data, generating the names of elements from the column headers.
Listing 1.7 Query to Generate Names of Elements from Column Headers
let $t := doc("bib.xhtml")//table[1] for $r in $t/tbody/tr return <book> { for $c at $i in $r/td return element{ lower-case(data($t/thead/tr/td[$i])) } { string( $c) } } </book>
Note the use of a computed element constructor that uses the column header to determine the name of the element. Listing 1.8 shows the portion of the output this query generates for the partial data shown in Table 1.2.
Listing 1.8 Output Generated by the Query of Listing 1.7
<book> <title>TCP/IP Illustrated</title> <publisher>Addison-Wesley</publisher> <price>65.95</price> <year>1994</year> </book> <book> <title>Advanced Programming in the Unix Environment</title> <publisher>Addison-Wesley</publisher> <price>65.95</price> <year>1992</year> </book>
Eliminating Duplicate Subtrees with distinct-values() and FLWOR Expressions
Data often contains duplicate values, and FLWOR expressions are often combined with the distinct-values() function to remove duplicates from subtrees. Let’s start with the following query, which returns the last name of each author:
doc("books.xml")//author/last
Since one of our authors wrote two of the books in the bibliography, the result of this query contains a duplicate:
<last>Stevens</last>
<last>Stevens</last>
<last>Abiteboul</last>
<last>Buneman</last>
<last>Suciu</last>
The distinct-values() function extracts the values of a sequence of nodes and creates a sequence of unique values, eliminating duplicates. Here is a query that uses distinct-values() to eliminate duplicate last names:
distinct-values(doc("books.xml")//author/last)
Here is the output of the above query:
Stevens Abiteboul Buneman Suciu
The distinct-values() function eliminates duplicates, but in order to do so, it extracts values from nodes. FLWOR expressions are often used together with distinct-values() to create subtrees that correspond to sets of one or more unique values. For the preceding query, we can use an element constructor to create a last element containing each value:
for $l in distinct-values(doc("books.xml")//author/last)
return <last>{ $l }</last>
Here is the output of the above query:
<last>Stevens</last>
<last>Abiteboul</last>
<last>Buneman</last>
<last>Suciu</last>
The same problem arises for complex subtrees. For instance, the following query returns authors, and one of the authors is a duplicate by both first and last name:
doc("books.xml")//author
The output of the above query appears in Listing 1.9.
Listing 1.9 Output of the Query for Authors
<authors> <author> <last>Stevens</last> <first>W.</first> </author> <author> <last>Stevens</last> <first>W.</first> </author> <author> <last>Abiteboul</last> <first>Serge</first> </author> <author> <last>Buneman</last> <first>Peter</first> </author> <author> <last>Suciu</last> <first>Dan</first> </author> </authors>
To eliminate duplicates from complex subtrees, we have to decide what criterion to use for detecting a duplicate. In this case, let’s say that an author is a duplicate if there is another author who has the same first and last names. Now let’s write a query that returns one author for each first and last name that occur together within an author element in our dataset:
let $a := doc("books.xml")//author
for $l in distinct-values($a/last),
$f in distinct-values($a[last=$l]/first)
return
<author>
<last>{ $l }</last>
<first>{ $f }</first>
</author>
In the output of the above query (Listing 1.10), each author’s name appears only once.
Listing 1.10 Output of Query to Avoid Duplicate Author Names
<authors> <author> <last>Stevens</last> <first>W.</first> </author> <author> <last>Abiteboul</last> <first>Serge</first> </author> <author> <last>Buneman</last> <first>Peter</first> </author> <author> <last>Suciu</last> <first>Dan</first> </author> </authors>
Joins: Combining Data Sources with for and where Clauses
A query may bind multiple variables in a for clause in order to combine information from different expressions. This is often done to bring together information from different data sources. For instance, suppose we have a file named reviews.xml that contains book reviews:
<reviews>
<entry>
<title>TCP/IP Illustrated</title>
<rating>5</rating>
<remarks>Excellent technical content. Not much plot.</remarks>
</entry>
</reviews>
A FLWOR expression can bind one variable to our bibliography data and another to the reviews, making it possible to compare data from both files and to create results that combine their information. For instance, a query could return the title of a book and any remarks found in a review.for $t in doc("books.xml")//title,
$e in doc("reviews.xml")//entry
where $t = $e/title
return <review>{ $t, $e/remarks }</review>
The result of this query is as follows:
<review>
<title>TCP/IP Illustrated</title>
<remarks>Excellent technical content. Not much plot.</remarks>
</review>
In this query, the for clauses create tuples from the Cartesian crossproduct of titles and entries, the where clause filters out tuples where the title of the review does not match the title of the book, and the return clause constructs the result from the remaining tuples. Note that only books with reviews are shown. SQL programmers will recognize the preceding query as an inner join, returning combinations of data that satisfy a condition.for $t in doc("books.xml")//title
for $e in doc("reviews.xml")//entry
where $t = $e/title
return <review>{ $t, $e/remarks }</review>
The query shown in Listing 1.11 returns the title of each book regardless of whether it has a review; if a book does have a review, the remarks found in the review are also included. SQL programmers will recognize this as a left outer join.
Listing 1.11 Query to Return Titles with or without Reviews
for $t in doc("books.xml")//title return <review> { $t } { for $e in doc("reviews.xml")//entry where $e/title = $t return $e/remarks } </review>
Inverting Hierarchies
XQuery can be used to do quite general transformations. One transformation that is used in many applications is colloquially referred to as “inverting a hierarchy”—creating a new document in which the top nodes represent information which was found in the lower nodes of the original document. For instance, in our sample data, publishers are found at the bottom of the hierarchy, and books are found near the top. Listing 1.12 shows a query that creates a list of titles published by each publisher, placing the publisher at the top of the hierarchy and listing the titles of books at the bottom.
Listing 1.12 Query to List Titles by Publisher
<listings> { for $p in distinct-values(doc("books.xml")//publisher) order by $p return <result> { $p } { for $b in doc("books.xml")/bib/book where $b/publisher = $p order by $b/title return $b/title } </result> } </listings>
The results of this query are as follows:
<listings>
<result>
<publisher>Addison-Wesley</publisher>
<title>Advanced Programming in the Unix Environment</title>
<title>TCP/IP Illustrated</title>
</result>
<result>
<publisher>Kluwer Academic Publishers</publisher>
<title>The Economics of Technology and Content for Digital TV</title>
</result>
<result>
<publisher>Morgan Kaufmann Publishers</publisher>
<title>Data on the Web</title>
</result>
</listings>
A more complex example of inverting a hierarchy is discussed in the following section on quantifiers.
for $b in doc("books.xml")//book
where some $a in $b/author
satisfies ($a/last="Stevens" and $a/first="W.")
return $b/title
The some quantifier in the where clause tests to see if there is at least one author that satisfies the conditions given inside the parentheses. Here is the result of the above query:
<title>TCP/IP Illustrated</title>
<title>Advanced Programming in the Unix Environment</title>
A universal quantifier tests whether every node in a sequence satisfies a condition. The following query tests to see if every author of a book is named W. Stevens:
for $b in doc("books.xml")//book
where every $a in $b/author
satisfies ($a/last="Stevens" and $a/first="W.")
return $b/title
Here is the result of the above query:
<title>TCP/IP Illustrated</title>
<title>Advanced Programming in the Unix Environment</title>
<title>The Economics of Technology and Content for Digital TV</title>
The last title returned, The Economics of Technology and Content for Digital TV, is the title of a book that has editors but no authors. For this book, the expression $b/author evaluates to an empty sequence. If a universal quantifier is applied to an empty sequence, it always returns true, because every item in that (empty) sequence satisfies the condition—even though there are no items.Listing 1.13 Query to List Books by Author
<author-list> { let $a := doc("books.xml")//author for $l in distinct-values($a/last), $f in distinct-values($a[last=$l]/first) order by $l, $f return <author> <name>{ $l, ", ", $f }</name> { for $b in doc("books.xml")/bib/book where some $ba in $b/author satisfies ($ba/last=$l and $ba/first=$f) order by $b/title return $b/title } </author> } </author-list>
The result of the above query is shown in Listing 1.14.
Listing 1.14 Results of Query to List Books by Author
<author-list> <author> <name>Stevens, W.</name> <title>Advanced Programming in the Unix Environment</title> <title>TCP/IP Illustrated</title> </author> <author> <name>Abiteboul, Serge</name> <title>Data on the Web</title> </author> <author> <name>Buneman, Peter</name> <title>Data on the Web</title> </author> <author> <name>Suciu, Dan</name> <title>Data on the Web</title> </author> </author-list>
Conditional Expressions
XQuery’s conditional expressions are used in the same way as conditional expressions in other languages. Listing 1.15 shows a query that uses a conditional expression to list the first two authors’ names for each book and a dummy name containing “et al.” to represent any remaining authors.
Listing 1.15 Query to List Author’s Names with “et al.”
for $b in doc("books.xml")//book return <book> { $b/title } { for $a at $i in $b/author where $i <= 2 return <author>{string($a/last), ", ", string($a/first)}</author> } { if (count($b/author) > 2) then <author>et al.</author> else () } </book>
In XQuery, both the then clause and the if clause are required. Note that the empty sequence () can be used to specify that a clause should return nothing. The output of this query is shown in Listing 1.16.
Listing 1.16 Result of Query from Listing 1.15
<book> <title>TCP/IP Illustrated</title> <author>Stevens, W.</author> </book> <book> <title>Advanced Programming in the Unix Environment</title> <author>Stevens, W.</author> </book> <book> <title>Data on the Web</title> <author>Abiteboul, Serge</author> <author>Buneman, Peter</author> <author>et al.</author> </book> <book> <title>The Economics of Technology and Content for Digital TV</title> </book>
Operators
The queries we have shown up to now all contain operators, which we have not yet covered. Like most languages, XQuery has arithmetic operators and comparison operators, and because sequences of nodes are a fundamental datatype in XQuery, it is not surprising that XQuery also has node sequence operators. This section describes these operators in some detail. In particular, it describes how XQuery treats some of the cases that arise quite easily when processing XML; for instance, consider the following expression: 1 * $b. How is this interpreted if $b is an empty sequence, untyped character data, an element, or a sequence of five nodes? Given the flexible structure of XML, it is imperative that cases like this be well defined in the language. (Chapter 2, “Influences on the Design of XQuery,” provides additional background on the technical complexities that the working group had to deal with to resolve these and similar issues.)
Two basic operations are central to the use of operators and functions in XQuery. The first is called typed value extraction. We have already used typed value extraction in many of our queries, without commenting on it. For instance, we have seen this query:
doc("books.xml")/bib/book/author[last='Stevens']
Consider the expression last='Stevens'. If last is an element, and 'Stevens' is a string, how can an element and a string be equal? The answer is that the = operator extracts the typed value of the element, resulting in a string value that is then compared to the string Stevens. If the document is governed by a W3C XML Schema, then it may be associated with a simple type, such as xs:integer. If so, the typed value will have whatever type has been assigned to the node by the schema. XQuery has a function called data() that extracts the typed value of a function. Assuming the following element has been validated by a schema processor, the result of this query is the integer 4:
data( <e xsi:type="xs:integer">4</e> )
A query may import a schema. We will discuss schema imports later, but schema imports have one effect that should be understood now. If typed value extraction is applied to an element, and the query has imported a schema definition for that element specifying that the element may have other elements as children, then typed value extraction raises an error.avg( 1, <e>2</e>, <e xsi:type="xs:integer">3</e> )
Atomization simply returns the typed value of every item in the sequence. The preceding query returns 2, which is the average of 1, 2, and 3. In XQuery, atomization is used for the operands of arithmetic expressions and comparison expressions. It is also used for the parameters and return values of functions and for cast expressions, which are discussed in other sections.
2 + <int>{ 2 }</int>
If an operand is an empty sequence, the result of an arithmetic operator is an empty sequence. Empty sequences in XQuery frequently operate like nulls in SQL. The result of the following query is an empty sequence:
2 + ()
If an operand is untyped data, it is cast to a double, raising an error if the cast fails. This implicit cast is important, because a great deal of XML data is found in documents that do not use W3C XML Schema, and therefore do not have simple or complex types. Many of these documents however contain data that is to be interpreted as numeric. The prices in our sample document are one example of this. The following query adds the first and second prices, returning the result as a double:
let $p := doc("books.xml")//price
return $p[1] + $p[2]
for $b in doc("books.xml")//book
where $b/title eq "Data on the Web"
return $b/price
Value Comparison Operator | General Comparison Operator |
eq | = |
ne | != |
lt | < |
le | <= |
gt | > |
ge | >= |
for $b in doc("books.xml")//book
where xs:decimal($b/price) gt 100.00
return $b/title
If the data were governed by a W3C XML Schema that declared price to be a decimal, this cast would not have been necessary. In general, if the data you are querying is meant to be interpreted as typed data, but there are no types in the XML, value comparisons force your query to cast when doing comparisons—general comparisons are more loosely typed and do not require such casts. This problem does not arise if the data is meant to be interpreted as string data, or if it contains the appropriate types.for $b in doc("books.xml")//book
where $b/author/last eq "Stevens"
return $b/title
The reason for the error is that many books have multiple authors, so the expression $b/author/last returns multiple nodes. The following query uses =, the general comparison that corresponds to eq, to return books for which any author’s last name is equal to Stevens:
for $b in doc("books.xml")//book
where $b/author/last = "Stevens"
return $b/title
There are two significant differences between value comparisons and general comparisons. The first is illustrated in the previous query. Like value comparisons, general comparisons apply atomization to both operands, but instead of requiring each operand to be a single atomic value, the result of this atomization may be a sequence of atomic values. The general comparison returns true if any value on the left matches any value on the right, using the appropriate comparison.for $b in doc("books.xml")//book
where $b/price = 100.00
return $b/title
In this query, 100.00 is a decimal, and the = operator casts the price to decimal as well. When a general comparison tests a pair of atomic values and one of these values is untyped, it examines the other atomic value to determine the required type to which it casts the untyped operand:
for $b in doc("books.xml")//book
where $b/author/first = "Serge"
and $b/author/last = "Suciu"
return $b
The result of this query may be somewhat surprising, as Listing 1.17 shows.
Listing 1.17
Surprising Results
<book year = "2000"> <title>Data on the Web</title> <author> <last>Abiteboul</last> <first>Serge</first> </author> <author> <last>Buneman</last> <first>Peter</first> </author> <author> <last>Suciu</last> <first>Dan</first> </author> <publisher>Morgan Kaufmann Publishers</publisher> <price>39.95</price> </book>Since this book does have an author whose first name is “Serge” and an author whose last name is “Suciu,” the result of the query is correct, but it is surprising. The following query expresses what the author of the previous query probably intended:
for $b in doc("books.xml")//book,
$a in $b/author
where $a/first="Serge"
and $a/last="Suciu"
return $b
Comparisons using the = operator are not transitive. Consider the following query:
let $a := ( <first>Jonathan</first>, <last>Robie</last> ),
$b := ( <first>Jonathan</first>, <last>Marsh</last> ),
$c := ( <first>Rodney</first>, <last>Marsh</last> )
return
<out>
<equals>{ $a = $b }</equals>
<equals>{ $b = $c }</equals>
<equals>{ $a = $c }</equals>
</out>
Remember that = returns true if there is a value on the left that matches a value on the right. The output of this query is as follows:
<out>
<equals>True</equals>
<equals>True</equals>
<equals>False</equals>
</out>
Node comparisons determine whether two expressions evaluate to the same node. There are two node comparisons in XQuery, is and is not. The following query tests whether the most expensive book is also the book with the greatest number of authors and editors:
let $b1 := for $b in doc("books.xml")//book
order by count($b/author) + count($b/editor)
return $b
let $b2 := for $b in doc("books.xml")//book
order by $b/price
return $b
return $b1[last()] is $b2[last()]
This query also illustrates the last() function, which determines whether a node is the last node in the sequence; in other words, $b1[last()] returns the last node in $b1.for $b in doc("books.xml")//book
let $a := ($b/author)[1],
$sa := ($b/author)[last="Abiteboul"]
where $a << $sa
return $b
In our sample data, there are no such books.
let $l := distinct-values(doc("books.xml")//(author | editor)/last)
order by $l
return <last>{ $l }</last>
Here is the result of the above query:
<last>Abiteboul</last>
<last>Buneman</last>
<last>Gerbarg</last>
<last>Stevens</last>
<last>Suciu</last>
The fact that the union operator always returns nodes in document order is sometimes quite useful. For instance, the following query sorts books based on the name of the first author or editor listed for the book:
for $b in doc("books.xml")//book
let $a1 := ($b/author union $b/editor)[1]
order by $a1/last, $a1/first
return $b
The intersect operator takes two node sequences as operands and returns a sequence containing all the nodes that occur in both operands. The except operator takes two node sequences as operands and returns a sequence containing all the nodes that occur in the first operand but not in the second operand. For instance, the following query returns a book with all of its children except for the price:
for $b in doc("books.xml")//book
where $b/title = "TCP/IP Illustrated"
return
<book>
{ $b/@* }
{ $b/* except $b/price }
</book>
The result of this query contains all attributes of the original book and all elements—in document order—except for the price element, which is omitted:
<book year = "1994">
<title>TCP/IP Illustrated</title>
<author>
<last>Stevens</last>
<first>W.</first>
</author>
<publisher>Addison-Wesley</publisher>
</book>
let $b := doc("books.xml")//book
let $avg := average( $b//price )
return $b[price > $avg]
For our sample data, Listing 1.18 shows the result of this query.
Listing 1.18 Result of Query for Books More Expensive Than Average
<book year = "1999"> <title>The Economics of Technology and Content for Digital TV</title> <editor> <last>Gerbarg</last> <first>Darcy</first> <affiliation>CITI</affiliation> </editor> <publisher>Kluwer Academic Publishers</publisher> <price>129.95</price> </book>
Note that price is the name of an element, but max() is defined for atomic values, not for elements. In XQuery, if the type of a function argument is an atomic type, then the following conversion rules are applied. If the argument is a node, its typed value is extracted, resulting in a sequence of values. If any value in the argument sequence is untyped, XQuery attempts to convert it to the required type and raises an error if it fails. A value is accepted if it has the expected type.
Other familiar functions in XQuery include numeric functions like round(), floor(), and ceiling(); string functions like concat(), string-length(), starts-with(), ends-with(), substring(), upper-case(), lower-case(); and casts for the various simple types. These are all covered in [XQ-FO], which defines the standard function library for XQuery; they need no further coverage here since they are straightforward.
XQuery also has a number of functions that are not found in most other languages. We have already covered distinct-values(), the input functions doc() and collection(). Two other frequently used functions are not() and empty(). The not() function is used in Boolean conditions; for instance, the following returns books where no author’s last name is Stevens:
for $b in doc("books.xml")//book
where not(some $a in $b/author satisfies $a/last="Stevens")
return $b
The empty() function reports whether a sequence is empty. For instance, the following query returns books that have authors, but does not return the one book that has only editors:
for $b in doc("books.xml")//book
where not(empty($b/author))
return $b
The opposite of empty() is exists(), which reports whether a sequence contains at least one item. The preceding query could also be written as follows:
for $b in doc("books.xml")//book
where exists($b/author)
return $b
XQuery also has functions that access various kinds of information associated with a node. The most common accessor functions are string(), which returns the string value of a node, and data(), which returns the typed value of a node. These functions require some explanation. The string value of a node includes the string representation of the text found in the node and its descendants, concatenated in document order. For instance, consider the following query:
string((doc("books.xml")//author)[1])
The result of this query is the string "Stevens W." (The exact result depends on the whitespace found in the original document—we have made some assumptions about what whitespace is present.)
for $b in doc("books.xml")/bib/book
where some $ba in $b/author satisfies
($ba/last=$l and $ba/first=$f)
order by $b/title
return $b/title
This code returns the titles of books written by a given author whose first name is bound to $f and whose last name is bound to $l. But you have to read all of the code in the query to understand that. Placing it in a named function makes its purpose clearer:
define function books-by-author($last, $first)
as element()*
{
for $b in doc("books.xml")/bib/book
where some $ba in $b/author satisfies
($ba/last=$last and $ba/first=$first)
order by $b/title
return $b/title
}
XQuery allows functions to be recursive, which is often important for processing the recursive structure of XML. One common reason for using recursive functions is that XML allows recursive structures. For instance, suppose a book chapter may consist of sections, which may be nested. The query in Listing 1.19 creates a table of contents, containing only the sections and the titles, and reflecting the structure of the original document in the table of contents.
Listing 1.19 Query to Create a Table of Contents
define function toc($book-or-section as element()) as element()* { for $section in $book-or-section/section return <section> { $section/@* , $section/title , toc($section) } </section> } <toc> { for $s in doc("xquery-book.xml")/book return toc($s) } </toc>
If two functions call each other, they are mutually recursive. Mutually recursive functions are allowed in XQuery.
Variable Definitions
A query can define a variable in the prolog. Such a variable is available at any point after it is declared. For instance, if access to the titles of books is used several times in a query, it can be provided in a variable definition:
define variable $titles { doc(“books.xml”)//title }
To avoid circular references, a variable definition may not call functions that are defined prior to the variable definition.
Listing 1.20 Module Declaration for a Library Module
module "http://example.com/xquery/library/book" define function toc($book-or-section as element()) as element()* { for $section in $book-or-section/section return <section> { $section/@* , $section/title , toc($section) } </section> }
Functions and variable definitions in library modules are namespacequalified. Any module can import another module using a module import, which specifies the URI of the module to be imported. It may also specify the location where the module can be found:
import module "http://example.com/xquery/library/book"
at "file:///c:/xquery/lib/book.xq"
The location is not required in an import, since some implementations can locate modules without it. Implementations are free to ignore the location if they have another way to find modules.import module namespace b = "http://example.com/xquery/library/book"
at "file:///c:/xquery/lib/book.xq"
<toc>
{
for $s in doc("xquery-book.xml")/book
return b:toc($s)
}
</toc>
When a module is imported, both its functions and its variables are made available to the importing module.
define function outtie($v as xs:integer) as xs:integer external
define variable $v as xs:integer external
XQuery does not specify how such functions and variables are made available by the external environment, or how function parameters and arguments are converted between the external environment and XQuery.
define function reverse($items)
{
let $count := count($items)
for $i in 0 to $count
return $items[$count - $i]
}
reverse( 1 to 5)
This function uses the to operator, which generates sequences of integers. For instance, the expression 1 to 5 generates the sequence 1, 2, 3, 4, 5. The reverse function takes this sequence and returns the sequence 5, 4, 3, 2, 1. Because this function does not specify a particular type for its parameter or return, it could also be used to return a sequence of some other type, such as a sequence of elements. Specifying more type information would make this function less useful.define function is-document-element($e as element())
as xs:boolean
{
if ($e/.. instance of document-node())
then true()
else false()
}
All the built-in XML Schema types are predefined in XQuery, and these can be used to write function signatures similar to those found in conventional programming languages. For instance, the query in Listing 1.21 defines a function that computes the nth Fibonacci number and calls that function to create the first ten values of the Fibonacci sequence.
Listing 1.21 Query to Create the First Ten Fibonacci Numbers
define function fibo($n as xs:integer) { if ($n = 0) then 0 else if ($n = 1) then 1 else (fibo($n - 1) + fibo($n - 2)) } let $seq := 1 to 10 for $n in $seq return <fibo n="{$n}">{ fibo($n) }</fibo>
Listing 1.22 shows the output of that query.
Listing 1.22 Results of the Query in Listing 1.21
<fibo n = "1">1</fibo> <fibo n = "2">1</fibo> <fibo n = "3">2</fibo> <fibo n = "4">3</fibo> <fibo n = "5">5</fibo> <fibo n = "6">8</fibo> <fibo n = "7">13</fibo> <fibo n = "8">21</fibo> <fibo n = "9">34</fibo> <fibo n = "10">55</fibo>
Schemas and Types
On several occasions, we have mentioned that XQuery can work with untyped data, strongly typed data, or mixtures of the two. If a document is governed by a DTD or has no schema at all, then documents contain very little type information, and queries rely on a set of rules to infer an appropriate type when they encounter values at run-time. For instance, the following query computes the average price of a book in our bibliography data:
avg( doc("books.xml")/bib/book/price )
Since the bibliography does not have a schema, each price element is untyped. The avg() function requires a numeric argument, so it converts each price to a double and then computes the average. The conversion rules are discussed in detail in a later section. The implicit conversion is useful when dealing with untyped data, but prices are generally best represented as decimals rather than floating-point numbers. Later in this chapter we will present a schema for the bibliography in order to add appropriate type information. The schema declares price to be a decimal, so the average would be computed using decimal numbers.define function books-by-author($author)
{
for $b in doc("books.xml")/bib/book
where some $ba in $b/author satisfies
($ba/last=$author/last and $ba/first=$author/first)
order by $b/title
return $b/title
}
Because this function does not specify what kind of element the parameter should be, it can be called with any element at all. For instance, a book element could be passed to this function. Worse yet, the query would not return an error, but would simply search for books containing an author element that exactly matches the book. Since such a match never occurs, this function always returns the empty sequence if called with a book element.Listing 1.23 Schema Import and Type Checking
import schema "urn:examples:xmp:bib" at "c:/dev/schemas/eg/bib.xsd" default element namespace = "urn:examples:xmp:bib" define function books-by-author($a as element(b:author)) as element(b:title)* { for $b in doc("books.xml")/bib/book where some $ba in $b/author satisfies ($ba/last=$a/last and $ba/first=$a/first) order by $b/title return $b/title }
In XQuery, a type error is raised when the type of an expression does not match the type required by the context in which it appears. For instance, given the previous function definition, the function call in the following expression raises a type error, since an element named book can never be a valid author element:
for $b in doc("books.xml")/bib/book
return books-by-author($b)
All XQuery implementations are required to detect type errors, but some implementations detect them before a query is executed, and others detect them at run-time when query expressions are evaluated. The process of analyzing a query for type errors before a query is executed is called static typing, and it can be done using only the imported schema information and the query itself—there is no need for data to do static typing. In XQuery, static typing is an optional feature, but an implementation that supports static typing must always detect type errors statically, before a query is executed.Listing 1.24 Assigning a Namespace Prefix in Schema Imports
import schema namespace b = "urn:examples:xmp:bib" at "c:/dev/schemas/eg/bib.xsd" define function books-by-author($a as element(b:author)) as element(b:title)* { for $b in doc("books.xml")/b:bib/b:book where some $ba in $b/b:author satisfies ($ba/b:last=$l and $ba/b:first=$f) order by $b/b:title return $b/b:title }
When an element is created, it is immediately validated if there is a schema definition for its name. For instance, the following query raises an error because the schema definition says that a book must have a price:
import schema "urn:examples:xmp:bib" at "c:/dev/schemas/eg/bib.xsd"
default element namespace = "urn:examples:xmp:bib"
<book year="1994">
<title>Catamaran Racing from Start to Finish</title>
<author><last>Berman</last><first>Phil</first></author>
<publisher>W.W. Norton & Company</publisher>
</book>
The schema import feature reduces errors by allowing queries to specify type information, but these errors are not caught until data with the wrong type information is actually encountered when executing a query. A query processor that implements the static typing feature can detect some kinds of errors by comparing a query to the imported schemas, which means that no data is required to find these errors. Let’s modify our query somewhat and introduce a spelling error—$a/first is misspelled as $a/firt in Listing 1.25.
Listing 1.25 Query with a Spelling Error
import schema "urn:examples:xmp:bib" at "c:/dev/schemas/eg/bib.xsd" default element namespace = "urn:examples:xmp:bib" define function books-by-author($a as element(author)) as element(title)* { for $b in doc("books.xml")/bib/book where some $ba in $b/author satisfies ($ba/last=$a/last and $ba/first=$a/firt) order by $b/title return $b/title }
An XQuery implementation that supports static typing can detect this error, because it has the definition for an author element, the function parameter is identified as such, and the schema says that an author element does not have a firt element. In an implementation that has schema import but not static typing, this function would actually have to call the function before the error would be raised.
However, in the following path expression, only the names of elements are stated:
doc("books.xml")/bib/book
XQuery allows element tests and attribute tests, node tests that are similar to the type declaration used for function parameters. In a path expression, the node test element(book) finds only elements with the same type as the globally declared book element, which must be found in the schemas that have been imported into the query. By using this instead of the name test book in the path expression, we can tell the query processor the element definition that will be associated with $b, which means that the static type system can guarantee us that a $b will contain title elements; see Listing 1.26.
Listing 1.26 Type Tests in Path Expressions
import schema "urn:examples:xmp:bib" at "c:/dev/schemas/eg/bib.xsd" default element namespace = "urn:examples:xmp:bib" define function books-by-author($a as element(author)) as element(title)* { for $b in doc("books.xml")/bib/element(book) where some $ba in $b/author satisfies ($ba/last=$a/last and $ba/first=$a/first) order by $b/title return $b/title }
Sequence Types
The preceding examples include several queries in which the names of types use a notation that can describe the types that arise in XML documents. Now we need to learn that syntax in some detail. Values in XQuery, in general, are sequences, so the types used to describe them are called sequence types. Some types are built in and may be used in any query without importing a schema into the query. Other types are defined in W3C XML Schemas and must be imported into a query before they can be used.
Built-in Types
If a query has not imported a W3C XML Schema, it still understands the structure of XML documents, including types like document, element, attribute, node, text node, processing instruction, comment, ID, IDREF, IDREFS, etc. In addition to these, it understands the built-in W3C XML Schema simple types.
Table 1.4 lists the built-in types that can be used as sequence types.
In the notation for sequence types, occurrence indicators may be used to indicate the number of items in a sequence. The character ? indicates zero or one items, * indicates zero or more items, and + indicates one or more items. Here are some examples of sequence types with occurrence indicators:
element()+ One or more elements
xs:integer? Zero or one integers
document-node()* Zero or more document nodes
Sequence Type Declaration | What It Matches |
element() | Any element node |
attribute() | Any attribute node |
document-node() | Any document node |
node() | Any node |
text() | Any text node |
processing-instruction() | Any processing instruction node |
processing-instruction("xmlstylesheet") | Any processing instruction node whose target is xml-stylesheet |
comment() | Any comment node |
empty() | An empty sequence |
item() | Any node or atomic value |
QName | An instance of a specific XML Schema built-in type, identified by the name of the type; e.g., xs:string, xs:boolean, xs:decimal, xs:float, xs:double, xs:anyType, xs:anySimpleType |
Listing 1.27 An Imported Schema for Bibliographies
<?xml version="1.0"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:bib="urn:examples:xmp:bib" targetNamespace="urn:examples:xmp:bib" elementFormDefault="qualified"> <xs:element name="bib"> <xs:complexType> <xs:sequence> <xs:element ref="bib:book" minOccurs="0" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="book"> <xs:complexType> <xs:sequence> <xs:element name="title" type="xs:string"/> <xs:element ref="bib:creator" minOccurs="1" maxOccurs="unbounded"/> <xs:element name="publisher" type="xs:string"/> <xs:element name="price" type="currency"/> <xs:element name="year" type="xs:gYear"/> </xs:sequence> <xs:attribute name="isbn" type="bib:isbn"/> </xs:complexType> </xs:element> <xs:element name="creator" type="person" abstract="true" /> <xs:element name="author" type="person" substitutionGroup="bib:creator"/> <xs:element name="editor" type="personWithAffiliation" substitutionGroup="bib:creator"/> <xs:complexType name="person"> <xs:sequence> <xs:element name="last" type="xs:string"/> <xs:element name="first" type="xs:string"/> </xs:sequence> </xs:complexType> <xs:complexType name="personWithAffiliation"> <xs:complexContent> <xs:extension base="person"> <xs:sequence> <xs:element name="affiliation" type="xs:string"/> </xs:sequence> </xs:extension> </xs:complexContent> </xs:complexType> <xs:simpleType name="isbn"> <xs:restriction base="xs:string"> <xs:pattern value="[0-9]{9}[0-9X]"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="currency"> <xs:restriction base="xs:decimal"> <xs:pattern value="\d+.\d{2}"/> </xs:restriction> </xs:simpleType> </xs:schema>
Here is an example of a bibliography element that conforms to this new definition:
<bib xmlns="urn:examples:xmp:bib">
<book isbn="0201563177">
<title>Advanced Programming in the Unix Environment</title>
<author><last>Stevens</last><first>W.</first></author>
<publisher>Addison-Wesley</publisher>
<price>65.95</price>
<year>1992</year>
</book>
</bib>
We do not teach the basics of XML Schema here—those who do not know XML Schema should look at XML Schema primer [SCHEMA]. However, to understand how XQuery leverages the type information found in a schema, we need to know what the schema says. Here are some aspects of the previous schema that affect the behavior of examples used in the rest of this chapter:
<xs:simpleType name="isbn">
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{9}[0-9X]"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="currency">
<xs:restriction base="xs:decimal">
<xs:pattern value="\d+.\d{2}"/>
</xs:restriction>
</xs:simpleType>
<xs:element name="creator" type="person" abstract="true" />
<xs:element name="author" type="person" substitutionGroup="bib:creator"/>
<xs:element name="editor" type="personWithAffiliation" substitutionGroup="bib:creator"/>
Listing 1.28 Content Model for the Book Element
<xs:element name="book"> <xs:complexType> <xs:sequence> <xs:element name="title" type="xs:string"/> <xs:element ref="bib:creator" minOccurs="1" maxOccurs="unbounded"/> <xs:element name="publisher" type="xs:string"/> <xs:element name="price" type="currency"/> <xs:element name="year" type="xs:gYear"/> </xs:sequence> <xs:attribute name="isbn" type="bib:isbn"/> </xs:complexType> </xs:element>
The following elements are globally declared: bib, book, creator, author, editor. The type of the bib and book elements is “anonymous,” which means that the schema does not give these types explicit names.
All of the named types in this schema are global; in fact, in XML Schema, all named types are global.
Now let us explore the sequence type notation used to refer to constructs imported from the above schema. The basic form of an element test has two parameters: the name of the element and the name of the type:
element(creator, person)
To match an element, both the name and the type must match. The name will match if the element’s name is creator or in the substitution group of creator; thus, in the above schema, the names author and editor would also match. The type will match if it is person or any other type derived from person by extension or restriction; thus, in the above schema, personWithAffiliation would also match. The second parameter can be omitted; if it is, the type is taken from the schema definition. Because the schema declares the type of creator to be person, the following declaration matches the same elements as the previous declaration:
element(creator)
In XML Schema, element and attribute definitions may be local, available only within a specific element or type. A context path may be used to identify a locally declared element or attribute. For instance, the following declaration matches the locally declared price element, which is found in the globally declared book element:
element(book/price)
Although this form is generally used to match locally declared elements, it will match any element whose name is price and which has the same type as the price element found in the globally declared book element. A similar form is used to match elements or attributes in globally defined types:
element(type(person)/last)
The same forms can be used for attributes, except that (1) attributes never have substitution groups in XML Schema; (2) attributes are not nillable in XML Schema; and (3) the element name is preceded by the @ symbol in the XQuery syntax. For instance, the following declaration matches attributes named price of type currency:
attribute(@price, currency)
The following declaration matches attributes named isbn of the type found for the corresponding attribute in the globally declared book element:
attribute(book/@isbn)
Table 1.5 summarizes the declarations made available by importing the schema shown in Listing 1.27.element(n, person nillable)
Sequence Type Declaration | What It Matches |
element(creator, person) | An element named creator of type person |
element(creator) | Any element named creator of type xs:string—the type declared for creator in the schema. |
element(*, person) | Any element of type person. |
element(book/price) | An element named price of type currency—the type declared for price elements inside a book element. |
element(type(person)/last) | An element named last of type xs:string—the type declared for last elements inside the person type. |
attribute(@price, currency) | An attribute named price of type currency. |
attribute(book/@isbn) | An attribute named isbn of type isbn—the type declared for isbn attributes in a book element. |
attribute(@*, currency) | Any attribute of type currency. |
bib:currency | A value of the user-defined type currency" |
<n xsi:nil=”true” />
import schema namespace bib="urn:examples:xmp:bib"
define function discount-price($b as element(bib:book))
as xs:decimal
{
0.80 * $b//bib:price
}
It might be called in a query as follows:
for $b in doc("books.xml")//bib:book
where $b/bib:title = "Data on the Web"
return
<result>
{
$b/bib:title,
<price>{ discount-price($b/bib:price) }</price>
}
</result>
In the preceding query, the price element passed to the function exactly matches the declared type of the parameter. XQuery also defines some conversion rules that are applied if the argument does not exactly match the type of the parameter. If the type of the argument does not match and cannot be converted, a type error is raised. One important conversion rule is that the value of an element can be extracted if the expected type is an atomic type and an element is encountered. This is known as atomization. For instance, consider the query in Listing 1.29.
Listing 1.29Atomization
import schema namespace bib="urn:examples:xmp:bib" define function discount-price($p as xs:decimal) as xs:decimal { 0.80 * $p//bib:price } for $b in doc("books.xml")//bib:book where $b/bib:title = "Data on the Web" return <result> { $b/bib:title, <price>{ discount-price($b/bib:price) }</price> } </result>
When the typed value of the price element is extracted, its type is bib:currency. The function parameter expects a value of type xs:decimal, but the schema imported into the query says that the currency type is derived from xs:decimal, so it is accepted as a decimal.
In general, the typed value of an element is a sequence. If any value in the argument sequence is untyped, XQuery attempts to convert it to the required type and raises a type error if it fails. For instance, we can call the revised discount-price() function as follows:
let $w := <foo>12.34</foo>
return discount-price($w)
In this example, the foo element is not validated, and contains no type information. When this element is passed to the function, which expects a decimal, the function first extracts the value, which is untyped. It then attempts to cast 12.34 to a decimal; because 12.34 is a legitimate lexical representation for a decimal, this cast succeeds. The last conversion rule for function parameters involves type promotion: If the parameter type is xs:double, an argument whose type is xs:float or xs:decimal will automatically be cast to the parameter type; if the parameter type is xs:float, an argument whose type is xs:decimal will automatically be cast to the parameter type.import schema namespace bib="urn:examples:xmp:bib"
define function discount-price($p as element(bib:book/bib:price))
as xs:decimal
{
0.80 * $p
}
If the price element had an anonymous type, this would be the only way to indicate a price element of that type. Since our schema says a price element has the type bib:currency, the preceding function is equivalent to this one:
import schema namespace bib="urn:examples:xmp:bib"
define function discount-price($p as element(bib:price, bib:currency))
as xs:decimal
{
0.80 * $p
}
The same conversion rules that are applied to function arguments are also applied to function return values. Consider the following function:
define function decimate($p as element(bib:price, bib:currency))
as xs:decimal
{
$p
}
In this function, $p is an element named bib:price of type bib:currency. When it is returned, the function applies the function conversion rules, extracting the value, which is an atomic value of type bib:currency, then returning it as a valid instance of xs:decimal, from which its type is derived.
xs:date("2000-01-01")
Constructor functions check a value to make sure that the argument is a legal value for the given type and raise an error if it is not. For instance, if the month had been 13, the constructor would have raised an error.xs:string( 12345 )
Some types can be cast to each other, others cannot. The set of casts that will succeed can be found in [XQ-FO]. Constructor functions are also created for imported simple types—this is discussed in the section on imported schemas.import schema namespace bib="urn:examples:xmp:bib"
bib:isbn("012345678X")
The constructor functions for types check all the facets for those types. For instance, the following query raises an error because the pattern in the type declaration says that an ISBN number may not end with the character Y:
import schema namespace bib="urn:examples:xmp:bib"
bib:isbn("012345678Y")
Listing 1.30 Declaring the Type of a Variable
import schema namespace bib="urn:examples:xmp:bib" for $b in doc("books.xml")//bib:book let $authors as element(bib:author)+ := $b//bib:author return <result> { $b/bib:title, $authors } </result>
Since the schema for a bibliography allows a book to have editors but no authors, this query will raise an error if such a book is encountered. If a programmer simply assumed all books have authors, using a typed variable might identify an error in a query.
The instance of Operator
The instance of operator tests an item for a given type. For instance, the following expression tests the variable $a to see if it is an element node:
$a instance of element()
As you recall, literals in XQuery have types. The following expressions each return true:
<foo/> instance of element()
3.14 instance of xs:decimal
"foo" instance of xs:string
(1, 2, 3) instance of xs:integer*
() instance of xs:integer?
(1, 2, 3) instance of xs:integer+
The following expressions each return false:
3.14 instance of xdt:untypedAtomic
"3.14" instance of xs:decimal
3.14 instance of xs:integer
Type comparisons take type hierarchies into account. For instance, recall that SKU is derived from xs:string. The following query returns true:
import schema namespace bib="urn:examples:xmp:bib"
bib:isbn("012345678X") instance of xs:string
Listing 1.31 Function Using the typeswitch Expression
define function wrapper($x as xs:anySimpleType) as element() { typeswitch ($x) case $i as xs:integer return <wrap xsi:type="xs:integer">{ $i }</wrap> case $d as xs:decimal return <wrap xsi:type="xs:decimal">{ $d }</wrap> default return error("unknown type!") } wrapper( 1 )
The case clause tests to see if $x has a certain type; if it does, the case clause creates a variable of that type and evaluates the associated return clause. The error function is a standard XQuery function that raises an error and aborts execution of the query. Here is the output of the query in Listing 1.31:
<wrap xsi:type="xs:integer">1</wrap>
The case clauses test to see if $x has a certain type; if it does, the case clause creates a variable of that type and evaluates the first return clause that matches the type of $x. In this example, 1 is both an integer and a decimal, since xs:integer is derived from xs:decimal in XML Schema, so the first matching clause is evaluated. The error function is a standard XQuery function that raises an error and aborts execution of the query.Listing 1.32 Using typeswitch to Implement Simple Polymorphism
import schema namespace bib="urn:examples:xmp:bib" define function pay-creator( $c as element(bib:creator), $p as xs:decimal) { typeswitch ($c) case $a as element(bib:author) return pay-author($a, $p) case $e as element(bib:editor) return pay-editor($e, $p) default return error("unknown creator element!") }
The treat as Expression
The treat as expression asserts that a value has a particular type, and raises an error if it does not. It is similar to a cast, except that it does not change the type of its argument, it merely examines it. Treat as and instance of could be used together to write the function shown in Listing 1.33, which has the same functionality as the function in Listing 1.32.
Listing 1.33 Using treat as and instance of to Implement Simple Polymorphism
import schema namespace bib="urn:examples:xmp:bib" define function pay-creator( $c as element(bib:creator), $p as xs:decimal) { if ($c instance of element(bib:author)) then pay-author($a, $p) else if ($c instance of element(bib:editor)) then pay-editor($e, $p) else error("unknown creator element!") }
In general, typeswitch is preferable for this kind of code, and it also provides better type information for processors that do static typing.
Implicit Validation and Element Constructors
We have already discussed the fact that validation of the elements constructed in a query is automatic if the declaration of an element is global and is found in a schema that has been imported into the query. Elements that do not correspond to a global element definition are not validated. In other words, element construction uses XML Schema’s lax validation mode. The query in Listing 1.34 creates a fully validated book element, with all the associated type information.
Listing 1.34 Query That Creates a Fully Validated Book Element
import schema namespace bib="urn:examples:xmp:bib" <bib:book isbn="0201633469"> <bib:title>TCP/IP Illustrated</bib:title> <bib:author> <bib:last>Stevens</bib:last> <bib:first>W.</bib:first> </bib:author> <bib:publisher>Addison-Wesley</bib:publisher> <bib:price>65.95</bib:price> <bib:year>1994</bib:year> </bib:book>
Because element constructors validate implicitly, errors are caught early, and the types of elements may be used appropriately throughout the expressions of a query. If the element constructor in Listing 1.34 had omitted a required element or misspelled the name of an element, an error would be raised.
Relational programmers are used to writing queries that return tables with only some columns from the original tables that were queried. These tables often have the same names as the original tables, but a different structure. Thus, a relational programmer is likely to write a query like the following:
import schema namespace bib="urn:examples:xmp:bib"
for $b in doc("books.xml")//bib:book
return
<bib:book>
{
$b/bib:title,
$b//element(bib:creator)
}
</bib:book>
This query raises an error, because the bib:book element that is returned has a structure that does not correspond to the schema definition. Validation can be turned off using a validate expression, as shown in Listing 1.35, which uses skip.
Listing 1.35 Using validate to Disable Validation
import schema namespace bib="urn:examples:xmp:bib" for $b in doc("books.xml")//bib:book return validate skip { <bib:book> { $b/bib:title, $b//element(bib:creator) } </bib:book> }
The validate expression can also be used to specify a validation context for locally declared elements or attributes. For instance, the price element is locally declared:
import schema namespace bib="urn:examples:xmp:bib"
validate context bib:book
{
<bib:price>49.99</bib:price>
}
If an element’s name is not recognized, it is treated as an untyped element unless xsi:type is specified. For instance, the following query returns a well-formed element with untyped content, because the bib:mug element is not defined in the schema:
import schema namespace bib="urn:examples:xmp:bib"
<bib:mug>49.99</bib:mug>
A query can specify the type of an element using the xsi:type attribute; in this case, the element is validated using the specified type:
import schema namespace bib="urn:examples:xmp:bib"
<bib:mug xsi:type="xs:decimal">49.99</bib:mug>
If a locally declared element is not wrapped in a validate expression that specifies the context, it will generally be treated as a well-formed element with untyped content, as in the following query:
import schema namespace bib="urn:examples:xmp:bib"
<bib:price>49.99</bib:price>
To prevent errors like this, you can set the default validation mode to strict, which means that all elements must be defined in an imported schema, or an error is raised. This is done in the prolog. The following query raises an error because the bib:price element is not recognized in the global context:
import schema namespace bib="urn:examples:xmp:bib"
validation strict
<bib:price>49.99</bib:price>
The validation mode may be set to lax, which is the default behavior, strict, as shown above, or skip if no validation is to be performed in the query.