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        // Convert typed column to Column<AnyType> for storage
22        let any_column = self.data_source.to_any_column(column);
23        self.columns.insert(name, any_column);
24    }
25
26    /// Add a column using builder pattern
27    pub fn with_column<NewColumnType>(mut self, column: T::Column<NewColumnType>) -> Self
28    where
29        NewColumnType: ColumnType,
30    {
31        self.add_column(column);
32        self
33    }
34
35    /// Copy column definitions from another table, skipping any whose name is
36    /// already present. With `names = None`, copies all columns; otherwise only
37    /// the listed ones. Used to inherit columns when deriving a table from
38    /// another (see `Table::derive_from`).
39    pub fn copy_columns_from<E2: Entity<T::Value>>(
40        &mut self,
41        other: &Table<T, E2>,
42        names: Option<&[&str]>,
43    ) {
44        for col in other.columns().values() {
45            let name = col.name();
46            if names.is_some_and(|ns| !ns.contains(&name)) {
47                continue;
48            }
49            if !self.columns.contains_key(name) {
50                self.add_column(col.clone());
51            }
52        }
53    }
54
55    /// Add a typed column to the table (mutable)
56    pub fn add_column_of<NewColumnType>(&mut self, name: impl Into<String>)
57    where
58        NewColumnType: ColumnType,
59    {
60        let column = self
61            .data_source
62            .create_column::<NewColumnType>(&name.into());
63        self.add_column(column);
64    }
65
66    /// Add an ID column — sets both the column and the id_field flag.
67    pub fn with_id_column(mut self, name: impl Into<String>) -> Self
68    where
69        T::Id: ColumnType,
70    {
71        let name = name.into();
72        self.id_field = Some(name.clone());
73        let column = self.data_source.create_column::<T::Id>(&name);
74        self.add_column(column);
75        self
76    }
77
78    /// Mark the id column as a text/string key so backends do not numerically
79    /// coerce it. Use for models whose id column is `TEXT` even when some ids
80    /// look numeric (e.g. ids from an external API mixed with generated UUIDs);
81    /// without this the Postgres backend binds an all-digit id like `"121"` as
82    /// `bigint`, which fails against a `TEXT` id column.
83    pub fn with_text_id(mut self) -> Self {
84        self.id_text = true;
85        self
86    }
87
88    /// Whether the id column is a text key (see [`Self::with_text_id`]).
89    pub fn id_is_text(&self) -> bool {
90        self.id_text
91    }
92
93    /// Mark the id column as backend-made (SQL auto-increment, SurrealDB
94    /// generated record ids, MongoDB ObjectIds). Vista metadata then flags it
95    /// [`ColumnFlag::Auto`](vantage_vista::ColumnFlag::Auto).
96    pub fn with_auto_id(mut self) -> Self {
97        self.id_auto = true;
98        self
99    }
100
101    /// Whether the backend makes this table's ids (see [`Self::with_auto_id`]).
102    pub fn has_auto_id(&self) -> bool {
103        self.id_auto
104    }
105
106    /// Add a typed column AND mark it as a display title.
107    ///
108    /// Title columns show alongside the id in generic list views and
109    /// lead the body of single-record displays. Multiple title columns
110    /// are allowed; their order matches the order of these calls.
111    pub fn with_title_column_of<NewColumnType>(mut self, name: impl Into<String>) -> Self
112    where
113        NewColumnType: ColumnType,
114    {
115        let name = name.into();
116        if !self.title_fields.contains(&name) {
117            self.title_fields.push(name.clone());
118        }
119        if self.title_field.is_none() {
120            self.title_field = Some(name.clone());
121        }
122        let column = self.data_source.create_column::<NewColumnType>(&name);
123        self.add_column(column);
124        self
125    }
126
127    /// Add a pre-built column AND mark it as a display title — the
128    /// flag-carrying twin of [`Self::with_title_column_of`], for callers
129    /// that need flags (searchable, mandatory) on a title column.
130    pub fn with_title_column<NewColumnType>(mut self, column: T::Column<NewColumnType>) -> Self
131    where
132        NewColumnType: ColumnType,
133    {
134        let name = column.name().to_string();
135        if !self.title_fields.contains(&name) {
136            self.title_fields.push(name.clone());
137        }
138        if self.title_field.is_none() {
139            self.title_field = Some(name);
140        }
141        self.add_column(column);
142        self
143    }
144
145    /// Add a typed column to the table (builder pattern)
146    pub fn with_column_of<NewColumnType>(self, name: impl Into<String>) -> Self
147    where
148        NewColumnType: ColumnType,
149    {
150        let column = self
151            .data_source
152            .create_column::<NewColumnType>(&name.into());
153        self.with_column(column)
154    }
155
156    /// Get all columns as type-erased columns (`Column<AnyType>`)
157    pub fn columns(&self) -> &IndexMap<String, T::Column<T::AnyType>> {
158        &self.columns
159    }
160
161    /// Get a typed column by converting from stored `Column<AnyType>`
162    pub fn get_column<Type>(&self, name: &str) -> Option<T::Column<Type>>
163    where
164        Type: ColumnType,
165    {
166        let any_column = self.columns.get(name)?;
167        self.data_source
168            .convert_any_column::<Type>(any_column.clone())
169    }
170
171    /// Get an expression for a column or computed expression by name.
172    ///
173    /// If `name` matches a registered expression (from `with_expression`), evaluates
174    /// and returns it. Otherwise returns the column as an expression. Returns `None`
175    /// if the name doesn't match either.
176    pub fn get_column_expr(&self, name: &str) -> Option<vantage_expressions::Expression<T::Value>>
177    where
178        T::Column<T::AnyType>: vantage_expressions::Expressive<T::Value>,
179    {
180        if let Some(expr_fn) = self.expressions.get(name) {
181            Some(expr_fn(self.as_entity_erased()))
182        } else {
183            use vantage_expressions::Expressive;
184            self.columns.get(name).map(|c| c.expr())
185        }
186    }
187}
188
189impl<T, E> Table<T, E>
190where
191    T: TableSource + ExprDataSource<T::Value>,
192    E: Entity<T::Value> + 'static,
193{
194    /// Expression yielding all values of the named column under the
195    /// table's current conditions.
196    ///
197    /// SQL backends materialise this as a `SELECT col FROM tbl WHERE …`
198    /// subquery (embeddable directly into IN clauses); non-query
199    /// backends wrap a `DeferredFn` that runs `list_table_values` and
200    /// projects the column at execute time.
201    ///
202    /// Panics if `column_name` isn't a column on this table — the
203    /// callsite is meant to be a literal column reference, so a typo
204    /// is a programmer error, not a runtime failure mode worth
205    /// surfacing as `Result`.
206    pub fn column_values_expr(&self, column_name: &str) -> Expression<T::Value> {
207        let col = self
208            .get_column::<T::AnyType>(column_name)
209            .unwrap_or_else(|| panic!("column {column_name:?} not found on table"));
210        self.data_source.column_table_values_expr(self, &col).expr()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::mocks::mock_column::MockColumn;
218    use crate::prelude::MockTableSource;
219    use serde_json::Value;
220    use vantage_types::EmptyEntity;
221
222    #[test]
223    fn test_add_column() {
224        let ds = MockTableSource::new();
225        let mut table = Table::<MockTableSource, EmptyEntity>::new("test", ds);
226
227        table.add_column(MockColumn::<String>::new("name"));
228
229        assert!(table.columns().contains_key("name"));
230        assert_eq!(table.columns().len(), 1);
231    }
232
233    #[test]
234    fn test_with_column() {
235        let ds = MockTableSource::new();
236        let table = Table::<MockTableSource, EmptyEntity>::new("test", ds)
237            .with_column(MockColumn::<Value>::new("name"))
238            .with_column(MockColumn::<i32>::new("email"));
239
240        assert!(table.columns().contains_key("name"));
241        assert!(table.columns().contains_key("email"));
242        assert_eq!(table.columns().len(), 2);
243    }
244
245    #[test]
246    #[should_panic(expected = "Duplicate column")]
247    fn test_duplicate_column_panics() {
248        let ds = MockTableSource::new();
249        let mut table = Table::<MockTableSource, EmptyEntity>::new("test", ds);
250
251        table.add_column(MockColumn::<String>::new("name"));
252        table.add_column(MockColumn::<String>::new("name")); // Should panic
253    }
254
255    #[test]
256    fn test_with_column_of() {
257        let ds = MockTableSource::new();
258        let table = Table::<MockTableSource, EmptyEntity>::new("test", ds)
259            .with_column_of::<String>("name")
260            .with_column_of::<i64>("age")
261            .with_column_of::<bool>("active");
262
263        assert!(table.columns().contains_key("name"));
264        assert!(table.columns().contains_key("age"));
265        assert!(table.columns().contains_key("active"));
266        assert_eq!(table.columns().len(), 3);
267    }
268
269    #[test]
270    fn test_add_column_of() {
271        let ds = MockTableSource::new();
272        let mut table = Table::<MockTableSource, EmptyEntity>::new("test", ds);
273
274        table.add_column_of::<String>("email");
275        table.add_column_of::<i64>("balance");
276
277        assert!(table.columns().contains_key("email"));
278        assert!(table.columns().contains_key("balance"));
279        assert_eq!(table.columns().len(), 2);
280    }
281
282    #[test]
283    fn test_columns_access() {
284        let ds = MockTableSource::new();
285        let table = Table::<MockTableSource, EmptyEntity>::new("test", ds)
286            .with_column_of::<String>("name")
287            .with_column_of::<i64>("age");
288
289        let columns = table.columns();
290        assert!(columns.contains_key("name"));
291        assert!(columns.contains_key("age"));
292        assert_eq!(columns.len(), 2);
293
294        let name_column = table.columns().get("name");
295        assert!(name_column.is_some());
296        assert_eq!(name_column.unwrap().name(), "name");
297
298        let missing_column = table.columns().get("missing");
299        assert!(missing_column.is_none());
300    }
301}