Skip to main content

vantage_table/traits/
table_source.rs

1use std::hash::Hash;
2use std::pin::Pin;
3
4use async_trait::async_trait;
5use futures_core::Stream;
6use indexmap::IndexMap;
7use vantage_dataset::traits::Result;
8use vantage_expressions::{
9    Expression,
10    traits::associated_expressions::AssociatedExpression,
11    traits::datasource::{DataSource, ExprDataSource},
12    traits::expressive::ExpressiveEnum,
13};
14use vantage_types::{Entity, Record};
15use vantage_vista::Transaction;
16
17use crate::{column::core::ColumnType, table::Table, traits::column_like::ColumnLike};
18
19/// A data source that holds tables: it names their column, value, id,
20/// source and condition types, and runs `Table`'s reads and writes.
21#[async_trait]
22pub trait TableSource: DataSource + Clone + 'static {
23    type Column<Type>: ColumnLike<Type> + Clone
24    where
25        Type: ColumnType;
26    type AnyType: ColumnType;
27    type Value: Clone + Send + Sync + 'static;
28    type Id: Send + Sync + Clone + Hash + Eq + 'static;
29
30    /// How this backend names its source. Most backends use `String`;
31    /// SQL/SurrealDB backends use `SelectSource<Self::Select>` so a table can
32    /// be sourced from an arbitrary sub-`SELECT`. See [`crate::source`].
33    type Source: crate::traits::table_source_spec::TableSourceSpec;
34
35    /// The condition type stored by `Table`. SQL/SurrealDB backends use
36    /// `Expression<Self::Value>`; document-oriented backends like MongoDB
37    /// can use a native filter type (e.g. `bson::Document`).
38    type Condition: Clone + Send + Sync + 'static;
39
40    /// Build a textual `field == value` condition.
41    ///
42    /// Generic UIs (e.g. CLI argument parsers) only have strings on
43    /// hand, so they need a way to construct a `Self::Condition` without
44    /// knowing the backend's native expression type. Backends that
45    /// don't support raw text filtering can leave the default, which
46    /// returns an error.
47    fn eq_condition(field: &str, value: &str) -> Result<Self::Condition> {
48        let _ = (field, value);
49        Err(vantage_core::error!(
50            "eq_condition not implemented for this TableSource"
51        ))
52    }
53
54    /// Build a `field == value` condition with a typed `Self::Value`.
55    ///
56    /// Sibling of [`TableSource::eq_condition`] for the case where the caller already
57    /// has a native-typed value in hand (e.g. `Reference::resolve_from_row`
58    /// pulling a join value out of a `Record<Self::Value>`). Avoids going
59    /// through string serialization and avoids the `Expression → T::Condition`
60    /// coercion that some document backends only support via a panic stub.
61    ///
62    /// Backends that participate in row-based reference traversal override;
63    /// the default returns an error so existing impls compile.
64    fn eq_value_condition(&self, field: &str, value: Self::Value) -> Result<Self::Condition> {
65        let _ = (field, value);
66        Err(vantage_core::error!(
67            "eq_value_condition not implemented for this TableSource"
68        ))
69    }
70
71    /// Build a `field IN (values)` condition from native-typed values: the
72    /// list-valued sibling of [`Self::eq_value_condition`].
73    /// [`Self::condition_column`] names `field` for it. An empty `values`
74    /// matches no row. The default returns an error.
75    fn in_values_condition(
76        &self,
77        field: &str,
78        values: Vec<Self::Value>,
79    ) -> Result<Self::Condition> {
80        let _ = (field, values);
81        Err(vantage_core::error!(
82            "in_values_condition not implemented for this TableSource"
83        ))
84    }
85
86    /// When `condition` is a literal `column = value`, return the pair.
87    ///
88    /// [`Table::add_condition`] registers it as a set invariant: an insert or
89    /// replace through the table fills an absent column, a patch may not change
90    /// it. Expression backends use [`crate::conditions::literal_equality`];
91    /// document backends read their own filter shape. Default `None`: the
92    /// condition still narrows, it just fills nothing.
93    fn condition_equality(&self, condition: &Self::Condition) -> Option<(String, Self::Value)> {
94        let _ = condition;
95        None
96    }
97
98    /// `condition` as text for people, e.g. `client_id = 7`: what a log entry
99    /// records for a bulk delete. Not for parsing; use
100    /// [`Self::condition_column`] / [`Self::condition_equality`] in code.
101    /// A part resolved only at query time renders as a placeholder.
102    fn describe_condition(&self, condition: &Self::Condition) -> String {
103        let _ = condition;
104        "<condition>".to_string()
105    }
106
107    /// The column `condition` narrows to values, for the shapes this backend
108    /// can read (`col = v`, `col IN (…)`, comparisons, `LIKE`), with a value
109    /// — not a column or an arbitrary expression — on the right. `None` when
110    /// it can't tell — a raw expression, a combination, a negation (`!=`,
111    /// `NOT IN`, `NOT LIKE`), `col = other_col` — so a check that requires a
112    /// condition on some column fails safe.
113    fn condition_column(&self, condition: &Self::Condition) -> Option<String> {
114        let _ = condition;
115        None
116    }
117
118    /// Whether every by-id write (`insert_table_value`, `replace_table_value`,
119    /// `patch_table_value`, `delete_table_value`, `insert_table_return_id_value`)
120    /// honours the write contract over `table.conditions()`: it only touches
121    /// rows in the set, and only leaves rows that still belong to it. A source
122    /// that returns `false` is refused any by-id write through a conditioned
123    /// table: `Table` refuses it with `Unsupported` before reaching the source.
124    /// Default `false`.
125    fn can_confine_writes(&self) -> bool {
126        false
127    }
128
129    /// Coerce a reference join value into the backend's native id form.
130    ///
131    /// Reference traversal (`resolve_from_row`) reads the join value raw out
132    /// of the parent row and uses it for both the narrowing eq-condition and
133    /// the target's insert invariant. On scalar-FK backends any value compares
134    /// as-is, so the default is identity. Backends with a richer id type
135    /// override: SurrealDB re-tags a `"table:key"` string — a record id that
136    /// round-tripped through JSON or a script — back into a record id, so the
137    /// condition renders a record literal (and an inserted child stores a
138    /// link), not a quoted string that matches nothing.
139    fn coerce_reference_value(&self, value: Self::Value) -> Self::Value {
140        value
141    }
142
143    /// The transaction this datasource runs its statements on, when it is
144    /// in transaction mode. Default `None`.
145    fn transaction(&self) -> Option<Transaction> {
146        None
147    }
148
149    /// This datasource back on its pool, for work that runs after the
150    /// transaction it was in. Default: a clone.
151    fn without_transaction(&self) -> Self {
152        self.clone()
153    }
154
155    /// Create a new column with the given name
156    fn create_column<Type: ColumnType>(&self, name: &str) -> Self::Column<Type>;
157
158    /// Convert a typed column to type-erased column
159    fn to_any_column<Type: ColumnType>(
160        &self,
161        column: Self::Column<Type>,
162    ) -> Self::Column<Self::AnyType>;
163
164    /// Attempt to convert a type-erased column back to typed column
165    fn convert_any_column<Type: ColumnType>(
166        &self,
167        any_column: Self::Column<Self::AnyType>,
168    ) -> Option<Self::Column<Type>>;
169
170    /// Create an expression from a template and parameters, similar to Expression::new
171    fn expr(
172        &self,
173        template: impl Into<String>,
174        parameters: Vec<ExpressiveEnum<Self::Value>>,
175    ) -> Expression<Self::Value>;
176
177    /// Create a search condition matching `search_value` across the columns
178    /// of [`Table::search_columns`]: those flagged `Searchable`, or every
179    /// column when none is. A backend may leave out columns it can't filter
180    /// on (dotted paths, imported columns). This is a *server-side*
181    /// capability — it pushes the match down to the backend's query engine.
182    ///
183    /// Different vendors implement search differently:
184    /// - SQL: `field LIKE '%value%'` (returns Expression)
185    /// - SurrealDB: `field CONTAINS 'value'` (returns Expression)
186    /// - MongoDB: `{ field: { $regex: 'value' } }` (returns MongoCondition)
187    ///
188    /// **Contract for backends that cannot search server-side** (e.g. CSV, redb):
189    /// return a condition that yields an `ErrorKind::Unsupported` error when it is
190    /// *resolved* — never a silent match-all (which leaks every row as if filtered)
191    /// and never a panic. Searching loaded/in-memory data is the Lens/Diorama
192    /// layer's responsibility, not the backend's.
193    fn search_table_condition<E>(
194        &self,
195        table: &Table<Self, E>,
196        search_value: &str,
197    ) -> Self::Condition
198    where
199        E: Entity<Self::Value>,
200        Self: Sized;
201
202    /// Get all data from a table as Record values with IDs (for ReadableValueSet implementation)
203    async fn list_table_values<E>(
204        &self,
205        table: &Table<Self, E>,
206    ) -> Result<IndexMap<Self::Id, Record<Self::Value>>>
207    where
208        E: Entity<Self::Value>,
209        Self: Sized;
210
211    /// Get a single record by ID as Record value (for ReadableValueSet implementation).
212    ///
213    /// Returns `Ok(None)` when no record exists with the given ID.
214    async fn get_table_value<E>(
215        &self,
216        table: &Table<Self, E>,
217        id: &Self::Id,
218    ) -> Result<Option<Record<Self::Value>>>
219    where
220        E: Entity<Self::Value>,
221        Self: Sized;
222
223    /// Get some data from a table as Record value with ID (for ReadableValueSet implementation)
224    async fn get_table_some_value<E>(
225        &self,
226        table: &Table<Self, E>,
227    ) -> Result<Option<(Self::Id, Record<Self::Value>)>>
228    where
229        E: Entity<Self::Value>,
230        Self: Sized;
231
232    /// Get count of records in the table
233    async fn get_table_count<E>(&self, table: &Table<Self, E>) -> Result<i64>
234    where
235        E: Entity<Self::Value>,
236        Self: Sized;
237
238    /// Get sum of a column in the table (returns native value type)
239    async fn get_table_sum<E>(
240        &self,
241        table: &Table<Self, E>,
242        column: &Self::Column<Self::AnyType>,
243    ) -> Result<Self::Value>
244    where
245        E: Entity<Self::Value>,
246        Self: Sized;
247
248    /// Get maximum value of a column in the table (returns native value type)
249    async fn get_table_max<E>(
250        &self,
251        table: &Table<Self, E>,
252        column: &Self::Column<Self::AnyType>,
253    ) -> Result<Self::Value>
254    where
255        E: Entity<Self::Value>,
256        Self: Sized;
257
258    /// Get minimum value of a column in the table (returns native value type)
259    async fn get_table_min<E>(
260        &self,
261        table: &Table<Self, E>,
262        column: &Self::Column<Self::AnyType>,
263    ) -> Result<Self::Value>
264    where
265        E: Entity<Self::Value>,
266        Self: Sized;
267
268    /// Insert a record by ID. **Confined and idempotent:** when the id is
269    /// already stored in `table.conditions()`, return the stored row unchanged;
270    /// when it is held by a row outside the set, fail with `Conflict`; a new
271    /// row must fit the conditions, else `Conflict` and nothing is stored.
272    /// Refuse with `Unsupported` when a condition can be neither applied nor
273    /// evaluated. See `WritableValueSet` for the table.
274    async fn insert_table_value<E>(
275        &self,
276        table: &Table<Self, E>,
277        id: &Self::Id,
278        record: &Record<Self::Value>,
279    ) -> Result<Record<Self::Value>>
280    where
281        E: Entity<Self::Value>,
282        Self: Sized;
283
284    /// Replace a record by ID, creating it when missing. **Confined and
285    /// idempotent:** an id held by a row outside `table.conditions()` fails
286    /// with `Conflict`; the written row must fit the conditions, else
287    /// `Conflict` and nothing changes. Refuse with `Unsupported` when a
288    /// condition can be neither applied nor evaluated. See `WritableValueSet`
289    /// for the table.
290    async fn replace_table_value<E>(
291        &self,
292        table: &Table<Self, E>,
293        id: &Self::Id,
294        record: &Record<Self::Value>,
295    ) -> Result<Record<Self::Value>>
296    where
297        E: Entity<Self::Value>,
298        Self: Sized;
299
300    /// Merge `partial` into the row with this ID. **Confined:** a missing row
301    /// or one outside `table.conditions()` is `NotFound` (marked); the merged
302    /// row must still fit the conditions, else `Conflict` and nothing changes.
303    /// Refuse with `Unsupported` when a condition can be neither applied nor
304    /// evaluated. See `WritableValueSet` for the table.
305    async fn patch_table_value<E>(
306        &self,
307        table: &Table<Self, E>,
308        id: &Self::Id,
309        partial: &Record<Self::Value>,
310    ) -> Result<Record<Self::Value>>
311    where
312        E: Entity<Self::Value>,
313        Self: Sized;
314
315    /// Delete a record by ID. **Confined and idempotent:** delete only when the
316    /// row is in `table.conditions()`; a missing row or a row outside the set
317    /// is `Ok(())` (no lookup is needed — SQL: `DELETE … WHERE id = ? AND
318    /// <conditions>`). Refuse with `Unsupported` when a condition can be
319    /// neither applied nor evaluated. See `WritableValueSet` for the table.
320    async fn delete_table_value<E>(&self, table: &Table<Self, E>, id: &Self::Id) -> Result<()>
321    where
322        E: Entity<Self::Value>,
323        Self: Sized;
324
325    /// Delete every record the table represents (for WritableValueSet).
326    ///
327    /// **Honour `table.conditions()`.** A conditioned table is a SUBSET, and
328    /// every read path already treats it as one, so deleting it must delete
329    /// that subset. An implementation that builds its statement from
330    /// `table_name()` alone silently truncates the table instead — the caller
331    /// narrowed to one row, and lost all of them. An unconditioned table
332    /// still deletes everything, which is the only case this looks like.
333    async fn delete_table_all_values<E>(&self, table: &Table<Self, E>) -> Result<()>
334    where
335        E: Entity<Self::Value>,
336        Self: Sized;
337
338    /// Insert a record under a generated ID and return it. **Confined, not
339    /// retry-safe:** the record must fit `table.conditions()`, else `Conflict`
340    /// and nothing is stored; a retry after a lost reply stores a second row.
341    /// Refuse with `Unsupported` when a condition can be neither applied nor
342    /// evaluated.
343    async fn insert_table_return_id_value<E>(
344        &self,
345        table: &Table<Self, E>,
346        record: &Record<Self::Value>,
347    ) -> Result<Self::Id>
348    where
349        E: Entity<Self::Value>,
350        Self: Sized;
351
352    /// Stream all records from a table as (Id, Record) pairs.
353    ///
354    /// Default implementation wraps `list_table_values` into a stream.
355    /// Backends with native streaming (e.g. REST APIs with pagination)
356    /// can override this to yield records incrementally.
357    #[allow(clippy::type_complexity)]
358    fn stream_table_values<'a, E>(
359        &'a self,
360        table: &Table<Self, E>,
361    ) -> Pin<Box<dyn Stream<Item = Result<(Self::Id, Record<Self::Value>)>> + Send + 'a>>
362    where
363        E: Entity<Self::Value> + 'a,
364        Self: Sized,
365    {
366        let table = table.clone();
367        Box::pin(async_stream::stream! {
368            let records = self.list_table_values(&table).await;
369            match records {
370                Ok(map) => {
371                    for item in map {
372                        yield Ok(item);
373                    }
374                }
375                Err(e) => yield Err(e),
376            }
377        })
378    }
379
380    /// Build a condition for "target_field IN (values of source_column from source_table)".
381    ///
382    /// Used by the relationship traversal system (`get_ref_as`) to filter
383    /// a target table by foreign key values from a source table.
384    ///
385    /// Each backend implements this natively:
386    /// - SQL backends build a subquery: `"id" IN (SELECT "bakery_id" FROM "client" WHERE ...)`
387    /// - MongoDB builds a deferred `{ field: { "$in": [...] } }` document
388    /// - CSV fetches values in memory and builds an IN condition
389    fn related_in_condition<SourceE: Entity<Self::Value> + 'static>(
390        &self,
391        target_field: &str,
392        source_table: &Table<Self, SourceE>,
393        source_column: &str,
394    ) -> Self::Condition
395    where
396        Self: Sized;
397
398    /// Build a correlated condition: `target_table.target_field = source_table.source_column`.
399    ///
400    /// Used by `get_subquery_as` / `get_subquery_erased` for embedding correlated
401    /// subqueries inside SELECT expressions (e.g.
402    /// `(SELECT COUNT(*) FROM order WHERE order.client_id = client.id)`), and by
403    /// the generic implicit-reference lowering. SQL and SurrealDB backends
404    /// produce table-qualified equality; backends without correlated subqueries
405    /// leave the default (which panics — they must also keep
406    /// [`supports_traversal`](Self::supports_traversal) `false`).
407    fn related_correlated_condition(
408        &self,
409        target_table: &str,
410        target_field: &str,
411        source_table: &str,
412        source_column: &str,
413    ) -> Self::Condition {
414        let _ = (target_table, target_field, source_table, source_column);
415        unimplemented!("correlated subqueries not supported by this backend")
416    }
417
418    /// Whether this backend can lower a dotted active column
419    /// (`"country.name"`) into its own query — a nested correlated subquery for
420    /// SQL, a native idiom path for SurrealDB, `$lookup` stages for MongoDB.
421    /// Backends that can build none of these (e.g. CSV, REST) leave the
422    /// default `false`, and
423    /// [`Table::with_active_columns`](crate::table::Table::with_active_columns)
424    /// rejects dotted names against them at build time rather than at fetch time.
425    fn supports_traversal(&self) -> bool {
426        false
427    }
428
429    /// Build a backend-native path expression for a dotted active column,
430    /// tried before the generic correlated-subquery chain.
431    ///
432    /// `hops` holds each traversed relation's **foreign-key/link field** (not
433    /// its registry name — a relation named `owner` over a link field `client`
434    /// must lower to `client.…`), and `column` the final field. The default
435    /// returns `None`, so backends fall back to the generic chain built on
436    /// [`related_correlated_condition`](Self::related_correlated_condition).
437    /// SurrealDB overrides this to emit an idiom path (`batch.golf_course.name`,
438    /// each segment escaped separately); multi-hop comes for free. The returned
439    /// expression is unaliased — `select()` aliases it to the dotted name.
440    fn traversal_path_expr(&self, hops: &[&str], column: &str) -> Option<Expression<Self::Value>> {
441        let _ = (hops, column);
442        None
443    }
444
445    /// Return an associated expression that, when resolved, yields all values
446    /// of the given typed column from this table (respecting current conditions).
447    ///
448    /// For query-language backends, this can be a subquery expression.
449    /// For simple backends (CSV), this uses a `DeferredFn` that loads data
450    /// and extracts the column values at execution time.
451    ///
452    /// The returned `AssociatedExpression` can be:
453    /// - Executed directly: `.get().await -> Result<Vec<Type>>`
454    /// - Composed into expressions: used via `Expressive` trait in `in_()` conditions
455    ///
456    /// ```rust,ignore
457    /// let fk_col = source.get_column::<String>("bakery_id").unwrap();
458    /// let fk_values = source.data_source().column_table_values_expr(&source, &fk_col);
459    /// // Execute: let ids = fk_values.get().await?;
460    /// // Or compose: target.add_condition(target["id"].in_((fk_values)));
461    /// ```
462    fn column_table_values_expr<'a, E, Type: ColumnType>(
463        &'a self,
464        table: &Table<Self, E>,
465        column: &Self::Column<Type>,
466    ) -> AssociatedExpression<'a, Self, Self::Value, Vec<Type>>
467    where
468        E: Entity<Self::Value> + 'static,
469        Self: ExprDataSource<Self::Value> + Sized;
470}