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<T: crate::types::SurrealRecord> SurrealQL for crate::types::Thing<T> {
186    fn surreal_type() -> &'static str {
187        "record"
188    }
189    fn render_literal(value: &Self, buf: &mut String) {
190        buf.push_str(T::table_name());
191        buf.push(':');
192        value.key.render_id(buf);
193    }
194}
195
196// ═══════════════════════════════════════════════════════════════════════════════
197// Literal — wraps a SurrealQL value as an expression
198// ═══════════════════════════════════════════════════════════════════════════════
199
200#[derive(Debug, Clone)]
201pub struct Literal<V: SurrealQL>(pub V);
202
203impl<V: SurrealQL> DynExpr for Literal<V> {
204    fn render_dyn(&self, buf: &mut String) {
205        V::render_literal(&self.0, buf);
206    }
207}
208
209// ═══════════════════════════════════════════════════════════════════════════════
210// Column — typed field reference (e.g., `asset.name`)
211// ═══════════════════════════════════════════════════════════════════════════════
212
213/// A typed reference to a record field — e.g. `Post::title()` yields a
214/// `Column<Post, String>`. Generated by `#[derive(SurrealRecord)]` and used to
215/// build type-checked filters (`.eq(...)`, `.gt(...)`, …) and projections.
216pub struct Column<T: SurrealRecord, V: SurrealQL> {
217    /// The database column name.
218    pub name: &'static str,
219    /// The SurrealQL type name for this column.
220    pub surreal_type: &'static str,
221    #[doc(hidden)]
222    pub _marker: std::marker::PhantomData<(T, V)>,
223}
224
225impl<T: SurrealRecord, V: SurrealQL> std::fmt::Debug for Column<T, V> {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        f.debug_struct("Column").field("name", &self.name).finish()
228    }
229}
230
231impl<T: SurrealRecord, V: SurrealQL> Clone for Column<T, V> {
232    fn clone(&self) -> Self {
233        *self
234    }
235}
236impl<T: SurrealRecord, V: SurrealQL> Copy for Column<T, V> {}
237
238impl<T: SurrealRecord, V: SurrealQL> DynExpr for Column<T, V> {
239    fn render_dyn(&self, buf: &mut String) {
240        buf.push_str(self.name);
241    }
242}
243
244// ═══════════════════════════════════════════════════════════════════════════════
245// Ident — an untyped column/field reference usable in WHERE clauses
246// ═══════════════════════════════════════════════════════════════════════════════
247
248/// An untyped identifier (column or field path) for building filter expressions
249/// where a typed [`Column`] accessor is unavailable (e.g. record-link fields the
250/// derive doesn't expose, or `tenant.slug` paths). Mirrors `Column`'s operators.
251#[derive(Debug, Clone, Copy)]
252pub struct Ident(pub &'static str);
253
254/// Construct an [`Ident`] for a field name.
255pub fn ident(name: &'static str) -> Ident {
256    Ident(name)
257}
258
259impl Ident {
260    fn dyn_box(&self) -> Box<dyn DynExpr> {
261        Box::new(Raw(self.0.to_string()))
262    }
263
264    pub fn eq<V: SurrealQL>(&self, v: V) -> EqExpr {
265        EqExpr {
266            left: self.dyn_box(),
267            right: Box::new(Literal(v)),
268        }
269    }
270    pub fn ne<V: SurrealQL>(&self, v: V) -> NeExpr {
271        NeExpr {
272            left: self.dyn_box(),
273            right: Box::new(Literal(v)),
274        }
275    }
276    pub fn gt<V: SurrealQL>(&self, v: V) -> GtExpr {
277        GtExpr {
278            left: self.dyn_box(),
279            right: Box::new(Literal(v)),
280        }
281    }
282    pub fn lt<V: SurrealQL>(&self, v: V) -> LtExpr {
283        LtExpr {
284            left: self.dyn_box(),
285            right: Box::new(Literal(v)),
286        }
287    }
288    pub fn gte<V: SurrealQL>(&self, v: V) -> GteExpr {
289        GteExpr {
290            left: self.dyn_box(),
291            right: Box::new(Literal(v)),
292        }
293    }
294    pub fn lte<V: SurrealQL>(&self, v: V) -> LteExpr {
295        LteExpr {
296            left: self.dyn_box(),
297            right: Box::new(Literal(v)),
298        }
299    }
300    pub fn contains<V: SurrealQL>(&self, v: V) -> ContainsExpr {
301        ContainsExpr {
302            haystack: self.dyn_box(),
303            needle: Box::new(Literal(v)),
304        }
305    }
306    /// Compare to an arbitrary expression — e.g. `asset = type::record('asset', …)`.
307    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
308        EqExpr {
309            left: self.dyn_box(),
310            right: Box::new(rhs),
311        }
312    }
313    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
314        NeExpr {
315            left: self.dyn_box(),
316            right: Box::new(rhs),
317        }
318    }
319    /// `field IS NONE`.
320    pub fn is_none(&self) -> Raw {
321        Raw(format!("{} IS NONE", self.0))
322    }
323}
324
325impl DynExpr for Ident {
326    fn render_dyn(&self, buf: &mut String) {
327        buf.push_str(self.0);
328    }
329}
330
331// ═══════════════════════════════════════════════════════════════════════════════
332// Raw — verbatim SurrealQL escape hatch (lambdas, IF/THEN/ELSE, field paths…)
333// ═══════════════════════════════════════════════════════════════════════════════
334
335/// A verbatim SurrealQL fragment. Use for expressions somnia does not model as
336/// typed nodes (e.g. `IF x != NONE THEN … END`, lambdas, `tenant.slug`).
337#[derive(Debug, Clone)]
338pub struct Raw(pub String);
339
340impl Raw {
341    pub fn new(s: impl Into<String>) -> Self {
342        Self(s.into())
343    }
344}
345
346impl DynExpr for Raw {
347    fn render_dyn(&self, buf: &mut String) {
348        buf.push_str(&self.0);
349    }
350}
351
352/// SurrealDB's `NONE`.
353#[derive(Debug, Clone)]
354pub struct NoneLit;
355impl DynExpr for NoneLit {
356    fn render_dyn(&self, buf: &mut String) {
357        buf.push_str("NONE");
358    }
359}
360
361// ═══════════════════════════════════════════════════════════════════════════════
362// RecordLink — `type::record('table', <key>)`
363// ═══════════════════════════════════════════════════════════════════════════════
364
365/// Builds a SurrealDB record link `type::record('table', <key>)`. The key is any
366/// literal value (typically the bare UUID/string id of the related row).
367#[derive(Debug)]
368pub struct RecordLink {
369    table: &'static str,
370    key: Box<dyn DynExpr>,
371}
372
373impl RecordLink {
374    /// `type::record('table', '<key literal>')`.
375    pub fn new<V: SurrealQL>(table: &'static str, key: V) -> Self {
376        Self {
377            table,
378            key: Box::new(Literal(key)),
379        }
380    }
381    /// `type::record('table', <key expr>)` — key rendered from an arbitrary expr.
382    pub fn from_expr(table: &'static str, key: impl DynExpr + 'static) -> Self {
383        Self {
384            table,
385            key: Box::new(key),
386        }
387    }
388}
389
390impl DynExpr for RecordLink {
391    fn render_dyn(&self, buf: &mut String) {
392        buf.push_str("type::record('");
393        buf.push_str(self.table);
394        buf.push_str("', ");
395        self.key.render_dyn(buf);
396        buf.push(')');
397    }
398}
399
400// ═══════════════════════════════════════════════════════════════════════════════
401// Path — graph traversal (`->edge->table`, `<-edge<-table`, `<->edge<->table`)
402// ═══════════════════════════════════════════════════════════════════════════════
403
404/// Direction of a single graph hop.
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406enum Dir {
407    /// outgoing edge — `->`
408    Out,
409    /// incoming edge — `<-`
410    In,
411    /// either direction — `<->`
412    Both,
413}
414
415impl Dir {
416    fn arrow(self) -> &'static str {
417        match self {
418            Dir::Out => "->",
419            Dir::In => "<-",
420            Dir::Both => "<->",
421        }
422    }
423}
424
425/// One hop in a graph [`Path`]: a direction, an edge table, an optional
426/// destination table, and an optional `WHERE` filter on the edge.
427#[derive(Debug)]
428struct Step {
429    dir: Dir,
430    edge: &'static str,
431    dest: Option<&'static str>,
432    filter: Option<Box<dyn DynExpr>>,
433}
434
435impl Step {
436    fn render(&self, buf: &mut String) {
437        buf.push_str(self.dir.arrow());
438        match &self.filter {
439            // `->(edge WHERE <expr>)`
440            Some(f) => {
441                buf.push('(');
442                buf.push_str(self.edge);
443                buf.push_str(" WHERE ");
444                f.render_dyn(buf);
445                buf.push(')');
446            }
447            None => buf.push_str(self.edge),
448        }
449        if let Some(dest) = self.dest {
450            buf.push_str(self.dir.arrow());
451            buf.push_str(dest);
452        }
453    }
454}
455
456/// The trailing field accessor on a [`Path`] — `.field` or `.*`.
457#[derive(Debug)]
458enum Tail {
459    Field(String),
460    All,
461}
462
463/// A graph-traversal expression — e.g. `->wrote->post`, `<-wrote<-user`, or a
464/// multi-hop chain with an optional `.field`/`.*` accessor at the end.
465///
466/// A `Path` is a [`DynExpr`], so it works anywhere the builder takes an
467/// expression: as a `SELECT` projection (`Projection::aliased(path, "posts")`),
468/// inside a `WHERE` filter, or as a `SET` value. With no start it is relative to
469/// the statement's `FROM` table; [`Path::from_record`] anchors it to a record.
470///
471/// ```ignore
472/// // ->wrote->post.title AS titles
473/// let p = Path::out::<Wrote>().to::<Post>().field("title");
474/// Post::table().project(vec![Projection::aliased(p, "titles")]);
475/// ```
476#[derive(Debug)]
477pub struct Path {
478    start: Option<Box<dyn DynExpr>>,
479    steps: Vec<Step>,
480    tail: Option<Tail>,
481}
482
483impl Path {
484    fn from_step(dir: Dir, edge: &'static str) -> Self {
485        Self {
486            start: None,
487            steps: vec![Step {
488                dir,
489                edge,
490                dest: None,
491                filter: None,
492            }],
493            tail: None,
494        }
495    }
496
497    /// Start an outgoing hop over edge `E` — `->edge`.
498    pub fn out<E: crate::types::SurrealEdge>() -> Self {
499        Self::from_step(Dir::Out, E::edge_name())
500    }
501    /// Start an incoming hop over edge `E` — `<-edge`.
502    pub fn inn<E: crate::types::SurrealEdge>() -> Self {
503        Self::from_step(Dir::In, E::edge_name())
504    }
505    /// Start a bidirectional hop over edge `E` — `<->edge`.
506    pub fn both<E: crate::types::SurrealEdge>() -> Self {
507        Self::from_step(Dir::Both, E::edge_name())
508    }
509
510    /// Start an outgoing hop over a raw edge name — `->edge`.
511    pub fn out_edge(edge: &'static str) -> Self {
512        Self::from_step(Dir::Out, edge)
513    }
514    /// Start an incoming hop over a raw edge name — `<-edge`.
515    pub fn in_edge(edge: &'static str) -> Self {
516        Self::from_step(Dir::In, edge)
517    }
518    /// Start a bidirectional hop over a raw edge name — `<->edge`.
519    pub fn both_edge(edge: &'static str) -> Self {
520        Self::from_step(Dir::Both, edge)
521    }
522
523    /// Anchor an existing path to a starting record literal — `<record><path>`
524    /// (e.g. `user:tobie->wrote->post`). Pass a [`Thing`](crate::types::Thing).
525    pub fn from_record<V: SurrealQL>(mut self, start: V) -> Self {
526        self.start = Some(Box::new(Literal(start)));
527        self
528    }
529
530    /// Anchor an existing path to a starting expression — e.g. a
531    /// [`RecordLink`] (`type::record('user', $id)->wrote->post`).
532    pub fn from_expr(mut self, start: impl DynExpr + 'static) -> Self {
533        self.start = Some(Box::new(start));
534        self
535    }
536
537    fn last_mut(&mut self) -> &mut Step {
538        self.steps.last_mut().expect("path always has ≥1 step")
539    }
540
541    /// Constrain the destination of the most recent hop to table `T` — `->edge->table`.
542    pub fn to<T: SurrealRecord>(mut self) -> Self {
543        self.last_mut().dest = Some(T::table_name());
544        self
545    }
546    /// Constrain the destination of the most recent hop to a raw table name.
547    pub fn to_table(mut self, table: &'static str) -> Self {
548        self.last_mut().dest = Some(table);
549        self
550    }
551
552    /// Filter the most recent hop's edge — `->(edge WHERE <expr>)`.
553    pub fn where_(mut self, expr: impl DynExpr + 'static) -> Self {
554        self.last_mut().filter = Some(Box::new(expr));
555        self
556    }
557
558    /// Chain another outgoing hop over edge `E`.
559    pub fn then_out<E: crate::types::SurrealEdge>(self) -> Self {
560        self.push_step(Dir::Out, E::edge_name())
561    }
562    /// Chain another incoming hop over edge `E`.
563    pub fn then_in<E: crate::types::SurrealEdge>(self) -> Self {
564        self.push_step(Dir::In, E::edge_name())
565    }
566    /// Chain another bidirectional hop over edge `E`.
567    pub fn then_both<E: crate::types::SurrealEdge>(self) -> Self {
568        self.push_step(Dir::Both, E::edge_name())
569    }
570    /// Chain another hop over a raw edge name, outgoing.
571    pub fn then_out_edge(self, edge: &'static str) -> Self {
572        self.push_step(Dir::Out, edge)
573    }
574    /// Chain another hop over a raw edge name, incoming.
575    pub fn then_in_edge(self, edge: &'static str) -> Self {
576        self.push_step(Dir::In, edge)
577    }
578
579    fn push_step(mut self, dir: Dir, edge: &'static str) -> Self {
580        self.steps.push(Step {
581            dir,
582            edge,
583            dest: None,
584            filter: None,
585        });
586        self
587    }
588
589    /// Append a field accessor — `<path>.field`.
590    pub fn field(mut self, name: impl Into<String>) -> Self {
591        self.tail = Some(Tail::Field(name.into()));
592        self
593    }
594    /// Append the all-fields accessor — `<path>.*`.
595    pub fn all(mut self) -> Self {
596        self.tail = Some(Tail::All);
597        self
598    }
599
600    /// `<path> CONTAINS <value>` — e.g. membership test over a traversed list.
601    pub fn contains<V: SurrealQL>(self, value: V) -> ContainsExpr {
602        ContainsExpr {
603            haystack: Box::new(self),
604            needle: Box::new(Literal(value)),
605        }
606    }
607    /// `<path> = <expr>`.
608    pub fn eq_expr(self, rhs: impl DynExpr + 'static) -> EqExpr {
609        EqExpr {
610            left: Box::new(self),
611            right: Box::new(rhs),
612        }
613    }
614}
615
616impl DynExpr for Path {
617    fn render_dyn(&self, buf: &mut String) {
618        if let Some(start) = &self.start {
619            start.render_dyn(buf);
620        }
621        for step in &self.steps {
622            step.render(buf);
623        }
624        match &self.tail {
625            Some(Tail::Field(f)) => {
626                buf.push('.');
627                buf.push_str(f);
628            }
629            Some(Tail::All) => buf.push_str(".*"),
630            None => {}
631        }
632    }
633}
634
635// ═══════════════════════════════════════════════════════════════════════════════
636// Func — `name(arg, arg, …)` (record::id, type::string, string::lowercase, …)
637// ═══════════════════════════════════════════════════════════════════════════════
638
639/// A SurrealQL function call `name(args…)`.
640#[derive(Debug)]
641pub struct Func {
642    name: &'static str,
643    args: Vec<Box<dyn DynExpr>>,
644}
645
646impl Func {
647    pub fn new(name: &'static str, args: Vec<Box<dyn DynExpr>>) -> Self {
648        Self { name, args }
649    }
650    /// Single-argument function over a bare column/identifier, e.g.
651    /// `record::id(id)` → `Func::of("record::id", "id")`.
652    pub fn of(name: &'static str, ident: &'static str) -> Self {
653        Self {
654            name,
655            args: vec![Box::new(Raw(ident.to_string()))],
656        }
657    }
658}
659
660impl DynExpr for Func {
661    fn render_dyn(&self, buf: &mut String) {
662        buf.push_str(self.name);
663        buf.push('(');
664        for (i, a) in self.args.iter().enumerate() {
665            if i > 0 {
666                buf.push_str(", ");
667            }
668            a.render_dyn(buf);
669        }
670        buf.push(')');
671    }
672}
673
674// ═══════════════════════════════════════════════════════════════════════════════
675// Binary expression nodes
676// ═══════════════════════════════════════════════════════════════════════════════
677
678macro_rules! binop {
679    ($name:ident, $op:literal) => {
680        #[derive(Debug)]
681        pub struct $name {
682            pub(crate) left: Box<dyn DynExpr>,
683            pub(crate) right: Box<dyn DynExpr>,
684        }
685        impl DynExpr for $name {
686            fn render_dyn(&self, buf: &mut String) {
687                self.left.render_dyn(buf);
688                buf.push(' ');
689                buf.push_str($op);
690                buf.push(' ');
691                self.right.render_dyn(buf);
692            }
693        }
694    };
695}
696
697binop!(EqExpr, "=");
698binop!(NeExpr, "!=");
699binop!(GtExpr, ">");
700binop!(LtExpr, "<");
701binop!(GteExpr, ">=");
702binop!(LteExpr, "<=");
703binop!(AndExpr, "AND");
704binop!(OrExpr, "OR");
705
706#[derive(Debug)]
707pub struct NotExpr {
708    pub(crate) inner: Box<dyn DynExpr>,
709}
710
711impl DynExpr for NotExpr {
712    fn render_dyn(&self, buf: &mut String) {
713        buf.push_str("NOT ");
714        self.inner.render_dyn(buf);
715    }
716}
717
718#[derive(Debug)]
719pub struct ContainsExpr {
720    pub(crate) haystack: Box<dyn DynExpr>,
721    pub(crate) needle: Box<dyn DynExpr>,
722}
723
724impl DynExpr for ContainsExpr {
725    fn render_dyn(&self, buf: &mut String) {
726        self.haystack.render_dyn(buf);
727        buf.push_str(" CONTAINS ");
728        self.needle.render_dyn(buf);
729    }
730}
731
732// ═══════════════════════════════════════════════════════════════════════════════
733// Column operator methods (Diesel-style: asset.name.eq("foo"))
734// ═══════════════════════════════════════════════════════════════════════════════
735
736impl<T: SurrealRecord, V: SurrealQL> Column<T, V> {
737    pub fn eq(&self, value: V) -> EqExpr {
738        EqExpr {
739            left: self.dyn_box(),
740            right: Box::new(Literal(value)),
741        }
742    }
743    pub fn ne(&self, value: V) -> NeExpr {
744        NeExpr {
745            left: self.dyn_box(),
746            right: Box::new(Literal(value)),
747        }
748    }
749    pub fn gt(&self, value: V) -> GtExpr {
750        GtExpr {
751            left: self.dyn_box(),
752            right: Box::new(Literal(value)),
753        }
754    }
755    pub fn lt(&self, value: V) -> LtExpr {
756        LtExpr {
757            left: self.dyn_box(),
758            right: Box::new(Literal(value)),
759        }
760    }
761    pub fn gte(&self, value: V) -> GteExpr {
762        GteExpr {
763            left: self.dyn_box(),
764            right: Box::new(Literal(value)),
765        }
766    }
767    pub fn lte(&self, value: V) -> LteExpr {
768        LteExpr {
769            left: self.dyn_box(),
770            right: Box::new(Literal(value)),
771        }
772    }
773    pub fn contains(&self, value: V) -> ContainsExpr {
774        ContainsExpr {
775            haystack: self.dyn_box(),
776            needle: Box::new(Literal(value)),
777        }
778    }
779
780    /// Compare this column to an arbitrary expression — e.g. a [`RecordLink`]
781    /// (`asset = type::record('asset', …)`) or [`NoneLit`] (`tenant = NONE`).
782    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
783        EqExpr {
784            left: self.dyn_box(),
785            right: Box::new(rhs),
786        }
787    }
788    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
789        NeExpr {
790            left: self.dyn_box(),
791            right: Box::new(rhs),
792        }
793    }
794    /// `column IS NONE`.
795    pub fn is_none(&self) -> Raw {
796        Raw(format!("{} IS NONE", self.name))
797    }
798
799    fn dyn_box(&self) -> Box<dyn DynExpr> {
800        Box::new(Self {
801            name: self.name,
802            surreal_type: self.surreal_type,
803            _marker: self._marker,
804        })
805    }
806}
807
808// Combinators: (a = 1).and(b = 2).or(c = 3). Available on every expression node.
809macro_rules! combinators {
810    ($($t:ty),* $(,)?) => {$(
811        impl $t {
812            pub fn and(self, other: impl DynExpr + 'static) -> AndExpr {
813                AndExpr { left: Box::new(self), right: Box::new(other) }
814            }
815            pub fn or(self, other: impl DynExpr + 'static) -> OrExpr {
816                OrExpr { left: Box::new(self), right: Box::new(other) }
817            }
818        }
819    )*};
820}
821combinators!(
822    EqExpr,
823    NeExpr,
824    GtExpr,
825    LtExpr,
826    GteExpr,
827    LteExpr,
828    AndExpr,
829    OrExpr,
830    ContainsExpr,
831    NotExpr,
832    Raw
833);
834
835/// Wraps an expression in parentheses: `(<expr>)`. Use to force grouping/precedence.
836#[derive(Debug)]
837pub struct Grouped(pub Box<dyn DynExpr>);
838
839impl Grouped {
840    pub fn new(inner: impl DynExpr + 'static) -> Self {
841        Self(Box::new(inner))
842    }
843}
844
845impl DynExpr for Grouped {
846    fn render_dyn(&self, buf: &mut String) {
847        buf.push('(');
848        self.0.render_dyn(buf);
849        buf.push(')');
850    }
851}
852
853combinators!(Grouped, Func, Path);
854
855// ═══════════════════════════════════════════════════════════════════════════════
856// Projection — a SELECT field, optionally `<expr> AS alias`
857// ═══════════════════════════════════════════════════════════════════════════════
858
859/// A single SELECT-list entry. Either a bare expression or `<expr> AS <alias>`.
860#[derive(Debug)]
861pub struct Projection {
862    expr: Box<dyn DynExpr>,
863    alias: Option<&'static str>,
864}
865
866impl Projection {
867    /// A bare field/expression with no alias.
868    pub fn new(expr: impl DynExpr + 'static) -> Self {
869        Self {
870            expr: Box::new(expr),
871            alias: None,
872        }
873    }
874    /// `<expr> AS <alias>`.
875    pub fn aliased(expr: impl DynExpr + 'static, alias: &'static str) -> Self {
876        Self {
877            expr: Box::new(expr),
878            alias: Some(alias),
879        }
880    }
881    pub fn render(&self, buf: &mut String) {
882        self.expr.render_dyn(buf);
883        if let Some(a) = self.alias {
884            buf.push_str(" AS ");
885            buf.push_str(a);
886        }
887    }
888}
889
890/// A bare column name as a projection: `name`.
891pub fn col(name: &'static str) -> Projection {
892    Projection::new(Raw(name.to_string()))
893}
894
895/// `<raw> AS <alias>` — verbatim expression with an alias.
896pub fn field(raw: &'static str, alias: &'static str) -> Projection {
897    Projection::aliased(Raw(raw.to_string()), alias)
898}
899
900// ═══════════════════════════════════════════════════════════════════════════════
901// ColumnSet — `*` selector generated by derive macro
902// ═══════════════════════════════════════════════════════════════════════════════
903
904#[derive(Debug, Clone)]
905pub struct ColumnMeta {
906    pub name: &'static str,
907    pub surreal_type: &'static str,
908}
909
910/// Select-all (`*`) column list. Generated by `#[derive(SurrealRecord)]`.
911pub struct ColumnSet<T: SurrealRecord> {
912    pub cols: &'static [ColumnMeta],
913    pub _marker: std::marker::PhantomData<T>,
914}
915
916impl<T: SurrealRecord> std::fmt::Debug for ColumnSet<T> {
917    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
918        f.debug_struct("ColumnSet")
919            .field("cols", &self.cols)
920            .finish()
921    }
922}
923
924impl<T: SurrealRecord> DynExpr for ColumnSet<T> {
925    fn render_dyn(&self, buf: &mut String) {
926        buf.push('*');
927    }
928}
929
930// ═══════════════════════════════════════════════════════════════════════════════
931// ORDER BY
932// ═══════════════════════════════════════════════════════════════════════════════
933
934/// Sort direction for `ORDER BY`.
935#[derive(Debug, Clone, Copy)]
936pub enum Order {
937    /// Ascending (`ASC`).
938    Asc,
939    /// Descending (`DESC`).
940    Desc,
941}
942
943impl Order {
944    pub fn render_suffix(&self) -> &'static str {
945        match self {
946            Order::Asc => "ASC",
947            Order::Desc => "DESC",
948        }
949    }
950}
951
952impl std::fmt::Display for Order {
953    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
954        f.write_str(self.render_suffix())
955    }
956}