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