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