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}