Skip to main content

vantage_table/table/impls/
selectable.rs

1use vantage_core::Result;
2use vantage_expressions::traits::selectable::Selectable;
3use vantage_expressions::{Expression, Expressive, SelectableDataSource, expr_any};
4use vantage_types::Entity;
5
6use crate::{
7    column::core::ColumnType,
8    source::{SelectSeed, SelectSource},
9    table::Table,
10    traits::column_like::ColumnLike,
11    traits::table_source::TableSource,
12};
13
14impl<T, E> Table<T, E>
15where
16    T: SelectableDataSource<T::Value, T::Condition> + TableSource,
17    T::Source: SelectSeed<T::Select, T::Value, T::Condition>,
18    T::Value: From<String>, // that's because table is specified as a string
19    E: Entity<T::Value>,
20{
21    /// Create a bare select with source, conditions, ordering, and pagination —
22    /// but no fields. Used by `select_column` and aggregates to avoid evaluating
23    /// all expressions.
24    pub fn select_empty(&self) -> T::Select {
25        let mut select = self.data_source.select();
26        self.source.seed(&mut select);
27
28        for condition in self.conditions.values() {
29            select.add_where_condition(condition.clone());
30        }
31
32        for (expr, direction) in self.order_by.values() {
33            let order = match direction {
34                crate::sorting::SortDirection::Ascending => vantage_expressions::Order::Asc,
35                crate::sorting::SortDirection::Descending => vantage_expressions::Order::Desc,
36            };
37            select.add_order_by(expr.clone(), order);
38        }
39
40        if let Some(pagination) = &self.pagination {
41            select.set_limit(Some(pagination.limit()), Some(pagination.skip()));
42        }
43
44        select
45    }
46
47    /// Create a select query with table configuration applied
48    pub fn select(&self) -> T::Select {
49        let mut select = self.select_empty();
50
51        // Add all columns as fields (or expressions if defined)
52        for column in self.columns.values() {
53            // Lazy-expression columns exist only on returned records — the
54            // source has no such field to project (SQLite would silently
55            // degrade the unknown quoted identifier to a string literal;
56            // other SQL backends would error).
57            if self.lazy_expressions.contains_key(column.name()) {
58                continue;
59            }
60            if let Some(expr_fn) = self.expressions.get(column.name()) {
61                let expr = expr_fn(self.as_entity_erased());
62                self.data_source.add_select_column(
63                    &mut select,
64                    expr_any!("({})", (expr)),
65                    Some(column.name()),
66                );
67            } else if let Some(alias) = column.alias() {
68                let expr = self.data_source.expr(column.name(), vec![]);
69                self.data_source
70                    .add_select_column(&mut select, expr, Some(alias));
71            } else {
72                select.add_field(column.name());
73            }
74        }
75
76        // Add expressions that don't correspond to any column
77        for (name, expr_fn) in &self.expressions {
78            if !self.columns.contains_key(name) {
79                let expr = expr_fn(self.as_entity_erased());
80                self.data_source.add_select_column(
81                    &mut select,
82                    expr_any!("({})", (expr)),
83                    Some(name),
84                );
85            }
86        }
87
88        select
89    }
90    /// Get count of records in the table
91    pub async fn get_count(&self) -> Result<i64> {
92        self.data_source.get_table_count(self).await
93    }
94
95    /// Get sum of a column in the table
96    pub async fn get_sum(&self, column: &T::Column<T::AnyType>) -> Result<T::Value> {
97        self.data_source.get_table_sum(self, column).await
98    }
99
100    /// Get max of a column in the table
101    pub async fn get_max(&self, column: &T::Column<T::AnyType>) -> Result<T::Value> {
102        self.data_source.get_table_max(self, column).await
103    }
104
105    /// Get min of a column in the table
106    pub async fn get_min(&self, column: &T::Column<T::AnyType>) -> Result<T::Value> {
107        self.data_source.get_table_min(self, column).await
108    }
109
110    /// Create a count query expression (does not execute).
111    /// The result is wrapped in parentheses so it's safe to nest as a subquery.
112    pub fn get_count_query(&self) -> Expression<T::Value> {
113        expr_any!("({})", (self.select_empty().as_count()))
114    }
115
116    /// Create a sum query expression for a column (does not execute).
117    /// The result is wrapped in parentheses so it's safe to nest as a subquery.
118    pub fn get_sum_query<Type>(&self, column: &T::Column<Type>) -> Expression<T::Value>
119    where
120        Type: ColumnType,
121        T::Column<Type>: Expressive<T::Value>,
122    {
123        expr_any!("({})", (self.select_empty().as_sum(column.expr())))
124    }
125
126    /// Create a subquery expression that selects a single column from this table.
127    ///
128    /// Builds `SELECT field FROM table WHERE conditions` — useful as a correlated
129    /// subquery inside `with_expression`. Returns `None` if `field` is not a
130    /// column on this table (mirroring [`get_column_expr`](Self::get_column_expr));
131    /// when the caller hardcodes a known column name, `.expect(...)` is fine:
132    ///
133    /// ```rust,ignore
134    /// .with_expression("category", |t| {
135    ///     t.get_subquery_as::<Category>("category").unwrap()
136    ///         .select_column("name")
137    ///         .expect("Category has a 'name' column")
138    /// })
139    /// ```
140    pub fn select_column(&self, field: &str) -> Option<Expression<T::Value>>
141    where
142        T::Column<T::AnyType>: Expressive<T::Value>,
143        T::Select: Expressive<T::Value>,
144    {
145        let expr = self.get_column_expr(field)?;
146        let mut select = self.select_empty();
147        select.clear_fields();
148        select.clear_order_by();
149        select.add_expression(expr);
150        Some(select.expr())
151    }
152}
153
154// Constructors for tables sourced from an arbitrary query (a derived / sub-SELECT
155// source). Only available to backends whose `Source` is `SelectSource<Select>`
156// (the four subquery-capable SQL/SurrealDB backends). `V`/`C`/`S` are named
157// explicitly rather than projected through `T` to avoid a bound-resolution cycle.
158impl<T, E, V, C, S> Table<T, E>
159where
160    T: SelectableDataSource<V, C, Select = S>
161        + TableSource<Value = V, Condition = C, Source = SelectSource<S>>,
162    V: Clone + Send + Sync + 'static + From<String>,
163    C: Clone + Send + Sync + 'static,
164    S: Expressive<V> + Clone,
165    E: Entity<V>,
166{
167    /// Build a read-only table whose FROM clause is `select`, exposed under
168    /// `alias`. Columns/relations start empty — declare or inherit them.
169    pub fn from_select(data_source: T, alias: impl Into<String>, select: S) -> Self {
170        let alias = alias.into();
171        let mut table = Table::new(alias.clone(), data_source);
172        table.source = SelectSource::query(select, alias);
173        table
174    }
175
176    /// Derive a table from an existing one: transform its select via `modifier`
177    /// and use the result as the (sub-SELECT) source, inheriting the listed
178    /// `columns` and `relations` plus identity/title metadata.
179    ///
180    /// `modifier` receives `source.select()` and decides flat-vs-wrapped — it
181    /// may extend the query in place (joins referencing the base tables) or wrap
182    /// it as a subquery (to filter/sort on a computed alias). Conditions already
183    /// baked into the base select are not re-applied.
184    pub fn derive_from<E2: Entity<V> + 'static>(
185        source: &Table<T, E2>,
186        alias: impl Into<String>,
187        modifier: impl FnOnce(S) -> S,
188        columns: &[&str],
189        relations: &[&str],
190    ) -> Self
191    where
192        T: 'static,
193        E: 'static,
194    {
195        let alias = alias.into();
196        let select = modifier(source.select());
197        let mut table = Table::new(alias.clone(), source.data_source().clone());
198        table.source = SelectSource::query(select, alias);
199        table.copy_columns_from(source, Some(columns));
200        table.copy_relations_from(source, Some(relations));
201        table.id_field = source.id_field.clone();
202        table.title_field = source.title_field.clone();
203        table.title_fields = source.title_fields.clone();
204        table
205    }
206}
207
208// Specific implementation for serde_json::Value that can use QuerySource
209impl<T, E> Table<T, E>
210where
211    T: SelectableDataSource<serde_json::Value, T::Condition>
212        + TableSource<Value = serde_json::Value>
213        + vantage_expressions::traits::datasource::ExprDataSource<serde_json::Value>,
214    T::Source: SelectSeed<T::Select, serde_json::Value, T::Condition>,
215    T::Value: From<String>,
216    E: Entity<serde_json::Value>,
217{
218    /// Get count using QuerySource for serde_json::Value
219    pub async fn get_count_via_query(&self) -> Result<i64> {
220        let count_query = self.get_count_query();
221        let result = self.data_source.execute(&count_query).await?;
222
223        // Unwrap a single-element array, e.g. `[{"count": 42}]` or `[42]`,
224        // which is how SQL/Surreal count queries commonly come back.
225        let result = match result.as_array().map(Vec::as_slice) {
226            Some([single]) => single,
227            _ => &result,
228        };
229
230        // Extract count from result - could be {"count": 42} or just 42.
231        // Anything else is an unexpected shape: surface it rather than
232        // silently reporting zero rows.
233        if let Some(count) = result.get("count").and_then(|v| v.as_i64()) {
234            Ok(count)
235        } else if let Some(count) = result.as_i64() {
236            Ok(count)
237        } else {
238            Err(vantage_core::util::error::vantage_error!(
239                "count query returned an unexpected result shape: {result}"
240            ))
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::mocks::mock_table_source::MockTableSource;
249    use serde_json::json;
250    use vantage_expressions::mocks::datasource::MockSelectableDataSource;
251    use vantage_expressions::traits::datasource::ExprDataSource;
252
253    #[tokio::test]
254    async fn test_selectable_functionality() {
255        let mock_select_source = MockSelectableDataSource::new(json!([
256            {"id": "1", "name": "Alice", "age": 30},
257            {"id": "2", "name": "Bob", "age": 25}
258        ]));
259
260        let mock_query_source = vantage_expressions::mocks::mock_builder::new()
261            .on_exact_select("(SELECT COUNT(*) FROM \"users\")", json!(42));
262
263        let table = MockTableSource::new()
264            .with_data(
265                "users",
266                vec![
267                    json!({"id": "1", "name": "Alice", "age": 30}),
268                    json!({"id": "2", "name": "Bob", "age": 25}),
269                ],
270            )
271            .await
272            .with_select_source(mock_select_source)
273            .with_query_source(mock_query_source);
274        let table = Table::<_, vantage_types::EmptyEntity>::new("users", table);
275
276        // Basic select
277        let select = table.select();
278        assert_eq!(select.source(), Some("users"));
279
280        // Validate SQL query generation
281        let query_expr: vantage_expressions::Expression<serde_json::Value> = select.into();
282        assert_eq!(query_expr.preview(), "SELECT * FROM users");
283
284        // Test count query generation
285        let count_query = table.get_count_query();
286        assert_eq!(count_query.preview(), "(SELECT COUNT(*) FROM \"users\")");
287
288        // TODO: This does not work with MockColumn - because it does not implement Expressive
289        // // Test sum query generation
290        // let age_column = table.data_source().create_column::<i64>("age");
291        // let sum_query = table.get_sum_query(&age_column);
292        // assert_eq!(sum_query.preview(), "SELECT SUM(age) FROM \"users\"");
293
294        // Test actual count/sum methods - get_count should return 42 from mock query source
295        let count = table.get_count_via_query().await.unwrap();
296        assert_eq!(count, 42);
297    }
298
299    async fn count_table_returning(
300        count_result: serde_json::Value,
301    ) -> Table<MockTableSource, vantage_types::EmptyEntity> {
302        let mock_select_source = MockSelectableDataSource::new(json!([]));
303        let mock_query_source = vantage_expressions::mocks::mock_builder::new()
304            .on_exact_select("(SELECT COUNT(*) FROM \"users\")", count_result);
305        let source = MockTableSource::new()
306            .with_select_source(mock_select_source)
307            .with_query_source(mock_query_source);
308        Table::<_, vantage_types::EmptyEntity>::new("users", source)
309    }
310
311    #[tokio::test]
312    async fn test_count_unwraps_single_element_array() {
313        // SQL/Surreal count queries commonly return `[{"count": N}]`.
314        let table = count_table_returning(json!([{"count": 7}])).await;
315        assert_eq!(table.get_count_via_query().await.unwrap(), 7);
316    }
317
318    #[tokio::test]
319    async fn test_count_errors_on_unexpected_shape() {
320        // An unrecognized result must surface as an error, not a silent zero.
321        let table = count_table_returning(json!({"total": 5})).await;
322        assert!(table.get_count_via_query().await.is_err());
323    }
324
325    #[tokio::test]
326    #[should_panic(expected = "MockTableSource select source not set")]
327    async fn test_panics_without_select_source() {
328        let table = Table::<_, vantage_types::EmptyEntity>::new("users", MockTableSource::new());
329        let _select = table.select();
330    }
331
332    #[tokio::test]
333    #[should_panic(expected = "MockTableSource query source not set")]
334    async fn test_panics_without_query_source() {
335        let table = Table::<_, vantage_types::EmptyEntity>::new("users", MockTableSource::new());
336        let query = table.data_source().expr("SELECT COUNT(*)", vec![]);
337        let _result = table.data_source().execute(&query).await;
338    }
339}