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