Skip to main content

vantage_table/table/
base.rs

1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use indexmap::IndexMap;
5use vantage_expressions::Expression;
6use vantage_types::{EmptyEntity, Entity};
7
8use crate::{
9    pagination::Pagination, references::Reference, sorting::SortDirection, table::hooks::Hooks,
10    traits::table_source::TableSource, traits::table_source_spec::TableSourceSpec,
11};
12
13/// Type alias for expression closures stored on Table.
14///
15/// Stored against the entity-erased `Table<T, EmptyEntity>` rather than the
16/// concrete `Table<T, E>` so the closures survive [`Table::into_entity`] — an
17/// expression only ever reads entity-agnostic table state (columns and
18/// relations by name, conditions, subqueries), never the entity's typed fields.
19/// [`Table::with_expression`] adapts the caller's `Fn(&Table<T, E>)` into this
20/// shape; see `Table::as_entity_erased` for the soundness of the cast.
21pub type ExpressionFn<T> =
22    Arc<dyn Fn(&Table<T, EmptyEntity>) -> Expression<<T as TableSource>::Value> + Send + Sync>;
23
24/// Type alias for lazy-expression callbacks stored on Table.
25///
26/// A lazy expression runs *after* the data source returns a record.
27/// Callbacks apply in declaration order: each borrows the record as built
28/// so far, and the value it returns is inserted under the expression's
29/// name. A later lazy expression therefore sees the columns produced by
30/// earlier ones — one expensive fetch (a file's contents) can feed several
31/// cheap derived columns. See [`Table::with_lazy_expression`].
32pub type LazyExpressionFn<T> = Arc<
33    dyn Fn(
34            &vantage_types::Record<<T as TableSource>::Value>,
35        ) -> std::pin::Pin<
36            Box<
37                dyn std::future::Future<Output = vantage_core::Result<<T as TableSource>::Value>>
38                    + Send,
39            >,
40        > + Send
41        + Sync,
42>;
43
44#[derive(Clone)]
45pub struct Table<T, E>
46where
47    T: TableSource,
48    E: Entity<T::Value>,
49{
50    pub(super) data_source: T,
51    pub(super) _phantom: PhantomData<E>,
52    pub(super) source: T::Source,
53    pub(super) columns: IndexMap<String, T::Column<T::AnyType>>,
54    pub(super) conditions: IndexMap<i64, T::Condition>,
55    pub(super) next_condition_id: i64,
56    pub(super) order_by: IndexMap<i64, (T::Condition, SortDirection)>,
57    pub(super) next_order_id: i64,
58    pub(super) refs: Option<IndexMap<String, Arc<dyn Reference>>>,
59    pub(super) contained: Vec<crate::references::ContainedRelation<T>>,
60    pub(super) expressions: IndexMap<String, ExpressionFn<T>>,
61    pub(super) lazy_expressions: IndexMap<String, LazyExpressionFn<T>>,
62    /// Columns the `Vista` computes after each read (see
63    /// `vantage_vista::Column::with_expression`). The table never selects or
64    /// writes them; it carries them so a Vista built from this table — a
65    /// traversal target included — knows them. Each carries its position
66    /// among all columns, stored and computed, as declared.
67    pub(super) computed_columns: IndexMap<String, (usize, vantage_vista::Column)>,
68    /// When `Some`, `select()` projects only these column names (plus the id
69    /// column, always). `None` keeps the default "project every column"
70    /// behavior. The set holds both plain column names and dotted implicit
71    /// references (`"country.name"`); set via [`Self::with_active_columns`].
72    pub(super) active_columns: Option<indexmap::IndexSet<String>>,
73    /// Dotted implicit-reference columns imported by traversal
74    /// (`"country.name"`). These are read-only, expression-backed projections;
75    /// they are tracked here so write paths never persist them (a SCHEMALESS
76    /// store would otherwise create a literal `country.name` field).
77    pub(super) imported_columns: indexmap::IndexSet<String>,
78    pub(super) pagination: Option<Pagination>,
79    pub(super) title_field: Option<String>,
80    pub(super) title_fields: Vec<String>,
81    pub(super) id_field: Option<String>,
82    /// When true, the id column is a text/string key and backends must NOT
83    /// numerically coerce it (e.g. the Postgres backend otherwise binds an
84    /// all-digit id like `"121"` as `bigint`, which breaks against a `TEXT` id
85    /// column). Set via [`Self::with_text_id`]. Defaults to false to preserve
86    /// the integer-id convention used by other models.
87    pub(super) id_text: bool,
88    /// Column values every row in this set must hold, because they are part of
89    /// the set's definition (e.g. a has-many child carries the parent's foreign
90    /// key). Registered wherever the table is narrowed by a literal
91    /// `column = value` (see [`Self::with_id`], `Reference::resolve_from_row`);
92    /// never from an expression scope. Enforced on write: a column the caller
93    /// left null/absent is filled, a matching value is kept, and a conflicting
94    /// value is rejected.
95    pub(super) invariants: IndexMap<String, T::Value>,
96    /// Lifecycle hooks (see [`Hook`](super::Hook)). Registered via [`Self::with_hook`].
97    pub(super) hooks: Hooks<T>,
98}
99
100impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
101    /// Create a new Table with the given table name and data source
102    pub fn new(table_name: impl Into<String>, data_source: T) -> Self {
103        Self {
104            data_source,
105            _phantom: PhantomData,
106            source: T::Source::from_name(table_name.into()),
107            columns: IndexMap::new(),
108            conditions: IndexMap::new(),
109            next_condition_id: 1,
110            order_by: IndexMap::new(),
111            next_order_id: 1,
112            refs: None,
113            contained: Vec::new(),
114            expressions: IndexMap::new(),
115            lazy_expressions: IndexMap::new(),
116            computed_columns: IndexMap::new(),
117            active_columns: None,
118            imported_columns: indexmap::IndexSet::new(),
119            pagination: None,
120            title_field: None,
121            title_fields: Vec::new(),
122            id_field: None,
123            id_text: false,
124            invariants: IndexMap::new(),
125            hooks: Hooks::default(),
126        }
127    }
128
129    /// Convert this table to use a different entity type.
130    ///
131    /// Computed expressions are carried over — they're stored entity-erased
132    /// (see [`ExpressionFn`]), so aggregates survive reference traversal that
133    /// erases the entity to `EmptyEntity` (e.g. `get_ref_from_row`).
134    pub fn into_entity<E2: Entity<T::Value>>(self) -> Table<T, E2> {
135        Table {
136            data_source: self.data_source,
137            _phantom: PhantomData,
138            source: self.source,
139            columns: self.columns,
140            conditions: self.conditions,
141            next_condition_id: self.next_condition_id,
142            order_by: self.order_by,
143            next_order_id: self.next_order_id,
144            refs: self.refs,
145            contained: self.contained,
146            expressions: self.expressions,
147            lazy_expressions: self.lazy_expressions,
148            computed_columns: self.computed_columns,
149            active_columns: self.active_columns,
150            imported_columns: self.imported_columns,
151            pagination: self.pagination,
152            title_field: self.title_field,
153            title_fields: self.title_fields,
154            id_field: self.id_field,
155            id_text: self.id_text,
156            invariants: self.invariants,
157            hooks: self.hooks,
158        }
159    }
160
161    /// Borrow this table as its entity-erased form `Table<T, EmptyEntity>`.
162    ///
163    /// `E` appears in `Table` only as `PhantomData<E>` (a zero-sized field), so
164    /// `Table<T, E>` and `Table<T, EmptyEntity>` are layout-identical and this
165    /// reinterpret is sound. Used to feed `self` to the entity-erased
166    /// [`ExpressionFn`] closures at evaluation time.
167    pub(crate) fn as_entity_erased(&self) -> &Table<T, EmptyEntity> {
168        // SAFETY: identical layout (E is PhantomData only); lifetime is tied to
169        // `&self`, and the borrow is shared/read-only.
170        unsafe { &*(self as *const Table<T, E> as *const Table<T, EmptyEntity>) }
171    }
172
173    /// Apply lazy expressions to one returned record, in declaration order.
174    /// Each callback borrows the record as built so far; the value it
175    /// returns is inserted under the expression's name before the next
176    /// callback runs. See [`Self::with_lazy_expression`].
177    /// Public so driver shells that bypass the `list_values` read path
178    /// (e.g. a REST shell's windowed fetch) can still apply lazy columns.
179    pub async fn apply_lazy_expressions(
180        &self,
181        record: &mut vantage_types::Record<T::Value>,
182    ) -> vantage_core::Result<()> {
183        for (name, f) in &self.lazy_expressions {
184            let value = f(record).await?;
185            record.insert(name.clone(), value);
186        }
187        Ok(())
188    }
189
190    /// Drop imported implicit-reference columns (`"country.name"`) from a write
191    /// payload. They are read-only, expression-backed projections that no
192    /// backend can honestly store; a round-trip (read → modify → save) would
193    /// otherwise carry them back, and a SCHEMALESS store would create a literal
194    /// `country.name` field. Called before invariants on the full-record write
195    /// paths (insert, insert-returning-id, replace); `patch_value` instead
196    /// rejects imported keys outright, since a partial payload is explicit
197    /// intent per key.
198    pub(super) fn strip_imported_columns(&self, record: &mut vantage_types::Record<T::Value>) {
199        for name in &self.imported_columns {
200            record.shift_remove(name);
201        }
202    }
203
204    /// Whether `name` is an imported implicit-reference column
205    /// (`"country.name"`) — an expression-backed traversal projection that is
206    /// read-only and must never be persisted, ordered, or searched as if it
207    /// were a physical field.
208    pub fn is_imported_column(&self, name: &str) -> bool {
209        self.imported_columns.contains(name)
210    }
211
212    /// Whether `name` is computed rather than stored: an imported
213    /// implicit-reference column, a server-side expression column, or a lazy
214    /// (post-fetch) computed column. Driver factories flag such columns
215    /// `calculated` in vista metadata so consumers render them read-only.
216    pub fn is_calculated_column(&self, name: &str) -> bool {
217        self.imported_columns.contains(name)
218            || self.expressions.contains_key(name)
219            || self.lazy_expressions.contains_key(name)
220    }
221
222    /// Snapshot the table's relations as Vista references (name, target type,
223    /// cardinality, foreign key). Driver factories fold this into
224    /// `VistaMetadata` so the erased `Vista` carries enough to drive nested
225    /// insert and relation traversal.
226    pub fn vista_references(&self) -> Vec<vantage_vista::Reference> {
227        self.refs
228            .as_ref()
229            .map(|refs| {
230                refs.iter()
231                    .map(|(name, r)| {
232                        vantage_vista::Reference::new(
233                            name.clone(),
234                            r.target_type_name().to_string(),
235                            r.cardinality(),
236                            r.foreign_key().to_string(),
237                        )
238                    })
239                    .collect()
240            })
241            .unwrap_or_default()
242    }
243
244    /// Register a column the `Vista` computes after each read, positioned
245    /// after every column added so far. Driver factories fold these into
246    /// `VistaMetadata`. A stored column of the same name (one inherited by a
247    /// derived table) is replaced, so the column is never both read and
248    /// computed.
249    pub fn add_computed_column(&mut self, column: vantage_vista::Column) {
250        if let Some((stored_index, _, _)) = self.columns.shift_remove_full(&column.name) {
251            let removed = self.position_of_stored(stored_index);
252            for (at, _) in self.computed_columns.values_mut() {
253                if *at > removed {
254                    *at -= 1;
255                }
256            }
257        }
258        let at = self.columns.len() + self.computed_columns.len();
259        self.computed_columns
260            .insert(column.name.clone(), (at, column));
261    }
262
263    /// Register `spec`'s column as computed when it declares a `lazy:` script
264    /// (see [`vantage_vista::ColumnSpec::lazy_column`]). Returns `false` for a
265    /// stored column, leaving the caller to add it.
266    pub fn add_lazy_spec_column<C>(
267        &mut self,
268        spec: &vantage_vista::ColumnSpec<C>,
269        name: &str,
270    ) -> vantage_core::Result<bool> {
271        let Some(column) = spec.lazy_column(name)? else {
272            return Ok(false);
273        };
274        self.add_computed_column(column);
275        Ok(true)
276    }
277
278    /// Position among all columns of the stored column at `stored_index`:
279    /// computed columns occupy their recorded positions, stored columns fill
280    /// the remaining slots in order.
281    fn position_of_stored(&self, stored_index: usize) -> usize {
282        let mut computed: Vec<usize> = self.computed_columns.values().map(|(at, _)| *at).collect();
283        computed.sort_unstable();
284        let mut at = stored_index;
285        for position in computed {
286            if position <= at {
287                at += 1;
288            }
289        }
290        at
291    }
292
293    /// Columns registered via [`Self::add_computed_column`], in order, each
294    /// with its position among all columns.
295    pub fn computed_columns(&self) -> impl Iterator<Item = (usize, &vantage_vista::Column)> {
296        self.computed_columns.values().map(|(at, c)| (*at, c))
297    }
298
299    /// Shape-only specs (name, host, kind, id) for the contained relations
300    /// declared on this table, for driver factories to fold into
301    /// `VistaMetadata`. Columns are derived at traversal from each relation's
302    /// `build_target` closure.
303    pub fn vista_contained(&self) -> Vec<vantage_vista::ContainedSpec> {
304        self.contained.iter().map(|c| c.spec()).collect()
305    }
306
307    /// Look up a contained relation by name (for the driver's traversal).
308    pub fn contained_relation(
309        &self,
310        name: &str,
311    ) -> Option<&crate::references::ContainedRelation<T>> {
312        self.contained.iter().find(|c| c.name() == name)
313    }
314
315    /// Use a callback with a builder pattern for configuration
316    pub fn with<F>(mut self, func: F) -> Self
317    where
318        F: FnOnce(&mut Self),
319    {
320        func(&mut self);
321        self
322    }
323
324    /// Get the table name.
325    ///
326    /// For a query-sourced table this is its FROM alias.
327    pub fn table_name(&self) -> &str {
328        self.source.name()
329    }
330
331    /// The table's source (a name, or a query used as a derived source).
332    pub fn source(&self) -> &T::Source {
333        &self.source
334    }
335
336    /// Override the table name. Used by REST API drivers to swap a
337    /// canonical resource path for a per-reference URI template at
338    /// traversal time.
339    ///
340    /// This replaces the source with a name-based one, so it must not be
341    /// called on a query-sourced (derived) table.
342    pub fn set_table_name(&mut self, name: impl Into<String>) {
343        self.source = T::Source::from_name(name.into());
344    }
345
346    /// Get the underlying data source
347    pub fn data_source(&self) -> &T {
348        &self.data_source
349    }
350
351    /// Get the title field column if set
352    pub fn title_field(&self) -> Option<&T::Column<T::AnyType>> {
353        self.title_field
354            .as_ref()
355            .and_then(|name| self.columns.get(name))
356    }
357
358    /// Names of columns marked as display titles (set via
359    /// [`Self::with_title_column_of`]). These show alongside the id in
360    /// list views and on the leading lines of single-record displays.
361    pub fn title_fields(&self) -> &[String] {
362        &self.title_fields
363    }
364
365    /// Get the id field column if set
366    pub fn id_field(&self) -> Option<&T::Column<T::AnyType>> {
367        self.id_field
368            .as_ref()
369            .and_then(|name| self.columns.get(name))
370    }
371
372    /// Name of the [`id_field`](Self::id_field) column, or `"id"` when there
373    /// is none.
374    pub fn id_field_name(&self) -> String {
375        use crate::traits::column_like::ColumnLike;
376        self.id_field()
377            .map(|c| c.name().to_string())
378            .unwrap_or_else(|| "id".to_string())
379    }
380
381    /// Mark an already-added column as the id field.
382    ///
383    /// Use this when the id column has been added via [`Self::add_column`]
384    /// (so its type and aliases were chosen explicitly) and you only need
385    /// to flag it. [`Self::with_id_column`] is the typed shortcut that
386    /// creates the column for you.
387    pub fn set_id_field(&mut self, name: impl Into<String>) {
388        self.id_field = Some(name.into());
389    }
390
391    /// Mark an already-added column as a display title.
392    ///
393    /// Companion to [`Self::set_id_field`] for spec-driven construction.
394    pub fn add_title_field(&mut self, name: impl Into<String>) {
395        let name = name.into();
396        if !self.title_fields.contains(&name) {
397            self.title_fields.push(name.clone());
398        }
399        if self.title_field.is_none() {
400            self.title_field = Some(name);
401        }
402    }
403
404    /// Get the current pagination configuration, if set
405    pub fn pagination(&self) -> Option<&Pagination> {
406        self.pagination.as_ref()
407    }
408
409    /// Column values every row in this set must hold (see the `invariants`
410    /// field): enforced on write — filled when null/absent, kept when matching,
411    /// rejected when conflicting.
412    pub fn invariants(&self) -> &IndexMap<String, T::Value> {
413        &self.invariants
414    }
415
416    /// Register an invariant value for `column` on this set.
417    ///
418    /// A later call for the same column overwrites the earlier invariant.
419    pub fn add_invariant(&mut self, column: impl Into<String>, value: T::Value) {
420        self.invariants.insert(column.into(), value);
421    }
422
423    /// Builder form of [`Self::add_invariant`].
424    pub fn with_invariant(mut self, column: impl Into<String>, value: T::Value) -> Self {
425        self.add_invariant(column, value);
426        self
427    }
428}
429
430impl<T: TableSource, E: Entity<T::Value>> std::ops::Index<&str> for Table<T, E> {
431    type Output = T::Column<T::AnyType>;
432
433    fn index(&self, index: &str) -> &Self::Output {
434        &self.columns[index]
435    }
436}
437
438impl<T: TableSource, E: Entity<T::Value>> std::fmt::Debug for Table<T, E> {
439    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440        f.debug_struct("Table")
441            .field("table_name", &self.table_name())
442            .field("columns", &self.columns.keys().collect::<Vec<_>>())
443            .field("conditions_count", &self.conditions.len())
444            .field(
445                "refs_count",
446                &self.refs.as_ref().map(|r| r.len()).unwrap_or(0),
447            )
448            .field("expressions_count", &self.expressions.len())
449            .finish()
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use crate::mocks::mock_table_source::MockTableSource;
457
458    #[test]
459    fn computed_column_replacing_a_stored_one_keeps_positions() {
460        let mut table = Table::<MockTableSource, EmptyEntity>::new("t", MockTableSource::new())
461            .with_column_of::<String>("a")
462            .with_column_of::<String>("b");
463        table.add_computed_column(vantage_vista::Column::new("x", "string"));
464        table.add_column_of::<String>("c");
465        assert_eq!(
466            table
467                .computed_columns()
468                .map(|(at, c)| (at, c.name.as_str()))
469                .collect::<Vec<_>>(),
470            vec![(2, "x")]
471        );
472
473        table.add_computed_column(vantage_vista::Column::new("a", "string"));
474
475        let stored: Vec<&str> = table.columns().keys().map(String::as_str).collect();
476        assert_eq!(stored, vec!["b", "c"]);
477        assert_eq!(
478            table
479                .computed_columns()
480                .map(|(at, c)| (at, c.name.as_str()))
481                .collect::<Vec<_>>(),
482            vec![(1, "x"), (3, "a")]
483        );
484    }
485}