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
134impl<T: SurrealQL> SurrealQL for Option<T> {
135    fn surreal_type() -> &'static str {
136        T::surreal_type()
137    }
138    fn render_literal(value: &Self, buf: &mut String) {
139        match value {
140            Some(v) => T::render_literal(v, buf),
141            None => buf.push_str("NONE"),
142        }
143    }
144}
145
146impl<T: crate::types::SurrealRecord> SurrealQL for crate::types::Thing<T> {
147    fn surreal_type() -> &'static str {
148        "record"
149    }
150    fn render_literal(value: &Self, buf: &mut String) {
151        use std::fmt::Write;
152        let _ = write!(buf, "{}:{}", T::table_name(), value.key);
153    }
154}
155
156// ═══════════════════════════════════════════════════════════════════════════════
157// Literal — wraps a SurrealQL value as an expression
158// ═══════════════════════════════════════════════════════════════════════════════
159
160#[derive(Debug, Clone)]
161pub struct Literal<V: SurrealQL>(pub V);
162
163impl<V: SurrealQL> DynExpr for Literal<V> {
164    fn render_dyn(&self, buf: &mut String) {
165        V::render_literal(&self.0, buf);
166    }
167}
168
169// ═══════════════════════════════════════════════════════════════════════════════
170// Column — typed field reference (e.g., `asset.name`)
171// ═══════════════════════════════════════════════════════════════════════════════
172
173pub struct Column<T: SurrealRecord, V: SurrealQL> {
174    pub name: &'static str,
175    pub surreal_type: &'static str,
176    pub _marker: std::marker::PhantomData<(T, V)>,
177}
178
179impl<T: SurrealRecord, V: SurrealQL> std::fmt::Debug for Column<T, V> {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        f.debug_struct("Column").field("name", &self.name).finish()
182    }
183}
184
185impl<T: SurrealRecord, V: SurrealQL> Clone for Column<T, V> {
186    fn clone(&self) -> Self {
187        *self
188    }
189}
190impl<T: SurrealRecord, V: SurrealQL> Copy for Column<T, V> {}
191
192impl<T: SurrealRecord, V: SurrealQL> DynExpr for Column<T, V> {
193    fn render_dyn(&self, buf: &mut String) {
194        buf.push_str(self.name);
195    }
196}
197
198// ═══════════════════════════════════════════════════════════════════════════════
199// Ident — an untyped column/field reference usable in WHERE clauses
200// ═══════════════════════════════════════════════════════════════════════════════
201
202/// An untyped identifier (column or field path) for building filter expressions
203/// where a typed [`Column`] accessor is unavailable (e.g. record-link fields the
204/// derive doesn't expose, or `tenant.slug` paths). Mirrors `Column`'s operators.
205#[derive(Debug, Clone, Copy)]
206pub struct Ident(pub &'static str);
207
208/// Construct an [`Ident`] for a field name.
209pub fn ident(name: &'static str) -> Ident {
210    Ident(name)
211}
212
213impl Ident {
214    fn dyn_box(&self) -> Box<dyn DynExpr> {
215        Box::new(Raw(self.0.to_string()))
216    }
217
218    pub fn eq<V: SurrealQL>(&self, v: V) -> EqExpr {
219        EqExpr {
220            left: self.dyn_box(),
221            right: Box::new(Literal(v)),
222        }
223    }
224    pub fn ne<V: SurrealQL>(&self, v: V) -> NeExpr {
225        NeExpr {
226            left: self.dyn_box(),
227            right: Box::new(Literal(v)),
228        }
229    }
230    pub fn gt<V: SurrealQL>(&self, v: V) -> GtExpr {
231        GtExpr {
232            left: self.dyn_box(),
233            right: Box::new(Literal(v)),
234        }
235    }
236    pub fn lt<V: SurrealQL>(&self, v: V) -> LtExpr {
237        LtExpr {
238            left: self.dyn_box(),
239            right: Box::new(Literal(v)),
240        }
241    }
242    pub fn gte<V: SurrealQL>(&self, v: V) -> GteExpr {
243        GteExpr {
244            left: self.dyn_box(),
245            right: Box::new(Literal(v)),
246        }
247    }
248    pub fn lte<V: SurrealQL>(&self, v: V) -> LteExpr {
249        LteExpr {
250            left: self.dyn_box(),
251            right: Box::new(Literal(v)),
252        }
253    }
254    pub fn contains<V: SurrealQL>(&self, v: V) -> ContainsExpr {
255        ContainsExpr {
256            haystack: self.dyn_box(),
257            needle: Box::new(Literal(v)),
258        }
259    }
260    /// Compare to an arbitrary expression — e.g. `asset = type::record('asset', …)`.
261    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
262        EqExpr {
263            left: self.dyn_box(),
264            right: Box::new(rhs),
265        }
266    }
267    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
268        NeExpr {
269            left: self.dyn_box(),
270            right: Box::new(rhs),
271        }
272    }
273    /// `field IS NONE`.
274    pub fn is_none(&self) -> Raw {
275        Raw(format!("{} IS NONE", self.0))
276    }
277}
278
279impl DynExpr for Ident {
280    fn render_dyn(&self, buf: &mut String) {
281        buf.push_str(self.0);
282    }
283}
284
285// ═══════════════════════════════════════════════════════════════════════════════
286// Raw — verbatim SurrealQL escape hatch (lambdas, IF/THEN/ELSE, field paths…)
287// ═══════════════════════════════════════════════════════════════════════════════
288
289/// A verbatim SurrealQL fragment. Use for expressions somnia does not model as
290/// typed nodes (e.g. `IF x != NONE THEN … END`, lambdas, `tenant.slug`).
291#[derive(Debug, Clone)]
292pub struct Raw(pub String);
293
294impl Raw {
295    pub fn new(s: impl Into<String>) -> Self {
296        Self(s.into())
297    }
298}
299
300impl DynExpr for Raw {
301    fn render_dyn(&self, buf: &mut String) {
302        buf.push_str(&self.0);
303    }
304}
305
306/// SurrealDB's `NONE`.
307#[derive(Debug, Clone)]
308pub struct NoneLit;
309impl DynExpr for NoneLit {
310    fn render_dyn(&self, buf: &mut String) {
311        buf.push_str("NONE");
312    }
313}
314
315// ═══════════════════════════════════════════════════════════════════════════════
316// RecordLink — `type::record('table', <key>)`
317// ═══════════════════════════════════════════════════════════════════════════════
318
319/// Builds a SurrealDB record link `type::record('table', <key>)`. The key is any
320/// literal value (typically the bare UUID/string id of the related row).
321#[derive(Debug)]
322pub struct RecordLink {
323    table: &'static str,
324    key: Box<dyn DynExpr>,
325}
326
327impl RecordLink {
328    /// `type::record('table', '<key literal>')`.
329    pub fn new<V: SurrealQL>(table: &'static str, key: V) -> Self {
330        Self {
331            table,
332            key: Box::new(Literal(key)),
333        }
334    }
335    /// `type::record('table', <key expr>)` — key rendered from an arbitrary expr.
336    pub fn from_expr(table: &'static str, key: impl DynExpr + 'static) -> Self {
337        Self {
338            table,
339            key: Box::new(key),
340        }
341    }
342}
343
344impl DynExpr for RecordLink {
345    fn render_dyn(&self, buf: &mut String) {
346        buf.push_str("type::record('");
347        buf.push_str(self.table);
348        buf.push_str("', ");
349        self.key.render_dyn(buf);
350        buf.push(')');
351    }
352}
353
354// ═══════════════════════════════════════════════════════════════════════════════
355// Func — `name(arg, arg, …)` (record::id, type::string, string::lowercase, …)
356// ═══════════════════════════════════════════════════════════════════════════════
357
358/// A SurrealQL function call `name(args…)`.
359#[derive(Debug)]
360pub struct Func {
361    name: &'static str,
362    args: Vec<Box<dyn DynExpr>>,
363}
364
365impl Func {
366    pub fn new(name: &'static str, args: Vec<Box<dyn DynExpr>>) -> Self {
367        Self { name, args }
368    }
369    /// Single-argument function over a bare column/identifier, e.g.
370    /// `record::id(id)` → `Func::of("record::id", "id")`.
371    pub fn of(name: &'static str, ident: &'static str) -> Self {
372        Self {
373            name,
374            args: vec![Box::new(Raw(ident.to_string()))],
375        }
376    }
377}
378
379impl DynExpr for Func {
380    fn render_dyn(&self, buf: &mut String) {
381        buf.push_str(self.name);
382        buf.push('(');
383        for (i, a) in self.args.iter().enumerate() {
384            if i > 0 {
385                buf.push_str(", ");
386            }
387            a.render_dyn(buf);
388        }
389        buf.push(')');
390    }
391}
392
393// ═══════════════════════════════════════════════════════════════════════════════
394// Binary expression nodes
395// ═══════════════════════════════════════════════════════════════════════════════
396
397macro_rules! binop {
398    ($name:ident, $op:literal) => {
399        #[derive(Debug)]
400        pub struct $name {
401            pub(crate) left: Box<dyn DynExpr>,
402            pub(crate) right: Box<dyn DynExpr>,
403        }
404        impl DynExpr for $name {
405            fn render_dyn(&self, buf: &mut String) {
406                self.left.render_dyn(buf);
407                buf.push(' ');
408                buf.push_str($op);
409                buf.push(' ');
410                self.right.render_dyn(buf);
411            }
412        }
413    };
414}
415
416binop!(EqExpr, "=");
417binop!(NeExpr, "!=");
418binop!(GtExpr, ">");
419binop!(LtExpr, "<");
420binop!(GteExpr, ">=");
421binop!(LteExpr, "<=");
422binop!(AndExpr, "AND");
423binop!(OrExpr, "OR");
424
425#[derive(Debug)]
426pub struct NotExpr {
427    pub(crate) inner: Box<dyn DynExpr>,
428}
429
430impl DynExpr for NotExpr {
431    fn render_dyn(&self, buf: &mut String) {
432        buf.push_str("NOT ");
433        self.inner.render_dyn(buf);
434    }
435}
436
437#[derive(Debug)]
438pub struct ContainsExpr {
439    pub(crate) haystack: Box<dyn DynExpr>,
440    pub(crate) needle: Box<dyn DynExpr>,
441}
442
443impl DynExpr for ContainsExpr {
444    fn render_dyn(&self, buf: &mut String) {
445        self.haystack.render_dyn(buf);
446        buf.push_str(" CONTAINS ");
447        self.needle.render_dyn(buf);
448    }
449}
450
451// ═══════════════════════════════════════════════════════════════════════════════
452// Column operator methods (Diesel-style: asset.name.eq("foo"))
453// ═══════════════════════════════════════════════════════════════════════════════
454
455impl<T: SurrealRecord, V: SurrealQL> Column<T, V> {
456    pub fn eq(&self, value: V) -> EqExpr {
457        EqExpr {
458            left: self.dyn_box(),
459            right: Box::new(Literal(value)),
460        }
461    }
462    pub fn ne(&self, value: V) -> NeExpr {
463        NeExpr {
464            left: self.dyn_box(),
465            right: Box::new(Literal(value)),
466        }
467    }
468    pub fn gt(&self, value: V) -> GtExpr {
469        GtExpr {
470            left: self.dyn_box(),
471            right: Box::new(Literal(value)),
472        }
473    }
474    pub fn lt(&self, value: V) -> LtExpr {
475        LtExpr {
476            left: self.dyn_box(),
477            right: Box::new(Literal(value)),
478        }
479    }
480    pub fn gte(&self, value: V) -> GteExpr {
481        GteExpr {
482            left: self.dyn_box(),
483            right: Box::new(Literal(value)),
484        }
485    }
486    pub fn lte(&self, value: V) -> LteExpr {
487        LteExpr {
488            left: self.dyn_box(),
489            right: Box::new(Literal(value)),
490        }
491    }
492    pub fn contains(&self, value: V) -> ContainsExpr {
493        ContainsExpr {
494            haystack: self.dyn_box(),
495            needle: Box::new(Literal(value)),
496        }
497    }
498
499    /// Compare this column to an arbitrary expression — e.g. a [`RecordLink`]
500    /// (`asset = type::record('asset', …)`) or [`NoneLit`] (`tenant = NONE`).
501    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
502        EqExpr {
503            left: self.dyn_box(),
504            right: Box::new(rhs),
505        }
506    }
507    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
508        NeExpr {
509            left: self.dyn_box(),
510            right: Box::new(rhs),
511        }
512    }
513    /// `column IS NONE`.
514    pub fn is_none(&self) -> Raw {
515        Raw(format!("{} IS NONE", self.name))
516    }
517
518    fn dyn_box(&self) -> Box<dyn DynExpr> {
519        Box::new(Self {
520            name: self.name,
521            surreal_type: self.surreal_type,
522            _marker: self._marker,
523        })
524    }
525}
526
527// Combinators: (a = 1).and(b = 2).or(c = 3). Available on every expression node.
528macro_rules! combinators {
529    ($($t:ty),* $(,)?) => {$(
530        impl $t {
531            pub fn and(self, other: impl DynExpr + 'static) -> AndExpr {
532                AndExpr { left: Box::new(self), right: Box::new(other) }
533            }
534            pub fn or(self, other: impl DynExpr + 'static) -> OrExpr {
535                OrExpr { left: Box::new(self), right: Box::new(other) }
536            }
537        }
538    )*};
539}
540combinators!(
541    EqExpr,
542    NeExpr,
543    GtExpr,
544    LtExpr,
545    GteExpr,
546    LteExpr,
547    AndExpr,
548    OrExpr,
549    ContainsExpr,
550    NotExpr,
551    Raw
552);
553
554/// Wraps an expression in parentheses: `(<expr>)`. Use to force grouping/precedence.
555#[derive(Debug)]
556pub struct Grouped(pub Box<dyn DynExpr>);
557
558impl Grouped {
559    pub fn new(inner: impl DynExpr + 'static) -> Self {
560        Self(Box::new(inner))
561    }
562}
563
564impl DynExpr for Grouped {
565    fn render_dyn(&self, buf: &mut String) {
566        buf.push('(');
567        self.0.render_dyn(buf);
568        buf.push(')');
569    }
570}
571
572combinators!(Grouped, Func);
573
574// ═══════════════════════════════════════════════════════════════════════════════
575// Projection — a SELECT field, optionally `<expr> AS alias`
576// ═══════════════════════════════════════════════════════════════════════════════
577
578/// A single SELECT-list entry. Either a bare expression or `<expr> AS <alias>`.
579#[derive(Debug)]
580pub struct Projection {
581    expr: Box<dyn DynExpr>,
582    alias: Option<&'static str>,
583}
584
585impl Projection {
586    /// A bare field/expression with no alias.
587    pub fn new(expr: impl DynExpr + 'static) -> Self {
588        Self {
589            expr: Box::new(expr),
590            alias: None,
591        }
592    }
593    /// `<expr> AS <alias>`.
594    pub fn aliased(expr: impl DynExpr + 'static, alias: &'static str) -> Self {
595        Self {
596            expr: Box::new(expr),
597            alias: Some(alias),
598        }
599    }
600    pub fn render(&self, buf: &mut String) {
601        self.expr.render_dyn(buf);
602        if let Some(a) = self.alias {
603            buf.push_str(" AS ");
604            buf.push_str(a);
605        }
606    }
607}
608
609/// A bare column name as a projection: `name`.
610pub fn col(name: &'static str) -> Projection {
611    Projection::new(Raw(name.to_string()))
612}
613
614/// `<raw> AS <alias>` — verbatim expression with an alias.
615pub fn field(raw: &'static str, alias: &'static str) -> Projection {
616    Projection::aliased(Raw(raw.to_string()), alias)
617}
618
619// ═══════════════════════════════════════════════════════════════════════════════
620// ColumnSet — `*` selector generated by derive macro
621// ═══════════════════════════════════════════════════════════════════════════════
622
623#[derive(Debug, Clone)]
624pub struct ColumnMeta {
625    pub name: &'static str,
626    pub surreal_type: &'static str,
627}
628
629/// Select-all (`*`) column list. Generated by `#[derive(SurrealRecord)]`.
630pub struct ColumnSet<T: SurrealRecord> {
631    pub cols: &'static [ColumnMeta],
632    pub _marker: std::marker::PhantomData<T>,
633}
634
635impl<T: SurrealRecord> std::fmt::Debug for ColumnSet<T> {
636    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637        f.debug_struct("ColumnSet")
638            .field("cols", &self.cols)
639            .finish()
640    }
641}
642
643impl<T: SurrealRecord> DynExpr for ColumnSet<T> {
644    fn render_dyn(&self, buf: &mut String) {
645        buf.push('*');
646    }
647}
648
649// ═══════════════════════════════════════════════════════════════════════════════
650// ORDER BY
651// ═══════════════════════════════════════════════════════════════════════════════
652
653#[derive(Debug, Clone, Copy)]
654pub enum Order {
655    Asc,
656    Desc,
657}
658
659impl Order {
660    pub fn render_suffix(&self) -> &'static str {
661        match self {
662            Order::Asc => "ASC",
663            Order::Desc => "DESC",
664        }
665    }
666}
667
668impl std::fmt::Display for Order {
669    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670        f.write_str(self.render_suffix())
671    }
672}