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/// Trait for table data sources that defines column type separate from execution
19/// TableSource represents a data source that can create and manage tables
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 /// Coerce a reference join value into the backend's native id form.
71 ///
72 /// Reference traversal (`resolve_from_row`) reads the join value raw out
73 /// of the parent row and uses it for both the narrowing eq-condition and
74 /// the target's insert invariant. On scalar-FK backends any value compares
75 /// as-is, so the default is identity. Backends with a richer id type
76 /// override: SurrealDB re-tags a `"table:key"` string — a record id that
77 /// round-tripped through JSON or a script — back into a record id, so the
78 /// condition renders a record literal (and an inserted child stores a
79 /// link), not a quoted string that matches nothing.
80 fn coerce_reference_value(&self, value: Self::Value) -> Self::Value {
81 value
82 }
83
84 /// Create a new column with the given name
85 fn create_column<Type: ColumnType>(&self, name: &str) -> Self::Column<Type>;
86
87 /// Convert a typed column to type-erased column
88 fn to_any_column<Type: ColumnType>(
89 &self,
90 column: Self::Column<Type>,
91 ) -> Self::Column<Self::AnyType>;
92
93 /// Attempt to convert a type-erased column back to typed column
94 fn convert_any_column<Type: ColumnType>(
95 &self,
96 any_column: Self::Column<Self::AnyType>,
97 ) -> Option<Self::Column<Type>>;
98
99 /// Create an expression from a template and parameters, similar to Expression::new
100 fn expr(
101 &self,
102 template: impl Into<String>,
103 parameters: Vec<ExpressiveEnum<Self::Value>>,
104 ) -> Expression<Self::Value>;
105
106 /// Create a search condition matching `search_value` across the table's
107 /// searchable fields. This is a *server-side* capability — it pushes the
108 /// match down to the backend's query engine.
109 ///
110 /// Different vendors implement search differently:
111 /// - SQL: `field LIKE '%value%'` (returns Expression)
112 /// - SurrealDB: `field CONTAINS 'value'` (returns Expression)
113 /// - MongoDB: `{ field: { $regex: 'value' } }` (returns MongoCondition)
114 ///
115 /// **Contract for backends that cannot search server-side** (e.g. CSV, redb):
116 /// return a condition that yields an `ErrorKind::Unsupported` error when it is
117 /// *resolved* — never a silent match-all (which leaks every row as if filtered)
118 /// and never a panic. Searching loaded/in-memory data is the Lens/Diorama
119 /// layer's responsibility, not the backend's.
120 fn search_table_condition<E>(
121 &self,
122 table: &Table<Self, E>,
123 search_value: &str,
124 ) -> Self::Condition
125 where
126 E: Entity<Self::Value>,
127 Self: Sized;
128
129 /// Get all data from a table as Record values with IDs (for ReadableValueSet implementation)
130 async fn list_table_values<E>(
131 &self,
132 table: &Table<Self, E>,
133 ) -> Result<IndexMap<Self::Id, Record<Self::Value>>>
134 where
135 E: Entity<Self::Value>,
136 Self: Sized;
137
138 /// Get a single record by ID as Record value (for ReadableValueSet implementation).
139 ///
140 /// Returns `Ok(None)` when no record exists with the given ID.
141 async fn get_table_value<E>(
142 &self,
143 table: &Table<Self, E>,
144 id: &Self::Id,
145 ) -> Result<Option<Record<Self::Value>>>
146 where
147 E: Entity<Self::Value>,
148 Self: Sized;
149
150 /// Get some data from a table as Record value with ID (for ReadableValueSet implementation)
151 async fn get_table_some_value<E>(
152 &self,
153 table: &Table<Self, E>,
154 ) -> Result<Option<(Self::Id, Record<Self::Value>)>>
155 where
156 E: Entity<Self::Value>,
157 Self: Sized;
158
159 /// Get count of records in the table
160 async fn get_table_count<E>(&self, table: &Table<Self, E>) -> Result<i64>
161 where
162 E: Entity<Self::Value>,
163 Self: Sized;
164
165 /// Get sum of a column in the table (returns native value type)
166 async fn get_table_sum<E>(
167 &self,
168 table: &Table<Self, E>,
169 column: &Self::Column<Self::AnyType>,
170 ) -> Result<Self::Value>
171 where
172 E: Entity<Self::Value>,
173 Self: Sized;
174
175 /// Get maximum value of a column in the table (returns native value type)
176 async fn get_table_max<E>(
177 &self,
178 table: &Table<Self, E>,
179 column: &Self::Column<Self::AnyType>,
180 ) -> Result<Self::Value>
181 where
182 E: Entity<Self::Value>,
183 Self: Sized;
184
185 /// Get minimum value of a column in the table (returns native value type)
186 async fn get_table_min<E>(
187 &self,
188 table: &Table<Self, E>,
189 column: &Self::Column<Self::AnyType>,
190 ) -> Result<Self::Value>
191 where
192 E: Entity<Self::Value>,
193 Self: Sized;
194
195 /// Insert a record as Record value (for WritableValueSet implementation)
196 async fn insert_table_value<E>(
197 &self,
198 table: &Table<Self, E>,
199 id: &Self::Id,
200 record: &Record<Self::Value>,
201 ) -> Result<Record<Self::Value>>
202 where
203 E: Entity<Self::Value>,
204 Self: Sized;
205
206 /// Replace a record as Record value (for WritableValueSet implementation)
207 async fn replace_table_value<E>(
208 &self,
209 table: &Table<Self, E>,
210 id: &Self::Id,
211 record: &Record<Self::Value>,
212 ) -> Result<Record<Self::Value>>
213 where
214 E: Entity<Self::Value>,
215 Self: Sized;
216
217 /// Patch a record as Record value (for WritableValueSet implementation)
218 async fn patch_table_value<E>(
219 &self,
220 table: &Table<Self, E>,
221 id: &Self::Id,
222 partial: &Record<Self::Value>,
223 ) -> Result<Record<Self::Value>>
224 where
225 E: Entity<Self::Value>,
226 Self: Sized;
227
228 /// Delete a record by ID (for WritableValueSet implementation)
229 async fn delete_table_value<E>(&self, table: &Table<Self, E>, id: &Self::Id) -> Result<()>
230 where
231 E: Entity<Self::Value>,
232 Self: Sized;
233
234 /// Delete all records (for WritableValueSet implementation)
235 async fn delete_table_all_values<E>(&self, table: &Table<Self, E>) -> Result<()>
236 where
237 E: Entity<Self::Value>,
238 Self: Sized;
239
240 /// Insert a record and return generated ID (for InsertableValueSet implementation)
241 async fn insert_table_return_id_value<E>(
242 &self,
243 table: &Table<Self, E>,
244 record: &Record<Self::Value>,
245 ) -> Result<Self::Id>
246 where
247 E: Entity<Self::Value>,
248 Self: Sized;
249
250 /// Stream all records from a table as (Id, Record) pairs.
251 ///
252 /// Default implementation wraps `list_table_values` into a stream.
253 /// Backends with native streaming (e.g. REST APIs with pagination)
254 /// can override this to yield records incrementally.
255 #[allow(clippy::type_complexity)]
256 fn stream_table_values<'a, E>(
257 &'a self,
258 table: &Table<Self, E>,
259 ) -> Pin<Box<dyn Stream<Item = Result<(Self::Id, Record<Self::Value>)>> + Send + 'a>>
260 where
261 E: Entity<Self::Value> + 'a,
262 Self: Sized,
263 {
264 let table = table.clone();
265 Box::pin(async_stream::stream! {
266 let records = self.list_table_values(&table).await;
267 match records {
268 Ok(map) => {
269 for item in map {
270 yield Ok(item);
271 }
272 }
273 Err(e) => yield Err(e),
274 }
275 })
276 }
277
278 /// Build a condition for "target_field IN (values of source_column from source_table)".
279 ///
280 /// Used by the relationship traversal system (`get_ref_as`) to filter
281 /// a target table by foreign key values from a source table.
282 ///
283 /// Each backend implements this natively:
284 /// - SQL backends build a subquery: `"id" IN (SELECT "bakery_id" FROM "client" WHERE ...)`
285 /// - MongoDB builds a deferred `{ field: { "$in": [...] } }` document
286 /// - CSV fetches values in memory and builds an IN condition
287 fn related_in_condition<SourceE: Entity<Self::Value> + 'static>(
288 &self,
289 target_field: &str,
290 source_table: &Table<Self, SourceE>,
291 source_column: &str,
292 ) -> Self::Condition
293 where
294 Self: Sized;
295
296 /// Build a correlated condition: `target_table.target_field = source_table.source_column`.
297 ///
298 /// Used by `get_subquery_as` / `get_subquery_erased` for embedding correlated
299 /// subqueries inside SELECT expressions (e.g.
300 /// `(SELECT COUNT(*) FROM order WHERE order.client_id = client.id)`), and by
301 /// the generic implicit-reference lowering. SQL and SurrealDB backends
302 /// produce table-qualified equality; backends without correlated subqueries
303 /// leave the default (which panics — they must also keep
304 /// [`supports_traversal`](Self::supports_traversal) `false`).
305 fn related_correlated_condition(
306 &self,
307 target_table: &str,
308 target_field: &str,
309 source_table: &str,
310 source_column: &str,
311 ) -> Self::Condition {
312 let _ = (target_table, target_field, source_table, source_column);
313 unimplemented!("correlated subqueries not supported by this backend")
314 }
315
316 /// Whether this backend can lower a dotted active column
317 /// (`"country.name"`) into its own query — a nested correlated subquery for
318 /// SQL, a native idiom path for SurrealDB. Backends that can build neither
319 /// (e.g. MongoDB, CSV, REST) leave the default `false`, and
320 /// [`Table::with_active_columns`](crate::table::Table::with_active_columns)
321 /// rejects dotted names against them at build time rather than at fetch time.
322 fn supports_traversal(&self) -> bool {
323 false
324 }
325
326 /// Build a backend-native path expression for a dotted active column,
327 /// tried before the generic correlated-subquery chain.
328 ///
329 /// `hops` holds each traversed relation's **foreign-key/link field** (not
330 /// its registry name — a relation named `owner` over a link field `client`
331 /// must lower to `client.…`), and `column` the final field. The default
332 /// returns `None`, so backends fall back to the generic chain built on
333 /// [`related_correlated_condition`](Self::related_correlated_condition).
334 /// SurrealDB overrides this to emit an idiom path (`batch.golf_course.name`,
335 /// each segment escaped separately); multi-hop comes for free. The returned
336 /// expression is unaliased — `select()` aliases it to the dotted name.
337 fn traversal_path_expr(&self, hops: &[&str], column: &str) -> Option<Expression<Self::Value>> {
338 let _ = (hops, column);
339 None
340 }
341
342 /// Return an associated expression that, when resolved, yields all values
343 /// of the given typed column from this table (respecting current conditions).
344 ///
345 /// For query-language backends, this can be a subquery expression.
346 /// For simple backends (CSV), this uses a `DeferredFn` that loads data
347 /// and extracts the column values at execution time.
348 ///
349 /// The returned `AssociatedExpression` can be:
350 /// - Executed directly: `.get().await -> Result<Vec<Type>>`
351 /// - Composed into expressions: used via `Expressive` trait in `in_()` conditions
352 ///
353 /// ```rust,ignore
354 /// let fk_col = source.get_column::<String>("bakery_id").unwrap();
355 /// let fk_values = source.data_source().column_table_values_expr(&source, &fk_col);
356 /// // Execute: let ids = fk_values.get().await?;
357 /// // Or compose: target.add_condition(target["id"].in_((fk_values)));
358 /// ```
359 fn column_table_values_expr<'a, E, Type: ColumnType>(
360 &'a self,
361 table: &Table<Self, E>,
362 column: &Self::Column<Type>,
363 ) -> AssociatedExpression<'a, Self, Self::Value, Vec<Type>>
364 where
365 E: Entity<Self::Value> + 'static,
366 Self: ExprDataSource<Self::Value> + Sized;
367}