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};
20
21/// How a mutating statement should return its affected rows.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Returning {
24    /// no RETURN clause
25    None,
26    /// `RETURN NONE`
27    Nothing,
28    /// `RETURN BEFORE`
29    Before,
30    /// `RETURN AFTER`
31    After,
32    /// `RETURN DIFF`
33    Diff,
34}
35
36impl Returning {
37    fn render(self, buf: &mut String) {
38        match self {
39            Returning::None => {}
40            Returning::Nothing => buf.push_str(" RETURN NONE"),
41            Returning::Before => buf.push_str(" RETURN BEFORE"),
42            Returning::After => buf.push_str(" RETURN AFTER"),
43            Returning::Diff => buf.push_str(" RETURN DIFF"),
44        }
45    }
46}
47
48/// A statement target: either a whole table (`asset`) or a single record link
49/// (`type::record('asset', '<id>')`).
50enum Target {
51    Table(&'static str),
52    Record(RecordLink),
53}
54
55impl Target {
56    fn render(&self, buf: &mut String) {
57        match self {
58            Target::Table(t) => buf.push_str(t),
59            Target::Record(r) => r.render_dyn(buf),
60        }
61    }
62}
63
64// ═══════════════════════════════════════════════════════════════════════════════
65// Table
66// ═══════════════════════════════════════════════════════════════════════════════
67
68/// Entry point to the query builder for a record type `T` — the value returned by
69/// the derived `T::table()`. Each method starts a statement builder.
70pub struct Table<T: SurrealRecord> {
71    _marker: std::marker::PhantomData<T>,
72}
73
74impl<T: SurrealRecord> Table<T> {
75    /// Create a `Table` builder. Prefer the derived `T::table()`.
76    pub fn new() -> Self {
77        Self {
78            _marker: std::marker::PhantomData,
79        }
80    }
81
82    /// Begin a `SELECT * FROM <table>` (pass the derived `T::all()`).
83    pub fn select(self, _cols: crate::expr::ColumnSet<T>) -> Select<T> {
84        Select::bare()
85    }
86
87    /// Select an explicit projection list (`SELECT <fields…> FROM table`).
88    pub fn project(self, fields: Vec<Projection>) -> Select<T> {
89        let mut s = Select::bare();
90        s.projections = fields;
91        s
92    }
93
94    /// `SELECT <path> AS <alias> FROM table` — project a graph traversal.
95    /// A convenience for `project(vec![Projection::aliased(path, alias)])`.
96    pub fn project_path(self, path: Path, alias: &'static str) -> Select<T> {
97        let mut s = Select::bare();
98        s.projections = vec![Projection::aliased(path, alias)];
99        s
100    }
101
102    /// `SELECT count() FROM table GROUP ALL` (the argument is currently ignored).
103    pub fn count(self, _field: &str) -> Select<T> {
104        let mut s = Select::bare();
105        s.count = true;
106        s.group_all = true;
107        s
108    }
109
110    /// Begin an `INSERT INTO <table> …`.
111    pub fn insert(self) -> Insert<T> {
112        Insert {
113            data: Vec::new(),
114            return_fields: vec![],
115        }
116    }
117    /// Begin a `CREATE <table> …`.
118    pub fn create(self) -> Create<T> {
119        Create::for_table()
120    }
121    /// Begin an `UPDATE <table> …`.
122    pub fn update(self) -> Update<T> {
123        Update::for_table()
124    }
125    /// `UPSERT` — update the matching record, or create it if it doesn't exist.
126    /// Same builder surface as [`update`](Self::update) (`record`/`set`/`merge`/
127    /// `content`/`filter`/`returning`/`then_select`).
128    pub fn upsert(self) -> Update<T> {
129        Update::for_upsert()
130    }
131    /// Begin a `DELETE <table> …`.
132    pub fn delete(self) -> Delete<T> {
133        Delete::for_table()
134    }
135}
136
137impl<T: SurrealRecord> Default for Table<T> {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143// ═══════════════════════════════════════════════════════════════════════════════
144// SELECT
145// ═══════════════════════════════════════════════════════════════════════════════
146
147/// A `SELECT` statement builder: projections, `WHERE`, `ORDER BY`, `LIMIT`,
148/// `START`, `FETCH`, `GROUP BY`/`GROUP ALL`, and `count()`.
149pub struct Select<T: SurrealRecord> {
150    _marker: std::marker::PhantomData<T>,
151    projections: Vec<Projection>,
152    filter: Option<Box<dyn DynExpr>>,
153    order: Vec<(String, Order)>,
154    limit: Option<u32>,
155    start: u32,
156    fetch: Vec<String>,
157    group_by: Vec<String>,
158    group_all: bool,
159    count: bool,
160    count_alias: Option<&'static str>,
161}
162
163impl<T: SurrealRecord> Select<T> {
164    fn bare() -> Self {
165        Select {
166            _marker: std::marker::PhantomData,
167            projections: Vec::new(),
168            filter: None,
169            order: Vec::new(),
170            limit: None,
171            start: 0,
172            fetch: Vec::new(),
173            group_by: Vec::new(),
174            group_all: false,
175            count: false,
176            count_alias: None,
177        }
178    }
179
180    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
181        self.filter = Some(Box::new(expr));
182        self
183    }
184    /// Add a `<path> AS <alias>` graph-traversal projection to the select list.
185    /// Appends to any existing projections, so `select(T::all()).with_path(p, "x")`
186    /// renders `SELECT *, <path> AS x` (a `*` is emitted only when the list is
187    /// otherwise empty).
188    pub fn with_path(mut self, path: Path, alias: &'static str) -> Self {
189        if self.projections.is_empty() {
190            self.projections
191                .push(Projection::new(crate::expr::Raw("*".to_string())));
192        }
193        self.projections.push(Projection::aliased(path, alias));
194        self
195    }
196    pub fn limit(mut self, n: u32) -> Self {
197        self.limit = Some(n);
198        self
199    }
200    pub fn start(mut self, n: u32) -> Self {
201        self.start = n;
202        self
203    }
204    pub fn fetch(mut self, field: &str) -> Self {
205        self.fetch.push(field.to_string());
206        self
207    }
208    pub fn group_by<C: DynExpr>(mut self, col: C) -> Self {
209        let mut buf = String::new();
210        col.render_dyn(&mut buf);
211        self.group_by.push(buf);
212        self
213    }
214    /// `GROUP ALL` (whole-table aggregate, e.g. with `count()`).
215    pub fn group_all(mut self) -> Self {
216        self.group_all = true;
217        self
218    }
219    /// Alias for the `count()` projection: `SELECT count() AS <alias>`.
220    pub fn count_as(mut self, alias: &'static str) -> Self {
221        self.count = true;
222        self.count_alias = Some(alias);
223        self
224    }
225
226    pub fn order_by<C: DynExpr>(mut self, col: C, dir: Order) -> Self {
227        let mut buf = String::new();
228        col.render_dyn(&mut buf);
229        self.order.push((buf, dir));
230        self
231    }
232
233    pub fn order_asc<C: DynExpr>(self, col: C) -> Self {
234        self.order_by(col, Order::Asc)
235    }
236    pub fn order_desc<C: DynExpr>(self, col: C) -> Self {
237        self.order_by(col, Order::Desc)
238    }
239
240    fn render_select_list(&self, q: &mut String) {
241        if self.count {
242            q.push_str("count()");
243            if let Some(a) = self.count_alias {
244                q.push_str(" AS ");
245                q.push_str(a);
246            }
247        } else if self.projections.is_empty() {
248            q.push('*');
249        } else {
250            for (i, p) in self.projections.iter().enumerate() {
251                if i > 0 {
252                    q.push_str(", ");
253                }
254                p.render(q);
255            }
256        }
257    }
258
259    pub fn to_surrealql(&self) -> String {
260        let mut q = String::from("SELECT ");
261        self.render_select_list(&mut q);
262        q.push_str(" FROM ");
263        q.push_str(T::table_name());
264        if let Some(ref f) = self.filter {
265            q.push_str(" WHERE ");
266            f.render_dyn(&mut q);
267        }
268        for (i, (col, dir)) in self.order.iter().enumerate() {
269            if i == 0 {
270                q.push_str(" ORDER BY ");
271            } else {
272                q.push_str(", ");
273            }
274            q.push_str(&format!("{col} {dir}"));
275        }
276        for (i, g) in self.group_by.iter().enumerate() {
277            if i == 0 {
278                q.push_str(" GROUP BY ");
279            } else {
280                q.push_str(", ");
281            }
282            q.push_str(g);
283        }
284        if self.group_all {
285            q.push_str(" GROUP ALL");
286        }
287        if self.start > 0 {
288            q.push_str(&format!(" START {}", self.start));
289        }
290        if let Some(n) = self.limit {
291            q.push_str(&format!(" LIMIT {n}"));
292        }
293        for f in &self.fetch {
294            q.push_str(&format!(" FETCH {f}"));
295        }
296        q
297    }
298}
299
300impl<T: SurrealRecord> std::fmt::Display for Select<T> {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        write!(f, "{}", self.to_surrealql())
303    }
304}
305
306// ═══════════════════════════════════════════════════════════════════════════════
307// INSERT
308// ═══════════════════════════════════════════════════════════════════════════════
309
310/// An `INSERT INTO <table> …` builder. Records are serialized inline as object
311/// literals; rendering requires `T: serde::Serialize`.
312pub struct Insert<T: SurrealRecord> {
313    data: Vec<T>,
314    return_fields: Vec<&'static str>,
315}
316
317impl<T: SurrealRecord> Insert<T> {
318    pub fn content(mut self, record: T) -> Self {
319        self.data.push(record);
320        self
321    }
322    pub fn return_field(mut self, field: &'static str) -> Self {
323        self.return_fields.push(field);
324        self
325    }
326    pub fn data(&self) -> &[T] {
327        &self.data
328    }
329
330    /// Render `INSERT INTO <table> <object|array> [RETURN AFTER]`, serializing the
331    /// queued record(s) inline as SurrealQL object literals (JSON is a valid
332    /// subset). A single record renders as `{ … }`, multiple as `[ {…}, {…} ]`.
333    pub fn to_surrealql(&self) -> String
334    where
335        T: serde::Serialize,
336    {
337        let body = match self.data.as_slice() {
338            [] => "[]".to_string(),
339            [one] => serde_json::to_string(one).unwrap_or_else(|_| "{}".to_string()),
340            many => serde_json::to_string(many).unwrap_or_else(|_| "[]".to_string()),
341        };
342        let returning = if self.return_fields.is_empty() {
343            ""
344        } else {
345            " RETURN AFTER"
346        };
347        format!("INSERT INTO {} {}{}", T::table_name(), body, returning)
348    }
349}
350
351// ═══════════════════════════════════════════════════════════════════════════════
352// UPDATE
353// ═══════════════════════════════════════════════════════════════════════════════
354
355enum SetVal {
356    /// `SET k = v` where v is a rendered expression
357    Assign(String, String),
358    /// `MERGE <expr>`
359    Merge(String),
360    /// `CONTENT <expr>` (full replace)
361    Content(String),
362}
363
364/// An `UPDATE`/`UPSERT` builder: `SET` / `MERGE` / `CONTENT`, an optional `WHERE`,
365/// and `RETURN`. Built via [`Table::update`] or [`Table::upsert`].
366pub struct Update<T: SurrealRecord> {
367    _marker: std::marker::PhantomData<T>,
368    verb: &'static str,
369    target: Target,
370    filter: Option<Box<dyn DynExpr>>,
371    sets: Vec<SetVal>,
372    returning: Returning,
373}
374
375impl<T: SurrealRecord> Update<T> {
376    pub(crate) fn for_table() -> Self {
377        Self::with_verb("UPDATE")
378    }
379
380    /// An `UPSERT` statement — same builder surface as `UPDATE`, but creates the
381    /// record if it doesn't exist. Built via [`Table::upsert`].
382    pub(crate) fn for_upsert() -> Self {
383        Self::with_verb("UPSERT")
384    }
385
386    fn with_verb(verb: &'static str) -> Self {
387        Self {
388            _marker: std::marker::PhantomData,
389            verb,
390            target: Target::Table(T::table_name()),
391            filter: None,
392            sets: Vec::new(),
393            returning: Returning::None,
394        }
395    }
396
397    /// Target a single record: `UPDATE type::record('table', <id>)`.
398    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
399        self.target = Target::Record(RecordLink::new(T::table_name(), id));
400        self
401    }
402
403    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
404        self.filter = Some(Box::new(expr));
405        self
406    }
407
408    /// `SET col = <literal>`.
409    pub fn set<C: SurrealQL>(mut self, col: Column<T, C>, value: C) -> Self {
410        let mut buf = String::new();
411        C::render_literal(&value, &mut buf);
412        self.sets.push(SetVal::Assign(col.name.to_string(), buf));
413        self
414    }
415    /// `SET col = <literal>` by raw column name.
416    pub fn set_lit<C: SurrealQL>(mut self, col: &str, value: C) -> Self {
417        let mut buf = String::new();
418        C::render_literal(&value, &mut buf);
419        self.sets.push(SetVal::Assign(col.to_string(), buf));
420        self
421    }
422    /// `SET col = <expr>` — e.g. a record link, `time::now()`, NONE, `use_count + 1`.
423    pub fn set_expr(mut self, col: &str, expr: impl DynExpr) -> Self {
424        let mut buf = String::new();
425        expr.render_dyn(&mut buf);
426        self.sets.push(SetVal::Assign(col.to_string(), buf));
427        self
428    }
429    /// `SET col = <raw SurrealQL>`.
430    pub fn set_raw(mut self, col: &str, raw: impl Into<String>) -> Self {
431        self.sets.push(SetVal::Assign(col.to_string(), raw.into()));
432        self
433    }
434    /// `MERGE <expr>` — deep-merge the given object into the record.
435    pub fn merge(mut self, expr: impl DynExpr) -> Self {
436        let mut buf = String::new();
437        expr.render_dyn(&mut buf);
438        self.sets.push(SetVal::Merge(buf));
439        self
440    }
441    /// `CONTENT <expr>` — full-replace the record's content (upsert by record id).
442    pub fn content(mut self, expr: impl DynExpr) -> Self {
443        let mut buf = String::new();
444        expr.render_dyn(&mut buf);
445        self.sets.push(SetVal::Content(buf));
446        self
447    }
448    pub fn returning(mut self, r: Returning) -> Self {
449        self.returning = r;
450        self
451    }
452
453    /// Follow this `UPDATE` with a reselecting [`Select`], joined as a `;`-separated
454    /// batch. See [`Create::then_select`] for motivation.
455    pub fn then_select(self, select: Select<T>) -> String {
456        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
457    }
458
459    pub fn to_surrealql(&self) -> String {
460        let mut q = String::from(self.verb);
461        q.push(' ');
462        self.target.render(&mut q);
463        // SurrealQL order: SET/MERGE/CONTENT first, then WHERE, then RETURN.
464        let mut set_pairs = Vec::new();
465        let mut merge_clause = None;
466        let mut content_clause = None;
467        for s in &self.sets {
468            match s {
469                SetVal::Assign(k, v) => set_pairs.push(format!("{k} = {v}")),
470                SetVal::Merge(v) => merge_clause = Some(v.clone()),
471                SetVal::Content(v) => content_clause = Some(v.clone()),
472            }
473        }
474        if let Some(c) = content_clause {
475            q.push_str(" CONTENT ");
476            q.push_str(&c);
477        } else if let Some(m) = merge_clause {
478            q.push_str(" MERGE ");
479            q.push_str(&m);
480        } else if !set_pairs.is_empty() {
481            q.push_str(" SET ");
482            q.push_str(&set_pairs.join(", "));
483        }
484        if let Some(ref f) = self.filter {
485            q.push_str(" WHERE ");
486            f.render_dyn(&mut q);
487        }
488        self.returning.render(&mut q);
489        q
490    }
491}
492
493impl<T: SurrealRecord> std::fmt::Display for Update<T> {
494    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495        write!(f, "{}", self.to_surrealql())
496    }
497}
498
499// ═══════════════════════════════════════════════════════════════════════════════
500// CREATE
501// ═══════════════════════════════════════════════════════════════════════════════
502
503enum CreateBody {
504    /// `CONTENT <expr>`
505    Content(String),
506    /// `SET a = x, b = y`
507    Set(Vec<(String, String)>),
508}
509
510/// `CREATE <target> [CONTENT … | SET …] [RETURN …]`.
511pub struct Create<T: SurrealRecord> {
512    _marker: std::marker::PhantomData<T>,
513    target: Target,
514    body: CreateBody,
515    returning: Returning,
516}
517
518impl<T: SurrealRecord> Create<T> {
519    pub(crate) fn for_table() -> Self {
520        Self {
521            _marker: std::marker::PhantomData,
522            target: Target::Table(T::table_name()),
523            body: CreateBody::Set(Vec::new()),
524            returning: Returning::None,
525        }
526    }
527
528    /// Target a single record id: `CREATE type::record('table', <id>)`.
529    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
530        self.target = Target::Record(RecordLink::new(T::table_name(), id));
531        self
532    }
533
534    /// `CONTENT <expr>` — replaces any accumulated SET pairs.
535    pub fn content(mut self, expr: impl DynExpr) -> Self {
536        let mut buf = String::new();
537        expr.render_dyn(&mut buf);
538        self.body = CreateBody::Content(buf);
539        self
540    }
541
542    /// `SET col = <literal>`.
543    pub fn set_lit<C: SurrealQL>(mut self, col: &str, value: C) -> Self {
544        let mut buf = String::new();
545        C::render_literal(&value, &mut buf);
546        self.push_set(col, buf);
547        self
548    }
549    /// `SET col = <expr>`.
550    pub fn set_expr(mut self, col: &str, expr: impl DynExpr) -> Self {
551        let mut buf = String::new();
552        expr.render_dyn(&mut buf);
553        self.push_set(col, buf);
554        self
555    }
556    /// `SET col = <raw SurrealQL>`.
557    pub fn set_raw(mut self, col: &str, raw: impl Into<String>) -> Self {
558        self.push_set(col, raw.into());
559        self
560    }
561
562    fn push_set(&mut self, col: &str, rendered: String) {
563        match &mut self.body {
564            CreateBody::Set(v) => v.push((col.to_string(), rendered)),
565            CreateBody::Content(_) => {
566                self.body = CreateBody::Set(vec![(col.to_string(), rendered)]);
567            }
568        }
569    }
570
571    pub fn returning(mut self, r: Returning) -> Self {
572        self.returning = r;
573        self
574    }
575
576    /// Follow this `CREATE` with a reselecting [`Select`], joined as a `;`-separated
577    /// batch. The select is rendered immediately, producing a complete SurrealQL
578    /// string ready for `db.query()`.
579    ///
580    /// This replaces the manual `Batch::new().push(create).push(select).to_surrealql()`
581    /// pattern for mutate-then-reselect workflows.
582    pub fn then_select(self, select: Select<T>) -> String {
583        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
584    }
585
586    pub fn to_surrealql(&self) -> String {
587        let mut q = String::from("CREATE ");
588        self.target.render(&mut q);
589        match &self.body {
590            CreateBody::Content(c) => {
591                q.push_str(" CONTENT ");
592                q.push_str(c);
593            }
594            CreateBody::Set(pairs) if !pairs.is_empty() => {
595                q.push_str(" SET ");
596                q.push_str(
597                    &pairs
598                        .iter()
599                        .map(|(k, v)| format!("{k} = {v}"))
600                        .collect::<Vec<_>>()
601                        .join(", "),
602                );
603            }
604            CreateBody::Set(_) => {}
605        }
606        self.returning.render(&mut q);
607        q
608    }
609}
610
611impl<T: SurrealRecord> std::fmt::Display for Create<T> {
612    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613        write!(f, "{}", self.to_surrealql())
614    }
615}
616
617// ═══════════════════════════════════════════════════════════════════════════════
618// DELETE
619// ═══════════════════════════════════════════════════════════════════════════════
620
621/// A `DELETE <target> [WHERE …] [RETURN …]` builder.
622pub struct Delete<T: SurrealRecord> {
623    _marker: std::marker::PhantomData<T>,
624    target: Target,
625    filter: Option<Box<dyn DynExpr>>,
626    returning: Returning,
627}
628
629impl<T: SurrealRecord> Delete<T> {
630    pub(crate) fn for_table() -> Self {
631        Self {
632            _marker: std::marker::PhantomData,
633            target: Target::Table(T::table_name()),
634            filter: None,
635            returning: Returning::None,
636        }
637    }
638    /// Target a single record: `DELETE type::record('table', <id>)`.
639    pub fn record<V: SurrealQL>(mut self, id: V) -> Self {
640        self.target = Target::Record(RecordLink::new(T::table_name(), id));
641        self
642    }
643    pub fn filter(mut self, expr: impl DynExpr + 'static) -> Self {
644        self.filter = Some(Box::new(expr));
645        self
646    }
647    pub fn returning(mut self, r: Returning) -> Self {
648        self.returning = r;
649        self
650    }
651
652    /// Follow this `DELETE` with a reselecting [`Select`], joined as a `;`-separated
653    /// batch. See [`Create::then_select`] for motivation.
654    pub fn then_select(self, select: Select<T>) -> String {
655        format!("{};\n{}", self.to_surrealql(), select.to_surrealql())
656    }
657
658    pub fn to_surrealql(&self) -> String {
659        let mut q = String::from("DELETE ");
660        self.target.render(&mut q);
661        if let Some(ref f) = self.filter {
662            q.push_str(" WHERE ");
663            f.render_dyn(&mut q);
664        }
665        self.returning.render(&mut q);
666        q
667    }
668}
669
670impl<T: SurrealRecord> std::fmt::Display for Delete<T> {
671    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672        write!(f, "{}", self.to_surrealql())
673    }
674}
675
676// ═══════════════════════════════════════════════════════════════════════════════
677// Batch — multiple statements joined by `;` (mutate-then-reselect pattern)
678// ═══════════════════════════════════════════════════════════════════════════════
679
680/// Concatenates SurrealQL statements with `;` separators. The store's typical
681/// pattern is a mutation followed by a SELECT that re-projects the row.
682#[derive(Default)]
683pub struct Batch {
684    statements: Vec<String>,
685}
686
687impl Batch {
688    pub fn new() -> Self {
689        Self {
690            statements: Vec::new(),
691        }
692    }
693    pub fn push(mut self, stmt: impl ToString) -> Self {
694        self.statements.push(stmt.to_string());
695        self
696    }
697    pub fn to_surrealql(&self) -> String {
698        self.statements.join(";\n")
699    }
700    /// Number of statements (useful for `.take(n)` indexing on the response).
701    pub fn len(&self) -> usize {
702        self.statements.len()
703    }
704    pub fn is_empty(&self) -> bool {
705        self.statements.is_empty()
706    }
707}
708
709impl std::fmt::Display for Batch {
710    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
711        write!(f, "{}", self.to_surrealql())
712    }
713}
714
715// ═══════════════════════════════════════════════════════════════════════════════
716// RELATE — graph edges
717// ═══════════════════════════════════════════════════════════════════════════════
718
719/// Render a record's id as `table:<escaped-key>` into `buf`.
720fn record_id(thing: &Thing<impl SurrealRecord>, buf: &mut String) {
721    buf.push_str(thing.table());
722    buf.push(':');
723    thing.key.render_id(buf);
724}
725
726/// Return a record's id as a `table:<escaped-key>` string.
727fn record_id_string(thing: &Thing<impl SurrealRecord>) -> String {
728    let mut s = String::new();
729    record_id(thing, &mut s);
730    s
731}
732
733/// Builds a graph edge statement `RELATE a -> edge -> b` for an edge type `E`.
734/// For edges that carry their own fields, see [`RelateEdge`].
735pub struct Relate<E: SurrealEdge> {
736    _marker: std::marker::PhantomData<E>,
737}
738
739impl<E: SurrealEdge> Relate<E> {
740    pub fn new() -> Self {
741        Self {
742            _marker: std::marker::PhantomData,
743        }
744    }
745
746    pub fn to_surrealql(
747        from: &Thing<impl SurrealRecord>,
748        to: &Thing<impl SurrealRecord>,
749    ) -> String {
750        let mut q = String::from("RELATE ");
751        record_id(from, &mut q);
752        q.push_str(" -> ");
753        q.push_str(E::edge_name());
754        q.push_str(" -> ");
755        record_id(to, &mut q);
756        q
757    }
758}
759
760impl<E: SurrealEdge> Default for Relate<E> {
761    fn default() -> Self {
762        Self::new()
763    }
764}
765
766// ═══════════════════════════════════════════════════════════════════════════════
767// RELATE with content
768// ═══════════════════════════════════════════════════════════════════════════════
769
770/// Build a RELATE query with edge content.
771///
772/// ```ignore
773/// RelateEdge::<Follows>::from(user).to(other).content(Follows { since: now }).build()
774/// ```
775pub struct RelateEdge<E: SurrealEdge> {
776    _marker: std::marker::PhantomData<E>,
777    from_label: String,
778    to_label: String,
779    content_json: Option<serde_json::Value>,
780}
781
782impl<E: SurrealEdge> RelateEdge<E> {
783    pub fn from(from: &Thing<impl SurrealRecord>) -> Self {
784        Self {
785            _marker: std::marker::PhantomData,
786            from_label: record_id_string(from),
787            to_label: String::new(),
788            content_json: None,
789        }
790    }
791
792    pub fn to(mut self, to: &Thing<impl SurrealRecord>) -> Self {
793        self.to_label = record_id_string(to);
794        self
795    }
796
797    /// Attach content to the edge record.
798    pub fn content(mut self, edge: &impl serde::Serialize) -> Self {
799        self.content_json = serde_json::to_value(edge).ok();
800        self
801    }
802
803    pub fn build(&self) -> String {
804        let mut q = format!(
805            "RELATE {} -> {} -> {}",
806            self.from_label,
807            E::edge_name(),
808            self.to_label
809        );
810        if let Some(ref c) = self.content_json {
811            q.push_str(&format!(
812                " CONTENT {}",
813                serde_json::to_string(c).unwrap_or_default()
814            ));
815        }
816        q
817    }
818}