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