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 name FROM bakery WHERE bakery.id = client.bakery_id …)     AS "client.bakery.name"
191    /// // FROM client_order
192    /// ```
193    ///
194    /// A non-dotted entry restricts projection to that existing column
195    /// (exactly today's declared columns). A dotted entry `a.b…c` resolves `a`,
196    /// `b`… as `has_one` relations and `c` as a column on the final target.
197    /// Everything is validated here, so every failure is a **build-time** error,
198    /// never a fetch-time surprise: unknown column, unknown relation, a
199    /// `has_many` hop, or a backend that cannot lower traversal into its query
200    /// (MongoDB, CSV, REST, CMD). Same-datasource only; cross-datasource
201    /// traversal is a Diorama augmentation concern.
202    pub fn with_active_columns(mut self, cols: &[&str]) -> Result<Self>
203    where
204        T: 'static,
205        E: 'static,
206        T::Column<T::AnyType>: Expressive<T::Value>,
207        T::Select: Expressive<T::Value>,
208    {
209        for &col in cols {
210            let parts: Vec<&str> = col.split('.').collect();
211            if parts.iter().any(|p| p.is_empty()) {
212                return Err(error!("invalid active column name", column = col));
213            }
214
215            if parts.len() >= 2 {
216                let column = parts[parts.len() - 1];
217                let hops = &parts[..parts.len() - 1];
218
219                if !self.data_source().supports_traversal() {
220                    return Err(error!(
221                        "backend does not support implicit-reference traversal in columns",
222                        column = col
223                    ));
224                }
225
226                // Validate the has_one chain and that the final column exists.
227                let target = self.resolve_has_one_target(hops)?;
228                if !target.columns().contains_key(column) {
229                    return Err(error!(
230                        "implicit reference target has no such column",
231                        column = column
232                    ));
233                }
234
235                // Lower to an expression: native path (SurrealDB idiom) first,
236                // else the generic nested correlated-subquery chain.
237                let expr = match self.data_source().traversal_path_expr(hops, column) {
238                    Some(e) => e,
239                    None => self.traverse_rest_generic(hops, column)?,
240                };
241
242                let dotted = col.to_string();
243                if !self.columns.contains_key(&dotted) {
244                    let column_def = self.data_source.create_column::<T::AnyType>(&dotted);
245                    self.add_column(column_def);
246                }
247                self = self.with_expression(&dotted, move |_| expr.clone());
248                self.imported_columns.insert(dotted.clone());
249                self.active_columns
250                    .get_or_insert_with(Default::default)
251                    .insert(dotted);
252            } else {
253                if !self.columns.contains_key(col) {
254                    return Err(error!("unknown active column", column = col));
255                }
256                self.active_columns
257                    .get_or_insert_with(Default::default)
258                    .insert(col.to_string());
259            }
260        }
261        Ok(self)
262    }
263
264    /// Recursively lower a dotted implicit reference into nested correlated
265    /// subqueries. One hop wraps the recursion's inner expression in a
266    /// `get_subquery_as` target via [`select_expression`](Self::select_expression);
267    /// the base case projects the final column. Used only when the backend has
268    /// no native [`traversal_path_expr`](crate::prelude::TableSource::traversal_path_expr).
269    fn traverse_rest_generic(&self, hops: &[&str], column: &str) -> Result<Expression<T::Value>>
270    where
271        T: 'static,
272        E: 'static,
273        T::Column<T::AnyType>: Expressive<T::Value>,
274        T::Select: Expressive<T::Value>,
275    {
276        match hops.split_first() {
277            None => self.get_column_expr(column).ok_or_else(|| {
278                error!(
279                    "implicit reference target has no such column",
280                    column = column
281                )
282            }),
283            Some((head, tail)) => {
284                let target: Table<T, EmptyEntity> = self.get_subquery_erased(head)?;
285                let inner = target.traverse_rest_generic(tail, column)?;
286                // The base case returns a bare column; a deeper hop returns a
287                // SELECT that must be parenthesized before it can nest as a
288                // scalar inside this hop's SELECT.
289                let inner = if tail.is_empty() {
290                    inner
291                } else {
292                    expr_any!("({})", (inner))
293                };
294                Ok(target.select_expression(inner))
295            }
296        }
297    }
298
299    /// Walk a chain of `has_one` hops and return the final target table,
300    /// erroring at build time on an unknown relation or a `has_many` hop.
301    fn resolve_has_one_target(&self, hops: &[&str]) -> Result<Table<T, EmptyEntity>>
302    where
303        T: 'static,
304        E: 'static,
305    {
306        let (head, tail) = hops
307            .split_first()
308            .ok_or_else(|| error!("empty implicit reference path"))?;
309        if self.ref_cardinality(head)? != vantage_vista::ReferenceKind::HasOne {
310            return Err(error!(
311                "implicit reference hop must traverse a has_one relation",
312                relation = *head
313            ));
314        }
315        let target: Table<T, EmptyEntity> = self.get_ref_target_erased(head)?;
316        if tail.is_empty() {
317            Ok(target)
318        } else {
319            target.resolve_has_one_target(tail)
320        }
321    }
322}
323
324// Constructors for tables sourced from an arbitrary query (a derived / sub-SELECT
325// source). Only available to backends whose `Source` is `SelectSource<Select>`
326// (the four subquery-capable SQL/SurrealDB backends). `V`/`C`/`S` are named
327// explicitly rather than projected through `T` to avoid a bound-resolution cycle.
328impl<T, E, V, C, S> Table<T, E>
329where
330    T: SelectableDataSource<V, C, Select = S>
331        + TableSource<Value = V, Condition = C, Source = SelectSource<S>>,
332    V: Clone + Send + Sync + 'static + From<String>,
333    C: Clone + Send + Sync + 'static,
334    S: Expressive<V> + Clone,
335    E: Entity<V>,
336{
337    /// Build a read-only table whose FROM clause is `select`, exposed under
338    /// `alias`. Columns/relations start empty — declare or inherit them.
339    pub fn from_select(data_source: T, alias: impl Into<String>, select: S) -> Self {
340        let alias = alias.into();
341        let mut table = Table::new(alias.clone(), data_source);
342        table.source = SelectSource::query(select, alias);
343        table
344    }
345
346    /// Derive a table from an existing one: transform its select via `modifier`
347    /// and use the result as the (sub-SELECT) source, inheriting the listed
348    /// `columns` and `relations` plus identity/title metadata.
349    ///
350    /// `modifier` receives `source.select()` and decides flat-vs-wrapped — it
351    /// may extend the query in place (joins referencing the base tables) or wrap
352    /// it as a subquery (to filter/sort on a computed alias). Conditions already
353    /// baked into the base select are not re-applied.
354    pub fn derive_from<E2: Entity<V> + 'static>(
355        source: &Table<T, E2>,
356        alias: impl Into<String>,
357        modifier: impl FnOnce(S) -> S,
358        columns: &[&str],
359        relations: &[&str],
360    ) -> Self
361    where
362        T: 'static,
363        E: 'static,
364    {
365        let alias = alias.into();
366        let select = modifier(source.select());
367        let mut table = Table::new(alias.clone(), source.data_source().clone());
368        table.source = SelectSource::query(select, alias);
369        table.copy_columns_from(source, Some(columns));
370        table.copy_relations_from(source, Some(relations));
371        table.id_field = source.id_field.clone();
372        table.title_field = source.title_field.clone();
373        table.title_fields = source.title_fields.clone();
374        table
375    }
376}
377
378// Specific implementation for serde_json::Value that can use QuerySource
379impl<T, E> Table<T, E>
380where
381    T: SelectableDataSource<serde_json::Value, T::Condition>
382        + TableSource<Value = serde_json::Value>
383        + vantage_expressions::traits::datasource::ExprDataSource<serde_json::Value>,
384    T::Source: SelectSeed<T::Select, serde_json::Value, T::Condition>,
385    T::Value: From<String>,
386    E: Entity<serde_json::Value>,
387{
388    /// Get count using QuerySource for serde_json::Value
389    pub async fn get_count_via_query(&self) -> Result<i64> {
390        let count_query = self.get_count_query();
391        let result = self.data_source.execute(&count_query).await?;
392
393        // Unwrap a single-element array, e.g. `[{"count": 42}]` or `[42]`,
394        // which is how SQL/Surreal count queries commonly come back.
395        let result = match result.as_array().map(Vec::as_slice) {
396            Some([single]) => single,
397            _ => &result,
398        };
399
400        // Extract count from result - could be {"count": 42} or just 42.
401        // Anything else is an unexpected shape: surface it rather than
402        // silently reporting zero rows.
403        if let Some(count) = result.get("count").and_then(|v| v.as_i64()) {
404            Ok(count)
405        } else if let Some(count) = result.as_i64() {
406            Ok(count)
407        } else {
408            Err(vantage_core::util::error::vantage_error!(
409                "count query returned an unexpected result shape: {result}"
410            ))
411        }
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::mocks::mock_table_source::MockTableSource;
419    use serde_json::json;
420    use vantage_expressions::mocks::datasource::MockSelectableDataSource;
421    use vantage_expressions::traits::datasource::ExprDataSource;
422
423    #[tokio::test]
424    async fn test_selectable_functionality() {
425        let mock_select_source = MockSelectableDataSource::new(json!([
426            {"id": "1", "name": "Alice", "age": 30},
427            {"id": "2", "name": "Bob", "age": 25}
428        ]));
429
430        let mock_query_source = vantage_expressions::mocks::mock_builder::new()
431            .on_exact_select("(SELECT COUNT(*) FROM \"users\")", json!(42));
432
433        let table = MockTableSource::new()
434            .with_data(
435                "users",
436                vec![
437                    json!({"id": "1", "name": "Alice", "age": 30}),
438                    json!({"id": "2", "name": "Bob", "age": 25}),
439                ],
440            )
441            .await
442            .with_select_source(mock_select_source)
443            .with_query_source(mock_query_source);
444        let table = Table::<_, vantage_types::EmptyEntity>::new("users", table);
445
446        // Basic select
447        let select = table.select();
448        assert_eq!(select.source(), Some("users"));
449
450        // Validate SQL query generation
451        let query_expr: vantage_expressions::Expression<serde_json::Value> = select.into();
452        assert_eq!(query_expr.preview(), "SELECT * FROM users");
453
454        // Test count query generation
455        let count_query = table.get_count_query();
456        assert_eq!(count_query.preview(), "(SELECT COUNT(*) FROM \"users\")");
457
458        // TODO: This does not work with MockColumn - because it does not implement Expressive
459        // // Test sum query generation
460        // let age_column = table.data_source().create_column::<i64>("age");
461        // let sum_query = table.get_sum_query(&age_column);
462        // assert_eq!(sum_query.preview(), "SELECT SUM(age) FROM \"users\"");
463
464        // Test actual count/sum methods - get_count should return 42 from mock query source
465        let count = table.get_count_via_query().await.unwrap();
466        assert_eq!(count, 42);
467    }
468
469    async fn count_table_returning(
470        count_result: serde_json::Value,
471    ) -> Table<MockTableSource, vantage_types::EmptyEntity> {
472        let mock_select_source = MockSelectableDataSource::new(json!([]));
473        let mock_query_source = vantage_expressions::mocks::mock_builder::new()
474            .on_exact_select("(SELECT COUNT(*) FROM \"users\")", count_result);
475        let source = MockTableSource::new()
476            .with_select_source(mock_select_source)
477            .with_query_source(mock_query_source);
478        Table::<_, vantage_types::EmptyEntity>::new("users", source)
479    }
480
481    #[tokio::test]
482    async fn test_count_unwraps_single_element_array() {
483        // SQL/Surreal count queries commonly return `[{"count": N}]`.
484        let table = count_table_returning(json!([{"count": 7}])).await;
485        assert_eq!(table.get_count_via_query().await.unwrap(), 7);
486    }
487
488    #[tokio::test]
489    async fn test_count_errors_on_unexpected_shape() {
490        // An unrecognized result must surface as an error, not a silent zero.
491        let table = count_table_returning(json!({"total": 5})).await;
492        assert!(table.get_count_via_query().await.is_err());
493    }
494
495    #[tokio::test]
496    #[should_panic(expected = "MockTableSource select source not set")]
497    async fn test_panics_without_select_source() {
498        let table = Table::<_, vantage_types::EmptyEntity>::new("users", MockTableSource::new());
499        let _select = table.select();
500    }
501
502    #[tokio::test]
503    #[should_panic(expected = "MockTableSource query source not set")]
504    async fn test_panics_without_query_source() {
505        let table = Table::<_, vantage_types::EmptyEntity>::new("users", MockTableSource::new());
506        let query = table.data_source().expr("SELECT COUNT(*)", vec![]);
507        let _result = table.data_source().execute(&query).await;
508    }
509}