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