Skip to main content

openehr_query/
ast.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-FileCopyrightText: openEHR Foundation
3// SPDX-License-Identifier: Apache-2.0
4
5//! AQL abstract syntax tree, transcribed from `AqlParser.g4` (vendored at
6//! `vendor/grammar/`).
7//!
8//! Each grammar rule maps to a type here; [`crate::parser`] builds these from
9//! the [`crate::lexer`] token stream. This is the SYNTACTIC tree only —
10//! resolving paths and typing quoted temporal literals are semantic concerns.
11//!
12//! Every [`IdentifiedPath`] and every WHERE condition carries a [`Span`]: where
13//! it was written in the source, so a diagnostic can name a position instead of
14//! echoing the text there. Spans never take part in equality.
15
16use std::ops::Range;
17
18use crate::lexer::CompOp;
19
20/// Where a node was written: a half-open byte range into the AQL source.
21///
22/// A span is position metadata, not syntax, so equality ignores it: two nodes
23/// are equal when their syntax is, wherever each was written. That keeps the
24/// printer's round-trip invariant (`parse(to_aql(ast)) == ast`) a statement
25/// about syntax alone. A node built in code, or parsed from a bare token slice
26/// ([`crate::parser::parse`]), has no source position and carries the
27/// [`Span::default`] unknown span.
28///
29/// Offsets are held as `u32`, which keeps a span small enough to ride on every
30/// path; a position past 4 GiB of source is not representable and reads as
31/// unknown.
32#[derive(Debug, Clone, Copy, Default)]
33pub struct Span {
34    bytes: Option<(u32, u32)>,
35}
36
37impl Span {
38    /// A span over `bytes` of the source.
39    #[must_use]
40    pub fn new(bytes: Range<usize>) -> Self {
41        let start = u32::try_from(bytes.start).ok();
42        let end = u32::try_from(bytes.end).ok();
43        Self {
44            bytes: start.zip(end),
45        }
46    }
47
48    /// The half-open byte range of the source this node was written at, or
49    /// `None` when the node has no source position.
50    #[must_use]
51    pub fn bytes(&self) -> Option<Range<usize>> {
52        let (start, end) = self.bytes?;
53        let start = usize::try_from(start).ok()?;
54        let end = usize::try_from(end).ok()?;
55        Some(start..end)
56    }
57}
58
59impl PartialEq for Span {
60    fn eq(&self, _other: &Self) -> bool {
61        true
62    }
63}
64
65impl Eq for Span {}
66
67/// `selectQuery : selectClause fromClause whereClause? orderByClause? limitClause?`
68#[derive(Debug, Clone, PartialEq)]
69pub struct SelectQuery {
70    /// `SELECT [DISTINCT] [TOP n] col, …`
71    pub select: SelectClause,
72    /// `FROM <containsExpr>`
73    pub from: ContainsExpr,
74    /// `WHERE <whereExpr>`
75    pub where_: Option<WhereExpr>,
76    /// `ORDER BY …`
77    pub order_by: Vec<OrderByExpr>,
78    /// `LIMIT n [OFFSET m]`
79    pub limit: Option<Limit>,
80}
81
82/// `selectClause : SELECT DISTINCT? top? selectExpr (',' selectExpr)*`
83#[derive(Debug, Clone, PartialEq)]
84pub struct SelectClause {
85    /// `DISTINCT` present.
86    pub distinct: bool,
87    /// The deprecated `TOP n [FORWARD|BACKWARD]`.
88    pub top: Option<Top>,
89    /// One or more selected columns.
90    pub columns: Vec<SelectExpr>,
91}
92
93/// The deprecated `top : TOP INTEGER (FORWARD|BACKWARD)?`.
94#[derive(Debug, Clone, PartialEq)]
95pub struct Top {
96    /// The row count.
97    pub count: i64,
98    /// Optional direction.
99    pub direction: Option<TopDirection>,
100}
101
102/// `TOP` direction.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum TopDirection {
105    /// `FORWARD`
106    Forward,
107    /// `BACKWARD`
108    Backward,
109}
110
111/// `selectExpr : columnExpr (AS aliasName)?`
112#[derive(Debug, Clone, PartialEq)]
113pub struct SelectExpr {
114    /// The selected expression.
115    pub column: ColumnExpr,
116    /// Optional `AS alias`.
117    pub alias: Option<String>,
118}
119
120/// `columnExpr : identifiedPath | primitive | aggregateFunctionCall | functionCall`
121#[derive(Debug, Clone, PartialEq)]
122pub enum ColumnExpr {
123    /// A data path.
124    Path(IdentifiedPath),
125    /// A literal.
126    Primitive(Primitive),
127    /// `COUNT`/`MIN`/`MAX`/`SUM`/`AVG`.
128    Aggregate(AggregateCall),
129    /// A named function call.
130    Function(FunctionCall),
131}
132
133/// `containsExpr` — a boolean tree whose leaves are class expressions each
134/// optionally constrained by a nested `CONTAINS`.
135#[derive(Debug, Clone, PartialEq)]
136pub enum ContainsExpr {
137    /// `classExprOperand (NOT? CONTAINS containsExpr)?`
138    Contained {
139        /// The class/version operand.
140        operand: ClassExprOperand,
141        /// Optional `[NOT] CONTAINS <sub>`.
142        contains: Option<Box<ContainsConstraint>>,
143    },
144    /// `containsExpr AND containsExpr`
145    And(Box<ContainsExpr>, Box<ContainsExpr>),
146    /// `containsExpr OR containsExpr`
147    Or(Box<ContainsExpr>, Box<ContainsExpr>),
148}
149
150/// The `[NOT] CONTAINS <containsExpr>` tail of a [`ContainsExpr::Contained`].
151#[derive(Debug, Clone, PartialEq)]
152pub struct ContainsConstraint {
153    /// `NOT CONTAINS`.
154    pub negated: bool,
155    /// The contained sub-expression.
156    pub expr: ContainsExpr,
157}
158
159/// `classExprOperand : #classExpression | #versionClassExpr`
160#[derive(Debug, Clone, PartialEq)]
161pub enum ClassExprOperand {
162    /// `IDENTIFIER variable? pathPredicate?` (e.g. `COMPOSITION c[openEHR-…]`).
163    Class {
164        /// The RM class name.
165        rm_type: String,
166        /// Optional bound variable.
167        variable: Option<String>,
168        /// Optional `[predicate]`.
169        predicate: Option<PathPredicate>,
170    },
171    /// `VERSION variable? [versionPredicate]?`
172    Version {
173        /// Optional bound variable.
174        variable: Option<String>,
175        /// Optional version predicate.
176        predicate: Option<VersionPredicate>,
177    },
178}
179
180/// `whereExpr` — a boolean tree of [`IdentifiedExpr`] leaves.
181#[derive(Debug, Clone, PartialEq)]
182pub enum WhereExpr {
183    /// A leaf condition, and where it was written.
184    Identified(IdentifiedExpr, Span),
185    /// `NOT whereExpr`
186    Not(Box<WhereExpr>),
187    /// `whereExpr AND whereExpr`
188    And(Box<WhereExpr>, Box<WhereExpr>),
189    /// `whereExpr OR whereExpr`
190    Or(Box<WhereExpr>, Box<WhereExpr>),
191}
192
193impl WhereExpr {
194    /// A leaf condition with no source position, for a tree built in code.
195    #[must_use]
196    pub fn identified(expr: IdentifiedExpr) -> Self {
197        Self::Identified(expr, Span::default())
198    }
199}
200
201/// `identifiedExpr` — a single WHERE condition.
202#[derive(Debug, Clone, PartialEq)]
203pub enum IdentifiedExpr {
204    /// `EXISTS identifiedPath`
205    Exists(IdentifiedPath),
206    /// `identifiedPath|functionCall COMPARISON_OPERATOR terminal`
207    Compare {
208        /// Left operand.
209        lhs: CompareOperand,
210        /// The operator.
211        op: CompOp,
212        /// Right operand.
213        rhs: Terminal,
214    },
215    /// `identifiedPath LIKE likeOperand`
216    Like {
217        /// The path.
218        path: IdentifiedPath,
219        /// String or parameter.
220        operand: LikeOperand,
221    },
222    /// `identifiedPath MATCHES matchesOperand`
223    Matches {
224        /// The path.
225        path: IdentifiedPath,
226        /// The match set.
227        operand: MatchesOperand,
228    },
229    /// A condition already resolved to a constant during semantic analysis —
230    /// the product of evaluating a `TERMINOLOGY()` Boolean value expression
231    /// (QUERY master03 §TERMINOLOGY: "as a Boolean value expression"). Never
232    /// produced by the parser.
233    Resolved(bool),
234}
235
236/// Left side of a comparison: a path or a function call.
237#[derive(Debug, Clone, PartialEq)]
238pub enum CompareOperand {
239    /// `identifiedPath`
240    Path(IdentifiedPath),
241    /// `functionCall`
242    Function(FunctionCall),
243}
244
245/// `terminal : primitive | PARAMETER | identifiedPath | functionCall`
246#[derive(Debug, Clone, PartialEq)]
247pub enum Terminal {
248    /// A literal.
249    Primitive(Primitive),
250    /// `$param`.
251    Parameter(String),
252    /// A path.
253    Path(IdentifiedPath),
254    /// A function call.
255    Function(FunctionCall),
256}
257
258/// `orderByExpr : identifiedPath (DESCENDING|DESC|ASCENDING|ASC)?`
259#[derive(Debug, Clone, PartialEq)]
260pub struct OrderByExpr {
261    /// The path to order by.
262    pub path: IdentifiedPath,
263    /// Sort direction (defaults to ascending when absent).
264    pub order: Option<SortOrder>,
265}
266
267/// Sort direction.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum SortOrder {
270    /// `ASC` / `ASCENDING`
271    Ascending,
272    /// `DESC` / `DESCENDING`
273    Descending,
274}
275
276/// `limitClause : LIMIT INTEGER (OFFSET INTEGER)?`
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct Limit {
279    /// Row limit.
280    pub limit: i64,
281    /// Optional offset.
282    pub offset: Option<i64>,
283}
284
285/// `identifiedPath : IDENTIFIER pathPredicate? ('/' objectPath)?`
286#[derive(Debug, Clone, PartialEq)]
287pub struct IdentifiedPath {
288    /// The root variable/identifier.
289    pub root: String,
290    /// Optional predicate on the root.
291    pub predicate: Option<PathPredicate>,
292    /// Optional trailing `/a/b/c` object path.
293    pub path: Option<ObjectPath>,
294    /// Where the path was written, root to last part.
295    pub span: Span,
296}
297
298impl IdentifiedPath {
299    /// A path with no source position, for a tree built in code.
300    #[must_use]
301    pub fn new(root: String, predicate: Option<PathPredicate>, path: Option<ObjectPath>) -> Self {
302        Self {
303            root,
304            predicate,
305            path,
306            span: Span::default(),
307        }
308    }
309}
310
311/// `objectPath : pathPart ('/' pathPart)*`
312#[derive(Debug, Clone, PartialEq)]
313pub struct ObjectPath {
314    /// The `/`-separated parts.
315    pub parts: Vec<PathPart>,
316}
317
318/// `pathPart : IDENTIFIER pathPredicate?`
319#[derive(Debug, Clone, PartialEq)]
320pub struct PathPart {
321    /// The attribute name.
322    pub name: String,
323    /// Optional `[predicate]`.
324    pub predicate: Option<PathPredicate>,
325}
326
327/// `pathPredicate : '[' (standardPredicate | archetypePredicate | nodePredicate) ']'`
328#[derive(Debug, Clone, PartialEq)]
329pub enum PathPredicate {
330    /// `objectPath COMPARISON_OPERATOR operand`
331    Standard(Box<StandardPredicate>),
332    /// `ARCHETYPE_HRID | PARAMETER`
333    Archetype(ArchetypePredicate),
334    /// A node predicate (id/at code, archetype id, boolean tree, …).
335    Node(Box<NodePredicate>),
336}
337
338/// `standardPredicate : objectPath COMPARISON_OPERATOR pathPredicateOperand`
339#[derive(Debug, Clone, PartialEq)]
340pub struct StandardPredicate {
341    /// The constrained path.
342    pub path: ObjectPath,
343    /// The operator.
344    pub op: CompOp,
345    /// The operand.
346    pub operand: PathPredicateOperand,
347}
348
349/// `archetypePredicate : ARCHETYPE_HRID | PARAMETER`
350#[derive(Debug, Clone, PartialEq)]
351pub enum ArchetypePredicate {
352    /// An archetype HRID.
353    Hrid(String),
354    /// `$param`.
355    Parameter(String),
356}
357
358/// `nodePredicate` — the (partial) common forms: a node code with optional
359/// name/term, an archetype id with optional name/term, a parameter, a standard
360/// comparison, or a boolean combination.
361#[derive(Debug, Clone, PartialEq)]
362pub enum NodePredicate {
363    /// `(ID_CODE|AT_CODE) (',' (STRING|PARAMETER|TERM_CODE|AT_CODE|ID_CODE))?`
364    Code {
365        /// The `id`/`at` code.
366        code: String,
367        /// Optional name/term operand.
368        name: Option<NodeNameConstraint>,
369    },
370    /// `ARCHETYPE_HRID (',' …)?`
371    Archetype {
372        /// The archetype HRID.
373        hrid: String,
374        /// Optional name/term operand.
375        name: Option<NodeNameConstraint>,
376    },
377    /// `$param`
378    Parameter(String),
379    /// `objectPath COMPARISON_OPERATOR operand`
380    Standard(Box<StandardPredicate>),
381    /// `objectPath MATCHES CONTAINED_REGEX`
382    MatchesRegex {
383        /// The constrained path.
384        path: ObjectPath,
385        /// The raw `{/regex/}` token text.
386        regex: String,
387    },
388    /// `nodePredicate AND nodePredicate`
389    And(Box<NodePredicate>, Box<NodePredicate>),
390    /// `nodePredicate OR nodePredicate`
391    Or(Box<NodePredicate>, Box<NodePredicate>),
392}
393
394/// The optional name/term after a node code in a [`NodePredicate`].
395#[derive(Debug, Clone, PartialEq)]
396pub enum NodeNameConstraint {
397    /// A quoted name.
398    String(String),
399    /// `$param`.
400    Parameter(String),
401    /// A term code.
402    TermCode(String),
403    /// An `at`/`id` code.
404    Code(String),
405}
406
407/// `versionPredicate : LATEST_VERSION | ALL_VERSIONS | standardPredicate`
408#[derive(Debug, Clone, PartialEq)]
409pub enum VersionPredicate {
410    /// `LATEST_VERSION`
411    Latest,
412    /// `ALL_VERSIONS`
413    All,
414    /// A standard comparison predicate.
415    Standard(Box<StandardPredicate>),
416}
417
418/// `pathPredicateOperand : primitive | objectPath | PARAMETER | ID_CODE | AT_CODE`
419#[derive(Debug, Clone, PartialEq)]
420pub enum PathPredicateOperand {
421    /// A literal.
422    Primitive(Primitive),
423    /// A path.
424    Path(ObjectPath),
425    /// `$param`.
426    Parameter(String),
427    /// An `id`/`at` code.
428    Code(String),
429}
430
431/// `likeOperand : STRING | PARAMETER`
432#[derive(Debug, Clone, PartialEq)]
433pub enum LikeOperand {
434    /// A quoted pattern.
435    String(String),
436    /// `$param`.
437    Parameter(String),
438}
439
440/// `matchesOperand : '{' valueListItem (',' valueListItem)* '}' | terminologyFunction | '{' URI '}'`
441#[derive(Debug, Clone, PartialEq)]
442pub enum MatchesOperand {
443    /// A `{ … }` value list.
444    ValueList(Vec<ValueListItem>),
445    /// `terminology(...)`.
446    Terminology(TerminologyFunction),
447    /// `{ uri }`.
448    Uri(String),
449}
450
451/// `valueListItem : primitive | PARAMETER | terminologyFunction`
452#[derive(Debug, Clone, PartialEq)]
453pub enum ValueListItem {
454    /// A literal.
455    Primitive(Primitive),
456    /// `$param`.
457    Parameter(String),
458    /// `terminology(...)`.
459    Terminology(TerminologyFunction),
460}
461
462/// `functionCall` — a named function with terminal arguments (also covers
463/// `terminologyFunction`, kept as [`FunctionCall::Terminology`]).
464///
465/// A name is classified against the built-in functions QUERY master03
466/// §Functions defines (the grammar's `STRING_FUNCTION_ID`,
467/// `NUMERIC_FUNCTION_ID` and `DATE_TIME_FUNCTION_ID` groups); a name outside
468/// them is [`FunctionCall::Other`]. Both keep the name as spelled, so the
469/// printer round-trips it.
470#[derive(Debug, Clone, PartialEq)]
471pub enum FunctionCall {
472    /// `name ( terminal, … )` naming an AQL built-in function.
473    Builtin {
474        /// The function the name denotes.
475        function: BuiltinFunction,
476        /// The name as written (AQL names are case-insensitive).
477        name: String,
478        /// The arguments.
479        args: Vec<Terminal>,
480    },
481    /// `IDENTIFIER ( terminal, … )` naming no AQL built-in function: the
482    /// spec's "various other functions may exist however in various AQL
483    /// implementations" (QUERY master03 §Functions).
484    Other {
485        /// The name as written.
486        name: String,
487        /// The arguments.
488        args: Vec<Terminal>,
489    },
490    /// `terminology(str, str, str)`.
491    Terminology(TerminologyFunction),
492}
493
494impl FunctionCall {
495    /// The call `name(args)`, classified: a built-in function when the name
496    /// denotes one, [`FunctionCall::Other`] otherwise.
497    #[must_use]
498    pub fn named(name: String, args: Vec<Terminal>) -> Self {
499        match BuiltinFunction::from_name(&name) {
500            Some(function) => Self::Builtin {
501                function,
502                name,
503                args,
504            },
505            None => Self::Other { name, args },
506        }
507    }
508}
509
510/// An AQL built-in single-row function (QUERY master03 §Functions), by group.
511///
512/// The aggregates and `TERMINOLOGY` are not here: their argument grammar
513/// differs, so the AST carries them as [`AggregateCall`] and
514/// [`TerminologyFunction`].
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
516pub enum BuiltinFunction {
517    /// A string function (`STRING_FUNCTION_ID`).
518    String(StringFunction),
519    /// A numeric function (`NUMERIC_FUNCTION_ID`).
520    Numeric(NumericFunction),
521    /// A date and time function (`DATE_TIME_FUNCTION_ID`).
522    DateTime(DateTimeFunction),
523}
524
525impl BuiltinFunction {
526    /// The built-in function `name` denotes, compared case-insensitively, or
527    /// `None` when AQL defines no function of that name.
528    #[must_use]
529    pub fn from_name(name: &str) -> Option<Self> {
530        StringFunction::from_name(name)
531            .map(Self::String)
532            .or_else(|| NumericFunction::from_name(name).map(Self::Numeric))
533            .or_else(|| DateTimeFunction::from_name(name).map(Self::DateTime))
534    }
535
536    /// The function's name as the specification spells it.
537    #[must_use]
538    pub const fn as_str(self) -> &'static str {
539        match self {
540            Self::String(f) => f.as_str(),
541            Self::Numeric(f) => f.as_str(),
542            Self::DateTime(f) => f.as_str(),
543        }
544    }
545}
546
547/// Declares one function-id group: the enum, its spelled names, and the
548/// case-insensitive lookup.
549macro_rules! function_group {
550    ($(#[$meta:meta])* $name:ident { $($(#[$vmeta:meta])* $variant:ident => $spelling:literal,)+ }) => {
551        $(#[$meta])*
552        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
553        pub enum $name {
554            $($(#[$vmeta])* $variant,)+
555        }
556
557        impl $name {
558            /// Every function of the group, in specification order.
559            pub const ALL: &[Self] = &[$(Self::$variant,)+];
560
561            /// The function `name` denotes, compared case-insensitively.
562            #[must_use]
563            pub fn from_name(name: &str) -> Option<Self> {
564                Self::ALL
565                    .iter()
566                    .copied()
567                    .find(|f| f.as_str().eq_ignore_ascii_case(name))
568            }
569
570            /// The function's name as the specification spells it.
571            #[must_use]
572            pub const fn as_str(self) -> &'static str {
573                match self {
574                    $(Self::$variant => $spelling,)+
575                }
576            }
577        }
578    };
579}
580
581function_group! {
582    /// A string function (QUERY master03 §Functions/String functions).
583    StringFunction {
584        /// `LENGTH(expression)`.
585        Length => "LENGTH",
586        /// `CONTAINS(expression, substring)`, the string function, not the
587        /// containment operator.
588        Contains => "CONTAINS",
589        /// `POSITION(substring, expression)`.
590        Position => "POSITION",
591        /// `SUBSTRING(expression, position[, length])`.
592        Substring => "SUBSTRING",
593        /// `CONCAT(expr1, expr2, …)`.
594        Concat => "CONCAT",
595        /// `CONCAT_WS(separator, expr1, expr2, …)`.
596        ConcatWs => "CONCAT_WS",
597    }
598}
599
600function_group! {
601    /// A numeric function (QUERY master03 §Functions/Numeric functions).
602    NumericFunction {
603        /// `ABS(expression)`.
604        Abs => "ABS",
605        /// `MOD(x, y)`.
606        Mod => "MOD",
607        /// `CEIL(expression)`.
608        Ceil => "CEIL",
609        /// `FLOOR(expression)`.
610        Floor => "FLOOR",
611        /// `ROUND(expression[, decimal])`.
612        Round => "ROUND",
613    }
614}
615
616function_group! {
617    /// A date and time function (QUERY master03 §Functions/Date and time
618    /// functions).
619    DateTimeFunction {
620        /// `CURRENT_DATE()`.
621        CurrentDate => "CURRENT_DATE",
622        /// `CURRENT_TIME()`.
623        CurrentTime => "CURRENT_TIME",
624        /// `CURRENT_DATE_TIME()`.
625        CurrentDateTime => "CURRENT_DATE_TIME",
626        /// `NOW()`, the specification's alias for `CURRENT_DATE_TIME()`, kept
627        /// apart so the query prints as written.
628        Now => "NOW",
629        /// `CURRENT_TIMEZONE()`.
630        CurrentTimezone => "CURRENT_TIMEZONE",
631    }
632}
633
634/// `terminologyFunction : TERMINOLOGY '(' STRING ',' STRING ',' STRING ')'`
635#[derive(Debug, Clone, PartialEq)]
636pub struct TerminologyFunction {
637    /// First argument (operation).
638    pub operation: String,
639    /// Second argument.
640    pub arg2: String,
641    /// Third argument.
642    pub arg3: String,
643}
644
645/// `aggregateFunctionCall`
646#[derive(Debug, Clone, PartialEq)]
647pub enum AggregateCall {
648    /// `COUNT ( DISTINCT? identifiedPath | '*' )`
649    Count {
650        /// `DISTINCT` present.
651        distinct: bool,
652        /// The path, or `None` for `COUNT(*)`.
653        path: Option<IdentifiedPath>,
654    },
655    /// `MIN|MAX|SUM|AVG ( identifiedPath )`
656    Stat {
657        /// Which aggregate.
658        func: StatFunc,
659        /// The path.
660        path: IdentifiedPath,
661    },
662}
663
664/// The non-count aggregate functions.
665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
666pub enum StatFunc {
667    /// `MIN`
668    Min,
669    /// `MAX`
670    Max,
671    /// `SUM`
672    Sum,
673    /// `AVG`
674    Avg,
675}
676
677/// `primitive` — an AQL literal.
678#[derive(Debug, Clone, PartialEq)]
679pub enum Primitive {
680    /// A quoted string (unescaping/temporal-typing deferred; raw slice sans
681    /// surrounding quotes).
682    String(String),
683    /// An integer literal.
684    Integer(i64),
685    /// A real literal.
686    Real(f64),
687    /// A boolean literal.
688    Boolean(bool),
689    /// `NULL`.
690    Null,
691}
692
693// ── path text rendering (RESULT_SET column `path`) ─────────────────────────────
694//
695// ITS-REST 1.1.0 RESULT_SET columns carry the SELECT expression's path
696// (`{"name": "#0", "path": "/ehr_id/value"}`) exactly as written in the query,
697// minus the root variable, rather than a normalized reconstruction.
698
699use std::fmt;
700
701impl IdentifiedPath {
702    /// The `RESULT_SET` column-path text of this select expression: the trailing
703    /// object path (with predicates) as written, `/`-prefixed; a bare variable
704    /// (whole-object select) renders as `"/"`.
705    #[must_use]
706    pub fn column_path_text(&self) -> String {
707        match &self.path {
708            None => "/".to_owned(),
709            Some(p) => format!("/{p}"),
710        }
711    }
712
713    /// The column-path text of this select expression with its root variable
714    /// kept, as the query wrote it: `c/uid/value`, or `c` for a bare variable.
715    ///
716    /// ITS-REST 1.1.0 `ResultSetColumn.path` is the "Path from the given AQL of
717    /// the specified column"; with the variable kept, two variables of one class
718    /// (`c1/uid/value`, `c2/uid/value`) render apart.
719    #[must_use]
720    pub fn column_path_with_variable(&self) -> String {
721        match &self.path {
722            None => self.root.clone(),
723            Some(p) => format!("{}/{p}", self.root),
724        }
725    }
726}
727
728impl fmt::Display for ObjectPath {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        for (i, part) in self.parts.iter().enumerate() {
731            if i > 0 {
732                f.write_str("/")?;
733            }
734            write!(f, "{part}")?;
735        }
736        Ok(())
737    }
738}
739
740impl fmt::Display for PathPart {
741    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742        f.write_str(&self.name)?;
743        if let Some(p) = &self.predicate {
744            write!(f, "[{p}]")?;
745        }
746        Ok(())
747    }
748}
749
750impl fmt::Display for PathPredicate {
751    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
752        match self {
753            PathPredicate::Standard(s) => write!(f, "{s}"),
754            PathPredicate::Archetype(ArchetypePredicate::Hrid(h)) => f.write_str(h),
755            PathPredicate::Archetype(ArchetypePredicate::Parameter(p)) => f.write_str(p),
756            PathPredicate::Node(n) => write!(f, "{n}"),
757        }
758    }
759}
760
761impl fmt::Display for StandardPredicate {
762    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
763        write!(f, "{}{}{}", self.path, comp_op_text(self.op), self.operand)
764    }
765}
766
767/// The AQL surface symbol of a comparison operator.
768#[must_use]
769pub fn comp_op_text(op: CompOp) -> &'static str {
770    match op {
771        CompOp::Eq => "=",
772        CompOp::Ne => "!=",
773        CompOp::Gt => ">",
774        CompOp::Ge => ">=",
775        CompOp::Lt => "<",
776        CompOp::Le => "<=",
777    }
778}
779
780impl fmt::Display for NodePredicate {
781    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
782        match self {
783            NodePredicate::Code { code, name } | NodePredicate::Archetype { hrid: code, name } => {
784                f.write_str(code)?;
785                if let Some(n) = name {
786                    write!(f, ",{n}")?;
787                }
788                Ok(())
789            }
790            NodePredicate::Parameter(p) => f.write_str(p),
791            NodePredicate::Standard(s) => write!(f, "{s}"),
792            NodePredicate::MatchesRegex { path, regex } => {
793                write!(f, "{path} matches {regex}")
794            }
795            NodePredicate::And(a, b) => write!(f, "{a} and {b}"),
796            NodePredicate::Or(a, b) => write!(f, "{a} or {b}"),
797        }
798    }
799}
800
801impl fmt::Display for NodeNameConstraint {
802    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
803        match self {
804            NodeNameConstraint::String(s) => {
805                write!(f, "'{}'", crate::printer::escape_string(s))
806            }
807            NodeNameConstraint::Parameter(p) | NodeNameConstraint::Code(p) => f.write_str(p),
808            NodeNameConstraint::TermCode(t) => f.write_str(t),
809        }
810    }
811}
812
813impl fmt::Display for PathPredicateOperand {
814    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815        match self {
816            PathPredicateOperand::Primitive(p) => write!(f, "{p}"),
817            PathPredicateOperand::Path(p) => write!(f, "{p}"),
818            PathPredicateOperand::Parameter(p) | PathPredicateOperand::Code(p) => f.write_str(p),
819        }
820    }
821}
822
823impl fmt::Display for Primitive {
824    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
825        match self {
826            Primitive::String(s) => write!(f, "'{}'", crate::printer::escape_string(s)),
827            Primitive::Integer(i) => write!(f, "{i}"),
828            Primitive::Real(r) => write!(f, "{r}"),
829            Primitive::Boolean(b) => write!(f, "{b}"),
830            Primitive::Null => f.write_str("NULL"),
831        }
832    }
833}