XQuery
Prolog and Declarations
XQuery prolog β module declarations, namespace imports, options, and configuration
#Prolog and Declarations
Every XQuery file begins with an optional prolog, a header section that sets up the execution environment before the main query body runs. The prolog declares namespaces, imports modules, defines variables and functions, and configures serialization.
For C# developers: the prolog combines several familiar concepts. It holds
usingdirectives, assembly-level attributes,.csprojproperties, and global configuration, all at the top of the file.
#Contents
#What the Prolog Is
An XQuery file has two parts:
-
Prolog β declarations that configure the environment (optional)
-
Body β the expression that produces results (required in main modules)
(: === PROLOG === :)
xquery version "4.0";
declare namespace app = "http://example.com/app";
import module namespace util = "http://example.com/util" at "util.xqm";
declare variable $config := map { "debug": true() };
(: === BODY === :)
<result>{ util:process(collection("data"), $config) }</result>
C# parallel:
// === "Prolog" in C# ===
using System; // namespace import
using MyApp.Utilities; // module import
// assembly attributes, global usings, etc.
// === "Body" in C# ===
public class Program
{
static void Main() { /* ... */ } // the executable part
}
The prolog is not just syntactic sugar. It controls fundamental behaviors: how namespaces resolve, what modules are available, how output is serialized, how empty sequences sort, and more.
#Version Declaration
The version declaration must be the very first thing in the file (before any comments or whitespace):
xquery version "4.0";
This tells the processor which version of the XQuery specification to use. Common values:
|
Version |
Notes |
|---|---|
|
|
Original XQuery (2007) |
|
|
Maps, arrays, try/catch, group by (2014) |
|
|
JSON support, arrow operator, string constructors (2017) |
|
|
Latest β enhanced maps/arrays, |
You can also specify the encoding:
xquery version "4.0" encoding "UTF-8";
C# parallel:
// C# language version in .csproj
<PropertyGroup>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
If you omit the version declaration, the processor uses its default version (typically the latest it supports).
#Module Declaration
A module declaration turns an XQuery file into a library module β a reusable package of functions and variables that other queries can import. Library modules have no body expression; they only provide declarations.
module namespace util = "http://example.com/util";
(: This file is now a library module.
It can only contain function and variable declarations.
It cannot have a query body. :)
declare function util:format-date($date as xs:date) as xs:string {
format-date($date, "[MNn] [D], [Y]")
};
declare function util:truncate($text as xs:string, $max as xs:integer) as xs:string {
if (string-length($text) > $max)
then concat(substring($text, 1, $max - 3), "...")
else $text
};
The module namespace declaration binds a prefix (util) to a namespace URI (http://example.com/util). Every function and variable in the module must use this namespace prefix.
C# parallel:
// A C# class library (reusable, no Main method)
namespace Example.Util;
public static class StringHelpers
{
public static string Truncate(string text, int max) =>
text.Length > max ? text[..(max - 3)] + "..." : text;
}
#Namespace Declarations
Namespaces prevent name collisions, just like C# namespaces. XQuery provides several ways to declare them.
#Explicit Namespace Declaration
declare namespace app = "http://example.com/app";
declare namespace hr = "http://example.com/hr";
(: Now use the prefixes in the query :)
//hr:employee[hr:department = "Engineering"]
#Default Element Namespace
Sets the namespace for element names that have no prefix:
declare default element namespace "http://www.w3.org/1999/xhtml";
(: These unprefixed elements are now in the XHTML namespace :)
<html>
<head><title>Page</title></head>
<body><p>Content</p></body>
</html>
Without this declaration, unprefixed elements are in no namespace.
#Default Function Namespace
Sets the namespace for unqualified function calls:
declare default function namespace "http://example.com/app";
(: Calls http://example.com/app:process(), not fn:process() :)
process($data)
By default, the function namespace is http://www.w3.org/2005/xpath-functions (the fn namespace), so unqualified function calls like count(), string(), contains() resolve to the standard XPath functions.
#Pre-Declared Namespaces
These namespaces are always available without declaring them:
|
Prefix |
Namespace URI |
Purpose |
|---|---|---|
|
|
|
Standard functions: |
|
|
|
Schema types: |
|
|
|
Schema instance attributes |
|
|
|
|
|
|
|
Math functions: |
|
|
|
Map functions: |
|
|
|
Array functions: |
|
|
|
Local functions in main modules |
C# parallel:
// Pre-declared namespaces are like C#'s implicit global usings
// In .NET 6+, these are available without explicit "using":
// System, System.Collections.Generic, System.Linq, System.Threading.Tasks, etc.
#Imports
#Module Import
Imports functions and variables from a library module:
import module namespace util = "http://example.com/util" at "util.xqm";
import module namespace db = "http://example.com/db" at "lib/database.xqm";
The at clause specifies the physical location of the module file. Some processors can resolve modules by namespace URI alone (using a catalog or repository), making the at clause optional.
After importing, you can use the module's functions and variables:
import module namespace str = "http://example.com/strings" at "strings.xqm";
str:capitalize("hello world")
(: Result: "Hello World" :)
#Importing Modules with Annotated Declarations
When a library module uses %public and %private annotations, only the %public (or unannotated, which defaults to public) declarations are visible to the importer:
(: strings.xqm :)
module namespace str = "http://example.com/strings";
declare %public function str:capitalize($text as xs:string) as xs:string {
str:apply-case-rule($text, "upper")
};
declare %private function str:apply-case-rule($text as xs:string, $rule as xs:string) as xs:string {
(: internal implementation β not visible to importers :)
...
};
(: main.xq :)
import module namespace str = "http://example.com/strings" at "strings.xqm";
str:capitalize("hello") (: OK β %public :)
(: str:apply-case-rule(...) ERROR β %private, not accessible :)
C# parallel:
// C# project reference + using
// In .csproj: <ProjectReference Include="../Util/Util.csproj" />
using Example.Util;
StringHelpers.Capitalize("hello world");
#Multiple Module Locations
A single namespace can be implemented across multiple files:
import module namespace util = "http://example.com/util"
at "util-strings.xqm", "util-dates.xqm", "util-numbers.xqm";
#Schema Import
Imports an XML Schema for type-aware processing:
import schema namespace tns = "http://example.com/orders" at "orders.xsd";
(: Now you can use schema-defined types :)
declare variable $order as schema-element(tns:order) := doc("order.xml")/tns:order;
Schema imports enable the processor to validate data against the schema and use schema-defined types in type annotations.
C# parallel:
// Like generating C# classes from an XSD with xsd.exe or XmlSchemaClassGenerator
// xsd.exe /c orders.xsd
// Then using the generated types for strongly-typed access
#Variable Declarations
#Simple Variable
declare variable $app-name := "Customer Portal";
declare variable $max-results := 100;
declare variable $debug := false();
#Typed Variable
Adding a type annotation provides compile-time checking:
declare variable $count as xs:integer := 0;
declare variable $rate as xs:decimal := 0.085;
declare variable $start-date as xs:date := xs:date("2025-01-01");
C# parallel:
const string AppName = "Customer Portal";
const int MaxResults = 100;
static readonly DateTime StartDate = new DateTime(2025, 1, 1);
#External Variables
The query declares external variables. The calling environment provides their values β a .NET application, a command-line tool, or a database engine:
(: Declared in the query β no default value :)
declare variable $user-id as xs:string external;
declare variable $page as xs:integer external;
(: Declared with a default value β the caller may override it :)
declare variable $page-size as xs:integer external := 25;
The caller provides the values:
using PhoenixmlDb.XQuery.Execution;
// C# β setting external variables before execution
var engine = new QueryEngine();
var compiled = engine.Compile(query);
using var context = engine.CreateContext();
context.SetExternalVariable("user-id", userId);
context.SetExternalVariable("page", currentPage);
// page-size not set β defaults to 25
var results = new List<object?>();
await foreach (var item in compiled.ExecutionPlan!.ExecuteAsync(context))
{
results.Add(item);
}
C# parallel:
// External variables are like method parameters or configuration values
public static IEnumerable<Order> GetOrders(string userId, int page, int pageSize = 25)
{
// userId and page are required; pageSize has a default
}
#Annotations on Declarations
Variable and function declarations can carry annotations that control visibility and convey metadata. Annotations are prefixed with % and appear before the variable or function keyword:
(: Visibility annotations :)
declare %public variable $api-version := "2.0";
declare %private variable $internal-key := "abc123";
(: Function annotations :)
declare %public function local:get-data() { collection("data") };
declare %private function local:validate($x) { ... };
The two built-in annotations are:
|
Annotation |
Meaning |
Default |
|---|---|---|
|
|
Visible to importing modules |
Yes (functions and variables are public by default) |
|
|
Hidden from importing modules |
No |
Processors and frameworks may define additional annotations. For example, REST API frameworks often use annotations like %rest:path("/endpoint") and %rest:GET to bind XQuery functions to HTTP routes.
C# parallel:
// Annotations are like C# access modifiers + attributes
public static string ApiVersion = "2.0"; // %public
private static string InternalKey = "abc123"; // %private
[HttpGet("/endpoint")] // %rest:path, %rest:GET
public IActionResult GetData() { ... }
#Complex Variable Values
Variables can hold any XQuery value β sequences, maps, arrays, XML nodes:
declare variable $config := map {
"database": "production",
"max-connections": 10,
"features": ["search", "export", "notifications"]
};
declare variable $template :=
<email>
<from>[email protected]</from>
<subject/>
<body/>
</email>;
declare variable $valid-statuses := ("active", "pending", "suspended");
#Option Declarations
Option declarations configure the processor's behavior, most commonly serialization (how results are output).
#Serialization Options
Serialization options control the format of the query's output. They use the output namespace:
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";
(: Output as indented XML :)
declare option output:method "xml";
declare option output:indent "yes";
declare option output:encoding "UTF-8";
(: Output as HTML5 :)
declare option output:method "html";
declare option output:html-version "5";
declare option output:indent "yes";
(: Output as JSON :)
declare option output:method "json";
declare option output:indent "yes";
(: Output as plain text :)
declare option output:method "text";
JSON serialization is a common use case β output XQuery maps and arrays as JSON:
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";
declare option output:method "json";
declare option output:indent "yes";
map {
"users": array {
for $u in collection("users")/user
return map {
"id": string($u/@id),
"name": string($u/name),
"email": string($u/email)
}
}
}
This query produces formatted JSON output directly, combining XQuery's data querying with JSON serialization.
Common serialization options:
|
Option |
Values |
Default |
Purpose |
|---|---|---|---|
|
|
|
|
Output format |
|
|
|
|
Pretty-print output |
|
|
|
|
Character encoding |
|
|
|
|
Include |
|
|
|
|
Standalone attribute in XML declaration |
|
|
MIME type string |
varies by method |
Content type hint |
C# parallel:
// Serialization settings in C#
var settings = new XmlWriterSettings
{
Indent = true,
Encoding = Encoding.UTF8,
OmitXmlDeclaration = false
};
// Or for JSON:
var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
#Environment Settings
These declarations control the processor's behavior for the entire module.
#Construction Mode
Controls whether type annotations are preserved on copied nodes:
declare construction preserve; (: keep original type annotations :)
declare construction strip; (: remove type annotations β default :)
#Ordering Mode
Controls whether the processor is required to maintain document order:
declare ordering ordered; (: maintain document order β default :)
declare ordering unordered; (: processor may reorder for optimization :)
Using unordered can improve performance when you do not care about result order (for example, when counting or aggregating).
#Empty Order
Controls where empty sequences sort in order by clauses:
declare default order empty greatest; (: empty sorts last in ascending :)
declare default order empty least; (: empty sorts first in ascending β default :)
(: Practical impact :)
for $emp in //employee
order by $emp/middle-name (: some employees have no middle name :)
return $emp/name
(: With "empty greatest": employees without middle names sort last :)
(: With "empty least": employees without middle names sort first :)
C# parallel:
// C# LINQ equivalent
employees.OrderBy(e => e.MiddleName ?? "") // empty least
employees.OrderBy(e => e.MiddleName ?? "\uFFFF") // empty greatest
#Boundary Space
Controls how whitespace between constructors is handled:
declare boundary-space preserve; (: keep whitespace between expressions :)
declare boundary-space strip; (: remove boundary whitespace β default :)
(: With boundary-space preserve: :)
<name>{ $first } { $last }</name>
(: Result: <name>John Smith</name> (space between first and last is preserved) :)
(: With boundary-space strip: :)
<name>{ $first }{ $last }</name>
(: Result: <name>JohnSmith</name> (you must add space explicitly) :)
#Copy Namespaces
Controls namespace handling when nodes are copied:
declare copy-namespaces preserve, inherit; (: default :)
declare copy-namespaces no-preserve, inherit;
declare copy-namespaces preserve, no-inherit;
declare copy-namespaces no-preserve, no-inherit;
-
preserve / no-preserve controls whether in-scope namespaces from the original node are copied.
-
inherit / no-inherit controls whether the copied node inherits namespaces from its new parent.
#Base URI
Sets the base URI for resolving relative URIs in doc(), collection(), and other functions:
declare base-uri "https://data.example.com/";
doc("customers.xml") (: resolves to https://data.example.com/customers.xml :)
#Default Collation
Sets the default collation for string comparisons and sorting:
declare default collation "http://www.w3.org/2013/collation/UCA?lang=en;strength=secondary";
Collations control how strings are compared β whether case matters, how accented characters sort, and language-specific ordering rules.
C# parallel:
// C# culture-specific comparison
var comparer = StringComparer.Create(new CultureInfo("en-US"), ignoreCase: true);
names.OrderBy(n => n, comparer);
#Declaration Order
Declarations in the prolog must follow a specific order. While not all categories are required, those that are present must appear in this sequence:
(: 1. Version declaration (must be very first) :)
xquery version "4.0";
(: 2. Module declaration (library modules only) :)
module namespace mylib = "http://example.com/mylib";
(: 3. Namespace declarations :)
declare namespace app = "http://example.com/app";
declare default element namespace "http://example.com/elements";
(: 4. Imports :)
import module namespace util = "http://example.com/util" at "util.xqm";
import schema namespace tns = "http://example.com/types" at "types.xsd";
(: 5. Environment settings :)
declare construction strip;
declare ordering ordered;
declare default order empty greatest;
declare boundary-space strip;
declare copy-namespaces preserve, inherit;
declare base-uri "https://example.com/data/";
declare default collation "http://www.w3.org/2013/collation/UCA?lang=en";
(: 6. Option declarations :)
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";
declare option output:method "xml";
declare option output:indent "yes";
(: 7. Variable declarations :)
declare variable $config := map { "debug": false() };
declare variable $input external;
(: 8. Function declarations :)
declare function local:process($data) { (: ... :) };
(: 9. Body expression (main modules only β not in library modules) :)
local:process(doc("data.xml"))
C# parallel:
// C# has a similar convention (enforced by style rules, not the compiler):
// 1. File-level attributes
// 2. using directives
// 3. namespace declaration
// 4. class/struct declarations
// 5. Fields, then constructors, then methods
#Main Module vs Library Module
XQuery has two kinds of modules, and the prolog is what distinguishes them.
#Main Module
A main module has a body expression β the query that produces results. It is the entry point, like Program.Main() in C#:
xquery version "4.0";
import module namespace util = "http://example.com/util" at "util.xqm";
declare variable $input external;
(: Body expression β this is what gets executed :)
<report generated="{ current-dateTime() }">
{
for $item in $input//order
return util:format-order($item)
}
</report>
#Library Module
A library module has a module declaration instead of a body. It provides functions and variables for other modules to import:
xquery version "4.0";
module namespace util = "http://example.com/util";
declare function util:format-order($order as element(order)) as element(formatted-order) {
<formatted-order id="{ $order/@id }">
<customer>{ $order/customer/text() }</customer>
<total>{ util:format-currency($order/total) }</total>
<date>{ format-date(xs:date($order/date), "[MNn] [D], [Y]") }</date>
</formatted-order>
};
declare function util:format-currency($amount as xs:decimal) as xs:string {
concat("$", format-number($amount, "#,##0.00"))
};
Key differences:
|
Aspect |
Main Module |
Library Module |
|---|---|---|
|
Has |
No |
Yes |
|
Has a body expression |
Yes (required) |
No (forbidden) |
|
Can be executed directly |
Yes |
No β must be imported |
|
File extension (convention) |
|
|
|
C# equivalent |
|
Class library |
#Complete Example
Here is a realistic main module prolog that uses most declaration types:
xquery version "4.0";
(: Namespace declarations :)
declare namespace app = "http://example.com/crm";
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";
(: Import utility modules :)
import module namespace str = "http://example.com/strings" at "lib/strings.xqm";
import module namespace fmt = "http://example.com/format" at "lib/format.xqm";
(: Environment settings :)
declare default order empty greatest;
declare default collation "http://www.w3.org/2013/collation/UCA?lang=en;strength=secondary";
(: Serialization options :)
declare option output:method "xml";
declare option output:indent "yes";
declare option output:encoding "UTF-8";
(: External variables β set by the calling application :)
declare variable $report-date as xs:date external := current-date();
declare variable $department as xs:string external;
declare variable $include-inactive as xs:boolean external := false();
(: Internal configuration :)
declare variable $config := map {
"max-results": 500,
"currency": "USD",
"date-format": "[Y]-[M01]-[D01]"
};
(: Local functions :)
declare function local:get-employees($dept as xs:string) as element(employee)* {
collection("employees")/employee[department = $dept]
[if ($include-inactive) then true() else status = "active"]
};
declare function local:build-summary($employees as element(employee)*) as element(summary) {
<summary>
<total-count>{ count($employees) }</total-count>
<avg-tenure>{
avg(
for $e in $employees
return days-from-duration(current-date() - xs:date($e/hire-date))
) div 365
}</avg-tenure>
<departments>{ count(distinct-values($employees/department)) }</departments>
</summary>
};
(: Body expression :)
let $employees := local:get-employees($department)
return
<app:report date="{ format-date($report-date, $config?date-format) }">
{ local:build-summary($employees) }
<employees>
{
for $emp in $employees
order by $emp/last-name collation
"http://www.w3.org/2013/collation/UCA?lang=en;strength=secondary"
return
<employee id="{ $emp/@id }">
<name>{ str:title-case(concat($emp/first-name, " ", $emp/last-name)) }</name>
<title>{ $emp/job-title/text() }</title>
<hired>{ format-date(xs:date($emp/hire-date), "[MNn] [D], [Y]") }</hired>
<salary>{ fmt:currency($emp/salary, $config?currency) }</salary>
</employee>
}
</employees>
</app:report>
Running this from C#:
using PhoenixmlDb.XQuery.Execution;
var engine = new QueryEngine();
var compiled = engine.Compile(File.ReadAllText("employee-report.xq"));
using var context = engine.CreateContext();
context.SetExternalVariable("department", "Engineering");
context.SetExternalVariable("report-date", DateTime.Now.ToString("yyyy-MM-dd"));
context.SetExternalVariable("include-inactive", false);
string report = "";
await foreach (var item in compiled.ExecutionPlan!.ExecuteAsync(context))
{
report += item;
}
Console.WriteLine(report);