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    /// When `Some`, `select()` projects only these column names (plus the id
63    /// column, always). `None` keeps the default "project every column"
64    /// behavior. The set holds both plain column names and dotted implicit
65    /// references (`"country.name"`); set via [`Self::with_active_columns`].
66    pub(super) active_columns: Option<indexmap::IndexSet<String>>,
67    /// Dotted implicit-reference columns imported by traversal
68    /// (`"country.name"`). These are read-only, expression-backed projections;
69    /// they are tracked here so write paths never persist them (a SCHEMALESS
70    /// store would otherwise create a literal `country.name` field).
71    pub(super) imported_columns: indexmap::IndexSet<String>,
72    pub(super) pagination: Option<Pagination>,
73    pub(super) title_field: Option<String>,
74    pub(super) title_fields: Vec<String>,
75    pub(super) id_field: Option<String>,
76    /// When true, the id column is a text/string key and backends must NOT
77    /// numerically coerce it (e.g. the Postgres backend otherwise binds an
78    /// all-digit id like `"121"` as `bigint`, which breaks against a `TEXT` id
79    /// column). Set via [`Self::with_text_id`]. Defaults to false to preserve
80    /// the integer-id convention used by other models.
81    pub(super) id_text: bool,
82    /// Column values every row in this set must hold, because they are part of
83    /// the set's definition (e.g. a has-many child carries the parent's foreign
84    /// key). Registered wherever the table is narrowed by a literal
85    /// `column = value` (see [`Self::with_id`], `Reference::resolve_from_row`);
86    /// never from an expression scope. Enforced on write: a column the caller
87    /// left null/absent is filled, a matching value is kept, and a conflicting
88    /// value is rejected.
89    pub(super) invariants: IndexMap<String, T::Value>,
90    /// Lifecycle hooks (see [`Hook`](super::Hook)). Registered via [`Self::with_hook`].
91    pub(super) hooks: Hooks<T>,
92}
93
94impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
95    /// Create a new Table with the given table name and data source
96    pub fn new(table_name: impl Into<String>, data_source: T) -> Self {
97        Self {
98            data_source,
99            _phantom: PhantomData,
100            source: T::Source::from_name(table_name.into()),
101            columns: IndexMap::new(),
102            conditions: IndexMap::new(),
103            next_condition_id: 1,
104            order_by: IndexMap::new(),
105            next_order_id: 1,
106            refs: None,
107            contained: Vec::new(),
108            expressions: IndexMap::new(),
109            lazy_expressions: IndexMap::new(),
110            active_columns: None,
111            imported_columns: indexmap::IndexSet::new(),
112            pagination: None,
113            title_field: None,
114            title_fields: Vec::new(),
115            id_field: None,
116            id_text: false,
117            invariants: IndexMap::new(),
118            hooks: Hooks::default(),
119        }
120    }
121
122    /// Convert this table to use a different entity type.
123    ///
124    /// Computed expressions are carried over — they're stored entity-erased
125    /// (see [`ExpressionFn`]), so aggregates survive reference traversal that
126    /// erases the entity to `EmptyEntity` (e.g. `get_ref_from_row`).
127    pub fn into_entity<E2: Entity<T::Value>>(self) -> Table<T, E2> {
128        Table {
129            data_source: self.data_source,
130            _phantom: PhantomData,
131            source: self.source,
132            columns: self.columns,
133            conditions: self.conditions,
134            next_condition_id: self.next_condition_id,
135            order_by: self.order_by,
136            next_order_id: self.next_order_id,
137            refs: self.refs,
138            contained: self.contained,
139            expressions: self.expressions,
140            lazy_expressions: self.lazy_expressions,
141            active_columns: self.active_columns,
142            imported_columns: self.imported_columns,
143            pagination: self.pagination,
144            title_field: self.title_field,
145            title_fields: self.title_fields,
146            id_field: self.id_field,
147            id_text: self.id_text,
148            invariants: self.invariants,
149            hooks: self.hooks,
150        }
151    }
152
153    /// Borrow this table as its entity-erased form `Table<T, EmptyEntity>`.
154    ///
155    /// `E` appears in `Table` only as `PhantomData<E>` (a zero-sized field), so
156    /// `Table<T, E>` and `Table<T, EmptyEntity>` are layout-identical and this
157    /// reinterpret is sound. Used to feed `self` to the entity-erased
158    /// [`ExpressionFn`] closures at evaluation time.
159    pub(crate) fn as_entity_erased(&self) -> &Table<T, EmptyEntity> {
160        // SAFETY: identical layout (E is PhantomData only); lifetime is tied to
161        // `&self`, and the borrow is shared/read-only.
162        unsafe { &*(self as *const Table<T, E> as *const Table<T, EmptyEntity>) }
163    }
164
165    /// Apply lazy expressions to one returned record, in declaration order.
166    /// Each callback borrows the record as built so far; the value it
167    /// returns is inserted under the expression's name before the next
168    /// callback runs. See [`Self::with_lazy_expression`].
169    /// Public so driver shells that bypass the `list_values` read path
170    /// (e.g. a REST shell's windowed fetch) can still apply lazy columns.
171    pub async fn apply_lazy_expressions(
172        &self,
173        record: &mut vantage_types::Record<T::Value>,
174    ) -> vantage_core::Result<()> {
175        for (name, f) in &self.lazy_expressions {
176            let value = f(record).await?;
177            record.insert(name.clone(), value);
178        }
179        Ok(())
180    }
181
182    /// Drop imported implicit-reference columns (`"country.name"`) from a write
183    /// payload. They are read-only, expression-backed projections that no
184    /// backend can honestly store; a round-trip (read → modify → save) would
185    /// otherwise carry them back, and a SCHEMALESS store would create a literal
186    /// `country.name` field. Called before invariants on the full-record write
187    /// paths (insert, insert-returning-id, replace); `patch_value` instead
188    /// rejects imported keys outright, since a partial payload is explicit
189    /// intent per key.
190    pub(super) fn strip_imported_columns(&self, record: &mut vantage_types::Record<T::Value>) {
191        for name in &self.imported_columns {
192            record.shift_remove(name);
193        }
194    }
195
196    /// Whether `name` is an imported implicit-reference column
197    /// (`"country.name"`) — an expression-backed traversal projection that is
198    /// read-only and must never be persisted, ordered, or searched as if it
199    /// were a physical field.
200    pub fn is_imported_column(&self, name: &str) -> bool {
201        self.imported_columns.contains(name)
202    }
203
204    /// Whether `name` is computed rather than stored: an imported
205    /// implicit-reference column, a server-side expression column, or a lazy
206    /// (post-fetch) computed column. Driver factories flag such columns
207    /// `calculated` in vista metadata so consumers render them read-only.
208    pub fn is_calculated_column(&self, name: &str) -> bool {
209        self.imported_columns.contains(name)
210            || self.expressions.contains_key(name)
211            || self.lazy_expressions.contains_key(name)
212    }
213
214    /// Snapshot the table's relations as Vista references (name, target type,
215    /// cardinality, foreign key). Driver factories fold this into
216    /// `VistaMetadata` so the erased `Vista` carries enough to drive nested
217    /// insert and relation traversal.
218    pub fn vista_references(&self) -> Vec<vantage_vista::Reference> {
219        self.refs
220            .as_ref()
221            .map(|refs| {
222                refs.iter()
223                    .map(|(name, r)| {
224                        vantage_vista::Reference::new(
225                            name.clone(),
226                            r.target_type_name().to_string(),
227                            r.cardinality(),
228                            r.foreign_key().to_string(),
229                        )
230                    })
231                    .collect()
232            })
233            .unwrap_or_default()
234    }
235
236    /// Shape-only specs (name, host, kind, id) for the contained relations
237    /// declared on this table, for driver factories to fold into
238    /// `VistaMetadata`. Columns are derived at traversal from each relation's
239    /// `build_target` closure.
240    pub fn vista_contained(&self) -> Vec<vantage_vista::ContainedSpec> {
241        self.contained.iter().map(|c| c.spec()).collect()
242    }
243
244    /// Look up a contained relation by name (for the driver's traversal).
245    pub fn contained_relation(
246        &self,
247        name: &str,
248    ) -> Option<&crate::references::ContainedRelation<T>> {
249        self.contained.iter().find(|c| c.name() == name)
250    }
251
252    /// Use a callback with a builder pattern for configuration
253    pub fn with<F>(mut self, func: F) -> Self
254    where
255        F: FnOnce(&mut Self),
256    {
257        func(&mut self);
258        self
259    }
260
261    /// Get the table name.
262    ///
263    /// For a query-sourced table this is its FROM alias.
264    pub fn table_name(&self) -> &str {
265        self.source.name()
266    }
267
268    /// The table's source (a name, or a query used as a derived source).
269    pub fn source(&self) -> &T::Source {
270        &self.source
271    }
272
273    /// Override the table name. Used by REST API drivers to swap a
274    /// canonical resource path for a per-reference URI template at
275    /// traversal time.
276    ///
277    /// This replaces the source with a name-based one, so it must not be
278    /// called on a query-sourced (derived) table.
279    pub fn set_table_name(&mut self, name: impl Into<String>) {
280        self.source = T::Source::from_name(name.into());
281    }
282
283    /// Get the underlying data source
284    pub fn data_source(&self) -> &T {
285        &self.data_source
286    }
287
288    /// Get the title field column if set
289    pub fn title_field(&self) -> Option<&T::Column<T::AnyType>> {
290        self.title_field
291            .as_ref()
292            .and_then(|name| self.columns.get(name))
293    }
294
295    /// Names of columns marked as display titles (set via
296    /// [`Self::with_title_column_of`]). These show alongside the id in
297    /// list views and on the leading lines of single-record displays.
298    pub fn title_fields(&self) -> &[String] {
299        &self.title_fields
300    }
301
302    /// Get the id field column if set
303    pub fn id_field(&self) -> Option<&T::Column<T::AnyType>> {
304        self.id_field
305            .as_ref()
306            .and_then(|name| self.columns.get(name))
307    }
308
309    /// Mark an already-added column as the id field.
310    ///
311    /// Use this when the id column has been added via [`Self::add_column`]
312    /// (so its type and aliases were chosen explicitly) and you only need
313    /// to flag it. [`Self::with_id_column`] is the typed shortcut that
314    /// creates the column for you.
315    pub fn set_id_field(&mut self, name: impl Into<String>) {
316        self.id_field = Some(name.into());
317    }
318
319    /// Mark an already-added column as a display title.
320    ///
321    /// Companion to [`Self::set_id_field`] for spec-driven construction.
322    pub fn add_title_field(&mut self, name: impl Into<String>) {
323        let name = name.into();
324        if !self.title_fields.contains(&name) {
325            self.title_fields.push(name.clone());
326        }
327        if self.title_field.is_none() {
328            self.title_field = Some(name);
329        }
330    }
331
332    /// Get the current pagination configuration, if set
333    pub fn pagination(&self) -> Option<&Pagination> {
334        self.pagination.as_ref()
335    }
336
337    /// Column values every row in this set must hold (see the `invariants`
338    /// field): enforced on write — filled when null/absent, kept when matching,
339    /// rejected when conflicting.
340    pub fn invariants(&self) -> &IndexMap<String, T::Value> {
341        &self.invariants
342    }
343
344    /// Register an invariant value for `column` on this set.
345    ///
346    /// A later call for the same column overwrites the earlier invariant.
347    pub fn add_invariant(&mut self, column: impl Into<String>, value: T::Value) {
348        self.invariants.insert(column.into(), value);
349    }
350
351    /// Builder form of [`Self::add_invariant`].
352    pub fn with_invariant(mut self, column: impl Into<String>, value: T::Value) -> Self {
353        self.add_invariant(column, value);
354        self
355    }
356}
357
358impl<T: TableSource, E: Entity<T::Value>> std::ops::Index<&str> for Table<T, E> {
359    type Output = T::Column<T::AnyType>;
360
361    fn index(&self, index: &str) -> &Self::Output {
362        &self.columns[index]
363    }
364}
365
366impl<T: TableSource, E: Entity<T::Value>> std::fmt::Debug for Table<T, E> {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        f.debug_struct("Table")
369            .field("table_name", &self.table_name())
370            .field("columns", &self.columns.keys().collect::<Vec<_>>())
371            .field("conditions_count", &self.conditions.len())
372            .field(
373                "refs_count",
374                &self.refs.as_ref().map(|r| r.len()).unwrap_or(0),
375            )
376            .field("expressions_count", &self.expressions.len())
377            .finish()
378    }
379}