12. Paths and query
Requirement prefix: Q12.
openEHR paths#
- Q12.1 The crate MUST parse openEHR paths of the form
/attribute[predicate]/attribute…, with predicates in these forms:[at0004],[openEHR-EHR-OBSERVATION.x.v2],['Systolic'],[at0004, 'Systolic'],[archetype_node_id='at0004'],[name/value='Systolic'],[2], and conjunctions withandor,. - Q12.2 A parse failure MUST report the offset at which parsing stopped.
- Q12.3 Quoted digits MUST be a name, not an index:
['3']is an element named3. - Q12.4 Index predicates are zero-based. openEHR's own examples are inconsistent and implementations differ; this crate documents zero-based and means it, because every other index in it is zero-based and one crate with two conventions produces off-by-one errors that read as correct code.
- Q12.5
path_exists,path_unique,items_at_path, anditem_at_pathMUST all be available, anditem_at_pathMUST fail on an ambiguous path rather than returning the first match. Taking the first of three repeatedELEMENTs silently returns one of three diagnoses. - Q12.6 A path naming an attribute the node's class does not have MUST resolve to no match rather than to a parse error: the path is well formed and this instance does not have it.
- Q12.7 Navigation MUST cover the composition tree from
COMPOSITIONdown to primitive attributes of aDATA_VALUE, includingvalue/magnitude,value/units,value/value, anddefining_code. - Q12.7a Navigation MUST reach the
DV_ORDEREDattributes —normal_range,other_reference_ranges,normal_status— and MUST continue through aDV_INTERVALtolower,upper, the two*_unboundedflags and the two*_includedflags, and through aREFERENCE_RANGEtomeaningandrange. A reference range is queried in practice — "results outside their own normal range" is a population query a clinician actually asks — and a path that stops at the value cannot express it. - Q12.7b The
*_unboundedflags MUST be navigable even though they are derived rather than stored (base::interval). Whether the answer is looked up or computed is not something a path should have to know. - Q12.8 A parsed path MUST print in the long predicate form and MUST re-parse to an equal path.
AQL#
Q12.9 The crate MUST parse
SELECT(withDISTINCTandTOP), aliases, function calls,FROMwithCONTAINS/NOT CONTAINS/AND/ORand parentheses, archetype and standard predicates,WHEREwith the comparison operators andAND/OR/NOT/EXISTS/MATCHES/LIKE,$parameters,ORDER BYwithASC/DESC, andLIMIT/OFFSET.Q12.9a Constructs the crate does not model —
SELECT *, theVERSIONclass extension, terminology-function subqueries — MUST be refused with an error naming this requirement, never parsed-and-ignored. A partially-understood query that looks fully understood is the failure mode.Q12.9b (amended 2026-08-21 — was a limitation, now a rule) A numeric literal MAY carry a leading
-, and the crate MUST parse one.-1and-2.5are values.This was a stated limitation until 2026-08-21, and the reason it stayed one is the reason the amendment is worth reading:
-is also the character that separates the parts of an archetype id, so a sign could not simply be added to the number scanner.A-27recorded the decision as unmade.The decision, now made: the sign is resolved by the parser, at a position where an operand is expected — never by the number scanner. The lexer gains
-as an ordinary symbol and nothing else changes there. An archetype id cannot be affected, becauseopenEHR-EHR-COMPOSITION.encounter.v1begins with a letter and is therefore scanned as a word, which absorbs its own hyphens and never reaches the symbol branch. A-only ever stands alone where a word did not claim it.What follows from resolving it in the parser rather than the lexer:
WHERE o/value/magnitude > -2.5is a comparison against minus two point five — a base excess, a temperature difference, a scale scored below zero.MATCHES {-1, 0, 1}is a three-element set, becauseMATCHESparses its values as operands like everything else.> -openEHR-EHR-COMPOSITION.encounter.v1is an error, because a sign must be followed by a number. It is refused rather than read as anything.
Q12.9e (added 2026-08-21) A rendered numeric literal MUST lex back as the same kind of literal. A real that renders without a decimal point is a real that comes back an integer.
format!("{v}")writes0for0.0and-0for-0.0, and this lexer reads a run of digits with no.as an integer — soNumber(0.0)round tripped toInteger(0), a type change, whichQ12.15's tree equality forbids. It had been true since the renderer was written and was invisible untilQ12.9ballowed a sign, because-0.0is the first value where the text also differs:-0reparses asInteger(0)and renders0.Display and not
Debug:Debugforf64may use exponent form for extreme values, andeis not in this number scanner, so1e300would lex as1followed by a word. Display writes the full decimal expansion and never does that.Q12.9d (added 2026-08-21)
LIMITandOFFSETMUST refuse a signed literal, and MUST say so.They take a count, not a value, and
Q12.9bmakes-5lexically well-formed where it previously was not — so what used to be refused incidentally, asunexpected character, now has to be refused deliberately. ALIMITthat clamped-5to0would return an empty result set that looks like an answer (db:P6.15).Q12.9c Limitation. A bracketed predicate MUST be either the archetype shorthand (
c[openEHR-EHR-COMPOSITION.encounter.v1]) or a full condition. There is no node-id shorthand:c[at0001]is refused rather than read asarchetype_node_id = 'at0001'. Recorded asA-30, and pinned by a test — refusal is also what keeps a bare word from being reported as an archetype id to the authorisation check ofQ12.13.Q12.10 The crate MUST NOT execute AQL (
S1.5), and no API may return anything shaped like a result set.Q12.11 Keywords MUST be case-insensitive.
Q12.12 A parse failure MUST report a byte offset.
Q12.13 The archetype ids, aliases, and parameters a query touches MUST be enumerable before it runs. That is what an authorisation check needs.
Q12.14 A static check MUST report a path rooted at an alias that
FROMdoes not bind. This is the error AQL's syntax makes easiest to write — rename a class alias and miss oneSELECTcolumn — and hardest to see, because such a query is syntactically perfect and returns nothing.Q12.15 (amended 2026-08-21) A parsed query MUST render to text that re-parses to an equal query — the same tree, not merely one that parses.
"Equivalent" was the original word and it was too weak to be checkable. The
FROMrenderer emitted(a CONTAINS b OR c)forOr(Contains(a, b), c), and that text re-parses cleanly intoContains(a, Or(b, c))— a containing either b or c, where the caller wrote either (a containing b) or c. Both are valid queries over different records, so "it re-parses" was satisfied and the caller's query had still been rewritten. Seeaudit.mdA-37.Q12.15a (added 2026-08-21) The renderer MUST emit whatever parentheses the grammar needs to reproduce the tree it was given.
FROMputsCONTAINS,ANDandORat one precedence level and letsCONTAINStake the whole remainder as its right operand, so an operand that is not a bare class is parenthesised. A bare class is not, so the ordinary query still renders asEHR e CONTAINS COMPOSITION crather than(EHR e) CONTAINS (COMPOSITION c).Q12.15b (added 2026-08-21) Lexing a string literal MUST preserve it exactly, and rendering one MUST escape whatever the lexer treats as special.
The lexer copied the literal one byte at a time into a
String, widening each UTF-8 byte to its ownchar:'Müller'lexed to'Müller', and aWHEREagainst it matched nobody. The query parsed, checked clean, and was about a different string — there was nothing for a caller to notice. Scanning by byte is still correct, because the only bytes the lexer examines are ASCII delimiters and an ASCII byte never occurs inside a multi-byte sequence; what was wrong was copying by byte.Rendering escapes
'and\and nothing else, because the lexer's rule is "a backslash introduces the next character literally" rather than a C-style table — so a rendered\nwould mean the lettern.