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::Entity;
9
10use crate::{
11    any::AnyTable,
12    references::{HasForeign, 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    /// Define a cross-persistence reference.
59    ///
60    /// The closure receives this table and returns an `AnyTable` from any backend
61    /// with deferred conditions attached.
62    ///
63    /// ```rust,ignore
64    /// .with_foreign("mongo_orders", "Table<MongoDB, Order>", |clients| {
65    ///     let mut orders = Order::mongo_table(mongo_db.clone());
66    ///     // attach deferred condition ...
67    ///     Ok(AnyTable::from_table(orders))
68    /// })
69    /// ```
70    pub fn with_foreign(
71        mut self,
72        relation: &str,
73        target_type: &'static str,
74        resolve: impl Fn(&Table<T, E>) -> Result<AnyTable> + Send + Sync + 'static,
75    ) -> Self {
76        let reference = HasForeign::<T, E>::new(target_type, resolve);
77        self.add_ref(relation, Box::new(reference));
78        self
79    }
80
81    pub(crate) fn add_ref(&mut self, relation: &str, reference: Box<dyn Reference>) {
82        if self.refs.is_none() {
83            self.refs = Some(IndexMap::new());
84        }
85        self.refs
86            .as_mut()
87            .unwrap()
88            .insert(relation.to_string(), Arc::from(reference));
89    }
90
91    pub fn references(&self) -> Vec<String> {
92        self.refs
93            .as_ref()
94            .map(|refs| refs.keys().cloned().collect())
95            .unwrap_or_default()
96    }
97
98    /// Check if a reference is cross-persistence (foreign).
99    pub fn is_foreign_ref(&self, relation: &str) -> Result<bool> {
100        let (reference, _) = self.lookup_ref(relation)?;
101        Ok(reference.is_foreign())
102    }
103
104    /// Get a same-backend related table with automatic downcasting.
105    ///
106    /// For foreign references, use `get_ref()` instead.
107    pub fn get_ref_as<E2: Entity<T::Value> + 'static>(
108        &self,
109        relation: &str,
110    ) -> Result<Table<T, E2>> {
111        let (reference, relation_str) = self.lookup_ref(relation)?;
112
113        if reference.is_foreign() {
114            return Err(error!(
115                "Cannot use get_ref_as for foreign references, use get_ref instead",
116                relation = relation_str.as_str()
117            ));
118        }
119
120        // 1. Build target
121        let source_id = self
122            .id_field()
123            .map(|c| c.name().to_string())
124            .unwrap_or_else(|| "id".to_string());
125
126        let mut target: Table<T, E2> = *reference
127            .build_target(self.data_source() as &dyn std::any::Any)
128            .downcast::<Table<T, E2>>()
129            .map_err(|_| {
130                error!(
131                    "Failed to downcast related table",
132                    relation = relation_str.as_str()
133                )
134            })?;
135
136        // 2. Get columns
137        let target_id = target
138            .id_field()
139            .map(|c| c.name().to_string())
140            .unwrap_or_else(|| "id".to_string());
141
142        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
143
144        // 3. Build and apply condition
145        let condition = self
146            .data_source()
147            .related_in_condition(&tgt_col, self, &src_col);
148        target.add_condition(condition);
149
150        Ok(target)
151    }
152
153    /// Get a related table as AnyTable — works for both same-backend and foreign refs.
154    pub fn get_ref(&self, relation: &str) -> Result<AnyTable> {
155        let (reference, _) = self.lookup_ref(relation)?;
156        reference.resolve_as_any(self as &dyn std::any::Any)
157    }
158
159    /// Get a correlated related table for use inside SELECT expressions.
160    ///
161    /// Unlike `get_ref_as` (which uses `IN (subquery)`), this produces a
162    /// correlated condition like `order.client_id = client.id`, suitable
163    /// for embedding as a subquery in a SELECT clause.
164    pub fn get_subquery_as<E2: Entity<T::Value> + 'static>(
165        &self,
166        relation: &str,
167    ) -> Result<Table<T, E2>> {
168        let (reference, relation_str) = self.lookup_ref(relation)?;
169
170        if reference.is_foreign() {
171            return Err(error!(
172                "Cannot use get_subquery_as for foreign references",
173                relation = relation_str.as_str()
174            ));
175        }
176
177        // 1. Build target
178        let source_id = self
179            .id_field()
180            .map(|c| c.name().to_string())
181            .unwrap_or_else(|| "id".to_string());
182
183        let mut target: Table<T, E2> = *reference
184            .build_target(self.data_source() as &dyn std::any::Any)
185            .downcast::<Table<T, E2>>()
186            .map_err(|_| {
187                error!(
188                    "Failed to downcast related table",
189                    relation = relation_str.as_str()
190                )
191            })?;
192
193        // 2. Get columns
194        let target_id = target
195            .id_field()
196            .map(|c| c.name().to_string())
197            .unwrap_or_else(|| "id".to_string());
198
199        let (src_col, tgt_col) = reference.columns(&source_id, &target_id);
200
201        // 3. Build correlated condition: target_table.tgt_col = source_table.src_col
202        let condition = self.data_source().related_correlated_condition(
203            target.table_name(),
204            &tgt_col,
205            self.table_name(),
206            &src_col,
207        );
208        target.add_condition(condition);
209
210        Ok(target)
211    }
212
213    /// Add a computed expression field using builder pattern.
214    ///
215    /// The closure receives `&Table<T, E>` and returns an `Expression<T::Value>`.
216    /// It is evaluated lazily when `select()` builds the query.
217    ///
218    /// ```rust,ignore
219    /// .with_expression("order_count", |t| {
220    ///     t.get_subquery_as::<Order>("orders").unwrap().get_count_query()
221    /// })
222    /// ```
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}