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