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::collections::BTreeMap;
10use std::fmt;
11
12use crate::types::SurrealRecord;
13
14// ═══════════════════════════════════════════════════════════════════════════════
15// DynExpr — type-erased expression (the internal transport)
16// ═══════════════════════════════════════════════════════════════════════════════
17
18/// A type-erased expression that can render itself into a SurrealQL buffer.
19/// Implemented by every expression node; the common currency of the builder.
20pub trait DynExpr: fmt::Debug + Send + Sync {
21    /// Append this expression's SurrealQL to `buf` with all values inlined.
22    fn render_dyn(&self, buf: &mut String);
23
24    /// Render with `$param` placeholders, collecting values into `params`.
25    /// Default falls back to inline rendering; [`Literal`] nodes override this
26    /// to emit `$pN` placeholders and collect their value.
27    fn render_dyn_params(
28        &self,
29        buf: &mut String,
30        _params: &mut BTreeMap<String, serde_json::Value>,
31    ) {
32        self.render_dyn(buf);
33    }
34}
35
36/// A boxed, type-erased expression. Useful for returning a composed filter from
37/// a helper function (e.g. a shared tenant/owner predicate).
38pub type DynExprBox = Box<dyn DynExpr>;
39
40impl DynExpr for Box<dyn DynExpr> {
41    fn render_dyn(&self, buf: &mut String) {
42        (**self).render_dyn(buf);
43    }
44    fn render_dyn_params(
45        &self,
46        buf: &mut String,
47        params: &mut BTreeMap<String, serde_json::Value>,
48    ) {
49        (**self).render_dyn_params(buf, params);
50    }
51}
52
53// ═══════════════════════════════════════════════════════════════════════════════
54// Expr — typed expression (public API)
55// ═══════════════════════════════════════════════════════════════════════════════
56
57/// A [`DynExpr`] that also reports a SurrealQL type hint. Auto-implemented for
58/// every `DynExpr` (returning `"any"`); concrete nodes may override the hint.
59pub trait Expr: DynExpr {
60    /// A best-effort SurrealQL type name for this expression.
61    fn ty_hint(&self) -> &'static str;
62}
63
64// Any DynExpr is automatically an Expr with a default ty_hint
65// (used internally; concrete types override this)
66impl<E: DynExpr> Expr for E {
67    fn ty_hint(&self) -> &'static str {
68        "any"
69    }
70}
71
72// ═══════════════════════════════════════════════════════════════════════════════
73// SurrealQL — literal value rendering
74// ═══════════════════════════════════════════════════════════════════════════════
75
76/// A Rust type that can be rendered as a SurrealQL literal (the right-hand side of
77/// comparisons, `SET` values, etc.). Implemented for the common scalar types,
78/// `Option<T>`, `serde_json::Value`, the geometry types, and `Thing<T>`.
79pub trait SurrealQL: fmt::Debug + Clone + Send + Sync + 'static {
80    /// The SurrealQL type name (e.g. `"string"`, `"datetime"`).
81    fn surreal_type() -> &'static str;
82    /// Append the literal form of `value` to `buf` (with any needed quoting/escaping).
83    fn render_literal(value: &Self, buf: &mut String);
84    /// Convert `value` to a JSON parameter for `$param` binding. Used by
85    /// [`to_surrealql_with_params`](crate::query::Select::to_surrealql_with_params).
86    fn to_param_value(value: &Self) -> serde_json::Value;
87}
88
89impl SurrealQL for String {
90    fn surreal_type() -> &'static str {
91        "string"
92    }
93    fn render_literal(value: &Self, buf: &mut String) {
94        let escaped = value.replace('\\', "\\\\").replace('\'', "\\'");
95        buf.push('\'');
96        buf.push_str(&escaped);
97        buf.push('\'');
98    }
99    fn to_param_value(value: &Self) -> serde_json::Value {
100        serde_json::Value::String(value.clone())
101    }
102}
103
104impl SurrealQL for bool {
105    fn surreal_type() -> &'static str {
106        "bool"
107    }
108    fn render_literal(value: &Self, buf: &mut String) {
109        buf.push_str(if *value { "true" } else { "false" });
110    }
111    fn to_param_value(value: &Self) -> serde_json::Value {
112        serde_json::Value::Bool(*value)
113    }
114}
115
116macro_rules! surreal_display {
117    ($t:ty, $name:literal) => {
118        impl SurrealQL for $t {
119            fn surreal_type() -> &'static str {
120                $name
121            }
122            fn render_literal(value: &Self, buf: &mut String) {
123                use std::fmt::Write;
124                let _ = write!(buf, "{value}");
125            }
126            fn to_param_value(value: &Self) -> serde_json::Value {
127                serde_json::to_value(value).unwrap_or(serde_json::Value::Null)
128            }
129        }
130    };
131}
132surreal_display!(i64, "int");
133surreal_display!(i32, "int");
134surreal_display!(i16, "int");
135surreal_display!(i8, "int");
136surreal_display!(f64, "float");
137surreal_display!(f32, "float");
138surreal_display!(u32, "int");
139surreal_display!(u64, "int");
140surreal_display!(u16, "int");
141surreal_display!(u8, "int");
142
143impl SurrealQL for chrono::DateTime<chrono::Utc> {
144    fn surreal_type() -> &'static str {
145        "datetime"
146    }
147    fn render_literal(value: &Self, buf: &mut String) {
148        // SurrealDB 2.0+ requires the `d` prefix on datetime literals. A bare
149        // quoted string is a `string`, not a `datetime`, so `created_at > '…'`
150        // would compare against the wrong type; `created_at > d'…'` is correct.
151        buf.push_str("d'");
152        buf.push_str(&value.to_rfc3339());
153        buf.push('\'');
154    }
155    fn to_param_value(value: &Self) -> serde_json::Value {
156        serde_json::Value::String(value.to_rfc3339())
157    }
158}
159
160impl SurrealQL for uuid::Uuid {
161    fn surreal_type() -> &'static str {
162        "uuid"
163    }
164    fn render_literal(value: &Self, buf: &mut String) {
165        use std::fmt::Write;
166        // SurrealDB 2.0+ requires the `u` prefix on uuid literals; a bare quoted
167        // string is a `string`, not a `uuid`.
168        buf.push_str("u'");
169        let _ = write!(buf, "{value}");
170        buf.push('\'');
171    }
172    fn to_param_value(value: &Self) -> serde_json::Value {
173        serde_json::Value::String(value.to_string())
174    }
175}
176
177impl SurrealQL for serde_json::Value {
178    fn surreal_type() -> &'static str {
179        "object"
180    }
181    fn render_literal(value: &Self, buf: &mut String) {
182        // JSON is a syntactic subset of SurrealQL value literals (objects,
183        // arrays, numbers, bools, null, double-quoted strings all parse), so
184        // the serialized form is a valid inline literal.
185        use std::fmt::Write;
186        let _ = write!(buf, "{value}");
187    }
188    fn to_param_value(value: &Self) -> serde_json::Value {
189        value.clone()
190    }
191}
192
193// Geometry literals render as GeoJSON objects (a valid SurrealQL object literal),
194// e.g. `{"type":"Point","coordinates":[1.0,2.0]}`.
195macro_rules! geometry_surrealql {
196    ($t:ident, $name:literal) => {
197        impl SurrealQL for crate::types::$t {
198            fn surreal_type() -> &'static str {
199                $name
200            }
201            fn render_literal(value: &Self, buf: &mut String) {
202                if let Ok(s) = serde_json::to_string(value) {
203                    buf.push_str(&s);
204                }
205            }
206            fn to_param_value(value: &Self) -> serde_json::Value {
207                serde_json::to_value(value).unwrap_or(serde_json::Value::Null)
208            }
209        }
210    };
211}
212geometry_surrealql!(Point, "geometry<point>");
213geometry_surrealql!(LineString, "geometry<line>");
214geometry_surrealql!(Polygon, "geometry<polygon>");
215
216impl<T: SurrealQL> SurrealQL for Option<T> {
217    fn surreal_type() -> &'static str {
218        T::surreal_type()
219    }
220    fn render_literal(value: &Self, buf: &mut String) {
221        match value {
222            Some(v) => T::render_literal(v, buf),
223            None => buf.push_str("NONE"),
224        }
225    }
226    fn to_param_value(value: &Self) -> serde_json::Value {
227        match value {
228            Some(v) => T::to_param_value(v),
229            None => serde_json::Value::Null,
230        }
231    }
232}
233
234impl<V: SurrealQL> SurrealQL for Vec<V> {
235    fn surreal_type() -> &'static str {
236        // The element type is lost at this level; the derive emits the precise
237        // `array<…>` for `DEFINE FIELD`. This hint is informational only.
238        "array"
239    }
240    fn render_literal(value: &Self, buf: &mut String) {
241        buf.push('[');
242        for (i, v) in value.iter().enumerate() {
243            if i > 0 {
244                buf.push_str(", ");
245            }
246            V::render_literal(v, buf);
247        }
248        buf.push(']');
249    }
250    fn to_param_value(value: &Self) -> serde_json::Value {
251        serde_json::Value::Array(value.iter().map(|v| V::to_param_value(v)).collect())
252    }
253}
254
255impl SurrealQL for std::time::Duration {
256    fn surreal_type() -> &'static str {
257        "duration"
258    }
259    fn render_literal(value: &Self, buf: &mut String) {
260        use std::fmt::Write;
261        // SurrealDB duration literal: a concatenation of unit-tagged components
262        // (e.g. `1s500ms`). Whole seconds + sub-second nanoseconds round-trips any
263        // `Duration` and parses cleanly.
264        let secs = value.as_secs();
265        let nanos = value.subsec_nanos();
266        if secs == 0 && nanos == 0 {
267            buf.push_str("0ns");
268            return;
269        }
270        if secs > 0 {
271            let _ = write!(buf, "{secs}s");
272        }
273        if nanos > 0 {
274            let _ = write!(buf, "{nanos}ns");
275        }
276    }
277    fn to_param_value(value: &Self) -> serde_json::Value {
278        let secs = value.as_secs();
279        let nanos = value.subsec_nanos();
280        if secs == 0 && nanos == 0 {
281            serde_json::Value::String("0ns".into())
282        } else if nanos == 0 {
283            serde_json::Value::String(format!("{secs}s"))
284        } else {
285            serde_json::Value::String(format!("{secs}s{nanos}ns"))
286        }
287    }
288}
289
290impl<T: crate::types::SurrealRecord> SurrealQL for crate::types::Thing<T> {
291    fn surreal_type() -> &'static str {
292        "record"
293    }
294    fn render_literal(value: &Self, buf: &mut String) {
295        buf.push_str(T::table_name());
296        buf.push(':');
297        value.key.render_id(buf);
298    }
299    fn to_param_value(value: &Self) -> serde_json::Value {
300        let mut s = String::new();
301        value.key.render_id(&mut s);
302        serde_json::Value::String(format!("{}:{}", T::table_name(), s))
303    }
304}
305
306// ═══════════════════════════════════════════════════════════════════════════════
307// Literal — wraps a SurrealQL value as an expression
308// ═══════════════════════════════════════════════════════════════════════════════
309
310#[derive(Debug, Clone)]
311pub struct Literal<V: SurrealQL>(pub V);
312
313impl<V: SurrealQL> DynExpr for Literal<V> {
314    fn render_dyn(&self, buf: &mut String) {
315        V::render_literal(&self.0, buf);
316    }
317    fn render_dyn_params(
318        &self,
319        buf: &mut String,
320        params: &mut BTreeMap<String, serde_json::Value>,
321    ) {
322        let name = format!("p{}", params.len());
323        buf.push('$');
324        buf.push_str(&name);
325        params.insert(name, V::to_param_value(&self.0));
326    }
327}
328
329// ═══════════════════════════════════════════════════════════════════════════════
330// Param — explicit named parameter (`$name` placeholder)
331// ═══════════════════════════════════════════════════════════════════════════════
332
333/// A named SurrealQL parameter `$name`. In inline mode renders as a literal;
334/// in param mode (`to_surrealql_with_params`) renders as `$name` and collects
335/// the value. Use when you want to reference the same value in multiple places
336/// or give params meaningful names.
337///
338/// ```ignore
339/// let title = Param::new("search", "hello");
340/// Post::table()
341///     .project(Post::all())
342///     .filter(Post::title().eq(title.clone()).or(Post::body().contains(title)))
343///     .to_surrealql_with_params();
344/// // → ("SELECT * FROM post WHERE title = $search OR body CONTAINS $search",
345/// //    {"search": "hello"})
346/// ```
347#[derive(Debug, Clone)]
348pub struct Param<V: SurrealQL> {
349    name: String,
350    value: V,
351}
352
353impl<V: SurrealQL> Param<V> {
354    /// Create a named parameter placeholder with the given name and value.
355    pub fn new(name: impl Into<String>, value: V) -> Self {
356        Self {
357            name: name.into(),
358            value,
359        }
360    }
361}
362
363impl<V: SurrealQL> DynExpr for Param<V> {
364    fn render_dyn(&self, buf: &mut String) {
365        // In inline mode, render the literal — the name is irrelevant.
366        V::render_literal(&self.value, buf);
367    }
368    fn render_dyn_params(
369        &self,
370        buf: &mut String,
371        params: &mut BTreeMap<String, serde_json::Value>,
372    ) {
373        buf.push('$');
374        buf.push_str(&self.name);
375        params
376            .entry(self.name.clone())
377            .or_insert_with(|| V::to_param_value(&self.value));
378    }
379}
380
381// ═══════════════════════════════════════════════════════════════════════════════
382// Column — typed field reference (e.g., `asset.name`)
383// ═══════════════════════════════════════════════════════════════════════════════
384
385/// A typed reference to a record field — e.g. `Post::title()` yields a
386/// `Column<Post, String>`. Generated by `#[derive(SurrealRecord)]` and used to
387/// build type-checked filters (`.eq(...)`, `.gt(...)`, …) and projections.
388pub struct Column<T: SurrealRecord, V: SurrealQL> {
389    /// The database column name.
390    pub name: &'static str,
391    /// The SurrealQL type name for this column.
392    pub surreal_type: &'static str,
393    #[doc(hidden)]
394    pub _marker: std::marker::PhantomData<(T, V)>,
395}
396
397impl<T: SurrealRecord, V: SurrealQL> std::fmt::Debug for Column<T, V> {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        f.debug_struct("Column").field("name", &self.name).finish()
400    }
401}
402
403impl<T: SurrealRecord, V: SurrealQL> Clone for Column<T, V> {
404    fn clone(&self) -> Self {
405        *self
406    }
407}
408impl<T: SurrealRecord, V: SurrealQL> Copy for Column<T, V> {}
409
410impl<T: SurrealRecord, V: SurrealQL> DynExpr for Column<T, V> {
411    fn render_dyn(&self, buf: &mut String) {
412        buf.push_str(self.name);
413    }
414}
415
416// ═══════════════════════════════════════════════════════════════════════════════
417// Ident — an untyped column/field reference usable in WHERE clauses
418// ═══════════════════════════════════════════════════════════════════════════════
419
420/// An untyped identifier (column or field path) for building filter expressions
421/// where a typed [`Column`] accessor is unavailable (e.g. record-link fields the
422/// derive doesn't expose, or `tenant.slug` paths). Mirrors `Column`'s operators.
423#[derive(Debug, Clone, Copy)]
424pub struct Ident(pub &'static str);
425
426/// Construct an [`Ident`] for a field name.
427pub fn ident(name: &'static str) -> Ident {
428    Ident(name)
429}
430
431impl Ident {
432    fn dyn_box(&self) -> Box<dyn DynExpr> {
433        Box::new(Raw(self.0.to_string()))
434    }
435
436    pub fn eq<V: SurrealQL>(&self, v: V) -> EqExpr {
437        EqExpr {
438            left: self.dyn_box(),
439            right: Box::new(Literal(v)),
440        }
441    }
442    pub fn ne<V: SurrealQL>(&self, v: V) -> NeExpr {
443        NeExpr {
444            left: self.dyn_box(),
445            right: Box::new(Literal(v)),
446        }
447    }
448    pub fn gt<V: SurrealQL>(&self, v: V) -> GtExpr {
449        GtExpr {
450            left: self.dyn_box(),
451            right: Box::new(Literal(v)),
452        }
453    }
454    pub fn lt<V: SurrealQL>(&self, v: V) -> LtExpr {
455        LtExpr {
456            left: self.dyn_box(),
457            right: Box::new(Literal(v)),
458        }
459    }
460    pub fn gte<V: SurrealQL>(&self, v: V) -> GteExpr {
461        GteExpr {
462            left: self.dyn_box(),
463            right: Box::new(Literal(v)),
464        }
465    }
466    pub fn lte<V: SurrealQL>(&self, v: V) -> LteExpr {
467        LteExpr {
468            left: self.dyn_box(),
469            right: Box::new(Literal(v)),
470        }
471    }
472    pub fn contains<V: SurrealQL>(&self, v: V) -> ContainsExpr {
473        ContainsExpr {
474            haystack: self.dyn_box(),
475            needle: Box::new(Literal(v)),
476        }
477    }
478    /// `field CONTAINS <expr>` — e.g. array-membership test with a [`Param`] or
479    /// other expression.
480    pub fn contains_expr(&self, expr: impl DynExpr + 'static) -> ContainsExpr {
481        ContainsExpr {
482            haystack: self.dyn_box(),
483            needle: Box::new(expr),
484        }
485    }
486    /// Compare to an arbitrary expression — e.g. `asset = type::record('asset', …)`.
487    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
488        EqExpr {
489            left: self.dyn_box(),
490            right: Box::new(rhs),
491        }
492    }
493    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
494        NeExpr {
495            left: self.dyn_box(),
496            right: Box::new(rhs),
497        }
498    }
499    /// `field IN <expr>` — e.g. an array literal or a parenthesized subquery.
500    pub fn in_expr(&self, rhs: impl DynExpr + 'static) -> InExpr {
501        InExpr {
502            left: self.dyn_box(),
503            right: Box::new(rhs),
504        }
505    }
506    /// `field NOT IN <expr>`.
507    pub fn not_in_expr(&self, rhs: impl DynExpr + 'static) -> NotInExpr {
508        NotInExpr {
509            left: self.dyn_box(),
510            right: Box::new(rhs),
511        }
512    }
513    /// `field IS NONE`.
514    pub fn is_none(&self) -> Raw {
515        Raw(format!("{} IS NONE", self.0))
516    }
517}
518
519impl DynExpr for Ident {
520    fn render_dyn(&self, buf: &mut String) {
521        buf.push_str(self.0);
522    }
523}
524
525// ═══════════════════════════════════════════════════════════════════════════════
526// Raw — verbatim SurrealQL escape hatch (lambdas, IF/THEN/ELSE, field paths…)
527// ═══════════════════════════════════════════════════════════════════════════════
528
529/// A verbatim SurrealQL fragment. Use for expressions somnia does not model as
530/// typed nodes (e.g. `IF x != NONE THEN … END`, lambdas, `tenant.slug`).
531#[derive(Debug, Clone)]
532pub struct Raw(pub String);
533
534impl Raw {
535    pub fn new(s: impl Into<String>) -> Self {
536        Self(s.into())
537    }
538}
539
540impl DynExpr for Raw {
541    fn render_dyn(&self, buf: &mut String) {
542        buf.push_str(&self.0);
543    }
544}
545
546/// SurrealDB's `NONE`.
547#[derive(Debug, Clone)]
548pub struct NoneLit;
549impl DynExpr for NoneLit {
550    fn render_dyn(&self, buf: &mut String) {
551        buf.push_str("NONE");
552    }
553}
554
555// ═══════════════════════════════════════════════════════════════════════════════
556// RecordLink — `type::record('table', <key>)`
557// ═══════════════════════════════════════════════════════════════════════════════
558
559/// Builds a SurrealDB record link `type::record('table', <key>)`. The key is any
560/// literal value (typically the bare UUID/string id of the related row).
561#[derive(Debug)]
562pub struct RecordLink {
563    table: &'static str,
564    key: Box<dyn DynExpr>,
565}
566
567impl RecordLink {
568    /// `type::record('table', '<key literal>')`.
569    pub fn new<V: SurrealQL>(table: &'static str, key: V) -> Self {
570        Self {
571            table,
572            key: Box::new(Literal(key)),
573        }
574    }
575    /// `type::record('table', <key expr>)` — key rendered from an arbitrary expr.
576    pub fn from_expr(table: &'static str, key: impl DynExpr + 'static) -> Self {
577        Self {
578            table,
579            key: Box::new(key),
580        }
581    }
582}
583
584impl DynExpr for RecordLink {
585    fn render_dyn(&self, buf: &mut String) {
586        buf.push_str("type::record('");
587        buf.push_str(self.table);
588        buf.push_str("', ");
589        self.key.render_dyn(buf);
590        buf.push(')');
591    }
592    fn render_dyn_params(
593        &self,
594        buf: &mut String,
595        params: &mut BTreeMap<String, serde_json::Value>,
596    ) {
597        buf.push_str("type::record('");
598        buf.push_str(self.table);
599        buf.push_str("', ");
600        self.key.render_dyn_params(buf, params);
601        buf.push(')');
602    }
603}
604
605// ═══════════════════════════════════════════════════════════════════════════════
606// Path — graph traversal (`->edge->table`, `<-edge<-table`, `<->edge<->table`)
607// ═══════════════════════════════════════════════════════════════════════════════
608
609/// Direction of a single graph hop.
610#[derive(Debug, Clone, Copy, PartialEq, Eq)]
611enum Dir {
612    /// outgoing edge — `->`
613    Out,
614    /// incoming edge — `<-`
615    In,
616    /// either direction — `<->`
617    Both,
618}
619
620impl Dir {
621    fn arrow(self) -> &'static str {
622        match self {
623            Dir::Out => "->",
624            Dir::In => "<-",
625            Dir::Both => "<->",
626        }
627    }
628}
629
630/// One hop in a graph [`Path`]: a direction, an edge table, an optional
631/// destination table, and an optional `WHERE` filter on the edge.
632#[derive(Debug)]
633struct Step {
634    dir: Dir,
635    edge: &'static str,
636    dest: Option<&'static str>,
637    filter: Option<Box<dyn DynExpr>>,
638}
639
640impl Step {
641    fn render(&self, buf: &mut String) {
642        buf.push_str(self.dir.arrow());
643        match &self.filter {
644            // `->(edge WHERE <expr>)`
645            Some(f) => {
646                buf.push('(');
647                buf.push_str(self.edge);
648                buf.push_str(" WHERE ");
649                f.render_dyn(buf);
650                buf.push(')');
651            }
652            None => buf.push_str(self.edge),
653        }
654        if let Some(dest) = self.dest {
655            buf.push_str(self.dir.arrow());
656            buf.push_str(dest);
657        }
658    }
659    fn render_params(&self, buf: &mut String, params: &mut BTreeMap<String, serde_json::Value>) {
660        buf.push_str(self.dir.arrow());
661        match &self.filter {
662            Some(f) => {
663                buf.push('(');
664                buf.push_str(self.edge);
665                buf.push_str(" WHERE ");
666                f.render_dyn_params(buf, params);
667                buf.push(')');
668            }
669            None => buf.push_str(self.edge),
670        }
671        if let Some(dest) = self.dest {
672            buf.push_str(self.dir.arrow());
673            buf.push_str(dest);
674        }
675    }
676}
677
678/// The trailing field accessor on a [`Path`] — `.field` or `.*`.
679#[derive(Debug)]
680enum Tail {
681    Field(String),
682    All,
683}
684
685/// A graph-traversal expression — e.g. `->wrote->post`, `<-wrote<-user`, or a
686/// multi-hop chain with an optional `.field`/`.*` accessor at the end.
687///
688/// A `Path` is a [`DynExpr`], so it works anywhere the builder takes an
689/// expression: as a `SELECT` projection (`Projection::aliased(path, "posts")`),
690/// inside a `WHERE` filter, or as a `SET` value. With no start it is relative to
691/// the statement's `FROM` table; [`Path::from_record`] anchors it to a record.
692///
693/// ```ignore
694/// // ->wrote->post.title AS titles
695/// let p = Path::out::<Wrote>().to::<Post>().field("title");
696/// Post::table().project(vec![Projection::aliased(p, "titles")]);
697/// ```
698#[derive(Debug)]
699pub struct Path {
700    start: Option<Box<dyn DynExpr>>,
701    recurse: Option<String>,
702    steps: Vec<Step>,
703    tail: Option<Tail>,
704}
705
706impl Path {
707    fn from_step(dir: Dir, edge: &'static str) -> Self {
708        Self {
709            start: None,
710            recurse: None,
711            steps: vec![Step {
712                dir,
713                edge,
714                dest: None,
715                filter: None,
716            }],
717            tail: None,
718        }
719    }
720
721    /// Start an outgoing hop over edge `E` — `->edge`.
722    pub fn out<E: crate::types::SurrealEdge>() -> Self {
723        Self::from_step(Dir::Out, E::edge_name())
724    }
725    /// Start an incoming hop over edge `E` — `<-edge`.
726    pub fn inn<E: crate::types::SurrealEdge>() -> Self {
727        Self::from_step(Dir::In, E::edge_name())
728    }
729    /// Start a bidirectional hop over edge `E` — `<->edge`.
730    pub fn both<E: crate::types::SurrealEdge>() -> Self {
731        Self::from_step(Dir::Both, E::edge_name())
732    }
733
734    /// Start an outgoing hop over a raw edge name — `->edge`.
735    pub fn out_edge(edge: &'static str) -> Self {
736        Self::from_step(Dir::Out, edge)
737    }
738    /// Start an incoming hop over a raw edge name — `<-edge`.
739    pub fn in_edge(edge: &'static str) -> Self {
740        Self::from_step(Dir::In, edge)
741    }
742    /// Start a bidirectional hop over a raw edge name — `<->edge`.
743    pub fn both_edge(edge: &'static str) -> Self {
744        Self::from_step(Dir::Both, edge)
745    }
746
747    /// Anchor an existing path to a starting record literal — `<record><path>`
748    /// (e.g. `user:tobie->wrote->post`). Pass a [`Thing`](crate::types::Thing).
749    pub fn from_record<V: SurrealQL>(mut self, start: V) -> Self {
750        self.start = Some(Box::new(Literal(start)));
751        self
752    }
753
754    /// Anchor an existing path to a starting expression — e.g. a
755    /// [`RecordLink`] (`type::record('user', $id)->wrote->post`).
756    pub fn from_expr(mut self, start: impl DynExpr + 'static) -> Self {
757        self.start = Some(Box::new(start));
758        self
759    }
760
761    fn last_mut(&mut self) -> &mut Step {
762        self.steps.last_mut().expect("path always has ≥1 step")
763    }
764
765    /// Constrain the destination of the most recent hop to table `T` — `->edge->table`.
766    pub fn to<T: SurrealRecord>(mut self) -> Self {
767        self.last_mut().dest = Some(T::table_name());
768        self
769    }
770    /// Constrain the destination of the most recent hop to a raw table name.
771    pub fn to_table(mut self, table: &'static str) -> Self {
772        self.last_mut().dest = Some(table);
773        self
774    }
775
776    /// Filter the most recent hop's edge — `->(edge WHERE <expr>)`.
777    pub fn where_(mut self, expr: impl DynExpr + 'static) -> Self {
778        self.last_mut().filter = Some(Box::new(expr));
779        self
780    }
781
782    /// Chain another outgoing hop over edge `E`.
783    pub fn then_out<E: crate::types::SurrealEdge>(self) -> Self {
784        self.push_step(Dir::Out, E::edge_name())
785    }
786    /// Chain another incoming hop over edge `E`.
787    pub fn then_in<E: crate::types::SurrealEdge>(self) -> Self {
788        self.push_step(Dir::In, E::edge_name())
789    }
790    /// Chain another bidirectional hop over edge `E`.
791    pub fn then_both<E: crate::types::SurrealEdge>(self) -> Self {
792        self.push_step(Dir::Both, E::edge_name())
793    }
794    /// Chain another hop over a raw edge name, outgoing.
795    pub fn then_out_edge(self, edge: &'static str) -> Self {
796        self.push_step(Dir::Out, edge)
797    }
798    /// Chain another hop over a raw edge name, incoming.
799    pub fn then_in_edge(self, edge: &'static str) -> Self {
800        self.push_step(Dir::In, edge)
801    }
802
803    fn push_step(mut self, dir: Dir, edge: &'static str) -> Self {
804        self.steps.push(Step {
805            dir,
806            edge,
807            dest: None,
808            filter: None,
809        });
810        self
811    }
812
813    /// Repeat the path recursively, unbounded — `@.{..}<path>`. Combine with
814    /// [`from_record`](Self::from_record) to anchor the recursion at a record
815    /// (`person:tobie.{..}->knows->person`).
816    pub fn recurse_all(mut self) -> Self {
817        self.recurse = Some("..".to_string());
818        self
819    }
820    /// Recurse up to `max` hops — `@.{..max}<path>`.
821    pub fn recurse_up_to(mut self, max: u32) -> Self {
822        self.recurse = Some(format!("..{max}"));
823        self
824    }
825    /// Recurse between `min` and `max` hops — `@.{min..max}<path>`.
826    pub fn recurse_range(mut self, min: u32, max: u32) -> Self {
827        self.recurse = Some(format!("{min}..{max}"));
828        self
829    }
830    /// Recurse exactly `n` hops — `@.{n}<path>`.
831    pub fn recurse_exact(mut self, n: u32) -> Self {
832        self.recurse = Some(format!("{n}"));
833        self
834    }
835
836    /// Append a field accessor — `<path>.field`.
837    pub fn field(mut self, name: impl Into<String>) -> Self {
838        self.tail = Some(Tail::Field(name.into()));
839        self
840    }
841    /// Append the all-fields accessor — `<path>.*`.
842    pub fn all(mut self) -> Self {
843        self.tail = Some(Tail::All);
844        self
845    }
846
847    /// `<path> CONTAINS <value>` — e.g. membership test over a traversed list.
848    pub fn contains<V: SurrealQL>(self, value: V) -> ContainsExpr {
849        ContainsExpr {
850            haystack: Box::new(self),
851            needle: Box::new(Literal(value)),
852        }
853    }
854    /// `<path> = <expr>`.
855    pub fn eq_expr(self, rhs: impl DynExpr + 'static) -> EqExpr {
856        EqExpr {
857            left: Box::new(self),
858            right: Box::new(rhs),
859        }
860    }
861}
862
863impl DynExpr for Path {
864    fn render_dyn(&self, buf: &mut String) {
865        match (&self.start, &self.recurse) {
866            // anchored recursion: `<record>.{range}<path>`
867            (Some(start), Some(range)) => {
868                start.render_dyn(buf);
869                buf.push_str(".{");
870                buf.push_str(range);
871                buf.push('}');
872            }
873            // relative recursion: `@.{range}<path>` (the `@` is the recursion point)
874            (None, Some(range)) => {
875                buf.push_str("@.{");
876                buf.push_str(range);
877                buf.push('}');
878            }
879            // no recursion: optional record anchor, then the hops
880            (Some(start), None) => start.render_dyn(buf),
881            (None, None) => {}
882        }
883        for step in &self.steps {
884            step.render(buf);
885        }
886        match &self.tail {
887            Some(Tail::Field(f)) => {
888                buf.push('.');
889                buf.push_str(f);
890            }
891            Some(Tail::All) => buf.push_str(".*"),
892            None => {}
893        }
894    }
895    fn render_dyn_params(
896        &self,
897        buf: &mut String,
898        params: &mut BTreeMap<String, serde_json::Value>,
899    ) {
900        match (&self.start, &self.recurse) {
901            (Some(start), Some(range)) => {
902                start.render_dyn_params(buf, params);
903                buf.push_str(".{");
904                buf.push_str(range);
905                buf.push('}');
906            }
907            (None, Some(range)) => {
908                buf.push_str("@.{");
909                buf.push_str(range);
910                buf.push('}');
911            }
912            (Some(start), None) => start.render_dyn_params(buf, params),
913            (None, None) => {}
914        }
915        for step in &self.steps {
916            step.render_params(buf, params);
917        }
918        match &self.tail {
919            Some(Tail::Field(f)) => {
920                buf.push('.');
921                buf.push_str(f);
922            }
923            Some(Tail::All) => buf.push_str(".*"),
924            None => {}
925        }
926    }
927}
928
929// ═══════════════════════════════════════════════════════════════════════════════
930// Func — `name(arg, arg, …)` (record::id, type::string, string::lowercase, …)
931// ═══════════════════════════════════════════════════════════════════════════════
932
933/// A SurrealQL function call `name(args…)`.
934#[derive(Debug)]
935pub struct Func {
936    name: &'static str,
937    args: Vec<Box<dyn DynExpr>>,
938}
939
940impl Func {
941    pub fn new(name: &'static str, args: Vec<Box<dyn DynExpr>>) -> Self {
942        Self { name, args }
943    }
944    /// Single-argument function over a bare column/identifier, e.g.
945    /// `record::id(id)` → `Func::of("record::id", "id")`.
946    pub fn of(name: &'static str, ident: &'static str) -> Self {
947        Self {
948            name,
949            args: vec![Box::new(Raw(ident.to_string()))],
950        }
951    }
952}
953
954impl DynExpr for Func {
955    fn render_dyn(&self, buf: &mut String) {
956        buf.push_str(self.name);
957        buf.push('(');
958        for (i, a) in self.args.iter().enumerate() {
959            if i > 0 {
960                buf.push_str(", ");
961            }
962            a.render_dyn(buf);
963        }
964        buf.push(')');
965    }
966    fn render_dyn_params(
967        &self,
968        buf: &mut String,
969        params: &mut BTreeMap<String, serde_json::Value>,
970    ) {
971        buf.push_str(self.name);
972        buf.push('(');
973        for (i, a) in self.args.iter().enumerate() {
974            if i > 0 {
975                buf.push_str(", ");
976            }
977            a.render_dyn_params(buf, params);
978        }
979        buf.push(')');
980    }
981}
982
983// ═══════════════════════════════════════════════════════════════════════════════
984// Binary expression nodes
985// ═══════════════════════════════════════════════════════════════════════════════
986
987macro_rules! binop {
988    ($name:ident, $op:literal) => {
989        #[derive(Debug)]
990        pub struct $name {
991            pub(crate) left: Box<dyn DynExpr>,
992            pub(crate) right: Box<dyn DynExpr>,
993        }
994        impl DynExpr for $name {
995            fn render_dyn(&self, buf: &mut String) {
996                self.left.render_dyn(buf);
997                buf.push(' ');
998                buf.push_str($op);
999                buf.push(' ');
1000                self.right.render_dyn(buf);
1001            }
1002            fn render_dyn_params(
1003                &self,
1004                buf: &mut String,
1005                params: &mut BTreeMap<String, serde_json::Value>,
1006            ) {
1007                self.left.render_dyn_params(buf, params);
1008                buf.push(' ');
1009                buf.push_str($op);
1010                buf.push(' ');
1011                self.right.render_dyn_params(buf, params);
1012            }
1013        }
1014    };
1015}
1016
1017binop!(EqExpr, "=");
1018binop!(NeExpr, "!=");
1019binop!(GtExpr, ">");
1020binop!(LtExpr, "<");
1021binop!(GteExpr, ">=");
1022binop!(LteExpr, "<=");
1023binop!(AndExpr, "AND");
1024binop!(OrExpr, "OR");
1025binop!(InExpr, "IN");
1026binop!(NotInExpr, "NOT IN");
1027
1028#[derive(Debug)]
1029pub struct NotExpr {
1030    pub(crate) inner: Box<dyn DynExpr>,
1031}
1032
1033impl DynExpr for NotExpr {
1034    fn render_dyn(&self, buf: &mut String) {
1035        buf.push_str("NOT ");
1036        self.inner.render_dyn(buf);
1037    }
1038    fn render_dyn_params(
1039        &self,
1040        buf: &mut String,
1041        params: &mut BTreeMap<String, serde_json::Value>,
1042    ) {
1043        buf.push_str("NOT ");
1044        self.inner.render_dyn_params(buf, params);
1045    }
1046}
1047
1048#[derive(Debug)]
1049pub struct ContainsExpr {
1050    pub(crate) haystack: Box<dyn DynExpr>,
1051    pub(crate) needle: Box<dyn DynExpr>,
1052}
1053
1054impl DynExpr for ContainsExpr {
1055    fn render_dyn(&self, buf: &mut String) {
1056        self.haystack.render_dyn(buf);
1057        buf.push_str(" CONTAINS ");
1058        self.needle.render_dyn(buf);
1059    }
1060    fn render_dyn_params(
1061        &self,
1062        buf: &mut String,
1063        params: &mut BTreeMap<String, serde_json::Value>,
1064    ) {
1065        self.haystack.render_dyn_params(buf, params);
1066        buf.push_str(" CONTAINS ");
1067        self.needle.render_dyn_params(buf, params);
1068    }
1069}
1070
1071// ═══════════════════════════════════════════════════════════════════════════════
1072// Full-text + vector search operators (@@ and <|k|>)
1073// ═══════════════════════════════════════════════════════════════════════════════
1074
1075/// Full-text match `<left> @@ <right>` (SurrealQL's `MATCHES`), requiring a
1076/// `SEARCH` index on the field. With a match *reference* it renders `@{n}@`,
1077/// which lets `search::score(n)` / `search::highlight(…, n)` pull relevance and
1078/// highlights for that predicate. Usually built via [`Column::matches`] or the
1079/// [`Search`](crate::query::Search) builder.
1080#[derive(Debug)]
1081pub struct MatchesExpr {
1082    pub(crate) left: Box<dyn DynExpr>,
1083    pub(crate) right: Box<dyn DynExpr>,
1084    pub(crate) reference: Option<u8>,
1085}
1086
1087impl MatchesExpr {
1088    /// Set the match reference `n` (renders `@n@`), enabling `search::score(n)`.
1089    pub fn reference(mut self, n: u8) -> Self {
1090        self.reference = Some(n);
1091        self
1092    }
1093    fn op(&self) -> String {
1094        match self.reference {
1095            Some(r) => format!("@{r}@"),
1096            None => "@@".to_string(),
1097        }
1098    }
1099}
1100
1101impl DynExpr for MatchesExpr {
1102    fn render_dyn(&self, buf: &mut String) {
1103        self.left.render_dyn(buf);
1104        buf.push(' ');
1105        buf.push_str(&self.op());
1106        buf.push(' ');
1107        self.right.render_dyn(buf);
1108    }
1109    fn render_dyn_params(
1110        &self,
1111        buf: &mut String,
1112        params: &mut BTreeMap<String, serde_json::Value>,
1113    ) {
1114        self.left.render_dyn_params(buf, params);
1115        buf.push(' ');
1116        buf.push_str(&self.op());
1117        buf.push(' ');
1118        self.right.render_dyn_params(buf, params);
1119    }
1120}
1121
1122/// K-nearest-neighbour operator `<field> <|k[,opt]|> <vector>`. With no option
1123/// it uses the field's vector index (`<|k|>`); `opt` carries either a brute-force
1124/// distance metric (`<|k,EUCLIDEAN|>`) or an HNSW search size (`<|k,40|>`).
1125/// Usually built via the [`VectorSearch`](crate::query::VectorSearch) builder.
1126#[derive(Debug)]
1127pub struct KnnExpr {
1128    pub(crate) left: Box<dyn DynExpr>,
1129    pub(crate) right: Box<dyn DynExpr>,
1130    pub(crate) k: u32,
1131    pub(crate) opt: Option<String>,
1132}
1133
1134impl KnnExpr {
1135    fn op(&self) -> String {
1136        match &self.opt {
1137            Some(o) => format!("<|{},{}|>", self.k, o),
1138            None => format!("<|{}|>", self.k),
1139        }
1140    }
1141}
1142
1143impl DynExpr for KnnExpr {
1144    fn render_dyn(&self, buf: &mut String) {
1145        self.left.render_dyn(buf);
1146        buf.push(' ');
1147        buf.push_str(&self.op());
1148        buf.push(' ');
1149        self.right.render_dyn(buf);
1150    }
1151    fn render_dyn_params(
1152        &self,
1153        buf: &mut String,
1154        params: &mut BTreeMap<String, serde_json::Value>,
1155    ) {
1156        self.left.render_dyn_params(buf, params);
1157        buf.push(' ');
1158        buf.push_str(&self.op());
1159        buf.push(' ');
1160        self.right.render_dyn_params(buf, params);
1161    }
1162}
1163
1164// ═══════════════════════════════════════════════════════════════════════════════
1165// Column operator methods (Diesel-style: asset.name.eq("foo"))
1166// ═══════════════════════════════════════════════════════════════════════════════
1167
1168impl<T: SurrealRecord, V: SurrealQL> Column<T, V> {
1169    pub fn eq(&self, value: V) -> EqExpr {
1170        EqExpr {
1171            left: self.dyn_box(),
1172            right: Box::new(Literal(value)),
1173        }
1174    }
1175    pub fn ne(&self, value: V) -> NeExpr {
1176        NeExpr {
1177            left: self.dyn_box(),
1178            right: Box::new(Literal(value)),
1179        }
1180    }
1181    pub fn gt(&self, value: V) -> GtExpr {
1182        GtExpr {
1183            left: self.dyn_box(),
1184            right: Box::new(Literal(value)),
1185        }
1186    }
1187    pub fn lt(&self, value: V) -> LtExpr {
1188        LtExpr {
1189            left: self.dyn_box(),
1190            right: Box::new(Literal(value)),
1191        }
1192    }
1193    pub fn gte(&self, value: V) -> GteExpr {
1194        GteExpr {
1195            left: self.dyn_box(),
1196            right: Box::new(Literal(value)),
1197        }
1198    }
1199    pub fn lte(&self, value: V) -> LteExpr {
1200        LteExpr {
1201            left: self.dyn_box(),
1202            right: Box::new(Literal(value)),
1203        }
1204    }
1205    pub fn contains(&self, value: V) -> ContainsExpr {
1206        ContainsExpr {
1207            haystack: self.dyn_box(),
1208            needle: Box::new(Literal(value)),
1209        }
1210    }
1211    /// `column CONTAINS <expr>` — e.g. array-membership test with an expression.
1212    pub fn contains_expr(&self, expr: impl DynExpr + 'static) -> ContainsExpr {
1213        ContainsExpr {
1214            haystack: self.dyn_box(),
1215            needle: Box::new(expr),
1216        }
1217    }
1218
1219    /// Compare this column to an arbitrary expression — e.g. a [`RecordLink`]
1220    /// (`asset = type::record('asset', …)`) or [`NoneLit`] (`tenant = NONE`).
1221    pub fn eq_expr(&self, rhs: impl DynExpr + 'static) -> EqExpr {
1222        EqExpr {
1223            left: self.dyn_box(),
1224            right: Box::new(rhs),
1225        }
1226    }
1227    pub fn ne_expr(&self, rhs: impl DynExpr + 'static) -> NeExpr {
1228        NeExpr {
1229            left: self.dyn_box(),
1230            right: Box::new(rhs),
1231        }
1232    }
1233    /// `column IN <expr>` — e.g. an array literal or a parenthesized subquery.
1234    pub fn in_expr(&self, rhs: impl DynExpr + 'static) -> InExpr {
1235        InExpr {
1236            left: self.dyn_box(),
1237            right: Box::new(rhs),
1238        }
1239    }
1240    /// `column NOT IN <expr>`.
1241    pub fn not_in_expr(&self, rhs: impl DynExpr + 'static) -> NotInExpr {
1242        NotInExpr {
1243            left: self.dyn_box(),
1244            right: Box::new(rhs),
1245        }
1246    }
1247    /// `column IS NONE`.
1248    pub fn is_none(&self) -> Raw {
1249        Raw(format!("{} IS NONE", self.name))
1250    }
1251
1252    /// Full-text match `column @@ <query>` — requires a `SEARCH` index on the
1253    /// column. Chain [`reference`](MatchesExpr::reference) to enable scoring.
1254    pub fn matches(&self, query: impl Into<String>) -> MatchesExpr {
1255        MatchesExpr {
1256            left: self.dyn_box(),
1257            right: Box::new(Literal(query.into())),
1258            reference: None,
1259        }
1260    }
1261
1262    fn dyn_box(&self) -> Box<dyn DynExpr> {
1263        Box::new(Self {
1264            name: self.name,
1265            surreal_type: self.surreal_type,
1266            _marker: self._marker,
1267        })
1268    }
1269}
1270
1271// Combinators: (a = 1).and(b = 2).or(c = 3). Available on every expression node.
1272macro_rules! combinators {
1273    ($($t:ty),* $(,)?) => {$(
1274        impl $t {
1275            pub fn and(self, other: impl DynExpr + 'static) -> AndExpr {
1276                AndExpr { left: Box::new(self), right: Box::new(other) }
1277            }
1278            pub fn or(self, other: impl DynExpr + 'static) -> OrExpr {
1279                OrExpr { left: Box::new(self), right: Box::new(other) }
1280            }
1281        }
1282    )*};
1283}
1284combinators!(
1285    EqExpr,
1286    NeExpr,
1287    GtExpr,
1288    LtExpr,
1289    GteExpr,
1290    LteExpr,
1291    AndExpr,
1292    OrExpr,
1293    ContainsExpr,
1294    InExpr,
1295    NotInExpr,
1296    NotExpr,
1297    MatchesExpr,
1298    KnnExpr,
1299    Raw
1300);
1301
1302/// Wraps an expression in parentheses: `(<expr>)`. Use to force grouping/precedence.
1303#[derive(Debug)]
1304pub struct Grouped(pub Box<dyn DynExpr>);
1305
1306impl Grouped {
1307    pub fn new(inner: impl DynExpr + 'static) -> Self {
1308        Self(Box::new(inner))
1309    }
1310}
1311
1312impl DynExpr for Grouped {
1313    fn render_dyn(&self, buf: &mut String) {
1314        buf.push('(');
1315        self.0.render_dyn(buf);
1316        buf.push(')');
1317    }
1318    fn render_dyn_params(
1319        &self,
1320        buf: &mut String,
1321        params: &mut BTreeMap<String, serde_json::Value>,
1322    ) {
1323        buf.push('(');
1324        self.0.render_dyn_params(buf, params);
1325        buf.push(')');
1326    }
1327}
1328
1329combinators!(Grouped, Func, Path);
1330
1331// ═══════════════════════════════════════════════════════════════════════════════
1332// Closure — anonymous function `|$a: t, …| [-> ret] <body>`
1333// ═══════════════════════════════════════════════════════════════════════════════
1334
1335/// A SurrealQL anonymous function (closure), e.g. `|$x: int| -> int $x * 2`. As
1336/// a [`DynExpr`] it nests anywhere an expression is taken — a `SET` value, a
1337/// `DEFINE PARAM`/`LET` value, or an argument to a higher-order function
1338/// (`array::map($xs, |$x: int| $x * 2)`).
1339///
1340/// ```ignore
1341/// Closure::new(Raw("$x * 2".into())).arg("x", "int").returns("int");
1342/// // |$x: int| -> int $x * 2
1343/// ```
1344#[derive(Debug)]
1345pub struct Closure {
1346    args: Vec<(String, String)>,
1347    returns: Option<String>,
1348    body: Box<dyn DynExpr>,
1349}
1350
1351impl Closure {
1352    /// A closure with the given body and no arguments yet.
1353    pub fn new(body: impl DynExpr + 'static) -> Self {
1354        Self {
1355            args: Vec::new(),
1356            returns: None,
1357            body: Box::new(body),
1358        }
1359    }
1360    /// Add a typed argument `$name: <surreal_type>` (a leading `$` is optional).
1361    pub fn arg(mut self, name: impl Into<String>, surreal_type: impl Into<String>) -> Self {
1362        let mut name = name.into();
1363        if !name.starts_with('$') {
1364            name.insert(0, '$');
1365        }
1366        self.args.push((name, surreal_type.into()));
1367        self
1368    }
1369    /// Set the return type (`-> <surreal_type>`).
1370    pub fn returns(mut self, surreal_type: impl Into<String>) -> Self {
1371        self.returns = Some(surreal_type.into());
1372        self
1373    }
1374
1375    fn render_head(&self, buf: &mut String) {
1376        buf.push('|');
1377        for (i, (name, ty)) in self.args.iter().enumerate() {
1378            if i > 0 {
1379                buf.push_str(", ");
1380            }
1381            buf.push_str(name);
1382            buf.push_str(": ");
1383            buf.push_str(ty);
1384        }
1385        buf.push('|');
1386        if let Some(ret) = &self.returns {
1387            buf.push_str(" -> ");
1388            buf.push_str(ret);
1389        }
1390        buf.push(' ');
1391    }
1392}
1393
1394impl DynExpr for Closure {
1395    fn render_dyn(&self, buf: &mut String) {
1396        self.render_head(buf);
1397        self.body.render_dyn(buf);
1398    }
1399    fn render_dyn_params(
1400        &self,
1401        buf: &mut String,
1402        params: &mut BTreeMap<String, serde_json::Value>,
1403    ) {
1404        self.render_head(buf);
1405        self.body.render_dyn_params(buf, params);
1406    }
1407}
1408
1409// ═══════════════════════════════════════════════════════════════════════════════
1410// IfExpr — IF … THEN … ELSE IF … ELSE … END
1411// ═══════════════════════════════════════════════════════════════════════════════
1412
1413/// A SurrealQL `IF <cond> THEN <expr> [ELSE IF <cond> THEN <expr>]… [ELSE <expr>]
1414/// END` expression. As a [`DynExpr`] it nests anywhere an expression is taken —
1415/// a `SET` value, a `SELECT` projection, a `RETURN`, or a `WHERE`.
1416///
1417/// ```ignore
1418/// IfExpr::new(Raw("age >= 18".into()), Raw("'adult'".into()))
1419///     .else_if(Raw("age >= 13".into()), Raw("'teen'".into()))
1420///     .else_(Raw("'child'".into()));
1421/// // IF age >= 18 THEN 'adult' ELSE IF age >= 13 THEN 'teen' ELSE 'child' END
1422/// ```
1423#[derive(Debug)]
1424pub struct IfExpr {
1425    branches: Vec<(Box<dyn DynExpr>, Box<dyn DynExpr>)>,
1426    else_branch: Option<Box<dyn DynExpr>>,
1427}
1428
1429impl IfExpr {
1430    /// Begin `IF <cond> THEN <then>`.
1431    pub fn new(cond: impl DynExpr + 'static, then: impl DynExpr + 'static) -> Self {
1432        Self {
1433            branches: vec![(Box::new(cond), Box::new(then))],
1434            else_branch: None,
1435        }
1436    }
1437    /// Add an `ELSE IF <cond> THEN <then>` branch.
1438    pub fn else_if(mut self, cond: impl DynExpr + 'static, then: impl DynExpr + 'static) -> Self {
1439        self.branches.push((Box::new(cond), Box::new(then)));
1440        self
1441    }
1442    /// Add the trailing `ELSE <expr>` branch.
1443    pub fn else_(mut self, expr: impl DynExpr + 'static) -> Self {
1444        self.else_branch = Some(Box::new(expr));
1445        self
1446    }
1447}
1448
1449impl DynExpr for IfExpr {
1450    fn render_dyn(&self, buf: &mut String) {
1451        for (i, (cond, then)) in self.branches.iter().enumerate() {
1452            buf.push_str(if i == 0 { "IF " } else { " ELSE IF " });
1453            cond.render_dyn(buf);
1454            buf.push_str(" THEN ");
1455            then.render_dyn(buf);
1456        }
1457        if let Some(e) = &self.else_branch {
1458            buf.push_str(" ELSE ");
1459            e.render_dyn(buf);
1460        }
1461        buf.push_str(" END");
1462    }
1463    fn render_dyn_params(
1464        &self,
1465        buf: &mut String,
1466        params: &mut BTreeMap<String, serde_json::Value>,
1467    ) {
1468        for (i, (cond, then)) in self.branches.iter().enumerate() {
1469            buf.push_str(if i == 0 { "IF " } else { " ELSE IF " });
1470            cond.render_dyn_params(buf, params);
1471            buf.push_str(" THEN ");
1472            then.render_dyn_params(buf, params);
1473        }
1474        if let Some(e) = &self.else_branch {
1475            buf.push_str(" ELSE ");
1476            e.render_dyn_params(buf, params);
1477        }
1478        buf.push_str(" END");
1479    }
1480}
1481
1482// ═══════════════════════════════════════════════════════════════════════════════
1483// Projection — a SELECT field, optionally `<expr> AS alias`
1484// ═══════════════════════════════════════════════════════════════════════════════
1485
1486/// A single SELECT-list entry. Either a bare expression or `<expr> AS <alias>`.
1487#[derive(Debug)]
1488pub struct Projection {
1489    expr: Box<dyn DynExpr>,
1490    alias: Option<&'static str>,
1491}
1492
1493impl Projection {
1494    /// A bare field/expression with no alias.
1495    pub fn new(expr: impl DynExpr + 'static) -> Self {
1496        Self {
1497            expr: Box::new(expr),
1498            alias: None,
1499        }
1500    }
1501    /// `<expr> AS <alias>`.
1502    pub fn aliased(expr: impl DynExpr + 'static, alias: &'static str) -> Self {
1503        Self {
1504            expr: Box::new(expr),
1505            alias: Some(alias),
1506        }
1507    }
1508    pub fn render(&self, buf: &mut String) {
1509        self.expr.render_dyn(buf);
1510        if let Some(a) = self.alias {
1511            buf.push_str(" AS ");
1512            buf.push_str(a);
1513        }
1514    }
1515    pub fn render_params(
1516        &self,
1517        buf: &mut String,
1518        params: &mut BTreeMap<String, serde_json::Value>,
1519    ) {
1520        self.expr.render_dyn_params(buf, params);
1521        if let Some(a) = self.alias {
1522            buf.push_str(" AS ");
1523            buf.push_str(a);
1524        }
1525    }
1526}
1527
1528/// A bare column name as a projection: `name`.
1529pub fn col(name: &'static str) -> Projection {
1530    Projection::new(Raw(name.to_string()))
1531}
1532
1533/// `<raw> AS <alias>` — verbatim expression with an alias.
1534pub fn field(raw: &'static str, alias: &'static str) -> Projection {
1535    Projection::aliased(Raw(raw.to_string()), alias)
1536}
1537
1538// ═══════════════════════════════════════════════════════════════════════════════
1539// ColumnSet — `*` selector generated by derive macro
1540// ═══════════════════════════════════════════════════════════════════════════════
1541
1542#[derive(Debug, Clone)]
1543pub struct ColumnMeta {
1544    pub name: &'static str,
1545    pub surreal_type: &'static str,
1546}
1547
1548/// Select-all (`*`) column list. Generated by `#[derive(SurrealRecord)]`.
1549pub struct ColumnSet<T: SurrealRecord> {
1550    pub cols: &'static [ColumnMeta],
1551    pub _marker: std::marker::PhantomData<T>,
1552}
1553
1554impl<T: SurrealRecord> std::fmt::Debug for ColumnSet<T> {
1555    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1556        f.debug_struct("ColumnSet")
1557            .field("cols", &self.cols)
1558            .finish()
1559    }
1560}
1561
1562impl<T: SurrealRecord> DynExpr for ColumnSet<T> {
1563    fn render_dyn(&self, buf: &mut String) {
1564        buf.push('*');
1565    }
1566}
1567
1568// ═══════════════════════════════════════════════════════════════════════════════
1569// ORDER BY
1570// ═══════════════════════════════════════════════════════════════════════════════
1571
1572/// Sort direction for `ORDER BY`.
1573#[derive(Debug, Clone, Copy)]
1574pub enum Order {
1575    /// Ascending (`ASC`).
1576    Asc,
1577    /// Descending (`DESC`).
1578    Desc,
1579}
1580
1581impl Order {
1582    pub fn render_suffix(&self) -> &'static str {
1583        match self {
1584            Order::Asc => "ASC",
1585            Order::Desc => "DESC",
1586        }
1587    }
1588}
1589
1590impl std::fmt::Display for Order {
1591    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1592        f.write_str(self.render_suffix())
1593    }
1594}