Skip to main content

somnia_core/
expr.rs

1use std::fmt;
2
3use crate::types::SurrealRecord;
4
5// ═══════════════════════════════════════════════════════════════════════════════
6// DynExpr — type-erased expression (the internal transport)
7// ═══════════════════════════════════════════════════════════════════════════════
8
9pub trait DynExpr: fmt::Debug + Send + Sync {
10    fn render_dyn(&self, buf: &mut String);
11}
12
13/// A boxed, type-erased expression. Useful for returning a composed filter from
14/// a helper function (e.g. a shared tenant/owner predicate).
15pub type DynExprBox = Box<dyn DynExpr>;
16
17impl DynExpr for Box<dyn DynExpr> {
18    fn render_dyn(&self, buf: &mut String) {
19        (**self).render_dyn(buf);
20    }
21}
22
23// ═══════════════════════════════════════════════════════════════════════════════
24// Expr — typed expression (public API)
25// ═══════════════════════════════════════════════════════════════════════════════
26
27pub trait Expr: DynExpr {
28    fn ty_hint(&self) -> &'static str;
29}
30
31// Any DynExpr is automatically an Expr with a default ty_hint
32// (used internally; concrete types override this)
33impl<E: DynExpr> Expr for E {
34    fn ty_hint(&self) -> &'static str {
35        "any"
36    }
37}
38
39// ═══════════════════════════════════════════════════════════════════════════════
40// SurrealQL — literal value rendering
41// ═══════════════════════════════════════════════════════════════════════════════
42
43pub trait SurrealQL: fmt::Debug + Clone + Send + Sync + 'static {
44    fn surreal_type() -> &'static str;
45    fn render_literal(value: &Self, buf: &mut String);
46}
47
48impl SurrealQL for String {
49    fn surreal_type() -> &'static str {
50        "string"
51    }
52    fn render_literal(value: &Self, buf: &mut String) {
53        let escaped = value.replace('\\', "\\\\").replace('\'', "\\'");
54        buf.push('\'');
55        buf.push_str(&escaped);
56        buf.push('\'');
57    }
58}
59
60impl SurrealQL for bool {
61    fn surreal_type() -> &'static str {
62        "bool"
63    }
64    fn render_literal(value: &Self, buf: &mut String) {
65        buf.push_str(if *value { "true" } else { "false" });
66    }
67}
68
69macro_rules! surreal_display {
70    ($t:ty, $name:literal) => {
71        impl SurrealQL for $t {
72            fn surreal_type() -> &'static str {
73                $name
74            }
75            fn render_literal(value: &Self, buf: &mut String) {
76                use std::fmt::Write;
77                let _ = write!(buf, "{value}");
78            }
79        }
80    };
81}
82surreal_display!(i64, "int");
83surreal_display!(i32, "int");
84surreal_display!(i16, "int");
85surreal_display!(i8, "int");
86surreal_display!(f64, "float");
87surreal_display!(f32, "float");
88surreal_display!(u32, "int");
89surreal_display!(u64, "int");
90surreal_display!(u16, "int");
91surreal_display!(u8, "int");
92
93impl SurrealQL for chrono::DateTime<chrono::Utc> {
94    fn surreal_type() -> &'static str {
95        "datetime"
96    }
97    fn render_literal(value: &Self, buf: &mut String) {
98        // SurrealDB 2.0+ requires the `d` prefix on datetime literals. A bare
99        // quoted string is a `string`, not a `datetime`, so `created_at > '…'`
100        // would compare against the wrong type; `created_at > d'…'` is correct.
101        buf.push_str("d'");
102        buf.push_str(&value.to_rfc3339());
103        buf.push('\'');
104    }
105}
106
107impl SurrealQL for uuid::Uuid {
108    fn surreal_type() -> &'static str {
109        "uuid"
110    }
111    fn render_literal(value: &Self, buf: &mut String) {
112        use std::fmt::Write;
113        // SurrealDB 2.0+ requires the `u` prefix on uuid literals; a bare quoted
114        // string is a `string`, not a `uuid`.
115        buf.push_str("u'");
116        let _ = write!(buf, "{value}");
117        buf.push('\'');
118    }
119}
120
121impl SurrealQL for serde_json::Value {
122    fn surreal_type() -> &'static str {
123        "object"
124    }
125    fn render_literal(value: &Self, buf: &mut String) {
126        // JSON is a syntactic subset of SurrealQL value literals (objects,
127        // arrays, numbers, bools, null, double-quoted strings all parse), so
128        // the serialized form is a valid inline literal.
129        use std::fmt::Write;
130        let _ = write!(buf, "{value}");
131    }
132}
133
134// Geometry literals render as GeoJSON objects (a valid SurrealQL object literal),
135// e.g. `{"type":"Point","coordinates":[1.0,2.0]}`.
136macro_rules! geometry_surrealql {
137    ($t:ident, $name:literal) => {
138        impl SurrealQL for crate::types::$t {
139            fn surreal_type() -> &'static str {
140                $name
141            }
142            fn render_literal(value: &Self, buf: &mut String) {
143                if let Ok(s) = serde_json::to_string(value) {
144                    buf.push_str(&s);
145                }
146            }
147        }
148    };
149}
150geometry_surrealql!(Point, "geometry<point>");
151geometry_surrealql!(LineString, "geometry<line>");
152geometry_surrealql!(Polygon, "geometry<polygon>");
153
154impl<T: SurrealQL> SurrealQL for Option<T> {
155    fn surreal_type() -> &'static str {
156        T::surreal_type()
157    }
158    fn render_literal(value: &Self, buf: &mut String) {
159        match value {
160            Some(v) => T::render_literal(v, buf),
161            None => buf.push_str("NONE"),
162        }
163    }
164}
165
166impl<T: crate::types::SurrealRecord> SurrealQL for crate::types::Thing<T> {
167    fn surreal_type() -> &'static str {
168        "record"
169    }
170    fn render_literal(value: &Self, buf: &mut String) {
171        buf.push_str(T::table_name());
172        buf.push(':');
173        value.key.render_id(buf);
174    }
175}
176
177// ═══════════════════════════════════════════════════════════════════════════════
178// Literal — wraps a SurrealQL value as an expression
179// ═══════════════════════════════════════════════════════════════════════════════
180
181#[derive(Debug, Clone)]
182pub struct Literal<V: SurrealQL>(pub V);
183
184impl<V: SurrealQL> DynExpr for Literal<V> {
185    fn render_dyn(&self, buf: &mut String) {
186        V::render_literal(&self.0, buf);
187    }
188}
189
190// ═══════════════════════════════════════════════════════════════════════════════
191// Column — typed field reference (e.g., `asset.name`)
192// ═══════════════════════════════════════════════════════════════════════════════
193
194pub struct Column<T: SurrealRecord, V: SurrealQL> {
195    pub name: &'static str,
196    pub surreal_type: &'static str,
197    pub _marker: std::marker::PhantomData<(T, V)>,
198}
199
200impl<T: SurrealRecord, V: SurrealQL> std::fmt::Debug for Column<T, V> {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.debug_struct("Column").field("name", &self.name).finish()
203    }
204}
205
206impl<T: SurrealRecord, V: SurrealQL> Clone for Column<T, V> {
207    fn clone(&self) -> Self {
208        *self
209    }
210}
211impl<T: SurrealRecord, V: SurrealQL> Copy for Column<T, V> {}
212
213impl<T: SurrealRecord, V: SurrealQL> DynExpr for Column<T, V> {
214    fn render_dyn(&self, buf: &mut String) {
215        buf.push_str(self.name);
216    }
217}
218
219// ═══════════════════════════════════════════════════════════════════════════════
220// Ident — an untyped column/field reference usable in WHERE clauses
221// ═══════════════════════════════════════════════════════════════════════════════
222
223/// An untyped identifier (column or field path) for building filter expressions
224/// where a typed [`Column`] accessor is unavailable (e.g. record-link fields the
225/// derive doesn't expose, or `tenant.slug` paths). Mirrors `Column`'s operators.
226#[derive(Debug, Clone, Copy)]
227pub struct Ident(pub &'static str);
228
229/// Construct an [`Ident`] for a field name.
230pub fn ident(name: &'static str) -> Ident {
231    Ident(name)
232}
233
234impl Ident {
235    fn dyn_box(&self) -> Box<dyn DynExpr> {
236        Box::new(Raw(self.0.to_string()))
237    }
238
239    pub fn eq<V: SurrealQL>(&self, v: V) -> EqExpr {
240        EqExpr {
241            left: self.dyn_box(),
242            right: Box::new(Literal(v)),
243        }
244    }
245    pub fn ne<V: SurrealQL>(&self, v: V) -> NeExpr {
246        NeExpr {
247            left: self.dyn_box(),
248            right: Box::new(Literal(v)),
249        }
250    }
251    pub fn gt<V: SurrealQL>(&self, v: V) -> GtExpr {
252        GtExpr {
253            left: self.dyn_box(),
254            right: Box::new(Literal(v)),
255        }
256    }
257    pub fn lt<V: SurrealQL>(&self, v: V) -> LtExpr {
258        LtExpr {
259            left: self.dyn_box(),
260            right: Box::new(Literal(v)),
261        }
262    }
263    pub fn gte<V: SurrealQL>(&self, v: V) -> GteExpr {
264        GteExpr {
265            left: self.dyn_box(),
266            right: Box::new(Literal(v)),
267        }
268    }
269    pub fn lte<V: SurrealQL>(&self, v: V) -> LteExpr {
270        LteExpr {
271            left: self.dyn_box(),
272            right: Box::new(Literal(v)),
273        }
274    }
275    pub fn contains<V: SurrealQL>(&self, v: V) -> ContainsExpr {
276        ContainsExpr {
277            haystack: self.dyn_box(),
278            needle: Box::new(Literal(v)),
279        }
280    }
281    /// Compare to an arbitrary expression — e.g. `asset = type::record('asset', …)`.
282    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
283        EqExpr {
284            left: self.dyn_box(),
285            right: Box::new(rhs),
286        }
287    }
288    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
289        NeExpr {
290            left: self.dyn_box(),
291            right: Box::new(rhs),
292        }
293    }
294    /// `field IS NONE`.
295    pub fn is_none(&self) -> Raw {
296        Raw(format!("{} IS NONE", self.0))
297    }
298}
299
300impl DynExpr for Ident {
301    fn render_dyn(&self, buf: &mut String) {
302        buf.push_str(self.0);
303    }
304}
305
306// ═══════════════════════════════════════════════════════════════════════════════
307// Raw — verbatim SurrealQL escape hatch (lambdas, IF/THEN/ELSE, field paths…)
308// ═══════════════════════════════════════════════════════════════════════════════
309
310/// A verbatim SurrealQL fragment. Use for expressions somnia does not model as
311/// typed nodes (e.g. `IF x != NONE THEN … END`, lambdas, `tenant.slug`).
312#[derive(Debug, Clone)]
313pub struct Raw(pub String);
314
315impl Raw {
316    pub fn new(s: impl Into<String>) -> Self {
317        Self(s.into())
318    }
319}
320
321impl DynExpr for Raw {
322    fn render_dyn(&self, buf: &mut String) {
323        buf.push_str(&self.0);
324    }
325}
326
327/// SurrealDB's `NONE`.
328#[derive(Debug, Clone)]
329pub struct NoneLit;
330impl DynExpr for NoneLit {
331    fn render_dyn(&self, buf: &mut String) {
332        buf.push_str("NONE");
333    }
334}
335
336// ═══════════════════════════════════════════════════════════════════════════════
337// RecordLink — `type::record('table', <key>)`
338// ═══════════════════════════════════════════════════════════════════════════════
339
340/// Builds a SurrealDB record link `type::record('table', <key>)`. The key is any
341/// literal value (typically the bare UUID/string id of the related row).
342#[derive(Debug)]
343pub struct RecordLink {
344    table: &'static str,
345    key: Box<dyn DynExpr>,
346}
347
348impl RecordLink {
349    /// `type::record('table', '<key literal>')`.
350    pub fn new<V: SurrealQL>(table: &'static str, key: V) -> Self {
351        Self {
352            table,
353            key: Box::new(Literal(key)),
354        }
355    }
356    /// `type::record('table', <key expr>)` — key rendered from an arbitrary expr.
357    pub fn from_expr(table: &'static str, key: impl DynExpr + 'static) -> Self {
358        Self {
359            table,
360            key: Box::new(key),
361        }
362    }
363}
364
365impl DynExpr for RecordLink {
366    fn render_dyn(&self, buf: &mut String) {
367        buf.push_str("type::record('");
368        buf.push_str(self.table);
369        buf.push_str("', ");
370        self.key.render_dyn(buf);
371        buf.push(')');
372    }
373}
374
375// ═══════════════════════════════════════════════════════════════════════════════
376// Func — `name(arg, arg, …)` (record::id, type::string, string::lowercase, …)
377// ═══════════════════════════════════════════════════════════════════════════════
378
379/// A SurrealQL function call `name(args…)`.
380#[derive(Debug)]
381pub struct Func {
382    name: &'static str,
383    args: Vec<Box<dyn DynExpr>>,
384}
385
386impl Func {
387    pub fn new(name: &'static str, args: Vec<Box<dyn DynExpr>>) -> Self {
388        Self { name, args }
389    }
390    /// Single-argument function over a bare column/identifier, e.g.
391    /// `record::id(id)` → `Func::of("record::id", "id")`.
392    pub fn of(name: &'static str, ident: &'static str) -> Self {
393        Self {
394            name,
395            args: vec![Box::new(Raw(ident.to_string()))],
396        }
397    }
398}
399
400impl DynExpr for Func {
401    fn render_dyn(&self, buf: &mut String) {
402        buf.push_str(self.name);
403        buf.push('(');
404        for (i, a) in self.args.iter().enumerate() {
405            if i > 0 {
406                buf.push_str(", ");
407            }
408            a.render_dyn(buf);
409        }
410        buf.push(')');
411    }
412}
413
414// ═══════════════════════════════════════════════════════════════════════════════
415// Binary expression nodes
416// ═══════════════════════════════════════════════════════════════════════════════
417
418macro_rules! binop {
419    ($name:ident, $op:literal) => {
420        #[derive(Debug)]
421        pub struct $name {
422            pub(crate) left: Box<dyn DynExpr>,
423            pub(crate) right: Box<dyn DynExpr>,
424        }
425        impl DynExpr for $name {
426            fn render_dyn(&self, buf: &mut String) {
427                self.left.render_dyn(buf);
428                buf.push(' ');
429                buf.push_str($op);
430                buf.push(' ');
431                self.right.render_dyn(buf);
432            }
433        }
434    };
435}
436
437binop!(EqExpr, "=");
438binop!(NeExpr, "!=");
439binop!(GtExpr, ">");
440binop!(LtExpr, "<");
441binop!(GteExpr, ">=");
442binop!(LteExpr, "<=");
443binop!(AndExpr, "AND");
444binop!(OrExpr, "OR");
445
446#[derive(Debug)]
447pub struct NotExpr {
448    pub(crate) inner: Box<dyn DynExpr>,
449}
450
451impl DynExpr for NotExpr {
452    fn render_dyn(&self, buf: &mut String) {
453        buf.push_str("NOT ");
454        self.inner.render_dyn(buf);
455    }
456}
457
458#[derive(Debug)]
459pub struct ContainsExpr {
460    pub(crate) haystack: Box<dyn DynExpr>,
461    pub(crate) needle: Box<dyn DynExpr>,
462}
463
464impl DynExpr for ContainsExpr {
465    fn render_dyn(&self, buf: &mut String) {
466        self.haystack.render_dyn(buf);
467        buf.push_str(" CONTAINS ");
468        self.needle.render_dyn(buf);
469    }
470}
471
472// ═══════════════════════════════════════════════════════════════════════════════
473// Column operator methods (Diesel-style: asset.name.eq("foo"))
474// ═══════════════════════════════════════════════════════════════════════════════
475
476impl<T: SurrealRecord, V: SurrealQL> Column<T, V> {
477    pub fn eq(&self, value: V) -> EqExpr {
478        EqExpr {
479            left: self.dyn_box(),
480            right: Box::new(Literal(value)),
481        }
482    }
483    pub fn ne(&self, value: V) -> NeExpr {
484        NeExpr {
485            left: self.dyn_box(),
486            right: Box::new(Literal(value)),
487        }
488    }
489    pub fn gt(&self, value: V) -> GtExpr {
490        GtExpr {
491            left: self.dyn_box(),
492            right: Box::new(Literal(value)),
493        }
494    }
495    pub fn lt(&self, value: V) -> LtExpr {
496        LtExpr {
497            left: self.dyn_box(),
498            right: Box::new(Literal(value)),
499        }
500    }
501    pub fn gte(&self, value: V) -> GteExpr {
502        GteExpr {
503            left: self.dyn_box(),
504            right: Box::new(Literal(value)),
505        }
506    }
507    pub fn lte(&self, value: V) -> LteExpr {
508        LteExpr {
509            left: self.dyn_box(),
510            right: Box::new(Literal(value)),
511        }
512    }
513    pub fn contains(&self, value: V) -> ContainsExpr {
514        ContainsExpr {
515            haystack: self.dyn_box(),
516            needle: Box::new(Literal(value)),
517        }
518    }
519
520    /// Compare this column to an arbitrary expression — e.g. a [`RecordLink`]
521    /// (`asset = type::record('asset', …)`) or [`NoneLit`] (`tenant = NONE`).
522    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
523        EqExpr {
524            left: self.dyn_box(),
525            right: Box::new(rhs),
526        }
527    }
528    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
529        NeExpr {
530            left: self.dyn_box(),
531            right: Box::new(rhs),
532        }
533    }
534    /// `column IS NONE`.
535    pub fn is_none(&self) -> Raw {
536        Raw(format!("{} IS NONE", self.name))
537    }
538
539    fn dyn_box(&self) -> Box<dyn DynExpr> {
540        Box::new(Self {
541            name: self.name,
542            surreal_type: self.surreal_type,
543            _marker: self._marker,
544        })
545    }
546}
547
548// Combinators: (a = 1).and(b = 2).or(c = 3). Available on every expression node.
549macro_rules! combinators {
550    ($($t:ty),* $(,)?) => {$(
551        impl $t {
552            pub fn and(self, other: impl DynExpr + 'static) -> AndExpr {
553                AndExpr { left: Box::new(self), right: Box::new(other) }
554            }
555            pub fn or(self, other: impl DynExpr + 'static) -> OrExpr {
556                OrExpr { left: Box::new(self), right: Box::new(other) }
557            }
558        }
559    )*};
560}
561combinators!(
562    EqExpr,
563    NeExpr,
564    GtExpr,
565    LtExpr,
566    GteExpr,
567    LteExpr,
568    AndExpr,
569    OrExpr,
570    ContainsExpr,
571    NotExpr,
572    Raw
573);
574
575/// Wraps an expression in parentheses: `(<expr>)`. Use to force grouping/precedence.
576#[derive(Debug)]
577pub struct Grouped(pub Box<dyn DynExpr>);
578
579impl Grouped {
580    pub fn new(inner: impl DynExpr + 'static) -> Self {
581        Self(Box::new(inner))
582    }
583}
584
585impl DynExpr for Grouped {
586    fn render_dyn(&self, buf: &mut String) {
587        buf.push('(');
588        self.0.render_dyn(buf);
589        buf.push(')');
590    }
591}
592
593combinators!(Grouped, Func);
594
595// ═══════════════════════════════════════════════════════════════════════════════
596// Projection — a SELECT field, optionally `<expr> AS alias`
597// ═══════════════════════════════════════════════════════════════════════════════
598
599/// A single SELECT-list entry. Either a bare expression or `<expr> AS <alias>`.
600#[derive(Debug)]
601pub struct Projection {
602    expr: Box<dyn DynExpr>,
603    alias: Option<&'static str>,
604}
605
606impl Projection {
607    /// A bare field/expression with no alias.
608    pub fn new(expr: impl DynExpr + 'static) -> Self {
609        Self {
610            expr: Box::new(expr),
611            alias: None,
612        }
613    }
614    /// `<expr> AS <alias>`.
615    pub fn aliased(expr: impl DynExpr + 'static, alias: &'static str) -> Self {
616        Self {
617            expr: Box::new(expr),
618            alias: Some(alias),
619        }
620    }
621    pub fn render(&self, buf: &mut String) {
622        self.expr.render_dyn(buf);
623        if let Some(a) = self.alias {
624            buf.push_str(" AS ");
625            buf.push_str(a);
626        }
627    }
628}
629
630/// A bare column name as a projection: `name`.
631pub fn col(name: &'static str) -> Projection {
632    Projection::new(Raw(name.to_string()))
633}
634
635/// `<raw> AS <alias>` — verbatim expression with an alias.
636pub fn field(raw: &'static str, alias: &'static str) -> Projection {
637    Projection::aliased(Raw(raw.to_string()), alias)
638}
639
640// ═══════════════════════════════════════════════════════════════════════════════
641// ColumnSet — `*` selector generated by derive macro
642// ═══════════════════════════════════════════════════════════════════════════════
643
644#[derive(Debug, Clone)]
645pub struct ColumnMeta {
646    pub name: &'static str,
647    pub surreal_type: &'static str,
648}
649
650/// Select-all (`*`) column list. Generated by `#[derive(SurrealRecord)]`.
651pub struct ColumnSet<T: SurrealRecord> {
652    pub cols: &'static [ColumnMeta],
653    pub _marker: std::marker::PhantomData<T>,
654}
655
656impl<T: SurrealRecord> std::fmt::Debug for ColumnSet<T> {
657    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
658        f.debug_struct("ColumnSet")
659            .field("cols", &self.cols)
660            .finish()
661    }
662}
663
664impl<T: SurrealRecord> DynExpr for ColumnSet<T> {
665    fn render_dyn(&self, buf: &mut String) {
666        buf.push('*');
667    }
668}
669
670// ═══════════════════════════════════════════════════════════════════════════════
671// ORDER BY
672// ═══════════════════════════════════════════════════════════════════════════════
673
674#[derive(Debug, Clone, Copy)]
675pub enum Order {
676    Asc,
677    Desc,
678}
679
680impl Order {
681    pub fn render_suffix(&self) -> &'static str {
682        match self {
683            Order::Asc => "ASC",
684            Order::Desc => "DESC",
685        }
686    }
687}
688
689impl std::fmt::Display for Order {
690    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691        f.write_str(self.render_suffix())
692    }
693}