7. AQL at the federation façade
The model rests on keeping two distinct AQL surfaces apart:
-
Façade AQL - what the client sends to the Federation Tier. It MAY identify the patient with
WHERE e/ehr_status/subject/external_ref/id/value = <patientId>. This is standard openEHR AQL. The gateway interprets the subject predicate and does not forward it. -
Node AQL - what the gateway dispatches to each node. It is standard, non-federated AQL, scoped to that node’s resolved
ehr_id, and contains no subject predicate.
7.1 The rewrite rule (normative)
For each node with a resolved ehr_id, the gateway produces the node AQL from the façade AQL by:
-
Substituting the patient predicate - replace
e/ehr_status/subject/external_ref/id/value = <patientId>withe/ehr_id/value = '<resolvedEhrId>'(equivalently, scope viaFROM EHR e[ehr_id/value='<resolvedEhrId>'], or send the query toPOST {base}/v1/query/aqlwith theehr_idquery parameter /openehr-ehr-idheader set to the resolved value). All three are legitimate single-EHR scopings; the canonical form in this spec is theWHERE e/ehr_id/value = …predicate (§7a.2, N29). -
Stripping every other patient identifier - any remaining directly identifying identifier, wherever it sits (
PARTY_IDENTIFIED/DV_IDENTIFIERpredicates, anENTRY-levelsubject, a projection path, a parameter, a header), MUST be consumed by resolution and removed, or the query rejected. The node receives theehr_idand nothing else that identifies the patient (§5.4, N33). -
Forwarding everything else unchanged - all other
WHEREterms go to the node verbatim, and execute within it (N9).LIMIT/OFFSETandORDER BYare the exception: they execute per node and are re-applied at the Tier, under the rules of §11.6. ForwardingLIMITverbatim is correct; forwardingOFFSETverbatim is not. -
Re-injecting the subject column if selected - if the façade AQL SELECTs the subject path (
e/ehr_status/subject/external_ref/id/value), the gateway MUST add that column back to each row as a constantSTRINGequal to the resolution input<patientId>. The value is the input, not something read from the CDR, since nodes may not store it. (N5.)
Reduction constraint. If a façade AQL cannot be reduced to a single ehr_id scope per node - say multiple subject predicates OR’d together with non-subject terms, so that no single resolved ehr_id covers the request - the gateway MUST reject the query with HTTP 400 instead of guessing.
7.2 Before → after example
Façade AQL received (patient identified the openEHR-idiomatic way):
SELECT c/uid/value AS composition_id, c/context/start_time/value AS start_time
FROM EHR e CONTAINS COMPOSITION c
WHERE e/ehr_status/subject/external_ref/id/value = '12345'
AND c/archetype_node_id = 'openEHR-EHR-COMPOSITION.encounter.v1'
Node AQL dispatched (canonical form: ehr_id in WHERE; the gateway MAY instead scope via FROM EHR e[ehr_id/value=…] or the ehr_id request parameter):
SELECT c/uid/value AS composition_id, c/context/start_time/value AS start_time
FROM EHR e CONTAINS COMPOSITION c
WHERE e/ehr_id/value = '550e8400-e29b-41d4-a716-446655440000'
AND c/archetype_node_id = 'openEHR-EHR-COMPOSITION.encounter.v1'
Here '550e8400-…' is the ehr_id the cross-reference service resolved for subject = '12345' at that particular node. A different node would receive a different ehr_id. Because composition_id is a c/uid/value, it is an OBJECT_VERSION_ID and already carries the creating_system_id that §12 routes follow-ups on. No uid rewriting is needed.
If the façade AQL had also SELECTed e/ehr_status/subject/external_ref/id/value AS patient_id, the gateway would re-inject patient_id = '12345' (the input) into every row, even though no node stores it.