Skip to main content

vantage_table/table/impls/
refereces.rs

1//! Table relationship methods for defining and traversing references.
2
3use ciborium::Value as CborValue;
4use indexmap::IndexMap;
5use std::sync::Arc;
6
7use vantage_core::{Result, error};
8use vantage_dataset::WritableValueSet;
9use vantage_expressions::Expression;
10use vantage_types::{EmptyEntity, Entity, Record};
11
12use crate::{
13    column::flags::ColumnFlag,
14    references::{ContainedRelation, HasMany, HasOne, Reference},
15    table::Table,
16    traits::{column_like::ColumnLike, table_source::TableSource},
17};
18
19impl<T: TableSource + 'static, E: Entity<T::Value> + 'static> Table<T, E> {
20    /// Define a one-to-one relationship.
21    ///
22    /// ```rust,ignore
23    /// .with_one("bakery", "bakery_id", Bakery::postgres_table)
24    /// ```
25    pub fn with_one<E2: Entity<T::Value> + 'static>(
26        mut self,
27        relation: &str,
28        foreign_key: &str,
29        build_target: impl Fn(T) -> Table<T, E2> + Send + Sync + 'static,
30    ) -> Self
31    where
32        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
33        T::Id: std::fmt::Display + From<String>,
34    {
35        let reference = HasOne::<T, E, E2>::new(foreign_key, build_target);
36        self.add_ref(relation, Box::new(reference));
37        self
38    }
39
40    /// Define a one-to-many relationship.
41    ///
42    /// ```rust,ignore
43    /// .with_many("orders", "client_id", Order::postgres_table)
44    /// ```
45    pub fn with_many<E2: Entity<T::Value> + 'static>(
46        mut self,
47        relation: &str,
48        foreign_key: &str,
49        build_target: impl Fn(T) -> Table<T, E2> + Send + Sync + 'static,
50    ) -> Self
51    where
52        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
53        T::Id: std::fmt::Display + From<String>,
54    {
55        let reference = HasMany::<T, E, E2>::new(foreign_key, build_target);
56        self.add_ref(relation, Box::new(reference));
57        self
58    }
59
60    /// Declare a `contains_one` relation: a single record embedded in
61    /// `host_column` (e.g. a product's `inventory` object), surfaced as a
62    /// sub-`Vista`. `build_target` builds the contained record's table —
63    /// the same closure shape as [`with_one`](Self::with_one).
64    ///
65    /// ```rust,ignore
66    /// .with_contained_one("inventory", "inventory", |db| {
67    ///     Table::new("inventory", db).with_column_of::<i64>("stock")
68    /// })
69    /// ```
70    pub fn with_contained_one(
71        mut self,
72        relation: &str,
73        host_column: &str,
74        build_target: impl Fn(T) -> Table<T, EmptyEntity> + Send + Sync + 'static,
75    ) -> Self {
76        self.contained.push(ContainedRelation::new(
77            relation,
78            host_column,
79            vantage_vista::ContainedKind::ContainsOne,
80            None,
81            build_target,
82        ));
83        self
84    }
85
86    /// Declare a `contains_many` relation: an array of records embedded in
87    /// `host_column` (e.g. an order's `lines`). `build_target` builds the
88    /// contained record's table; `id_column` names the field used as each
89    /// record's id (`None` → positional index).
90    ///
91    /// ```rust,ignore
92    /// .with_contained_many("lines", "lines", |db| {
93    ///     Table::new("lines", db)
94    ///         .with_column_of::<Thing>("product")
95    ///         .with_column_of::<i64>("quantity")
96    /// }, None)
97    /// ```
98    pub fn with_contained_many(
99        mut self,
100        relation: &str,
101        host_column: &str,
102        build_target: impl Fn(T) -> Table<T, EmptyEntity> + Send + Sync + 'static,
103        id_column: Option<&str>,
104    ) -> Self {
105        self.contained.push(ContainedRelation::new(
106            relation,
107            host_column,
108            vantage_vista::ContainedKind::ContainsMany,
109            id_column.map(str::to_string),
110            build_target,
111        ));
112        self
113    }
114
115    /// Harvest this table's columns as Vista columns (name + declared type,
116    /// hidden flag). Used to give a contained sub-Vista its schema.
117    pub fn vista_columns(&self) -> Vec<vantage_vista::Column>
118    where
119        T::Column<T::AnyType>: ColumnLike<T::AnyType>,
120    {
121        self.columns()
122            .iter()
123            .map(|(name, col)| {
124                let mut vc = vantage_vista::Column::new(name.clone(), col.get_type().to_string());
125                if col.flags().contains(&ColumnFlag::Hidden) {
126                    vc = vc.hidden();
127                }
128                // Imported implicit-reference columns are read-only: flag them
129                // `calculated` so consumers render them read-only and keep them
130                // out of forms/write payloads (mirrors the write-side strip).
131                if self.imported_columns.contains(name) {
132                    vc = vc.with_flag(vantage_vista::flags::CALCULATED);
133                }
134                vc
135            })
136            .collect()
137    }
138
139    /// Resolve a contained relation embedded in `row` into a sub-`Vista`.
140    ///
141    /// This is the backend-agnostic skeleton every driver shares: seed the
142    /// embedded records, wire the eager writeback (patch the host column on the
143    /// parent row), and the traverse-out resolver. The driver supplies only the
144    /// three things it alone knows:
145    /// - `parent_id` — the row's id in the driver's native id type;
146    /// - `wrap` — turn a target `Table` into a `Vista` via the driver's factory
147    ///   (used when a contained record traverses out to a real table);
148    /// - `decode_host` / `encode_host` — the host-column codec: native
149    ///   passthrough, or JSON parse/serialize for backends without nested
150    ///   columns.
151    #[allow(clippy::too_many_arguments)]
152    pub fn get_contained_ref(
153        &self,
154        relation: &str,
155        row: &Record<CborValue>,
156        parent_id: T::Id,
157        wrap: impl Fn(Table<T, EmptyEntity>) -> Result<vantage_vista::Vista> + Send + Sync + 'static,
158        decode_host: impl Fn(&CborValue) -> Option<CborValue>,
159        encode_host: impl Fn(CborValue) -> CborValue + Send + Sync + 'static,
160    ) -> Result<vantage_vista::Vista>
161    where
162        T::Value: From<CborValue> + Send + Sync + vantage_types::InvariantValue,
163        T::Id: Clone + Send + Sync,
164        T::Column<T::AnyType>: ColumnLike<T::AnyType>,
165    {
166        let rel = self
167            .contained_relation(relation)
168            .ok_or_else(|| error!("unknown contained relation", relation = relation))?;
169        let host_value = row.get(rel.host_column()).and_then(decode_host);
170
171        let contained_table = rel.build_target(self.data_source().clone());
172        let mut spec = vantage_vista::ContainedSpec::new(rel.name(), rel.host_column(), rel.kind());
173        if let Some(id) = rel.id_column() {
174            spec = spec.with_id_column(id);
175        }
176        spec = spec.with_columns(contained_table.vista_columns());
177
178        let host_column = rel.host_column().to_string();
179        let parent_table = self.clone();
180        let writeback: vantage_vista::ContainedWriteback =
181            Arc::new(move |collection: CborValue| {
182                let parent_table = parent_table.clone();
183                let host_column = host_column.clone();
184                let parent_id = parent_id.clone();
185                let value = T::Value::from(encode_host(collection));
186                Box::pin(async move {
187                    let mut patch: Record<T::Value> = Record::new();
188                    patch.insert(host_column, value);
189                    parent_table.patch_value(parent_id.clone(), &patch).await?;
190                    Ok(())
191                })
192            });
193
194        let ref_resolver: vantage_vista::ContainedRefResolver =
195            Arc::new(move |relation: &str, child_row: &Record<CborValue>| {
196                let native: Record<T::Value> = child_row
197                    .iter()
198                    .map(|(k, v)| (k.clone(), T::Value::from(v.clone())))
199                    .collect();
200                let target = contained_table.get_ref_from_row::<EmptyEntity>(relation, &native)?;
201                wrap(target)
202            });
203
204        vantage_vista::build_contained_vista(
205            &spec,
206            host_value.as_ref(),
207            writeback,
208            Some(ref_resolver),
209        )
210    }
211
212    /// Lower a YAML `contained:` section into `with_contained_*` registrations,
213    /// reusing the driver's `build_col` to construct each contained record's
214    /// columns. Column-build errors surface here; the per-relation target
215    /// closure stays infallible by cloning the pre-built columns.
216    pub fn with_contained_specs<C>(
217        mut self,
218        specs: &IndexMap<String, vantage_vista::ContainedYaml<C>>,
219        build_col: impl Fn(&str, &vantage_vista::ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
220    ) -> Result<Self>
221    where
222        T::Column<T::AnyType>: Clone,
223    {
224        for (relation, c) in specs {
225            let cols = c
226                .columns
227                .iter()
228                .map(|(n, cs)| build_col(n, cs))
229                .collect::<Result<Vec<_>>>()?;
230            let rel = relation.clone();
231            let host = c.host_column.clone();
232            let build = move |db: T| {
233                let mut t = Table::<T, EmptyEntity>::new(rel.clone(), db);
234                for col in &cols {
235                    t.add_column(col.clone());
236                }
237                t
238            };
239            self = match c.kind {
240                vantage_vista::ContainedKind::ContainsOne => {
241                    self.with_contained_one(relation, &host, build)
242                }
243                vantage_vista::ContainedKind::ContainsMany => {
244                    self.with_contained_many(relation, &host, build, c.id_column.as_deref())
245                }
246            };
247        }
248        Ok(self)
249    }
250
251    pub(crate) fn add_ref(&mut self, relation: &str, reference: Box<dyn Reference>) {
252        self.add_ref_arc(relation, Arc::from(reference));
253    }
254
255    /// Insert an already-shared reference. Used to inherit relations when
256    /// deriving a table from another (the same `Arc` is shared, not rebuilt).
257    pub(crate) fn add_ref_arc(&mut self, relation: &str, reference: Arc<dyn Reference>) {
258        self.refs
259            .get_or_insert_with(IndexMap::new)
260            .insert(relation.to_string(), reference);
261    }
262
263    /// Borrow this table's relations, if any.
264    pub(crate) fn refs_ref(&self) -> Option<&IndexMap<String, Arc<dyn Reference>>> {
265        self.refs.as_ref()
266    }
267
268    /// Copy relations from another table, sharing the underlying `Arc`s. With
269    /// `names = None`, copies all relations; otherwise only the listed ones.
270    /// An inherited relation keeps working as long as the derived table still
271    /// projects the column its foreign key references.
272    pub fn copy_relations_from<E2: Entity<T::Value> + 'static>(
273        &mut self,
274        other: &Table<T, E2>,
275        names: Option<&[&str]>,
276    ) {
277        let Some(refs) = other.refs_ref() else {
278            return;
279        };
280        for (name, reference) in refs {
281            if names.is_some_and(|ns| !ns.contains(&name.as_str())) {
282                continue;
283            }
284            self.add_ref_arc(name, reference.clone());
285        }
286    }
287
288    pub fn references(&self) -> Vec<String> {
289        self.refs
290            .as_ref()
291            .map(|refs| refs.keys().cloned().collect())
292            .unwrap_or_default()
293    }
294
295    /// Narrow the table to a single row by id.
296    ///
297    /// Pairs with `get_some_value` for the "I only know an id" workflow.
298    /// The actual condition construction goes through
299    /// `TableSource::eq_value_condition`, so backends that don't yet
300    /// implement that path return an error here.
301    pub fn with_id(mut self, id: impl Into<T::Value>) -> Result<Self> {
302        let id_name = self
303            .id_field()
304            .ok_or_else(|| error!("id field not set on table"))?
305            .name()
306            .to_string();
307        let id = id.into();
308        let condition = self
309            .data_source()
310            .eq_value_condition(&id_name, id.clone())?;
311        self.add_condition(condition);
312        // A row inserted into "this id" should conform to it.
313        self.add_invariant(id_name, id);
314        Ok(self)
315    }
316
317    /// Traverse a same-persistence reference using a known source row as the
318    /// join origin.
319    ///
320    /// Reads the join field value out of `row`, builds the target table via
321    /// the reference's stored factory, and applies one eq-condition that
322    /// selects the related rows. No subquery, no deferred fetch — `row`
323    /// already carries the value.
324    ///
325    /// `HasOne` reads from its stored foreign-key column; `HasMany` reads
326    /// from the source's id field (looked up here and forwarded into the
327    /// reference). The returned table preserves columns, refs, and
328    /// expressions from the reference's factory; only the entity type
329    /// changes if `E2` differs from the factory's output.
330    pub fn get_ref_from_row<E2: Entity<T::Value> + 'static>(
331        &self,
332        relation: &str,
333        row: &Record<T::Value>,
334    ) -> Result<Table<T, E2>> {
335        let (reference, _) = self.lookup_ref(relation)?;
336        let source_id = self
337            .id_field()
338            .map(|c| c.name().to_string())
339            .unwrap_or_else(|| "id".to_string());
340
341        let target_dyn = reference.resolve_from_row(
342            self.data_source() as &dyn std::any::Any,
343            &source_id,
344            row as &dyn std::any::Any,
345        )?;
346
347        let target_empty: Table<T, EmptyEntity> =
348            *target_dyn
349                .downcast::<Table<T, EmptyEntity>>()
350                .map_err(|_| error!("Failed to downcast target table to Table<T, EmptyEntity>"))?;
351
352        Ok(target_empty.into_entity::<E2>())
353    }
354
355    /// Traverse a same-backend relation into a typed `Table<T, E2>` with an
356    /// `IN (subquery)` filter on the source column.
357    ///
358    /// Use this when the parent table already carries the narrowing
359    /// conditions (e.g. `clients.add_condition(is_paying = true)`) and you
360    /// want every related child row matching that filter. For the
361    /// "I have a specific row in hand" case, prefer
362    /// [`Table::get_ref_from_row`] — it pushes a plain eq-condition
363    /// instead of a subquery.
364    pub fn get_ref_as<E2: Entity<T::Value> + 'static>(
365        &self,
366        relation: &str,
367    ) -> Result<Table<T, E2>> {
368        let (reference, relation_str) = self.lookup_ref(relation)?;
369
370        let source_id = self
371            .id_field()
372            .map(|c| c.name().to_string())
373            .unwrap_or_else(|| "id".to_string());
374
375        let mut target: Table<T, E2> = *reference
376            .build_target(self.data_source() as &dyn std::any::Any)
377            .downcast::<Table<T, E2>>()
378            .map_err(|_| {
379                error!(
380                    "Failed to downcast related table",
381                    relation = relation_str.as_str()
382                )
383            })?;
384
385        let target_id = target
386            .id_field()
387            .map(|c| c.name().to_string())
388            .unwrap_or_else(|| "id".to_string());
389
390        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
391
392        let condition = self
393            .data_source()
394            .related_in_condition(&tgt_col, self, &src_col);
395        target.add_condition(condition);
396
397        Ok(target)
398    }
399
400    /// Get a correlated related table for use inside SELECT expressions.
401    ///
402    /// Unlike [`Self::get_ref_as`] (which uses `IN (subquery)`), this produces a
403    /// correlated condition like `order.client_id = client.id`, suitable
404    /// for embedding as a subquery in a SELECT clause via
405    /// [`Self::with_expression`].
406    pub fn get_subquery_as<E2: Entity<T::Value> + 'static>(
407        &self,
408        relation: &str,
409    ) -> Result<Table<T, E2>> {
410        let (reference, relation_str) = self.lookup_ref(relation)?;
411        let mut target: Table<T, E2> = *reference
412            .build_target(self.data_source() as &dyn std::any::Any)
413            .downcast::<Table<T, E2>>()
414            .map_err(|_| {
415                error!(
416                    "Failed to downcast related table",
417                    relation = relation_str.as_str()
418                )
419            })?;
420        self.attach_correlated_condition(reference, &mut target);
421        Ok(target)
422    }
423
424    /// Entity-erased [`get_subquery_as`](Self::get_subquery_as): builds the
425    /// correlated target as `Table<T, EmptyEntity>` without the caller naming
426    /// the target's concrete entity type. Used by implicit-reference traversal,
427    /// which walks relations by name and cannot know each hop's entity type.
428    pub fn get_subquery_erased(&self, relation: &str) -> Result<Table<T, EmptyEntity>> {
429        let (reference, relation_str) = self.lookup_ref(relation)?;
430        let mut target: Table<T, EmptyEntity> = *reference
431            .build_target_erased(self.data_source() as &dyn std::any::Any)
432            .downcast::<Table<T, EmptyEntity>>()
433            .map_err(|_| {
434                error!(
435                    "Failed to downcast related table",
436                    relation = relation_str.as_str()
437                )
438            })?;
439        self.attach_correlated_condition(reference, &mut target);
440        Ok(target)
441    }
442
443    /// Attach the correlated join condition binding `target` back to `self`
444    /// (`target.<id> = self.<foreign_key>`). Shared by the typed and erased
445    /// subquery builders, which otherwise differ only in the target entity type
446    /// and which `build_target*` closure produced `target`.
447    fn attach_correlated_condition<E2: Entity<T::Value> + 'static>(
448        &self,
449        reference: &dyn Reference,
450        target: &mut Table<T, E2>,
451    ) {
452        let source_id = self
453            .id_field()
454            .map(|c| c.name().to_string())
455            .unwrap_or_else(|| "id".to_string());
456        let target_id = target
457            .id_field()
458            .map(|c| c.name().to_string())
459            .unwrap_or_else(|| "id".to_string());
460        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
461        let condition = self.data_source().related_correlated_condition(
462            target.table_name(),
463            &tgt_col,
464            self.table_name(),
465            &src_col,
466        );
467        target.add_condition(condition);
468    }
469
470    /// Entity-erased [`get_ref_target`](Self::get_ref_target): the bare target
471    /// (no condition) as `Table<T, EmptyEntity>`. Used to walk an
472    /// implicit-reference chain and read the final target's column definitions.
473    pub fn get_ref_target_erased(&self, relation: &str) -> Result<Table<T, EmptyEntity>> {
474        let (reference, relation_str) = self.lookup_ref(relation)?;
475        let target: Table<T, EmptyEntity> = *reference
476            .build_target_erased(self.data_source() as &dyn std::any::Any)
477            .downcast::<Table<T, EmptyEntity>>()
478            .map_err(|_| {
479                error!(
480                    "Failed to downcast related table",
481                    relation = relation_str.as_str()
482                )
483            })?;
484        Ok(target)
485    }
486
487    /// Build the relation's target table with **no condition** applied.
488    ///
489    /// Unlike [`Self::get_ref_from_row`] / [`Self::get_ref_as`] (which select
490    /// the related rows for a known parent), this returns the bare target — the
491    /// table you'd insert a new related row into. Used by Vista's nested insert
492    /// to obtain the destination for a has-one/has-many child before any join
493    /// value exists.
494    pub fn get_ref_target<E2: Entity<T::Value> + 'static>(
495        &self,
496        relation: &str,
497    ) -> Result<Table<T, E2>> {
498        let (reference, relation_str) = self.lookup_ref(relation)?;
499        let target: Table<T, E2> = *reference
500            .build_target(self.data_source() as &dyn std::any::Any)
501            .downcast::<Table<T, E2>>()
502            .map_err(|_| {
503                error!(
504                    "Failed to downcast related table",
505                    relation = relation_str.as_str()
506                )
507            })?;
508        Ok(target)
509    }
510
511    /// Add a computed expression field using builder pattern.
512    ///
513    /// The closure receives `&Table<T, E>` and returns an `Expression<T::Value>`.
514    /// It is evaluated lazily when `select()` builds the query.
515    pub fn with_expression(
516        mut self,
517        name: &str,
518        expr_fn: impl Fn(&Table<T, E>) -> Expression<T::Value> + Send + Sync + 'static,
519    ) -> Self
520    where
521        T: 'static,
522        E: 'static,
523    {
524        // Adapt the caller's `Fn(&Table<T, E>)` into the entity-erased
525        // `ExpressionFn<T>` we store, so it survives `into_entity`. The closure
526        // never touches entity-typed fields, only name-keyed table state.
527        let wrapped: crate::table::base::ExpressionFn<T> =
528            Arc::new(move |erased: &Table<T, vantage_types::EmptyEntity>| {
529                // SAFETY: layout-identical (E is PhantomData only); shared borrow.
530                let typed: &Table<T, E> = unsafe {
531                    &*(erased as *const Table<T, vantage_types::EmptyEntity> as *const Table<T, E>)
532                };
533                expr_fn(typed)
534            });
535        self.expressions.insert(name.to_string(), wrapped);
536        self
537    }
538
539    /// Add a lazy computed column using builder pattern.
540    ///
541    /// Unlike [`Self::with_expression`] (which lowers into the data source's
542    /// query), a lazy expression runs in Rust on the record the data source
543    /// *returned*. Callbacks apply in declaration order: each borrows the
544    /// record as built so far, and the value it returns is inserted under
545    /// `name` before the next callback runs — so one expensive fetch (a
546    /// file's contents) can feed several cheap derived columns declared
547    /// after it.
548    ///
549    /// The name is also registered as a column, so the computed field shows
550    /// up in the table's schema (and in Vista metadata derived from it).
551    /// Because the callback's future must not borrow the record, clone what
552    /// you need from `record` before going async.
553    ///
554    /// Lazy expressions run on the `list`/`get` read paths (both raw-record
555    /// and entity forms). They never participate in queries — no conditions,
556    /// ordering, or pushdown — they exist only on returned data.
557    pub fn with_lazy_expression<F, Fut>(mut self, name: &str, f: F) -> Self
558    where
559        F: Fn(&vantage_types::Record<T::Value>) -> Fut + Send + Sync + 'static,
560        Fut: std::future::Future<Output = Result<T::Value>> + Send + 'static,
561    {
562        if !self.columns.contains_key(name) {
563            let column = self.data_source.create_column::<T::AnyType>(name);
564            self.add_column(column);
565        }
566        self.add_lazy_expression(name, Arc::new(move |record| Box::pin(f(record))));
567        self
568    }
569
570    /// Register a pre-boxed lazy expression callback under `name`, without
571    /// touching the column set. Factories lowering a spec (which declares
572    /// its columns separately) use this; application code should prefer
573    /// [`Self::with_lazy_expression`].
574    pub fn add_lazy_expression(&mut self, name: &str, f: crate::table::base::LazyExpressionFn<T>) {
575        self.lazy_expressions.insert(name.to_string(), f);
576    }
577
578    fn lookup_ref(&self, relation: &str) -> Result<(&dyn Reference, String)> {
579        let table_name = self.table_name().to_string();
580        let refs = self.refs.as_ref().ok_or_else(|| {
581            error!(
582                "No references defined on table",
583                table = table_name.as_str()
584            )
585        })?;
586
587        let relation_str = relation.to_string();
588        let reference = refs.get(relation).ok_or_else(|| {
589            error!(
590                "Reference not found on table",
591                relation = relation_str.as_str(),
592                table = table_name.as_str()
593            )
594        })?;
595
596        Ok((reference.as_ref(), relation_str))
597    }
598
599    /// Look up cardinality for a registered relation.
600    pub fn ref_cardinality(&self, relation: &str) -> Result<vantage_vista::ReferenceKind> {
601        let (reference, _) = self.lookup_ref(relation)?;
602        Ok(reference.cardinality())
603    }
604
605    /// Look up the foreign-key/link field for a registered relation. For a
606    /// `has_one` this is the source-side column carrying the link — the field
607    /// a backend-native traversal (e.g. a SurrealDB idiom path) descends
608    /// through, which need not match the relation's registry name.
609    pub fn ref_foreign_key(&self, relation: &str) -> Result<String> {
610        let (reference, _) = self.lookup_ref(relation)?;
611        Ok(reference.foreign_key().to_string())
612    }
613
614    /// List all registered relations with their cardinality.
615    pub fn ref_kinds(&self) -> Vec<(String, vantage_vista::ReferenceKind)> {
616        self.refs
617            .as_ref()
618            .map(|refs| {
619                refs.iter()
620                    .map(|(name, r)| (name.clone(), r.cardinality()))
621                    .collect()
622            })
623            .unwrap_or_default()
624    }
625}