Skip to main content

vantage_table/table/impls/
refereces.rs

1//! Table relationship methods for defining and traversing references.
2
3use indexmap::IndexMap;
4use std::sync::Arc;
5
6use vantage_core::{Result, error};
7use vantage_expressions::Expression;
8use vantage_types::{EmptyEntity, Entity, Record};
9
10use crate::{
11    any::AnyTable,
12    references::{HasMany, HasOne, Reference},
13    table::Table,
14    traits::{column_like::ColumnLike, table_source::TableSource},
15};
16
17impl<T: TableSource + 'static, E: Entity<T::Value> + 'static> Table<T, E> {
18    /// Define a one-to-one relationship.
19    ///
20    /// ```rust,ignore
21    /// .with_one("bakery", "bakery_id", Bakery::postgres_table)
22    /// ```
23    pub fn with_one<E2: Entity<T::Value> + 'static>(
24        mut self,
25        relation: &str,
26        foreign_key: &str,
27        build_target: impl Fn(T) -> Table<T, E2> + Send + Sync + 'static,
28    ) -> Self
29    where
30        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
31        T::Id: std::fmt::Display + From<String>,
32    {
33        let reference = HasOne::<T, E, E2>::new(foreign_key, build_target);
34        self.add_ref(relation, Box::new(reference));
35        self
36    }
37
38    /// Define a one-to-many relationship.
39    ///
40    /// ```rust,ignore
41    /// .with_many("orders", "client_id", Order::postgres_table)
42    /// ```
43    pub fn with_many<E2: Entity<T::Value> + 'static>(
44        mut self,
45        relation: &str,
46        foreign_key: &str,
47        build_target: impl Fn(T) -> Table<T, E2> + Send + Sync + 'static,
48    ) -> Self
49    where
50        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
51        T::Id: std::fmt::Display + From<String>,
52    {
53        let reference = HasMany::<T, E, E2>::new(foreign_key, build_target);
54        self.add_ref(relation, Box::new(reference));
55        self
56    }
57
58    pub(crate) fn add_ref(&mut self, relation: &str, reference: Box<dyn Reference>) {
59        if self.refs.is_none() {
60            self.refs = Some(IndexMap::new());
61        }
62        self.refs
63            .as_mut()
64            .unwrap()
65            .insert(relation.to_string(), Arc::from(reference));
66    }
67
68    pub fn references(&self) -> Vec<String> {
69        self.refs
70            .as_ref()
71            .map(|refs| refs.keys().cloned().collect())
72            .unwrap_or_default()
73    }
74
75    /// Narrow the table to a single row by id.
76    ///
77    /// Pairs with `get_some_value` for the "I only know an id" workflow.
78    /// The actual condition construction goes through
79    /// `TableSource::eq_value_condition`, so backends that don't yet
80    /// implement that path return an error here.
81    pub fn with_id(mut self, id: impl Into<T::Value>) -> Result<Self> {
82        let id_name = self
83            .id_field()
84            .ok_or_else(|| error!("id field not set on table"))?
85            .name()
86            .to_string();
87        let condition = self.data_source().eq_value_condition(&id_name, id.into())?;
88        self.add_condition(condition);
89        Ok(self)
90    }
91
92    /// Traverse a same-persistence reference using a known source row as the
93    /// join origin.
94    ///
95    /// Reads the join field value out of `row`, builds the target table via
96    /// the reference's stored factory, and applies one eq-condition that
97    /// selects the related rows. No subquery, no deferred fetch — `row`
98    /// already carries the value.
99    ///
100    /// `HasOne` reads from its stored foreign-key column; `HasMany` reads
101    /// from the source's id field (looked up here and forwarded into the
102    /// reference). The returned table preserves columns, refs, and
103    /// expressions from the reference's factory; only the entity type
104    /// changes if `E2` differs from the factory's output.
105    pub fn get_ref_from_row<E2: Entity<T::Value> + 'static>(
106        &self,
107        relation: &str,
108        row: &Record<T::Value>,
109    ) -> Result<Table<T, E2>> {
110        let (reference, _) = self.lookup_ref(relation)?;
111        let source_id = self
112            .id_field()
113            .map(|c| c.name().to_string())
114            .unwrap_or_else(|| "id".to_string());
115
116        let target_dyn = reference.resolve_from_row(
117            self.data_source() as &dyn std::any::Any,
118            &source_id,
119            row as &dyn std::any::Any,
120        )?;
121
122        let target_empty: Table<T, EmptyEntity> =
123            *target_dyn
124                .downcast::<Table<T, EmptyEntity>>()
125                .map_err(|_| error!("Failed to downcast target table to Table<T, EmptyEntity>"))?;
126
127        Ok(target_empty.into_entity::<E2>())
128    }
129
130    /// Get a same-backend related table with automatic downcasting.
131    ///
132    /// Legacy AnyTable-flavoured path; slated for deletion in Stage 9 alongside
133    /// `AnyTable`. New code should prefer [`Table::get_ref_from_row`] (typed) or
134    /// `Vista::get_ref` (erased).
135    pub fn get_ref_as<E2: Entity<T::Value> + 'static>(
136        &self,
137        relation: &str,
138    ) -> Result<Table<T, E2>> {
139        let (reference, relation_str) = self.lookup_ref(relation)?;
140
141        // 1. Build target
142        let source_id = self
143            .id_field()
144            .map(|c| c.name().to_string())
145            .unwrap_or_else(|| "id".to_string());
146
147        let mut target: Table<T, E2> = *reference
148            .build_target(self.data_source() as &dyn std::any::Any)
149            .downcast::<Table<T, E2>>()
150            .map_err(|_| {
151                error!(
152                    "Failed to downcast related table",
153                    relation = relation_str.as_str()
154                )
155            })?;
156
157        // 2. Get columns
158        let target_id = target
159            .id_field()
160            .map(|c| c.name().to_string())
161            .unwrap_or_else(|| "id".to_string());
162
163        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
164
165        // 3. Build and apply condition
166        let condition = self
167            .data_source()
168            .related_in_condition(&tgt_col, self, &src_col);
169        target.add_condition(condition);
170
171        Ok(target)
172    }
173
174    /// Get a related table as AnyTable.
175    ///
176    /// Legacy AnyTable-flavoured path; slated for deletion in Stage 9.
177    pub fn get_ref(&self, relation: &str) -> Result<AnyTable> {
178        let (reference, _) = self.lookup_ref(relation)?;
179        reference.resolve_as_any(self as &dyn std::any::Any)
180    }
181
182    /// Get a correlated related table for use inside SELECT expressions.
183    ///
184    /// Unlike `get_ref_as` (which uses `IN (subquery)`), this produces a
185    /// correlated condition like `order.client_id = client.id`, suitable
186    /// for embedding as a subquery in a SELECT clause.
187    pub fn get_subquery_as<E2: Entity<T::Value> + 'static>(
188        &self,
189        relation: &str,
190    ) -> Result<Table<T, E2>> {
191        let (reference, relation_str) = self.lookup_ref(relation)?;
192
193        // 1. Build target
194        let source_id = self
195            .id_field()
196            .map(|c| c.name().to_string())
197            .unwrap_or_else(|| "id".to_string());
198
199        let mut target: Table<T, E2> = *reference
200            .build_target(self.data_source() as &dyn std::any::Any)
201            .downcast::<Table<T, E2>>()
202            .map_err(|_| {
203                error!(
204                    "Failed to downcast related table",
205                    relation = relation_str.as_str()
206                )
207            })?;
208
209        // 2. Get columns
210        let target_id = target
211            .id_field()
212            .map(|c| c.name().to_string())
213            .unwrap_or_else(|| "id".to_string());
214
215        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
216
217        // 3. Build correlated condition: target_table.tgt_col = source_table.src_col
218        let condition = self.data_source().related_correlated_condition(
219            target.table_name(),
220            &tgt_col,
221            self.table_name(),
222            &src_col,
223        );
224        target.add_condition(condition);
225
226        Ok(target)
227    }
228
229    /// Add a computed expression field using builder pattern.
230    ///
231    /// The closure receives `&Table<T, E>` and returns an `Expression<T::Value>`.
232    /// It is evaluated lazily when `select()` builds the query.
233    ///
234    /// ```rust,ignore
235    /// .with_expression("order_count", |t| {
236    ///     t.get_subquery_as::<Order>("orders").unwrap().get_count_query()
237    /// })
238    /// ```
239    pub fn with_expression(
240        mut self,
241        name: &str,
242        expr_fn: impl Fn(&Table<T, E>) -> Expression<T::Value> + Send + Sync + 'static,
243    ) -> Self {
244        self.expressions.insert(name.to_string(), Arc::new(expr_fn));
245        self
246    }
247
248    fn lookup_ref(&self, relation: &str) -> Result<(&dyn Reference, String)> {
249        let table_name = self.table_name().to_string();
250        let refs = self.refs.as_ref().ok_or_else(|| {
251            error!(
252                "No references defined on table",
253                table = table_name.as_str()
254            )
255        })?;
256
257        let relation_str = relation.to_string();
258        let reference = refs.get(relation).ok_or_else(|| {
259            error!(
260                "Reference not found on table",
261                relation = relation_str.as_str(),
262                table = table_name.as_str()
263            )
264        })?;
265
266        Ok((reference.as_ref(), relation_str))
267    }
268
269    /// Look up cardinality for a registered relation.
270    pub fn ref_cardinality(&self, relation: &str) -> Result<vantage_vista::ReferenceKind> {
271        let (reference, _) = self.lookup_ref(relation)?;
272        Ok(reference.cardinality())
273    }
274
275    /// List all registered relations with their cardinality.
276    pub fn ref_kinds(&self) -> Vec<(String, vantage_vista::ReferenceKind)> {
277        self.refs
278            .as_ref()
279            .map(|refs| {
280                refs.iter()
281                    .map(|(name, r)| (name.clone(), r.cardinality()))
282                    .collect()
283            })
284            .unwrap_or_default()
285    }
286}