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 for column in self.columns.values() {
54 // Lazy-expression columns exist only on returned records — the
55 // source has no such field to project (SQLite would silently
56 // degrade the unknown quoted identifier to a string literal;
57 // other SQL backends would error).
58 if self.lazy_expressions.contains_key(column.name()) {
59 continue;
60 }
61 // With an active-column set, project only its members. The id
62 // column is always projected — consumers rely on it to key rows.
63 if !self.is_active(column.name()) {
64 continue;
65 }
66 if let Some(expr_fn) = self.expressions.get(column.name()) {
67 let expr = expr_fn(self.as_entity_erased());
68 self.data_source.add_select_column(
69 &mut select,
70 expr_any!("({})", (expr)),
71 Some(column.name()),
72 );
73 } else if let Some(alias) = column.alias() {
74 let expr = self.data_source.expr(column.name(), vec![]);
75 self.data_source
76 .add_select_column(&mut select, expr, Some(alias));
77 } else {
78 select.add_field(column.name());
79 }
80 }
81
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. 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 self.import_dotted_column(col)?;
218 self.active_columns
219 .get_or_insert_with(Default::default)
220 .insert(col.to_string());
221 } else {
222 // Expression columns registered via `with_expression` alone
223 // (no column def) are projectable too — activating them must
224 // work, or an active set would silently drop them for good.
225 if !self.columns.contains_key(col) && !self.expressions.contains_key(col) {
226 return Err(error!("unknown active column", column = col));
227 }
228 self.active_columns
229 .get_or_insert_with(Default::default)
230 .insert(col.to_string());
231 }
232 }
233 Ok(self)
234 }
235
236 /// Import a dotted implicit-reference column (`a.b…c`) **additively**:
237 /// the column joins the projection without restricting it, so the
238 /// declared columns keep flowing. Use this to give one consumer (an API
239 /// vista, a page) an extra traversal column that the shared table
240 /// definition does not carry.
241 pub fn with_imported_column(mut self, col: &str) -> Result<Self>
242 where
243 T: 'static,
244 E: 'static,
245 T::Column<T::AnyType>: Expressive<T::Value>,
246 T::Select: Expressive<T::Value>,
247 {
248 self.import_dotted_column(col)?;
249 Ok(self)
250 }
251
252 /// In-place form of [`with_imported_column`](Self::with_imported_column).
253 pub fn add_imported_column(&mut self, col: &str) -> Result<()>
254 where
255 T: 'static,
256 E: 'static,
257 T::Column<T::AnyType>: Expressive<T::Value>,
258 T::Select: Expressive<T::Value>,
259 {
260 self.import_dotted_column(col)
261 }
262
263 /// Shared body of the dotted branch: validate the `has_one` chain, lower
264 /// it to an expression (native idiom path or nested subqueries), register
265 /// the dotted alias as a column + expression and mark it imported. Does
266 /// NOT touch `active_columns` — the caller decides whether the projection
267 /// is restricted.
268 fn import_dotted_column(&mut self, col: &str) -> Result<()>
269 where
270 T: 'static,
271 E: 'static,
272 T::Column<T::AnyType>: Expressive<T::Value>,
273 T::Select: Expressive<T::Value>,
274 {
275 let parts: Vec<&str> = col.split('.').collect();
276 if parts.len() < 2 || parts.iter().any(|p| p.is_empty()) {
277 return Err(error!("invalid dotted column name", column = col));
278 }
279 let column = parts[parts.len() - 1];
280 let hops = &parts[..parts.len() - 1];
281
282 if !self.data_source().supports_traversal() {
283 return Err(error!(
284 "backend does not support implicit-reference traversal in columns",
285 column = col
286 ));
287 }
288
289 let (target, fk_hops) = self.resolve_has_one_target(hops)?;
290 if !target.columns().contains_key(column) {
291 return Err(error!(
292 "implicit reference target has no such column",
293 column = column
294 ));
295 }
296
297 // Lower to an expression: native path first, else the generic
298 // nested correlated-subquery chain. The native path receives
299 // the foreign-key/link *fields*, not the relation names — a
300 // SurrealDB idiom path traverses record-link fields, and a
301 // relation is free to be named differently from its FK
302 // (`with_one("owner", "client", …)` must lower to
303 // `client.name`, not the nonexistent `owner.name`).
304 let fk_refs: Vec<&str> = fk_hops.iter().map(String::as_str).collect();
305 let expr = match self.data_source().traversal_path_expr(&fk_refs, column) {
306 Some(e) => e,
307 None => self.traverse_rest_generic(hops, column)?,
308 };
309
310 let dotted = col.to_string();
311 if !self.columns.contains_key(&dotted) {
312 let column_def = self.data_source.create_column::<T::AnyType>(&dotted);
313 self.add_column(column_def);
314 }
315 let wrapped: crate::table::base::ExpressionFn<T> = Arc::new(move |_| expr.clone());
316 self.expressions.insert(dotted.clone(), wrapped);
317 self.imported_columns.insert(dotted.clone());
318 Ok(())
319 }
320
321 /// Recursively lower a dotted implicit reference into nested correlated
322 /// subqueries. One hop wraps the recursion's inner expression in a
323 /// `get_subquery_as` target via [`select_expression`](Self::select_expression);
324 /// the base case projects the final column. Used only when the backend has
325 /// no native [`traversal_path_expr`](crate::prelude::TableSource::traversal_path_expr).
326 fn traverse_rest_generic(&self, hops: &[&str], column: &str) -> Result<Expression<T::Value>>
327 where
328 T: 'static,
329 E: 'static,
330 T::Column<T::AnyType>: Expressive<T::Value>,
331 T::Select: Expressive<T::Value>,
332 {
333 match hops.split_first() {
334 None => self.get_column_expr(column).ok_or_else(|| {
335 error!(
336 "implicit reference target has no such column",
337 column = column
338 )
339 }),
340 Some((head, tail)) => {
341 let target: Table<T, EmptyEntity> = self.get_subquery_erased(head)?;
342 let inner = target.traverse_rest_generic(tail, column)?;
343 // The base case returns a bare column; a deeper hop returns a
344 // SELECT that must be parenthesized before it can nest as a
345 // scalar inside this hop's SELECT.
346 let inner = if tail.is_empty() {
347 inner
348 } else {
349 expr_any!("({})", (inner))
350 };
351 Ok(target.select_expression(inner))
352 }
353 }
354 }
355
356 /// Walk a chain of `has_one` hops and return the final target table along
357 /// with each hop's foreign-key/link field (in hop order), erroring at
358 /// build time on an unknown relation or a `has_many` hop. The FK fields
359 /// feed the backend-native path lowering, which traverses fields — the
360 /// relation *names* only address the refs registry.
361 fn resolve_has_one_target(&self, hops: &[&str]) -> Result<(Table<T, EmptyEntity>, Vec<String>)>
362 where
363 T: 'static,
364 E: 'static,
365 {
366 let (head, tail) = hops
367 .split_first()
368 .ok_or_else(|| error!("empty implicit reference path"))?;
369 if self.ref_cardinality(head)? != vantage_vista::ReferenceKind::HasOne {
370 return Err(error!(
371 "implicit reference hop must traverse a has_one relation",
372 relation = *head
373 ));
374 }
375 let fk = self.ref_foreign_key(head)?;
376 let target: Table<T, EmptyEntity> = self.get_ref_target_erased(head)?;
377 if tail.is_empty() {
378 Ok((target, vec![fk]))
379 } else {
380 let (final_target, mut fks) = target.resolve_has_one_target(tail)?;
381 fks.insert(0, fk);
382 Ok((final_target, fks))
383 }
384 }
385}
386
387// Constructors for tables sourced from an arbitrary query (a derived / sub-SELECT
388// source). Only available to backends whose `Source` is `SelectSource<Select>`
389// (the four subquery-capable SQL/SurrealDB backends). `V`/`C`/`S` are named
390// explicitly rather than projected through `T` to avoid a bound-resolution cycle.
391impl<T, E, V, C, S> Table<T, E>
392where
393 T: SelectableDataSource<V, C, Select = S>
394 + TableSource<Value = V, Condition = C, Source = SelectSource<S>>,
395 V: Clone + Send + Sync + 'static + From<String>,
396 C: Clone + Send + Sync + 'static,
397 S: Expressive<V> + Clone,
398 E: Entity<V>,
399{
400 /// Build a read-only table whose FROM clause is `select`, exposed under
401 /// `alias`. Columns/relations start empty — declare or inherit them.
402 pub fn from_select(data_source: T, alias: impl Into<String>, select: S) -> Self {
403 let alias = alias.into();
404 let mut table = Table::new(alias.clone(), data_source);
405 table.source = SelectSource::query(select, alias);
406 table
407 }
408
409 /// Derive a table from an existing one: transform its select via `modifier`
410 /// and use the result as the (sub-SELECT) source, inheriting the listed
411 /// `columns` and `relations` plus identity/title metadata.
412 ///
413 /// `modifier` receives `source.select()` and decides flat-vs-wrapped — it
414 /// may extend the query in place (joins referencing the base tables) or wrap
415 /// it as a subquery (to filter/sort on a computed alias). Conditions already
416 /// baked into the base select are not re-applied.
417 ///
418 /// Implicit references are **not** inherited: the derived table starts with
419 /// no active set, and listing an imported dotted column in `columns` copies
420 /// only its bare definition (no traversal expression, no read-only
421 /// tracking). Re-declare traversals on the derived table if needed.
422 pub fn derive_from<E2: Entity<V> + 'static>(
423 source: &Table<T, E2>,
424 alias: impl Into<String>,
425 modifier: impl FnOnce(S) -> S,
426 columns: &[&str],
427 relations: &[&str],
428 ) -> Self
429 where
430 T: 'static,
431 E: 'static,
432 {
433 let alias = alias.into();
434 let select = modifier(source.select());
435 let mut table = Table::new(alias.clone(), source.data_source().clone());
436 table.source = SelectSource::query(select, alias);
437 table.copy_columns_from(source, Some(columns));
438 table.copy_relations_from(source, Some(relations));
439 table.id_field = source.id_field.clone();
440 table.title_field = source.title_field.clone();
441 table.title_fields = source.title_fields.clone();
442 table
443 }
444}
445
446impl<T, E> Table<T, E>
447where
448 T: SelectableDataSource<serde_json::Value, T::Condition>
449 + TableSource<Value = serde_json::Value>
450 + vantage_expressions::traits::datasource::ExprDataSource<serde_json::Value>,
451 T::Source: SelectSeed<T::Select, serde_json::Value, T::Condition>,
452 T::Value: From<String>,
453 E: Entity<serde_json::Value>,
454{
455 /// Get count using QuerySource for serde_json::Value
456 pub async fn get_count_via_query(&self) -> Result<i64> {
457 let count_query = self.get_count_query();
458 let result = self.data_source.execute(&count_query).await?;
459
460 // Unwrap a single-element array, e.g. `[{"count": 42}]` or `[42]`,
461 // which is how SQL/Surreal count queries commonly come back.
462 let result = match result.as_array().map(Vec::as_slice) {
463 Some([single]) => single,
464 _ => &result,
465 };
466
467 // Anything but `{"count": N}` or a bare `N` is an unexpected shape:
468 // surface it rather than silently reporting zero rows.
469 if let Some(count) = result.get("count").and_then(|v| v.as_i64()) {
470 Ok(count)
471 } else if let Some(count) = result.as_i64() {
472 Ok(count)
473 } else {
474 Err(vantage_core::error!(
475 "count query returned an unexpected result shape",
476 result = result
477 ))
478 }
479 }
480}