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::fmt;
10
11use crate::types::SurrealRecord;
12
13// ═══════════════════════════════════════════════════════════════════════════════
14// DynExpr — type-erased expression (the internal transport)
15// ═══════════════════════════════════════════════════════════════════════════════
16
17/// A type-erased expression that can render itself into a SurrealQL buffer.
18/// Implemented by every expression node; the common currency of the builder.
19pub trait DynExpr: fmt::Debug + Send + Sync {
20    /// Append this expression's SurrealQL to `buf`.
21    fn render_dyn(&self, buf: &mut String);
22}
23
24/// A boxed, type-erased expression. Useful for returning a composed filter from
25/// a helper function (e.g. a shared tenant/owner predicate).
26pub type DynExprBox = Box<dyn DynExpr>;
27
28impl DynExpr for Box<dyn DynExpr> {
29    fn render_dyn(&self, buf: &mut String) {
30        (**self).render_dyn(buf);
31    }
32}
33
34// ═══════════════════════════════════════════════════════════════════════════════
35// Expr — typed expression (public API)
36// ═══════════════════════════════════════════════════════════════════════════════
37
38/// A [`DynExpr`] that also reports a SurrealQL type hint. Auto-implemented for
39/// every `DynExpr` (returning `"any"`); concrete nodes may override the hint.
40pub trait Expr: DynExpr {
41    /// A best-effort SurrealQL type name for this expression.
42    fn ty_hint(&self) -> &'static str;
43}
44
45// Any DynExpr is automatically an Expr with a default ty_hint
46// (used internally; concrete types override this)
47impl<E: DynExpr> Expr for E {
48    fn ty_hint(&self) -> &'static str {
49        "any"
50    }
51}
52
53// ═══════════════════════════════════════════════════════════════════════════════
54// SurrealQL — literal value rendering
55// ═══════════════════════════════════════════════════════════════════════════════
56
57/// A Rust type that can be rendered as a SurrealQL literal (the right-hand side of
58/// comparisons, `SET` values, etc.). Implemented for the common scalar types,
59/// `Option<T>`, `serde_json::Value`, the geometry types, and `Thing<T>`.
60pub trait SurrealQL: fmt::Debug + Clone + Send + Sync + 'static {
61    /// The SurrealQL type name (e.g. `"string"`, `"datetime"`).
62    fn surreal_type() -> &'static str;
63    /// Append the literal form of `value` to `buf` (with any needed quoting/escaping).
64    fn render_literal(value: &Self, buf: &mut String);
65}
66
67impl SurrealQL for String {
68    fn surreal_type() -> &'static str {
69        "string"
70    }
71    fn render_literal(value: &Self, buf: &mut String) {
72        let escaped = value.replace('\\', "\\\\").replace('\'', "\\'");
73        buf.push('\'');
74        buf.push_str(&escaped);
75        buf.push('\'');
76    }
77}
78
79impl SurrealQL for bool {
80    fn surreal_type() -> &'static str {
81        "bool"
82    }
83    fn render_literal(value: &Self, buf: &mut String) {
84        buf.push_str(if *value { "true" } else { "false" });
85    }
86}
87
88macro_rules! surreal_display {
89    ($t:ty, $name:literal) => {
90        impl SurrealQL for $t {
91            fn surreal_type() -> &'static str {
92                $name
93            }
94            fn render_literal(value: &Self, buf: &mut String) {
95                use std::fmt::Write;
96                let _ = write!(buf, "{value}");
97            }
98        }
99    };
100}
101surreal_display!(i64, "int");
102surreal_display!(i32, "int");
103surreal_display!(i16, "int");
104surreal_display!(i8, "int");
105surreal_display!(f64, "float");
106surreal_display!(f32, "float");
107surreal_display!(u32, "int");
108surreal_display!(u64, "int");
109surreal_display!(u16, "int");
110surreal_display!(u8, "int");
111
112impl SurrealQL for chrono::DateTime<chrono::Utc> {
113    fn surreal_type() -> &'static str {
114        "datetime"
115    }
116    fn render_literal(value: &Self, buf: &mut String) {
117        // SurrealDB 2.0+ requires the `d` prefix on datetime literals. A bare
118        // quoted string is a `string`, not a `datetime`, so `created_at > '…'`
119        // would compare against the wrong type; `created_at > d'…'` is correct.
120        buf.push_str("d'");
121        buf.push_str(&value.to_rfc3339());
122        buf.push('\'');
123    }
124}
125
126impl SurrealQL for uuid::Uuid {
127    fn surreal_type() -> &'static str {
128        "uuid"
129    }
130    fn render_literal(value: &Self, buf: &mut String) {
131        use std::fmt::Write;
132        // SurrealDB 2.0+ requires the `u` prefix on uuid literals; a bare quoted
133        // string is a `string`, not a `uuid`.
134        buf.push_str("u'");
135        let _ = write!(buf, "{value}");
136        buf.push('\'');
137    }
138}
139
140impl SurrealQL for serde_json::Value {
141    fn surreal_type() -> &'static str {
142        "object"
143    }
144    fn render_literal(value: &Self, buf: &mut String) {
145        // JSON is a syntactic subset of SurrealQL value literals (objects,
146        // arrays, numbers, bools, null, double-quoted strings all parse), so
147        // the serialized form is a valid inline literal.
148        use std::fmt::Write;
149        let _ = write!(buf, "{value}");
150    }
151}
152
153// Geometry literals render as GeoJSON objects (a valid SurrealQL object literal),
154// e.g. `{"type":"Point","coordinates":[1.0,2.0]}`.
155macro_rules! geometry_surrealql {
156    ($t:ident, $name:literal) => {
157        impl SurrealQL for crate::types::$t {
158            fn surreal_type() -> &'static str {
159                $name
160            }
161            fn render_literal(value: &Self, buf: &mut String) {
162                if let Ok(s) = serde_json::to_string(value) {
163                    buf.push_str(&s);
164                }
165            }
166        }
167    };
168}
169geometry_surrealql!(Point, "geometry<point>");
170geometry_surrealql!(LineString, "geometry<line>");
171geometry_surrealql!(Polygon, "geometry<polygon>");
172
173impl<T: SurrealQL> SurrealQL for Option<T> {
174    fn surreal_type() -> &'static str {
175        T::surreal_type()
176    }
177    fn render_literal(value: &Self, buf: &mut String) {
178        match value {
179            Some(v) => T::render_literal(v, buf),
180            None => buf.push_str("NONE"),
181        }
182    }
183}
184
185impl<V: SurrealQL> SurrealQL for Vec<V> {
186    fn surreal_type() -> &'static str {
187        // The element type is lost at this level; the derive emits the precise
188        // `array<…>` for `DEFINE FIELD`. This hint is informational only.
189        "array"
190    }
191    fn render_literal(value: &Self, buf: &mut String) {
192        buf.push('[');
193        for (i, v) in value.iter().enumerate() {
194            if i > 0 {
195                buf.push_str(", ");
196            }
197            V::render_literal(v, buf);
198        }
199        buf.push(']');
200    }
201}
202
203impl SurrealQL for std::time::Duration {
204    fn surreal_type() -> &'static str {
205        "duration"
206    }
207    fn render_literal(value: &Self, buf: &mut String) {
208        use std::fmt::Write;
209        // SurrealDB duration literal: a concatenation of unit-tagged components
210        // (e.g. `1s500ms`). Whole seconds + sub-second nanoseconds round-trips any
211        // `Duration` and parses cleanly.
212        let secs = value.as_secs();
213        let nanos = value.subsec_nanos();
214        if secs == 0 && nanos == 0 {
215            buf.push_str("0ns");
216            return;
217        }
218        if secs > 0 {
219            let _ = write!(buf, "{secs}s");
220        }
221        if nanos > 0 {
222            let _ = write!(buf, "{nanos}ns");
223        }
224    }
225}
226
227impl<T: crate::types::SurrealRecord> SurrealQL for crate::types::Thing<T> {
228    fn surreal_type() -> &'static str {
229        "record"
230    }
231    fn render_literal(value: &Self, buf: &mut String) {
232        buf.push_str(T::table_name());
233        buf.push(':');
234        value.key.render_id(buf);
235    }
236}
237
238// ═══════════════════════════════════════════════════════════════════════════════
239// Literal — wraps a SurrealQL value as an expression
240// ═══════════════════════════════════════════════════════════════════════════════
241
242#[derive(Debug, Clone)]
243pub struct Literal<V: SurrealQL>(pub V);
244
245impl<V: SurrealQL> DynExpr for Literal<V> {
246    fn render_dyn(&self, buf: &mut String) {
247        V::render_literal(&self.0, buf);
248    }
249}
250
251// ═══════════════════════════════════════════════════════════════════════════════
252// Column — typed field reference (e.g., `asset.name`)
253// ═══════════════════════════════════════════════════════════════════════════════
254
255/// A typed reference to a record field — e.g. `Post::title()` yields a
256/// `Column<Post, String>`. Generated by `#[derive(SurrealRecord)]` and used to
257/// build type-checked filters (`.eq(...)`, `.gt(...)`, …) and projections.
258pub struct Column<T: SurrealRecord, V: SurrealQL> {
259    /// The database column name.
260    pub name: &'static str,
261    /// The SurrealQL type name for this column.
262    pub surreal_type: &'static str,
263    #[doc(hidden)]
264    pub _marker: std::marker::PhantomData<(T, V)>,
265}
266
267impl<T: SurrealRecord, V: SurrealQL> std::fmt::Debug for Column<T, V> {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        f.debug_struct("Column").field("name", &self.name).finish()
270    }
271}
272
273impl<T: SurrealRecord, V: SurrealQL> Clone for Column<T, V> {
274    fn clone(&self) -> Self {
275        *self
276    }
277}
278impl<T: SurrealRecord, V: SurrealQL> Copy for Column<T, V> {}
279
280impl<T: SurrealRecord, V: SurrealQL> DynExpr for Column<T, V> {
281    fn render_dyn(&self, buf: &mut String) {
282        buf.push_str(self.name);
283    }
284}
285
286// ═══════════════════════════════════════════════════════════════════════════════
287// Ident — an untyped column/field reference usable in WHERE clauses
288// ═══════════════════════════════════════════════════════════════════════════════
289
290/// An untyped identifier (column or field path) for building filter expressions
291/// where a typed [`Column`] accessor is unavailable (e.g. record-link fields the
292/// derive doesn't expose, or `tenant.slug` paths). Mirrors `Column`'s operators.
293#[derive(Debug, Clone, Copy)]
294pub struct Ident(pub &'static str);
295
296/// Construct an [`Ident`] for a field name.
297pub fn ident(name: &'static str) -> Ident {
298    Ident(name)
299}
300
301impl Ident {
302    fn dyn_box(&self) -> Box<dyn DynExpr> {
303        Box::new(Raw(self.0.to_string()))
304    }
305
306    pub fn eq<V: SurrealQL>(&self, v: V) -> EqExpr {
307        EqExpr {
308            left: self.dyn_box(),
309            right: Box::new(Literal(v)),
310        }
311    }
312    pub fn ne<V: SurrealQL>(&self, v: V) -> NeExpr {
313        NeExpr {
314            left: self.dyn_box(),
315            right: Box::new(Literal(v)),
316        }
317    }
318    pub fn gt<V: SurrealQL>(&self, v: V) -> GtExpr {
319        GtExpr {
320            left: self.dyn_box(),
321            right: Box::new(Literal(v)),
322        }
323    }
324    pub fn lt<V: SurrealQL>(&self, v: V) -> LtExpr {
325        LtExpr {
326            left: self.dyn_box(),
327            right: Box::new(Literal(v)),
328        }
329    }
330    pub fn gte<V: SurrealQL>(&self, v: V) -> GteExpr {
331        GteExpr {
332            left: self.dyn_box(),
333            right: Box::new(Literal(v)),
334        }
335    }
336    pub fn lte<V: SurrealQL>(&self, v: V) -> LteExpr {
337        LteExpr {
338            left: self.dyn_box(),
339            right: Box::new(Literal(v)),
340        }
341    }
342    pub fn contains<V: SurrealQL>(&self, v: V) -> ContainsExpr {
343        ContainsExpr {
344            haystack: self.dyn_box(),
345            needle: Box::new(Literal(v)),
346        }
347    }
348    /// Compare to an arbitrary expression — e.g. `asset = type::record('asset', …)`.
349    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
350        EqExpr {
351            left: self.dyn_box(),
352            right: Box::new(rhs),
353        }
354    }
355    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
356        NeExpr {
357            left: self.dyn_box(),
358            right: Box::new(rhs),
359        }
360    }
361    /// `field IS NONE`.
362    pub fn is_none(&self) -> Raw {
363        Raw(format!("{} IS NONE", self.0))
364    }
365}
366
367impl DynExpr for Ident {
368    fn render_dyn(&self, buf: &mut String) {
369        buf.push_str(self.0);
370    }
371}
372
373// ═══════════════════════════════════════════════════════════════════════════════
374// Raw — verbatim SurrealQL escape hatch (lambdas, IF/THEN/ELSE, field paths…)
375// ═══════════════════════════════════════════════════════════════════════════════
376
377/// A verbatim SurrealQL fragment. Use for expressions somnia does not model as
378/// typed nodes (e.g. `IF x != NONE THEN … END`, lambdas, `tenant.slug`).
379#[derive(Debug, Clone)]
380pub struct Raw(pub String);
381
382impl Raw {
383    pub fn new(s: impl Into<String>) -> Self {
384        Self(s.into())
385    }
386}
387
388impl DynExpr for Raw {
389    fn render_dyn(&self, buf: &mut String) {
390        buf.push_str(&self.0);
391    }
392}
393
394/// SurrealDB's `NONE`.
395#[derive(Debug, Clone)]
396pub struct NoneLit;
397impl DynExpr for NoneLit {
398    fn render_dyn(&self, buf: &mut String) {
399        buf.push_str("NONE");
400    }
401}
402
403// ═══════════════════════════════════════════════════════════════════════════════
404// RecordLink — `type::record('table', <key>)`
405// ═══════════════════════════════════════════════════════════════════════════════
406
407/// Builds a SurrealDB record link `type::record('table', <key>)`. The key is any
408/// literal value (typically the bare UUID/string id of the related row).
409#[derive(Debug)]
410pub struct RecordLink {
411    table: &'static str,
412    key: Box<dyn DynExpr>,
413}
414
415impl RecordLink {
416    /// `type::record('table', '<key literal>')`.
417    pub fn new<V: SurrealQL>(table: &'static str, key: V) -> Self {
418        Self {
419            table,
420            key: Box::new(Literal(key)),
421        }
422    }
423    /// `type::record('table', <key expr>)` — key rendered from an arbitrary expr.
424    pub fn from_expr(table: &'static str, key: impl DynExpr + 'static) -> Self {
425        Self {
426            table,
427            key: Box::new(key),
428        }
429    }
430}
431
432impl DynExpr for RecordLink {
433    fn render_dyn(&self, buf: &mut String) {
434        buf.push_str("type::record('");
435        buf.push_str(self.table);
436        buf.push_str("', ");
437        self.key.render_dyn(buf);
438        buf.push(')');
439    }
440}
441
442// ═══════════════════════════════════════════════════════════════════════════════
443// Path — graph traversal (`->edge->table`, `<-edge<-table`, `<->edge<->table`)
444// ═══════════════════════════════════════════════════════════════════════════════
445
446/// Direction of a single graph hop.
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448enum Dir {
449    /// outgoing edge — `->`
450    Out,
451    /// incoming edge — `<-`
452    In,
453    /// either direction — `<->`
454    Both,
455}
456
457impl Dir {
458    fn arrow(self) -> &'static str {
459        match self {
460            Dir::Out => "->",
461            Dir::In => "<-",
462            Dir::Both => "<->",
463        }
464    }
465}
466
467/// One hop in a graph [`Path`]: a direction, an edge table, an optional
468/// destination table, and an optional `WHERE` filter on the edge.
469#[derive(Debug)]
470struct Step {
471    dir: Dir,
472    edge: &'static str,
473    dest: Option<&'static str>,
474    filter: Option<Box<dyn DynExpr>>,
475}
476
477impl Step {
478    fn render(&self, buf: &mut String) {
479        buf.push_str(self.dir.arrow());
480        match &self.filter {
481            // `->(edge WHERE <expr>)`
482            Some(f) => {
483                buf.push('(');
484                buf.push_str(self.edge);
485                buf.push_str(" WHERE ");
486                f.render_dyn(buf);
487                buf.push(')');
488            }
489            None => buf.push_str(self.edge),
490        }
491        if let Some(dest) = self.dest {
492            buf.push_str(self.dir.arrow());
493            buf.push_str(dest);
494        }
495    }
496}
497
498/// The trailing field accessor on a [`Path`] — `.field` or `.*`.
499#[derive(Debug)]
500enum Tail {
501    Field(String),
502    All,
503}
504
505/// A graph-traversal expression — e.g. `->wrote->post`, `<-wrote<-user`, or a
506/// multi-hop chain with an optional `.field`/`.*` accessor at the end.
507///
508/// A `Path` is a [`DynExpr`], so it works anywhere the builder takes an
509/// expression: as a `SELECT` projection (`Projection::aliased(path, "posts")`),
510/// inside a `WHERE` filter, or as a `SET` value. With no start it is relative to
511/// the statement's `FROM` table; [`Path::from_record`] anchors it to a record.
512///
513/// ```ignore
514/// // ->wrote->post.title AS titles
515/// let p = Path::out::<Wrote>().to::<Post>().field("title");
516/// Post::table().project(vec![Projection::aliased(p, "titles")]);
517/// ```
518#[derive(Debug)]
519pub struct Path {
520    start: Option<Box<dyn DynExpr>>,
521    recurse: Option<String>,
522    steps: Vec<Step>,
523    tail: Option<Tail>,
524}
525
526impl Path {
527    fn from_step(dir: Dir, edge: &'static str) -> Self {
528        Self {
529            start: None,
530            recurse: None,
531            steps: vec![Step {
532                dir,
533                edge,
534                dest: None,
535                filter: None,
536            }],
537            tail: None,
538        }
539    }
540
541    /// Start an outgoing hop over edge `E` — `->edge`.
542    pub fn out<E: crate::types::SurrealEdge>() -> Self {
543        Self::from_step(Dir::Out, E::edge_name())
544    }
545    /// Start an incoming hop over edge `E` — `<-edge`.
546    pub fn inn<E: crate::types::SurrealEdge>() -> Self {
547        Self::from_step(Dir::In, E::edge_name())
548    }
549    /// Start a bidirectional hop over edge `E` — `<->edge`.
550    pub fn both<E: crate::types::SurrealEdge>() -> Self {
551        Self::from_step(Dir::Both, E::edge_name())
552    }
553
554    /// Start an outgoing hop over a raw edge name — `->edge`.
555    pub fn out_edge(edge: &'static str) -> Self {
556        Self::from_step(Dir::Out, edge)
557    }
558    /// Start an incoming hop over a raw edge name — `<-edge`.
559    pub fn in_edge(edge: &'static str) -> Self {
560        Self::from_step(Dir::In, edge)
561    }
562    /// Start a bidirectional hop over a raw edge name — `<->edge`.
563    pub fn both_edge(edge: &'static str) -> Self {
564        Self::from_step(Dir::Both, edge)
565    }
566
567    /// Anchor an existing path to a starting record literal — `<record><path>`
568    /// (e.g. `user:tobie->wrote->post`). Pass a [`Thing`](crate::types::Thing).
569    pub fn from_record<V: SurrealQL>(mut self, start: V) -> Self {
570        self.start = Some(Box::new(Literal(start)));
571        self
572    }
573
574    /// Anchor an existing path to a starting expression — e.g. a
575    /// [`RecordLink`] (`type::record('user', $id)->wrote->post`).
576    pub fn from_expr(mut self, start: impl DynExpr + 'static) -> Self {
577        self.start = Some(Box::new(start));
578        self
579    }
580
581    fn last_mut(&mut self) -> &mut Step {
582        self.steps.last_mut().expect("path always has ≥1 step")
583    }
584
585    /// Constrain the destination of the most recent hop to table `T` — `->edge->table`.
586    pub fn to<T: SurrealRecord>(mut self) -> Self {
587        self.last_mut().dest = Some(T::table_name());
588        self
589    }
590    /// Constrain the destination of the most recent hop to a raw table name.
591    pub fn to_table(mut self, table: &'static str) -> Self {
592        self.last_mut().dest = Some(table);
593        self
594    }
595
596    /// Filter the most recent hop's edge — `->(edge WHERE <expr>)`.
597    pub fn where_(mut self, expr: impl DynExpr + 'static) -> Self {
598        self.last_mut().filter = Some(Box::new(expr));
599        self
600    }
601
602    /// Chain another outgoing hop over edge `E`.
603    pub fn then_out<E: crate::types::SurrealEdge>(self) -> Self {
604        self.push_step(Dir::Out, E::edge_name())
605    }
606    /// Chain another incoming hop over edge `E`.
607    pub fn then_in<E: crate::types::SurrealEdge>(self) -> Self {
608        self.push_step(Dir::In, E::edge_name())
609    }
610    /// Chain another bidirectional hop over edge `E`.
611    pub fn then_both<E: crate::types::SurrealEdge>(self) -> Self {
612        self.push_step(Dir::Both, E::edge_name())
613    }
614    /// Chain another hop over a raw edge name, outgoing.
615    pub fn then_out_edge(self, edge: &'static str) -> Self {
616        self.push_step(Dir::Out, edge)
617    }
618    /// Chain another hop over a raw edge name, incoming.
619    pub fn then_in_edge(self, edge: &'static str) -> Self {
620        self.push_step(Dir::In, edge)
621    }
622
623    fn push_step(mut self, dir: Dir, edge: &'static str) -> Self {
624        self.steps.push(Step {
625            dir,
626            edge,
627            dest: None,
628            filter: None,
629        });
630        self
631    }
632
633    /// Repeat the path recursively, unbounded — `@.{..}<path>`. Combine with
634    /// [`from_record`](Self::from_record) to anchor the recursion at a record
635    /// (`person:tobie.{..}->knows->person`).
636    pub fn recurse_all(mut self) -> Self {
637        self.recurse = Some("..".to_string());
638        self
639    }
640    /// Recurse up to `max` hops — `@.{..max}<path>`.
641    pub fn recurse_up_to(mut self, max: u32) -> Self {
642        self.recurse = Some(format!("..{max}"));
643        self
644    }
645    /// Recurse between `min` and `max` hops — `@.{min..max}<path>`.
646    pub fn recurse_range(mut self, min: u32, max: u32) -> Self {
647        self.recurse = Some(format!("{min}..{max}"));
648        self
649    }
650    /// Recurse exactly `n` hops — `@.{n}<path>`.
651    pub fn recurse_exact(mut self, n: u32) -> Self {
652        self.recurse = Some(format!("{n}"));
653        self
654    }
655
656    /// Append a field accessor — `<path>.field`.
657    pub fn field(mut self, name: impl Into<String>) -> Self {
658        self.tail = Some(Tail::Field(name.into()));
659        self
660    }
661    /// Append the all-fields accessor — `<path>.*`.
662    pub fn all(mut self) -> Self {
663        self.tail = Some(Tail::All);
664        self
665    }
666
667    /// `<path> CONTAINS <value>` — e.g. membership test over a traversed list.
668    pub fn contains<V: SurrealQL>(self, value: V) -> ContainsExpr {
669        ContainsExpr {
670            haystack: Box::new(self),
671            needle: Box::new(Literal(value)),
672        }
673    }
674    /// `<path> = <expr>`.
675    pub fn eq_expr(self, rhs: impl DynExpr + 'static) -> EqExpr {
676        EqExpr {
677            left: Box::new(self),
678            right: Box::new(rhs),
679        }
680    }
681}
682
683impl DynExpr for Path {
684    fn render_dyn(&self, buf: &mut String) {
685        match (&self.start, &self.recurse) {
686            // anchored recursion: `<record>.{range}<path>`
687            (Some(start), Some(range)) => {
688                start.render_dyn(buf);
689                buf.push_str(".{");
690                buf.push_str(range);
691                buf.push('}');
692            }
693            // relative recursion: `@.{range}<path>` (the `@` is the recursion point)
694            (None, Some(range)) => {
695                buf.push_str("@.{");
696                buf.push_str(range);
697                buf.push('}');
698            }
699            // no recursion: optional record anchor, then the hops
700            (Some(start), None) => start.render_dyn(buf),
701            (None, None) => {}
702        }
703        for step in &self.steps {
704            step.render(buf);
705        }
706        match &self.tail {
707            Some(Tail::Field(f)) => {
708                buf.push('.');
709                buf.push_str(f);
710            }
711            Some(Tail::All) => buf.push_str(".*"),
712            None => {}
713        }
714    }
715}
716
717// ═══════════════════════════════════════════════════════════════════════════════
718// Func — `name(arg, arg, …)` (record::id, type::string, string::lowercase, …)
719// ═══════════════════════════════════════════════════════════════════════════════
720
721/// A SurrealQL function call `name(args…)`.
722#[derive(Debug)]
723pub struct Func {
724    name: &'static str,
725    args: Vec<Box<dyn DynExpr>>,
726}
727
728impl Func {
729    pub fn new(name: &'static str, args: Vec<Box<dyn DynExpr>>) -> Self {
730        Self { name, args }
731    }
732    /// Single-argument function over a bare column/identifier, e.g.
733    /// `record::id(id)` → `Func::of("record::id", "id")`.
734    pub fn of(name: &'static str, ident: &'static str) -> Self {
735        Self {
736            name,
737            args: vec![Box::new(Raw(ident.to_string()))],
738        }
739    }
740}
741
742impl DynExpr for Func {
743    fn render_dyn(&self, buf: &mut String) {
744        buf.push_str(self.name);
745        buf.push('(');
746        for (i, a) in self.args.iter().enumerate() {
747            if i > 0 {
748                buf.push_str(", ");
749            }
750            a.render_dyn(buf);
751        }
752        buf.push(')');
753    }
754}
755
756// ═══════════════════════════════════════════════════════════════════════════════
757// Binary expression nodes
758// ═══════════════════════════════════════════════════════════════════════════════
759
760macro_rules! binop {
761    ($name:ident, $op:literal) => {
762        #[derive(Debug)]
763        pub struct $name {
764            pub(crate) left: Box<dyn DynExpr>,
765            pub(crate) right: Box<dyn DynExpr>,
766        }
767        impl DynExpr for $name {
768            fn render_dyn(&self, buf: &mut String) {
769                self.left.render_dyn(buf);
770                buf.push(' ');
771                buf.push_str($op);
772                buf.push(' ');
773                self.right.render_dyn(buf);
774            }
775        }
776    };
777}
778
779binop!(EqExpr, "=");
780binop!(NeExpr, "!=");
781binop!(GtExpr, ">");
782binop!(LtExpr, "<");
783binop!(GteExpr, ">=");
784binop!(LteExpr, "<=");
785binop!(AndExpr, "AND");
786binop!(OrExpr, "OR");
787
788#[derive(Debug)]
789pub struct NotExpr {
790    pub(crate) inner: Box<dyn DynExpr>,
791}
792
793impl DynExpr for NotExpr {
794    fn render_dyn(&self, buf: &mut String) {
795        buf.push_str("NOT ");
796        self.inner.render_dyn(buf);
797    }
798}
799
800#[derive(Debug)]
801pub struct ContainsExpr {
802    pub(crate) haystack: Box<dyn DynExpr>,
803    pub(crate) needle: Box<dyn DynExpr>,
804}
805
806impl DynExpr for ContainsExpr {
807    fn render_dyn(&self, buf: &mut String) {
808        self.haystack.render_dyn(buf);
809        buf.push_str(" CONTAINS ");
810        self.needle.render_dyn(buf);
811    }
812}
813
814// ═══════════════════════════════════════════════════════════════════════════════
815// Column operator methods (Diesel-style: asset.name.eq("foo"))
816// ═══════════════════════════════════════════════════════════════════════════════
817
818impl<T: SurrealRecord, V: SurrealQL> Column<T, V> {
819    pub fn eq(&self, value: V) -> EqExpr {
820        EqExpr {
821            left: self.dyn_box(),
822            right: Box::new(Literal(value)),
823        }
824    }
825    pub fn ne(&self, value: V) -> NeExpr {
826        NeExpr {
827            left: self.dyn_box(),
828            right: Box::new(Literal(value)),
829        }
830    }
831    pub fn gt(&self, value: V) -> GtExpr {
832        GtExpr {
833            left: self.dyn_box(),
834            right: Box::new(Literal(value)),
835        }
836    }
837    pub fn lt(&self, value: V) -> LtExpr {
838        LtExpr {
839            left: self.dyn_box(),
840            right: Box::new(Literal(value)),
841        }
842    }
843    pub fn gte(&self, value: V) -> GteExpr {
844        GteExpr {
845            left: self.dyn_box(),
846            right: Box::new(Literal(value)),
847        }
848    }
849    pub fn lte(&self, value: V) -> LteExpr {
850        LteExpr {
851            left: self.dyn_box(),
852            right: Box::new(Literal(value)),
853        }
854    }
855    pub fn contains(&self, value: V) -> ContainsExpr {
856        ContainsExpr {
857            haystack: self.dyn_box(),
858            needle: Box::new(Literal(value)),
859        }
860    }
861
862    /// Compare this column to an arbitrary expression — e.g. a [`RecordLink`]
863    /// (`asset = type::record('asset', …)`) or [`NoneLit`] (`tenant = NONE`).
864    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
865        EqExpr {
866            left: self.dyn_box(),
867            right: Box::new(rhs),
868        }
869    }
870    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
871        NeExpr {
872            left: self.dyn_box(),
873            right: Box::new(rhs),
874        }
875    }
876    /// `column IS NONE`.
877    pub fn is_none(&self) -> Raw {
878        Raw(format!("{} IS NONE", self.name))
879    }
880
881    fn dyn_box(&self) -> Box<dyn DynExpr> {
882        Box::new(Self {
883            name: self.name,
884            surreal_type: self.surreal_type,
885            _marker: self._marker,
886        })
887    }
888}
889
890// Combinators: (a = 1).and(b = 2).or(c = 3). Available on every expression node.
891macro_rules! combinators {
892    ($($t:ty),* $(,)?) => {$(
893        impl $t {
894            pub fn and(self, other: impl DynExpr + 'static) -> AndExpr {
895                AndExpr { left: Box::new(self), right: Box::new(other) }
896            }
897            pub fn or(self, other: impl DynExpr + 'static) -> OrExpr {
898                OrExpr { left: Box::new(self), right: Box::new(other) }
899            }
900        }
901    )*};
902}
903combinators!(
904    EqExpr,
905    NeExpr,
906    GtExpr,
907    LtExpr,
908    GteExpr,
909    LteExpr,
910    AndExpr,
911    OrExpr,
912    ContainsExpr,
913    NotExpr,
914    Raw
915);
916
917/// Wraps an expression in parentheses: `(<expr>)`. Use to force grouping/precedence.
918#[derive(Debug)]
919pub struct Grouped(pub Box<dyn DynExpr>);
920
921impl Grouped {
922    pub fn new(inner: impl DynExpr + 'static) -> Self {
923        Self(Box::new(inner))
924    }
925}
926
927impl DynExpr for Grouped {
928    fn render_dyn(&self, buf: &mut String) {
929        buf.push('(');
930        self.0.render_dyn(buf);
931        buf.push(')');
932    }
933}
934
935combinators!(Grouped, Func, Path);
936
937// ═══════════════════════════════════════════════════════════════════════════════
938// Projection — a SELECT field, optionally `<expr> AS alias`
939// ═══════════════════════════════════════════════════════════════════════════════
940
941/// A single SELECT-list entry. Either a bare expression or `<expr> AS <alias>`.
942#[derive(Debug)]
943pub struct Projection {
944    expr: Box<dyn DynExpr>,
945    alias: Option<&'static str>,
946}
947
948impl Projection {
949    /// A bare field/expression with no alias.
950    pub fn new(expr: impl DynExpr + 'static) -> Self {
951        Self {
952            expr: Box::new(expr),
953            alias: None,
954        }
955    }
956    /// `<expr> AS <alias>`.
957    pub fn aliased(expr: impl DynExpr + 'static, alias: &'static str) -> Self {
958        Self {
959            expr: Box::new(expr),
960            alias: Some(alias),
961        }
962    }
963    pub fn render(&self, buf: &mut String) {
964        self.expr.render_dyn(buf);
965        if let Some(a) = self.alias {
966            buf.push_str(" AS ");
967            buf.push_str(a);
968        }
969    }
970}
971
972/// A bare column name as a projection: `name`.
973pub fn col(name: &'static str) -> Projection {
974    Projection::new(Raw(name.to_string()))
975}
976
977/// `<raw> AS <alias>` — verbatim expression with an alias.
978pub fn field(raw: &'static str, alias: &'static str) -> Projection {
979    Projection::aliased(Raw(raw.to_string()), alias)
980}
981
982// ═══════════════════════════════════════════════════════════════════════════════
983// ColumnSet — `*` selector generated by derive macro
984// ═══════════════════════════════════════════════════════════════════════════════
985
986#[derive(Debug, Clone)]
987pub struct ColumnMeta {
988    pub name: &'static str,
989    pub surreal_type: &'static str,
990}
991
992/// Select-all (`*`) column list. Generated by `#[derive(SurrealRecord)]`.
993pub struct ColumnSet<T: SurrealRecord> {
994    pub cols: &'static [ColumnMeta],
995    pub _marker: std::marker::PhantomData<T>,
996}
997
998impl<T: SurrealRecord> std::fmt::Debug for ColumnSet<T> {
999    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1000        f.debug_struct("ColumnSet")
1001            .field("cols", &self.cols)
1002            .finish()
1003    }
1004}
1005
1006impl<T: SurrealRecord> DynExpr for ColumnSet<T> {
1007    fn render_dyn(&self, buf: &mut String) {
1008        buf.push('*');
1009    }
1010}
1011
1012// ═══════════════════════════════════════════════════════════════════════════════
1013// ORDER BY
1014// ═══════════════════════════════════════════════════════════════════════════════
1015
1016/// Sort direction for `ORDER BY`.
1017#[derive(Debug, Clone, Copy)]
1018pub enum Order {
1019    /// Ascending (`ASC`).
1020    Asc,
1021    /// Descending (`DESC`).
1022    Desc,
1023}
1024
1025impl Order {
1026    pub fn render_suffix(&self) -> &'static str {
1027        match self {
1028            Order::Asc => "ASC",
1029            Order::Desc => "DESC",
1030        }
1031    }
1032}
1033
1034impl std::fmt::Display for Order {
1035    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1036        f.write_str(self.render_suffix())
1037    }
1038}