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
13mod accessors;
14mod computed;
15mod relations;
16
17/// Type alias for expression closures stored on Table.
18///
19/// Stored against the entity-erased `Table<T, EmptyEntity>` rather than the
20/// concrete `Table<T, E>` so the closures survive [`Table::into_entity`] — an
21/// expression only ever reads entity-agnostic table state (columns and
22/// relations by name, conditions, subqueries), never the entity's typed fields.
23/// [`Table::with_expression`] adapts the caller's `Fn(&Table<T, E>)` into this
24/// shape; see `Table::as_entity_erased` for the soundness of the cast.
25pub type ExpressionFn<T> =
26    Arc<dyn Fn(&Table<T, EmptyEntity>) -> Expression<<T as TableSource>::Value> + Send + Sync>;
27
28/// Type alias for lazy-expression callbacks stored on Table.
29///
30/// A lazy expression runs *after* the data source returns a record.
31/// Callbacks apply in declaration order: each borrows the record as built
32/// so far, and the value it returns is inserted under the expression's
33/// name. A later lazy expression therefore sees the columns produced by
34/// earlier ones — one expensive fetch (a file's contents) can feed several
35/// cheap derived columns. See [`Table::with_lazy_expression`].
36pub type LazyExpressionFn<T> = Arc<
37    dyn Fn(
38            &vantage_types::Record<<T as TableSource>::Value>,
39        ) -> std::pin::Pin<
40            Box<
41                dyn std::future::Future<Output = vantage_core::Result<<T as TableSource>::Value>>
42                    + Send,
43            >,
44        > + Send
45        + Sync,
46>;
47
48#[derive(Clone)]
49pub struct Table<T, E>
50where
51    T: TableSource,
52    E: Entity<T::Value>,
53{
54    pub(super) data_source: T,
55    pub(super) _phantom: PhantomData<E>,
56    pub(super) source: T::Source,
57    pub(super) columns: IndexMap<String, T::Column<T::AnyType>>,
58    pub(super) conditions: IndexMap<i64, T::Condition>,
59    pub(super) next_condition_id: i64,
60    pub(super) order_by: IndexMap<i64, (T::Condition, SortDirection)>,
61    pub(super) next_order_id: i64,
62    pub(super) refs: Option<IndexMap<String, Arc<dyn Reference>>>,
63    pub(super) contained: Vec<crate::references::ContainedRelation<T>>,
64    pub(super) expressions: IndexMap<String, ExpressionFn<T>>,
65    pub(super) lazy_expressions: IndexMap<String, LazyExpressionFn<T>>,
66    /// Columns the `Vista` computes after each read (see
67    /// `vantage_vista::Column::with_expression`). The table never selects or
68    /// writes them; it carries them so a Vista built from this table — a
69    /// traversal target included — knows them. Each carries its position
70    /// among all columns, stored and computed, as declared.
71    pub(super) computed_columns: IndexMap<String, (usize, vantage_vista::Column)>,
72    /// When `Some`, `select()` projects only these column names (plus the id
73    /// column, always). `None` keeps the default "project every column"
74    /// behavior. The set holds both plain column names and dotted implicit
75    /// references (`"country.name"`); set via [`Self::with_active_columns`].
76    pub(super) active_columns: Option<indexmap::IndexSet<String>>,
77    /// Dotted implicit-reference columns imported by traversal
78    /// (`"country.name"`). These are read-only, expression-backed projections;
79    /// they are tracked here so write paths never persist them (a SCHEMALESS
80    /// store would otherwise create a literal `country.name` field).
81    pub(super) imported_columns: indexmap::IndexSet<String>,
82    pub(super) pagination: Option<Pagination>,
83    pub(super) title_field: Option<String>,
84    pub(super) title_fields: Vec<String>,
85    pub(super) id_field: Option<String>,
86    /// When true, the id column is a text/string key and backends must NOT
87    /// numerically coerce it (e.g. the Postgres backend otherwise binds an
88    /// all-digit id like `"121"` as `bigint`, which breaks against a `TEXT` id
89    /// column). Set via [`Self::with_text_id`]. Defaults to false to preserve
90    /// the integer-id convention used by other models.
91    pub(super) id_text: bool,
92    /// When true, the backend makes this table's ids (auto-increment,
93    /// generated record ids). Set via [`Self::with_auto_id`]; Vista metadata
94    /// flags the id column `auto`.
95    pub(super) id_auto: bool,
96    /// Column values every row in this set must hold, because they are part of
97    /// the set's definition (e.g. a has-many child carries the parent's foreign
98    /// key). Registered wherever the table is narrowed by a literal
99    /// `column = value` (see [`Self::with_id`], `Reference::resolve_from_row`);
100    /// never from an expression scope (and every literal `column = value`
101    /// passed to `add_condition`). Enforced on write: insert and replace fill a
102    /// null/absent column and reject a conflicting one; patch never fills, and
103    /// rejects a present value that differs (or is null).
104    pub(super) invariants: IndexMap<String, T::Value>,
105    /// Lifecycle hooks (see [`Hook`](super::Hook)). Registered via [`Self::with_hook`].
106    pub(super) hooks: Hooks<T>,
107}
108
109impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
110    /// Create a new Table with the given table name and data source
111    pub fn new(table_name: impl Into<String>, data_source: T) -> Self {
112        Self {
113            data_source,
114            _phantom: PhantomData,
115            source: T::Source::from_name(table_name.into()),
116            columns: IndexMap::new(),
117            conditions: IndexMap::new(),
118            next_condition_id: 1,
119            order_by: IndexMap::new(),
120            next_order_id: 1,
121            refs: None,
122            contained: Vec::new(),
123            expressions: IndexMap::new(),
124            lazy_expressions: IndexMap::new(),
125            computed_columns: IndexMap::new(),
126            active_columns: None,
127            imported_columns: indexmap::IndexSet::new(),
128            pagination: None,
129            title_field: None,
130            title_fields: Vec::new(),
131            id_field: None,
132            id_text: false,
133            id_auto: false,
134            invariants: IndexMap::new(),
135            hooks: Hooks::default(),
136        }
137    }
138
139    /// Convert this table to use a different entity type.
140    ///
141    /// Computed expressions are carried over — they're stored entity-erased
142    /// (see [`ExpressionFn`]), so aggregates survive reference traversal that
143    /// erases the entity to `EmptyEntity` (e.g. `get_ref_from_row`).
144    pub fn into_entity<E2: Entity<T::Value>>(self) -> Table<T, E2> {
145        Table {
146            data_source: self.data_source,
147            _phantom: PhantomData,
148            source: self.source,
149            columns: self.columns,
150            conditions: self.conditions,
151            next_condition_id: self.next_condition_id,
152            order_by: self.order_by,
153            next_order_id: self.next_order_id,
154            refs: self.refs,
155            contained: self.contained,
156            expressions: self.expressions,
157            lazy_expressions: self.lazy_expressions,
158            computed_columns: self.computed_columns,
159            active_columns: self.active_columns,
160            imported_columns: self.imported_columns,
161            pagination: self.pagination,
162            title_field: self.title_field,
163            title_fields: self.title_fields,
164            id_field: self.id_field,
165            id_text: self.id_text,
166            id_auto: self.id_auto,
167            invariants: self.invariants,
168            hooks: self.hooks,
169        }
170    }
171
172    /// Borrow this table as its entity-erased form `Table<T, EmptyEntity>`.
173    ///
174    /// `E` appears in `Table` only as `PhantomData<E>` (a zero-sized field), so
175    /// `Table<T, E>` and `Table<T, EmptyEntity>` are layout-identical and this
176    /// reinterpret is sound. Used to feed `self` to the entity-erased
177    /// [`ExpressionFn`] closures at evaluation time.
178    pub(crate) fn as_entity_erased(&self) -> &Table<T, EmptyEntity> {
179        // SAFETY: identical layout (E is PhantomData only); lifetime is tied to
180        // `&self`, and the borrow is shared/read-only.
181        unsafe { &*(self as *const Table<T, E> as *const Table<T, EmptyEntity>) }
182    }
183}
184
185impl<T: TableSource, E: Entity<T::Value>> std::ops::Index<&str> for Table<T, E> {
186    type Output = T::Column<T::AnyType>;
187
188    fn index(&self, index: &str) -> &Self::Output {
189        &self.columns[index]
190    }
191}
192
193impl<T: TableSource, E: Entity<T::Value>> std::fmt::Debug for Table<T, E> {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        f.debug_struct("Table")
196            .field("table_name", &self.table_name())
197            .field("columns", &self.columns.keys().collect::<Vec<_>>())
198            .field("conditions_count", &self.conditions.len())
199            .field(
200                "refs_count",
201                &self.refs.as_ref().map(|r| r.len()).unwrap_or(0),
202            )
203            .field("expressions_count", &self.expressions.len())
204            .finish()
205    }
206}
207
208#[cfg(test)]
209mod tests;