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    /// When true, the backend makes this table's ids (auto-increment,
89    /// generated record ids). Set via [`Self::with_auto_id`]; Vista metadata
90    /// flags the id column `auto`.
91    pub(super) id_auto: bool,
92    /// Column values every row in this set must hold, because they are part of
93    /// the set's definition (e.g. a has-many child carries the parent's foreign
94    /// key). Registered wherever the table is narrowed by a literal
95    /// `column = value` (see [`Self::with_id`], `Reference::resolve_from_row`);
96    /// never from an expression scope (and every literal `column = value`
97    /// passed to `add_condition`). Enforced on write: insert and replace fill a
98    /// null/absent column and reject a conflicting one; patch never fills, and
99    /// rejects a present value that differs (or is null).
100    pub(super) invariants: IndexMap<String, T::Value>,
101    /// Lifecycle hooks (see [`Hook`](super::Hook)). Registered via [`Self::with_hook`].
102    pub(super) hooks: Hooks<T>,
103}
104
105impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
106    /// Create a new Table with the given table name and data source
107    pub fn new(table_name: impl Into<String>, data_source: T) -> Self {
108        Self {
109            data_source,
110            _phantom: PhantomData,
111            source: T::Source::from_name(table_name.into()),
112            columns: IndexMap::new(),
113            conditions: IndexMap::new(),
114            next_condition_id: 1,
115            order_by: IndexMap::new(),
116            next_order_id: 1,
117            refs: None,
118            contained: Vec::new(),
119            expressions: IndexMap::new(),
120            lazy_expressions: IndexMap::new(),
121            computed_columns: IndexMap::new(),
122            active_columns: None,
123            imported_columns: indexmap::IndexSet::new(),
124            pagination: None,
125            title_field: None,
126            title_fields: Vec::new(),
127            id_field: None,
128            id_text: false,
129            id_auto: false,
130            invariants: IndexMap::new(),
131            hooks: Hooks::default(),
132        }
133    }
134
135    /// Convert this table to use a different entity type.
136    ///
137    /// Computed expressions are carried over — they're stored entity-erased
138    /// (see [`ExpressionFn`]), so aggregates survive reference traversal that
139    /// erases the entity to `EmptyEntity` (e.g. `get_ref_from_row`).
140    pub fn into_entity<E2: Entity<T::Value>>(self) -> Table<T, E2> {
141        Table {
142            data_source: self.data_source,
143            _phantom: PhantomData,
144            source: self.source,
145            columns: self.columns,
146            conditions: self.conditions,
147            next_condition_id: self.next_condition_id,
148            order_by: self.order_by,
149            next_order_id: self.next_order_id,
150            refs: self.refs,
151            contained: self.contained,
152            expressions: self.expressions,
153            lazy_expressions: self.lazy_expressions,
154            computed_columns: self.computed_columns,
155            active_columns: self.active_columns,
156            imported_columns: self.imported_columns,
157            pagination: self.pagination,
158            title_field: self.title_field,
159            title_fields: self.title_fields,
160            id_field: self.id_field,
161            id_text: self.id_text,
162            id_auto: self.id_auto,
163            invariants: self.invariants,
164            hooks: self.hooks,
165        }
166    }
167
168    /// Borrow this table as its entity-erased form `Table<T, EmptyEntity>`.
169    ///
170    /// `E` appears in `Table` only as `PhantomData<E>` (a zero-sized field), so
171    /// `Table<T, E>` and `Table<T, EmptyEntity>` are layout-identical and this
172    /// reinterpret is sound. Used to feed `self` to the entity-erased
173    /// [`ExpressionFn`] closures at evaluation time.
174    pub(crate) fn as_entity_erased(&self) -> &Table<T, EmptyEntity> {
175        // SAFETY: identical layout (E is PhantomData only); lifetime is tied to
176        // `&self`, and the borrow is shared/read-only.
177        unsafe { &*(self as *const Table<T, E> as *const Table<T, EmptyEntity>) }
178    }
179
180    /// Apply lazy expressions to one returned record, in declaration order.
181    /// Each callback borrows the record as built so far; the value it
182    /// returns is inserted under the expression's name before the next
183    /// callback runs. See [`Self::with_lazy_expression`].
184    /// Public so driver shells that bypass the `list_values` read path
185    /// (e.g. a REST shell's windowed fetch) can still apply lazy columns.
186    pub async fn apply_lazy_expressions(
187        &self,
188        record: &mut vantage_types::Record<T::Value>,
189    ) -> vantage_core::Result<()> {
190        for (name, f) in &self.lazy_expressions {
191            let value = f(record).await?;
192            record.insert(name.clone(), value);
193        }
194        Ok(())
195    }
196
197    /// Drop imported implicit-reference columns (`"country.name"`) from a write
198    /// payload. They are read-only, expression-backed projections that no
199    /// backend can honestly store; a round-trip (read → modify → save) would
200    /// otherwise carry them back, and a SCHEMALESS store would create a literal
201    /// `country.name` field. Called before invariants on the full-record write
202    /// paths (insert, insert-returning-id, replace); `patch_value` instead
203    /// rejects imported keys outright, since a partial payload is explicit
204    /// intent per key.
205    pub(super) fn strip_imported_columns(&self, record: &mut vantage_types::Record<T::Value>) {
206        for name in &self.imported_columns {
207            record.shift_remove(name);
208        }
209    }
210
211    /// Whether `name` is an imported implicit-reference column
212    /// (`"country.name"`) — an expression-backed traversal projection that is
213    /// read-only and must never be persisted, ordered, or searched as if it
214    /// were a physical field.
215    pub fn is_imported_column(&self, name: &str) -> bool {
216        self.imported_columns.contains(name)
217    }
218
219    /// Whether `name` is computed rather than stored: an imported
220    /// implicit-reference column, a server-side expression column, or a lazy
221    /// (post-fetch) computed column. Driver factories flag such columns
222    /// `calculated` in vista metadata so consumers render them read-only.
223    pub fn is_calculated_column(&self, name: &str) -> bool {
224        self.imported_columns.contains(name)
225            || self.expressions.contains_key(name)
226            || self.lazy_expressions.contains_key(name)
227    }
228
229    /// Snapshot the table's relations as Vista references (name, target type,
230    /// cardinality, foreign key). Driver factories fold this into
231    /// `VistaMetadata` so the erased `Vista` carries enough to drive nested
232    /// insert and relation traversal.
233    pub fn vista_references(&self) -> Vec<vantage_vista::Reference> {
234        self.refs
235            .as_ref()
236            .map(|refs| {
237                refs.iter()
238                    .map(|(name, r)| {
239                        vantage_vista::Reference::new(
240                            name.clone(),
241                            r.target_type_name().to_string(),
242                            r.cardinality(),
243                            r.foreign_key().to_string(),
244                        )
245                    })
246                    .collect()
247            })
248            .unwrap_or_default()
249    }
250
251    /// Register a column the `Vista` computes after each read, positioned
252    /// after every column added so far. Driver factories fold these into
253    /// `VistaMetadata`. A stored column of the same name (one inherited by a
254    /// derived table) is replaced, so the column is never both read and
255    /// computed.
256    pub fn add_computed_column(&mut self, column: vantage_vista::Column) {
257        if let Some((stored_index, _, _)) = self.columns.shift_remove_full(&column.name) {
258            let removed = self.position_of_stored(stored_index);
259            for (at, _) in self.computed_columns.values_mut() {
260                if *at > removed {
261                    *at -= 1;
262                }
263            }
264        }
265        let at = self.columns.len() + self.computed_columns.len();
266        self.computed_columns
267            .insert(column.name.clone(), (at, column));
268    }
269
270    /// Register `spec`'s column as computed when it declares a `lazy:` script
271    /// (see [`vantage_vista::ColumnSpec::lazy_column`]). Returns `false` for a
272    /// stored column, leaving the caller to add it.
273    pub fn add_lazy_spec_column<C>(
274        &mut self,
275        spec: &vantage_vista::ColumnSpec<C>,
276        name: &str,
277    ) -> vantage_core::Result<bool> {
278        let Some(column) = spec.lazy_column(name)? else {
279            return Ok(false);
280        };
281        self.add_computed_column(column);
282        Ok(true)
283    }
284
285    /// Position among all columns of the stored column at `stored_index`:
286    /// computed columns occupy their recorded positions, stored columns fill
287    /// the remaining slots in order.
288    fn position_of_stored(&self, stored_index: usize) -> usize {
289        let mut computed: Vec<usize> = self.computed_columns.values().map(|(at, _)| *at).collect();
290        computed.sort_unstable();
291        let mut at = stored_index;
292        for position in computed {
293            if position <= at {
294                at += 1;
295            }
296        }
297        at
298    }
299
300    /// Columns registered via [`Self::add_computed_column`], in order, each
301    /// with its position among all columns.
302    pub fn computed_columns(&self) -> impl Iterator<Item = (usize, &vantage_vista::Column)> {
303        self.computed_columns.values().map(|(at, c)| (*at, c))
304    }
305
306    /// Shape-only specs (name, host, kind, id) for the contained relations
307    /// declared on this table, for driver factories to fold into
308    /// `VistaMetadata`. Columns are derived at traversal from each relation's
309    /// `build_target` closure.
310    pub fn vista_contained(&self) -> Vec<vantage_vista::ContainedSpec> {
311        self.contained.iter().map(|c| c.spec()).collect()
312    }
313
314    /// Look up a contained relation by name (for the driver's traversal).
315    pub fn contained_relation(
316        &self,
317        name: &str,
318    ) -> Option<&crate::references::ContainedRelation<T>> {
319        self.contained.iter().find(|c| c.name() == name)
320    }
321
322    /// Use a callback with a builder pattern for configuration
323    pub fn with<F>(mut self, func: F) -> Self
324    where
325        F: FnOnce(&mut Self),
326    {
327        func(&mut self);
328        self
329    }
330
331    /// Get the table name.
332    ///
333    /// For a query-sourced table this is its FROM alias.
334    pub fn table_name(&self) -> &str {
335        self.source.name()
336    }
337
338    /// Refuse a by-id write through a conditioned table whose source can't
339    /// keep it inside the set (see [`TableSource::can_confine_writes`]). An
340    /// unconditioned table writes anywhere.
341    pub(crate) fn require_confined_writes(&self, verb: &'static str) -> vantage_core::Result<()> {
342        if self.conditions.is_empty() || self.data_source.can_confine_writes() {
343            return Ok(());
344        }
345        Err(vantage_core::error!(
346            "this data source can't keep a write inside a narrowed table",
347            verb = verb,
348            table = self.table_name()
349        )
350        .mark_unsupported())
351    }
352
353    /// The table's source (a name, or a query used as a derived source).
354    pub fn source(&self) -> &T::Source {
355        &self.source
356    }
357
358    /// Override the table name. Used by REST API drivers to swap a
359    /// canonical resource path for a per-reference URI template at
360    /// traversal time.
361    ///
362    /// This replaces the source with a name-based one, so it must not be
363    /// called on a query-sourced (derived) table.
364    pub fn set_table_name(&mut self, name: impl Into<String>) {
365        self.source = T::Source::from_name(name.into());
366    }
367
368    /// Get the underlying data source
369    pub fn data_source(&self) -> &T {
370        &self.data_source
371    }
372
373    /// Get the title field column if set
374    pub fn title_field(&self) -> Option<&T::Column<T::AnyType>> {
375        self.title_field
376            .as_ref()
377            .and_then(|name| self.columns.get(name))
378    }
379
380    /// Names of columns marked as display titles (set via
381    /// [`Self::with_title_column_of`]). These show alongside the id in
382    /// list views and on the leading lines of single-record displays.
383    pub fn title_fields(&self) -> &[String] {
384        &self.title_fields
385    }
386
387    /// Get the id field column if set
388    pub fn id_field(&self) -> Option<&T::Column<T::AnyType>> {
389        self.id_field
390            .as_ref()
391            .and_then(|name| self.columns.get(name))
392    }
393
394    /// Name of the [`id_field`](Self::id_field) column, or `"id"` when there
395    /// is none.
396    pub fn id_field_name(&self) -> String {
397        use crate::traits::column_like::ColumnLike;
398        self.id_field()
399            .map(|c| c.name().to_string())
400            .unwrap_or_else(|| "id".to_string())
401    }
402
403    /// Mark an already-added column as the id field.
404    ///
405    /// Use this when the id column has been added via [`Self::add_column`]
406    /// (so its type and aliases were chosen explicitly) and you only need
407    /// to flag it. [`Self::with_id_column`] is the typed shortcut that
408    /// creates the column for you.
409    pub fn set_id_field(&mut self, name: impl Into<String>) {
410        self.id_field = Some(name.into());
411    }
412
413    /// Mark an already-added column as a display title.
414    ///
415    /// Companion to [`Self::set_id_field`] for spec-driven construction.
416    pub fn add_title_field(&mut self, name: impl Into<String>) {
417        let name = name.into();
418        if !self.title_fields.contains(&name) {
419            self.title_fields.push(name.clone());
420        }
421        if self.title_field.is_none() {
422            self.title_field = Some(name);
423        }
424    }
425
426    /// Get the current pagination configuration, if set
427    pub fn pagination(&self) -> Option<&Pagination> {
428        self.pagination.as_ref()
429    }
430
431    /// Column values every row in this set must hold (see the `invariants`
432    /// field): insert and replace fill a null/absent column and reject a
433    /// conflicting one; patch never fills, and rejects a present value that
434    /// differs (or is null).
435    pub fn invariants(&self) -> &IndexMap<String, T::Value> {
436        &self.invariants
437    }
438
439    /// Register an invariant value for `column` on this set.
440    ///
441    /// A later call for the same column overwrites the earlier invariant.
442    pub fn add_invariant(&mut self, column: impl Into<String>, value: T::Value) {
443        self.invariants.insert(column.into(), value);
444    }
445
446    /// Builder form of [`Self::add_invariant`].
447    pub fn with_invariant(mut self, column: impl Into<String>, value: T::Value) -> Self {
448        self.add_invariant(column, value);
449        self
450    }
451}
452
453impl<T: TableSource, E: Entity<T::Value>> std::ops::Index<&str> for Table<T, E> {
454    type Output = T::Column<T::AnyType>;
455
456    fn index(&self, index: &str) -> &Self::Output {
457        &self.columns[index]
458    }
459}
460
461impl<T: TableSource, E: Entity<T::Value>> std::fmt::Debug for Table<T, E> {
462    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463        f.debug_struct("Table")
464            .field("table_name", &self.table_name())
465            .field("columns", &self.columns.keys().collect::<Vec<_>>())
466            .field("conditions_count", &self.conditions.len())
467            .field(
468                "refs_count",
469                &self.refs.as_ref().map(|r| r.len()).unwrap_or(0),
470            )
471            .field("expressions_count", &self.expressions.len())
472            .finish()
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use crate::mocks::mock_table_source::MockTableSource;
480
481    #[test]
482    fn computed_column_replacing_a_stored_one_keeps_positions() {
483        let mut table = Table::<MockTableSource, EmptyEntity>::new("t", MockTableSource::new())
484            .with_column_of::<String>("a")
485            .with_column_of::<String>("b");
486        table.add_computed_column(vantage_vista::Column::new("x", "string"));
487        table.add_column_of::<String>("c");
488        assert_eq!(
489            table
490                .computed_columns()
491                .map(|(at, c)| (at, c.name.as_str()))
492                .collect::<Vec<_>>(),
493            vec![(2, "x")]
494        );
495
496        table.add_computed_column(vantage_vista::Column::new("a", "string"));
497
498        let stored: Vec<&str> = table.columns().keys().map(String::as_str).collect();
499        assert_eq!(stored, vec!["b", "c"]);
500        assert_eq!(
501            table
502                .computed_columns()
503                .map(|(at, c)| (at, c.name.as_str()))
504                .collect::<Vec<_>>(),
505            vec![(1, "x"), (3, "a")]
506        );
507    }
508}