Skip to main content

vantage_table/table/base/
computed.rs

1//! Columns a table computes rather than stores: server-side expressions,
2//! lazy (post-fetch) expressions, imported implicit-reference columns and
3//! the columns a `Vista` computes after each read.
4
5use std::sync::Arc;
6
7use vantage_core::Result;
8use vantage_expressions::Expression;
9use vantage_types::Entity;
10
11use super::Table;
12use crate::traits::table_source::TableSource;
13
14impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
15    /// Apply lazy expressions to one returned record, in declaration order.
16    /// Each callback borrows the record as built so far; the value it
17    /// returns is inserted under the expression's name before the next
18    /// callback runs. See [`Self::with_lazy_expression`].
19    /// Public so driver shells that bypass the `list_values` read path
20    /// (e.g. a REST shell's windowed fetch) can still apply lazy columns.
21    pub async fn apply_lazy_expressions(
22        &self,
23        record: &mut vantage_types::Record<T::Value>,
24    ) -> vantage_core::Result<()> {
25        for (name, f) in &self.lazy_expressions {
26            let value = f(record).await?;
27            record.insert(name.clone(), value);
28        }
29        Ok(())
30    }
31
32    /// Drop imported implicit-reference columns (`"country.name"`) from a write
33    /// payload. They are read-only, expression-backed projections that no
34    /// backend can honestly store; a round-trip (read → modify → save) would
35    /// otherwise carry them back, and a SCHEMALESS store would create a literal
36    /// `country.name` field. Called before invariants on the full-record write
37    /// paths (insert, insert-returning-id, replace); `patch_value` instead
38    /// rejects imported keys outright, since a partial payload is explicit
39    /// intent per key.
40    pub(in crate::table) fn strip_imported_columns(
41        &self,
42        record: &mut vantage_types::Record<T::Value>,
43    ) {
44        for name in &self.imported_columns {
45            record.shift_remove(name);
46        }
47    }
48
49    /// Whether `name` is an imported implicit-reference column
50    /// (`"country.name"`) — an expression-backed traversal projection that is
51    /// read-only and must never be persisted, ordered, or searched as if it
52    /// were a physical field.
53    pub fn is_imported_column(&self, name: &str) -> bool {
54        self.imported_columns.contains(name)
55    }
56
57    /// Whether `name` is computed rather than stored: an imported
58    /// implicit-reference column, a server-side expression column, or a lazy
59    /// (post-fetch) computed column. Driver factories flag such columns
60    /// `calculated` in vista metadata so consumers render them read-only.
61    pub fn is_calculated_column(&self, name: &str) -> bool {
62        self.imported_columns.contains(name)
63            || self.expressions.contains_key(name)
64            || self.lazy_expressions.contains_key(name)
65    }
66
67    /// Register a column the `Vista` computes after each read, positioned
68    /// after every column added so far. Driver factories fold these into
69    /// `VistaMetadata`. A stored column of the same name (one inherited by a
70    /// derived table) is replaced, so the column is never both read and
71    /// computed.
72    pub fn add_computed_column(&mut self, column: vantage_vista::Column) {
73        if let Some((stored_index, _, _)) = self.columns.shift_remove_full(&column.name) {
74            let removed = self.position_of_stored(stored_index);
75            for (at, _) in self.computed_columns.values_mut() {
76                if *at > removed {
77                    *at -= 1;
78                }
79            }
80        }
81        let at = self.columns.len() + self.computed_columns.len();
82        self.computed_columns
83            .insert(column.name.clone(), (at, column));
84    }
85
86    /// Register `spec`'s column as computed when it declares a `lazy:` script
87    /// (see [`vantage_vista::ColumnSpec::lazy_column`]). Returns `false` for a
88    /// stored column, leaving the caller to add it.
89    pub fn add_lazy_spec_column<C>(
90        &mut self,
91        spec: &vantage_vista::ColumnSpec<C>,
92        name: &str,
93    ) -> vantage_core::Result<bool> {
94        let Some(column) = spec.lazy_column(name)? else {
95            return Ok(false);
96        };
97        self.add_computed_column(column);
98        Ok(true)
99    }
100
101    /// Position among all columns of the stored column at `stored_index`:
102    /// computed columns occupy their recorded positions, stored columns fill
103    /// the remaining slots in order.
104    pub(in crate::table) fn position_of_stored(&self, stored_index: usize) -> usize {
105        let mut computed: Vec<usize> = self.computed_columns.values().map(|(at, _)| *at).collect();
106        computed.sort_unstable();
107        let mut at = stored_index;
108        for position in computed {
109            if position <= at {
110                at += 1;
111            }
112        }
113        at
114    }
115
116    /// Columns registered via [`Self::add_computed_column`], in order, each
117    /// with its position among all columns.
118    pub fn computed_columns(&self) -> impl Iterator<Item = (usize, &vantage_vista::Column)> {
119        self.computed_columns.values().map(|(at, c)| (*at, c))
120    }
121}
122
123impl<T: TableSource + 'static, E: Entity<T::Value> + 'static> Table<T, E> {
124    /// Add a computed expression field using builder pattern.
125    ///
126    /// The closure receives `&Table<T, E>` and returns an `Expression<T::Value>`.
127    /// It is evaluated lazily when `select()` builds the query.
128    pub fn with_expression(
129        mut self,
130        name: &str,
131        expr_fn: impl Fn(&Table<T, E>) -> Expression<T::Value> + Send + Sync + 'static,
132    ) -> Self
133    where
134        T: 'static,
135        E: 'static,
136    {
137        // Adapt the caller's `Fn(&Table<T, E>)` into the entity-erased
138        // `ExpressionFn<T>` we store, so it survives `into_entity`. The closure
139        // never touches entity-typed fields, only name-keyed table state.
140        let wrapped: crate::table::base::ExpressionFn<T> =
141            Arc::new(move |erased: &Table<T, vantage_types::EmptyEntity>| {
142                // SAFETY: layout-identical (E is PhantomData only); shared borrow.
143                let typed: &Table<T, E> = unsafe {
144                    &*(erased as *const Table<T, vantage_types::EmptyEntity> as *const Table<T, E>)
145                };
146                expr_fn(typed)
147            });
148        self.expressions.insert(name.to_string(), wrapped);
149        self
150    }
151
152    /// Add a lazy computed column using builder pattern.
153    ///
154    /// Unlike [`Self::with_expression`] (which lowers into the data source's
155    /// query), a lazy expression runs in Rust on the record the data source
156    /// *returned*. Callbacks apply in declaration order: each borrows the
157    /// record as built so far, and the value it returns is inserted under
158    /// `name` before the next callback runs — so one expensive fetch (a
159    /// file's contents) can feed several cheap derived columns declared
160    /// after it.
161    ///
162    /// The name is also registered as a column, so the computed field shows
163    /// up in the table's schema (and in Vista metadata derived from it).
164    /// Because the callback's future must not borrow the record, clone what
165    /// you need from `record` before going async.
166    ///
167    /// Lazy expressions run on the `list`/`get` read paths (both raw-record
168    /// and entity forms). They never participate in queries — no conditions,
169    /// ordering, or pushdown — they exist only on returned data.
170    pub fn with_lazy_expression<F, Fut>(mut self, name: &str, f: F) -> Self
171    where
172        F: Fn(&vantage_types::Record<T::Value>) -> Fut + Send + Sync + 'static,
173        Fut: std::future::Future<Output = Result<T::Value>> + Send + 'static,
174    {
175        if !self.columns.contains_key(name) {
176            let column = self.data_source.create_column::<T::AnyType>(name);
177            self.add_column(column);
178        }
179        self.add_lazy_expression(name, Arc::new(move |record| Box::pin(f(record))));
180        self
181    }
182
183    /// Register a pre-boxed lazy expression callback under `name`, without
184    /// touching the column set. Factories lowering a spec (which declares
185    /// its columns separately) use this; application code should prefer
186    /// [`Self::with_lazy_expression`].
187    pub fn add_lazy_expression(&mut self, name: &str, f: crate::table::base::LazyExpressionFn<T>) {
188        self.lazy_expressions.insert(name.to_string(), f);
189    }
190}