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