Skip to main content

vantage_table/table/impls/
references.rs

1//! Table relationship methods for defining and traversing references.
2
3use std::sync::Arc;
4
5use indexmap::IndexMap;
6use vantage_core::{Result, error};
7use vantage_types::Entity;
8
9use crate::{
10    references::{HasMany, HasOne, Reference},
11    table::Table,
12    traits::table_source::TableSource,
13};
14
15mod contained;
16mod traversal;
17
18impl<T: TableSource + 'static, E: Entity<T::Value> + 'static> Table<T, E> {
19    /// Define a one-to-one relationship.
20    ///
21    /// ```rust,ignore
22    /// .with_one("bakery", "bakery_id", Bakery::postgres_table)
23    /// ```
24    pub fn with_one<E2: Entity<T::Value> + 'static>(
25        mut self,
26        relation: &str,
27        foreign_key: &str,
28        build_target: impl Fn(T) -> Table<T, E2> + Send + Sync + 'static,
29    ) -> Self
30    where
31        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
32        T::Id: std::fmt::Display + From<String>,
33    {
34        let reference = HasOne::<T, E, E2>::new(foreign_key, build_target);
35        self.add_ref(relation, Box::new(reference));
36        self
37    }
38
39    /// Define a one-to-many relationship.
40    ///
41    /// ```rust,ignore
42    /// .with_many("orders", "client_id", Order::postgres_table)
43    /// ```
44    pub fn with_many<E2: Entity<T::Value> + 'static>(
45        mut self,
46        relation: &str,
47        foreign_key: &str,
48        build_target: impl Fn(T) -> Table<T, E2> + Send + Sync + 'static,
49    ) -> Self
50    where
51        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
52        T::Id: std::fmt::Display + From<String>,
53    {
54        let reference = HasMany::<T, E, E2>::new(foreign_key, build_target);
55        self.add_ref(relation, Box::new(reference));
56        self
57    }
58
59    /// [`with_one`](Self::with_one) whose target build can fail: traversing
60    /// the relation returns the error instead of an unconditioned table.
61    /// `target` names the target in metadata, read without running the build.
62    pub fn try_with_one<E2: Entity<T::Value> + 'static>(
63        mut self,
64        relation: &str,
65        foreign_key: &str,
66        target: &str,
67        build_target: impl Fn(T) -> Result<Table<T, E2>> + Send + Sync + 'static,
68    ) -> Self
69    where
70        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
71        T::Id: std::fmt::Display + From<String>,
72    {
73        let reference = HasOne::<T, E, E2>::try_new(foreign_key, target, build_target);
74        self.add_ref(relation, Box::new(reference));
75        self
76    }
77
78    /// [`with_many`](Self::with_many) whose target build can fail.
79    pub fn try_with_many<E2: Entity<T::Value> + 'static>(
80        mut self,
81        relation: &str,
82        foreign_key: &str,
83        target: &str,
84        build_target: impl Fn(T) -> Result<Table<T, E2>> + Send + Sync + 'static,
85    ) -> Self
86    where
87        T::Value: Into<ciborium::Value> + From<ciborium::Value>,
88        T::Id: std::fmt::Display + From<String>,
89    {
90        let reference = HasMany::<T, E, E2>::try_new(foreign_key, target, build_target);
91        self.add_ref(relation, Box::new(reference));
92        self
93    }
94
95    pub(crate) fn add_ref(&mut self, relation: &str, reference: Box<dyn Reference>) {
96        self.add_ref_arc(relation, Arc::from(reference));
97    }
98
99    /// Insert an already-shared reference. Used to inherit relations when
100    /// deriving a table from another (the same `Arc` is shared, not rebuilt).
101    pub(crate) fn add_ref_arc(&mut self, relation: &str, reference: Arc<dyn Reference>) {
102        self.refs
103            .get_or_insert_with(IndexMap::new)
104            .insert(relation.to_string(), reference);
105    }
106
107    /// Copy relations from another table, sharing the underlying `Arc`s. With
108    /// `names = None`, copies all relations; otherwise only the listed ones.
109    /// An inherited relation keeps working as long as the derived table still
110    /// projects the column its foreign key references.
111    pub fn copy_relations_from<E2: Entity<T::Value> + 'static>(
112        &mut self,
113        other: &Table<T, E2>,
114        names: Option<&[&str]>,
115    ) {
116        let Some(refs) = &other.refs else {
117            return;
118        };
119        for (name, reference) in refs {
120            if names.is_some_and(|ns| !ns.contains(&name.as_str())) {
121                continue;
122            }
123            self.add_ref_arc(name, reference.clone());
124        }
125    }
126
127    pub fn references(&self) -> Vec<String> {
128        self.refs
129            .as_ref()
130            .map(|refs| refs.keys().cloned().collect())
131            .unwrap_or_default()
132    }
133
134    fn lookup_ref(&self, relation: &str) -> Result<&dyn Reference> {
135        let table_name = self.table_name();
136        let refs = self
137            .refs
138            .as_ref()
139            .ok_or_else(|| error!("No references defined on table", table = table_name))?;
140
141        let reference = refs.get(relation).ok_or_else(|| {
142            error!(
143                "Reference not found on table",
144                relation = relation,
145                table = table_name
146            )
147        })?;
148
149        Ok(reference.as_ref())
150    }
151
152    /// A registered relation as Vista metadata. Kind and key questions are
153    /// read off this, so they can't disagree with what `vista_references`
154    /// publishes. The target name stays empty: nothing here reads it, and
155    /// naming it would build the target.
156    fn ref_shape(&self, relation: &str) -> Result<vantage_vista::Reference> {
157        let reference = self.lookup_ref(relation)?;
158        Ok(reference.vista_reference(relation, String::new(), &self.id_field_name()))
159    }
160
161    /// Look up cardinality for a registered relation.
162    pub fn ref_cardinality(&self, relation: &str) -> Result<vantage_vista::ReferenceKind> {
163        Ok(self.ref_shape(relation)?.kind)
164    }
165
166    /// Look up the foreign-key/link field for a registered relation. For a
167    /// `has_one` this is the source-side column carrying the link — the field
168    /// a backend-native traversal (e.g. a SurrealDB idiom path) descends
169    /// through, which need not match the relation's registry name. For a
170    /// `has_many` it is the target-side column carrying the parent's id.
171    pub fn ref_foreign_key(&self, relation: &str) -> Result<String> {
172        let shape = self.ref_shape(relation)?;
173        let key = shape
174            .keys
175            .first()
176            .ok_or_else(|| error!("Reference has no join key", relation = relation))?;
177        Ok(match shape.kind {
178            vantage_vista::ReferenceKind::HasMany => key.to.clone().unwrap_or_default(),
179            _ => key.from.clone(),
180        })
181    }
182
183    /// List all registered relations with their cardinality.
184    pub fn ref_kinds(&self) -> Vec<(String, vantage_vista::ReferenceKind)> {
185        let Some(refs) = &self.refs else {
186            return Vec::new();
187        };
188        let source_id = self.id_field_name();
189        refs.iter()
190            .map(|(name, r)| {
191                let kind = r.vista_reference(name, String::new(), &source_id).kind;
192                (name.clone(), kind)
193            })
194            .collect()
195    }
196}