Skip to main content

vantage_table/table/impls/
selectable.rs

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