Skip to main content

somnia_core/
expr.rs

1//! The expression tree used inside `WHERE`/`SET`/projection clauses.
2//!
3//! Values render to SurrealQL via the [`SurrealQL`] trait (literals) and the
4//! [`DynExpr`] trait (composed expressions). The building blocks include typed
5//! [`Column`] accessors, the untyped [`Ident`], the [`Raw`] escape hatch,
6//! [`RecordLink`] (`type::record(...)`), [`Func`] calls, and comparison/logical
7//! operators that combine with `.and()` / `.or()`.
8
9use std::collections::BTreeMap;
10use std::fmt;
11
12use crate::types::SurrealRecord;
13
14// ═══════════════════════════════════════════════════════════════════════════════
15// DynExpr — type-erased expression (the internal transport)
16// ═══════════════════════════════════════════════════════════════════════════════
17
18/// A type-erased expression that can render itself into a SurrealQL buffer.
19/// Implemented by every expression node; the common currency of the builder.
20pub trait DynExpr: fmt::Debug + Send + Sync {
21    /// Append this expression's SurrealQL to `buf` with all values inlined.
22    fn render_dyn(&self, buf: &mut String);
23
24    /// Render with `$param` placeholders, collecting values into `params`.
25    /// Default falls back to inline rendering; [`Literal`] nodes override this
26    /// to emit `$pN` placeholders and collect their value.
27    fn render_dyn_params(
28        &self,
29        buf: &mut String,
30        _params: &mut BTreeMap<String, serde_json::Value>,
31    ) {
32        self.render_dyn(buf);
33    }
34}
35
36/// A boxed, type-erased expression. Useful for returning a composed filter from
37/// a helper function (e.g. a shared tenant/owner predicate).
38pub type DynExprBox = Box<dyn DynExpr>;
39
40impl DynExpr for Box<dyn DynExpr> {
41    fn render_dyn(&self, buf: &mut String) {
42        (**self).render_dyn(buf);
43    }
44    fn render_dyn_params(
45        &self,
46        buf: &mut String,
47        params: &mut BTreeMap<String, serde_json::Value>,
48    ) {
49        (**self).render_dyn_params(buf, params);
50    }
51}
52
53// ═══════════════════════════════════════════════════════════════════════════════
54// Expr — typed expression (public API)
55// ═══════════════════════════════════════════════════════════════════════════════
56
57/// A [`DynExpr`] that also reports a SurrealQL type hint. Auto-implemented for
58/// every `DynExpr` (returning `"any"`); concrete nodes may override the hint.
59pub trait Expr: DynExpr {
60    /// A best-effort SurrealQL type name for this expression.
61    fn ty_hint(&self) -> &'static str;
62}
63
64// Any DynExpr is automatically an Expr with a default ty_hint
65// (used internally; concrete types override this)
66impl<E: DynExpr> Expr for E {
67    fn ty_hint(&self) -> &'static str {
68        "any"
69    }
70}
71
72// ═══════════════════════════════════════════════════════════════════════════════
73// SurrealQL — literal value rendering
74// ═══════════════════════════════════════════════════════════════════════════════
75
76/// A Rust type that can be rendered as a SurrealQL literal (the right-hand side of
77/// comparisons, `SET` values, etc.). Implemented for the common scalar types,
78/// `Option<T>`, `serde_json::Value`, the geometry types, and `Thing<T>`.
79pub trait SurrealQL: fmt::Debug + Clone + Send + Sync + 'static {
80    /// The SurrealQL type name (e.g. `"string"`, `"datetime"`).
81    fn surreal_type() -> &'static str;
82    /// Append the literal form of `value` to `buf` (with any needed quoting/escaping).
83    fn render_literal(value: &Self, buf: &mut String);
84    /// Convert `value` to a JSON parameter for `$param` binding. Used by
85    /// [`to_surrealql_with_params`](crate::query::Select::to_surrealql_with_params).
86    fn to_param_value(value: &Self) -> serde_json::Value;
87}
88
89impl SurrealQL for String {
90    fn surreal_type() -> &'static str {
91        "string"
92    }
93    fn render_literal(value: &Self, buf: &mut String) {
94        let escaped = value.replace('\\', "\\\\").replace('\'', "\\'");
95        buf.push('\'');
96        buf.push_str(&escaped);
97        buf.push('\'');
98    }
99    fn to_param_value(value: &Self) -> serde_json::Value {
100        serde_json::Value::String(value.clone())
101    }
102}
103
104impl SurrealQL for bool {
105    fn surreal_type() -> &'static str {
106        "bool"
107    }
108    fn render_literal(value: &Self, buf: &mut String) {
109        buf.push_str(if *value { "true" } else { "false" });
110    }
111    fn to_param_value(value: &Self) -> serde_json::Value {
112        serde_json::Value::Bool(*value)
113    }
114}
115
116macro_rules! surreal_display {
117    ($t:ty, $name:literal) => {
118        impl SurrealQL for $t {
119            fn surreal_type() -> &'static str {
120                $name
121            }
122            fn render_literal(value: &Self, buf: &mut String) {
123                use std::fmt::Write;
124                let _ = write!(buf, "{value}");
125            }
126            fn to_param_value(value: &Self) -> serde_json::Value {
127                serde_json::to_value(value).unwrap_or(serde_json::Value::Null)
128            }
129        }
130    };
131}
132surreal_display!(i64, "int");
133surreal_display!(i32, "int");
134surreal_display!(i16, "int");
135surreal_display!(i8, "int");
136surreal_display!(f64, "float");
137surreal_display!(f32, "float");
138surreal_display!(u32, "int");
139surreal_display!(u64, "int");
140surreal_display!(u16, "int");
141surreal_display!(u8, "int");
142
143impl SurrealQL for chrono::DateTime<chrono::Utc> {
144    fn surreal_type() -> &'static str {
145        "datetime"
146    }
147    fn render_literal(value: &Self, buf: &mut String) {
148        // SurrealDB 2.0+ requires the `d` prefix on datetime literals. A bare
149        // quoted string is a `string`, not a `datetime`, so `created_at > '…'`
150        // would compare against the wrong type; `created_at > d'…'` is correct.
151        buf.push_str("d'");
152        buf.push_str(&value.to_rfc3339());
153        buf.push('\'');
154    }
155    fn to_param_value(value: &Self) -> serde_json::Value {
156        serde_json::Value::String(value.to_rfc3339())
157    }
158}
159
160impl SurrealQL for uuid::Uuid {
161    fn surreal_type() -> &'static str {
162        "uuid"
163    }
164    fn render_literal(value: &Self, buf: &mut String) {
165        use std::fmt::Write;
166        // SurrealDB 2.0+ requires the `u` prefix on uuid literals; a bare quoted
167        // string is a `string`, not a `uuid`.
168        buf.push_str("u'");
169        let _ = write!(buf, "{value}");
170        buf.push('\'');
171    }
172    fn to_param_value(value: &Self) -> serde_json::Value {
173        serde_json::Value::String(value.to_string())
174    }
175}
176
177impl SurrealQL for serde_json::Value {
178    fn surreal_type() -> &'static str {
179        "object"
180    }
181    fn render_literal(value: &Self, buf: &mut String) {
182        // JSON is a syntactic subset of SurrealQL value literals (objects,
183        // arrays, numbers, bools, null, double-quoted strings all parse), so
184        // the serialized form is a valid inline literal.
185        use std::fmt::Write;
186        let _ = write!(buf, "{value}");
187    }
188    fn to_param_value(value: &Self) -> serde_json::Value {
189        value.clone()
190    }
191}
192
193// Geometry literals render as GeoJSON objects (a valid SurrealQL object literal),
194// e.g. `{"type":"Point","coordinates":[1.0,2.0]}`.
195macro_rules! geometry_surrealql {
196    ($t:ident, $name:literal) => {
197        impl SurrealQL for crate::types::$t {
198            fn surreal_type() -> &'static str {
199                $name
200            }
201            fn render_literal(value: &Self, buf: &mut String) {
202                if let Ok(s) = serde_json::to_string(value) {
203                    buf.push_str(&s);
204                }
205            }
206            fn to_param_value(value: &Self) -> serde_json::Value {
207                serde_json::to_value(value).unwrap_or(serde_json::Value::Null)
208            }
209        }
210    };
211}
212geometry_surrealql!(Point, "geometry<point>");
213geometry_surrealql!(LineString, "geometry<line>");
214geometry_surrealql!(Polygon, "geometry<polygon>");
215
216impl<T: SurrealQL> SurrealQL for Option<T> {
217    fn surreal_type() -> &'static str {
218        T::surreal_type()
219    }
220    fn render_literal(value: &Self, buf: &mut String) {
221        match value {
222            Some(v) => T::render_literal(v, buf),
223            None => buf.push_str("NONE"),
224        }
225    }
226    fn to_param_value(value: &Self) -> serde_json::Value {
227        match value {
228            Some(v) => T::to_param_value(v),
229            None => serde_json::Value::Null,
230        }
231    }
232}
233
234impl<V: SurrealQL> SurrealQL for Vec<V> {
235    fn surreal_type() -> &'static str {
236        // The element type is lost at this level; the derive emits the precise
237        // `array<…>` for `DEFINE FIELD`. This hint is informational only.
238        "array"
239    }
240    fn render_literal(value: &Self, buf: &mut String) {
241        buf.push('[');
242        for (i, v) in value.iter().enumerate() {
243            if i > 0 {
244                buf.push_str(", ");
245            }
246            V::render_literal(v, buf);
247        }
248        buf.push(']');
249    }
250    fn to_param_value(value: &Self) -> serde_json::Value {
251        serde_json::Value::Array(value.iter().map(|v| V::to_param_value(v)).collect())
252    }
253}
254
255impl SurrealQL for std::time::Duration {
256    fn surreal_type() -> &'static str {
257        "duration"
258    }
259    fn render_literal(value: &Self, buf: &mut String) {
260        use std::fmt::Write;
261        // SurrealDB duration literal: a concatenation of unit-tagged components
262        // (e.g. `1s500ms`). Whole seconds + sub-second nanoseconds round-trips any
263        // `Duration` and parses cleanly.
264        let secs = value.as_secs();
265        let nanos = value.subsec_nanos();
266        if secs == 0 && nanos == 0 {
267            buf.push_str("0ns");
268            return;
269        }
270        if secs > 0 {
271            let _ = write!(buf, "{secs}s");
272        }
273        if nanos > 0 {
274            let _ = write!(buf, "{nanos}ns");
275        }
276    }
277    fn to_param_value(value: &Self) -> serde_json::Value {
278        let secs = value.as_secs();
279        let nanos = value.subsec_nanos();
280        if secs == 0 && nanos == 0 {
281            serde_json::Value::String("0ns".into())
282        } else if nanos == 0 {
283            serde_json::Value::String(format!("{secs}s"))
284        } else {
285            serde_json::Value::String(format!("{secs}s{nanos}ns"))
286        }
287    }
288}
289
290impl<T: crate::types::SurrealRecord> SurrealQL for crate::types::Thing<T> {
291    fn surreal_type() -> &'static str {
292        "record"
293    }
294    fn render_literal(value: &Self, buf: &mut String) {
295        buf.push_str(T::table_name());
296        buf.push(':');
297        value.key.render_id(buf);
298    }
299    fn to_param_value(value: &Self) -> serde_json::Value {
300        let mut s = String::new();
301        value.key.render_id(&mut s);
302        serde_json::Value::String(format!("{}:{}", T::table_name(), s))
303    }
304}
305
306// ═══════════════════════════════════════════════════════════════════════════════
307// Literal — wraps a SurrealQL value as an expression
308// ═══════════════════════════════════════════════════════════════════════════════
309
310#[derive(Debug, Clone)]
311pub struct Literal<V: SurrealQL>(pub V);
312
313impl<V: SurrealQL> DynExpr for Literal<V> {
314    fn render_dyn(&self, buf: &mut String) {
315        V::render_literal(&self.0, buf);
316    }
317    fn render_dyn_params(
318        &self,
319        buf: &mut String,
320        params: &mut BTreeMap<String, serde_json::Value>,
321    ) {
322        let name = format!("p{}", params.len());
323        buf.push('$');
324        buf.push_str(&name);
325        params.insert(name, V::to_param_value(&self.0));
326    }
327}
328
329// ═══════════════════════════════════════════════════════════════════════════════
330// Param — explicit named parameter (`$name` placeholder)
331// ═══════════════════════════════════════════════════════════════════════════════
332
333/// A named SurrealQL parameter `$name`. In inline mode renders as a literal;
334/// in param mode (`to_surrealql_with_params`) renders as `$name` and collects
335/// the value. Use when you want to reference the same value in multiple places
336/// or give params meaningful names.
337///
338/// ```ignore
339/// let title = Param::new("search", "hello");
340/// Post::table()
341///     .project(Post::all())
342///     .filter(Post::title().eq(title.clone()).or(Post::body().contains(title)))
343///     .to_surrealql_with_params();
344/// // → ("SELECT * FROM post WHERE title = $search OR body CONTAINS $search",
345/// //    {"search": "hello"})
346/// ```
347#[derive(Debug, Clone)]
348pub struct Param<V: SurrealQL> {
349    name: String,
350    value: V,
351}
352
353impl<V: SurrealQL> Param<V> {
354    /// Create a named parameter placeholder with the given name and value.
355    pub fn new(name: impl Into<String>, value: V) -> Self {
356        Self {
357            name: name.into(),
358            value,
359        }
360    }
361}
362
363impl<V: SurrealQL> DynExpr for Param<V> {
364    fn render_dyn(&self, buf: &mut String) {
365        // In inline mode, render the literal — the name is irrelevant.
366        V::render_literal(&self.value, buf);
367    }
368    fn render_dyn_params(
369        &self,
370        buf: &mut String,
371        params: &mut BTreeMap<String, serde_json::Value>,
372    ) {
373        buf.push('$');
374        buf.push_str(&self.name);
375        params
376            .entry(self.name.clone())
377            .or_insert_with(|| V::to_param_value(&self.value));
378    }
379}
380
381// ═══════════════════════════════════════════════════════════════════════════════
382// Column — typed field reference (e.g., `asset.name`)
383// ═══════════════════════════════════════════════════════════════════════════════
384
385/// A typed reference to a record field — e.g. `Post::title()` yields a
386/// `Column<Post, String>`. Generated by `#[derive(SurrealRecord)]` and used to
387/// build type-checked filters (`.eq(...)`, `.gt(...)`, …) and projections.
388pub struct Column<T: SurrealRecord, V: SurrealQL> {
389    /// The database column name.
390    pub name: &'static str,
391    /// The SurrealQL type name for this column.
392    pub surreal_type: &'static str,
393    #[doc(hidden)]
394    pub _marker: std::marker::PhantomData<(T, V)>,
395}
396
397impl<T: SurrealRecord, V: SurrealQL> std::fmt::Debug for Column<T, V> {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        f.debug_struct("Column").field("name", &self.name).finish()
400    }
401}
402
403impl<T: SurrealRecord, V: SurrealQL> Clone for Column<T, V> {
404    fn clone(&self) -> Self {
405        *self
406    }
407}
408impl<T: SurrealRecord, V: SurrealQL> Copy for Column<T, V> {}
409
410impl<T: SurrealRecord, V: SurrealQL> DynExpr for Column<T, V> {
411    fn render_dyn(&self, buf: &mut String) {
412        buf.push_str(self.name);
413    }
414}
415
416// ═══════════════════════════════════════════════════════════════════════════════
417// Ident — an untyped column/field reference usable in WHERE clauses
418// ═══════════════════════════════════════════════════════════════════════════════
419
420/// An untyped identifier (column or field path) for building filter expressions
421/// where a typed [`Column`] accessor is unavailable (e.g. record-link fields the
422/// derive doesn't expose, or `tenant.slug` paths). Mirrors `Column`'s operators.
423#[derive(Debug, Clone, Copy)]
424pub struct Ident(pub &'static str);
425
426/// Construct an [`Ident`] for a field name.
427pub fn ident(name: &'static str) -> Ident {
428    Ident(name)
429}
430
431impl Ident {
432    fn dyn_box(&self) -> Box<dyn DynExpr> {
433        Box::new(Raw(self.0.to_string()))
434    }
435
436    pub fn eq<V: SurrealQL>(&self, v: V) -> EqExpr {
437        EqExpr {
438            left: self.dyn_box(),
439            right: Box::new(Literal(v)),
440        }
441    }
442    pub fn ne<V: SurrealQL>(&self, v: V) -> NeExpr {
443        NeExpr {
444            left: self.dyn_box(),
445            right: Box::new(Literal(v)),
446        }
447    }
448    pub fn gt<V: SurrealQL>(&self, v: V) -> GtExpr {
449        GtExpr {
450            left: self.dyn_box(),
451            right: Box::new(Literal(v)),
452        }
453    }
454    pub fn lt<V: SurrealQL>(&self, v: V) -> LtExpr {
455        LtExpr {
456            left: self.dyn_box(),
457            right: Box::new(Literal(v)),
458        }
459    }
460    pub fn gte<V: SurrealQL>(&self, v: V) -> GteExpr {
461        GteExpr {
462            left: self.dyn_box(),
463            right: Box::new(Literal(v)),
464        }
465    }
466    pub fn lte<V: SurrealQL>(&self, v: V) -> LteExpr {
467        LteExpr {
468            left: self.dyn_box(),
469            right: Box::new(Literal(v)),
470        }
471    }
472    pub fn contains<V: SurrealQL>(&self, v: V) -> ContainsExpr {
473        ContainsExpr {
474            haystack: self.dyn_box(),
475            needle: Box::new(Literal(v)),
476        }
477    }
478    /// `field CONTAINS <expr>` — e.g. array-membership test with a [`Param`] or
479    /// other expression.
480    pub fn contains_expr(&self, expr: impl DynExpr + 'static) -> ContainsExpr {
481        ContainsExpr {
482            haystack: self.dyn_box(),
483            needle: Box::new(expr),
484        }
485    }
486    /// Compare to an arbitrary expression — e.g. `asset = type::record('asset', …)`.
487    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
488        EqExpr {
489            left: self.dyn_box(),
490            right: Box::new(rhs),
491        }
492    }
493    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
494        NeExpr {
495            left: self.dyn_box(),
496            right: Box::new(rhs),
497        }
498    }
499    /// `field IN <expr>` — e.g. an array literal or a parenthesized subquery.
500    pub fn in_expr(&self, rhs: impl DynExpr + 'static) -> InExpr {
501        InExpr {
502            left: self.dyn_box(),
503            right: Box::new(rhs),
504        }
505    }
506    /// `field NOT IN <expr>`.
507    pub fn not_in_expr(&self, rhs: impl DynExpr + 'static) -> NotInExpr {
508        NotInExpr {
509            left: self.dyn_box(),
510            right: Box::new(rhs),
511        }
512    }
513    /// `field IS NONE`.
514    pub fn is_none(&self) -> Raw {
515        Raw(format!("{} IS NONE", self.0))
516    }
517}
518
519impl DynExpr for Ident {
520    fn render_dyn(&self, buf: &mut String) {
521        buf.push_str(self.0);
522    }
523}
524
525// ═══════════════════════════════════════════════════════════════════════════════
526// Raw — verbatim SurrealQL escape hatch (lambdas, IF/THEN/ELSE, field paths…)
527// ═══════════════════════════════════════════════════════════════════════════════
528
529/// A verbatim SurrealQL fragment. Use for expressions somnia does not model as
530/// typed nodes (e.g. `IF x != NONE THEN … END`, lambdas, `tenant.slug`).
531#[derive(Debug, Clone)]
532pub struct Raw(pub String);
533
534impl Raw {
535    pub fn new(s: impl Into<String>) -> Self {
536        Self(s.into())
537    }
538}
539
540impl DynExpr for Raw {
541    fn render_dyn(&self, buf: &mut String) {
542        buf.push_str(&self.0);
543    }
544}
545
546/// SurrealDB's `NONE`.
547#[derive(Debug, Clone)]
548pub struct NoneLit;
549impl DynExpr for NoneLit {
550    fn render_dyn(&self, buf: &mut String) {
551        buf.push_str("NONE");
552    }
553}
554
555// ═══════════════════════════════════════════════════════════════════════════════
556// RecordLink — `type::record('table', <key>)`
557// ═══════════════════════════════════════════════════════════════════════════════
558
559/// Builds a SurrealDB record link `type::record('table', <key>)`. The key is any
560/// literal value (typically the bare UUID/string id of the related row).
561#[derive(Debug)]
562pub struct RecordLink {
563    table: &'static str,
564    key: Box<dyn DynExpr>,
565}
566
567impl RecordLink {
568    /// `type::record('table', '<key literal>')`.
569    pub fn new<V: SurrealQL>(table: &'static str, key: V) -> Self {
570        Self {
571            table,
572            key: Box::new(Literal(key)),
573        }
574    }
575    /// `type::record('table', <key expr>)` — key rendered from an arbitrary expr.
576    pub fn from_expr(table: &'static str, key: impl DynExpr + 'static) -> Self {
577        Self {
578            table,
579            key: Box::new(key),
580        }
581    }
582}
583
584impl DynExpr for RecordLink {
585    fn render_dyn(&self, buf: &mut String) {
586        buf.push_str("type::record('");
587        buf.push_str(self.table);
588        buf.push_str("', ");
589        self.key.render_dyn(buf);
590        buf.push(')');
591    }
592    fn render_dyn_params(
593        &self,
594        buf: &mut String,
595        params: &mut BTreeMap<String, serde_json::Value>,
596    ) {
597        buf.push_str("type::record('");
598        buf.push_str(self.table);
599        buf.push_str("', ");
600        self.key.render_dyn_params(buf, params);
601        buf.push(')');
602    }
603}
604
605// ═══════════════════════════════════════════════════════════════════════════════
606// Path — graph traversal (`->edge->table`, `<-edge<-table`, `<->edge<->table`)
607// ═══════════════════════════════════════════════════════════════════════════════
608
609/// Direction of a single graph hop.
610#[derive(Debug, Clone, Copy, PartialEq, Eq)]
611enum Dir {
612    /// outgoing edge — `->`
613    Out,
614    /// incoming edge — `<-`
615    In,
616    /// either direction — `<->`
617    Both,
618}
619
620impl Dir {
621    fn arrow(self) -> &'static str {
622        match self {
623            Dir::Out => "->",
624            Dir::In => "<-",
625            Dir::Both => "<->",
626        }
627    }
628}
629
630/// One hop in a graph [`Path`]: a direction, an edge table, an optional
631/// destination table, and an optional `WHERE` filter on the edge.
632#[derive(Debug)]
633struct Step {
634    dir: Dir,
635    edge: &'static str,
636    dest: Option<&'static str>,
637    filter: Option<Box<dyn DynExpr>>,
638}
639
640impl Step {
641    fn render(&self, buf: &mut String) {
642        buf.push_str(self.dir.arrow());
643        match &self.filter {
644            // `->(edge WHERE <expr>)`
645            Some(f) => {
646                buf.push('(');
647                buf.push_str(self.edge);
648                buf.push_str(" WHERE ");
649                f.render_dyn(buf);
650                buf.push(')');
651            }
652            None => buf.push_str(self.edge),
653        }
654        if let Some(dest) = self.dest {
655            buf.push_str(self.dir.arrow());
656            buf.push_str(dest);
657        }
658    }
659    fn render_params(&self, buf: &mut String, params: &mut BTreeMap<String, serde_json::Value>) {
660        buf.push_str(self.dir.arrow());
661        match &self.filter {
662            Some(f) => {
663                buf.push('(');
664                buf.push_str(self.edge);
665                buf.push_str(" WHERE ");
666                f.render_dyn_params(buf, params);
667                buf.push(')');
668            }
669            None => buf.push_str(self.edge),
670        }
671        if let Some(dest) = self.dest {
672            buf.push_str(self.dir.arrow());
673            buf.push_str(dest);
674        }
675    }
676}
677
678/// The trailing field accessor on a [`Path`] — `.field` or `.*`.
679#[derive(Debug)]
680enum Tail {
681    Field(String),
682    All,
683}
684
685/// A graph-traversal expression — e.g. `->wrote->post`, `<-wrote<-user`, or a
686/// multi-hop chain with an optional `.field`/`.*` accessor at the end.
687///
688/// A `Path` is a [`DynExpr`], so it works anywhere the builder takes an
689/// expression: as a `SELECT` projection (`Projection::aliased(path, "posts")`),
690/// inside a `WHERE` filter, or as a `SET` value. With no start it is relative to
691/// the statement's `FROM` table; [`Path::from_record`] anchors it to a record.
692///
693/// ```ignore
694/// // ->wrote->post.title AS titles
695/// let p = Path::out::<Wrote>().to::<Post>().field("title");
696/// Post::table().project(vec![Projection::aliased(p, "titles")]);
697/// ```
698#[derive(Debug)]
699pub struct Path {
700    start: Option<Box<dyn DynExpr>>,
701    recurse: Option<String>,
702    steps: Vec<Step>,
703    tail: Option<Tail>,
704}
705
706impl Path {
707    fn from_step(dir: Dir, edge: &'static str) -> Self {
708        Self {
709            start: None,
710            recurse: None,
711            steps: vec![Step {
712                dir,
713                edge,
714                dest: None,
715                filter: None,
716            }],
717            tail: None,
718        }
719    }
720
721    /// Start an outgoing hop over edge `E` — `->edge`.
722    pub fn out<E: crate::types::SurrealEdge>() -> Self {
723        Self::from_step(Dir::Out, E::edge_name())
724    }
725    /// Start an incoming hop over edge `E` — `<-edge`.
726    pub fn inn<E: crate::types::SurrealEdge>() -> Self {
727        Self::from_step(Dir::In, E::edge_name())
728    }
729    /// Start a bidirectional hop over edge `E` — `<->edge`.
730    pub fn both<E: crate::types::SurrealEdge>() -> Self {
731        Self::from_step(Dir::Both, E::edge_name())
732    }
733
734    /// Start an outgoing hop over a raw edge name — `->edge`.
735    pub fn out_edge(edge: &'static str) -> Self {
736        Self::from_step(Dir::Out, edge)
737    }
738    /// Start an incoming hop over a raw edge name — `<-edge`.
739    pub fn in_edge(edge: &'static str) -> Self {
740        Self::from_step(Dir::In, edge)
741    }
742    /// Start a bidirectional hop over a raw edge name — `<->edge`.
743    pub fn both_edge(edge: &'static str) -> Self {
744        Self::from_step(Dir::Both, edge)
745    }
746
747    /// Anchor an existing path to a starting record literal — `<record><path>`
748    /// (e.g. `user:tobie->wrote->post`). Pass a [`Thing`](crate::types::Thing).
749    pub fn from_record<V: SurrealQL>(mut self, start: V) -> Self {
750        self.start = Some(Box::new(Literal(start)));
751        self
752    }
753
754    /// Anchor an existing path to a starting expression — e.g. a
755    /// [`RecordLink`] (`type::record('user', $id)->wrote->post`).
756    pub fn from_expr(mut self, start: impl DynExpr + 'static) -> Self {
757        self.start = Some(Box::new(start));
758        self
759    }
760
761    fn last_mut(&mut self) -> &mut Step {
762        self.steps.last_mut().expect("path always has ≥1 step")
763    }
764
765    /// Constrain the destination of the most recent hop to table `T` — `->edge->table`.
766    pub fn to<T: SurrealRecord>(mut self) -> Self {
767        self.last_mut().dest = Some(T::table_name());
768        self
769    }
770    /// Constrain the destination of the most recent hop to a raw table name.
771    pub fn to_table(mut self, table: &'static str) -> Self {
772        self.last_mut().dest = Some(table);
773        self
774    }
775
776    /// Filter the most recent hop's edge — `->(edge WHERE <expr>)`.
777    pub fn where_(mut self, expr: impl DynExpr + 'static) -> Self {
778        self.last_mut().filter = Some(Box::new(expr));
779        self
780    }
781
782    /// Chain another outgoing hop over edge `E`.
783    pub fn then_out<E: crate::types::SurrealEdge>(self) -> Self {
784        self.push_step(Dir::Out, E::edge_name())
785    }
786    /// Chain another incoming hop over edge `E`.
787    pub fn then_in<E: crate::types::SurrealEdge>(self) -> Self {
788        self.push_step(Dir::In, E::edge_name())
789    }
790    /// Chain another bidirectional hop over edge `E`.
791    pub fn then_both<E: crate::types::SurrealEdge>(self) -> Self {
792        self.push_step(Dir::Both, E::edge_name())
793    }
794    /// Chain another hop over a raw edge name, outgoing.
795    pub fn then_out_edge(self, edge: &'static str) -> Self {
796        self.push_step(Dir::Out, edge)
797    }
798    /// Chain another hop over a raw edge name, incoming.
799    pub fn then_in_edge(self, edge: &'static str) -> Self {
800        self.push_step(Dir::In, edge)
801    }
802
803    fn push_step(mut self, dir: Dir, edge: &'static str) -> Self {
804        self.steps.push(Step {
805            dir,
806            edge,
807            dest: None,
808            filter: None,
809        });
810        self
811    }
812
813    /// Repeat the path recursively, unbounded — `@.{..}<path>`. Combine with
814    /// [`from_record`](Self::from_record) to anchor the recursion at a record
815    /// (`person:tobie.{..}->knows->person`).
816    pub fn recurse_all(mut self) -> Self {
817        self.recurse = Some("..".to_string());
818        self
819    }
820    /// Recurse up to `max` hops — `@.{..max}<path>`.
821    pub fn recurse_up_to(mut self, max: u32) -> Self {
822        self.recurse = Some(format!("..{max}"));
823        self
824    }
825    /// Recurse between `min` and `max` hops — `@.{min..max}<path>`.
826    pub fn recurse_range(mut self, min: u32, max: u32) -> Self {
827        self.recurse = Some(format!("{min}..{max}"));
828        self
829    }
830    /// Recurse exactly `n` hops — `@.{n}<path>`.
831    pub fn recurse_exact(mut self, n: u32) -> Self {
832        self.recurse = Some(format!("{n}"));
833        self
834    }
835
836    /// Append a field accessor — `<path>.field`.
837    pub fn field(mut self, name: impl Into<String>) -> Self {
838        self.tail = Some(Tail::Field(name.into()));
839        self
840    }
841    /// Append the all-fields accessor — `<path>.*`.
842    pub fn all(mut self) -> Self {
843        self.tail = Some(Tail::All);
844        self
845    }
846
847    /// `<path> CONTAINS <value>` — e.g. membership test over a traversed list.
848    pub fn contains<V: SurrealQL>(self, value: V) -> ContainsExpr {
849        ContainsExpr {
850            haystack: Box::new(self),
851            needle: Box::new(Literal(value)),
852        }
853    }
854    /// `<path> = <expr>`.
855    pub fn eq_expr(self, rhs: impl DynExpr + 'static) -> EqExpr {
856        EqExpr {
857            left: Box::new(self),
858            right: Box::new(rhs),
859        }
860    }
861}
862
863impl DynExpr for Path {
864    fn render_dyn(&self, buf: &mut String) {
865        match (&self.start, &self.recurse) {
866            // anchored recursion: `<record>.{range}<path>`
867            (Some(start), Some(range)) => {
868                start.render_dyn(buf);
869                buf.push_str(".{");
870                buf.push_str(range);
871                buf.push('}');
872            }
873            // relative recursion: `@.{range}<path>` (the `@` is the recursion point)
874            (None, Some(range)) => {
875                buf.push_str("@.{");
876                buf.push_str(range);
877                buf.push('}');
878            }
879            // no recursion: optional record anchor, then the hops
880            (Some(start), None) => start.render_dyn(buf),
881            (None, None) => {}
882        }
883        for step in &self.steps {
884            step.render(buf);
885        }
886        match &self.tail {
887            Some(Tail::Field(f)) => {
888                buf.push('.');
889                buf.push_str(f);
890            }
891            Some(Tail::All) => buf.push_str(".*"),
892            None => {}
893        }
894    }
895    fn render_dyn_params(
896        &self,
897        buf: &mut String,
898        params: &mut BTreeMap<String, serde_json::Value>,
899    ) {
900        match (&self.start, &self.recurse) {
901            (Some(start), Some(range)) => {
902                start.render_dyn_params(buf, params);
903                buf.push_str(".{");
904                buf.push_str(range);
905                buf.push('}');
906            }
907            (None, Some(range)) => {
908                buf.push_str("@.{");
909                buf.push_str(range);
910                buf.push('}');
911            }
912            (Some(start), None) => start.render_dyn_params(buf, params),
913            (None, None) => {}
914        }
915        for step in &self.steps {
916            step.render_params(buf, params);
917        }
918        match &self.tail {
919            Some(Tail::Field(f)) => {
920                buf.push('.');
921                buf.push_str(f);
922            }
923            Some(Tail::All) => buf.push_str(".*"),
924            None => {}
925        }
926    }
927}
928
929// ═══════════════════════════════════════════════════════════════════════════════
930// Func — `name(arg, arg, …)` (record::id, type::string, string::lowercase, …)
931// ═══════════════════════════════════════════════════════════════════════════════
932
933/// A SurrealQL function call `name(args…)`.
934#[derive(Debug)]
935pub struct Func {
936    name: &'static str,
937    args: Vec<Box<dyn DynExpr>>,
938}
939
940impl Func {
941    pub fn new(name: &'static str, args: Vec<Box<dyn DynExpr>>) -> Self {
942        Self { name, args }
943    }
944    /// Single-argument function over a bare column/identifier, e.g.
945    /// `record::id(id)` → `Func::of("record::id", "id")`.
946    pub fn of(name: &'static str, ident: &'static str) -> Self {
947        Self {
948            name,
949            args: vec![Box::new(Raw(ident.to_string()))],
950        }
951    }
952}
953
954impl DynExpr for Func {
955    fn render_dyn(&self, buf: &mut String) {
956        buf.push_str(self.name);
957        buf.push('(');
958        for (i, a) in self.args.iter().enumerate() {
959            if i > 0 {
960                buf.push_str(", ");
961            }
962            a.render_dyn(buf);
963        }
964        buf.push(')');
965    }
966    fn render_dyn_params(
967        &self,
968        buf: &mut String,
969        params: &mut BTreeMap<String, serde_json::Value>,
970    ) {
971        buf.push_str(self.name);
972        buf.push('(');
973        for (i, a) in self.args.iter().enumerate() {
974            if i > 0 {
975                buf.push_str(", ");
976            }
977            a.render_dyn_params(buf, params);
978        }
979        buf.push(')');
980    }
981}
982
983// ═══════════════════════════════════════════════════════════════════════════════
984// Binary expression nodes
985// ═══════════════════════════════════════════════════════════════════════════════
986
987macro_rules! binop {
988    ($name:ident, $op:literal) => {
989        #[derive(Debug)]
990        pub struct $name {
991            pub(crate) left: Box<dyn DynExpr>,
992            pub(crate) right: Box<dyn DynExpr>,
993        }
994        impl DynExpr for $name {
995            fn render_dyn(&self, buf: &mut String) {
996                self.left.render_dyn(buf);
997                buf.push(' ');
998                buf.push_str($op);
999                buf.push(' ');
1000                self.right.render_dyn(buf);
1001            }
1002            fn render_dyn_params(
1003                &self,
1004                buf: &mut String,
1005                params: &mut BTreeMap<String, serde_json::Value>,
1006            ) {
1007                self.left.render_dyn_params(buf, params);
1008                buf.push(' ');
1009                buf.push_str($op);
1010                buf.push(' ');
1011                self.right.render_dyn_params(buf, params);
1012            }
1013        }
1014    };
1015}
1016
1017binop!(EqExpr, "=");
1018binop!(NeExpr, "!=");
1019binop!(GtExpr, ">");
1020binop!(LtExpr, "<");
1021binop!(GteExpr, ">=");
1022binop!(LteExpr, "<=");
1023binop!(AndExpr, "AND");
1024binop!(OrExpr, "OR");
1025binop!(InExpr, "IN");
1026binop!(NotInExpr, "NOT IN");
1027
1028#[derive(Debug)]
1029pub struct NotExpr {
1030    pub(crate) inner: Box<dyn DynExpr>,
1031}
1032
1033impl DynExpr for NotExpr {
1034    fn render_dyn(&self, buf: &mut String) {
1035        buf.push_str("NOT ");
1036        self.inner.render_dyn(buf);
1037    }
1038    fn render_dyn_params(
1039        &self,
1040        buf: &mut String,
1041        params: &mut BTreeMap<String, serde_json::Value>,
1042    ) {
1043        buf.push_str("NOT ");
1044        self.inner.render_dyn_params(buf, params);
1045    }
1046}
1047
1048#[derive(Debug)]
1049pub struct ContainsExpr {
1050    pub(crate) haystack: Box<dyn DynExpr>,
1051    pub(crate) needle: Box<dyn DynExpr>,
1052}
1053
1054impl DynExpr for ContainsExpr {
1055    fn render_dyn(&self, buf: &mut String) {
1056        self.haystack.render_dyn(buf);
1057        buf.push_str(" CONTAINS ");
1058        self.needle.render_dyn(buf);
1059    }
1060    fn render_dyn_params(
1061        &self,
1062        buf: &mut String,
1063        params: &mut BTreeMap<String, serde_json::Value>,
1064    ) {
1065        self.haystack.render_dyn_params(buf, params);
1066        buf.push_str(" CONTAINS ");
1067        self.needle.render_dyn_params(buf, params);
1068    }
1069}
1070
1071// ═══════════════════════════════════════════════════════════════════════════════
1072// Column operator methods (Diesel-style: asset.name.eq("foo"))
1073// ═══════════════════════════════════════════════════════════════════════════════
1074
1075impl<T: SurrealRecord, V: SurrealQL> Column<T, V> {
1076    pub fn eq(&self, value: V) -> EqExpr {
1077        EqExpr {
1078            left: self.dyn_box(),
1079            right: Box::new(Literal(value)),
1080        }
1081    }
1082    pub fn ne(&self, value: V) -> NeExpr {
1083        NeExpr {
1084            left: self.dyn_box(),
1085            right: Box::new(Literal(value)),
1086        }
1087    }
1088    pub fn gt(&self, value: V) -> GtExpr {
1089        GtExpr {
1090            left: self.dyn_box(),
1091            right: Box::new(Literal(value)),
1092        }
1093    }
1094    pub fn lt(&self, value: V) -> LtExpr {
1095        LtExpr {
1096            left: self.dyn_box(),
1097            right: Box::new(Literal(value)),
1098        }
1099    }
1100    pub fn gte(&self, value: V) -> GteExpr {
1101        GteExpr {
1102            left: self.dyn_box(),
1103            right: Box::new(Literal(value)),
1104        }
1105    }
1106    pub fn lte(&self, value: V) -> LteExpr {
1107        LteExpr {
1108            left: self.dyn_box(),
1109            right: Box::new(Literal(value)),
1110        }
1111    }
1112    pub fn contains(&self, value: V) -> ContainsExpr {
1113        ContainsExpr {
1114            haystack: self.dyn_box(),
1115            needle: Box::new(Literal(value)),
1116        }
1117    }
1118    /// `column CONTAINS <expr>` — e.g. array-membership test with an expression.
1119    pub fn contains_expr(&self, expr: impl DynExpr + 'static) -> ContainsExpr {
1120        ContainsExpr {
1121            haystack: self.dyn_box(),
1122            needle: Box::new(expr),
1123        }
1124    }
1125
1126    /// Compare this column to an arbitrary expression — e.g. a [`RecordLink`]
1127    /// (`asset = type::record('asset', …)`) or [`NoneLit`] (`tenant = NONE`).
1128    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
1129        EqExpr {
1130            left: self.dyn_box(),
1131            right: Box::new(rhs),
1132        }
1133    }
1134    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
1135        NeExpr {
1136            left: self.dyn_box(),
1137            right: Box::new(rhs),
1138        }
1139    }
1140    /// `column IN <expr>` — e.g. an array literal or a parenthesized subquery.
1141    pub fn in_expr(&self, rhs: impl DynExpr + 'static) -> InExpr {
1142        InExpr {
1143            left: self.dyn_box(),
1144            right: Box::new(rhs),
1145        }
1146    }
1147    /// `column NOT IN <expr>`.
1148    pub fn not_in_expr(&self, rhs: impl DynExpr + 'static) -> NotInExpr {
1149        NotInExpr {
1150            left: self.dyn_box(),
1151            right: Box::new(rhs),
1152        }
1153    }
1154    /// `column IS NONE`.
1155    pub fn is_none(&self) -> Raw {
1156        Raw(format!("{} IS NONE", self.name))
1157    }
1158
1159    fn dyn_box(&self) -> Box<dyn DynExpr> {
1160        Box::new(Self {
1161            name: self.name,
1162            surreal_type: self.surreal_type,
1163            _marker: self._marker,
1164        })
1165    }
1166}
1167
1168// Combinators: (a = 1).and(b = 2).or(c = 3). Available on every expression node.
1169macro_rules! combinators {
1170    ($($t:ty),* $(,)?) => {$(
1171        impl $t {
1172            pub fn and(self, other: impl DynExpr + 'static) -> AndExpr {
1173                AndExpr { left: Box::new(self), right: Box::new(other) }
1174            }
1175            pub fn or(self, other: impl DynExpr + 'static) -> OrExpr {
1176                OrExpr { left: Box::new(self), right: Box::new(other) }
1177            }
1178        }
1179    )*};
1180}
1181combinators!(
1182    EqExpr,
1183    NeExpr,
1184    GtExpr,
1185    LtExpr,
1186    GteExpr,
1187    LteExpr,
1188    AndExpr,
1189    OrExpr,
1190    ContainsExpr,
1191    InExpr,
1192    NotInExpr,
1193    NotExpr,
1194    Raw
1195);
1196
1197/// Wraps an expression in parentheses: `(<expr>)`. Use to force grouping/precedence.
1198#[derive(Debug)]
1199pub struct Grouped(pub Box<dyn DynExpr>);
1200
1201impl Grouped {
1202    pub fn new(inner: impl DynExpr + 'static) -> Self {
1203        Self(Box::new(inner))
1204    }
1205}
1206
1207impl DynExpr for Grouped {
1208    fn render_dyn(&self, buf: &mut String) {
1209        buf.push('(');
1210        self.0.render_dyn(buf);
1211        buf.push(')');
1212    }
1213    fn render_dyn_params(
1214        &self,
1215        buf: &mut String,
1216        params: &mut BTreeMap<String, serde_json::Value>,
1217    ) {
1218        buf.push('(');
1219        self.0.render_dyn_params(buf, params);
1220        buf.push(')');
1221    }
1222}
1223
1224combinators!(Grouped, Func, Path);
1225
1226// ═══════════════════════════════════════════════════════════════════════════════
1227// IfExpr — IF … THEN … ELSE IF … ELSE … END
1228// ═══════════════════════════════════════════════════════════════════════════════
1229
1230/// A SurrealQL `IF <cond> THEN <expr> [ELSE IF <cond> THEN <expr>]… [ELSE <expr>]
1231/// END` expression. As a [`DynExpr`] it nests anywhere an expression is taken —
1232/// a `SET` value, a `SELECT` projection, a `RETURN`, or a `WHERE`.
1233///
1234/// ```ignore
1235/// IfExpr::new(Raw("age >= 18".into()), Raw("'adult'".into()))
1236///     .else_if(Raw("age >= 13".into()), Raw("'teen'".into()))
1237///     .else_(Raw("'child'".into()));
1238/// // IF age >= 18 THEN 'adult' ELSE IF age >= 13 THEN 'teen' ELSE 'child' END
1239/// ```
1240#[derive(Debug)]
1241pub struct IfExpr {
1242    branches: Vec<(Box<dyn DynExpr>, Box<dyn DynExpr>)>,
1243    else_branch: Option<Box<dyn DynExpr>>,
1244}
1245
1246impl IfExpr {
1247    /// Begin `IF <cond> THEN <then>`.
1248    pub fn new(cond: impl DynExpr + 'static, then: impl DynExpr + 'static) -> Self {
1249        Self {
1250            branches: vec![(Box::new(cond), Box::new(then))],
1251            else_branch: None,
1252        }
1253    }
1254    /// Add an `ELSE IF <cond> THEN <then>` branch.
1255    pub fn else_if(mut self, cond: impl DynExpr + 'static, then: impl DynExpr + 'static) -> Self {
1256        self.branches.push((Box::new(cond), Box::new(then)));
1257        self
1258    }
1259    /// Add the trailing `ELSE <expr>` branch.
1260    pub fn else_(mut self, expr: impl DynExpr + 'static) -> Self {
1261        self.else_branch = Some(Box::new(expr));
1262        self
1263    }
1264}
1265
1266impl DynExpr for IfExpr {
1267    fn render_dyn(&self, buf: &mut String) {
1268        for (i, (cond, then)) in self.branches.iter().enumerate() {
1269            buf.push_str(if i == 0 { "IF " } else { " ELSE IF " });
1270            cond.render_dyn(buf);
1271            buf.push_str(" THEN ");
1272            then.render_dyn(buf);
1273        }
1274        if let Some(e) = &self.else_branch {
1275            buf.push_str(" ELSE ");
1276            e.render_dyn(buf);
1277        }
1278        buf.push_str(" END");
1279    }
1280    fn render_dyn_params(
1281        &self,
1282        buf: &mut String,
1283        params: &mut BTreeMap<String, serde_json::Value>,
1284    ) {
1285        for (i, (cond, then)) in self.branches.iter().enumerate() {
1286            buf.push_str(if i == 0 { "IF " } else { " ELSE IF " });
1287            cond.render_dyn_params(buf, params);
1288            buf.push_str(" THEN ");
1289            then.render_dyn_params(buf, params);
1290        }
1291        if let Some(e) = &self.else_branch {
1292            buf.push_str(" ELSE ");
1293            e.render_dyn_params(buf, params);
1294        }
1295        buf.push_str(" END");
1296    }
1297}
1298
1299// ═══════════════════════════════════════════════════════════════════════════════
1300// Projection — a SELECT field, optionally `<expr> AS alias`
1301// ═══════════════════════════════════════════════════════════════════════════════
1302
1303/// A single SELECT-list entry. Either a bare expression or `<expr> AS <alias>`.
1304#[derive(Debug)]
1305pub struct Projection {
1306    expr: Box<dyn DynExpr>,
1307    alias: Option<&'static str>,
1308}
1309
1310impl Projection {
1311    /// A bare field/expression with no alias.
1312    pub fn new(expr: impl DynExpr + 'static) -> Self {
1313        Self {
1314            expr: Box::new(expr),
1315            alias: None,
1316        }
1317    }
1318    /// `<expr> AS <alias>`.
1319    pub fn aliased(expr: impl DynExpr + 'static, alias: &'static str) -> Self {
1320        Self {
1321            expr: Box::new(expr),
1322            alias: Some(alias),
1323        }
1324    }
1325    pub fn render(&self, buf: &mut String) {
1326        self.expr.render_dyn(buf);
1327        if let Some(a) = self.alias {
1328            buf.push_str(" AS ");
1329            buf.push_str(a);
1330        }
1331    }
1332    pub fn render_params(
1333        &self,
1334        buf: &mut String,
1335        params: &mut BTreeMap<String, serde_json::Value>,
1336    ) {
1337        self.expr.render_dyn_params(buf, params);
1338        if let Some(a) = self.alias {
1339            buf.push_str(" AS ");
1340            buf.push_str(a);
1341        }
1342    }
1343}
1344
1345/// A bare column name as a projection: `name`.
1346pub fn col(name: &'static str) -> Projection {
1347    Projection::new(Raw(name.to_string()))
1348}
1349
1350/// `<raw> AS <alias>` — verbatim expression with an alias.
1351pub fn field(raw: &'static str, alias: &'static str) -> Projection {
1352    Projection::aliased(Raw(raw.to_string()), alias)
1353}
1354
1355// ═══════════════════════════════════════════════════════════════════════════════
1356// ColumnSet — `*` selector generated by derive macro
1357// ═══════════════════════════════════════════════════════════════════════════════
1358
1359#[derive(Debug, Clone)]
1360pub struct ColumnMeta {
1361    pub name: &'static str,
1362    pub surreal_type: &'static str,
1363}
1364
1365/// Select-all (`*`) column list. Generated by `#[derive(SurrealRecord)]`.
1366pub struct ColumnSet<T: SurrealRecord> {
1367    pub cols: &'static [ColumnMeta],
1368    pub _marker: std::marker::PhantomData<T>,
1369}
1370
1371impl<T: SurrealRecord> std::fmt::Debug for ColumnSet<T> {
1372    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1373        f.debug_struct("ColumnSet")
1374            .field("cols", &self.cols)
1375            .finish()
1376    }
1377}
1378
1379impl<T: SurrealRecord> DynExpr for ColumnSet<T> {
1380    fn render_dyn(&self, buf: &mut String) {
1381        buf.push('*');
1382    }
1383}
1384
1385// ═══════════════════════════════════════════════════════════════════════════════
1386// ORDER BY
1387// ═══════════════════════════════════════════════════════════════════════════════
1388
1389/// Sort direction for `ORDER BY`.
1390#[derive(Debug, Clone, Copy)]
1391pub enum Order {
1392    /// Ascending (`ASC`).
1393    Asc,
1394    /// Descending (`DESC`).
1395    Desc,
1396}
1397
1398impl Order {
1399    pub fn render_suffix(&self) -> &'static str {
1400        match self {
1401            Order::Asc => "ASC",
1402            Order::Desc => "DESC",
1403        }
1404    }
1405}
1406
1407impl std::fmt::Display for Order {
1408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1409        f.write_str(self.render_suffix())
1410    }
1411}