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}