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                vc
129            })
130            .collect()
131    }
132
133    /// Resolve a contained relation embedded in `row` into a sub-`Vista`.
134    ///
135    /// This is the backend-agnostic skeleton every driver shares: seed the
136    /// embedded records, wire the eager writeback (patch the host column on the
137    /// parent row), and the traverse-out resolver. The driver supplies only the
138    /// three things it alone knows:
139    /// - `parent_id` — the row's id in the driver's native id type;
140    /// - `wrap` — turn a target `Table` into a `Vista` via the driver's factory
141    ///   (used when a contained record traverses out to a real table);
142    /// - `decode_host` / `encode_host` — the host-column codec: native
143    ///   passthrough, or JSON parse/serialize for backends without nested
144    ///   columns.
145    #[allow(clippy::too_many_arguments)]
146    pub fn get_contained_ref(
147        &self,
148        relation: &str,
149        row: &Record<CborValue>,
150        parent_id: T::Id,
151        wrap: impl Fn(Table<T, EmptyEntity>) -> Result<vantage_vista::Vista> + Send + Sync + 'static,
152        decode_host: impl Fn(&CborValue) -> Option<CborValue>,
153        encode_host: impl Fn(CborValue) -> CborValue + Send + Sync + 'static,
154    ) -> Result<vantage_vista::Vista>
155    where
156        T::Value: From<CborValue> + Send + Sync,
157        T::Id: Clone + Send + Sync,
158        T::Column<T::AnyType>: ColumnLike<T::AnyType>,
159    {
160        let rel = self
161            .contained_relation(relation)
162            .ok_or_else(|| error!("unknown contained relation", relation = relation))?;
163        let host_value = row.get(rel.host_column()).and_then(decode_host);
164
165        let contained_table = rel.build_target(self.data_source().clone());
166        let mut spec = vantage_vista::ContainedSpec::new(rel.name(), rel.host_column(), rel.kind());
167        if let Some(id) = rel.id_column() {
168            spec = spec.with_id_column(id);
169        }
170        spec = spec.with_columns(contained_table.vista_columns());
171
172        let host_column = rel.host_column().to_string();
173        let parent_table = self.clone();
174        let writeback: vantage_vista::ContainedWriteback =
175            Arc::new(move |collection: CborValue| {
176                let parent_table = parent_table.clone();
177                let host_column = host_column.clone();
178                let parent_id = parent_id.clone();
179                let value = T::Value::from(encode_host(collection));
180                Box::pin(async move {
181                    let mut patch: Record<T::Value> = Record::new();
182                    patch.insert(host_column, value);
183                    parent_table.patch_value(&parent_id, &patch).await?;
184                    Ok(())
185                })
186            });
187
188        let ref_resolver: vantage_vista::ContainedRefResolver =
189            Arc::new(move |relation: &str, child_row: &Record<CborValue>| {
190                let native: Record<T::Value> = child_row
191                    .iter()
192                    .map(|(k, v)| (k.clone(), T::Value::from(v.clone())))
193                    .collect();
194                let target = contained_table.get_ref_from_row::<EmptyEntity>(relation, &native)?;
195                wrap(target)
196            });
197
198        vantage_vista::build_contained_vista(
199            &spec,
200            host_value.as_ref(),
201            writeback,
202            Some(ref_resolver),
203        )
204    }
205
206    /// Lower a YAML `contained:` section into `with_contained_*` registrations,
207    /// reusing the driver's `build_col` to construct each contained record's
208    /// columns. Column-build errors surface here; the per-relation target
209    /// closure stays infallible by cloning the pre-built columns.
210    pub fn with_contained_specs<C>(
211        mut self,
212        specs: &IndexMap<String, vantage_vista::ContainedYaml<C>>,
213        build_col: impl Fn(&str, &vantage_vista::ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
214    ) -> Result<Self>
215    where
216        T::Column<T::AnyType>: Clone,
217    {
218        for (relation, c) in specs {
219            let cols = c
220                .columns
221                .iter()
222                .map(|(n, cs)| build_col(n, cs))
223                .collect::<Result<Vec<_>>>()?;
224            let rel = relation.clone();
225            let host = c.host_column.clone();
226            let build = move |db: T| {
227                let mut t = Table::<T, EmptyEntity>::new(rel.clone(), db);
228                for col in &cols {
229                    t.add_column(col.clone());
230                }
231                t
232            };
233            self = match c.kind {
234                vantage_vista::ContainedKind::ContainsOne => {
235                    self.with_contained_one(relation, &host, build)
236                }
237                vantage_vista::ContainedKind::ContainsMany => {
238                    self.with_contained_many(relation, &host, build, c.id_column.as_deref())
239                }
240            };
241        }
242        Ok(self)
243    }
244
245    pub(crate) fn add_ref(&mut self, relation: &str, reference: Box<dyn Reference>) {
246        self.add_ref_arc(relation, Arc::from(reference));
247    }
248
249    /// Insert an already-shared reference. Used to inherit relations when
250    /// deriving a table from another (the same `Arc` is shared, not rebuilt).
251    pub(crate) fn add_ref_arc(&mut self, relation: &str, reference: Arc<dyn Reference>) {
252        self.refs
253            .get_or_insert_with(IndexMap::new)
254            .insert(relation.to_string(), reference);
255    }
256
257    /// Borrow this table's relations, if any.
258    pub(crate) fn refs_ref(&self) -> Option<&IndexMap<String, Arc<dyn Reference>>> {
259        self.refs.as_ref()
260    }
261
262    /// Copy relations from another table, sharing the underlying `Arc`s. With
263    /// `names = None`, copies all relations; otherwise only the listed ones.
264    /// An inherited relation keeps working as long as the derived table still
265    /// projects the column its foreign key references.
266    pub fn copy_relations_from<E2: Entity<T::Value> + 'static>(
267        &mut self,
268        other: &Table<T, E2>,
269        names: Option<&[&str]>,
270    ) {
271        let Some(refs) = other.refs_ref() else {
272            return;
273        };
274        for (name, reference) in refs {
275            if names.is_some_and(|ns| !ns.contains(&name.as_str())) {
276                continue;
277            }
278            self.add_ref_arc(name, reference.clone());
279        }
280    }
281
282    pub fn references(&self) -> Vec<String> {
283        self.refs
284            .as_ref()
285            .map(|refs| refs.keys().cloned().collect())
286            .unwrap_or_default()
287    }
288
289    /// Narrow the table to a single row by id.
290    ///
291    /// Pairs with `get_some_value` for the "I only know an id" workflow.
292    /// The actual condition construction goes through
293    /// `TableSource::eq_value_condition`, so backends that don't yet
294    /// implement that path return an error here.
295    pub fn with_id(mut self, id: impl Into<T::Value>) -> Result<Self> {
296        let id_name = self
297            .id_field()
298            .ok_or_else(|| error!("id field not set on table"))?
299            .name()
300            .to_string();
301        let condition = self.data_source().eq_value_condition(&id_name, id.into())?;
302        self.add_condition(condition);
303        Ok(self)
304    }
305
306    /// Traverse a same-persistence reference using a known source row as the
307    /// join origin.
308    ///
309    /// Reads the join field value out of `row`, builds the target table via
310    /// the reference's stored factory, and applies one eq-condition that
311    /// selects the related rows. No subquery, no deferred fetch — `row`
312    /// already carries the value.
313    ///
314    /// `HasOne` reads from its stored foreign-key column; `HasMany` reads
315    /// from the source's id field (looked up here and forwarded into the
316    /// reference). The returned table preserves columns, refs, and
317    /// expressions from the reference's factory; only the entity type
318    /// changes if `E2` differs from the factory's output.
319    pub fn get_ref_from_row<E2: Entity<T::Value> + 'static>(
320        &self,
321        relation: &str,
322        row: &Record<T::Value>,
323    ) -> Result<Table<T, E2>> {
324        let (reference, _) = self.lookup_ref(relation)?;
325        let source_id = self
326            .id_field()
327            .map(|c| c.name().to_string())
328            .unwrap_or_else(|| "id".to_string());
329
330        let target_dyn = reference.resolve_from_row(
331            self.data_source() as &dyn std::any::Any,
332            &source_id,
333            row as &dyn std::any::Any,
334        )?;
335
336        let target_empty: Table<T, EmptyEntity> =
337            *target_dyn
338                .downcast::<Table<T, EmptyEntity>>()
339                .map_err(|_| error!("Failed to downcast target table to Table<T, EmptyEntity>"))?;
340
341        Ok(target_empty.into_entity::<E2>())
342    }
343
344    /// Traverse a same-backend relation into a typed `Table<T, E2>` with an
345    /// `IN (subquery)` filter on the source column.
346    ///
347    /// Use this when the parent table already carries the narrowing
348    /// conditions (e.g. `clients.add_condition(is_paying = true)`) and you
349    /// want every related child row matching that filter. For the
350    /// "I have a specific row in hand" case, prefer
351    /// [`Table::get_ref_from_row`] — it pushes a plain eq-condition
352    /// instead of a subquery.
353    pub fn get_ref_as<E2: Entity<T::Value> + 'static>(
354        &self,
355        relation: &str,
356    ) -> Result<Table<T, E2>> {
357        let (reference, relation_str) = self.lookup_ref(relation)?;
358
359        let source_id = self
360            .id_field()
361            .map(|c| c.name().to_string())
362            .unwrap_or_else(|| "id".to_string());
363
364        let mut target: Table<T, E2> = *reference
365            .build_target(self.data_source() as &dyn std::any::Any)
366            .downcast::<Table<T, E2>>()
367            .map_err(|_| {
368                error!(
369                    "Failed to downcast related table",
370                    relation = relation_str.as_str()
371                )
372            })?;
373
374        let target_id = target
375            .id_field()
376            .map(|c| c.name().to_string())
377            .unwrap_or_else(|| "id".to_string());
378
379        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
380
381        let condition = self
382            .data_source()
383            .related_in_condition(&tgt_col, self, &src_col);
384        target.add_condition(condition);
385
386        Ok(target)
387    }
388
389    /// Get a correlated related table for use inside SELECT expressions.
390    ///
391    /// Unlike [`Self::get_ref_as`] (which uses `IN (subquery)`), this produces a
392    /// correlated condition like `order.client_id = client.id`, suitable
393    /// for embedding as a subquery in a SELECT clause via
394    /// [`Self::with_expression`].
395    pub fn get_subquery_as<E2: Entity<T::Value> + 'static>(
396        &self,
397        relation: &str,
398    ) -> Result<Table<T, E2>> {
399        let (reference, relation_str) = self.lookup_ref(relation)?;
400
401        let source_id = self
402            .id_field()
403            .map(|c| c.name().to_string())
404            .unwrap_or_else(|| "id".to_string());
405
406        let mut target: Table<T, E2> = *reference
407            .build_target(self.data_source() as &dyn std::any::Any)
408            .downcast::<Table<T, E2>>()
409            .map_err(|_| {
410                error!(
411                    "Failed to downcast related table",
412                    relation = relation_str.as_str()
413                )
414            })?;
415
416        let target_id = target
417            .id_field()
418            .map(|c| c.name().to_string())
419            .unwrap_or_else(|| "id".to_string());
420
421        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
422
423        let condition = self.data_source().related_correlated_condition(
424            target.table_name(),
425            &tgt_col,
426            self.table_name(),
427            &src_col,
428        );
429        target.add_condition(condition);
430
431        Ok(target)
432    }
433
434    /// Build the relation's target table with **no condition** applied.
435    ///
436    /// Unlike [`Self::get_ref_from_row`] / [`Self::get_ref_as`] (which select
437    /// the related rows for a known parent), this returns the bare target — the
438    /// table you'd insert a new related row into. Used by Vista's nested insert
439    /// to obtain the destination for a has-one/has-many child before any join
440    /// value exists.
441    pub fn get_ref_target<E2: Entity<T::Value> + 'static>(
442        &self,
443        relation: &str,
444    ) -> Result<Table<T, E2>> {
445        let (reference, relation_str) = self.lookup_ref(relation)?;
446        let target: Table<T, E2> = *reference
447            .build_target(self.data_source() as &dyn std::any::Any)
448            .downcast::<Table<T, E2>>()
449            .map_err(|_| {
450                error!(
451                    "Failed to downcast related table",
452                    relation = relation_str.as_str()
453                )
454            })?;
455        Ok(target)
456    }
457
458    /// Add a computed expression field using builder pattern.
459    ///
460    /// The closure receives `&Table<T, E>` and returns an `Expression<T::Value>`.
461    /// It is evaluated lazily when `select()` builds the query.
462    pub fn with_expression(
463        mut self,
464        name: &str,
465        expr_fn: impl Fn(&Table<T, E>) -> Expression<T::Value> + Send + Sync + 'static,
466    ) -> Self {
467        self.expressions.insert(name.to_string(), Arc::new(expr_fn));
468        self
469    }
470
471    fn lookup_ref(&self, relation: &str) -> Result<(&dyn Reference, String)> {
472        let table_name = self.table_name().to_string();
473        let refs = self.refs.as_ref().ok_or_else(|| {
474            error!(
475                "No references defined on table",
476                table = table_name.as_str()
477            )
478        })?;
479
480        let relation_str = relation.to_string();
481        let reference = refs.get(relation).ok_or_else(|| {
482            error!(
483                "Reference not found on table",
484                relation = relation_str.as_str(),
485                table = table_name.as_str()
486            )
487        })?;
488
489        Ok((reference.as_ref(), relation_str))
490    }
491
492    /// Look up cardinality for a registered relation.
493    pub fn ref_cardinality(&self, relation: &str) -> Result<vantage_vista::ReferenceKind> {
494        let (reference, _) = self.lookup_ref(relation)?;
495        Ok(reference.cardinality())
496    }
497
498    /// List all registered relations with their cardinality.
499    pub fn ref_kinds(&self) -> Vec<(String, vantage_vista::ReferenceKind)> {
500        self.refs
501            .as_ref()
502            .map(|refs| {
503                refs.iter()
504                    .map(|(name, r)| (name.clone(), r.cardinality()))
505                    .collect()
506            })
507            .unwrap_or_default()
508    }
509}