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#[derive(Clone)]
25pub struct Table<T, E>
26where
27    T: TableSource,
28    E: Entity<T::Value>,
29{
30    pub(super) data_source: T,
31    pub(super) _phantom: PhantomData<E>,
32    pub(super) source: T::Source,
33    pub(super) columns: IndexMap<String, T::Column<T::AnyType>>,
34    pub(super) conditions: IndexMap<i64, T::Condition>,
35    pub(super) next_condition_id: i64,
36    pub(super) order_by: IndexMap<i64, (T::Condition, SortDirection)>,
37    pub(super) next_order_id: i64,
38    pub(super) refs: Option<IndexMap<String, Arc<dyn Reference>>>,
39    pub(super) contained: Vec<crate::references::ContainedRelation<T>>,
40    pub(super) expressions: IndexMap<String, ExpressionFn<T>>,
41    pub(super) pagination: Option<Pagination>,
42    pub(super) title_field: Option<String>,
43    pub(super) title_fields: Vec<String>,
44    pub(super) id_field: Option<String>,
45    /// Column values every row in this set must hold, because they are part of
46    /// the set's definition (e.g. a has-many child carries the parent's foreign
47    /// key). Registered wherever the table is narrowed by a literal
48    /// `column = value` (see [`Self::with_id`], `Reference::resolve_from_row`);
49    /// never from an expression scope. Enforced on write: a column the caller
50    /// left null/absent is filled, a matching value is kept, and a conflicting
51    /// value is rejected.
52    pub(super) invariants: IndexMap<String, T::Value>,
53    /// Lifecycle hooks (see [`Hook`]). Registered via [`Self::with_hook`].
54    pub(super) hooks: Hooks<T>,
55}
56
57impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
58    /// Create a new Table with the given table name and data source
59    pub fn new(table_name: impl Into<String>, data_source: T) -> Self {
60        Self {
61            data_source,
62            _phantom: PhantomData,
63            source: T::Source::from_name(table_name.into()),
64            columns: IndexMap::new(),
65            conditions: IndexMap::new(),
66            next_condition_id: 1,
67            order_by: IndexMap::new(),
68            next_order_id: 1,
69            refs: None,
70            contained: Vec::new(),
71            expressions: IndexMap::new(),
72            pagination: None,
73            title_field: None,
74            title_fields: Vec::new(),
75            id_field: None,
76            invariants: IndexMap::new(),
77            hooks: Hooks::default(),
78        }
79    }
80
81    /// Convert this table to use a different entity type.
82    ///
83    /// Computed expressions are carried over — they're stored entity-erased
84    /// (see [`ExpressionFn`]), so aggregates survive reference traversal that
85    /// erases the entity to `EmptyEntity` (e.g. `get_ref_from_row`).
86    pub fn into_entity<E2: Entity<T::Value>>(self) -> Table<T, E2> {
87        Table {
88            data_source: self.data_source,
89            _phantom: PhantomData,
90            source: self.source,
91            columns: self.columns,
92            conditions: self.conditions,
93            next_condition_id: self.next_condition_id,
94            order_by: self.order_by,
95            next_order_id: self.next_order_id,
96            refs: self.refs,
97            contained: self.contained,
98            expressions: self.expressions,
99            pagination: self.pagination,
100            title_field: self.title_field,
101            title_fields: self.title_fields,
102            id_field: self.id_field,
103            invariants: self.invariants,
104            hooks: self.hooks,
105        }
106    }
107
108    /// Borrow this table as its entity-erased form `Table<T, EmptyEntity>`.
109    ///
110    /// `E` appears in `Table` only as `PhantomData<E>` (a zero-sized field), so
111    /// `Table<T, E>` and `Table<T, EmptyEntity>` are layout-identical and this
112    /// reinterpret is sound. Used to feed `self` to the entity-erased
113    /// [`ExpressionFn`] closures at evaluation time.
114    pub(crate) fn as_entity_erased(&self) -> &Table<T, EmptyEntity> {
115        // SAFETY: identical layout (E is PhantomData only); lifetime is tied to
116        // `&self`, and the borrow is shared/read-only.
117        unsafe { &*(self as *const Table<T, E> as *const Table<T, EmptyEntity>) }
118    }
119
120    /// Snapshot the table's relations as Vista references (name, target type,
121    /// cardinality, foreign key). Driver factories fold this into
122    /// `VistaMetadata` so the erased `Vista` carries enough to drive nested
123    /// insert and relation traversal.
124    pub fn vista_references(&self) -> Vec<vantage_vista::Reference> {
125        self.refs
126            .as_ref()
127            .map(|refs| {
128                refs.iter()
129                    .map(|(name, r)| {
130                        vantage_vista::Reference::new(
131                            name.clone(),
132                            r.target_type_name().to_string(),
133                            r.cardinality(),
134                            r.foreign_key().to_string(),
135                        )
136                    })
137                    .collect()
138            })
139            .unwrap_or_default()
140    }
141
142    /// Shape-only specs (name, host, kind, id) for the contained relations
143    /// declared on this table, for driver factories to fold into
144    /// `VistaMetadata`. Columns are derived at traversal from each relation's
145    /// `build_target` closure.
146    pub fn vista_contained(&self) -> Vec<vantage_vista::ContainedSpec> {
147        self.contained.iter().map(|c| c.spec()).collect()
148    }
149
150    /// Look up a contained relation by name (for the driver's traversal).
151    pub fn contained_relation(
152        &self,
153        name: &str,
154    ) -> Option<&crate::references::ContainedRelation<T>> {
155        self.contained.iter().find(|c| c.name() == name)
156    }
157
158    /// Use a callback with a builder pattern for configuration
159    pub fn with<F>(mut self, func: F) -> Self
160    where
161        F: FnOnce(&mut Self),
162    {
163        func(&mut self);
164        self
165    }
166
167    /// Get the table name.
168    ///
169    /// For a query-sourced table this is its FROM alias.
170    pub fn table_name(&self) -> &str {
171        self.source.name()
172    }
173
174    /// The table's source (a name, or a query used as a derived source).
175    pub fn source(&self) -> &T::Source {
176        &self.source
177    }
178
179    /// Override the table name. Used by REST API drivers to swap a
180    /// canonical resource path for a per-reference URI template at
181    /// traversal time.
182    ///
183    /// This replaces the source with a name-based one, so it must not be
184    /// called on a query-sourced (derived) table.
185    pub fn set_table_name(&mut self, name: impl Into<String>) {
186        self.source = T::Source::from_name(name.into());
187    }
188
189    /// Get the underlying data source
190    pub fn data_source(&self) -> &T {
191        &self.data_source
192    }
193
194    /// Get the title field column if set
195    pub fn title_field(&self) -> Option<&T::Column<T::AnyType>> {
196        self.title_field
197            .as_ref()
198            .and_then(|name| self.columns.get(name))
199    }
200
201    /// Names of columns marked as display titles (set via
202    /// [`Self::with_title_column_of`]). These show alongside the id in
203    /// list views and on the leading lines of single-record displays.
204    pub fn title_fields(&self) -> &[String] {
205        &self.title_fields
206    }
207
208    /// Get the id field column if set
209    pub fn id_field(&self) -> Option<&T::Column<T::AnyType>> {
210        self.id_field
211            .as_ref()
212            .and_then(|name| self.columns.get(name))
213    }
214
215    /// Mark an already-added column as the id field.
216    ///
217    /// Use this when the id column has been added via [`Self::add_column`]
218    /// (so its type and aliases were chosen explicitly) and you only need
219    /// to flag it. [`Self::with_id_column`] is the typed shortcut that
220    /// creates the column for you.
221    pub fn set_id_field(&mut self, name: impl Into<String>) {
222        self.id_field = Some(name.into());
223    }
224
225    /// Mark an already-added column as a display title.
226    ///
227    /// Companion to [`Self::set_id_field`] for spec-driven construction.
228    pub fn add_title_field(&mut self, name: impl Into<String>) {
229        let name = name.into();
230        if !self.title_fields.contains(&name) {
231            self.title_fields.push(name.clone());
232        }
233        if self.title_field.is_none() {
234            self.title_field = Some(name);
235        }
236    }
237
238    /// Get the current pagination configuration, if set
239    pub fn pagination(&self) -> Option<&Pagination> {
240        self.pagination.as_ref()
241    }
242
243    /// Column values every row in this set must hold (see the `invariants`
244    /// field): enforced on write — filled when null/absent, kept when matching,
245    /// rejected when conflicting.
246    pub fn invariants(&self) -> &IndexMap<String, T::Value> {
247        &self.invariants
248    }
249
250    /// Register an invariant value for `column` on this set.
251    ///
252    /// A later call for the same column overwrites the earlier invariant.
253    pub fn add_invariant(&mut self, column: impl Into<String>, value: T::Value) {
254        self.invariants.insert(column.into(), value);
255    }
256
257    /// Builder form of [`Self::add_invariant`].
258    pub fn with_invariant(mut self, column: impl Into<String>, value: T::Value) -> Self {
259        self.add_invariant(column, value);
260        self
261    }
262}
263
264impl<T: TableSource, E: Entity<T::Value>> std::ops::Index<&str> for Table<T, E> {
265    type Output = T::Column<T::AnyType>;
266
267    fn index(&self, index: &str) -> &Self::Output {
268        &self.columns[index]
269    }
270}
271
272impl<T: TableSource, E: Entity<T::Value>> std::fmt::Debug for Table<T, E> {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        f.debug_struct("Table")
275            .field("table_name", &self.table_name())
276            .field("columns", &self.columns.keys().collect::<Vec<_>>())
277            .field("conditions_count", &self.conditions.len())
278            .field(
279                "refs_count",
280                &self.refs.as_ref().map(|r| r.len()).unwrap_or(0),
281            )
282            .field("expressions_count", &self.expressions.len())
283            .finish()
284    }
285}