Skip to main content

vantage_table/table/impls/
spec.rs

1//! Lowering a YAML [`VistaSpec`] onto a table:
2//! the steps every driver factory shares. Type mapping and driver blocks stay
3//! with the driver, passed in as `build` functions.
4
5use indexmap::IndexMap;
6use vantage_core::{Context, Result, VantageError, error};
7use vantage_expressions::{Expressive, SelectableDataSource};
8use vantage_types::{EmptyEntity, Entity};
9use vantage_vista::{ColumnFlag, ColumnSpec, JoinKey, ReferenceKind, ReferenceSugar, VistaSpec};
10
11use crate::{
12    source::SelectSource,
13    table::Table,
14    table::inject::{BuildEnv, BuildKind, BuildTrail, SpecTableBuilder},
15    traits::table_source::TableSource,
16};
17
18impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
19    /// Add one spec column. A `lazy:` column becomes a computed column;
20    /// any other is built by `build` (unless the table already has it, as a
21    /// derived table may) and registered as a title when flagged `title`.
22    /// Returns whether the column is stored.
23    pub fn add_spec_column<C>(
24        &mut self,
25        name: &str,
26        spec: &ColumnSpec<C>,
27        build: impl FnOnce(&str, &ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
28    ) -> Result<bool> {
29        if self.add_lazy_spec_column(spec, name)? {
30            return Ok(false);
31        }
32        if !self.columns.contains_key(name) {
33            self.add_column(build(name, spec)?);
34        }
35        if spec.has_flag(ColumnFlag::Title) {
36            self.add_title_field(name);
37        }
38        Ok(true)
39    }
40
41    /// [`add_spec_column`](Self::add_spec_column) for each column, in order.
42    pub fn add_spec_columns<C>(
43        &mut self,
44        columns: &IndexMap<String, ColumnSpec<C>>,
45        build: impl Fn(&str, &ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
46    ) -> Result<()> {
47        for (name, spec) in columns {
48            self.add_spec_column(name, spec, &build)?;
49        }
50        Ok(())
51    }
52
53    /// Mark the spec's id column as the id field. Errors when the spec
54    /// doesn't declare that column.
55    pub fn set_spec_id_field(&mut self, id: &str) -> Result<()> {
56        if !self.columns.contains_key(id) {
57            return Err(error!("id column not present in spec.columns", id = id));
58        }
59        self.set_id_field(id);
60        Ok(())
61    }
62}
63
64impl<T: TableSource + 'static, E: Entity<T::Value> + 'static> Table<T, E>
65where
66    T::Value: Into<ciborium::Value> + From<ciborium::Value>,
67    T::Id: std::fmt::Display + From<String>,
68{
69    /// Register every join `spec` declares as `with_one` / `with_many`: its
70    /// `references:` entries, then the column-level `references:` sugar
71    /// (see [`spec_references`]).
72    ///
73    /// At traversal time the reference asks `env`'s resolver for the target's
74    /// current spec and rebuilds it with `build`, in a fresh environment of
75    /// kind [`BuildKind::Reference`] carrying `env`'s injector. A miss or a
76    /// build error is a traversal error, never a wider table. Without a
77    /// resolver the target is a column-less `Table::new(target)`, still run
78    /// through the injector. A contained kind is an error: contained
79    /// relations are declared under `contained:`.
80    pub fn with_spec_references<X, C, R, S: 'static>(
81        mut self,
82        spec: &VistaSpec<X, C, R>,
83        env: &BuildEnv<S, T>,
84        build: SpecTableBuilder<S, T>,
85    ) -> Result<Self> {
86        for (relation, reference) in &spec.references {
87            let foreign_key = reference.foreign_key(relation);
88            self = self.with_spec_join(
89                &spec.name,
90                relation,
91                &reference.table,
92                reference.kind,
93                &foreign_key,
94                env,
95                build,
96            )?;
97        }
98        for (column, sugar) in column_references(spec) {
99            let (target, kind, foreign_key) = match sugar {
100                ReferenceSugar::Sugar(target) => (target, ReferenceKind::HasOne, column.clone()),
101                ReferenceSugar::Full(r) => (&r.table, r.kind, r.foreign_key(column)),
102                _ => return Err(unknown_sugar(column)),
103            };
104            self =
105                self.with_spec_join(&spec.name, column, target, kind, &foreign_key, env, build)?;
106        }
107        Ok(self)
108    }
109
110    #[allow(clippy::too_many_arguments)]
111    fn with_spec_join<S: 'static>(
112        self,
113        from: &str,
114        relation: &str,
115        target: &str,
116        kind: ReferenceKind,
117        foreign_key: &str,
118        env: &BuildEnv<S, T>,
119        build: SpecTableBuilder<S, T>,
120    ) -> Result<Self> {
121        let target_name = target.to_string();
122        let relation_name = relation.to_string();
123        let child = BuildEnv {
124            resolver: env.resolver.clone(),
125            injector: env.injector.clone(),
126            trail: BuildTrail::default().enter(target)?,
127            kind: BuildKind::Reference {
128                from: from.to_string(),
129                relation: relation.to_string(),
130            },
131        };
132        let build_target = move |source: T| -> Result<Table<T, EmptyEntity>> {
133            let Some(r) = &child.resolver else {
134                let mut table = Table::new(target_name.clone(), source);
135                child.inject(&target_name, &mut table, build)?;
136                return Ok(table);
137            };
138            let spec = r.resolve(&target_name).ok_or_else(|| {
139                error!(
140                    "reference target has no spec",
141                    relation = relation_name.as_str(),
142                    target = target_name.as_str()
143                )
144            })?;
145            build(&spec, source, &child).with_context(|| {
146                error!(
147                    "reference target failed to build",
148                    relation = relation_name.as_str(),
149                    target = target_name.as_str()
150                )
151            })
152        };
153        match kind {
154            ReferenceKind::HasOne => {
155                Ok(self.try_with_one(relation, foreign_key, target, build_target))
156            }
157            ReferenceKind::HasMany => {
158                Ok(self.try_with_many(relation, foreign_key, target, build_target))
159            }
160            _ => Err(error!(
161                "references: entries must be has_one or has_many",
162                relation = relation
163            )),
164        }
165    }
166}
167
168/// Every join `spec` declares, as Vista references: each `references:`
169/// entry, then each column-level `references:` not shadowed by an entry of
170/// the same name. The sugar names its relation after the column, which is
171/// also its foreign key: `bakery: { references: bakery }` is a has-one
172/// keyed by `bakery`. `source_id` is the column a `has_many` reads from
173/// this table's rows.
174pub fn spec_references<X, C, R>(
175    spec: &VistaSpec<X, C, R>,
176    source_id: &str,
177) -> Result<Vec<vantage_vista::Reference>> {
178    let mut out = Vec::with_capacity(spec.references.len());
179    for (relation, reference) in &spec.references {
180        out.push(reference.to_reference(relation, source_id)?);
181    }
182    for (column, sugar) in column_references(spec) {
183        out.push(match sugar {
184            ReferenceSugar::Sugar(target) => {
185                vantage_vista::Reference::has_one(column, target, JoinKey::to_target_id(column))
186            }
187            ReferenceSugar::Full(r) => r.to_reference(column, source_id)?,
188            _ => return Err(unknown_sugar(column)),
189        });
190    }
191    Ok(out)
192}
193
194fn unknown_sugar(column: &str) -> VantageError {
195    error!(
196        "column references: takes a table name or a full reference",
197        column = column
198    )
199}
200
201/// The column-level `references:` of `spec` that no `references:` entry
202/// shadows, keyed by column.
203fn column_references<X, C, R>(
204    spec: &VistaSpec<X, C, R>,
205) -> impl Iterator<Item = (&String, &ReferenceSugar)> {
206    spec.columns.iter().filter_map(|(column, col)| {
207        let sugar = col.references.as_ref()?;
208        (!spec.references.contains_key(column)).then_some((column, sugar))
209    })
210}
211
212// Same bounds as `Table::derive_from`, which this builds on.
213impl<T, E, V, Cond, S> Table<T, E>
214where
215    T: SelectableDataSource<V, Cond, Select = S>
216        + TableSource<Value = V, Condition = Cond, Source = SelectSource<S>>,
217    V: Clone + Send + Sync + 'static + From<String>,
218    Cond: Clone + Send + Sync + 'static,
219    S: Expressive<V> + Clone,
220    E: Entity<V>,
221{
222    /// Lower a `base:` spec: a table over `select` (the base's select, possibly
223    /// transformed) that inherits the listed base columns and relations, adds
224    /// the spec's own columns (aggregate outputs, say) and contained
225    /// relations, and keeps the base's id unless the spec names one.
226    pub fn derive_from_spec<E2: Entity<V> + 'static, X, C, R>(
227        base: &Table<T, E2>,
228        spec: &VistaSpec<X, C, R>,
229        select: S,
230        inherit_columns: &[String],
231        inherit_relations: &[String],
232        build_column: impl Fn(&str, &ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
233    ) -> Result<Self>
234    where
235        T: 'static,
236        E: 'static,
237    {
238        let columns: Vec<&str> = inherit_columns.iter().map(String::as_str).collect();
239        let relations: Vec<&str> = inherit_relations.iter().map(String::as_str).collect();
240        let mut table = Self::derive_from(
241            base,
242            spec.name.clone(),
243            move |_| select,
244            &columns,
245            &relations,
246        );
247        table.add_spec_columns(&spec.columns, &build_column)?;
248        if let Some(id) = &spec.id_column {
249            table.set_id_field(id);
250        }
251        table.with_contained_specs(&spec.contained, build_column)
252    }
253}