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/// An expression closure stored on a table, against the entity-erased table so
18/// it survives [`Table::into_entity`]. See [`Table::with_expression`].
19pub type ExpressionFn<T> =
20    Arc<dyn Fn(&Table<T, EmptyEntity>) -> Expression<<T as TableSource>::Value> + Send + Sync>;
21
22/// A lazy-expression callback stored on a table: runs on each returned record
23/// and sees the columns earlier callbacks produced. See
24/// [`Table::with_lazy_expression`].
25pub type LazyExpressionFn<T> = Arc<
26    dyn Fn(
27            &vantage_types::Record<<T as TableSource>::Value>,
28        ) -> std::pin::Pin<
29            Box<
30                dyn std::future::Future<Output = vantage_core::Result<<T as TableSource>::Value>>
31                    + Send,
32            >,
33        > + Send
34        + Sync,
35>;
36
37#[derive(Clone)]
38pub struct Table<T, E>
39where
40    T: TableSource,
41    E: Entity<T::Value>,
42{
43    pub(super) data_source: T,
44    pub(super) _phantom: PhantomData<E>,
45    pub(super) source: T::Source,
46    pub(super) columns: IndexMap<String, T::Column<T::AnyType>>,
47    pub(super) conditions: IndexMap<i64, T::Condition>,
48    pub(super) next_condition_id: i64,
49    pub(super) order_by: IndexMap<i64, (T::Condition, SortDirection)>,
50    pub(super) next_order_id: i64,
51    pub(super) refs: Option<IndexMap<String, Arc<dyn Reference>>>,
52    pub(super) contained: Vec<crate::references::ContainedRelation<T>>,
53    pub(super) expressions: IndexMap<String, ExpressionFn<T>>,
54    pub(super) lazy_expressions: IndexMap<String, LazyExpressionFn<T>>,
55    /// Vista-computed columns with their position among all columns, stored
56    /// and computed. The table never selects or writes them.
57    pub(super) computed_columns: IndexMap<String, (usize, vantage_vista::Column)>,
58    /// When `Some`, `select()` projects only these names (plus the id column);
59    /// dotted names are implicit references. See [`Self::with_active_columns`].
60    pub(super) active_columns: Option<indexmap::IndexSet<String>>,
61    /// Dotted implicit-reference columns (`"country.name"`): read-only
62    /// projections that write paths must never persist (a SCHEMALESS store
63    /// would create a literal `country.name` field).
64    pub(super) imported_columns: indexmap::IndexSet<String>,
65    pub(super) pagination: Option<Pagination>,
66    pub(super) title_field: Option<String>,
67    pub(super) title_fields: Vec<String>,
68    pub(super) id_field: Option<String>,
69    /// See [`Self::with_text_id`].
70    pub(super) id_text: bool,
71    /// See [`Self::with_auto_id`].
72    pub(super) id_auto: bool,
73    /// See [`Self::invariants`].
74    pub(super) invariants: IndexMap<String, T::Value>,
75    pub(super) hooks: Hooks<T>,
76}
77
78impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
79    /// Create a new Table with the given table name and data source
80    pub fn new(table_name: impl Into<String>, data_source: T) -> Self {
81        Self {
82            data_source,
83            _phantom: PhantomData,
84            source: T::Source::from_name(table_name.into()),
85            columns: IndexMap::new(),
86            conditions: IndexMap::new(),
87            next_condition_id: 1,
88            order_by: IndexMap::new(),
89            next_order_id: 1,
90            refs: None,
91            contained: Vec::new(),
92            expressions: IndexMap::new(),
93            lazy_expressions: IndexMap::new(),
94            computed_columns: IndexMap::new(),
95            active_columns: None,
96            imported_columns: indexmap::IndexSet::new(),
97            pagination: None,
98            title_field: None,
99            title_fields: Vec::new(),
100            id_field: None,
101            id_text: false,
102            id_auto: false,
103            invariants: IndexMap::new(),
104            hooks: Hooks::default(),
105        }
106    }
107
108    /// Convert this table to use a different entity type.
109    ///
110    /// Computed expressions are carried over — they're stored entity-erased
111    /// (see [`ExpressionFn`]), so aggregates survive reference traversal that
112    /// erases the entity to `EmptyEntity` (e.g. `get_ref_from_row`).
113    pub fn into_entity<E2: Entity<T::Value>>(self) -> Table<T, E2> {
114        Table {
115            data_source: self.data_source,
116            _phantom: PhantomData,
117            source: self.source,
118            columns: self.columns,
119            conditions: self.conditions,
120            next_condition_id: self.next_condition_id,
121            order_by: self.order_by,
122            next_order_id: self.next_order_id,
123            refs: self.refs,
124            contained: self.contained,
125            expressions: self.expressions,
126            lazy_expressions: self.lazy_expressions,
127            computed_columns: self.computed_columns,
128            active_columns: self.active_columns,
129            imported_columns: self.imported_columns,
130            pagination: self.pagination,
131            title_field: self.title_field,
132            title_fields: self.title_fields,
133            id_field: self.id_field,
134            id_text: self.id_text,
135            id_auto: self.id_auto,
136            invariants: self.invariants,
137            hooks: self.hooks,
138        }
139    }
140
141    /// Borrow this table as its entity-erased form `Table<T, EmptyEntity>`.
142    ///
143    /// `E` appears in `Table` only as `PhantomData<E>` (a zero-sized field), so
144    /// `Table<T, E>` and `Table<T, EmptyEntity>` are layout-identical and this
145    /// reinterpret is sound. Used to feed `self` to the entity-erased
146    /// [`ExpressionFn`] closures at evaluation time.
147    pub(crate) fn as_entity_erased(&self) -> &Table<T, EmptyEntity> {
148        // SAFETY: identical layout (E is PhantomData only); lifetime is tied to
149        // `&self`, and the borrow is shared/read-only.
150        unsafe { &*(self as *const Table<T, E> as *const Table<T, EmptyEntity>) }
151    }
152}
153
154impl<T: TableSource, E: Entity<T::Value>> std::ops::Index<&str> for Table<T, E> {
155    type Output = T::Column<T::AnyType>;
156
157    fn index(&self, index: &str) -> &Self::Output {
158        &self.columns[index]
159    }
160}
161
162impl<T: TableSource, E: Entity<T::Value>> std::fmt::Debug for Table<T, E> {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        f.debug_struct("Table")
165            .field("table_name", &self.table_name())
166            .field("columns", &self.columns.keys().collect::<Vec<_>>())
167            .field("conditions_count", &self.conditions.len())
168            .field(
169                "refs_count",
170                &self.refs.as_ref().map(|r| r.len()).unwrap_or(0),
171            )
172            .field("expressions_count", &self.expressions.len())
173            .finish()
174    }
175}