PhoenixmlDb
XQuery Library Modules
Importing XQuery library modules from files and http(s) URLs, the module settings and limits, resolution rules, fn:load-xquery-module, and the errors each host reports
#XQuery Library Modules
A library module is an XQuery file that begins module namespace p = "uri";. It is a file on disk
or a document at an http(s) URL. A query brings it in with
import module namespace p = "uri" at "location";
or with fn:load-xquery-module("uri", map { "location-hints": "location" }).
Library modules work in the embedded engine, the gRPC server and the REST server. For the language itself, see Functions and Modules.
#Enabling modules
Nothing outside the stored documents is readable by default. An operator lists the directories and http(s) origins that modules may be read from. The same lists govern every other read; see Resource Access.
#Embedded engine
using PhoenixmlDb.Storage;
using PhoenixmlDb.Storage.Security;
var options = new ResourceAccessOptions();
options.AllowedFileRoots.Add("/srv/xquery-modules"); // absolute path of an existing directory
options.AllowedHttpOrigins.Add("https://modules.example.com"); // scheme://host[:port], no path
using var db = new DocumentDatabase("/var/lib/mydb");
db.ResourceAccessPolicy = ResourceAccessPolicy.Create(options);
-
The default is
ResourceAccessPolicy.DenyAll. -
ResourceAccessPolicy.CreatethrowsArgumentExceptionlisting each bad value.ResourceAccessOptions.Validate()returns the same list. -
Under
ResourceAccessPolicy.Unrestricted, the XQuery library resolves and reads locations itself, and the limits and error texts on this page do not apply.
#Servers
The gRPC server (PhoenixmlDb.Server) and the REST server bind the same section,
PhoenixmlDb:ResourceAccess:
{
"PhoenixmlDb": {
"ResourceAccess": {
"AllowedFileRoots": [ "/srv/xquery-modules" ],
"AllowedHttpOrigins": [ "https://modules.example.com" ]
}
}
}
For the gRPC server, as environment variables:
PhoenixmlDb__ResourceAccess__AllowedFileRoots__0=/srv/xquery-modules
PhoenixmlDb__ResourceAccess__AllowedHttpOrigins__0=https://modules.example.com
A bad value stops startup with a message that names the key, for example:
PhoenixmlDb:ResourceAccess:MaxModules must be between 1 and 1024 (was 0).
The gRPC server logs the policy at startup:
file roots: …; HTTP origins: …; module limits: depth 32, 256 modules, 4194304 bytes each, 33554432 bytes in total; text resources: 67108864 bytes each, 268435456 bytes in total
#Settings
All settings are under PhoenixmlDb:ResourceAccess. Each is also a property of the same name on
ResourceAccessOptions.
|
Setting |
Default |
Meaning |
|---|---|---|
|
|
empty |
Absolute paths of existing directories. A query or stylesheet may read their files, at any depth. Nothing is writable. |
|
|
empty |
Origins a query or stylesheet may fetch from: |
|
|
|
How many imports or loads deep a module may be, counted from the query (a module the query imports is 1). Also bounds chains of schema documents. |
|
|
|
1–1024. How many distinct module files and schema documents one query brings in. |
|
|
|
1–268435456. Largest module file, schema document, or external entity fetched over http(s). |
|
|
|
|
|
|
|
1–1073741824. Largest file read by |
|
|
|
|
Bounds that are not settings:
-
An import chain longer than 64 modules is
XQST0059, whateverMaxModuleDepthis. It applies to the longest chain in the import graph. -
One http(s) fetch may take 30 seconds, headers and body together.
-
Redirects are not followed.
PhoenixmlDb:ResourceAccess:AllowStylesheetFileAccess is not a setting. A configuration that
carries the key, with any value, fails validation at startup:
PhoenixmlDb:ResourceAccess:AllowStylesheetFileAccess is not a setting: what a stylesheet may read follows PhoenixmlDb:ResourceAccess:AllowedFileRoots and PhoenixmlDb:ResourceAccess:AllowedHttpOrigins. Remove it.
#Worked example
Five documents in the container, b1.xml … b5.xml:
<book isbn="isbn-1"><title>t1</title><price>10</price></book>
<book isbn="isbn-2"><title>t2</title><price>20</price></book>
…
<book isbn="isbn-5"><title>t5</title><price>50</price></book>
Module file /srv/xquery-modules/shop.xqm, with /srv/xquery-modules in AllowedFileRoots:
module namespace shop = "urn:example:shop";
declare variable $shop:vat as xs:decimal := 0.2;
declare variable $shop:factor as xs:decimal := 1 + $shop:vat;
declare %private variable $shop:currency := "EUR";
declare %private function shop:format($amount as xs:decimal) as xs:string {
$shop:currency || " " || string($amount)
};
declare function shop:gross($book as element(book)) as xs:string {
shop:format(xs:decimal($book/price) * $shop:factor)
};
declare function shop:titles() as xs:string* {
collection()/book/title/string()
};
declare function shop:cheaper-than($limit as xs:decimal) as element(book)* {
collection()/book[xs:decimal(price) lt $limit]
};
declare function shop:by-isbn($isbn as xs:string) as element(book)* {
collection()/book[@isbn = $isbn]
};
declare function shop:label($book as element(book)) as xs:string {
"book " || $book/title
};
Query:
import module namespace shop = "urn:example:shop" at "/srv/xquery-modules/shop.xqm";
shop:gross(/book)
Result: EUR 12, EUR 24, EUR 36, EUR 48, EUR 60.
More queries with the same import:
|
Query |
Result |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
error |
|
|
error |
The same module through fn:load-xquery-module:
load-xquery-module("urn:example:shop", map { "location-hints": "/srv/xquery-modules/shop.xqm" })
?functions(QName("urn:example:shop", "gross"))(1)(/book)
Result: EUR 12, EUR 24, EUR 36, EUR 48, EUR 60.
#How a module function reaches the stored documents
-
A function body has no context item. Pass the node in (
shop:gross(/book)) or readcollection(). A function that uses/with no argument isXPDY0002. -
A query whose answer depends on one document at a time runs once per document.
-
A query that reaches
collection(), in its own text or inside an imported module function it calls, runs once over the whole container. -
A module that is only loaded at run time with
fn:load-xquery-moduleis not looked into when the engine decides how to run the query.count(collection())inside a loaded function answers once per document. Read the collection in the query text to get one answer.
#Resolution rules
|
Where the location is written |
It means |
|---|---|
|
In the query, absolute path or |
that file |
|
In the query, |
that URL |
|
In the query, relative, with |
relative to that base |
|
In the query, relative, with no base |
no location (see Relative references with no base) |
|
In a module on disk, relative |
relative to the directory of that module's file |
|
In a module fetched over http(s), relative |
relative to that module's URL |
|
In a module, absolute |
that file or URL |
-
Every location is checked against the allowlist, at every depth.
-
at "a.xqm", "b.xqm"loads both as one module. Every listed location must exist, hold a library module, and declare the imported namespace. -
Two modules that import each other compile and call each other.
-
A location is allowed when the file it resolves to is inside an allowed directory. A symbolic link inside an allowed directory that leads out of it is refused.
-
Only regular files are read.
-
A module is read once per query, however many documents the query runs over. Nothing is kept between queries: the next query reads and compiles the module again.
-
If the query and a module write the same relative hint and the two resolve to different files, the query is refused (
XQST0059 … name different files). -
import schemawritten inside a library module isXQST0009. Import the schema in the query. -
Cancelling the query stops the reading of modules.
-
A query that includes a module read from an http(s) location reads no files; see Modules read over HTTP.
#
fn:load-xquery-module
-
location-hintsis required. Without it:FOQM0002("no location-hints were given, and modules are loaded from location hints only"). -
The result is
map { "functions": map(QName → map(arity → function)), "variables": map(QName → value) }with the public declarations only. -
The
variablesoption (a map withxs:QNamekeys) binds the module's external variables.context-itemsupplies its context item. -
Several hints load one module.
-
The limits are the query's. A loaded module continues from the depth of the code that loads it and counts in the same totals.
-
The same load made again by the query itself in one evaluation (same module URI and locations, no
variables, nocontext-item) returns the module already loaded. Every other load is a new module. One evaluation loads at mostMaxModules. -
A loaded module runs under the database's query limits, including the regular-expression match time limit.
|
Code |
When |
|---|---|
|
|
empty module URI |
|
|
no |
|
|
the module has a static error (the module's own code is in the message) |
|
|
|
|
|
a malformed option |
|
|
an external variable was not supplied; the module needs a context item and none was given |
#Errors
How each host reports a query error:
|
Host |
Answer |
|---|---|
|
Embedded |
exception with |
|
REST server |
HTTP 400, problem body with |
|
gRPC, unary calls |
|
|
gRPC, streaming query |
status |
|
Situation |
|
|
Message says |
|---|---|---|---|
|
Location outside the allowed directories |
|
|
the location as written; "is not allowed by the resource access settings: it is not inside a directory listed in PhoenixmlDb:ResourceAccess:AllowedFileRoots." |
|
Origin not allowed |
|
|
the URL; "its origin is not listed in PhoenixmlDb:ResourceAccess:AllowedHttpOrigins." |
|
Another scheme ( |
|
|
"only file and http(s) locations can be allowed" |
|
Relative location with no base |
|
|
"it cannot be resolved to an absolute location (PhoenixmlDb:ResourceAccess)." |
|
A module's own import is refused |
|
|
the location as the module wrote it, "imported by the module at '…'", and the setting |
|
File missing inside an allowed directory |
|
|
"was not found: there is no file to read at that location." |
|
http(s) module missing |
|
|
"could not be read: GET … returned HTTP 404" |
|
Redirect |
|
|
"returned HTTP 302 (redirects are not followed)" |
|
Fetch past 30 s |
|
|
"did not complete within 30 s" |
|
Deeper than |
|
|
"is more than N imports deep, counted from the query (PhoenixmlDb:ResourceAccess:MaxModuleDepth)" |
|
More than |
|
|
the location and the setting |
|
Chain longer than 64 modules |
|
|
"is imported through a chain of more than 64 modules" |
|
Syntax error in a module |
|
|
"The module at '…' has a syntax error: line L, column C: …" |
|
File declares another namespace |
|
|
"holds a module for namespace 'A', not for the imported namespace 'B'." |
|
File is not a library module |
|
|
"is not a library module: it does not begin with a 'module namespace' declaration." |
|
|
|
|
"…not supported in a library module here: import the schema in the query that imports the module." |
|
Function the module does not declare |
|
— |
"Unknown function: name#arity" |
|
Private function or variable used by the query |
|
not in the result map |
|
|
Static error inside a module |
its own code |
|
the analyzer's message |
On the REST server, the compile-only endpoints read the imported modules too:
POST /api/query/validate reports each static error with its own code, and
POST /api/query/compile answers 400 with xqueryErrorCode.
#Known limits
-
Modules are not stored in the database. A module is a file or a URL.
-
There is no compiled-module cache. Every query reads and compiles its modules again, and fetches an http(s) module again.
-
A predicate inside a module function is not answered from a value index.
/book[@isbn = 'isbn-7'] ! shop:label(.)uses the index;shop:by-isbn('isbn-7')returns the right book from a scan. -
A module loaded only at run time does not change how the query is run (see How a module function reaches the stored documents).
-
fn:unparsed-textandfn:json-docin queries read files only, not http(s). -
XSLT cannot import XQuery modules.
#Next Steps
-
Resource Access: everything else a query or stylesheet may read