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` (the argument is currently ignored).
110    pub fn count(self, _field: &str) -> 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: &str) -> Self {
228        self.fetch.push(field.to_string());
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: &str) -> Self {
257        self.omit.push(field.to_string());
258        self
259    }
260    /// `SPLIT <field>` — fan one row out into multiple rows by an array field.
261    pub fn split(mut self, field: &str) -> Self {
262        self.split.push(field.to_string());
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: &str, value: C) -> Self {
672        self.sets.push(SetVal::Assign(
673            col.to_string(),
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: &str, expr: impl DynExpr + 'static) -> Self {
680        self.sets
681            .push(SetVal::Assign(col.to_string(), Box::new(expr)));
682        self
683    }
684    /// `SET col = <raw SurrealQL>`.
685    pub fn set_raw(mut self, col: &str, raw: impl Into<String>) -> Self {
686        self.sets.push(SetVal::Assign(
687            col.to_string(),
688            Box::new(crate::expr::Raw(raw.into())),
689        ));
690        self
691    }
692    /// `MERGE <expr>` — deep-merge the given object into the record.
693    pub fn merge(mut self, expr: impl DynExpr + 'static) -> Self {
694        self.sets.push(SetVal::Merge(Box::new(expr)));
695        self
696    }
697    /// `CONTENT <expr>` — full-replace the record's content (upsert by record id).
698    pub fn content(mut self, expr: impl DynExpr + 'static) -> Self {
699        self.sets.push(SetVal::Content(Box::new(expr)));
700        self
701    }
702    pub fn returning(mut self, r: Returning) -> Self {
703        self.returning = r;
704        self
705    }
706
707    /// Follow this `UPDATE` with a reselecting [`Select`], joined as a `;`-separated
708    /// batch. See [`Create::then_select`] for motivation.
709    pub fn then_select(self, select: Select<T>) -> String {
710        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
711    }
712
713    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
714    pub fn then_select_params(
715        self,
716        select: Select<T>,
717    ) -> (String, BTreeMap<String, serde_json::Value>) {
718        let (mut_q, mut params) = self.to_surrealql_with_params();
719        let (sel_q, sel_params) = select.to_surrealql_with_params();
720        params.extend(sel_params);
721        (format!("{mut_q};\n{sel_q}"), params)
722    }
723
724    pub fn to_surrealql(&self) -> String {
725        let mut q = String::from(self.verb);
726        q.push(' ');
727        self.target.render(&mut q);
728        // SurrealQL order: SET/MERGE/CONTENT first, then WHERE, then RETURN.
729        let mut set_pairs = Vec::new();
730        let mut trait_buf = String::new();
731        for s in &self.sets {
732            s.render(&mut trait_buf, &mut set_pairs);
733        }
734        if !trait_buf.is_empty() {
735            q.push_str(&trait_buf);
736        } else if !set_pairs.is_empty() {
737            q.push_str(" SET ");
738            q.push_str(&set_pairs.join(", "));
739        }
740        if let Some(ref f) = self.filter {
741            q.push_str(" WHERE ");
742            f.render_dyn(&mut q);
743        }
744        self.returning.render(&mut q);
745        q
746    }
747
748    /// Render with `$param` placeholders instead of inlined literals.
749    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
750        let mut params = BTreeMap::new();
751        let mut q = String::from(self.verb);
752        q.push(' ');
753        self.target.render_params(&mut q, &mut params);
754        let mut set_pairs = Vec::new();
755        let mut trait_buf = String::new();
756        for s in &self.sets {
757            s.render_params(&mut trait_buf, &mut set_pairs, &mut params);
758        }
759        if !trait_buf.is_empty() {
760            q.push_str(&trait_buf);
761        } else if !set_pairs.is_empty() {
762            q.push_str(" SET ");
763            q.push_str(&set_pairs.join(", "));
764        }
765        if let Some(ref f) = self.filter {
766            q.push_str(" WHERE ");
767            f.render_dyn_params(&mut q, &mut params);
768        }
769        self.returning.render(&mut q);
770        (q, params)
771    }
772}
773
774impl<T: SurrealRecord> std::fmt::Display for Update<T> {
775    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776        write!(f, "{}", self.to_surrealql())
777    }
778}
779
780// ═══════════════════════════════════════════════════════════════════════════════
781// CREATE
782// ═══════════════════════════════════════════════════════════════════════════════
783
784enum CreateBody {
785    /// `CONTENT <expr>`
786    Content(Box<dyn DynExpr>),
787    /// `SET a = x, b = y`
788    Set(Vec<(String, Box<dyn DynExpr>)>),
789}
790
791/// `CREATE <target> [CONTENT … | SET …] [RETURN …]`.
792pub struct Create<T: SurrealRecord> {
793    _marker: std::marker::PhantomData<T>,
794    target: Target,
795    body: CreateBody,
796    returning: Returning,
797}
798
799impl<T: SurrealRecord> Create<T> {
800    pub(crate) fn for_table() -> Self {
801        Self {
802            _marker: std::marker::PhantomData,
803            target: Target::Table(T::table_name()),
804            body: CreateBody::Set(Vec::new()),
805            returning: Returning::None,
806        }
807    }
808
809    /// Target a single record id: `CREATE type::record('table', <id>)`.
810    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
811        self.target = Target::Record(RecordLink::new(T::table_name(), id));
812        self
813    }
814
815    /// `CONTENT <expr>` — replaces any accumulated SET pairs.
816    pub fn content(mut self, expr: impl DynExpr + 'static) -> Self {
817        self.body = CreateBody::Content(Box::new(expr));
818        self
819    }
820
821    /// `SET col = <literal>`.
822    pub fn set_lit<C: SurrealQL>(mut self, col: &str, value: C) -> Self {
823        self.push_set(col, Box::new(crate::expr::Literal(value)));
824        self
825    }
826    /// `SET col = <expr>`.
827    pub fn set_expr(mut self, col: &str, expr: impl DynExpr + 'static) -> Self {
828        self.push_set(col, Box::new(expr));
829        self
830    }
831    /// `SET col = <raw SurrealQL>`.
832    pub fn set_raw(mut self, col: &str, raw: impl Into<String>) -> Self {
833        self.push_set(col, Box::new(crate::expr::Raw(raw.into())));
834        self
835    }
836
837    fn push_set(&mut self, col: &str, expr: Box<dyn DynExpr>) {
838        match &mut self.body {
839            CreateBody::Set(v) => v.push((col.to_string(), expr)),
840            CreateBody::Content(_) => {
841                self.body = CreateBody::Set(vec![(col.to_string(), expr)]);
842            }
843        }
844    }
845
846    pub fn returning(mut self, r: Returning) -> Self {
847        self.returning = r;
848        self
849    }
850
851    /// Follow this `CREATE` with a reselecting [`Select`], joined as a `;`-separated
852    /// batch. The select is rendered immediately, producing a complete SurrealQL
853    /// string ready for `db.query()`.
854    ///
855    /// This replaces the manual `Batch::new().push(create).push(select).to_surrealql()`
856    /// pattern for mutate-then-reselect workflows.
857    pub fn then_select(self, select: Select<T>) -> String {
858        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
859    }
860
861    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
862    pub fn then_select_params(
863        self,
864        select: Select<T>,
865    ) -> (String, BTreeMap<String, serde_json::Value>) {
866        let (mut_q, mut params) = self.to_surrealql_with_params();
867        let (sel_q, sel_params) = select.to_surrealql_with_params();
868        params.extend(sel_params);
869        (format!("{mut_q};\n{sel_q}"), params)
870    }
871
872    pub fn to_surrealql(&self) -> String {
873        let mut q = String::from("CREATE ");
874        self.target.render(&mut q);
875        match &self.body {
876            CreateBody::Content(c) => {
877                q.push_str(" CONTENT ");
878                c.render_dyn(&mut q);
879            }
880            CreateBody::Set(pairs) if !pairs.is_empty() => {
881                q.push_str(" SET ");
882                q.push_str(
883                    &pairs
884                        .iter()
885                        .map(|(k, v)| {
886                            let mut val = String::new();
887                            v.render_dyn(&mut val);
888                            format!("{k} = {val}")
889                        })
890                        .collect::<Vec<_>>()
891                        .join(", "),
892                );
893            }
894            CreateBody::Set(_) => {}
895        }
896        self.returning.render(&mut q);
897        q
898    }
899
900    /// Render with `$param` placeholders instead of inlined literals.
901    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
902        let mut params = BTreeMap::new();
903        let mut q = String::from("CREATE ");
904        self.target.render_params(&mut q, &mut params);
905        match &self.body {
906            CreateBody::Content(c) => {
907                q.push_str(" CONTENT ");
908                c.render_dyn_params(&mut q, &mut params);
909            }
910            CreateBody::Set(pairs) if !pairs.is_empty() => {
911                q.push_str(" SET ");
912                q.push_str(
913                    &pairs
914                        .iter()
915                        .map(|(k, v)| {
916                            let mut val = String::new();
917                            v.render_dyn_params(&mut val, &mut params);
918                            format!("{k} = {val}")
919                        })
920                        .collect::<Vec<_>>()
921                        .join(", "),
922                );
923            }
924            CreateBody::Set(_) => {}
925        }
926        self.returning.render(&mut q);
927        (q, params)
928    }
929}
930
931impl<T: SurrealRecord> std::fmt::Display for Create<T> {
932    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
933        write!(f, "{}", self.to_surrealql())
934    }
935}
936
937// ═══════════════════════════════════════════════════════════════════════════════
938// DELETE
939// ═══════════════════════════════════════════════════════════════════════════════
940
941/// A `DELETE <target> [WHERE …] [RETURN …]` builder.
942pub struct Delete<T: SurrealRecord> {
943    _marker: std::marker::PhantomData<T>,
944    target: Target,
945    filter: Option<Box<dyn DynExpr>>,
946    returning: Returning,
947}
948
949impl<T: SurrealRecord> Delete<T> {
950    pub(crate) fn for_table() -> Self {
951        Self {
952            _marker: std::marker::PhantomData,
953            target: Target::Table(T::table_name()),
954            filter: None,
955            returning: Returning::None,
956        }
957    }
958    /// Target a single record: `DELETE type::record('table', <id>)`.
959    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
960        self.target = Target::Record(RecordLink::new(T::table_name(), id));
961        self
962    }
963    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
964        self.filter = Some(Box::new(expr));
965        self
966    }
967    pub fn returning(mut self, r: Returning) -> Self {
968        self.returning = r;
969        self
970    }
971
972    /// Follow this `DELETE` with a reselecting [`Select`], joined as a `;`-separated
973    /// batch. See [`Create::then_select`] for motivation.
974    pub fn then_select(self, select: Select<T>) -> String {
975        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
976    }
977
978    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
979    pub fn then_select_params(
980        self,
981        select: Select<T>,
982    ) -> (String, BTreeMap<String, serde_json::Value>) {
983        let (mut_q, mut params) = self.to_surrealql_with_params();
984        let (sel_q, sel_params) = select.to_surrealql_with_params();
985        params.extend(sel_params);
986        (format!("{mut_q};\n{sel_q}"), params)
987    }
988
989    pub fn to_surrealql(&self) -> String {
990        let mut q = String::from("DELETE ");
991        self.target.render(&mut q);
992        if let Some(ref f) = self.filter {
993            q.push_str(" WHERE ");
994            f.render_dyn(&mut q);
995        }
996        self.returning.render(&mut q);
997        q
998    }
999
1000    /// Render with `$param` placeholders instead of inlined literals.
1001    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1002        let mut params = BTreeMap::new();
1003        let mut q = String::from("DELETE ");
1004        self.target.render_params(&mut q, &mut params);
1005        if let Some(ref f) = self.filter {
1006            q.push_str(" WHERE ");
1007            f.render_dyn_params(&mut q, &mut params);
1008        }
1009        self.returning.render(&mut q);
1010        (q, params)
1011    }
1012}
1013
1014impl<T: SurrealRecord> std::fmt::Display for Delete<T> {
1015    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1016        write!(f, "{}", self.to_surrealql())
1017    }
1018}
1019
1020// ═══════════════════════════════════════════════════════════════════════════════
1021// Batch — multiple statements joined by `;` (mutate-then-reselect pattern)
1022// ═══════════════════════════════════════════════════════════════════════════════
1023
1024/// Concatenates SurrealQL statements with `;` separators. The store's typical
1025/// pattern is a mutation followed by a SELECT that re-projects the row.
1026#[derive(Default)]
1027pub struct Batch {
1028    statements: Vec<String>,
1029}
1030
1031impl Batch {
1032    pub fn new() -> Self {
1033        Self {
1034            statements: Vec::new(),
1035        }
1036    }
1037    pub fn push(mut self, stmt: impl ToString) -> Self {
1038        self.statements.push(stmt.to_string());
1039        self
1040    }
1041    pub fn to_surrealql(&self) -> String {
1042        self.statements.join(";\n")
1043    }
1044    /// Number of statements (useful for `.take(n)` indexing on the response).
1045    pub fn len(&self) -> usize {
1046        self.statements.len()
1047    }
1048    pub fn is_empty(&self) -> bool {
1049        self.statements.is_empty()
1050    }
1051}
1052
1053impl std::fmt::Display for Batch {
1054    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1055        write!(f, "{}", self.to_surrealql())
1056    }
1057}
1058
1059// ═══════════════════════════════════════════════════════════════════════════════
1060// Transaction — BEGIN … COMMIT/CANCEL (atomic multi-statement)
1061// ═══════════════════════════════════════════════════════════════════════════════
1062
1063/// Wraps statements in a SurrealDB transaction —
1064/// `BEGIN TRANSACTION; … ; COMMIT TRANSACTION;`. Either every statement applies
1065/// or none do: SurrealDB rolls the whole block back if any statement errors, and
1066/// [`cancel`](Self::cancel) terminates with `CANCEL TRANSACTION` to roll back
1067/// explicitly. Unlike [`Batch`] (a plain `;`-joined sequence), a transaction is
1068/// atomic.
1069///
1070/// Push already-rendered statements (`to_surrealql()` output); each is
1071/// `;`-terminated automatically.
1072#[derive(Default)]
1073pub struct Transaction {
1074    statements: Vec<String>,
1075    cancel: bool,
1076}
1077
1078impl Transaction {
1079    pub fn new() -> Self {
1080        Self::default()
1081    }
1082    /// Add a statement to the transaction body.
1083    pub fn push(mut self, stmt: impl ToString) -> Self {
1084        self.statements.push(stmt.to_string());
1085        self
1086    }
1087    /// Terminate with `CANCEL TRANSACTION` (roll back) instead of `COMMIT`.
1088    pub fn cancel(mut self) -> Self {
1089        self.cancel = true;
1090        self
1091    }
1092    pub fn to_surrealql(&self) -> String {
1093        let mut out = String::from("BEGIN TRANSACTION;\n");
1094        for s in &self.statements {
1095            out.push_str(s);
1096            if !s.trim_end().ends_with(';') {
1097                out.push(';');
1098            }
1099            out.push('\n');
1100        }
1101        out.push_str(if self.cancel {
1102            "CANCEL TRANSACTION;"
1103        } else {
1104            "COMMIT TRANSACTION;"
1105        });
1106        out
1107    }
1108    /// Number of statements in the transaction body (excludes BEGIN/COMMIT).
1109    pub fn len(&self) -> usize {
1110        self.statements.len()
1111    }
1112    pub fn is_empty(&self) -> bool {
1113        self.statements.is_empty()
1114    }
1115}
1116
1117impl std::fmt::Display for Transaction {
1118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1119        write!(f, "{}", self.to_surrealql())
1120    }
1121}
1122
1123// ═══════════════════════════════════════════════════════════════════════════════
1124// RELATE — graph edges
1125// ═══════════════════════════════════════════════════════════════════════════════
1126
1127/// Render a record's id as `table:<escaped-key>` into `buf`.
1128fn record_id(thing: &Thing<impl SurrealRecord>, buf: &mut String) {
1129    buf.push_str(thing.table());
1130    buf.push(':');
1131    thing.key.render_id(buf);
1132}
1133
1134/// Return a record's id as a `table:<escaped-key>` string.
1135fn record_id_string(thing: &Thing<impl SurrealRecord>) -> String {
1136    let mut s = String::new();
1137    record_id(thing, &mut s);
1138    s
1139}
1140
1141/// Builds a graph edge statement `RELATE a -> edge -> b` for an edge type `E`.
1142/// For edges that carry their own fields, see [`RelateEdge`].
1143pub struct Relate<E: SurrealEdge> {
1144    _marker: std::marker::PhantomData<E>,
1145}
1146
1147impl<E: SurrealEdge> Relate<E> {
1148    pub fn new() -> Self {
1149        Self {
1150            _marker: std::marker::PhantomData,
1151        }
1152    }
1153
1154    pub fn to_surrealql(
1155        from: &Thing<impl SurrealRecord>,
1156        to: &Thing<impl SurrealRecord>,
1157    ) -> String {
1158        let mut q = String::from("RELATE ");
1159        record_id(from, &mut q);
1160        q.push_str(" -> ");
1161        q.push_str(E::edge_name());
1162        q.push_str(" -> ");
1163        record_id(to, &mut q);
1164        q
1165    }
1166}
1167
1168impl<E: SurrealEdge> Default for Relate<E> {
1169    fn default() -> Self {
1170        Self::new()
1171    }
1172}
1173
1174// ═══════════════════════════════════════════════════════════════════════════════
1175// RELATE with content
1176// ═══════════════════════════════════════════════════════════════════════════════
1177
1178/// Build a RELATE query with edge content.
1179///
1180/// ```ignore
1181/// RelateEdge::<Follows>::from(user).to(other).content(Follows { since: now }).build()
1182/// ```
1183pub struct RelateEdge<E: SurrealEdge> {
1184    _marker: std::marker::PhantomData<E>,
1185    from_label: String,
1186    to_label: String,
1187    content_json: Option<serde_json::Value>,
1188    return_fields: Vec<&'static str>,
1189    returning: Returning,
1190}
1191
1192impl<E: SurrealEdge> RelateEdge<E> {
1193    pub fn from(from: &Thing<impl SurrealRecord>) -> Self {
1194        Self {
1195            _marker: std::marker::PhantomData,
1196            from_label: record_id_string(from),
1197            to_label: String::new(),
1198            content_json: None,
1199            return_fields: Vec::new(),
1200            returning: Returning::None,
1201        }
1202    }
1203
1204    pub fn to(mut self, to: &Thing<impl SurrealRecord>) -> Self {
1205        self.to_label = record_id_string(to);
1206        self
1207    }
1208
1209    /// Attach content to the edge record.
1210    pub fn content(mut self, edge: &impl serde::Serialize) -> Self {
1211        self.content_json = serde_json::to_value(edge).ok();
1212        self
1213    }
1214
1215    /// Add a field to the `RETURN <projection>` list (e.g. `RETURN id`). Multiple
1216    /// calls accumulate; takes precedence over [`returning`](Self::returning).
1217    pub fn return_field(mut self, field: &'static str) -> Self {
1218        self.return_fields.push(field);
1219        self
1220    }
1221    /// Set a `RETURN NONE|BEFORE|AFTER|DIFF` clause on the edge creation.
1222    pub fn returning(mut self, r: Returning) -> Self {
1223        self.returning = r;
1224        self
1225    }
1226
1227    pub fn build(&self) -> String {
1228        let mut q = format!(
1229            "RELATE {} -> {} -> {}",
1230            self.from_label,
1231            E::edge_name(),
1232            self.to_label
1233        );
1234        if let Some(ref c) = self.content_json {
1235            q.push_str(&format!(
1236                " CONTENT {}",
1237                serde_json::to_string(c).unwrap_or_default()
1238            ));
1239        }
1240        if !self.return_fields.is_empty() {
1241            q.push_str(" RETURN ");
1242            q.push_str(&self.return_fields.join(", "));
1243        } else {
1244            self.returning.render(&mut q);
1245        }
1246        q
1247    }
1248}
1249
1250// ═══════════════════════════════════════════════════════════════════════════════
1251// LET — session-scoped variable assignment
1252// ═══════════════════════════════════════════════════════════════════════════════
1253
1254/// Builds a `LET $var = <expr>` statement for session-scoped variables.
1255/// The variable is available in subsequent queries within the same session.
1256///
1257/// ```ignore
1258/// LetVar::new("limit", 10u32).to_surrealql();      // LET $limit = 10;
1259/// LetVar::new("ts", Raw("time::now()")).to_surrealql(); // LET $ts = time::now();
1260/// ```
1261pub struct LetVar {
1262    name: String,
1263    value: Box<dyn DynExpr>,
1264}
1265
1266impl LetVar {
1267    /// Create a `LET $name = <expr>` statement.
1268    pub fn new(name: impl Into<String>, value: impl DynExpr + 'static) -> Self {
1269        Self {
1270            name: name.into(),
1271            value: Box::new(value),
1272        }
1273    }
1274
1275    /// Create a `LET $name = <literal>` statement.
1276    pub fn literal<V: SurrealQL>(name: impl Into<String>, value: V) -> Self {
1277        Self {
1278            name: name.into(),
1279            value: Box::new(crate::expr::Literal(value)),
1280        }
1281    }
1282
1283    pub fn to_surrealql(&self) -> String {
1284        let mut q = format!("LET ${} = ", self.name);
1285        self.value.render_dyn(&mut q);
1286        q
1287    }
1288
1289    /// Render with `$param` placeholders (the `LET` value becomes a `$param`).
1290    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1291        let mut params = BTreeMap::new();
1292        let mut q = format!("LET ${} = ", self.name);
1293        self.value.render_dyn_params(&mut q, &mut params);
1294        (q, params)
1295    }
1296}
1297
1298impl std::fmt::Display for LetVar {
1299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1300        write!(f, "{}", self.to_surrealql())
1301    }
1302}
1303
1304// ═══════════════════════════════════════════════════════════════════════════════
1305// FOR — iterate over an array, running a body per element
1306// ═══════════════════════════════════════════════════════════════════════════════
1307
1308/// Builds a `FOR $<var> IN <array> { <body> }` loop. The loop variable `$<var>`
1309/// is bound inside the body; push one or more statements as the body.
1310///
1311/// ```ignore
1312/// For::new("n", Raw("[1, 2, 3]".into()))
1313///     .push("CREATE counter SET v = $n")
1314///     .to_surrealql();
1315/// // FOR $n IN [1, 2, 3] { CREATE counter SET v = $n; }
1316/// ```
1317pub struct For {
1318    var: String,
1319    array: Box<dyn DynExpr>,
1320    body: Vec<String>,
1321}
1322
1323impl For {
1324    /// `FOR $<var> IN <array>` — the array is any expression (a literal array, a
1325    /// `$param`, a subquery, …).
1326    pub fn new(var: impl Into<String>, array: impl DynExpr + 'static) -> Self {
1327        Self {
1328            var: var.into(),
1329            array: Box::new(array),
1330            body: Vec::new(),
1331        }
1332    }
1333    /// Add a statement to the loop body.
1334    pub fn push(mut self, stmt: impl Into<String>) -> Self {
1335        self.body.push(stmt.into());
1336        self
1337    }
1338    pub fn to_surrealql(&self) -> String {
1339        let mut q = format!("FOR ${} IN ", self.var);
1340        self.array.render_dyn(&mut q);
1341        q.push_str(" { ");
1342        for s in &self.body {
1343            q.push_str(s);
1344            if !s.trim_end().ends_with(';') {
1345                q.push(';');
1346            }
1347            q.push(' ');
1348        }
1349        q.push('}');
1350        q
1351    }
1352}
1353
1354impl std::fmt::Display for For {
1355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1356        write!(f, "{}", self.to_surrealql())
1357    }
1358}
1359
1360// ═══════════════════════════════════════════════════════════════════════════════
1361// DEFINE INDEX
1362// ═══════════════════════════════════════════════════════════════════════════════
1363
1364/// The kind of a `DEFINE INDEX` — what trails the field list.
1365enum IndexKind {
1366    /// a plain (non-unique) index — no trailing clause
1367    Plain,
1368    /// `UNIQUE`
1369    Unique,
1370    /// a verbatim trailing clause, e.g. `SEARCH ANALYZER ascii BM25 HIGHLIGHTS`
1371    /// or `HNSW DIMENSION 128 DIST COSINE` — the escape hatch for full-text and
1372    /// vector indexes whose exact options depend on the engine build.
1373    Raw(String),
1374}
1375
1376/// Builds a `DEFINE INDEX` statement — plain, composite, `UNIQUE`, full-text
1377/// (`SEARCH`), or vector (`HNSW`/`MTREE`) indexes.
1378///
1379/// ```ignore
1380/// // DEFINE INDEX IF NOT EXISTS email_idx ON TABLE user FIELDS email UNIQUE
1381/// DefineIndex::new("email_idx", "user").field("email").unique().to_surrealql();
1382///
1383/// // composite, vector
1384/// DefineIndex::new("name_idx", "user").fields(["first", "last"]).to_surrealql();
1385/// DefineIndex::new("emb_idx", "doc").field("embedding").hnsw(128, "COSINE").to_surrealql();
1386/// ```
1387pub struct DefineIndex {
1388    name: String,
1389    table: String,
1390    fields: Vec<String>,
1391    kind: IndexKind,
1392    if_not_exists: bool,
1393    comment: Option<String>,
1394    concurrently: bool,
1395}
1396
1397impl DefineIndex {
1398    /// Begin `DEFINE INDEX IF NOT EXISTS <name> ON TABLE <table>`.
1399    pub fn new(name: impl Into<String>, table: impl Into<String>) -> Self {
1400        Self {
1401            name: name.into(),
1402            table: table.into(),
1403            fields: Vec::new(),
1404            kind: IndexKind::Plain,
1405            if_not_exists: true,
1406            comment: None,
1407            concurrently: false,
1408        }
1409    }
1410
1411    /// Add one indexed field/column.
1412    pub fn field(mut self, name: impl Into<String>) -> Self {
1413        self.fields.push(name.into());
1414        self
1415    }
1416    /// Add several indexed fields/columns (a composite index).
1417    pub fn fields<I, S>(mut self, names: I) -> Self
1418    where
1419        I: IntoIterator<Item = S>,
1420        S: Into<String>,
1421    {
1422        self.fields.extend(names.into_iter().map(Into::into));
1423        self
1424    }
1425
1426    /// Mark the index `UNIQUE`.
1427    pub fn unique(mut self) -> Self {
1428        self.kind = IndexKind::Unique;
1429        self
1430    }
1431    /// A full-text `SEARCH ANALYZER <analyzer>` index. Append further options
1432    /// (`BM25`, `HIGHLIGHTS`, …) with [`raw`](Self::raw) if your engine needs them.
1433    pub fn search(mut self, analyzer: &str) -> Self {
1434        self.kind = IndexKind::Raw(format!("SEARCH ANALYZER {analyzer}"));
1435        self
1436    }
1437    /// An `HNSW` vector index of the given dimension and distance function
1438    /// (e.g. `"COSINE"`, `"EUCLIDEAN"`).
1439    pub fn hnsw(mut self, dimension: u32, dist: &str) -> Self {
1440        self.kind = IndexKind::Raw(format!("HNSW DIMENSION {dimension} DIST {dist}"));
1441        self
1442    }
1443    /// An `MTREE` vector index of the given dimension and distance function.
1444    pub fn mtree(mut self, dimension: u32, dist: &str) -> Self {
1445        self.kind = IndexKind::Raw(format!("MTREE DIMENSION {dimension} DIST {dist}"));
1446        self
1447    }
1448    /// Set a verbatim trailing clause (the escape hatch for index options somnia
1449    /// doesn't model), e.g. `"SEARCH ANALYZER ascii BM25 HIGHLIGHTS"`.
1450    pub fn raw(mut self, tail: impl Into<String>) -> Self {
1451        self.kind = IndexKind::Raw(tail.into());
1452        self
1453    }
1454
1455    /// Drop the `IF NOT EXISTS` guard.
1456    pub fn overwrite(mut self) -> Self {
1457        self.if_not_exists = false;
1458        self
1459    }
1460    /// Attach a `COMMENT '<text>'`.
1461    pub fn comment(mut self, text: impl Into<String>) -> Self {
1462        self.comment = Some(text.into());
1463        self
1464    }
1465    /// Build the index `CONCURRENTLY` (non-blocking).
1466    pub fn concurrently(mut self) -> Self {
1467        self.concurrently = true;
1468        self
1469    }
1470
1471    pub fn to_surrealql(&self) -> String {
1472        let guard = if self.if_not_exists {
1473            "IF NOT EXISTS "
1474        } else {
1475            ""
1476        };
1477        let mut q = format!(
1478            "DEFINE INDEX {guard}{} ON TABLE {} FIELDS {}",
1479            self.name,
1480            self.table,
1481            self.fields.join(", "),
1482        );
1483        match &self.kind {
1484            IndexKind::Plain => {}
1485            IndexKind::Unique => q.push_str(" UNIQUE"),
1486            IndexKind::Raw(tail) => {
1487                q.push(' ');
1488                q.push_str(tail);
1489            }
1490        }
1491        if let Some(c) = &self.comment {
1492            let escaped = c.replace('\\', "\\\\").replace('\'', "\\'");
1493            q.push_str(&format!(" COMMENT '{escaped}'"));
1494        }
1495        if self.concurrently {
1496            q.push_str(" CONCURRENTLY");
1497        }
1498        q
1499    }
1500
1501    /// `REMOVE INDEX IF EXISTS <name> ON TABLE <table>` — the inverse statement.
1502    pub fn remove(name: &str, table: &str) -> String {
1503        format!("REMOVE INDEX IF EXISTS {name} ON TABLE {table}")
1504    }
1505}
1506
1507impl std::fmt::Display for DefineIndex {
1508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1509        write!(f, "{}", self.to_surrealql())
1510    }
1511}
1512
1513// ═══════════════════════════════════════════════════════════════════════════════
1514// DEFINE EVENT / FUNCTION / ANALYZER / PARAM
1515// ═══════════════════════════════════════════════════════════════════════════════
1516
1517fn guard(if_not_exists: bool) -> &'static str {
1518    if if_not_exists {
1519        "IF NOT EXISTS "
1520    } else {
1521        ""
1522    }
1523}
1524
1525/// `DEFINE EVENT <name> ON TABLE <table> WHEN <cond> THEN <block>` — a trigger
1526/// that fires on `CREATE`/`UPDATE`/`DELETE`. `$event`, `$before`, `$after`,
1527/// `$value` are available inside `when`/`then`.
1528///
1529/// ```ignore
1530/// DefineEvent::new("on_publish", "post")
1531///     .when("$event = 'UPDATE' AND $after.published = true")
1532///     .then("{ CREATE notification SET post = $after.id }")
1533///     .to_surrealql();
1534/// ```
1535pub struct DefineEvent {
1536    name: String,
1537    table: String,
1538    when: String,
1539    then: String,
1540    if_not_exists: bool,
1541}
1542
1543impl DefineEvent {
1544    pub fn new(name: impl Into<String>, table: impl Into<String>) -> Self {
1545        Self {
1546            name: name.into(),
1547            table: table.into(),
1548            when: String::new(),
1549            then: String::new(),
1550            if_not_exists: true,
1551        }
1552    }
1553    /// The `WHEN <condition>` guard (raw SurrealQL).
1554    pub fn when(mut self, cond: impl Into<String>) -> Self {
1555        self.when = cond.into();
1556        self
1557    }
1558    /// The `THEN <block>` body (raw SurrealQL, typically a `{ … }` block).
1559    pub fn then(mut self, block: impl Into<String>) -> Self {
1560        self.then = block.into();
1561        self
1562    }
1563    /// Drop the `IF NOT EXISTS` guard.
1564    pub fn overwrite(mut self) -> Self {
1565        self.if_not_exists = false;
1566        self
1567    }
1568    pub fn to_surrealql(&self) -> String {
1569        format!(
1570            "DEFINE EVENT {}{} ON TABLE {} WHEN {} THEN {}",
1571            guard(self.if_not_exists),
1572            self.name,
1573            self.table,
1574            self.when,
1575            self.then
1576        )
1577    }
1578    /// `REMOVE EVENT IF EXISTS <name> ON TABLE <table>`.
1579    pub fn remove(name: &str, table: &str) -> String {
1580        format!("REMOVE EVENT IF EXISTS {name} ON TABLE {table}")
1581    }
1582}
1583
1584impl std::fmt::Display for DefineEvent {
1585    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1586        write!(f, "{}", self.to_surrealql())
1587    }
1588}
1589
1590/// `DEFINE FUNCTION fn::<name>(<args>) -> <ret> { <body> }` — a user-defined
1591/// SurrealQL function. The `fn::` prefix is added automatically.
1592///
1593/// ```ignore
1594/// DefineFunction::new("greet")
1595///     .arg("name", "string")
1596///     .returns("string")
1597///     .body("RETURN 'hi ' + $name;")
1598///     .to_surrealql();
1599/// ```
1600pub struct DefineFunction {
1601    name: String,
1602    args: Vec<(String, String)>,
1603    returns: Option<String>,
1604    body: String,
1605    if_not_exists: bool,
1606}
1607
1608impl DefineFunction {
1609    pub fn new(name: impl Into<String>) -> Self {
1610        Self {
1611            name: name.into(),
1612            args: Vec::new(),
1613            returns: None,
1614            body: String::new(),
1615            if_not_exists: true,
1616        }
1617    }
1618    /// Add a typed argument — `$name: type`.
1619    pub fn arg(mut self, name: impl Into<String>, ty: impl Into<String>) -> Self {
1620        self.args.push((name.into(), ty.into()));
1621        self
1622    }
1623    /// Declared return type (`-> <ty>`).
1624    pub fn returns(mut self, ty: impl Into<String>) -> Self {
1625        self.returns = Some(ty.into());
1626        self
1627    }
1628    /// The function body (raw SurrealQL statements, e.g. `RETURN …;`).
1629    pub fn body(mut self, body: impl Into<String>) -> Self {
1630        self.body = body.into();
1631        self
1632    }
1633    pub fn overwrite(mut self) -> Self {
1634        self.if_not_exists = false;
1635        self
1636    }
1637    pub fn to_surrealql(&self) -> String {
1638        let args = self
1639            .args
1640            .iter()
1641            .map(|(n, t)| format!("${n}: {t}"))
1642            .collect::<Vec<_>>()
1643            .join(", ");
1644        let ret = self
1645            .returns
1646            .as_ref()
1647            .map(|r| format!(" -> {r}"))
1648            .unwrap_or_default();
1649        format!(
1650            "DEFINE FUNCTION {}fn::{}({}){} {{ {} }}",
1651            guard(self.if_not_exists),
1652            self.name,
1653            args,
1654            ret,
1655            self.body
1656        )
1657    }
1658    /// `REMOVE FUNCTION IF EXISTS fn::<name>`.
1659    pub fn remove(name: &str) -> String {
1660        format!("REMOVE FUNCTION IF EXISTS fn::{name}")
1661    }
1662}
1663
1664impl std::fmt::Display for DefineFunction {
1665    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1666        write!(f, "{}", self.to_surrealql())
1667    }
1668}
1669
1670/// `DEFINE ANALYZER <name> TOKENIZERS <toks> FILTERS <filters>` — a full-text
1671/// tokenizer + filter pipeline (referenced by a `SEARCH` index).
1672pub struct DefineAnalyzer {
1673    name: String,
1674    tokenizers: Vec<String>,
1675    filters: Vec<String>,
1676    if_not_exists: bool,
1677}
1678
1679impl DefineAnalyzer {
1680    pub fn new(name: impl Into<String>) -> Self {
1681        Self {
1682            name: name.into(),
1683            tokenizers: Vec::new(),
1684            filters: Vec::new(),
1685            if_not_exists: true,
1686        }
1687    }
1688    /// Set the tokenizers (e.g. `["class"]`, `["blank", "punct"]`).
1689    pub fn tokenizers<I, S>(mut self, toks: I) -> Self
1690    where
1691        I: IntoIterator<Item = S>,
1692        S: Into<String>,
1693    {
1694        self.tokenizers = toks.into_iter().map(Into::into).collect();
1695        self
1696    }
1697    /// Set the filters (e.g. `["lowercase", "ascii", "snowball(english)"]`).
1698    pub fn filters<I, S>(mut self, filters: I) -> Self
1699    where
1700        I: IntoIterator<Item = S>,
1701        S: Into<String>,
1702    {
1703        self.filters = filters.into_iter().map(Into::into).collect();
1704        self
1705    }
1706    pub fn overwrite(mut self) -> Self {
1707        self.if_not_exists = false;
1708        self
1709    }
1710    pub fn to_surrealql(&self) -> String {
1711        let mut q = format!("DEFINE ANALYZER {}{}", guard(self.if_not_exists), self.name);
1712        if !self.tokenizers.is_empty() {
1713            q.push_str(" TOKENIZERS ");
1714            q.push_str(&self.tokenizers.join(", "));
1715        }
1716        if !self.filters.is_empty() {
1717            q.push_str(" FILTERS ");
1718            q.push_str(&self.filters.join(", "));
1719        }
1720        q
1721    }
1722    /// `REMOVE ANALYZER IF EXISTS <name>`.
1723    pub fn remove(name: &str) -> String {
1724        format!("REMOVE ANALYZER IF EXISTS {name}")
1725    }
1726}
1727
1728impl std::fmt::Display for DefineAnalyzer {
1729    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1730        write!(f, "{}", self.to_surrealql())
1731    }
1732}
1733
1734/// `DEFINE PARAM $<name> VALUE <value>` — a database-scoped parameter. The `$`
1735/// prefix is added automatically.
1736pub struct DefineParam {
1737    name: String,
1738    value: String,
1739    if_not_exists: bool,
1740}
1741
1742impl DefineParam {
1743    /// Begin a `DEFINE PARAM` with a raw SurrealQL value expression.
1744    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
1745        Self {
1746            name: name.into(),
1747            value: value.into(),
1748            if_not_exists: true,
1749        }
1750    }
1751    /// Set the value from a typed literal instead of a raw string.
1752    pub fn value_lit<V: SurrealQL>(mut self, value: V) -> Self {
1753        let mut buf = String::new();
1754        V::render_literal(&value, &mut buf);
1755        self.value = buf;
1756        self
1757    }
1758    pub fn overwrite(mut self) -> Self {
1759        self.if_not_exists = false;
1760        self
1761    }
1762    pub fn to_surrealql(&self) -> String {
1763        format!(
1764            "DEFINE PARAM {}${} VALUE {}",
1765            guard(self.if_not_exists),
1766            self.name,
1767            self.value
1768        )
1769    }
1770    /// `REMOVE PARAM IF EXISTS $<name>`.
1771    pub fn remove(name: &str) -> String {
1772        format!("REMOVE PARAM IF EXISTS ${name}")
1773    }
1774}
1775
1776impl std::fmt::Display for DefineParam {
1777    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1778        write!(f, "{}", self.to_surrealql())
1779    }
1780}