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    /// Add a typed column to the table (mutable)
36    pub fn add_column_of<NewColumnType>(&mut self, name: impl Into<String>)
37    where
38        NewColumnType: ColumnType,
39    {
40        let column = self
41            .data_source
42            .create_column::<NewColumnType>(&name.into());
43        self.add_column(column);
44    }
45
46    /// Add an ID column — sets both the column and the id_field flag.
47    pub fn with_id_column(mut self, name: impl Into<String>) -> Self
48    where
49        T::Id: ColumnType,
50    {
51        let name = name.into();
52        self.id_field = Some(name.clone());
53        let column = self.data_source.create_column::<T::Id>(&name);
54        self.add_column(column);
55        self
56    }
57
58    /// Add a typed column AND mark it as a display title.
59    ///
60    /// Title columns show alongside the id in generic list views and
61    /// lead the body of single-record displays. Multiple title columns
62    /// are allowed; their order matches the order of these calls.
63    pub fn with_title_column_of<NewColumnType>(mut self, name: impl Into<String>) -> Self
64    where
65        NewColumnType: ColumnType,
66    {
67        let name = name.into();
68        if !self.title_fields.contains(&name) {
69            self.title_fields.push(name.clone());
70        }
71        if self.title_field.is_none() {
72            self.title_field = Some(name.clone());
73        }
74        let column = self.data_source.create_column::<NewColumnType>(&name);
75        self.add_column(column);
76        self
77    }
78
79    /// Add a typed column to the table (builder pattern)
80    pub fn with_column_of<NewColumnType>(self, name: impl Into<String>) -> Self
81    where
82        NewColumnType: ColumnType,
83    {
84        let column = self
85            .data_source
86            .create_column::<NewColumnType>(&name.into());
87        self.with_column(column)
88    }
89
90    /// Get all columns as type-erased columns (`Column<AnyType>`)
91    pub fn columns(&self) -> &IndexMap<String, T::Column<T::AnyType>> {
92        &self.columns
93    }
94
95    /// Get a typed column by converting from stored `Column<AnyType>`
96    pub fn get_column<Type>(&self, name: &str) -> Option<T::Column<Type>>
97    where
98        Type: ColumnType,
99    {
100        let any_column = self.columns.get(name)?;
101        self.data_source
102            .convert_any_column::<Type>(any_column.clone())
103    }
104
105    /// Get an expression for a column or computed expression by name.
106    ///
107    /// If `name` matches a registered expression (from `with_expression`), evaluates
108    /// and returns it. Otherwise returns the column as an expression. Returns `None`
109    /// if the name doesn't match either.
110    pub fn get_column_expr(&self, name: &str) -> Option<vantage_expressions::Expression<T::Value>>
111    where
112        T::Column<T::AnyType>: vantage_expressions::Expressive<T::Value>,
113    {
114        if let Some(expr_fn) = self.expressions.get(name) {
115            Some(expr_fn(self))
116        } else {
117            use vantage_expressions::Expressive;
118            self.columns.get(name).map(|c| c.expr())
119        }
120    }
121}
122
123impl<T, E> Table<T, E>
124where
125    T: TableSource + ExprDataSource<T::Value>,
126    E: Entity<T::Value> + 'static,
127{
128    /// Expression yielding all values of the named column under the
129    /// table's current conditions.
130    ///
131    /// SQL backends materialise this as a `SELECT col FROM tbl WHERE …`
132    /// subquery (embeddable directly into IN clauses); non-query
133    /// backends wrap a `DeferredFn` that runs `list_table_values` and
134    /// projects the column at execute time.
135    ///
136    /// Panics if `column_name` isn't a column on this table — the
137    /// callsite is meant to be a literal column reference, so a typo
138    /// is a programmer error, not a runtime failure mode worth
139    /// surfacing as `Result`.
140    pub fn column_values_expr(&self, column_name: &str) -> Expression<T::Value> {
141        let col = self
142            .get_column::<T::AnyType>(column_name)
143            .unwrap_or_else(|| panic!("column {column_name:?} not found on table"));
144        self.data_source.column_table_values_expr(self, &col).expr()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::mocks::mock_column::MockColumn;
152    use crate::prelude::MockTableSource;
153    use serde_json::Value;
154    use vantage_types::EmptyEntity;
155
156    #[test]
157    fn test_add_column() {
158        let ds = MockTableSource::new();
159        let mut table = Table::<MockTableSource, EmptyEntity>::new("test", ds);
160
161        table.add_column(MockColumn::<String>::new("name"));
162
163        assert!(table.columns().contains_key("name"));
164        assert_eq!(table.columns().len(), 1);
165    }
166
167    #[test]
168    fn test_with_column() {
169        let ds = MockTableSource::new();
170        let table = Table::<MockTableSource, EmptyEntity>::new("test", ds)
171            .with_column(MockColumn::<Value>::new("name"))
172            .with_column(MockColumn::<i32>::new("email"));
173
174        assert!(table.columns().contains_key("name"));
175        assert!(table.columns().contains_key("email"));
176        assert_eq!(table.columns().len(), 2);
177    }
178
179    #[test]
180    #[should_panic(expected = "Duplicate column")]
181    fn test_duplicate_column_panics() {
182        let ds = MockTableSource::new();
183        let mut table = Table::<MockTableSource, EmptyEntity>::new("test", ds);
184
185        table.add_column(MockColumn::<String>::new("name"));
186        table.add_column(MockColumn::<String>::new("name")); // Should panic
187    }
188
189    #[test]
190    fn test_with_column_of() {
191        let ds = MockTableSource::new();
192        let table = Table::<MockTableSource, EmptyEntity>::new("test", ds)
193            .with_column_of::<String>("name")
194            .with_column_of::<i64>("age")
195            .with_column_of::<bool>("active");
196
197        assert!(table.columns().contains_key("name"));
198        assert!(table.columns().contains_key("age"));
199        assert!(table.columns().contains_key("active"));
200        assert_eq!(table.columns().len(), 3);
201    }
202
203    #[test]
204    fn test_add_column_of() {
205        let ds = MockTableSource::new();
206        let mut table = Table::<MockTableSource, EmptyEntity>::new("test", ds);
207
208        table.add_column_of::<String>("email");
209        table.add_column_of::<i64>("balance");
210
211        assert!(table.columns().contains_key("email"));
212        assert!(table.columns().contains_key("balance"));
213        assert_eq!(table.columns().len(), 2);
214    }
215
216    #[test]
217    fn test_columns_access() {
218        let ds = MockTableSource::new();
219        let table = Table::<MockTableSource, EmptyEntity>::new("test", ds)
220            .with_column_of::<String>("name")
221            .with_column_of::<i64>("age");
222
223        let columns = table.columns();
224        assert!(columns.contains_key("name"));
225        assert!(columns.contains_key("age"));
226        assert_eq!(columns.len(), 2);
227
228        let name_column = table.columns().get("name");
229        assert!(name_column.is_some());
230        assert_eq!(name_column.unwrap().name(), "name");
231
232        let missing_column = table.columns().get("missing");
233        assert!(missing_column.is_none());
234    }
235}