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 /// Create a new column with the given name
71 fn create_column<Type: ColumnType>(&self, name: &str) -> Self::Column<Type>;
72
73 /// Convert a typed column to type-erased column
74 fn to_any_column<Type: ColumnType>(
75 &self,
76 column: Self::Column<Type>,
77 ) -> Self::Column<Self::AnyType>;
78
79 /// Attempt to convert a type-erased column back to typed column
80 fn convert_any_column<Type: ColumnType>(
81 &self,
82 any_column: Self::Column<Self::AnyType>,
83 ) -> Option<Self::Column<Type>>;
84
85 /// Create an expression from a template and parameters, similar to Expression::new
86 fn expr(
87 &self,
88 template: impl Into<String>,
89 parameters: Vec<ExpressiveEnum<Self::Value>>,
90 ) -> Expression<Self::Value>;
91
92 /// Create a search condition matching `search_value` across the table's
93 /// searchable fields. This is a *server-side* capability — it pushes the
94 /// match down to the backend's query engine.
95 ///
96 /// Different vendors implement search differently:
97 /// - SQL: `field LIKE '%value%'` (returns Expression)
98 /// - SurrealDB: `field CONTAINS 'value'` (returns Expression)
99 /// - MongoDB: `{ field: { $regex: 'value' } }` (returns MongoCondition)
100 ///
101 /// **Contract for backends that cannot search server-side** (e.g. CSV, redb):
102 /// return a condition that yields an `ErrorKind::Unsupported` error when it is
103 /// *resolved* — never a silent match-all (which leaks every row as if filtered)
104 /// and never a panic. Searching loaded/in-memory data is the Lens/Diorama
105 /// layer's responsibility, not the backend's.
106 fn search_table_condition<E>(
107 &self,
108 table: &Table<Self, E>,
109 search_value: &str,
110 ) -> Self::Condition
111 where
112 E: Entity<Self::Value>,
113 Self: Sized;
114
115 /// Get all data from a table as Record values with IDs (for ReadableValueSet implementation)
116 async fn list_table_values<E>(
117 &self,
118 table: &Table<Self, E>,
119 ) -> Result<IndexMap<Self::Id, Record<Self::Value>>>
120 where
121 E: Entity<Self::Value>,
122 Self: Sized;
123
124 /// Get a single record by ID as Record value (for ReadableValueSet implementation).
125 ///
126 /// Returns `Ok(None)` when no record exists with the given ID.
127 async fn get_table_value<E>(
128 &self,
129 table: &Table<Self, E>,
130 id: &Self::Id,
131 ) -> Result<Option<Record<Self::Value>>>
132 where
133 E: Entity<Self::Value>,
134 Self: Sized;
135
136 /// Get some data from a table as Record value with ID (for ReadableValueSet implementation)
137 async fn get_table_some_value<E>(
138 &self,
139 table: &Table<Self, E>,
140 ) -> Result<Option<(Self::Id, Record<Self::Value>)>>
141 where
142 E: Entity<Self::Value>,
143 Self: Sized;
144
145 /// Get count of records in the table
146 async fn get_table_count<E>(&self, table: &Table<Self, E>) -> Result<i64>
147 where
148 E: Entity<Self::Value>,
149 Self: Sized;
150
151 /// Get sum of a column in the table (returns native value type)
152 async fn get_table_sum<E>(
153 &self,
154 table: &Table<Self, E>,
155 column: &Self::Column<Self::AnyType>,
156 ) -> Result<Self::Value>
157 where
158 E: Entity<Self::Value>,
159 Self: Sized;
160
161 /// Get maximum value of a column in the table (returns native value type)
162 async fn get_table_max<E>(
163 &self,
164 table: &Table<Self, E>,
165 column: &Self::Column<Self::AnyType>,
166 ) -> Result<Self::Value>
167 where
168 E: Entity<Self::Value>,
169 Self: Sized;
170
171 /// Get minimum value of a column in the table (returns native value type)
172 async fn get_table_min<E>(
173 &self,
174 table: &Table<Self, E>,
175 column: &Self::Column<Self::AnyType>,
176 ) -> Result<Self::Value>
177 where
178 E: Entity<Self::Value>,
179 Self: Sized;
180
181 /// Insert a record as Record value (for WritableValueSet implementation)
182 async fn insert_table_value<E>(
183 &self,
184 table: &Table<Self, E>,
185 id: &Self::Id,
186 record: &Record<Self::Value>,
187 ) -> Result<Record<Self::Value>>
188 where
189 E: Entity<Self::Value>,
190 Self: Sized;
191
192 /// Replace a record as Record value (for WritableValueSet implementation)
193 async fn replace_table_value<E>(
194 &self,
195 table: &Table<Self, E>,
196 id: &Self::Id,
197 record: &Record<Self::Value>,
198 ) -> Result<Record<Self::Value>>
199 where
200 E: Entity<Self::Value>,
201 Self: Sized;
202
203 /// Patch a record as Record value (for WritableValueSet implementation)
204 async fn patch_table_value<E>(
205 &self,
206 table: &Table<Self, E>,
207 id: &Self::Id,
208 partial: &Record<Self::Value>,
209 ) -> Result<Record<Self::Value>>
210 where
211 E: Entity<Self::Value>,
212 Self: Sized;
213
214 /// Delete a record by ID (for WritableValueSet implementation)
215 async fn delete_table_value<E>(&self, table: &Table<Self, E>, id: &Self::Id) -> Result<()>
216 where
217 E: Entity<Self::Value>,
218 Self: Sized;
219
220 /// Delete all records (for WritableValueSet implementation)
221 async fn delete_table_all_values<E>(&self, table: &Table<Self, E>) -> Result<()>
222 where
223 E: Entity<Self::Value>,
224 Self: Sized;
225
226 /// Insert a record and return generated ID (for InsertableValueSet implementation)
227 async fn insert_table_return_id_value<E>(
228 &self,
229 table: &Table<Self, E>,
230 record: &Record<Self::Value>,
231 ) -> Result<Self::Id>
232 where
233 E: Entity<Self::Value>,
234 Self: Sized;
235
236 /// Stream all records from a table as (Id, Record) pairs.
237 ///
238 /// Default implementation wraps `list_table_values` into a stream.
239 /// Backends with native streaming (e.g. REST APIs with pagination)
240 /// can override this to yield records incrementally.
241 #[allow(clippy::type_complexity)]
242 fn stream_table_values<'a, E>(
243 &'a self,
244 table: &Table<Self, E>,
245 ) -> Pin<Box<dyn Stream<Item = Result<(Self::Id, Record<Self::Value>)>> + Send + 'a>>
246 where
247 E: Entity<Self::Value> + 'a,
248 Self: Sized,
249 {
250 let table = table.clone();
251 Box::pin(async_stream::stream! {
252 let records = self.list_table_values(&table).await;
253 match records {
254 Ok(map) => {
255 for item in map {
256 yield Ok(item);
257 }
258 }
259 Err(e) => yield Err(e),
260 }
261 })
262 }
263
264 /// Build a condition for "target_field IN (values of source_column from source_table)".
265 ///
266 /// Used by the relationship traversal system (`get_ref_as`) to filter
267 /// a target table by foreign key values from a source table.
268 ///
269 /// Each backend implements this natively:
270 /// - SQL backends build a subquery: `"id" IN (SELECT "bakery_id" FROM "client" WHERE ...)`
271 /// - MongoDB builds a deferred `{ field: { "$in": [...] } }` document
272 /// - CSV fetches values in memory and builds an IN condition
273 fn related_in_condition<SourceE: Entity<Self::Value> + 'static>(
274 &self,
275 target_field: &str,
276 source_table: &Table<Self, SourceE>,
277 source_column: &str,
278 ) -> Self::Condition
279 where
280 Self: Sized;
281
282 /// Build a correlated condition: `target_table.target_field = source_table.source_column`.
283 ///
284 /// Used by `get_subquery_as` for embedding correlated subqueries inside SELECT
285 /// expressions (e.g. `(SELECT COUNT(*) FROM order WHERE order.client_id = client.id)`).
286 ///
287 /// SQL backends produce table-qualified equality; non-SQL backends may not support
288 /// correlated subqueries and should leave the default (which panics).
289 fn related_correlated_condition(
290 &self,
291 target_table: &str,
292 target_field: &str,
293 source_table: &str,
294 source_column: &str,
295 ) -> Self::Condition {
296 let _ = (target_table, target_field, source_table, source_column);
297 unimplemented!("correlated subqueries not supported by this backend")
298 }
299
300 /// Return an associated expression that, when resolved, yields all values
301 /// of the given typed column from this table (respecting current conditions).
302 ///
303 /// For query-language backends, this can be a subquery expression.
304 /// For simple backends (CSV), this uses a `DeferredFn` that loads data
305 /// and extracts the column values at execution time.
306 ///
307 /// The returned `AssociatedExpression` can be:
308 /// - Executed directly: `.get().await -> Result<Vec<Type>>`
309 /// - Composed into expressions: used via `Expressive` trait in `in_()` conditions
310 ///
311 /// ```rust,ignore
312 /// let fk_col = source.get_column::<String>("bakery_id").unwrap();
313 /// let fk_values = source.data_source().column_table_values_expr(&source, &fk_col);
314 /// // Execute: let ids = fk_values.get().await?;
315 /// // Or compose: target.add_condition(target["id"].in_((fk_values)));
316 /// ```
317 fn column_table_values_expr<'a, E, Type: ColumnType>(
318 &'a self,
319 table: &Table<Self, E>,
320 column: &Self::Column<Type>,
321 ) -> AssociatedExpression<'a, Self, Self::Value, Vec<Type>>
322 where
323 E: Entity<Self::Value> + 'static,
324 Self: ExprDataSource<Self::Value> + Sized;
325}