Stored queries
A deployment that sets [stored_queries] offers the federated stored-query
registry, and OPTIONS {base}/ declares it as
definition.stored_query_registry: true (§12.7, N44). The gateway then holds
each definition itself: it is authoritative for it, and a query invoked by
name runs over every member exactly as if you had sent its text to
POST {base}/v1/query/aql.
Store a query with its AQL as the text/plain body, at a version:
PUT {base}/v1/definition/query/org.example::compositions/1.0.0
Content-Type: text/plain
SELECT c/uid/value FROM EHR e CONTAINS COMPOSITION c
WHERE e/ehr_status/subject/external_ref/id/value = $patient
AND e/ehr_status/subject/external_ref/namespace = 'urn:oid:2.999.1'
- The name is
[{namespace}::]{query-name}overa-z,A-Z,0-9,_,.and-, and the query name is neveraql(ITS-REST). The version ismajor.minor.patch(ITS-REST’s semver path segment, §12.7). Anything else is a400(query-name-invalid,query-version-invalid). The body istext/plain: send thatContent-Type, withcharset=utf-8if you like, or none. Any other is a415(media-type-unsupported), and nothing is stored. APUTwith no version is a400(query-version-required), because the registry stores only at a version.query_type, when sent, isAQLin any case. - A stored version is immutable. A second
PUTto a name and version the registry holds is a409(stored-query-held), the held text stands, and so does the refusal after the gateway restarts. Store a change as a new version (§12.7, N44). This holds for every secondPUT, the same query naming members included; a member that misses a version is sent it by the operator (repairing drift). - The gateway analyses the text as it analyses a query you send, with every
$parameterstanding in for a value you will bind. A text it would refuse whatever you bind is refused now,400with the same code a query would get (not-aql,unreducible, and so on). A refusal that depends on how you target the query, such as an aggregate across several members, is left to each invocation. - Name the patient through a
$parameter. A definition that names the patient by a literal identifier is refused400(subject-literal), because the registry would hold that identifier at rest (§5.4.1, N33). The message never quotes it, and the security log records only the position of the predicate. - The answer is
200withLocationnaming the stored version, as a reference relative to the request URL, because the gateway does not know the base URL its clients use (§4.1). - The namespace
eu.ferrofed.eehrxf, and every namespace nested under it, in any letter case, holds the gateway’s own queries (the gateway’s own queries). APUTthere is a409(stored-query-reserved) at any version, and nothing is stored. - A deployment may run the registry read-only, its definitions published by
its operator. Every
PUTthere is a405(stored-query-read-only) withAllow: GET, OPTIONS, andOPTIONSon the path lists noPUT; reading and running the definitions it holds work as below. - Behind several replicas sharing one registry, a version one replica
stored is read and run at every other, and of two
PUTs of the same new version at once exactly one is stored; the other is a409(stored-query-held).
The gateway holds the canonical print of the parsed query, so a comment or
any other text the parser drops is not kept, and GET returns the query in
that form.
Read it back with GET on the same path, which answers the ITS-REST
StoredQuery (name, type, version, saved, q), or 404
(stored-query-unknown). GET {base}/v1/definition/query/{pattern} lists
every version of every stored query whose name starts with the pattern.
Run it by name, binding its parameters in the ITS-REST Query body:
POST {base}/v1/query/org.example::compositions
Content-Type: application/json
{"query_parameters": {"patient": "12345"}}
- Without a version, the highest version runs.
/{version}picks one: an exactmajor.minor.patch, or a{major}or{major}.{minor}prefix that runs the highest version it matches (ITS-REST). A name or version the registry does not hold is a404(stored-query-unknown). - Every rule of an inline query applies unchanged:
offsetandfetch, the completeness and dedup headers,Prefer: wait, and the targeting headers. A definition that carriesFROM ENDPOINTorORGANISATIONis targeted by it, and a header that selects other endpoints is a400(targeting-conflict). - A parameter you do not bind, or bind and the query does not use, is a
400(parameters) naming it, never its value. - The
Querybody is JSON, under the sameContent-Typerule as an inline query:application/jsonor none, and any other is a415(media-type-unsupported) that asks no node. - The answer is the ordinary federated
RESULT_SET, withnamenaming the gateway’s stored query andqits stored text (§9.1, §12.7). No node receives your patient identifier, and no node receives the stored query by name: each gets the standard AQL of an inline query. GET {base}/v1/query/{name}[/{version}]runs it the same way with the members in the query string, as the GET form of an inline query does:offsetandfetchby name, and every other pair a query parameter. The storedGETforms declare noq, soq=…binds$q.ehr_idis dropped, and a query string the decoder refuses is a400(body-invalid).
Templates go to the one node you name (templates and
definitions). Without the registry, GET and
POST {base}/v1/query/{name} answer 501.
See how it works: definitions and stored queries.
The gateway’s own queries
Wherever the registry is offered, it also holds the gateway’s own stored
queries, read-only, under the namespace eu.ferrofed.eehrxf at the one
version 1.0.0. ITS-REST names a stored query [{namespace}::]{query-name},
the namespace “in a form of a reverse domain name”, so the gateway’s sits
under the reverse of ferrofed.eu. Today they are the patient summary’s
section queries, one per section openEHR content feeds:
eu.ferrofed.eehrxf::patient-summary-{section}, where {section} is
allergies-and-intolerances, problems, medication-summary,
medical-devices-and-implants, procedures, immunisations,
social-history, pregnancy-history, advance-directives,
observation-results or care-plans.
-
List them with
GET {base}/v1/definition/query/eu.ferrofed.eehrxf::and read one as any other;savedis the day its version was fixed. -
Run one by name, binding the patient and the namespace that issued the identifier:
POST {base}/v1/query/eu.ferrofed.eehrxf::patient-summary-problems Content-Type: application/json {"query_parameters": {"patient": "12345", "namespace": "urn:oid:2.999.1"}}Each row is one composition that contains one of the section’s archetypes: the composition, its uid and its template id, from every member, with each member’s status in
meta.federation. No node receives the identifier. -
No
PUTstores into the namespace, and a version there never changes (§12.7, N44). A store that holds a definition there, from a restore, a manual insert or a definition file, refuses the start, andferrofed config checkrefuses such a definition file, naming the definition and never its text.
Which archetypes select each section, and the sections no query feeds, are listed in the clinical safety risk file.
Distributing a stored query
A deployment that sets federation.fan_out_stored_queries beside the
registry also distributes a definition to the members you name, and
OPTIONS {base}/ declares definition.stored_query_fan_out: true (§12.7,
N44). It is off by default, and it is never declared without the registry.
Where the registry is offered without it, a stored-query PUT or a GET of
a version that carries openEHR-federation-endpoint or
openEHR-federation-organisation is a 400
(stored-query-fan-out-unsupported): nothing is stored or read, so a
request for distribution is never answered as a plain one. Where it is
offered:
- Ask for it on the
PUT:openEHR-federation-endpoint: *names every active member, and a header that selects endpoints names those. APUTthat names none is stored at the registry alone, as above. A list naming a suspended endpoint, or*with no active member, is a404(no-destination) and nothing is stored. - The registry stores the definition first, under every rule above. Each
named member is then sent the registry’s copy, its canonical AQL, with
ITS-REST
PUT /definition/query/{name}/{version}andquery_type=AQL, on its own. No header of yours is sent. A member that fails never removes the registry’s definition, and a member that accepted is never sent a rollback. - The answer is the registry’s
StoredQuery(name,type,version,saved,q) withmeta.federationbeside it, oneendpoints[]entry per registry member in the shape of a federated result set’s (§9.5), andLocationnaming the stored version, andmeta.registry: "stored"says the request stored it. The statuses are those of the template fan-out:200when every member you named accepted,207withcomplete: falsewhen some did, and504or424when none did. Whatever the status, the registry holds the definition. A member that failed carries its HTTP status and an excerpt of its message inerror, as in a query’s answer (A node’s error inendpoints[]); no other part of a node’s body is copied into the answer. - A definition whose AQL carries a
FROM ENDPOINTorORGANISATIONdirective is refused400(definition-endpoint-targeted) and nothing is stored: a node cannot run a directive that names members of the federation (§12.7, §8.1). Store it without naming members and it runs federated, targeted by its directive. - An invocation always runs the registry’s copy, inline, whatever a member holds under the same name (§12.7).
PUT {base}/v1/definition/query/org.example::compositions/1.0.0
openEHR-federation-endpoint: *
Content-Type: text/plain
SELECT c/uid/value FROM EHR e CONTAINS COMPOSITION c
WHERE e/ehr_status/subject/external_ref/id/value = $patient
AND e/ehr_status/subject/external_ref/namespace = 'urn:oid:2.999.1'
A member’s copy can drift from the registry’s: a failed distribution, a
local PUT at the node, a restore, or a member admitted later (§12.7). To
check, GET the version naming members in the same headers:
GET {base}/v1/definition/query/org.example::compositions/1.0.0
openEHR-federation-endpoint: *
- Each named member is asked for its copy of that version with ITS-REST
GET /definition/query/{name}/{version}. A{major}or{major}.{minor}prefix selects the registry’s version first, and the members are asked for that one. - The answer is the registry’s
StoredQuerywithmeta.federation. A member whose copy is the same query isactive; layout and comments do not count, because both sides are compared as their canonical prints. A member whose copy differs isnode-errorwitherror.code: "definition-differs", and one that holds none isnode-errorwitherror.code: "definition-missing". A member that fails or does not answer is reported as in the distribution. No member’s copy is copied into the answer. - The status is
200when every member you named matches, and207withcomplete: falseotherwise.openEHR-federation-endpointandopenEHR-federation-system-idlist the matching members. - Without a header the
GETanswers from the registry alone, as above.
Repairing drift
A member that missed a distribution, or was admitted after it, does not get
the version through the ITS-REST surface: every second PUT of a held
version is a 409 (§12.7). The specification gives drift repair no
request, so the gateway offers it to its operator alone, on the
admin listener beside the metrics. Where
[metrics] listen is unset, the action does not exist. It answers a
caller whose access token carries the operator_scope of its issuer, from
any address; without a token it is 401 (unauthenticated), and without
the scope 403 (scope-insufficient)
(Who the admin listener serves).
Run the drift check above to find the members, then name them in the same
targeting headers:
POST /admin/stored-queries/org.example::compositions/1.0.0/distribute
Authorization: Bearer <an access token with the operator scope>
openEHR-federation-endpoint: node-c-pub
- The request carries no body. Each named member is sent the registry’s
held copy of that exact version, as in a first distribution, and the
registry’s copy, its
savedtime included, does not change. - The answer has the shape and the statuses of a first distribution (
200,207withcomplete: false,504or424), andmeta.registry: "held"says the registry held the version and stored nothing. - It is refused before any node is asked:
404(stored-query-unknown) for a version the registry does not hold;400for a request naming no member (target-required), a body (body-invalid), a deployment withoutfederation.fan_out_stored_queries(stored-query-fan-out-unsupported), or a definition carrying aFROM ENDPOINTorORGANISATIONdirective (definition-endpoint-targeted). - A read-only registry answers it
405(stored-query-read-only) with an emptyAllow: its operator publishes the definitions, and the gateway distributes none of them (RFC 9110 §10.2.1).