Skip to main content

vantage_table/table/impls/
columns.rs

1use indexmap::IndexMap;
2use vantage_expressions::{Expression, Expressive, traits::datasource::ExprDataSource};
3use vantage_types::Entity;
4
5use crate::{
6    column::core::ColumnType, prelude::ColumnLike, table::Table, traits::table_source::TableSource,
7};
8
9impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
10    /// Add a column to the table (accepts any typed column, converts to `Column<AnyType>`)
11    pub fn add_column<NewColumnType>(&mut self, column: T::Column<NewColumnType>)
12    where
13        NewColumnType: ColumnType,
14    {
15        let name = column.name().to_string();
16
17        if self.columns.contains_key(&name) {
18            panic!("Duplicate column: {}", name);
19        }
20
21        let any_column = self.data_source.to_any_column(column);
22        self.columns.insert(name, any_column);
23    }
24
25    /// Add a column using builder pattern
26    pub fn with_column<NewColumnType>(mut self, column: T::Column<NewColumnType>) -> Self
27    where
28        NewColumnType: ColumnType,
29    {
30        self.add_column(column);
31        self
32    }
33
34    /// Copy column definitions from another table, skipping any whose name is
35    /// already present. With `names = None`, copies all columns; otherwise only
36    /// the listed ones. Used to inherit columns when deriving a table from
37    /// another (see `Table::derive_from`).
38    pub fn copy_columns_from<E2: Entity<T::Value>>(
39        &mut self,
40        other: &Table<T, E2>,
41        names: Option<&[&str]>,
42    ) {
43        for col in other.columns().values() {
44            let name = col.name();
45            if names.is_some_and(|ns| !ns.contains(&name)) {
46                continue;
47            }
48            if !self.columns.contains_key(name) {
49                self.add_column(col.clone());
50            }
51        }
52    }
53
54    /// Add a typed column to the table (mutable)
55    pub fn add_column_of<NewColumnType>(&mut self, name: impl Into<String>)
56    where
57        NewColumnType: ColumnType,
58    {
59        let column = self
60            .data_source
61            .create_column::<NewColumnType>(&name.into());
62        self.add_column(column);
63    }
64
65    /// Add an ID column — sets both the column and the id_field flag.
66    pub fn with_id_column(mut self, name: impl Into<String>) -> Self
67    where
68        T::Id: ColumnType,
69    {
70        let name = name.into();
71        self.id_field = Some(name.clone());
72        let column = self.data_source.create_column::<T::Id>(&name);
73        self.add_column(column);
74        self
75    }
76
77    /// Mark the id column as a text/string key so backends do not numerically
78    /// coerce it. Use for models whose id column is `TEXT` even when some ids
79    /// look numeric (e.g. ids from an external API mixed with generated UUIDs);
80    /// without this the Postgres backend binds an all-digit id like `"121"` as
81    /// `bigint`, which fails against a `TEXT` id column.
82    pub fn with_text_id(mut self) -> Self {
83        self.id_text = true;
84        self
85    }
86
87    /// Whether the id column is a text key (see [`Self::with_text_id`]).
88    pub fn id_is_text(&self) -> bool {
89        self.id_text
90    }
91
92    /// Mark the id column as backend-made (SQL auto-increment, SurrealDB
93    /// generated record ids, MongoDB ObjectIds). Vista metadata then flags it
94    /// [`ColumnFlag::Auto`](vantage_vista::ColumnFlag::Auto).
95    pub fn with_auto_id(mut self) -> Self {
96        self.id_auto = true;
97        self
98    }
99
100    /// Whether the backend makes this table's ids (see [`Self::with_auto_id`]).
101    pub fn has_auto_id(&self) -> bool {
102        self.id_auto
103    }
104
105    /// Add a typed column AND mark it as a display title.
106    ///
107    /// Title columns show alongside the id in generic list views and
108    /// lead the body of single-record displays. Multiple title columns
109    /// are allowed; their order matches the order of these calls.
110    pub fn with_title_column_of<NewColumnType>(mut self, name: impl Into<String>) -> Self
111    where
112        NewColumnType: ColumnType,
113    {
114        let name = name.into();
115        if !self.title_fields.contains(&name) {
116            self.title_fields.push(name.clone());
117        }
118        if self.title_field.is_none() {
119            self.title_field = Some(name.clone());
120        }
121        let column = self.data_source.create_column::<NewColumnType>(&name);
122        self.add_column(column);
123        self
124    }
125
126    /// Add a pre-built column AND mark it as a display title — the
127    /// flag-carrying twin of [`Self::with_title_column_of`], for callers
128    /// that need flags (searchable, mandatory) on a title column.
129    pub fn with_title_column<NewColumnType>(mut self, column: T::Column<NewColumnType>) -> Self
130    where
131        NewColumnType: ColumnType,
132    {
133        let name = column.name().to_string();
134        if !self.title_fields.contains(&name) {
135            self.title_fields.push(name.clone());
136        }
137        if self.title_field.is_none() {
138            self.title_field = Some(name);
139        }
140        self.add_column(column);
141        self
142    }
143
144    /// Add a typed column to the table (builder pattern)
145    pub fn with_column_of<NewColumnType>(self, name: impl Into<String>) -> Self
146    where
147        NewColumnType: ColumnType,
148    {
149        let column = self
150            .data_source
151            .create_column::<NewColumnType>(&name.into());
152        self.with_column(column)
153    }
154
155    /// Take a column out of the table: stored, expression, lazy or
156    /// vista-computed, so it is neither selected nor shown. Invariants and
157    /// conditions naming it stay, so inserts are still stamped with its
158    /// value and rows are still narrowed by it; references hold their
159    /// foreign key by name and keep traversing.
160    pub fn remove_column(&mut self, name: &str) -> vantage_core::Result<()> {
161        if name == self.id_field_name() {
162            return Err(vantage_core::error!(format!(
163                "`{name}` is the id column of `{}`; it can't be removed",
164                self.table_name()
165            )));
166        }
167        let mut found = false;
168        if let Some((stored_index, _, _)) = self.columns.shift_remove_full(name) {
169            found = true;
170            let removed = self.position_of_stored(stored_index);
171            for (at, _) in self.computed_columns.values_mut() {
172                if *at > removed {
173                    *at -= 1;
174                }
175            }
176        }
177        if let Some((removed, _)) = self.computed_columns.shift_remove(name) {
178            found = true;
179            for (at, _) in self.computed_columns.values_mut() {
180                if *at > removed {
181                    *at -= 1;
182                }
183            }
184        }
185        found |= self.expressions.shift_remove(name).is_some();
186        found |= self.lazy_expressions.shift_remove(name).is_some();
187        if !found {
188            return Err(vantage_core::error!(format!(
189                "no column `{name}` in `{}`",
190                self.table_name()
191            )));
192        }
193        self.imported_columns.shift_remove(name);
194        if let Some(active) = &mut self.active_columns {
195            active.shift_remove(name);
196        }
197        self.title_fields.retain(|title| title != name);
198        if self.title_field.as_deref() == Some(name) {
199            self.title_field = self.title_fields.first().cloned();
200        }
201        Ok(())
202    }
203
204    /// Get all columns as type-erased columns (`Column<AnyType>`)
205    pub fn columns(&self) -> &IndexMap<String, T::Column<T::AnyType>> {
206        &self.columns
207    }
208
209    /// Get a typed column by converting from stored `Column<AnyType>`
210    pub fn get_column<Type>(&self, name: &str) -> Option<T::Column<Type>>
211    where
212        Type: ColumnType,
213    {
214        let any_column = self.columns.get(name)?;
215        self.data_source
216            .convert_any_column::<Type>(any_column.clone())
217    }
218
219    /// Get an expression for a column or computed expression by name.
220    ///
221    /// If `name` matches a registered expression (from `with_expression`), evaluates
222    /// and returns it. Otherwise returns the column as an expression. Returns `None`
223    /// if the name doesn't match either.
224    pub fn get_column_expr(&self, name: &str) -> Option<vantage_expressions::Expression<T::Value>>
225    where
226        T::Column<T::AnyType>: vantage_expressions::Expressive<T::Value>,
227    {
228        if let Some(expr_fn) = self.expressions.get(name) {
229            Some(expr_fn(self.as_entity_erased()))
230        } else {
231            use vantage_expressions::Expressive;
232            self.columns.get(name).map(|c| c.expr())
233        }
234    }
235}
236
237impl<T, E> Table<T, E>
238where
239    T: TableSource + ExprDataSource<T::Value>,
240    E: Entity<T::Value> + 'static,
241{
242    /// Expression yielding all values of the named column under the
243    /// table's current conditions.
244    ///
245    /// SQL backends materialise this as a `SELECT col FROM tbl WHERE …`
246    /// subquery (embeddable directly into IN clauses); non-query
247    /// backends wrap a `DeferredFn` that runs `list_table_values` and
248    /// projects the column at execute time.
249    ///
250    /// Panics if `column_name` isn't a column on this table — the
251    /// callsite is meant to be a literal column reference, so a typo
252    /// is a programmer error, not a runtime failure mode worth
253    /// surfacing as `Result`.
254    pub fn column_values_expr(&self, column_name: &str) -> Expression<T::Value> {
255        let col = self
256            .get_column::<T::AnyType>(column_name)
257            .unwrap_or_else(|| panic!("column {column_name:?} not found on table"));
258        self.data_source.column_table_values_expr(self, &col).expr()
259    }
260}