Skip to main content

somnia_core/
query.rs

1//! SurrealQL statement builders.
2//!
3//! Each builder is reached from [`Table`] (itself produced by the derived
4//! `Type::table()`) and rendered to a string with `to_surrealql()`:
5//!
6//! - [`Select`] — `SELECT … FROM …`
7//! - [`Create`] — `CREATE …`
8//! - [`Insert`] — `INSERT INTO …`
9//! - [`Update`] — `UPDATE …` (and `UPSERT …` via [`Table::upsert`])
10//! - [`Delete`] — `DELETE …`
11//! - [`Relate`] / [`RelateEdge`] — `RELATE a -> edge -> b`
12//! - [`Batch`] — several statements joined with `;`
13//!
14//! Mutations also offer `then_select(...)` to chain a reselect as a batch.
15
16use crate::{
17    expr::{Column, DynExpr, Order, Path, Projection, RecordLink, SurrealQL},
18    types::{SurrealEdge, SurrealRecord, Thing},
19};
20use std::collections::BTreeMap;
21
22/// How a mutating statement should return its affected rows.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Returning {
25    /// no RETURN clause
26    None,
27    /// `RETURN NONE`
28    Nothing,
29    /// `RETURN BEFORE`
30    Before,
31    /// `RETURN AFTER`
32    After,
33    /// `RETURN DIFF`
34    Diff,
35}
36
37impl Returning {
38    fn render(self, buf: &mut String) {
39        match self {
40            Returning::None => {}
41            Returning::Nothing => buf.push_str(" RETURN NONE"),
42            Returning::Before => buf.push_str(" RETURN BEFORE"),
43            Returning::After => buf.push_str(" RETURN AFTER"),
44            Returning::Diff => buf.push_str(" RETURN DIFF"),
45        }
46    }
47}
48
49/// A statement target: either a whole table (`asset`) or a single record link
50/// (`type::record('asset', '<id>')`).
51enum Target {
52    Table(&'static str),
53    Record(RecordLink),
54}
55
56impl Target {
57    fn render(&self, buf: &mut String) {
58        match self {
59            Target::Table(t) => buf.push_str(t),
60            Target::Record(r) => r.render_dyn(buf),
61        }
62    }
63    fn render_params(&self, buf: &mut String, params: &mut BTreeMap<String, serde_json::Value>) {
64        match self {
65            Target::Table(t) => buf.push_str(t),
66            Target::Record(r) => r.render_dyn_params(buf, params),
67        }
68    }
69}
70
71// ═══════════════════════════════════════════════════════════════════════════════
72// Table
73// ═══════════════════════════════════════════════════════════════════════════════
74
75/// Entry point to the query builder for a record type `T` — the value returned by
76/// the derived `T::table()`. Each method starts a statement builder.
77pub struct Table<T: SurrealRecord> {
78    _marker: std::marker::PhantomData<T>,
79}
80
81impl<T: SurrealRecord> Table<T> {
82    /// Create a `Table` builder. Prefer the derived `T::table()`.
83    pub fn new() -> Self {
84        Self {
85            _marker: std::marker::PhantomData,
86        }
87    }
88
89    /// Begin a `SELECT * FROM <table>` (pass the derived `T::all()`).
90    pub fn select(self, _cols: crate::expr::ColumnSet<T>) -> Select<T> {
91        Select::bare()
92    }
93
94    /// Select an explicit projection list (`SELECT <fields…> FROM table`).
95    pub fn project(self, fields: Vec<Projection>) -> Select<T> {
96        let mut s = Select::bare();
97        s.projections = fields;
98        s
99    }
100
101    /// `SELECT <path> AS <alias> FROM table` — project a graph traversal.
102    /// A convenience for `project(vec![Projection::aliased(path, alias)])`.
103    pub fn project_path(self, path: Path, alias: &'static str) -> Select<T> {
104        let mut s = Select::bare();
105        s.projections = vec![Projection::aliased(path, alias)];
106        s
107    }
108
109    /// `SELECT count() FROM table GROUP ALL`.
110    pub fn count(self) -> Select<T> {
111        let mut s = Select::bare();
112        s.count = true;
113        s.group_all = true;
114        s
115    }
116
117    /// Begin an `INSERT INTO <table> …`.
118    pub fn insert(self) -> Insert<T> {
119        Insert {
120            data: Vec::new(),
121            return_fields: vec![],
122            returning: Returning::None,
123        }
124    }
125    /// Begin a `CREATE <table> …`.
126    pub fn create(self) -> Create<T> {
127        Create::for_table()
128    }
129    /// Begin an `UPDATE <table> …`.
130    pub fn update(self) -> Update<T> {
131        Update::for_table()
132    }
133    /// `UPSERT` — update the matching record, or create it if it doesn't exist.
134    /// Same builder surface as [`update`](Self::update) (`record`/`set`/`merge`/
135    /// `content`/`filter`/`returning`/`then_select`).
136    pub fn upsert(self) -> Update<T> {
137        Update::for_upsert()
138    }
139    /// Begin a `DELETE <table> …`.
140    pub fn delete(self) -> Delete<T> {
141        Delete::for_table()
142    }
143}
144
145impl<T: SurrealRecord> Default for Table<T> {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151// ═══════════════════════════════════════════════════════════════════════════════
152// SELECT
153// ═══════════════════════════════════════════════════════════════════════════════
154
155/// A `SELECT` statement builder: projections, `WHERE`, `ORDER BY`, `LIMIT`,
156/// `START`, `FETCH`, `GROUP BY`/`GROUP ALL`, `count()`, and the modifiers
157/// `VALUE`/`OMIT`/`SPLIT`/`WITH`/`TIMEOUT`/`EXPLAIN`.
158pub struct Select<T: SurrealRecord> {
159    _marker: std::marker::PhantomData<T>,
160    projections: Vec<Projection>,
161    value: bool,
162    omit: Vec<String>,
163    with: Option<String>,
164    filter: Option<Box<dyn DynExpr>>,
165    split: Vec<String>,
166    order: Vec<(String, Order)>,
167    limit: Option<u32>,
168    start: u32,
169    fetch: Vec<String>,
170    group_by: Vec<String>,
171    group_all: bool,
172    count: bool,
173    count_alias: Option<&'static str>,
174    timeout: Option<String>,
175    explain: Option<bool>,
176    from_sub: Option<Box<Select<T>>>,
177}
178
179impl<T: SurrealRecord> Select<T> {
180    fn bare() -> Self {
181        Select {
182            _marker: std::marker::PhantomData,
183            projections: Vec::new(),
184            value: false,
185            omit: Vec::new(),
186            with: None,
187            filter: None,
188            split: Vec::new(),
189            order: Vec::new(),
190            limit: None,
191            start: 0,
192            fetch: Vec::new(),
193            group_by: Vec::new(),
194            group_all: false,
195            count: false,
196            count_alias: None,
197            timeout: None,
198            explain: None,
199            from_sub: None,
200        }
201    }
202
203    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
204        self.filter = Some(Box::new(expr));
205        self
206    }
207    /// Add a `<path> AS <alias>` graph-traversal projection to the select list.
208    /// Appends to any existing projections, so `select(T::all()).with_path(p, "x")`
209    /// renders `SELECT *, <path> AS x` (a `*` is emitted only when the list is
210    /// otherwise empty).
211    pub fn with_path(mut self, path: Path, alias: &'static str) -> Self {
212        if self.projections.is_empty() {
213            self.projections
214                .push(Projection::new(crate::expr::Raw("*".to_string())));
215        }
216        self.projections.push(Projection::aliased(path, alias));
217        self
218    }
219    pub fn limit(mut self, n: u32) -> Self {
220        self.limit = Some(n);
221        self
222    }
223    pub fn start(mut self, n: u32) -> Self {
224        self.start = n;
225        self
226    }
227    pub fn fetch(mut self, field: impl Into<String>) -> Self {
228        self.fetch.push(field.into());
229        self
230    }
231    pub fn group_by<C: DynExpr>(mut self, col: C) -> Self {
232        let mut buf = String::new();
233        col.render_dyn(&mut buf);
234        self.group_by.push(buf);
235        self
236    }
237    /// `GROUP ALL` (whole-table aggregate, e.g. with `count()`).
238    pub fn group_all(mut self) -> Self {
239        self.group_all = true;
240        self
241    }
242    /// Alias for the `count()` projection: `SELECT count() AS <alias>`.
243    pub fn count_as(mut self, alias: &'static str) -> Self {
244        self.count = true;
245        self.count_alias = Some(alias);
246        self
247    }
248
249    /// `SELECT VALUE …` — return bare values instead of field-wrapping objects.
250    /// Pair with a single projection (e.g. `project(vec![col("name")]).value()`).
251    pub fn value(mut self) -> Self {
252        self.value = true;
253        self
254    }
255    /// `OMIT <field>` — exclude a field from a `SELECT *`.
256    pub fn omit(mut self, field: impl Into<String>) -> Self {
257        self.omit.push(field.into());
258        self
259    }
260    /// `SPLIT <field>` — fan one row out into multiple rows by an array field.
261    pub fn split(mut self, field: impl Into<String>) -> Self {
262        self.split.push(field.into());
263        self
264    }
265    /// `WITH INDEX <a, b>` — force the planner to use the named index(es).
266    pub fn with_index<I, S>(mut self, indexes: I) -> Self
267    where
268        I: IntoIterator<Item = S>,
269        S: AsRef<str>,
270    {
271        let list = indexes
272            .into_iter()
273            .map(|s| s.as_ref().to_string())
274            .collect::<Vec<_>>()
275            .join(", ");
276        self.with = Some(format!("WITH INDEX {list}"));
277        self
278    }
279    /// `WITH NOINDEX` — force a table scan (ignore indexes).
280    pub fn with_no_index(mut self) -> Self {
281        self.with = Some("WITH NOINDEX".to_string());
282        self
283    }
284    /// `TIMEOUT <duration>` — abort the query after the given duration (e.g. `"5s"`).
285    pub fn timeout(mut self, duration: impl Into<String>) -> Self {
286        self.timeout = Some(duration.into());
287        self
288    }
289    /// `SELECT … FROM (<subquery>)` — read from a subquery instead of the base
290    /// table. The subquery (a `Select<T>` of the same record type) renders
291    /// parenthesized in place of the table name.
292    pub fn from_subquery(mut self, sub: Select<T>) -> Self {
293        self.from_sub = Some(Box::new(sub));
294        self
295    }
296
297    /// `EXPLAIN` — return the query plan instead of results.
298    pub fn explain(mut self) -> Self {
299        self.explain = Some(false);
300        self
301    }
302    /// `EXPLAIN FULL` — return the query plan with execution detail.
303    pub fn explain_full(mut self) -> Self {
304        self.explain = Some(true);
305        self
306    }
307
308    pub fn order_by<C: DynExpr>(mut self, col: C, dir: Order) -> Self {
309        let mut buf = String::new();
310        col.render_dyn(&mut buf);
311        self.order.push((buf, dir));
312        self
313    }
314
315    pub fn order_asc<C: DynExpr>(self, col: C) -> Self {
316        self.order_by(col, Order::Asc)
317    }
318    pub fn order_desc<C: DynExpr>(self, col: C) -> Self {
319        self.order_by(col, Order::Desc)
320    }
321
322    fn render_select_list(&self, q: &mut String) {
323        if self.count {
324            q.push_str("count()");
325            if let Some(a) = self.count_alias {
326                q.push_str(" AS ");
327                q.push_str(a);
328            }
329        } else if self.projections.is_empty() {
330            q.push('*');
331        } else {
332            for (i, p) in self.projections.iter().enumerate() {
333                if i > 0 {
334                    q.push_str(", ");
335                }
336                p.render(q);
337            }
338        }
339    }
340
341    fn render_select_list_params(
342        &self,
343        q: &mut String,
344        params: &mut BTreeMap<String, serde_json::Value>,
345    ) {
346        if self.count {
347            q.push_str("count()");
348            if let Some(a) = self.count_alias {
349                q.push_str(" AS ");
350                q.push_str(a);
351            }
352        } else if self.projections.is_empty() {
353            q.push('*');
354        } else {
355            for (i, p) in self.projections.iter().enumerate() {
356                if i > 0 {
357                    q.push_str(", ");
358                }
359                p.render_params(q, params);
360            }
361        }
362    }
363
364    /// Shared renderer for both inline and `$param` modes. When `param_mode` is
365    /// set, literals render as `$pN` placeholders collected into `params`;
366    /// otherwise they render inline (and `params` is ignored). A single map is
367    /// threaded through so a nested subquery's params merge into the parent's.
368    fn render(
369        &self,
370        q: &mut String,
371        params: &mut BTreeMap<String, serde_json::Value>,
372        param_mode: bool,
373    ) {
374        q.push_str("SELECT ");
375        if self.value {
376            q.push_str("VALUE ");
377        }
378        if param_mode {
379            self.render_select_list_params(q, params);
380        } else {
381            self.render_select_list(q);
382        }
383        if !self.omit.is_empty() {
384            q.push_str(" OMIT ");
385            q.push_str(&self.omit.join(", "));
386        }
387        q.push_str(" FROM ");
388        match &self.from_sub {
389            Some(sub) => {
390                q.push('(');
391                sub.render(q, params, param_mode);
392                q.push(')');
393            }
394            None => q.push_str(T::table_name()),
395        }
396        if let Some(w) = &self.with {
397            q.push(' ');
398            q.push_str(w);
399        }
400        if let Some(ref f) = self.filter {
401            q.push_str(" WHERE ");
402            if param_mode {
403                f.render_dyn_params(q, params);
404            } else {
405                f.render_dyn(q);
406            }
407        }
408        for (i, s) in self.split.iter().enumerate() {
409            q.push_str(if i == 0 { " SPLIT " } else { ", " });
410            q.push_str(s);
411        }
412        for (i, (col, dir)) in self.order.iter().enumerate() {
413            q.push_str(if i == 0 { " ORDER BY " } else { ", " });
414            q.push_str(&format!("{col} {dir}"));
415        }
416        for (i, g) in self.group_by.iter().enumerate() {
417            q.push_str(if i == 0 { " GROUP BY " } else { ", " });
418            q.push_str(g);
419        }
420        if self.group_all {
421            q.push_str(" GROUP ALL");
422        }
423        if self.start > 0 {
424            q.push_str(&format!(" START {}", self.start));
425        }
426        if let Some(n) = self.limit {
427            q.push_str(&format!(" LIMIT {n}"));
428        }
429        for f in &self.fetch {
430            q.push_str(&format!(" FETCH {f}"));
431        }
432        if let Some(t) = &self.timeout {
433            q.push_str(" TIMEOUT ");
434            q.push_str(t);
435        }
436        match self.explain {
437            Some(true) => q.push_str(" EXPLAIN FULL"),
438            Some(false) => q.push_str(" EXPLAIN"),
439            None => {}
440        }
441    }
442
443    pub fn to_surrealql(&self) -> String {
444        let mut q = String::new();
445        let mut sink = BTreeMap::new();
446        self.render(&mut q, &mut sink, false);
447        q
448    }
449
450    /// Render the statement with `$param` placeholders instead of inlined
451    /// literals, returning the SQL string and a map of parameter name to value.
452    /// Literal values become numbered `$p0`, `$p1`, …; explicit [`Param`](crate::expr::Param)
453    /// wrappers use their declared name.
454    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
455        let mut params = BTreeMap::new();
456        let mut q = String::new();
457        self.render(&mut q, &mut params, true);
458        (q, params)
459    }
460}
461
462impl<T: SurrealRecord> std::fmt::Debug for Select<T> {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        f.debug_struct("Select")
465            .field("sql", &self.to_surrealql())
466            .finish()
467    }
468}
469
470/// A `Select` is usable as an expression — rendered parenthesized — so it can be
471/// embedded as a subquery: a scalar/`IN` operand in a `WHERE`, a projection, or a
472/// `SET`/`FROM` value. Params from the subquery merge into the parent's map.
473impl<T: SurrealRecord> DynExpr for Select<T> {
474    fn render_dyn(&self, buf: &mut String) {
475        let mut sink = BTreeMap::new();
476        buf.push('(');
477        self.render(buf, &mut sink, false);
478        buf.push(')');
479    }
480    fn render_dyn_params(
481        &self,
482        buf: &mut String,
483        params: &mut BTreeMap<String, serde_json::Value>,
484    ) {
485        buf.push('(');
486        self.render(buf, params, true);
487        buf.push(')');
488    }
489}
490
491impl<T: SurrealRecord> std::fmt::Display for Select<T> {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        write!(f, "{}", self.to_surrealql())
494    }
495}
496
497// ═══════════════════════════════════════════════════════════════════════════════
498// INSERT
499// ═══════════════════════════════════════════════════════════════════════════════
500
501/// An `INSERT INTO <table> …` builder. Records are serialized inline as object
502/// literals; rendering requires `T: serde::Serialize`.
503pub struct Insert<T: SurrealRecord> {
504    data: Vec<T>,
505    return_fields: Vec<&'static str>,
506    returning: Returning,
507}
508
509impl<T: SurrealRecord> Insert<T> {
510    pub fn content(mut self, record: T) -> Self {
511        self.data.push(record);
512        self
513    }
514    /// Add a field to the `RETURN <projection>` list. Multiple calls accumulate
515    /// (`RETURN id, name`). Takes precedence over [`returning`](Self::returning).
516    pub fn return_field(mut self, field: &'static str) -> Self {
517        self.return_fields.push(field);
518        self
519    }
520    /// Set a `RETURN NONE|BEFORE|AFTER|DIFF` clause (used when no explicit
521    /// [`return_field`](Self::return_field) projection is given).
522    pub fn returning(mut self, r: Returning) -> Self {
523        self.returning = r;
524        self
525    }
526    pub fn data(&self) -> &[T] {
527        &self.data
528    }
529
530    /// Render `INSERT INTO <table> <object|array> [RETURN …]`, serializing the
531    /// queued record(s) inline as SurrealQL object literals (JSON is a valid
532    /// subset). A single record renders as `{ … }`, multiple as `[ {…}, {…} ]`.
533    /// A `RETURN` projection (from [`return_field`](Self::return_field)) renders
534    /// the field list; otherwise the [`returning`](Self::returning) variant.
535    pub fn to_surrealql(&self) -> String
536    where
537        T: serde::Serialize,
538    {
539        let body = match self.data.as_slice() {
540            [] => "[]".to_string(),
541            [one] => serde_json::to_string(one).unwrap_or_else(|_| "{}".to_string()),
542            many => serde_json::to_string(many).unwrap_or_else(|_| "[]".to_string()),
543        };
544        let mut q = format!("INSERT INTO {} {}", T::table_name(), body);
545        if !self.return_fields.is_empty() {
546            q.push_str(" RETURN ");
547            q.push_str(&self.return_fields.join(", "));
548        } else {
549            self.returning.render(&mut q);
550        }
551        q
552    }
553}
554
555// ═══════════════════════════════════════════════════════════════════════════════
556// UPDATE
557// ═══════════════════════════════════════════════════════════════════════════════
558
559enum SetVal {
560    /// `SET k = <expr>`
561    Assign(String, Box<dyn DynExpr>),
562    /// `MERGE <expr>`
563    Merge(Box<dyn DynExpr>),
564    /// `CONTENT <expr>` (full replace)
565    Content(Box<dyn DynExpr>),
566}
567
568impl SetVal {
569    fn render(&self, buf: &mut String, set_pairs: &mut Vec<String>) {
570        match self {
571            SetVal::Assign(k, v) => {
572                let mut val_buf = String::new();
573                v.render_dyn(&mut val_buf);
574                set_pairs.push(format!("{k} = {val_buf}"));
575            }
576            SetVal::Merge(v) => {
577                let mut val_buf = String::new();
578                v.render_dyn(&mut val_buf);
579                buf.push_str(" MERGE ");
580                buf.push_str(&val_buf);
581            }
582            SetVal::Content(v) => {
583                let mut val_buf = String::new();
584                v.render_dyn(&mut val_buf);
585                buf.push_str(" CONTENT ");
586                buf.push_str(&val_buf);
587            }
588        }
589    }
590    fn render_params(
591        &self,
592        buf: &mut String,
593        set_pairs: &mut Vec<String>,
594        params: &mut BTreeMap<String, serde_json::Value>,
595    ) {
596        match self {
597            SetVal::Assign(k, v) => {
598                let mut val_buf = String::new();
599                v.render_dyn_params(&mut val_buf, params);
600                set_pairs.push(format!("{k} = {val_buf}"));
601            }
602            SetVal::Merge(v) => {
603                let mut val_buf = String::new();
604                v.render_dyn_params(&mut val_buf, params);
605                buf.push_str(" MERGE ");
606                buf.push_str(&val_buf);
607            }
608            SetVal::Content(v) => {
609                let mut val_buf = String::new();
610                v.render_dyn_params(&mut val_buf, params);
611                buf.push_str(" CONTENT ");
612                buf.push_str(&val_buf);
613            }
614        }
615    }
616}
617
618/// An `UPDATE`/`UPSERT` builder: `SET` / `MERGE` / `CONTENT`, an optional `WHERE`,
619/// and `RETURN`. Built via [`Table::update`] or [`Table::upsert`].
620pub struct Update<T: SurrealRecord> {
621    _marker: std::marker::PhantomData<T>,
622    verb: &'static str,
623    target: Target,
624    filter: Option<Box<dyn DynExpr>>,
625    sets: Vec<SetVal>,
626    returning: Returning,
627}
628
629impl<T: SurrealRecord> Update<T> {
630    pub(crate) fn for_table() -> Self {
631        Self::with_verb("UPDATE")
632    }
633
634    /// An `UPSERT` statement — same builder surface as `UPDATE`, but creates the
635    /// record if it doesn't exist. Built via [`Table::upsert`].
636    pub(crate) fn for_upsert() -> Self {
637        Self::with_verb("UPSERT")
638    }
639
640    fn with_verb(verb: &'static str) -> Self {
641        Self {
642            _marker: std::marker::PhantomData,
643            verb,
644            target: Target::Table(T::table_name()),
645            filter: None,
646            sets: Vec::new(),
647            returning: Returning::None,
648        }
649    }
650
651    /// Target a single record: `UPDATE type::record('table', <id>)`.
652    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
653        self.target = Target::Record(RecordLink::new(T::table_name(), id));
654        self
655    }
656
657    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
658        self.filter = Some(Box::new(expr));
659        self
660    }
661
662    /// `SET col = <literal>`.
663    pub fn set<C: SurrealQL>(mut self, col: Column<T, C>, value: C) -> Self {
664        self.sets.push(SetVal::Assign(
665            col.name.to_string(),
666            Box::new(crate::expr::Literal(value)),
667        ));
668        self
669    }
670    /// `SET col = <literal>` by raw column name.
671    pub fn set_lit<C: SurrealQL>(mut self, col: impl Into<String>, value: C) -> Self {
672        self.sets.push(SetVal::Assign(
673            col.into(),
674            Box::new(crate::expr::Literal(value)),
675        ));
676        self
677    }
678    /// `SET col = <expr>` — e.g. a record link, `time::now()`, NONE, `use_count + 1`.
679    pub fn set_expr(mut self, col: impl Into<String>, expr: impl DynExpr + 'static) -> Self {
680        self.sets.push(SetVal::Assign(col.into(), Box::new(expr)));
681        self
682    }
683    /// `SET col = <raw SurrealQL>`.
684    pub fn set_raw(mut self, col: impl Into<String>, raw: impl Into<String>) -> Self {
685        self.sets.push(SetVal::Assign(
686            col.into(),
687            Box::new(crate::expr::Raw(raw.into())),
688        ));
689        self
690    }
691    /// `MERGE <expr>` — deep-merge the given object into the record.
692    pub fn merge(mut self, expr: impl DynExpr + 'static) -> Self {
693        self.sets.push(SetVal::Merge(Box::new(expr)));
694        self
695    }
696    /// `CONTENT <expr>` — full-replace the record's content (upsert by record id).
697    pub fn content(mut self, expr: impl DynExpr + 'static) -> Self {
698        self.sets.push(SetVal::Content(Box::new(expr)));
699        self
700    }
701    pub fn returning(mut self, r: Returning) -> Self {
702        self.returning = r;
703        self
704    }
705
706    /// Follow this `UPDATE` with a reselecting [`Select`], joined as a `;`-separated
707    /// batch. See [`Create::then_select`] for motivation.
708    pub fn then_select(self, select: Select<T>) -> String {
709        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
710    }
711
712    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
713    pub fn then_select_params(
714        self,
715        select: Select<T>,
716    ) -> (String, BTreeMap<String, serde_json::Value>) {
717        let (mut_q, mut params) = self.to_surrealql_with_params();
718        let (sel_q, sel_params) = select.to_surrealql_with_params();
719        params.extend(sel_params);
720        (format!("{mut_q};\n{sel_q}"), params)
721    }
722
723    pub fn to_surrealql(&self) -> String {
724        let mut q = String::from(self.verb);
725        q.push(' ');
726        self.target.render(&mut q);
727        // SurrealQL order: SET/MERGE/CONTENT first, then WHERE, then RETURN.
728        let mut set_pairs = Vec::new();
729        let mut trait_buf = String::new();
730        for s in &self.sets {
731            s.render(&mut trait_buf, &mut set_pairs);
732        }
733        if !trait_buf.is_empty() {
734            q.push_str(&trait_buf);
735        } else if !set_pairs.is_empty() {
736            q.push_str(" SET ");
737            q.push_str(&set_pairs.join(", "));
738        }
739        if let Some(ref f) = self.filter {
740            q.push_str(" WHERE ");
741            f.render_dyn(&mut q);
742        }
743        self.returning.render(&mut q);
744        q
745    }
746
747    /// Render with `$param` placeholders instead of inlined literals.
748    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
749        let mut params = BTreeMap::new();
750        let mut q = String::from(self.verb);
751        q.push(' ');
752        self.target.render_params(&mut q, &mut params);
753        let mut set_pairs = Vec::new();
754        let mut trait_buf = String::new();
755        for s in &self.sets {
756            s.render_params(&mut trait_buf, &mut set_pairs, &mut params);
757        }
758        if !trait_buf.is_empty() {
759            q.push_str(&trait_buf);
760        } else if !set_pairs.is_empty() {
761            q.push_str(" SET ");
762            q.push_str(&set_pairs.join(", "));
763        }
764        if let Some(ref f) = self.filter {
765            q.push_str(" WHERE ");
766            f.render_dyn_params(&mut q, &mut params);
767        }
768        self.returning.render(&mut q);
769        (q, params)
770    }
771}
772
773impl<T: SurrealRecord> std::fmt::Display for Update<T> {
774    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
775        write!(f, "{}", self.to_surrealql())
776    }
777}
778
779// ═══════════════════════════════════════════════════════════════════════════════
780// CREATE
781// ═══════════════════════════════════════════════════════════════════════════════
782
783enum CreateBody {
784    /// `CONTENT <expr>`
785    Content(Box<dyn DynExpr>),
786    /// `SET a = x, b = y`
787    Set(Vec<(String, Box<dyn DynExpr>)>),
788}
789
790/// `CREATE <target> [CONTENT … | SET …] [RETURN …]`.
791pub struct Create<T: SurrealRecord> {
792    _marker: std::marker::PhantomData<T>,
793    target: Target,
794    body: CreateBody,
795    returning: Returning,
796}
797
798impl<T: SurrealRecord> Create<T> {
799    pub(crate) fn for_table() -> Self {
800        Self {
801            _marker: std::marker::PhantomData,
802            target: Target::Table(T::table_name()),
803            body: CreateBody::Set(Vec::new()),
804            returning: Returning::None,
805        }
806    }
807
808    /// Target a single record id: `CREATE type::record('table', <id>)`.
809    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
810        self.target = Target::Record(RecordLink::new(T::table_name(), id));
811        self
812    }
813
814    /// `CONTENT <expr>` — replaces any accumulated SET pairs.
815    pub fn content(mut self, expr: impl DynExpr + 'static) -> Self {
816        self.body = CreateBody::Content(Box::new(expr));
817        self
818    }
819
820    /// `SET col = <literal>`.
821    pub fn set_lit<C: SurrealQL>(mut self, col: impl Into<String>, value: C) -> Self {
822        self.push_set(col.into(), Box::new(crate::expr::Literal(value)));
823        self
824    }
825    /// `SET col = <expr>`.
826    pub fn set_expr(mut self, col: impl Into<String>, expr: impl DynExpr + 'static) -> Self {
827        self.push_set(col.into(), Box::new(expr));
828        self
829    }
830    /// `SET col = <raw SurrealQL>`.
831    pub fn set_raw(mut self, col: impl Into<String>, raw: impl Into<String>) -> Self {
832        self.push_set(col.into(), Box::new(crate::expr::Raw(raw.into())));
833        self
834    }
835
836    fn push_set(&mut self, col: String, expr: Box<dyn DynExpr>) {
837        match &mut self.body {
838            CreateBody::Set(v) => v.push((col, expr)),
839            CreateBody::Content(_) => {
840                self.body = CreateBody::Set(vec![(col, expr)]);
841            }
842        }
843    }
844
845    pub fn returning(mut self, r: Returning) -> Self {
846        self.returning = r;
847        self
848    }
849
850    /// Follow this `CREATE` with a reselecting [`Select`], joined as a `;`-separated
851    /// batch. The select is rendered immediately, producing a complete SurrealQL
852    /// string ready for `db.query()`.
853    ///
854    /// This replaces the manual `Batch::new().push(create).push(select).to_surrealql()`
855    /// pattern for mutate-then-reselect workflows.
856    pub fn then_select(self, select: Select<T>) -> String {
857        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
858    }
859
860    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
861    pub fn then_select_params(
862        self,
863        select: Select<T>,
864    ) -> (String, BTreeMap<String, serde_json::Value>) {
865        let (mut_q, mut params) = self.to_surrealql_with_params();
866        let (sel_q, sel_params) = select.to_surrealql_with_params();
867        params.extend(sel_params);
868        (format!("{mut_q};\n{sel_q}"), params)
869    }
870
871    pub fn to_surrealql(&self) -> String {
872        let mut q = String::from("CREATE ");
873        self.target.render(&mut q);
874        match &self.body {
875            CreateBody::Content(c) => {
876                q.push_str(" CONTENT ");
877                c.render_dyn(&mut q);
878            }
879            CreateBody::Set(pairs) if !pairs.is_empty() => {
880                q.push_str(" SET ");
881                q.push_str(
882                    &pairs
883                        .iter()
884                        .map(|(k, v)| {
885                            let mut val = String::new();
886                            v.render_dyn(&mut val);
887                            format!("{k} = {val}")
888                        })
889                        .collect::<Vec<_>>()
890                        .join(", "),
891                );
892            }
893            CreateBody::Set(_) => {}
894        }
895        self.returning.render(&mut q);
896        q
897    }
898
899    /// Render with `$param` placeholders instead of inlined literals.
900    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
901        let mut params = BTreeMap::new();
902        let mut q = String::from("CREATE ");
903        self.target.render_params(&mut q, &mut params);
904        match &self.body {
905            CreateBody::Content(c) => {
906                q.push_str(" CONTENT ");
907                c.render_dyn_params(&mut q, &mut params);
908            }
909            CreateBody::Set(pairs) if !pairs.is_empty() => {
910                q.push_str(" SET ");
911                q.push_str(
912                    &pairs
913                        .iter()
914                        .map(|(k, v)| {
915                            let mut val = String::new();
916                            v.render_dyn_params(&mut val, &mut params);
917                            format!("{k} = {val}")
918                        })
919                        .collect::<Vec<_>>()
920                        .join(", "),
921                );
922            }
923            CreateBody::Set(_) => {}
924        }
925        self.returning.render(&mut q);
926        (q, params)
927    }
928}
929
930impl<T: SurrealRecord> std::fmt::Display for Create<T> {
931    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
932        write!(f, "{}", self.to_surrealql())
933    }
934}
935
936// ═══════════════════════════════════════════════════════════════════════════════
937// DELETE
938// ═══════════════════════════════════════════════════════════════════════════════
939
940/// A `DELETE <target> [WHERE …] [RETURN …]` builder.
941pub struct Delete<T: SurrealRecord> {
942    _marker: std::marker::PhantomData<T>,
943    target: Target,
944    filter: Option<Box<dyn DynExpr>>,
945    returning: Returning,
946}
947
948impl<T: SurrealRecord> Delete<T> {
949    pub(crate) fn for_table() -> Self {
950        Self {
951            _marker: std::marker::PhantomData,
952            target: Target::Table(T::table_name()),
953            filter: None,
954            returning: Returning::None,
955        }
956    }
957    /// Target a single record: `DELETE type::record('table', <id>)`.
958    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
959        self.target = Target::Record(RecordLink::new(T::table_name(), id));
960        self
961    }
962    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
963        self.filter = Some(Box::new(expr));
964        self
965    }
966    pub fn returning(mut self, r: Returning) -> Self {
967        self.returning = r;
968        self
969    }
970
971    /// Follow this `DELETE` with a reselecting [`Select`], joined as a `;`-separated
972    /// batch. See [`Create::then_select`] for motivation.
973    pub fn then_select(self, select: Select<T>) -> String {
974        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
975    }
976
977    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
978    pub fn then_select_params(
979        self,
980        select: Select<T>,
981    ) -> (String, BTreeMap<String, serde_json::Value>) {
982        let (mut_q, mut params) = self.to_surrealql_with_params();
983        let (sel_q, sel_params) = select.to_surrealql_with_params();
984        params.extend(sel_params);
985        (format!("{mut_q};\n{sel_q}"), params)
986    }
987
988    pub fn to_surrealql(&self) -> String {
989        let mut q = String::from("DELETE ");
990        self.target.render(&mut q);
991        if let Some(ref f) = self.filter {
992            q.push_str(" WHERE ");
993            f.render_dyn(&mut q);
994        }
995        self.returning.render(&mut q);
996        q
997    }
998
999    /// Render with `$param` placeholders instead of inlined literals.
1000    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1001        let mut params = BTreeMap::new();
1002        let mut q = String::from("DELETE ");
1003        self.target.render_params(&mut q, &mut params);
1004        if let Some(ref f) = self.filter {
1005            q.push_str(" WHERE ");
1006            f.render_dyn_params(&mut q, &mut params);
1007        }
1008        self.returning.render(&mut q);
1009        (q, params)
1010    }
1011}
1012
1013impl<T: SurrealRecord> std::fmt::Display for Delete<T> {
1014    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1015        write!(f, "{}", self.to_surrealql())
1016    }
1017}
1018
1019// ═══════════════════════════════════════════════════════════════════════════════
1020// Batch — multiple statements joined by `;` (mutate-then-reselect pattern)
1021// ═══════════════════════════════════════════════════════════════════════════════
1022
1023/// Concatenates SurrealQL statements with `;` separators. The store's typical
1024/// pattern is a mutation followed by a SELECT that re-projects the row.
1025#[derive(Default)]
1026pub struct Batch {
1027    statements: Vec<String>,
1028}
1029
1030impl Batch {
1031    pub fn new() -> Self {
1032        Self {
1033            statements: Vec::new(),
1034        }
1035    }
1036    pub fn push(mut self, stmt: impl ToString) -> Self {
1037        self.statements.push(stmt.to_string());
1038        self
1039    }
1040    pub fn to_surrealql(&self) -> String {
1041        self.statements.join(";\n")
1042    }
1043    /// Number of statements (useful for `.take(n)` indexing on the response).
1044    pub fn len(&self) -> usize {
1045        self.statements.len()
1046    }
1047    pub fn is_empty(&self) -> bool {
1048        self.statements.is_empty()
1049    }
1050}
1051
1052impl std::fmt::Display for Batch {
1053    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1054        write!(f, "{}", self.to_surrealql())
1055    }
1056}
1057
1058// ═══════════════════════════════════════════════════════════════════════════════
1059// Transaction — BEGIN … COMMIT/CANCEL (atomic multi-statement)
1060// ═══════════════════════════════════════════════════════════════════════════════
1061
1062/// Wraps statements in a SurrealDB transaction —
1063/// `BEGIN TRANSACTION; … ; COMMIT TRANSACTION;`. Either every statement applies
1064/// or none do: SurrealDB rolls the whole block back if any statement errors, and
1065/// [`cancel`](Self::cancel) terminates with `CANCEL TRANSACTION` to roll back
1066/// explicitly. Unlike [`Batch`] (a plain `;`-joined sequence), a transaction is
1067/// atomic.
1068///
1069/// Push already-rendered statements (`to_surrealql()` output); each is
1070/// `;`-terminated automatically.
1071#[derive(Default)]
1072pub struct Transaction {
1073    statements: Vec<String>,
1074    cancel: bool,
1075}
1076
1077impl Transaction {
1078    pub fn new() -> Self {
1079        Self::default()
1080    }
1081    /// Add a statement to the transaction body.
1082    pub fn push(mut self, stmt: impl ToString) -> Self {
1083        self.statements.push(stmt.to_string());
1084        self
1085    }
1086    /// Terminate with `CANCEL TRANSACTION` (roll back) instead of `COMMIT`.
1087    pub fn cancel(mut self) -> Self {
1088        self.cancel = true;
1089        self
1090    }
1091    pub fn to_surrealql(&self) -> String {
1092        let mut out = String::from("BEGIN TRANSACTION;\n");
1093        for s in &self.statements {
1094            out.push_str(s);
1095            if !s.trim_end().ends_with(';') {
1096                out.push(';');
1097            }
1098            out.push('\n');
1099        }
1100        out.push_str(if self.cancel {
1101            "CANCEL TRANSACTION;"
1102        } else {
1103            "COMMIT TRANSACTION;"
1104        });
1105        out
1106    }
1107    /// Number of statements in the transaction body (excludes BEGIN/COMMIT).
1108    pub fn len(&self) -> usize {
1109        self.statements.len()
1110    }
1111    pub fn is_empty(&self) -> bool {
1112        self.statements.is_empty()
1113    }
1114}
1115
1116impl std::fmt::Display for Transaction {
1117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1118        write!(f, "{}", self.to_surrealql())
1119    }
1120}
1121
1122// ═══════════════════════════════════════════════════════════════════════════════
1123// RELATE — graph edges
1124// ═══════════════════════════════════════════════════════════════════════════════
1125
1126/// Render a record's id as `table:<escaped-key>` into `buf`.
1127fn record_id(thing: &Thing<impl SurrealRecord>, buf: &mut String) {
1128    buf.push_str(thing.table());
1129    buf.push(':');
1130    thing.key.render_id(buf);
1131}
1132
1133/// Return a record's id as a `table:<escaped-key>` string.
1134fn record_id_string(thing: &Thing<impl SurrealRecord>) -> String {
1135    let mut s = String::new();
1136    record_id(thing, &mut s);
1137    s
1138}
1139
1140/// Builds a graph edge statement `RELATE a -> edge -> b` for an edge type `E`.
1141/// For edges that carry their own fields, see [`RelateEdge`].
1142pub struct Relate<E: SurrealEdge> {
1143    _marker: std::marker::PhantomData<E>,
1144}
1145
1146impl<E: SurrealEdge> Relate<E> {
1147    pub fn new() -> Self {
1148        Self {
1149            _marker: std::marker::PhantomData,
1150        }
1151    }
1152
1153    pub fn to_surrealql(
1154        from: &Thing<impl SurrealRecord>,
1155        to: &Thing<impl SurrealRecord>,
1156    ) -> String {
1157        let mut q = String::from("RELATE ");
1158        record_id(from, &mut q);
1159        q.push_str(" -> ");
1160        q.push_str(E::edge_name());
1161        q.push_str(" -> ");
1162        record_id(to, &mut q);
1163        q
1164    }
1165}
1166
1167impl<E: SurrealEdge> Default for Relate<E> {
1168    fn default() -> Self {
1169        Self::new()
1170    }
1171}
1172
1173// ═══════════════════════════════════════════════════════════════════════════════
1174// RELATE with content
1175// ═══════════════════════════════════════════════════════════════════════════════
1176
1177/// Build a RELATE query with edge content.
1178///
1179/// ```ignore
1180/// RelateEdge::<Follows>::from(user).to(other).content(Follows { since: now }).build()
1181/// ```
1182pub struct RelateEdge<E: SurrealEdge> {
1183    _marker: std::marker::PhantomData<E>,
1184    from_label: String,
1185    to_label: String,
1186    content_json: Option<serde_json::Value>,
1187    return_fields: Vec<&'static str>,
1188    returning: Returning,
1189}
1190
1191impl<E: SurrealEdge> RelateEdge<E> {
1192    pub fn from(from: &Thing<impl SurrealRecord>) -> Self {
1193        Self {
1194            _marker: std::marker::PhantomData,
1195            from_label: record_id_string(from),
1196            to_label: String::new(),
1197            content_json: None,
1198            return_fields: Vec::new(),
1199            returning: Returning::None,
1200        }
1201    }
1202
1203    pub fn to(mut self, to: &Thing<impl SurrealRecord>) -> Self {
1204        self.to_label = record_id_string(to);
1205        self
1206    }
1207
1208    /// Attach content to the edge record.
1209    pub fn content(mut self, edge: &impl serde::Serialize) -> Self {
1210        self.content_json = serde_json::to_value(edge).ok();
1211        self
1212    }
1213
1214    /// Add a field to the `RETURN <projection>` list (e.g. `RETURN id`). Multiple
1215    /// calls accumulate; takes precedence over [`returning`](Self::returning).
1216    pub fn return_field(mut self, field: &'static str) -> Self {
1217        self.return_fields.push(field);
1218        self
1219    }
1220    /// Set a `RETURN NONE|BEFORE|AFTER|DIFF` clause on the edge creation.
1221    pub fn returning(mut self, r: Returning) -> Self {
1222        self.returning = r;
1223        self
1224    }
1225
1226    pub fn build(&self) -> String {
1227        let mut q = format!(
1228            "RELATE {} -> {} -> {}",
1229            self.from_label,
1230            E::edge_name(),
1231            self.to_label
1232        );
1233        if let Some(ref c) = self.content_json {
1234            q.push_str(&format!(
1235                " CONTENT {}",
1236                serde_json::to_string(c).unwrap_or_default()
1237            ));
1238        }
1239        if !self.return_fields.is_empty() {
1240            q.push_str(" RETURN ");
1241            q.push_str(&self.return_fields.join(", "));
1242        } else {
1243            self.returning.render(&mut q);
1244        }
1245        q
1246    }
1247}
1248
1249// ═══════════════════════════════════════════════════════════════════════════════
1250// LET — session-scoped variable assignment
1251// ═══════════════════════════════════════════════════════════════════════════════
1252
1253/// Builds a `LET $var = <expr>` statement for session-scoped variables.
1254/// The variable is available in subsequent queries within the same session.
1255///
1256/// ```ignore
1257/// LetVar::new("limit", 10u32).to_surrealql();      // LET $limit = 10;
1258/// LetVar::new("ts", Raw("time::now()")).to_surrealql(); // LET $ts = time::now();
1259/// ```
1260pub struct LetVar {
1261    name: String,
1262    value: Box<dyn DynExpr>,
1263}
1264
1265impl LetVar {
1266    /// Create a `LET $name = <expr>` statement.
1267    pub fn new(name: impl Into<String>, value: impl DynExpr + 'static) -> Self {
1268        Self {
1269            name: name.into(),
1270            value: Box::new(value),
1271        }
1272    }
1273
1274    /// Create a `LET $name = <literal>` statement.
1275    pub fn literal<V: SurrealQL>(name: impl Into<String>, value: V) -> Self {
1276        Self {
1277            name: name.into(),
1278            value: Box::new(crate::expr::Literal(value)),
1279        }
1280    }
1281
1282    pub fn to_surrealql(&self) -> String {
1283        let mut q = format!("LET ${} = ", self.name);
1284        self.value.render_dyn(&mut q);
1285        q
1286    }
1287
1288    /// Render with `$param` placeholders (the `LET` value becomes a `$param`).
1289    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1290        let mut params = BTreeMap::new();
1291        let mut q = format!("LET ${} = ", self.name);
1292        self.value.render_dyn_params(&mut q, &mut params);
1293        (q, params)
1294    }
1295}
1296
1297impl std::fmt::Display for LetVar {
1298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1299        write!(f, "{}", self.to_surrealql())
1300    }
1301}
1302
1303// ═══════════════════════════════════════════════════════════════════════════════
1304// FOR — iterate over an array, running a body per element
1305// ═══════════════════════════════════════════════════════════════════════════════
1306
1307/// Builds a `FOR $<var> IN <array> { <body> }` loop. The loop variable `$<var>`
1308/// is bound inside the body; push one or more statements as the body.
1309///
1310/// ```ignore
1311/// For::new("n", Raw("[1, 2, 3]".into()))
1312///     .push("CREATE counter SET v = $n")
1313///     .to_surrealql();
1314/// // FOR $n IN [1, 2, 3] { CREATE counter SET v = $n; }
1315/// ```
1316pub struct For {
1317    var: String,
1318    array: Box<dyn DynExpr>,
1319    body: Vec<String>,
1320}
1321
1322impl For {
1323    /// `FOR $<var> IN <array>` — the array is any expression (a literal array, a
1324    /// `$param`, a subquery, …).
1325    pub fn new(var: impl Into<String>, array: impl DynExpr + 'static) -> Self {
1326        Self {
1327            var: var.into(),
1328            array: Box::new(array),
1329            body: Vec::new(),
1330        }
1331    }
1332    /// Add a statement to the loop body.
1333    pub fn push(mut self, stmt: impl Into<String>) -> Self {
1334        self.body.push(stmt.into());
1335        self
1336    }
1337    pub fn to_surrealql(&self) -> String {
1338        let mut q = format!("FOR ${} IN ", self.var);
1339        self.array.render_dyn(&mut q);
1340        q.push_str(" { ");
1341        for s in &self.body {
1342            q.push_str(s);
1343            if !s.trim_end().ends_with(';') {
1344                q.push(';');
1345            }
1346            q.push(' ');
1347        }
1348        q.push('}');
1349        q
1350    }
1351}
1352
1353impl std::fmt::Display for For {
1354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1355        write!(f, "{}", self.to_surrealql())
1356    }
1357}
1358
1359// ═══════════════════════════════════════════════════════════════════════════════
1360// DEFINE INDEX
1361// ═══════════════════════════════════════════════════════════════════════════════
1362
1363/// The kind of a `DEFINE INDEX` — what trails the field list.
1364enum IndexKind {
1365    /// a plain (non-unique) index — no trailing clause
1366    Plain,
1367    /// `UNIQUE`
1368    Unique,
1369    /// a verbatim trailing clause, e.g. `SEARCH ANALYZER ascii BM25 HIGHLIGHTS`
1370    /// or `HNSW DIMENSION 128 DIST COSINE` — the escape hatch for full-text and
1371    /// vector indexes whose exact options depend on the engine build.
1372    Raw(String),
1373}
1374
1375/// Builds a `DEFINE INDEX` statement — plain, composite, `UNIQUE`, full-text
1376/// (`SEARCH`), or vector (`HNSW`/`MTREE`) indexes.
1377///
1378/// ```ignore
1379/// // DEFINE INDEX IF NOT EXISTS email_idx ON TABLE user FIELDS email UNIQUE
1380/// DefineIndex::new("email_idx", "user").field("email").unique().to_surrealql();
1381///
1382/// // composite, vector
1383/// DefineIndex::new("name_idx", "user").fields(["first", "last"]).to_surrealql();
1384/// DefineIndex::new("emb_idx", "doc").field("embedding").hnsw(128, "COSINE").to_surrealql();
1385/// ```
1386pub struct DefineIndex {
1387    name: String,
1388    table: String,
1389    fields: Vec<String>,
1390    kind: IndexKind,
1391    if_not_exists: bool,
1392    comment: Option<String>,
1393    concurrently: bool,
1394}
1395
1396impl DefineIndex {
1397    /// Begin `DEFINE INDEX IF NOT EXISTS <name> ON TABLE <table>`.
1398    pub fn new(name: impl Into<String>, table: impl Into<String>) -> Self {
1399        Self {
1400            name: name.into(),
1401            table: table.into(),
1402            fields: Vec::new(),
1403            kind: IndexKind::Plain,
1404            if_not_exists: true,
1405            comment: None,
1406            concurrently: false,
1407        }
1408    }
1409
1410    /// Add one indexed field/column.
1411    pub fn field(mut self, name: impl Into<String>) -> Self {
1412        self.fields.push(name.into());
1413        self
1414    }
1415    /// Add several indexed fields/columns (a composite index).
1416    pub fn fields<I, S>(mut self, names: I) -> Self
1417    where
1418        I: IntoIterator<Item = S>,
1419        S: Into<String>,
1420    {
1421        self.fields.extend(names.into_iter().map(Into::into));
1422        self
1423    }
1424
1425    /// Mark the index `UNIQUE`.
1426    pub fn unique(mut self) -> Self {
1427        self.kind = IndexKind::Unique;
1428        self
1429    }
1430    /// A full-text `SEARCH ANALYZER <analyzer>` index. Append further options
1431    /// (`BM25`, `HIGHLIGHTS`, …) with [`raw`](Self::raw) if your engine needs them.
1432    pub fn search(mut self, analyzer: impl Into<String>) -> Self {
1433        self.kind = IndexKind::Raw(format!("SEARCH ANALYZER {}", analyzer.into()));
1434        self
1435    }
1436    /// An `HNSW` vector index of the given dimension and distance function
1437    /// (e.g. `"COSINE"`, `"EUCLIDEAN"`).
1438    pub fn hnsw(mut self, dimension: u32, dist: impl Into<String>) -> Self {
1439        self.kind = IndexKind::Raw(format!("HNSW DIMENSION {dimension} DIST {}", dist.into()));
1440        self
1441    }
1442    /// An `MTREE` vector index of the given dimension and distance function.
1443    pub fn mtree(mut self, dimension: u32, dist: impl Into<String>) -> Self {
1444        self.kind = IndexKind::Raw(format!("MTREE DIMENSION {dimension} DIST {}", dist.into()));
1445        self
1446    }
1447    /// Set a verbatim trailing clause (the escape hatch for index options somnia
1448    /// doesn't model), e.g. `"SEARCH ANALYZER ascii BM25 HIGHLIGHTS"`.
1449    pub fn raw(mut self, tail: impl Into<String>) -> Self {
1450        self.kind = IndexKind::Raw(tail.into());
1451        self
1452    }
1453
1454    /// Drop the `IF NOT EXISTS` guard.
1455    pub fn overwrite(mut self) -> Self {
1456        self.if_not_exists = false;
1457        self
1458    }
1459    /// Attach a `COMMENT '<text>'`.
1460    pub fn comment(mut self, text: impl Into<String>) -> Self {
1461        self.comment = Some(text.into());
1462        self
1463    }
1464    /// Build the index `CONCURRENTLY` (non-blocking).
1465    pub fn concurrently(mut self) -> Self {
1466        self.concurrently = true;
1467        self
1468    }
1469
1470    pub fn to_surrealql(&self) -> String {
1471        let guard = if self.if_not_exists {
1472            "IF NOT EXISTS "
1473        } else {
1474            ""
1475        };
1476        let mut q = format!(
1477            "DEFINE INDEX {guard}{} ON TABLE {} FIELDS {}",
1478            self.name,
1479            self.table,
1480            self.fields.join(", "),
1481        );
1482        match &self.kind {
1483            IndexKind::Plain => {}
1484            IndexKind::Unique => q.push_str(" UNIQUE"),
1485            IndexKind::Raw(tail) => {
1486                q.push(' ');
1487                q.push_str(tail);
1488            }
1489        }
1490        if let Some(c) = &self.comment {
1491            let escaped = c.replace('\\', "\\\\").replace('\'', "\\'");
1492            q.push_str(&format!(" COMMENT '{escaped}'"));
1493        }
1494        if self.concurrently {
1495            q.push_str(" CONCURRENTLY");
1496        }
1497        q
1498    }
1499
1500    /// `REMOVE INDEX IF EXISTS <name> ON TABLE <table>` — the inverse statement.
1501    pub fn remove(name: &str, table: &str) -> String {
1502        format!("REMOVE INDEX IF EXISTS {name} ON TABLE {table}")
1503    }
1504}
1505
1506impl std::fmt::Display for DefineIndex {
1507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1508        write!(f, "{}", self.to_surrealql())
1509    }
1510}
1511
1512// ═══════════════════════════════════════════════════════════════════════════════
1513// DEFINE EVENT / FUNCTION / ANALYZER / PARAM
1514// ═══════════════════════════════════════════════════════════════════════════════
1515
1516fn guard(if_not_exists: bool) -> &'static str {
1517    if if_not_exists {
1518        "IF NOT EXISTS "
1519    } else {
1520        ""
1521    }
1522}
1523
1524/// `DEFINE EVENT <name> ON TABLE <table> WHEN <cond> THEN <block>` — a trigger
1525/// that fires on `CREATE`/`UPDATE`/`DELETE`. `$event`, `$before`, `$after`,
1526/// `$value` are available inside `when`/`then`.
1527///
1528/// ```ignore
1529/// DefineEvent::new("on_publish", "post")
1530///     .when("$event = 'UPDATE' AND $after.published = true")
1531///     .then("{ CREATE notification SET post = $after.id }")
1532///     .to_surrealql();
1533/// ```
1534pub struct DefineEvent {
1535    name: String,
1536    table: String,
1537    when: String,
1538    then: String,
1539    if_not_exists: bool,
1540}
1541
1542impl DefineEvent {
1543    pub fn new(name: impl Into<String>, table: impl Into<String>) -> Self {
1544        Self {
1545            name: name.into(),
1546            table: table.into(),
1547            when: String::new(),
1548            then: String::new(),
1549            if_not_exists: true,
1550        }
1551    }
1552    /// The `WHEN <condition>` guard (raw SurrealQL).
1553    pub fn when(mut self, cond: impl Into<String>) -> Self {
1554        self.when = cond.into();
1555        self
1556    }
1557    /// The `THEN <block>` body (raw SurrealQL, typically a `{ … }` block).
1558    pub fn then(mut self, block: impl Into<String>) -> Self {
1559        self.then = block.into();
1560        self
1561    }
1562    /// Drop the `IF NOT EXISTS` guard.
1563    pub fn overwrite(mut self) -> Self {
1564        self.if_not_exists = false;
1565        self
1566    }
1567    pub fn to_surrealql(&self) -> String {
1568        format!(
1569            "DEFINE EVENT {}{} ON TABLE {} WHEN {} THEN {}",
1570            guard(self.if_not_exists),
1571            self.name,
1572            self.table,
1573            self.when,
1574            self.then
1575        )
1576    }
1577    /// `REMOVE EVENT IF EXISTS <name> ON TABLE <table>`.
1578    pub fn remove(name: &str, table: &str) -> String {
1579        format!("REMOVE EVENT IF EXISTS {name} ON TABLE {table}")
1580    }
1581}
1582
1583impl std::fmt::Display for DefineEvent {
1584    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1585        write!(f, "{}", self.to_surrealql())
1586    }
1587}
1588
1589/// `DEFINE FUNCTION fn::<name>(<args>) -> <ret> { <body> }` — a user-defined
1590/// SurrealQL function. The `fn::` prefix is added automatically.
1591///
1592/// ```ignore
1593/// DefineFunction::new("greet")
1594///     .arg("name", "string")
1595///     .returns("string")
1596///     .body("RETURN 'hi ' + $name;")
1597///     .to_surrealql();
1598/// ```
1599pub struct DefineFunction {
1600    name: String,
1601    args: Vec<(String, String)>,
1602    returns: Option<String>,
1603    body: String,
1604    if_not_exists: bool,
1605}
1606
1607impl DefineFunction {
1608    pub fn new(name: impl Into<String>) -> Self {
1609        Self {
1610            name: name.into(),
1611            args: Vec::new(),
1612            returns: None,
1613            body: String::new(),
1614            if_not_exists: true,
1615        }
1616    }
1617    /// Add a typed argument — `$name: type`.
1618    pub fn arg(mut self, name: impl Into<String>, ty: impl Into<String>) -> Self {
1619        self.args.push((name.into(), ty.into()));
1620        self
1621    }
1622    /// Declared return type (`-> <ty>`).
1623    pub fn returns(mut self, ty: impl Into<String>) -> Self {
1624        self.returns = Some(ty.into());
1625        self
1626    }
1627    /// The function body (raw SurrealQL statements, e.g. `RETURN …;`).
1628    pub fn body(mut self, body: impl Into<String>) -> Self {
1629        self.body = body.into();
1630        self
1631    }
1632    pub fn overwrite(mut self) -> Self {
1633        self.if_not_exists = false;
1634        self
1635    }
1636    pub fn to_surrealql(&self) -> String {
1637        let args = self
1638            .args
1639            .iter()
1640            .map(|(n, t)| format!("${n}: {t}"))
1641            .collect::<Vec<_>>()
1642            .join(", ");
1643        let ret = self
1644            .returns
1645            .as_ref()
1646            .map(|r| format!(" -> {r}"))
1647            .unwrap_or_default();
1648        format!(
1649            "DEFINE FUNCTION {}fn::{}({}){} {{ {} }}",
1650            guard(self.if_not_exists),
1651            self.name,
1652            args,
1653            ret,
1654            self.body
1655        )
1656    }
1657    /// `REMOVE FUNCTION IF EXISTS fn::<name>`.
1658    pub fn remove(name: &str) -> String {
1659        format!("REMOVE FUNCTION IF EXISTS fn::{name}")
1660    }
1661}
1662
1663impl std::fmt::Display for DefineFunction {
1664    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1665        write!(f, "{}", self.to_surrealql())
1666    }
1667}
1668
1669/// `DEFINE ANALYZER <name> TOKENIZERS <toks> FILTERS <filters>` — a full-text
1670/// tokenizer + filter pipeline (referenced by a `SEARCH` index).
1671pub struct DefineAnalyzer {
1672    name: String,
1673    tokenizers: Vec<String>,
1674    filters: Vec<String>,
1675    if_not_exists: bool,
1676}
1677
1678impl DefineAnalyzer {
1679    pub fn new(name: impl Into<String>) -> Self {
1680        Self {
1681            name: name.into(),
1682            tokenizers: Vec::new(),
1683            filters: Vec::new(),
1684            if_not_exists: true,
1685        }
1686    }
1687    /// Set the tokenizers (e.g. `["class"]`, `["blank", "punct"]`).
1688    pub fn tokenizers<I, S>(mut self, toks: I) -> Self
1689    where
1690        I: IntoIterator<Item = S>,
1691        S: Into<String>,
1692    {
1693        self.tokenizers = toks.into_iter().map(Into::into).collect();
1694        self
1695    }
1696    /// Set the filters (e.g. `["lowercase", "ascii", "snowball(english)"]`).
1697    pub fn filters<I, S>(mut self, filters: I) -> Self
1698    where
1699        I: IntoIterator<Item = S>,
1700        S: Into<String>,
1701    {
1702        self.filters = filters.into_iter().map(Into::into).collect();
1703        self
1704    }
1705    pub fn overwrite(mut self) -> Self {
1706        self.if_not_exists = false;
1707        self
1708    }
1709    pub fn to_surrealql(&self) -> String {
1710        let mut q = format!("DEFINE ANALYZER {}{}", guard(self.if_not_exists), self.name);
1711        if !self.tokenizers.is_empty() {
1712            q.push_str(" TOKENIZERS ");
1713            q.push_str(&self.tokenizers.join(", "));
1714        }
1715        if !self.filters.is_empty() {
1716            q.push_str(" FILTERS ");
1717            q.push_str(&self.filters.join(", "));
1718        }
1719        q
1720    }
1721    /// `REMOVE ANALYZER IF EXISTS <name>`.
1722    pub fn remove(name: &str) -> String {
1723        format!("REMOVE ANALYZER IF EXISTS {name}")
1724    }
1725}
1726
1727impl std::fmt::Display for DefineAnalyzer {
1728    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1729        write!(f, "{}", self.to_surrealql())
1730    }
1731}
1732
1733/// `DEFINE PARAM $<name> VALUE <value>` — a database-scoped parameter. The `$`
1734/// prefix is added automatically.
1735pub struct DefineParam {
1736    name: String,
1737    value: String,
1738    if_not_exists: bool,
1739}
1740
1741impl DefineParam {
1742    /// Begin a `DEFINE PARAM` with a raw SurrealQL value expression.
1743    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
1744        Self {
1745            name: name.into(),
1746            value: value.into(),
1747            if_not_exists: true,
1748        }
1749    }
1750    /// Set the value from a typed literal instead of a raw string.
1751    pub fn value_lit<V: SurrealQL>(mut self, value: V) -> Self {
1752        let mut buf = String::new();
1753        V::render_literal(&value, &mut buf);
1754        self.value = buf;
1755        self
1756    }
1757    pub fn overwrite(mut self) -> Self {
1758        self.if_not_exists = false;
1759        self
1760    }
1761    pub fn to_surrealql(&self) -> String {
1762        format!(
1763            "DEFINE PARAM {}${} VALUE {}",
1764            guard(self.if_not_exists),
1765            self.name,
1766            self.value
1767        )
1768    }
1769    /// `REMOVE PARAM IF EXISTS $<name>`.
1770    pub fn remove(name: &str) -> String {
1771        format!("REMOVE PARAM IF EXISTS ${name}")
1772    }
1773}
1774
1775impl std::fmt::Display for DefineParam {
1776    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1777        write!(f, "{}", self.to_surrealql())
1778    }
1779}