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    /// Get all columns as type-erased columns (`Column<AnyType>`)
156    pub fn columns(&self) -> &IndexMap<String, T::Column<T::AnyType>> {
157        &self.columns
158    }
159
160    /// Get a typed column by converting from stored `Column<AnyType>`
161    pub fn get_column<Type>(&self, name: &str) -> Option<T::Column<Type>>
162    where
163        Type: ColumnType,
164    {
165        let any_column = self.columns.get(name)?;
166        self.data_source
167            .convert_any_column::<Type>(any_column.clone())
168    }
169
170    /// Get an expression for a column or computed expression by name.
171    ///
172    /// If `name` matches a registered expression (from `with_expression`), evaluates
173    /// and returns it. Otherwise returns the column as an expression. Returns `None`
174    /// if the name doesn't match either.
175    pub fn get_column_expr(&self, name: &str) -> Option<vantage_expressions::Expression<T::Value>>
176    where
177        T::Column<T::AnyType>: vantage_expressions::Expressive<T::Value>,
178    {
179        if let Some(expr_fn) = self.expressions.get(name) {
180            Some(expr_fn(self.as_entity_erased()))
181        } else {
182            use vantage_expressions::Expressive;
183            self.columns.get(name).map(|c| c.expr())
184        }
185    }
186}
187
188impl<T, E> Table<T, E>
189where
190    T: TableSource + ExprDataSource<T::Value>,
191    E: Entity<T::Value> + 'static,
192{
193    /// Expression yielding all values of the named column under the
194    /// table's current conditions.
195    ///
196    /// SQL backends materialise this as a `SELECT col FROM tbl WHERE …`
197    /// subquery (embeddable directly into IN clauses); non-query
198    /// backends wrap a `DeferredFn` that runs `list_table_values` and
199    /// projects the column at execute time.
200    ///
201    /// Panics if `column_name` isn't a column on this table — the
202    /// callsite is meant to be a literal column reference, so a typo
203    /// is a programmer error, not a runtime failure mode worth
204    /// surfacing as `Result`.
205    pub fn column_values_expr(&self, column_name: &str) -> Expression<T::Value> {
206        let col = self
207            .get_column::<T::AnyType>(column_name)
208            .unwrap_or_else(|| panic!("column {column_name:?} not found on table"));
209        self.data_source.column_table_values_expr(self, &col).expr()
210    }
211}