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