Skip to main content

vantage_table/table/impls/
selectable.rs

1use vantage_core::{Result, error};
2use vantage_expressions::traits::selectable::Selectable;
3use vantage_expressions::{Expression, Expressive, SelectableDataSource, expr_any};
4use vantage_types::{EmptyEntity, 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            // With an active-column set, project only its members. The id
61            // column is always projected — consumers rely on it to key rows.
62            if !self.is_active(column.name()) {
63                continue;
64            }
65            if let Some(expr_fn) = self.expressions.get(column.name()) {
66                let expr = expr_fn(self.as_entity_erased());
67                self.data_source.add_select_column(
68                    &mut select,
69                    expr_any!("({})", (expr)),
70                    Some(column.name()),
71                );
72            } else if let Some(alias) = column.alias() {
73                let expr = self.data_source.expr(column.name(), vec![]);
74                self.data_source
75                    .add_select_column(&mut select, expr, Some(alias));
76            } else {
77                select.add_field(column.name());
78            }
79        }
80
81        // Add expressions that don't correspond to any column
82        for (name, expr_fn) in &self.expressions {
83            if !self.columns.contains_key(name) && self.is_active(name) {
84                let expr = expr_fn(self.as_entity_erased());
85                self.data_source.add_select_column(
86                    &mut select,
87                    expr_any!("({})", (expr)),
88                    Some(name),
89                );
90            }
91        }
92
93        select
94    }
95    /// Get count of records in the table
96    pub async fn get_count(&self) -> Result<i64> {
97        self.data_source.get_table_count(self).await
98    }
99
100    /// Get sum of a column in the table
101    pub async fn get_sum(&self, column: &T::Column<T::AnyType>) -> Result<T::Value> {
102        self.data_source.get_table_sum(self, column).await
103    }
104
105    /// Get max of a column in the table
106    pub async fn get_max(&self, column: &T::Column<T::AnyType>) -> Result<T::Value> {
107        self.data_source.get_table_max(self, column).await
108    }
109
110    /// Get min of a column in the table
111    pub async fn get_min(&self, column: &T::Column<T::AnyType>) -> Result<T::Value> {
112        self.data_source.get_table_min(self, column).await
113    }
114
115    /// Create a count query expression (does not execute).
116    /// The result is wrapped in parentheses so it's safe to nest as a subquery.
117    pub fn get_count_query(&self) -> Expression<T::Value> {
118        expr_any!("({})", (self.select_empty().as_count()))
119    }
120
121    /// Create a sum query expression for a column (does not execute).
122    /// The result is wrapped in parentheses so it's safe to nest as a subquery.
123    pub fn get_sum_query<Type>(&self, column: &T::Column<Type>) -> Expression<T::Value>
124    where
125        Type: ColumnType,
126        T::Column<Type>: Expressive<T::Value>,
127    {
128        expr_any!("({})", (self.select_empty().as_sum(column.expr())))
129    }
130
131    /// Create a subquery expression that selects a single column from this table.
132    ///
133    /// Builds `SELECT field FROM table WHERE conditions` — useful as a correlated
134    /// subquery inside `with_expression`. Returns `None` if `field` is not a
135    /// column on this table (mirroring [`get_column_expr`](Self::get_column_expr));
136    /// when the caller hardcodes a known column name, `.expect(...)` is fine:
137    ///
138    /// ```rust,ignore
139    /// .with_expression("category", |t| {
140    ///     t.get_subquery_as::<Category>("category").unwrap()
141    ///         .select_column("name")
142    ///         .expect("Category has a 'name' column")
143    /// })
144    /// ```
145    pub fn select_column(&self, field: &str) -> Option<Expression<T::Value>>
146    where
147        T::Column<T::AnyType>: Expressive<T::Value>,
148        T::Select: Expressive<T::Value>,
149    {
150        Some(self.select_expression(self.get_column_expr(field)?))
151    }
152
153    /// Wrap an arbitrary expression as a single-column subquery over this
154    /// table's source and conditions: `(SELECT <expr> FROM table WHERE …)`.
155    ///
156    /// Extracted from [`select_column`](Self::select_column) so a traversal
157    /// expression can nest one subquery inside another (multi-hop implicit
158    /// references). Fields and ordering are cleared — only `expr` is projected.
159    pub fn select_expression(&self, expr: Expression<T::Value>) -> Expression<T::Value>
160    where
161        T::Select: Expressive<T::Value>,
162    {
163        let mut select = self.select_empty();
164        select.clear_fields();
165        select.clear_order_by();
166        select.add_expression(expr);
167        select.expr()
168    }
169
170    /// Whether `name` is projected by [`select`](Self::select). With no active
171    /// set every column is active; otherwise only the set's members are, plus
172    /// the id column (always projected — consumers key rows by it).
173    fn is_active(&self, name: &str) -> bool {
174        match &self.active_columns {
175            None => true,
176            Some(set) => set.contains(name) || self.id_field.as_deref() == Some(name),
177        }
178    }
179
180    /// Restrict this table to an explicit set of columns, and import **implicit
181    /// references** — dotted names that traverse declared `has_one` relations
182    /// and surface the target's field as a read-only, typed column aliased
183    /// under the literal dotted name.
184    ///
185    /// ```rust,ignore
186    /// let orders = Order::sqlite_table(db)
187    ///     .with_active_columns(&["id", "client.name", "client.bakery.name"])?;
188    /// // SELECT id,
189    /// //   (SELECT name FROM client WHERE client.id = client_order.client_id) AS "client.name",
190    /// //   (SELECT (SELECT name FROM bakery WHERE bakery.id = client.bakery_id)
191    /// //      FROM client WHERE client.id = client_order.client_id)           AS "client.bakery.name"
192    /// // FROM client_order
193    /// ```
194    ///
195    /// A non-dotted entry restricts projection to an existing column or
196    /// expression column (exactly today's declared set). A dotted entry `a.b…c`
197    /// resolves `a`, `b`… as `has_one` relations and `c` as a column on the
198    /// final target. Everything is validated here, so every failure is a
199    /// **build-time** error, never a fetch-time surprise: unknown column,
200    /// unknown relation, a `has_many` hop, or a backend that cannot lower
201    /// traversal into its query (e.g. MongoDB, CSV, REST). Same-datasource
202    /// only; cross-datasource traversal is a Diorama augmentation concern.
203    pub fn with_active_columns(mut self, cols: &[&str]) -> Result<Self>
204    where
205        T: 'static,
206        E: 'static,
207        T::Column<T::AnyType>: Expressive<T::Value>,
208        T::Select: Expressive<T::Value>,
209    {
210        for &col in cols {
211            let parts: Vec<&str> = col.split('.').collect();
212            if parts.iter().any(|p| p.is_empty()) {
213                return Err(error!("invalid active column name", column = col));
214            }
215
216            if parts.len() >= 2 {
217                let column = parts[parts.len() - 1];
218                let hops = &parts[..parts.len() - 1];
219
220                if !self.data_source().supports_traversal() {
221                    return Err(error!(
222                        "backend does not support implicit-reference traversal in columns",
223                        column = col
224                    ));
225                }
226
227                // Validate the has_one chain and that the final column exists.
228                let (target, fk_hops) = self.resolve_has_one_target(hops)?;
229                if !target.columns().contains_key(column) {
230                    return Err(error!(
231                        "implicit reference target has no such column",
232                        column = column
233                    ));
234                }
235
236                // Lower to an expression: native path first, else the generic
237                // nested correlated-subquery chain. The native path receives
238                // the foreign-key/link *fields*, not the relation names — a
239                // SurrealDB idiom path traverses record-link fields, and a
240                // relation is free to be named differently from its FK
241                // (`with_one("owner", "client", …)` must lower to
242                // `client.name`, not the nonexistent `owner.name`).
243                let fk_refs: Vec<&str> = fk_hops.iter().map(String::as_str).collect();
244                let expr = match self.data_source().traversal_path_expr(&fk_refs, column) {
245                    Some(e) => e,
246                    None => self.traverse_rest_generic(hops, column)?,
247                };
248
249                let dotted = col.to_string();
250                if !self.columns.contains_key(&dotted) {
251                    let column_def = self.data_source.create_column::<T::AnyType>(&dotted);
252                    self.add_column(column_def);
253                }
254                self = self.with_expression(&dotted, move |_| expr.clone());
255                self.imported_columns.insert(dotted.clone());
256                self.active_columns
257                    .get_or_insert_with(Default::default)
258                    .insert(dotted);
259            } else {
260                // Expression columns registered via `with_expression` alone
261                // (no column def) are projectable too — activating them must
262                // work, or an active set would silently drop them for good.
263                if !self.columns.contains_key(col) && !self.expressions.contains_key(col) {
264                    return Err(error!("unknown active column", column = col));
265                }
266                self.active_columns
267                    .get_or_insert_with(Default::default)
268                    .insert(col.to_string());
269            }
270        }
271        Ok(self)
272    }
273
274    /// Recursively lower a dotted implicit reference into nested correlated
275    /// subqueries. One hop wraps the recursion's inner expression in a
276    /// `get_subquery_as` target via [`select_expression`](Self::select_expression);
277    /// the base case projects the final column. Used only when the backend has
278    /// no native [`traversal_path_expr`](crate::prelude::TableSource::traversal_path_expr).
279    fn traverse_rest_generic(&self, hops: &[&str], column: &str) -> Result<Expression<T::Value>>
280    where
281        T: 'static,
282        E: 'static,
283        T::Column<T::AnyType>: Expressive<T::Value>,
284        T::Select: Expressive<T::Value>,
285    {
286        match hops.split_first() {
287            None => self.get_column_expr(column).ok_or_else(|| {
288                error!(
289                    "implicit reference target has no such column",
290                    column = column
291                )
292            }),
293            Some((head, tail)) => {
294                let target: Table<T, EmptyEntity> = self.get_subquery_erased(head)?;
295                let inner = target.traverse_rest_generic(tail, column)?;
296                // The base case returns a bare column; a deeper hop returns a
297                // SELECT that must be parenthesized before it can nest as a
298                // scalar inside this hop's SELECT.
299                let inner = if tail.is_empty() {
300                    inner
301                } else {
302                    expr_any!("({})", (inner))
303                };
304                Ok(target.select_expression(inner))
305            }
306        }
307    }
308
309    /// Walk a chain of `has_one` hops and return the final target table along
310    /// with each hop's foreign-key/link field (in hop order), erroring at
311    /// build time on an unknown relation or a `has_many` hop. The FK fields
312    /// feed the backend-native path lowering, which traverses fields — the
313    /// relation *names* only address the refs registry.
314    fn resolve_has_one_target(&self, hops: &[&str]) -> Result<(Table<T, EmptyEntity>, Vec<String>)>
315    where
316        T: 'static,
317        E: 'static,
318    {
319        let (head, tail) = hops
320            .split_first()
321            .ok_or_else(|| error!("empty implicit reference path"))?;
322        if self.ref_cardinality(head)? != vantage_vista::ReferenceKind::HasOne {
323            return Err(error!(
324                "implicit reference hop must traverse a has_one relation",
325                relation = *head
326            ));
327        }
328        let fk = self.ref_foreign_key(head)?;
329        let target: Table<T, EmptyEntity> = self.get_ref_target_erased(head)?;
330        if tail.is_empty() {
331            Ok((target, vec![fk]))
332        } else {
333            let (final_target, mut fks) = target.resolve_has_one_target(tail)?;
334            fks.insert(0, fk);
335            Ok((final_target, fks))
336        }
337    }
338}
339
340// Constructors for tables sourced from an arbitrary query (a derived / sub-SELECT
341// source). Only available to backends whose `Source` is `SelectSource<Select>`
342// (the four subquery-capable SQL/SurrealDB backends). `V`/`C`/`S` are named
343// explicitly rather than projected through `T` to avoid a bound-resolution cycle.
344impl<T, E, V, C, S> Table<T, E>
345where
346    T: SelectableDataSource<V, C, Select = S>
347        + TableSource<Value = V, Condition = C, Source = SelectSource<S>>,
348    V: Clone + Send + Sync + 'static + From<String>,
349    C: Clone + Send + Sync + 'static,
350    S: Expressive<V> + Clone,
351    E: Entity<V>,
352{
353    /// Build a read-only table whose FROM clause is `select`, exposed under
354    /// `alias`. Columns/relations start empty — declare or inherit them.
355    pub fn from_select(data_source: T, alias: impl Into<String>, select: S) -> Self {
356        let alias = alias.into();
357        let mut table = Table::new(alias.clone(), data_source);
358        table.source = SelectSource::query(select, alias);
359        table
360    }
361
362    /// Derive a table from an existing one: transform its select via `modifier`
363    /// and use the result as the (sub-SELECT) source, inheriting the listed
364    /// `columns` and `relations` plus identity/title metadata.
365    ///
366    /// `modifier` receives `source.select()` and decides flat-vs-wrapped — it
367    /// may extend the query in place (joins referencing the base tables) or wrap
368    /// it as a subquery (to filter/sort on a computed alias). Conditions already
369    /// baked into the base select are not re-applied.
370    ///
371    /// Implicit references are **not** inherited: the derived table starts with
372    /// no active set, and listing an imported dotted column in `columns` copies
373    /// only its bare definition (no traversal expression, no read-only
374    /// tracking). Re-declare traversals on the derived table if needed.
375    pub fn derive_from<E2: Entity<V> + 'static>(
376        source: &Table<T, E2>,
377        alias: impl Into<String>,
378        modifier: impl FnOnce(S) -> S,
379        columns: &[&str],
380        relations: &[&str],
381    ) -> Self
382    where
383        T: 'static,
384        E: 'static,
385    {
386        let alias = alias.into();
387        let select = modifier(source.select());
388        let mut table = Table::new(alias.clone(), source.data_source().clone());
389        table.source = SelectSource::query(select, alias);
390        table.copy_columns_from(source, Some(columns));
391        table.copy_relations_from(source, Some(relations));
392        table.id_field = source.id_field.clone();
393        table.title_field = source.title_field.clone();
394        table.title_fields = source.title_fields.clone();
395        table
396    }
397}
398
399// Specific implementation for serde_json::Value that can use QuerySource
400impl<T, E> Table<T, E>
401where
402    T: SelectableDataSource<serde_json::Value, T::Condition>
403        + TableSource<Value = serde_json::Value>
404        + vantage_expressions::traits::datasource::ExprDataSource<serde_json::Value>,
405    T::Source: SelectSeed<T::Select, serde_json::Value, T::Condition>,
406    T::Value: From<String>,
407    E: Entity<serde_json::Value>,
408{
409    /// Get count using QuerySource for serde_json::Value
410    pub async fn get_count_via_query(&self) -> Result<i64> {
411        let count_query = self.get_count_query();
412        let result = self.data_source.execute(&count_query).await?;
413
414        // Unwrap a single-element array, e.g. `[{"count": 42}]` or `[42]`,
415        // which is how SQL/Surreal count queries commonly come back.
416        let result = match result.as_array().map(Vec::as_slice) {
417            Some([single]) => single,
418            _ => &result,
419        };
420
421        // Extract count from result - could be {"count": 42} or just 42.
422        // Anything else is an unexpected shape: surface it rather than
423        // silently reporting zero rows.
424        if let Some(count) = result.get("count").and_then(|v| v.as_i64()) {
425            Ok(count)
426        } else if let Some(count) = result.as_i64() {
427            Ok(count)
428        } else {
429            Err(vantage_core::util::error::vantage_error!(
430                "count query returned an unexpected result shape: {result}"
431            ))
432        }
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::mocks::mock_table_source::MockTableSource;
440    use serde_json::json;
441    use vantage_expressions::mocks::datasource::MockSelectableDataSource;
442    use vantage_expressions::traits::datasource::ExprDataSource;
443
444    #[tokio::test]
445    async fn test_selectable_functionality() {
446        let mock_select_source = MockSelectableDataSource::new(json!([
447            {"id": "1", "name": "Alice", "age": 30},
448            {"id": "2", "name": "Bob", "age": 25}
449        ]));
450
451        let mock_query_source = vantage_expressions::mocks::mock_builder::new()
452            .on_exact_select("(SELECT COUNT(*) FROM \"users\")", json!(42));
453
454        let table = MockTableSource::new()
455            .with_data(
456                "users",
457                vec![
458                    json!({"id": "1", "name": "Alice", "age": 30}),
459                    json!({"id": "2", "name": "Bob", "age": 25}),
460                ],
461            )
462            .await
463            .with_select_source(mock_select_source)
464            .with_query_source(mock_query_source);
465        let table = Table::<_, vantage_types::EmptyEntity>::new("users", table);
466
467        // Basic select
468        let select = table.select();
469        assert_eq!(select.source(), Some("users"));
470
471        // Validate SQL query generation
472        let query_expr: vantage_expressions::Expression<serde_json::Value> = select.into();
473        assert_eq!(query_expr.preview(), "SELECT * FROM users");
474
475        // Test count query generation
476        let count_query = table.get_count_query();
477        assert_eq!(count_query.preview(), "(SELECT COUNT(*) FROM \"users\")");
478
479        // TODO: This does not work with MockColumn - because it does not implement Expressive
480        // // Test sum query generation
481        // let age_column = table.data_source().create_column::<i64>("age");
482        // let sum_query = table.get_sum_query(&age_column);
483        // assert_eq!(sum_query.preview(), "SELECT SUM(age) FROM \"users\"");
484
485        // Test actual count/sum methods - get_count should return 42 from mock query source
486        let count = table.get_count_via_query().await.unwrap();
487        assert_eq!(count, 42);
488    }
489
490    async fn count_table_returning(
491        count_result: serde_json::Value,
492    ) -> Table<MockTableSource, vantage_types::EmptyEntity> {
493        let mock_select_source = MockSelectableDataSource::new(json!([]));
494        let mock_query_source = vantage_expressions::mocks::mock_builder::new()
495            .on_exact_select("(SELECT COUNT(*) FROM \"users\")", count_result);
496        let source = MockTableSource::new()
497            .with_select_source(mock_select_source)
498            .with_query_source(mock_query_source);
499        Table::<_, vantage_types::EmptyEntity>::new("users", source)
500    }
501
502    #[tokio::test]
503    async fn test_count_unwraps_single_element_array() {
504        // SQL/Surreal count queries commonly return `[{"count": N}]`.
505        let table = count_table_returning(json!([{"count": 7}])).await;
506        assert_eq!(table.get_count_via_query().await.unwrap(), 7);
507    }
508
509    #[tokio::test]
510    async fn test_count_errors_on_unexpected_shape() {
511        // An unrecognized result must surface as an error, not a silent zero.
512        let table = count_table_returning(json!({"total": 5})).await;
513        assert!(table.get_count_via_query().await.is_err());
514    }
515
516    #[tokio::test]
517    #[should_panic(expected = "MockTableSource select source not set")]
518    async fn test_panics_without_select_source() {
519        let table = Table::<_, vantage_types::EmptyEntity>::new("users", MockTableSource::new());
520        let _select = table.select();
521    }
522
523    #[tokio::test]
524    #[should_panic(expected = "MockTableSource query source not set")]
525    async fn test_panics_without_query_source() {
526        let table = Table::<_, vantage_types::EmptyEntity>::new("users", MockTableSource::new());
527        let query = table.data_source().expr("SELECT COUNT(*)", vec![]);
528        let _result = table.data_source().execute(&query).await;
529    }
530}