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    /// Begin a full-text [`Search`] over `field` for `query`
145    /// (`SELECT … FROM <table> WHERE field @@ 'query'`). Pair with a `SEARCH`
146    /// index on the field (see [`DefineIndex::search`]).
147    pub fn search(self, field: impl Into<String>, query: impl Into<String>) -> Search<T> {
148        Search::bare(field, query)
149    }
150
151    /// Begin a vector K-nearest-neighbour [`VectorSearch`] over `field` for the
152    /// query `vector` (`SELECT … FROM <table> WHERE field <|k|> [vector]`). Pair
153    /// with an `HNSW`/`MTREE` index on the field (see [`DefineIndex::hnsw`]).
154    pub fn nearest(self, field: impl Into<String>, vector: Vec<f32>) -> VectorSearch<T> {
155        VectorSearch::bare(field, vector)
156    }
157}
158
159impl<T: SurrealRecord> Default for Table<T> {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165// ═══════════════════════════════════════════════════════════════════════════════
166// SELECT
167// ═══════════════════════════════════════════════════════════════════════════════
168
169/// A `SELECT` statement builder: projections, `WHERE`, `ORDER BY`, `LIMIT`,
170/// `START`, `FETCH`, `GROUP BY`/`GROUP ALL`, `count()`, and the modifiers
171/// `VALUE`/`OMIT`/`SPLIT`/`WITH`/`TIMEOUT`/`EXPLAIN`.
172pub struct Select<T: SurrealRecord> {
173    _marker: std::marker::PhantomData<T>,
174    projections: Vec<Projection>,
175    value: bool,
176    omit: Vec<String>,
177    with: Option<String>,
178    filter: Option<Box<dyn DynExpr>>,
179    split: Vec<String>,
180    order: Vec<(String, Order)>,
181    limit: Option<u32>,
182    start: u32,
183    fetch: Vec<String>,
184    group_by: Vec<String>,
185    group_all: bool,
186    count: bool,
187    count_alias: Option<&'static str>,
188    timeout: Option<String>,
189    explain: Option<bool>,
190    from_sub: Option<Box<Select<T>>>,
191}
192
193impl<T: SurrealRecord> Select<T> {
194    fn bare() -> Self {
195        Select {
196            _marker: std::marker::PhantomData,
197            projections: Vec::new(),
198            value: false,
199            omit: Vec::new(),
200            with: None,
201            filter: None,
202            split: Vec::new(),
203            order: Vec::new(),
204            limit: None,
205            start: 0,
206            fetch: Vec::new(),
207            group_by: Vec::new(),
208            group_all: false,
209            count: false,
210            count_alias: None,
211            timeout: None,
212            explain: None,
213            from_sub: None,
214        }
215    }
216
217    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
218        self.filter = Some(Box::new(expr));
219        self
220    }
221    /// Add a `<path> AS <alias>` graph-traversal projection to the select list.
222    /// Appends to any existing projections, so `select(T::all()).with_path(p, "x")`
223    /// renders `SELECT *, <path> AS x` (a `*` is emitted only when the list is
224    /// otherwise empty).
225    pub fn with_path(mut self, path: Path, alias: &'static str) -> Self {
226        if self.projections.is_empty() {
227            self.projections
228                .push(Projection::new(crate::expr::Raw("*".to_string())));
229        }
230        self.projections.push(Projection::aliased(path, alias));
231        self
232    }
233    pub fn limit(mut self, n: u32) -> Self {
234        self.limit = Some(n);
235        self
236    }
237    pub fn start(mut self, n: u32) -> Self {
238        self.start = n;
239        self
240    }
241    pub fn fetch(mut self, field: impl Into<String>) -> Self {
242        self.fetch.push(field.into());
243        self
244    }
245    pub fn group_by<C: DynExpr>(mut self, col: C) -> Self {
246        let mut buf = String::new();
247        col.render_dyn(&mut buf);
248        self.group_by.push(buf);
249        self
250    }
251    /// `GROUP ALL` (whole-table aggregate, e.g. with `count()`).
252    pub fn group_all(mut self) -> Self {
253        self.group_all = true;
254        self
255    }
256    /// Alias for the `count()` projection: `SELECT count() AS <alias>`.
257    pub fn count_as(mut self, alias: &'static str) -> Self {
258        self.count = true;
259        self.count_alias = Some(alias);
260        self
261    }
262
263    /// `SELECT VALUE …` — return bare values instead of field-wrapping objects.
264    /// Pair with a single projection (e.g. `project(vec![col("name")]).value()`).
265    pub fn value(mut self) -> Self {
266        self.value = true;
267        self
268    }
269    /// `OMIT <field>` — exclude a field from a `SELECT *`.
270    pub fn omit(mut self, field: impl Into<String>) -> Self {
271        self.omit.push(field.into());
272        self
273    }
274    /// `SPLIT <field>` — fan one row out into multiple rows by an array field.
275    pub fn split(mut self, field: impl Into<String>) -> Self {
276        self.split.push(field.into());
277        self
278    }
279    /// `WITH INDEX <a, b>` — force the planner to use the named index(es).
280    pub fn with_index<I, S>(mut self, indexes: I) -> Self
281    where
282        I: IntoIterator<Item = S>,
283        S: AsRef<str>,
284    {
285        let list = indexes
286            .into_iter()
287            .map(|s| s.as_ref().to_string())
288            .collect::<Vec<_>>()
289            .join(", ");
290        self.with = Some(format!("WITH INDEX {list}"));
291        self
292    }
293    /// `WITH NOINDEX` — force a table scan (ignore indexes).
294    pub fn with_no_index(mut self) -> Self {
295        self.with = Some("WITH NOINDEX".to_string());
296        self
297    }
298    /// `TIMEOUT <duration>` — abort the query after the given duration (e.g. `"5s"`).
299    pub fn timeout(mut self, duration: impl Into<String>) -> Self {
300        self.timeout = Some(duration.into());
301        self
302    }
303    /// `SELECT … FROM (<subquery>)` — read from a subquery instead of the base
304    /// table. The subquery (a `Select<T>` of the same record type) renders
305    /// parenthesized in place of the table name.
306    pub fn from_subquery(mut self, sub: Select<T>) -> Self {
307        self.from_sub = Some(Box::new(sub));
308        self
309    }
310
311    /// `EXPLAIN` — return the query plan instead of results.
312    pub fn explain(mut self) -> Self {
313        self.explain = Some(false);
314        self
315    }
316    /// `EXPLAIN FULL` — return the query plan with execution detail.
317    pub fn explain_full(mut self) -> Self {
318        self.explain = Some(true);
319        self
320    }
321
322    pub fn order_by<C: DynExpr>(mut self, col: C, dir: Order) -> Self {
323        let mut buf = String::new();
324        col.render_dyn(&mut buf);
325        self.order.push((buf, dir));
326        self
327    }
328
329    pub fn order_asc<C: DynExpr>(self, col: C) -> Self {
330        self.order_by(col, Order::Asc)
331    }
332    pub fn order_desc<C: DynExpr>(self, col: C) -> Self {
333        self.order_by(col, Order::Desc)
334    }
335
336    fn render_select_list(&self, q: &mut String) {
337        if self.count {
338            q.push_str("count()");
339            if let Some(a) = self.count_alias {
340                q.push_str(" AS ");
341                q.push_str(a);
342            }
343        } else if self.projections.is_empty() {
344            q.push('*');
345        } else {
346            for (i, p) in self.projections.iter().enumerate() {
347                if i > 0 {
348                    q.push_str(", ");
349                }
350                p.render(q);
351            }
352        }
353    }
354
355    fn render_select_list_params(
356        &self,
357        q: &mut String,
358        params: &mut BTreeMap<String, serde_json::Value>,
359    ) {
360        if self.count {
361            q.push_str("count()");
362            if let Some(a) = self.count_alias {
363                q.push_str(" AS ");
364                q.push_str(a);
365            }
366        } else if self.projections.is_empty() {
367            q.push('*');
368        } else {
369            for (i, p) in self.projections.iter().enumerate() {
370                if i > 0 {
371                    q.push_str(", ");
372                }
373                p.render_params(q, params);
374            }
375        }
376    }
377
378    /// Shared renderer for both inline and `$param` modes. When `param_mode` is
379    /// set, literals render as `$pN` placeholders collected into `params`;
380    /// otherwise they render inline (and `params` is ignored). A single map is
381    /// threaded through so a nested subquery's params merge into the parent's.
382    fn render(
383        &self,
384        q: &mut String,
385        params: &mut BTreeMap<String, serde_json::Value>,
386        param_mode: bool,
387    ) {
388        q.push_str("SELECT ");
389        if self.value {
390            q.push_str("VALUE ");
391        }
392        if param_mode {
393            self.render_select_list_params(q, params);
394        } else {
395            self.render_select_list(q);
396        }
397        if !self.omit.is_empty() {
398            q.push_str(" OMIT ");
399            q.push_str(&self.omit.join(", "));
400        }
401        q.push_str(" FROM ");
402        match &self.from_sub {
403            Some(sub) => {
404                q.push('(');
405                sub.render(q, params, param_mode);
406                q.push(')');
407            }
408            None => q.push_str(T::table_name()),
409        }
410        if let Some(w) = &self.with {
411            q.push(' ');
412            q.push_str(w);
413        }
414        if let Some(ref f) = self.filter {
415            q.push_str(" WHERE ");
416            if param_mode {
417                f.render_dyn_params(q, params);
418            } else {
419                f.render_dyn(q);
420            }
421        }
422        for (i, s) in self.split.iter().enumerate() {
423            q.push_str(if i == 0 { " SPLIT " } else { ", " });
424            q.push_str(s);
425        }
426        for (i, (col, dir)) in self.order.iter().enumerate() {
427            q.push_str(if i == 0 { " ORDER BY " } else { ", " });
428            q.push_str(&format!("{col} {dir}"));
429        }
430        for (i, g) in self.group_by.iter().enumerate() {
431            q.push_str(if i == 0 { " GROUP BY " } else { ", " });
432            q.push_str(g);
433        }
434        if self.group_all {
435            q.push_str(" GROUP ALL");
436        }
437        if self.start > 0 {
438            q.push_str(&format!(" START {}", self.start));
439        }
440        if let Some(n) = self.limit {
441            q.push_str(&format!(" LIMIT {n}"));
442        }
443        for f in &self.fetch {
444            q.push_str(&format!(" FETCH {f}"));
445        }
446        if let Some(t) = &self.timeout {
447            q.push_str(" TIMEOUT ");
448            q.push_str(t);
449        }
450        match self.explain {
451            Some(true) => q.push_str(" EXPLAIN FULL"),
452            Some(false) => q.push_str(" EXPLAIN"),
453            None => {}
454        }
455    }
456
457    pub fn to_surrealql(&self) -> String {
458        let mut q = String::new();
459        let mut sink = BTreeMap::new();
460        self.render(&mut q, &mut sink, false);
461        q
462    }
463
464    /// Render the statement with `$param` placeholders instead of inlined
465    /// literals, returning the SQL string and a map of parameter name to value.
466    /// Literal values become numbered `$p0`, `$p1`, …; explicit [`Param`](crate::expr::Param)
467    /// wrappers use their declared name.
468    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
469        let mut params = BTreeMap::new();
470        let mut q = String::new();
471        self.render(&mut q, &mut params, true);
472        (q, params)
473    }
474}
475
476impl<T: SurrealRecord> std::fmt::Debug for Select<T> {
477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478        f.debug_struct("Select")
479            .field("sql", &self.to_surrealql())
480            .finish()
481    }
482}
483
484/// A `Select` is usable as an expression — rendered parenthesized — so it can be
485/// embedded as a subquery: a scalar/`IN` operand in a `WHERE`, a projection, or a
486/// `SET`/`FROM` value. Params from the subquery merge into the parent's map.
487impl<T: SurrealRecord> DynExpr for Select<T> {
488    fn render_dyn(&self, buf: &mut String) {
489        let mut sink = BTreeMap::new();
490        buf.push('(');
491        self.render(buf, &mut sink, false);
492        buf.push(')');
493    }
494    fn render_dyn_params(
495        &self,
496        buf: &mut String,
497        params: &mut BTreeMap<String, serde_json::Value>,
498    ) {
499        buf.push('(');
500        self.render(buf, params, true);
501        buf.push(')');
502    }
503}
504
505impl<T: SurrealRecord> std::fmt::Display for Select<T> {
506    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507        write!(f, "{}", self.to_surrealql())
508    }
509}
510
511// ═══════════════════════════════════════════════════════════════════════════════
512// SEARCH — full-text query helper (field @@ 'query')
513// ═══════════════════════════════════════════════════════════════════════════════
514
515/// A full-text `SELECT` over a `SEARCH`-indexed field, reached from
516/// [`Table::search`]. Renders `SELECT … FROM <table> WHERE field @@ 'query'`,
517/// and — once a match [`reference`](Self::reference) is set — can project
518/// `search::score(n)` and order by relevance.
519///
520/// ```ignore
521/// Post::table()
522///     .search("body", "rust database")
523///     .score_as("score")     // SELECT *, search::score(0) AS score
524///     .order_by_score()      // ORDER BY search::score(0) DESC
525///     .limit(10)
526///     .to_surrealql();
527/// // SELECT *, search::score(0) AS score FROM post
528/// //   WHERE body @0@ 'rust database' ORDER BY score DESC LIMIT 10
529/// ```
530pub struct Search<T: SurrealRecord> {
531    _marker: std::marker::PhantomData<T>,
532    field: String,
533    query: String,
534    reference: Option<u8>,
535    extra: Option<Box<dyn DynExpr>>,
536    score_alias: Option<String>,
537    order_by_score: bool,
538    limit: Option<u32>,
539}
540
541impl<T: SurrealRecord> Search<T> {
542    fn bare(field: impl Into<String>, query: impl Into<String>) -> Self {
543        Self {
544            _marker: std::marker::PhantomData,
545            field: field.into(),
546            query: query.into(),
547            reference: None,
548            extra: None,
549            score_alias: None,
550            order_by_score: false,
551            limit: None,
552        }
553    }
554
555    /// The match reference used in `@n@` (default `0` once scoring is requested).
556    fn ref_num(&self) -> u8 {
557        self.reference.unwrap_or(0)
558    }
559
560    /// Set the match reference `n` explicitly (renders `field @n@ 'query'`).
561    pub fn reference(mut self, n: u8) -> Self {
562        self.reference = Some(n);
563        self
564    }
565
566    /// AND an additional predicate onto the `WHERE` clause.
567    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
568        self.extra = Some(match self.extra.take() {
569            Some(prev) => Box::new(crate::expr::AndExpr {
570                left: prev,
571                right: Box::new(expr),
572            }),
573            None => Box::new(expr),
574        });
575        self
576    }
577
578    /// Project the relevance score as `search::score(n) AS <alias>`. Enables the
579    /// match reference (`@n@`) so the score is computable.
580    pub fn score_as(mut self, alias: impl Into<String>) -> Self {
581        self.reference.get_or_insert(0);
582        self.score_alias = Some(alias.into());
583        self
584    }
585
586    /// `ORDER BY <score> DESC` — most relevant first. Projects the score (default
587    /// alias `score`) and enables the match reference, since `ORDER BY` sorts on
588    /// the projected alias rather than the `search::score(n)` call directly.
589    pub fn order_by_score(mut self) -> Self {
590        self.reference.get_or_insert(0);
591        self.score_alias.get_or_insert_with(|| "score".to_string());
592        self.order_by_score = true;
593        self
594    }
595
596    /// `LIMIT n`.
597    pub fn limit(mut self, n: u32) -> Self {
598        self.limit = Some(n);
599        self
600    }
601
602    fn predicate(&self) -> crate::expr::MatchesExpr {
603        crate::expr::MatchesExpr {
604            left: Box::new(crate::expr::Raw(self.field.clone())),
605            right: Box::new(crate::expr::Literal(self.query.clone())),
606            reference: self.reference,
607        }
608    }
609
610    fn render(
611        &self,
612        q: &mut String,
613        params: &mut BTreeMap<String, serde_json::Value>,
614        param_mode: bool,
615    ) {
616        let r = self.ref_num();
617        q.push_str("SELECT *");
618        if let Some(alias) = &self.score_alias {
619            q.push_str(&format!(", search::score({r}) AS {alias}"));
620        }
621        q.push_str(" FROM ");
622        q.push_str(T::table_name());
623        q.push_str(" WHERE ");
624        let pred = self.predicate();
625        if param_mode {
626            pred.render_dyn_params(q, params);
627        } else {
628            pred.render_dyn(q);
629        }
630        if let Some(extra) = &self.extra {
631            q.push_str(" AND ");
632            if param_mode {
633                extra.render_dyn_params(q, params);
634            } else {
635                extra.render_dyn(q);
636            }
637        }
638        if self.order_by_score {
639            let alias = self.score_alias.as_deref().unwrap_or("score");
640            q.push_str(&format!(" ORDER BY {alias} DESC"));
641        }
642        if let Some(n) = self.limit {
643            q.push_str(&format!(" LIMIT {n}"));
644        }
645    }
646
647    /// Render to a SurrealQL string with literals inlined.
648    pub fn to_surrealql(&self) -> String {
649        let mut q = String::new();
650        let mut sink = BTreeMap::new();
651        self.render(&mut q, &mut sink, false);
652        q
653    }
654
655    /// Render with `$param` placeholders (the search query becomes a bound param).
656    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
657        let mut params = BTreeMap::new();
658        let mut q = String::new();
659        self.render(&mut q, &mut params, true);
660        (q, params)
661    }
662}
663
664impl<T: SurrealRecord> std::fmt::Display for Search<T> {
665    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
666        write!(f, "{}", self.to_surrealql())
667    }
668}
669
670// ═══════════════════════════════════════════════════════════════════════════════
671// VECTOR SEARCH — KNN query helper (field <|k|> [vector])
672// ═══════════════════════════════════════════════════════════════════════════════
673
674/// How the KNN operator resolves neighbours. SurrealDB 3.x requires the operator
675/// to carry a second operand — either an HNSW candidate-list size (`<|k,ef|>`,
676/// using the field's `HNSW` index) or a brute-force distance metric
677/// (`<|k,METRIC|>`). The bare `<|k|>` form is no longer supported.
678#[derive(Debug, Clone)]
679enum KnnMode {
680    /// `<|k,ef|>` — HNSW index search; `ef` (candidate-list size) defaults to `k`.
681    Hnsw(Option<u32>),
682    /// `<|k,METRIC|>` — brute force with the named distance (e.g. `EUCLIDEAN`).
683    Brute(String),
684}
685
686/// A vector K-nearest-neighbour `SELECT`, reached from [`Table::nearest`].
687/// Renders `SELECT … FROM <table> WHERE field <|k,ef|> [vector]`, optionally
688/// projecting / ordering by the computed `vector::distance::knn()`. By default
689/// it uses the field's HNSW index with `ef = k`; call [`distance`](Self::distance)
690/// for a brute-force scan or [`ef`](Self::ef) to tune recall.
691///
692/// ```ignore
693/// Doc::table()
694///     .nearest("embedding", vec![0.1, 0.2, 0.3])
695///     .k(5)
696///     .distance_as("dist")      // SELECT *, vector::distance::knn() AS dist
697///     .order_by_distance()      // ORDER BY dist
698///     .to_surrealql();
699/// // SELECT *, vector::distance::knn() AS dist FROM doc
700/// //   WHERE embedding <|5,5|> [0.1, 0.2, 0.3] ORDER BY dist
701/// ```
702pub struct VectorSearch<T: SurrealRecord> {
703    _marker: std::marker::PhantomData<T>,
704    field: String,
705    vector: Vec<f32>,
706    k: u32,
707    mode: KnnMode,
708    extra: Option<Box<dyn DynExpr>>,
709    distance_alias: Option<String>,
710    order_by_distance: bool,
711    limit: Option<u32>,
712}
713
714impl<T: SurrealRecord> VectorSearch<T> {
715    fn bare(field: impl Into<String>, vector: Vec<f32>) -> Self {
716        Self {
717            _marker: std::marker::PhantomData,
718            field: field.into(),
719            vector,
720            k: 10,
721            mode: KnnMode::Hnsw(None),
722            extra: None,
723            distance_alias: None,
724            order_by_distance: false,
725            limit: None,
726        }
727    }
728
729    /// Number of neighbours `k` to return (default `10`).
730    pub fn k(mut self, k: u32) -> Self {
731        self.k = k;
732        self
733    }
734
735    /// Brute-force KNN with an explicit distance metric (`<|k,METRIC|>`), e.g.
736    /// `"EUCLIDEAN"`, `"COSINE"`, `"MANHATTAN"`.
737    pub fn distance(mut self, metric: impl Into<String>) -> Self {
738        self.mode = KnnMode::Brute(metric.into());
739        self
740    }
741
742    /// HNSW KNN with explicit candidate-list size `ef` (`<|k,ef|>`); larger `ef`
743    /// trades latency for recall. Defaults to `ef = k` when unset.
744    pub fn ef(mut self, ef: u32) -> Self {
745        self.mode = KnnMode::Hnsw(Some(ef));
746        self
747    }
748
749    /// AND an additional predicate onto the `WHERE` clause.
750    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
751        self.extra = Some(match self.extra.take() {
752            Some(prev) => Box::new(crate::expr::AndExpr {
753                left: prev,
754                right: Box::new(expr),
755            }),
756            None => Box::new(expr),
757        });
758        self
759    }
760
761    /// Project the computed distance as `vector::distance::knn() AS <alias>`.
762    pub fn distance_as(mut self, alias: impl Into<String>) -> Self {
763        self.distance_alias = Some(alias.into());
764        self
765    }
766
767    /// `ORDER BY <distance>` — nearest first. Projects the computed distance
768    /// (default alias `distance`), since `ORDER BY` sorts on the projected alias
769    /// rather than the `vector::distance::knn()` call directly.
770    pub fn order_by_distance(mut self) -> Self {
771        self.distance_alias
772            .get_or_insert_with(|| "distance".to_string());
773        self.order_by_distance = true;
774        self
775    }
776
777    /// `LIMIT n`.
778    pub fn limit(mut self, n: u32) -> Self {
779        self.limit = Some(n);
780        self
781    }
782
783    fn predicate(&self) -> crate::expr::KnnExpr {
784        let opt = match &self.mode {
785            KnnMode::Hnsw(Some(ef)) => Some(ef.to_string()),
786            KnnMode::Hnsw(None) => Some(self.k.to_string()),
787            KnnMode::Brute(m) => Some(m.clone()),
788        };
789        crate::expr::KnnExpr {
790            left: Box::new(crate::expr::Raw(self.field.clone())),
791            right: Box::new(crate::expr::Literal(self.vector.clone())),
792            k: self.k,
793            opt,
794        }
795    }
796
797    fn render(
798        &self,
799        q: &mut String,
800        params: &mut BTreeMap<String, serde_json::Value>,
801        param_mode: bool,
802    ) {
803        q.push_str("SELECT *");
804        if let Some(alias) = &self.distance_alias {
805            q.push_str(&format!(", vector::distance::knn() AS {alias}"));
806        }
807        q.push_str(" FROM ");
808        q.push_str(T::table_name());
809        q.push_str(" WHERE ");
810        let pred = self.predicate();
811        if param_mode {
812            pred.render_dyn_params(q, params);
813        } else {
814            pred.render_dyn(q);
815        }
816        if let Some(extra) = &self.extra {
817            q.push_str(" AND ");
818            if param_mode {
819                extra.render_dyn_params(q, params);
820            } else {
821                extra.render_dyn(q);
822            }
823        }
824        if self.order_by_distance {
825            let alias = self.distance_alias.as_deref().unwrap_or("distance");
826            q.push_str(&format!(" ORDER BY {alias}"));
827        }
828        if let Some(n) = self.limit {
829            q.push_str(&format!(" LIMIT {n}"));
830        }
831    }
832
833    /// Render to a SurrealQL string with the query vector inlined.
834    pub fn to_surrealql(&self) -> String {
835        let mut q = String::new();
836        let mut sink = BTreeMap::new();
837        self.render(&mut q, &mut sink, false);
838        q
839    }
840
841    /// Render with `$param` placeholders (the query vector becomes a bound param).
842    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
843        let mut params = BTreeMap::new();
844        let mut q = String::new();
845        self.render(&mut q, &mut params, true);
846        (q, params)
847    }
848}
849
850impl<T: SurrealRecord> std::fmt::Display for VectorSearch<T> {
851    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
852        write!(f, "{}", self.to_surrealql())
853    }
854}
855
856// ═══════════════════════════════════════════════════════════════════════════════
857// INSERT
858// ═══════════════════════════════════════════════════════════════════════════════
859
860/// An `INSERT INTO <table> …` builder. Records are serialized inline as object
861/// literals; rendering requires `T: serde::Serialize`.
862pub struct Insert<T: SurrealRecord> {
863    data: Vec<T>,
864    return_fields: Vec<&'static str>,
865    returning: Returning,
866}
867
868impl<T: SurrealRecord> Insert<T> {
869    pub fn content(mut self, record: T) -> Self {
870        self.data.push(record);
871        self
872    }
873    /// Add a field to the `RETURN <projection>` list. Multiple calls accumulate
874    /// (`RETURN id, name`). Takes precedence over [`returning`](Self::returning).
875    pub fn return_field(mut self, field: &'static str) -> Self {
876        self.return_fields.push(field);
877        self
878    }
879    /// Set a `RETURN NONE|BEFORE|AFTER|DIFF` clause (used when no explicit
880    /// [`return_field`](Self::return_field) projection is given).
881    pub fn returning(mut self, r: Returning) -> Self {
882        self.returning = r;
883        self
884    }
885    pub fn data(&self) -> &[T] {
886        &self.data
887    }
888
889    /// Render `INSERT INTO <table> <object|array> [RETURN …]`, serializing the
890    /// queued record(s) inline as SurrealQL object literals (JSON is a valid
891    /// subset). A single record renders as `{ … }`, multiple as `[ {…}, {…} ]`.
892    /// A `RETURN` projection (from [`return_field`](Self::return_field)) renders
893    /// the field list; otherwise the [`returning`](Self::returning) variant.
894    pub fn to_surrealql(&self) -> String
895    where
896        T: serde::Serialize,
897    {
898        let body = match self.data.as_slice() {
899            [] => "[]".to_string(),
900            [one] => serde_json::to_string(one).unwrap_or_else(|_| "{}".to_string()),
901            many => serde_json::to_string(many).unwrap_or_else(|_| "[]".to_string()),
902        };
903        let mut q = format!("INSERT INTO {} {}", T::table_name(), body);
904        if !self.return_fields.is_empty() {
905            q.push_str(" RETURN ");
906            q.push_str(&self.return_fields.join(", "));
907        } else {
908            self.returning.render(&mut q);
909        }
910        q
911    }
912}
913
914// ═══════════════════════════════════════════════════════════════════════════════
915// UPDATE
916// ═══════════════════════════════════════════════════════════════════════════════
917
918enum SetVal {
919    /// `SET k = <expr>`
920    Assign(String, Box<dyn DynExpr>),
921    /// `MERGE <expr>`
922    Merge(Box<dyn DynExpr>),
923    /// `CONTENT <expr>` (full replace)
924    Content(Box<dyn DynExpr>),
925}
926
927impl SetVal {
928    fn render(&self, buf: &mut String, set_pairs: &mut Vec<String>) {
929        match self {
930            SetVal::Assign(k, v) => {
931                let mut val_buf = String::new();
932                v.render_dyn(&mut val_buf);
933                set_pairs.push(format!("{k} = {val_buf}"));
934            }
935            SetVal::Merge(v) => {
936                let mut val_buf = String::new();
937                v.render_dyn(&mut val_buf);
938                buf.push_str(" MERGE ");
939                buf.push_str(&val_buf);
940            }
941            SetVal::Content(v) => {
942                let mut val_buf = String::new();
943                v.render_dyn(&mut val_buf);
944                buf.push_str(" CONTENT ");
945                buf.push_str(&val_buf);
946            }
947        }
948    }
949    fn render_params(
950        &self,
951        buf: &mut String,
952        set_pairs: &mut Vec<String>,
953        params: &mut BTreeMap<String, serde_json::Value>,
954    ) {
955        match self {
956            SetVal::Assign(k, v) => {
957                let mut val_buf = String::new();
958                v.render_dyn_params(&mut val_buf, params);
959                set_pairs.push(format!("{k} = {val_buf}"));
960            }
961            SetVal::Merge(v) => {
962                let mut val_buf = String::new();
963                v.render_dyn_params(&mut val_buf, params);
964                buf.push_str(" MERGE ");
965                buf.push_str(&val_buf);
966            }
967            SetVal::Content(v) => {
968                let mut val_buf = String::new();
969                v.render_dyn_params(&mut val_buf, params);
970                buf.push_str(" CONTENT ");
971                buf.push_str(&val_buf);
972            }
973        }
974    }
975}
976
977/// An `UPDATE`/`UPSERT` builder: `SET` / `MERGE` / `CONTENT`, an optional `WHERE`,
978/// and `RETURN`. Built via [`Table::update`] or [`Table::upsert`].
979pub struct Update<T: SurrealRecord> {
980    _marker: std::marker::PhantomData<T>,
981    verb: &'static str,
982    target: Target,
983    filter: Option<Box<dyn DynExpr>>,
984    sets: Vec<SetVal>,
985    returning: Returning,
986}
987
988impl<T: SurrealRecord> Update<T> {
989    pub(crate) fn for_table() -> Self {
990        Self::with_verb("UPDATE")
991    }
992
993    /// An `UPSERT` statement — same builder surface as `UPDATE`, but creates the
994    /// record if it doesn't exist. Built via [`Table::upsert`].
995    pub(crate) fn for_upsert() -> Self {
996        Self::with_verb("UPSERT")
997    }
998
999    fn with_verb(verb: &'static str) -> Self {
1000        Self {
1001            _marker: std::marker::PhantomData,
1002            verb,
1003            target: Target::Table(T::table_name()),
1004            filter: None,
1005            sets: Vec::new(),
1006            returning: Returning::None,
1007        }
1008    }
1009
1010    /// Target a single record: `UPDATE type::record('table', <id>)`.
1011    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
1012        self.target = Target::Record(RecordLink::new(T::table_name(), id));
1013        self
1014    }
1015
1016    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
1017        self.filter = Some(Box::new(expr));
1018        self
1019    }
1020
1021    /// `SET col = <literal>`.
1022    pub fn set<C: SurrealQL>(mut self, col: Column<T, C>, value: C) -> Self {
1023        self.sets.push(SetVal::Assign(
1024            col.name.to_string(),
1025            Box::new(crate::expr::Literal(value)),
1026        ));
1027        self
1028    }
1029    /// `SET col = <literal>` by raw column name.
1030    pub fn set_lit<C: SurrealQL>(mut self, col: impl Into<String>, value: C) -> Self {
1031        self.sets.push(SetVal::Assign(
1032            col.into(),
1033            Box::new(crate::expr::Literal(value)),
1034        ));
1035        self
1036    }
1037    /// `SET col = <expr>` — e.g. a record link, `time::now()`, NONE, `use_count + 1`.
1038    pub fn set_expr(mut self, col: impl Into<String>, expr: impl DynExpr + 'static) -> Self {
1039        self.sets.push(SetVal::Assign(col.into(), Box::new(expr)));
1040        self
1041    }
1042    /// `SET col = <raw SurrealQL>`.
1043    pub fn set_raw(mut self, col: impl Into<String>, raw: impl Into<String>) -> Self {
1044        self.sets.push(SetVal::Assign(
1045            col.into(),
1046            Box::new(crate::expr::Raw(raw.into())),
1047        ));
1048        self
1049    }
1050    /// `MERGE <expr>` — deep-merge the given object into the record.
1051    pub fn merge(mut self, expr: impl DynExpr + 'static) -> Self {
1052        self.sets.push(SetVal::Merge(Box::new(expr)));
1053        self
1054    }
1055    /// `CONTENT <expr>` — full-replace the record's content (upsert by record id).
1056    pub fn content(mut self, expr: impl DynExpr + 'static) -> Self {
1057        self.sets.push(SetVal::Content(Box::new(expr)));
1058        self
1059    }
1060    pub fn returning(mut self, r: Returning) -> Self {
1061        self.returning = r;
1062        self
1063    }
1064
1065    /// Follow this `UPDATE` with a reselecting [`Select`], joined as a `;`-separated
1066    /// batch. See [`Create::then_select`] for motivation.
1067    pub fn then_select(self, select: Select<T>) -> String {
1068        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
1069    }
1070
1071    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
1072    pub fn then_select_params(
1073        self,
1074        select: Select<T>,
1075    ) -> (String, BTreeMap<String, serde_json::Value>) {
1076        let (mut_q, mut params) = self.to_surrealql_with_params();
1077        let (sel_q, sel_params) = select.to_surrealql_with_params();
1078        params.extend(sel_params);
1079        (format!("{mut_q};\n{sel_q}"), params)
1080    }
1081
1082    pub fn to_surrealql(&self) -> String {
1083        let mut q = String::from(self.verb);
1084        q.push(' ');
1085        self.target.render(&mut q);
1086        // SurrealQL order: SET/MERGE/CONTENT first, then WHERE, then RETURN.
1087        let mut set_pairs = Vec::new();
1088        let mut trait_buf = String::new();
1089        for s in &self.sets {
1090            s.render(&mut trait_buf, &mut set_pairs);
1091        }
1092        if !trait_buf.is_empty() {
1093            q.push_str(&trait_buf);
1094        } else if !set_pairs.is_empty() {
1095            q.push_str(" SET ");
1096            q.push_str(&set_pairs.join(", "));
1097        }
1098        if let Some(ref f) = self.filter {
1099            q.push_str(" WHERE ");
1100            f.render_dyn(&mut q);
1101        }
1102        self.returning.render(&mut q);
1103        q
1104    }
1105
1106    /// Render with `$param` placeholders instead of inlined literals.
1107    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1108        let mut params = BTreeMap::new();
1109        let mut q = String::from(self.verb);
1110        q.push(' ');
1111        self.target.render_params(&mut q, &mut params);
1112        let mut set_pairs = Vec::new();
1113        let mut trait_buf = String::new();
1114        for s in &self.sets {
1115            s.render_params(&mut trait_buf, &mut set_pairs, &mut params);
1116        }
1117        if !trait_buf.is_empty() {
1118            q.push_str(&trait_buf);
1119        } else if !set_pairs.is_empty() {
1120            q.push_str(" SET ");
1121            q.push_str(&set_pairs.join(", "));
1122        }
1123        if let Some(ref f) = self.filter {
1124            q.push_str(" WHERE ");
1125            f.render_dyn_params(&mut q, &mut params);
1126        }
1127        self.returning.render(&mut q);
1128        (q, params)
1129    }
1130}
1131
1132impl<T: SurrealRecord> std::fmt::Display for Update<T> {
1133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1134        write!(f, "{}", self.to_surrealql())
1135    }
1136}
1137
1138// ═══════════════════════════════════════════════════════════════════════════════
1139// CREATE
1140// ═══════════════════════════════════════════════════════════════════════════════
1141
1142enum CreateBody {
1143    /// `CONTENT <expr>`
1144    Content(Box<dyn DynExpr>),
1145    /// `SET a = x, b = y`
1146    Set(Vec<(String, Box<dyn DynExpr>)>),
1147}
1148
1149/// `CREATE <target> [CONTENT … | SET …] [RETURN …]`.
1150pub struct Create<T: SurrealRecord> {
1151    _marker: std::marker::PhantomData<T>,
1152    target: Target,
1153    body: CreateBody,
1154    returning: Returning,
1155}
1156
1157impl<T: SurrealRecord> Create<T> {
1158    pub(crate) fn for_table() -> Self {
1159        Self {
1160            _marker: std::marker::PhantomData,
1161            target: Target::Table(T::table_name()),
1162            body: CreateBody::Set(Vec::new()),
1163            returning: Returning::None,
1164        }
1165    }
1166
1167    /// Target a single record id: `CREATE type::record('table', <id>)`.
1168    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
1169        self.target = Target::Record(RecordLink::new(T::table_name(), id));
1170        self
1171    }
1172
1173    /// `CONTENT <expr>` — replaces any accumulated SET pairs.
1174    pub fn content(mut self, expr: impl DynExpr + 'static) -> Self {
1175        self.body = CreateBody::Content(Box::new(expr));
1176        self
1177    }
1178
1179    /// `SET col = <literal>`.
1180    pub fn set_lit<C: SurrealQL>(mut self, col: impl Into<String>, value: C) -> Self {
1181        self.push_set(col.into(), Box::new(crate::expr::Literal(value)));
1182        self
1183    }
1184    /// `SET col = <expr>`.
1185    pub fn set_expr(mut self, col: impl Into<String>, expr: impl DynExpr + 'static) -> Self {
1186        self.push_set(col.into(), Box::new(expr));
1187        self
1188    }
1189    /// `SET col = <raw SurrealQL>`.
1190    pub fn set_raw(mut self, col: impl Into<String>, raw: impl Into<String>) -> Self {
1191        self.push_set(col.into(), Box::new(crate::expr::Raw(raw.into())));
1192        self
1193    }
1194
1195    fn push_set(&mut self, col: String, expr: Box<dyn DynExpr>) {
1196        match &mut self.body {
1197            CreateBody::Set(v) => v.push((col, expr)),
1198            CreateBody::Content(_) => {
1199                self.body = CreateBody::Set(vec![(col, expr)]);
1200            }
1201        }
1202    }
1203
1204    pub fn returning(mut self, r: Returning) -> Self {
1205        self.returning = r;
1206        self
1207    }
1208
1209    /// Follow this `CREATE` with a reselecting [`Select`], joined as a `;`-separated
1210    /// batch. The select is rendered immediately, producing a complete SurrealQL
1211    /// string ready for `db.query()`.
1212    ///
1213    /// This replaces the manual `Batch::new().push(create).push(select).to_surrealql()`
1214    /// pattern for mutate-then-reselect workflows.
1215    pub fn then_select(self, select: Select<T>) -> String {
1216        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
1217    }
1218
1219    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
1220    pub fn then_select_params(
1221        self,
1222        select: Select<T>,
1223    ) -> (String, BTreeMap<String, serde_json::Value>) {
1224        let (mut_q, mut params) = self.to_surrealql_with_params();
1225        let (sel_q, sel_params) = select.to_surrealql_with_params();
1226        params.extend(sel_params);
1227        (format!("{mut_q};\n{sel_q}"), params)
1228    }
1229
1230    pub fn to_surrealql(&self) -> String {
1231        let mut q = String::from("CREATE ");
1232        self.target.render(&mut q);
1233        match &self.body {
1234            CreateBody::Content(c) => {
1235                q.push_str(" CONTENT ");
1236                c.render_dyn(&mut q);
1237            }
1238            CreateBody::Set(pairs) if !pairs.is_empty() => {
1239                q.push_str(" SET ");
1240                q.push_str(
1241                    &pairs
1242                        .iter()
1243                        .map(|(k, v)| {
1244                            let mut val = String::new();
1245                            v.render_dyn(&mut val);
1246                            format!("{k} = {val}")
1247                        })
1248                        .collect::<Vec<_>>()
1249                        .join(", "),
1250                );
1251            }
1252            CreateBody::Set(_) => {}
1253        }
1254        self.returning.render(&mut q);
1255        q
1256    }
1257
1258    /// Render with `$param` placeholders instead of inlined literals.
1259    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1260        let mut params = BTreeMap::new();
1261        let mut q = String::from("CREATE ");
1262        self.target.render_params(&mut q, &mut params);
1263        match &self.body {
1264            CreateBody::Content(c) => {
1265                q.push_str(" CONTENT ");
1266                c.render_dyn_params(&mut q, &mut params);
1267            }
1268            CreateBody::Set(pairs) if !pairs.is_empty() => {
1269                q.push_str(" SET ");
1270                q.push_str(
1271                    &pairs
1272                        .iter()
1273                        .map(|(k, v)| {
1274                            let mut val = String::new();
1275                            v.render_dyn_params(&mut val, &mut params);
1276                            format!("{k} = {val}")
1277                        })
1278                        .collect::<Vec<_>>()
1279                        .join(", "),
1280                );
1281            }
1282            CreateBody::Set(_) => {}
1283        }
1284        self.returning.render(&mut q);
1285        (q, params)
1286    }
1287}
1288
1289impl<T: SurrealRecord> std::fmt::Display for Create<T> {
1290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1291        write!(f, "{}", self.to_surrealql())
1292    }
1293}
1294
1295// ═══════════════════════════════════════════════════════════════════════════════
1296// DELETE
1297// ═══════════════════════════════════════════════════════════════════════════════
1298
1299/// A `DELETE <target> [WHERE …] [RETURN …]` builder.
1300pub struct Delete<T: SurrealRecord> {
1301    _marker: std::marker::PhantomData<T>,
1302    target: Target,
1303    filter: Option<Box<dyn DynExpr>>,
1304    returning: Returning,
1305}
1306
1307impl<T: SurrealRecord> Delete<T> {
1308    pub(crate) fn for_table() -> Self {
1309        Self {
1310            _marker: std::marker::PhantomData,
1311            target: Target::Table(T::table_name()),
1312            filter: None,
1313            returning: Returning::None,
1314        }
1315    }
1316    /// Target a single record: `DELETE type::record('table', <id>)`.
1317    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
1318        self.target = Target::Record(RecordLink::new(T::table_name(), id));
1319        self
1320    }
1321    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
1322        self.filter = Some(Box::new(expr));
1323        self
1324    }
1325    pub fn returning(mut self, r: Returning) -> Self {
1326        self.returning = r;
1327        self
1328    }
1329
1330    /// Follow this `DELETE` with a reselecting [`Select`], joined as a `;`-separated
1331    /// batch. See [`Create::then_select`] for motivation.
1332    pub fn then_select(self, select: Select<T>) -> String {
1333        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
1334    }
1335
1336    /// Like [`then_select`](Self::then_select) but renders with `$param` placeholders.
1337    pub fn then_select_params(
1338        self,
1339        select: Select<T>,
1340    ) -> (String, BTreeMap<String, serde_json::Value>) {
1341        let (mut_q, mut params) = self.to_surrealql_with_params();
1342        let (sel_q, sel_params) = select.to_surrealql_with_params();
1343        params.extend(sel_params);
1344        (format!("{mut_q};\n{sel_q}"), params)
1345    }
1346
1347    pub fn to_surrealql(&self) -> String {
1348        let mut q = String::from("DELETE ");
1349        self.target.render(&mut q);
1350        if let Some(ref f) = self.filter {
1351            q.push_str(" WHERE ");
1352            f.render_dyn(&mut q);
1353        }
1354        self.returning.render(&mut q);
1355        q
1356    }
1357
1358    /// Render with `$param` placeholders instead of inlined literals.
1359    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1360        let mut params = BTreeMap::new();
1361        let mut q = String::from("DELETE ");
1362        self.target.render_params(&mut q, &mut params);
1363        if let Some(ref f) = self.filter {
1364            q.push_str(" WHERE ");
1365            f.render_dyn_params(&mut q, &mut params);
1366        }
1367        self.returning.render(&mut q);
1368        (q, params)
1369    }
1370}
1371
1372impl<T: SurrealRecord> std::fmt::Display for Delete<T> {
1373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1374        write!(f, "{}", self.to_surrealql())
1375    }
1376}
1377
1378// ═══════════════════════════════════════════════════════════════════════════════
1379// Batch — multiple statements joined by `;` (mutate-then-reselect pattern)
1380// ═══════════════════════════════════════════════════════════════════════════════
1381
1382/// Concatenates SurrealQL statements with `;` separators. The store's typical
1383/// pattern is a mutation followed by a SELECT that re-projects the row.
1384#[derive(Default)]
1385pub struct Batch {
1386    statements: Vec<String>,
1387}
1388
1389impl Batch {
1390    pub fn new() -> Self {
1391        Self {
1392            statements: Vec::new(),
1393        }
1394    }
1395    pub fn push(mut self, stmt: impl ToString) -> Self {
1396        self.statements.push(stmt.to_string());
1397        self
1398    }
1399    pub fn to_surrealql(&self) -> String {
1400        self.statements.join(";\n")
1401    }
1402    /// Number of statements (useful for `.take(n)` indexing on the response).
1403    pub fn len(&self) -> usize {
1404        self.statements.len()
1405    }
1406    pub fn is_empty(&self) -> bool {
1407        self.statements.is_empty()
1408    }
1409}
1410
1411impl std::fmt::Display for Batch {
1412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1413        write!(f, "{}", self.to_surrealql())
1414    }
1415}
1416
1417// ═══════════════════════════════════════════════════════════════════════════════
1418// Transaction — BEGIN … COMMIT/CANCEL (atomic multi-statement)
1419// ═══════════════════════════════════════════════════════════════════════════════
1420
1421/// Wraps statements in a SurrealDB transaction —
1422/// `BEGIN TRANSACTION; … ; COMMIT TRANSACTION;`. Either every statement applies
1423/// or none do: SurrealDB rolls the whole block back if any statement errors, and
1424/// [`cancel`](Self::cancel) terminates with `CANCEL TRANSACTION` to roll back
1425/// explicitly. Unlike [`Batch`] (a plain `;`-joined sequence), a transaction is
1426/// atomic.
1427///
1428/// Push already-rendered statements (`to_surrealql()` output); each is
1429/// `;`-terminated automatically.
1430#[derive(Default)]
1431pub struct Transaction {
1432    statements: Vec<String>,
1433    cancel: bool,
1434}
1435
1436impl Transaction {
1437    pub fn new() -> Self {
1438        Self::default()
1439    }
1440    /// Add a statement to the transaction body.
1441    pub fn push(mut self, stmt: impl ToString) -> Self {
1442        self.statements.push(stmt.to_string());
1443        self
1444    }
1445    /// Terminate with `CANCEL TRANSACTION` (roll back) instead of `COMMIT`.
1446    pub fn cancel(mut self) -> Self {
1447        self.cancel = true;
1448        self
1449    }
1450    pub fn to_surrealql(&self) -> String {
1451        let mut out = String::from("BEGIN TRANSACTION;\n");
1452        for s in &self.statements {
1453            out.push_str(s);
1454            if !s.trim_end().ends_with(';') {
1455                out.push(';');
1456            }
1457            out.push('\n');
1458        }
1459        out.push_str(if self.cancel {
1460            "CANCEL TRANSACTION;"
1461        } else {
1462            "COMMIT TRANSACTION;"
1463        });
1464        out
1465    }
1466    /// Number of statements in the transaction body (excludes BEGIN/COMMIT).
1467    pub fn len(&self) -> usize {
1468        self.statements.len()
1469    }
1470    pub fn is_empty(&self) -> bool {
1471        self.statements.is_empty()
1472    }
1473}
1474
1475impl std::fmt::Display for Transaction {
1476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1477        write!(f, "{}", self.to_surrealql())
1478    }
1479}
1480
1481// ═══════════════════════════════════════════════════════════════════════════════
1482// RELATE — graph edges
1483// ═══════════════════════════════════════════════════════════════════════════════
1484
1485/// Render a record's id as `table:<escaped-key>` into `buf`.
1486fn record_id(thing: &Thing<impl SurrealRecord>, buf: &mut String) {
1487    buf.push_str(thing.table());
1488    buf.push(':');
1489    thing.key.render_id(buf);
1490}
1491
1492/// Return a record's id as a `table:<escaped-key>` string.
1493fn record_id_string(thing: &Thing<impl SurrealRecord>) -> String {
1494    let mut s = String::new();
1495    record_id(thing, &mut s);
1496    s
1497}
1498
1499/// Builds a graph edge statement `RELATE a -> edge -> b` for an edge type `E`.
1500/// For edges that carry their own fields, see [`RelateEdge`].
1501pub struct Relate<E: SurrealEdge> {
1502    _marker: std::marker::PhantomData<E>,
1503}
1504
1505impl<E: SurrealEdge> Relate<E> {
1506    pub fn new() -> Self {
1507        Self {
1508            _marker: std::marker::PhantomData,
1509        }
1510    }
1511
1512    pub fn to_surrealql(
1513        from: &Thing<impl SurrealRecord>,
1514        to: &Thing<impl SurrealRecord>,
1515    ) -> String {
1516        let mut q = String::from("RELATE ");
1517        record_id(from, &mut q);
1518        q.push_str(" -> ");
1519        q.push_str(E::edge_name());
1520        q.push_str(" -> ");
1521        record_id(to, &mut q);
1522        q
1523    }
1524}
1525
1526impl<E: SurrealEdge> Default for Relate<E> {
1527    fn default() -> Self {
1528        Self::new()
1529    }
1530}
1531
1532// ═══════════════════════════════════════════════════════════════════════════════
1533// RELATE with content
1534// ═══════════════════════════════════════════════════════════════════════════════
1535
1536/// Build a RELATE query with edge content.
1537///
1538/// ```ignore
1539/// RelateEdge::<Follows>::from(user).to(other).content(Follows { since: now }).build()
1540/// ```
1541pub struct RelateEdge<E: SurrealEdge> {
1542    _marker: std::marker::PhantomData<E>,
1543    from_label: String,
1544    to_label: String,
1545    content_json: Option<serde_json::Value>,
1546    return_fields: Vec<&'static str>,
1547    returning: Returning,
1548}
1549
1550impl<E: SurrealEdge> RelateEdge<E> {
1551    pub fn from(from: &Thing<impl SurrealRecord>) -> Self {
1552        Self {
1553            _marker: std::marker::PhantomData,
1554            from_label: record_id_string(from),
1555            to_label: String::new(),
1556            content_json: None,
1557            return_fields: Vec::new(),
1558            returning: Returning::None,
1559        }
1560    }
1561
1562    pub fn to(mut self, to: &Thing<impl SurrealRecord>) -> Self {
1563        self.to_label = record_id_string(to);
1564        self
1565    }
1566
1567    /// Attach content to the edge record.
1568    pub fn content(mut self, edge: &impl serde::Serialize) -> Self {
1569        self.content_json = serde_json::to_value(edge).ok();
1570        self
1571    }
1572
1573    /// Add a field to the `RETURN <projection>` list (e.g. `RETURN id`). Multiple
1574    /// calls accumulate; takes precedence over [`returning`](Self::returning).
1575    pub fn return_field(mut self, field: &'static str) -> Self {
1576        self.return_fields.push(field);
1577        self
1578    }
1579    /// Set a `RETURN NONE|BEFORE|AFTER|DIFF` clause on the edge creation.
1580    pub fn returning(mut self, r: Returning) -> Self {
1581        self.returning = r;
1582        self
1583    }
1584
1585    pub fn build(&self) -> String {
1586        let mut q = format!(
1587            "RELATE {} -> {} -> {}",
1588            self.from_label,
1589            E::edge_name(),
1590            self.to_label
1591        );
1592        if let Some(ref c) = self.content_json {
1593            q.push_str(&format!(
1594                " CONTENT {}",
1595                serde_json::to_string(c).unwrap_or_default()
1596            ));
1597        }
1598        if !self.return_fields.is_empty() {
1599            q.push_str(" RETURN ");
1600            q.push_str(&self.return_fields.join(", "));
1601        } else {
1602            self.returning.render(&mut q);
1603        }
1604        q
1605    }
1606}
1607
1608// ═══════════════════════════════════════════════════════════════════════════════
1609// LET — session-scoped variable assignment
1610// ═══════════════════════════════════════════════════════════════════════════════
1611
1612/// Builds a `LET $var = <expr>` statement for session-scoped variables.
1613/// The variable is available in subsequent queries within the same session.
1614///
1615/// ```ignore
1616/// LetVar::new("limit", 10u32).to_surrealql();      // LET $limit = 10;
1617/// LetVar::new("ts", Raw("time::now()")).to_surrealql(); // LET $ts = time::now();
1618/// ```
1619pub struct LetVar {
1620    name: String,
1621    value: Box<dyn DynExpr>,
1622}
1623
1624impl LetVar {
1625    /// Create a `LET $name = <expr>` statement.
1626    pub fn new(name: impl Into<String>, value: impl DynExpr + 'static) -> Self {
1627        Self {
1628            name: name.into(),
1629            value: Box::new(value),
1630        }
1631    }
1632
1633    /// Create a `LET $name = <literal>` statement.
1634    pub fn literal<V: SurrealQL>(name: impl Into<String>, value: V) -> Self {
1635        Self {
1636            name: name.into(),
1637            value: Box::new(crate::expr::Literal(value)),
1638        }
1639    }
1640
1641    pub fn to_surrealql(&self) -> String {
1642        let mut q = format!("LET ${} = ", self.name);
1643        self.value.render_dyn(&mut q);
1644        q
1645    }
1646
1647    /// Render with `$param` placeholders (the `LET` value becomes a `$param`).
1648    pub fn to_surrealql_with_params(&self) -> (String, BTreeMap<String, serde_json::Value>) {
1649        let mut params = BTreeMap::new();
1650        let mut q = format!("LET ${} = ", self.name);
1651        self.value.render_dyn_params(&mut q, &mut params);
1652        (q, params)
1653    }
1654}
1655
1656impl std::fmt::Display for LetVar {
1657    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1658        write!(f, "{}", self.to_surrealql())
1659    }
1660}
1661
1662// ═══════════════════════════════════════════════════════════════════════════════
1663// FOR — iterate over an array, running a body per element
1664// ═══════════════════════════════════════════════════════════════════════════════
1665
1666/// Builds a `FOR $<var> IN <array> { <body> }` loop. The loop variable `$<var>`
1667/// is bound inside the body; push one or more statements as the body.
1668///
1669/// ```ignore
1670/// For::new("n", Raw("[1, 2, 3]".into()))
1671///     .push("CREATE counter SET v = $n")
1672///     .to_surrealql();
1673/// // FOR $n IN [1, 2, 3] { CREATE counter SET v = $n; }
1674/// ```
1675pub struct For {
1676    var: String,
1677    array: Box<dyn DynExpr>,
1678    body: Vec<String>,
1679}
1680
1681impl For {
1682    /// `FOR $<var> IN <array>` — the array is any expression (a literal array, a
1683    /// `$param`, a subquery, …).
1684    pub fn new(var: impl Into<String>, array: impl DynExpr + 'static) -> Self {
1685        Self {
1686            var: var.into(),
1687            array: Box::new(array),
1688            body: Vec::new(),
1689        }
1690    }
1691    /// Add a statement to the loop body.
1692    pub fn push(mut self, stmt: impl Into<String>) -> Self {
1693        self.body.push(stmt.into());
1694        self
1695    }
1696    pub fn to_surrealql(&self) -> String {
1697        let mut q = format!("FOR ${} IN ", self.var);
1698        self.array.render_dyn(&mut q);
1699        q.push_str(" { ");
1700        for s in &self.body {
1701            q.push_str(s);
1702            if !s.trim_end().ends_with(';') {
1703                q.push(';');
1704            }
1705            q.push(' ');
1706        }
1707        q.push('}');
1708        q
1709    }
1710}
1711
1712impl std::fmt::Display for For {
1713    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1714        write!(f, "{}", self.to_surrealql())
1715    }
1716}
1717
1718// ═══════════════════════════════════════════════════════════════════════════════
1719// DEFINE INDEX
1720// ═══════════════════════════════════════════════════════════════════════════════
1721
1722/// The kind of a `DEFINE INDEX` — what trails the field list.
1723enum IndexKind {
1724    /// a plain (non-unique) index — no trailing clause
1725    Plain,
1726    /// `UNIQUE`
1727    Unique,
1728    /// a verbatim trailing clause, e.g. `SEARCH ANALYZER ascii BM25 HIGHLIGHTS`
1729    /// or `HNSW DIMENSION 128 DIST COSINE` — the escape hatch for full-text and
1730    /// vector indexes whose exact options depend on the engine build.
1731    Raw(String),
1732}
1733
1734/// Builds a `DEFINE INDEX` statement — plain, composite, `UNIQUE`, full-text
1735/// (`SEARCH`), or vector (`HNSW`/`MTREE`) indexes.
1736///
1737/// ```ignore
1738/// // DEFINE INDEX IF NOT EXISTS email_idx ON TABLE user FIELDS email UNIQUE
1739/// DefineIndex::new("email_idx", "user").field("email").unique().to_surrealql();
1740///
1741/// // composite, vector
1742/// DefineIndex::new("name_idx", "user").fields(["first", "last"]).to_surrealql();
1743/// DefineIndex::new("emb_idx", "doc").field("embedding").hnsw(128, "COSINE").to_surrealql();
1744/// ```
1745pub struct DefineIndex {
1746    name: String,
1747    table: String,
1748    fields: Vec<String>,
1749    kind: IndexKind,
1750    if_not_exists: bool,
1751    comment: Option<String>,
1752    concurrently: bool,
1753}
1754
1755impl DefineIndex {
1756    /// Begin `DEFINE INDEX IF NOT EXISTS <name> ON TABLE <table>`.
1757    pub fn new(name: impl Into<String>, table: impl Into<String>) -> Self {
1758        Self {
1759            name: name.into(),
1760            table: table.into(),
1761            fields: Vec::new(),
1762            kind: IndexKind::Plain,
1763            if_not_exists: true,
1764            comment: None,
1765            concurrently: false,
1766        }
1767    }
1768
1769    /// Add one indexed field/column.
1770    pub fn field(mut self, name: impl Into<String>) -> Self {
1771        self.fields.push(name.into());
1772        self
1773    }
1774    /// Add several indexed fields/columns (a composite index).
1775    pub fn fields<I, S>(mut self, names: I) -> Self
1776    where
1777        I: IntoIterator<Item = S>,
1778        S: Into<String>,
1779    {
1780        self.fields.extend(names.into_iter().map(Into::into));
1781        self
1782    }
1783
1784    /// Mark the index `UNIQUE`.
1785    pub fn unique(mut self) -> Self {
1786        self.kind = IndexKind::Unique;
1787        self
1788    }
1789    /// A full-text `FULLTEXT ANALYZER <analyzer>` index (SurrealDB 3.x; the
1790    /// pre-3.x `SEARCH` keyword is no longer accepted). Append further options
1791    /// (`BM25`, `HIGHLIGHTS`, …) with [`raw`](Self::raw) — `BM25` is required for
1792    /// `search::score()` to be available.
1793    pub fn search(mut self, analyzer: impl Into<String>) -> Self {
1794        self.kind = IndexKind::Raw(format!("FULLTEXT ANALYZER {}", analyzer.into()));
1795        self
1796    }
1797    /// An `HNSW` vector index of the given dimension and distance function
1798    /// (e.g. `"COSINE"`, `"EUCLIDEAN"`).
1799    pub fn hnsw(mut self, dimension: u32, dist: impl Into<String>) -> Self {
1800        self.kind = IndexKind::Raw(format!("HNSW DIMENSION {dimension} DIST {}", dist.into()));
1801        self
1802    }
1803    /// An `MTREE` vector index of the given dimension and distance function.
1804    pub fn mtree(mut self, dimension: u32, dist: impl Into<String>) -> Self {
1805        self.kind = IndexKind::Raw(format!("MTREE DIMENSION {dimension} DIST {}", dist.into()));
1806        self
1807    }
1808    /// Set a verbatim trailing clause (the escape hatch for index options somnia
1809    /// doesn't model), e.g. `"SEARCH ANALYZER ascii BM25 HIGHLIGHTS"`.
1810    pub fn raw(mut self, tail: impl Into<String>) -> Self {
1811        self.kind = IndexKind::Raw(tail.into());
1812        self
1813    }
1814
1815    /// Drop the `IF NOT EXISTS` guard.
1816    pub fn overwrite(mut self) -> Self {
1817        self.if_not_exists = false;
1818        self
1819    }
1820    /// Attach a `COMMENT '<text>'`.
1821    pub fn comment(mut self, text: impl Into<String>) -> Self {
1822        self.comment = Some(text.into());
1823        self
1824    }
1825    /// Build the index `CONCURRENTLY` (non-blocking).
1826    pub fn concurrently(mut self) -> Self {
1827        self.concurrently = true;
1828        self
1829    }
1830
1831    pub fn to_surrealql(&self) -> String {
1832        let guard = if self.if_not_exists {
1833            "IF NOT EXISTS "
1834        } else {
1835            ""
1836        };
1837        let mut q = format!(
1838            "DEFINE INDEX {guard}{} ON TABLE {} FIELDS {}",
1839            self.name,
1840            self.table,
1841            self.fields.join(", "),
1842        );
1843        match &self.kind {
1844            IndexKind::Plain => {}
1845            IndexKind::Unique => q.push_str(" UNIQUE"),
1846            IndexKind::Raw(tail) => {
1847                q.push(' ');
1848                q.push_str(tail);
1849            }
1850        }
1851        if let Some(c) = &self.comment {
1852            let escaped = c.replace('\\', "\\\\").replace('\'', "\\'");
1853            q.push_str(&format!(" COMMENT '{escaped}'"));
1854        }
1855        if self.concurrently {
1856            q.push_str(" CONCURRENTLY");
1857        }
1858        q
1859    }
1860
1861    /// `REMOVE INDEX IF EXISTS <name> ON TABLE <table>` — the inverse statement.
1862    pub fn remove(name: &str, table: &str) -> String {
1863        format!("REMOVE INDEX IF EXISTS {name} ON TABLE {table}")
1864    }
1865}
1866
1867impl std::fmt::Display for DefineIndex {
1868    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1869        write!(f, "{}", self.to_surrealql())
1870    }
1871}
1872
1873// ═══════════════════════════════════════════════════════════════════════════════
1874// DEFINE EVENT / FUNCTION / ANALYZER / PARAM
1875// ═══════════════════════════════════════════════════════════════════════════════
1876
1877fn guard(if_not_exists: bool) -> &'static str {
1878    if if_not_exists {
1879        "IF NOT EXISTS "
1880    } else {
1881        ""
1882    }
1883}
1884
1885/// `DEFINE EVENT <name> ON TABLE <table> WHEN <cond> THEN <block>` — a trigger
1886/// that fires on `CREATE`/`UPDATE`/`DELETE`. `$event`, `$before`, `$after`,
1887/// `$value` are available inside `when`/`then`.
1888///
1889/// ```ignore
1890/// DefineEvent::new("on_publish", "post")
1891///     .when("$event = 'UPDATE' AND $after.published = true")
1892///     .then("{ CREATE notification SET post = $after.id }")
1893///     .to_surrealql();
1894/// ```
1895pub struct DefineEvent {
1896    name: String,
1897    table: String,
1898    when: String,
1899    then: String,
1900    if_not_exists: bool,
1901}
1902
1903impl DefineEvent {
1904    pub fn new(name: impl Into<String>, table: impl Into<String>) -> Self {
1905        Self {
1906            name: name.into(),
1907            table: table.into(),
1908            when: String::new(),
1909            then: String::new(),
1910            if_not_exists: true,
1911        }
1912    }
1913    /// The `WHEN <condition>` guard (raw SurrealQL).
1914    pub fn when(mut self, cond: impl Into<String>) -> Self {
1915        self.when = cond.into();
1916        self
1917    }
1918    /// The `THEN <block>` body (raw SurrealQL, typically a `{ … }` block).
1919    pub fn then(mut self, block: impl Into<String>) -> Self {
1920        self.then = block.into();
1921        self
1922    }
1923    /// Drop the `IF NOT EXISTS` guard.
1924    pub fn overwrite(mut self) -> Self {
1925        self.if_not_exists = false;
1926        self
1927    }
1928    pub fn to_surrealql(&self) -> String {
1929        format!(
1930            "DEFINE EVENT {}{} ON TABLE {} WHEN {} THEN {}",
1931            guard(self.if_not_exists),
1932            self.name,
1933            self.table,
1934            self.when,
1935            self.then
1936        )
1937    }
1938    /// `REMOVE EVENT IF EXISTS <name> ON TABLE <table>`.
1939    pub fn remove(name: &str, table: &str) -> String {
1940        format!("REMOVE EVENT IF EXISTS {name} ON TABLE {table}")
1941    }
1942}
1943
1944impl std::fmt::Display for DefineEvent {
1945    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1946        write!(f, "{}", self.to_surrealql())
1947    }
1948}
1949
1950/// `DEFINE FUNCTION fn::<name>(<args>) -> <ret> { <body> }` — a user-defined
1951/// SurrealQL function. The `fn::` prefix is added automatically.
1952///
1953/// ```ignore
1954/// DefineFunction::new("greet")
1955///     .arg("name", "string")
1956///     .returns("string")
1957///     .body("RETURN 'hi ' + $name;")
1958///     .to_surrealql();
1959/// ```
1960pub struct DefineFunction {
1961    name: String,
1962    args: Vec<(String, String)>,
1963    returns: Option<String>,
1964    body: String,
1965    if_not_exists: bool,
1966}
1967
1968impl DefineFunction {
1969    pub fn new(name: impl Into<String>) -> Self {
1970        Self {
1971            name: name.into(),
1972            args: Vec::new(),
1973            returns: None,
1974            body: String::new(),
1975            if_not_exists: true,
1976        }
1977    }
1978    /// Add a typed argument — `$name: type`.
1979    pub fn arg(mut self, name: impl Into<String>, ty: impl Into<String>) -> Self {
1980        self.args.push((name.into(), ty.into()));
1981        self
1982    }
1983    /// Declared return type (`-> <ty>`).
1984    pub fn returns(mut self, ty: impl Into<String>) -> Self {
1985        self.returns = Some(ty.into());
1986        self
1987    }
1988    /// The function body (raw SurrealQL statements, e.g. `RETURN …;`).
1989    pub fn body(mut self, body: impl Into<String>) -> Self {
1990        self.body = body.into();
1991        self
1992    }
1993    pub fn overwrite(mut self) -> Self {
1994        self.if_not_exists = false;
1995        self
1996    }
1997    pub fn to_surrealql(&self) -> String {
1998        let args = self
1999            .args
2000            .iter()
2001            .map(|(n, t)| format!("${n}: {t}"))
2002            .collect::<Vec<_>>()
2003            .join(", ");
2004        let ret = self
2005            .returns
2006            .as_ref()
2007            .map(|r| format!(" -> {r}"))
2008            .unwrap_or_default();
2009        format!(
2010            "DEFINE FUNCTION {}fn::{}({}){} {{ {} }}",
2011            guard(self.if_not_exists),
2012            self.name,
2013            args,
2014            ret,
2015            self.body
2016        )
2017    }
2018    /// `REMOVE FUNCTION IF EXISTS fn::<name>`.
2019    pub fn remove(name: &str) -> String {
2020        format!("REMOVE FUNCTION IF EXISTS fn::{name}")
2021    }
2022}
2023
2024impl std::fmt::Display for DefineFunction {
2025    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2026        write!(f, "{}", self.to_surrealql())
2027    }
2028}
2029
2030/// `DEFINE ANALYZER <name> TOKENIZERS <toks> FILTERS <filters>` — a full-text
2031/// tokenizer + filter pipeline (referenced by a `SEARCH` index).
2032pub struct DefineAnalyzer {
2033    name: String,
2034    tokenizers: Vec<String>,
2035    filters: Vec<String>,
2036    if_not_exists: bool,
2037}
2038
2039impl DefineAnalyzer {
2040    pub fn new(name: impl Into<String>) -> Self {
2041        Self {
2042            name: name.into(),
2043            tokenizers: Vec::new(),
2044            filters: Vec::new(),
2045            if_not_exists: true,
2046        }
2047    }
2048    /// Set the tokenizers (e.g. `["class"]`, `["blank", "punct"]`).
2049    pub fn tokenizers<I, S>(mut self, toks: I) -> Self
2050    where
2051        I: IntoIterator<Item = S>,
2052        S: Into<String>,
2053    {
2054        self.tokenizers = toks.into_iter().map(Into::into).collect();
2055        self
2056    }
2057    /// Set the filters (e.g. `["lowercase", "ascii", "snowball(english)"]`).
2058    pub fn filters<I, S>(mut self, filters: I) -> Self
2059    where
2060        I: IntoIterator<Item = S>,
2061        S: Into<String>,
2062    {
2063        self.filters = filters.into_iter().map(Into::into).collect();
2064        self
2065    }
2066    pub fn overwrite(mut self) -> Self {
2067        self.if_not_exists = false;
2068        self
2069    }
2070    pub fn to_surrealql(&self) -> String {
2071        let mut q = format!("DEFINE ANALYZER {}{}", guard(self.if_not_exists), self.name);
2072        if !self.tokenizers.is_empty() {
2073            q.push_str(" TOKENIZERS ");
2074            q.push_str(&self.tokenizers.join(", "));
2075        }
2076        if !self.filters.is_empty() {
2077            q.push_str(" FILTERS ");
2078            q.push_str(&self.filters.join(", "));
2079        }
2080        q
2081    }
2082    /// `REMOVE ANALYZER IF EXISTS <name>`.
2083    pub fn remove(name: &str) -> String {
2084        format!("REMOVE ANALYZER IF EXISTS {name}")
2085    }
2086}
2087
2088impl std::fmt::Display for DefineAnalyzer {
2089    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2090        write!(f, "{}", self.to_surrealql())
2091    }
2092}
2093
2094/// `DEFINE PARAM $<name> VALUE <value>` — a database-scoped parameter. The `$`
2095/// prefix is added automatically.
2096pub struct DefineParam {
2097    name: String,
2098    value: String,
2099    if_not_exists: bool,
2100}
2101
2102impl DefineParam {
2103    /// Begin a `DEFINE PARAM` with a raw SurrealQL value expression.
2104    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
2105        Self {
2106            name: name.into(),
2107            value: value.into(),
2108            if_not_exists: true,
2109        }
2110    }
2111    /// Set the value from a typed literal instead of a raw string.
2112    pub fn value_lit<V: SurrealQL>(mut self, value: V) -> Self {
2113        let mut buf = String::new();
2114        V::render_literal(&value, &mut buf);
2115        self.value = buf;
2116        self
2117    }
2118    pub fn overwrite(mut self) -> Self {
2119        self.if_not_exists = false;
2120        self
2121    }
2122    pub fn to_surrealql(&self) -> String {
2123        format!(
2124            "DEFINE PARAM {}${} VALUE {}",
2125            guard(self.if_not_exists),
2126            self.name,
2127            self.value
2128        )
2129    }
2130    /// `REMOVE PARAM IF EXISTS $<name>`.
2131    pub fn remove(name: &str) -> String {
2132        format!("REMOVE PARAM IF EXISTS ${name}")
2133    }
2134}
2135
2136impl std::fmt::Display for DefineParam {
2137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2138        write!(f, "{}", self.to_surrealql())
2139    }
2140}