vantage_vista/source.rs
1use std::pin::Pin;
2
3use async_trait::async_trait;
4use ciborium::Value as CborValue;
5use futures_core::Stream;
6use indexmap::IndexMap;
7use vantage_core::{Result, VantageError, error};
8use vantage_types::Record;
9
10use crate::{
11 capabilities::VistaCapabilities,
12 column::Column,
13 reference::{ContainedSpec, Reference},
14 sort::SortDirection,
15 vista::Vista,
16};
17
18/// Per-driver executor for a `Vista`.
19///
20/// Implementations live in driver crates (vantage-sqlite, vantage-mongodb,
21/// vantage-aws, etc.). Each method receives `&Vista` so the driver can read
22/// the current condition state, columns, and other metadata.
23///
24/// `Id = String` and `Value = ciborium::Value` at this boundary, so every
25/// driver's native id (Mongo `ObjectId`, Surreal `Thing`, …) stringifies
26/// here. Methods are named with the `_vista_` infix to mirror
27/// `TableSource`'s `_table_` convention; `Vista`'s `ValueSet` impls
28/// delegate by stripping the infix.
29///
30/// `id: &String` (rather than `&str`) is intentional: the upstream
31/// `vantage_dataset::ValueSet` trait family fixes `Id = String` and uses
32/// `&Self::Id` in its signatures, so impls receive `&String` and forward
33/// it through unchanged.
34#[async_trait]
35#[allow(clippy::ptr_arg)]
36pub trait TableShell: Send + Sync + 'static {
37 // ---- Schema --------------------------------------------------------------
38 //
39 // The shell owns the schema. `Vista` is a thin wrapper that forwards its
40 // metadata accessors here. No defaults — every impl must answer (an empty
41 // schema is a deliberate choice the impl declares explicitly).
42
43 fn columns(&self) -> &IndexMap<String, Column>;
44
45 fn references(&self) -> &IndexMap<String, Reference>;
46
47 fn id_column(&self) -> Option<&str>;
48
49 // ---- ReadableValueSet delegates ----------------------------------------
50
51 async fn list_vista_values(&self, vista: &Vista)
52 -> Result<IndexMap<String, Record<CborValue>>>;
53
54 async fn get_vista_value(
55 &self,
56 vista: &Vista,
57 id: &String,
58 ) -> Result<Option<Record<CborValue>>>;
59
60 /// Fetch one record by id, with the caller's existing (cheap) record
61 /// available to drivers that can use it (e.g. a cmd detail script reading
62 /// list-pass columns). The default ignores `row` and delegates to
63 /// [`get_vista_value`](Self::get_vista_value); only drivers that benefit
64 /// override it.
65 async fn get_vista_value_with_row(
66 &self,
67 vista: &Vista,
68 id: &String,
69 _row: &Record<CborValue>,
70 ) -> Result<Option<Record<CborValue>>> {
71 self.get_vista_value(vista, id).await
72 }
73
74 async fn get_vista_some_value(
75 &self,
76 vista: &Vista,
77 ) -> Result<Option<(String, Record<CborValue>)>>;
78
79 /// Default implementation wraps `list_vista_values`. Drivers with native
80 /// streaming (cursor-based queries, paginated REST APIs) override.
81 #[allow(clippy::type_complexity)]
82 fn stream_vista_values<'a>(
83 &'a self,
84 vista: &'a Vista,
85 ) -> Pin<Box<dyn Stream<Item = Result<(String, Record<CborValue>)>> + Send + 'a>>
86 where
87 Self: Sync,
88 {
89 Box::pin(async_stream::stream! {
90 match self.list_vista_values(vista).await {
91 Ok(map) => {
92 for item in map {
93 yield Ok(item);
94 }
95 }
96 Err(e) => yield Err(e),
97 }
98 })
99 }
100
101 // ---- WritableValueSet delegates ----------------------------------------
102 //
103 // Default impls return a typed VantageError via `default_error` — drivers
104 // override only what they actually support. The matching `VistaCapabilities`
105 // flag must be set to `true` for any method the driver implements; if the
106 // flag is `true` but the trait method falls through to the default,
107 // `default_error` produces an `Unimplemented`-kind error (placeholder
108 // detected). If the flag is `false`, it produces `Unsupported`. Both are
109 // emitted as tracing events at construction.
110
111 async fn insert_vista_value(
112 &self,
113 _vista: &Vista,
114 _id: &String,
115 _record: &Record<CborValue>,
116 ) -> Result<Record<CborValue>> {
117 Err(self.default_error("insert_vista_value", "can_insert"))
118 }
119
120 async fn replace_vista_value(
121 &self,
122 _vista: &Vista,
123 _id: &String,
124 _record: &Record<CborValue>,
125 ) -> Result<Record<CborValue>> {
126 Err(self.default_error("replace_vista_value", "can_update"))
127 }
128
129 async fn patch_vista_value(
130 &self,
131 _vista: &Vista,
132 _id: &String,
133 _partial: &Record<CborValue>,
134 ) -> Result<Record<CborValue>> {
135 Err(self.default_error("patch_vista_value", "can_update"))
136 }
137
138 async fn delete_vista_value(&self, _vista: &Vista, _id: &String) -> Result<()> {
139 Err(self.default_error("delete_vista_value", "can_delete"))
140 }
141
142 async fn delete_vista_all_values(&self, _vista: &Vista) -> Result<()> {
143 Err(self.default_error("delete_vista_all_values", "can_delete"))
144 }
145
146 // ---- InsertableValueSet delegate ---------------------------------------
147
148 async fn insert_vista_return_id_value(
149 &self,
150 _vista: &Vista,
151 _record: &Record<CborValue>,
152 ) -> Result<String> {
153 Err(self.default_error("insert_vista_return_id_value", "can_insert"))
154 }
155
156 // ---- Aggregates --------------------------------------------------------
157
158 /// Default impl falls back to `list_vista_values` — drivers with native
159 /// count (`SELECT COUNT(*)`, etc.) override.
160 async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
161 Ok(self.list_vista_values(vista).await?.len() as i64)
162 }
163
164 // ---- Conditions --------------------------------------------------------
165
166 /// Translate `field == value` into the driver's native condition type and
167 /// apply it to the wrapped table. The default impl returns `Unimplemented`
168 /// — every driver is expected to override.
169 ///
170 /// `value` is the universal CBOR carrier; the driver picks the appropriate
171 /// translation (e.g. `cbor_to_bson` for Mongo, `cbor → AnyCsvType` for CSV).
172 fn add_eq_condition(&mut self, _field: &str, _value: &CborValue) -> Result<()> {
173 Err(error!(
174 format!(
175 "add_eq_condition not implemented for '{}'",
176 std::any::type_name::<Self>()
177 ),
178 method = "add_eq_condition",
179 source_type = std::any::type_name::<Self>()
180 )
181 .mark_unimplemented()
182 .traced())
183 }
184
185 /// Push a driver-native condition into the wrapped table. The
186 /// caller boxes the condition as `dyn Any` and the driver
187 /// downcasts to its own `T::Condition`. Used by YAML-driven
188 /// relation traversal, where the factory constructs a
189 /// `DeferredFn`-bearing condition outside the value-set surface
190 /// (which only accepts scalar eq) and pushes it through this
191 /// channel. Default is `Unimplemented`.
192 fn add_raw_condition(
193 &mut self,
194 _condition: Box<dyn std::any::Any + Send + Sync>,
195 ) -> Result<()> {
196 Err(error!(
197 format!(
198 "add_raw_condition not implemented for '{}'",
199 std::any::type_name::<Self>()
200 ),
201 method = "add_raw_condition",
202 source_type = std::any::type_name::<Self>()
203 )
204 .mark_unimplemented()
205 .traced())
206 }
207
208 // ---- Pagination --------------------------------------------------------
209
210 /// Declare how many records constitute one page. Used by both
211 /// [`fetch_page`](Self::fetch_page) and [`fetch_next`](Self::fetch_next).
212 /// Default returns `default_error("set_page_size", "can_set_page_size")`.
213 fn set_page_size(&mut self, _size: usize) -> Result<()> {
214 Err(self.default_error("set_page_size", "can_set_page_size"))
215 }
216
217 /// Fetch a specific page (1-based) using offset-style pagination. The
218 /// per-page count comes from the most recent
219 /// [`set_page_size`](Self::set_page_size).
220 ///
221 /// Drivers without random-access pagination (DynamoDB, most token-paginated
222 /// REST APIs) leave the default in place, which produces `Unsupported`.
223 /// Callers should branch on `vista.capabilities().can_fetch_page` first.
224 async fn fetch_page(
225 &self,
226 _vista: &Vista,
227 _page: usize,
228 ) -> Result<Vec<(String, Record<CborValue>)>> {
229 Err(self.default_error("fetch_page", "can_fetch_page"))
230 }
231
232 /// Cursor-style chain fetch. Pass `None` on the first call; pass the
233 /// previous call's returned token on subsequent calls. Returned token is
234 /// `None` when the result set is exhausted.
235 ///
236 /// The token is **driver-private** — its shape is whatever the backend
237 /// finds convenient (DynamoDB `LastEvaluatedKey` as a CBOR map, REST
238 /// `nextToken` as `CborValue::Text`, offset-based as `CborValue::Integer`).
239 /// Consumers treat it as opaque and round-trip it back unchanged.
240 ///
241 /// Default returns `default_error("fetch_next", "can_fetch_next")`.
242 async fn fetch_next(
243 &self,
244 _vista: &Vista,
245 _token: Option<CborValue>,
246 ) -> Result<(Vec<(String, Record<CborValue>)>, Option<CborValue>)> {
247 Err(self.default_error("fetch_next", "can_fetch_next"))
248 }
249
250 /// Fetch the half-open row window `[offset, offset + limit)` in the
251 /// source's natural order. Offset-style like [`fetch_page`](Self::fetch_page)
252 /// but addressed by absolute row index rather than page number, so it
253 /// maps directly onto a diorama `on_load_chunk` `Range<usize>` — which
254 /// is *not* guaranteed page-aligned. This is the primitive a paged,
255 /// lazily-loaded grid drives on scroll.
256 ///
257 /// Drivers leave the default in place (producing `Unsupported`) until
258 /// they implement it; callers branch on
259 /// `vista.capabilities().can_fetch_window` first. Default returns
260 /// `default_error("fetch_window", "can_fetch_window")`.
261 async fn fetch_window(
262 &self,
263 _vista: &Vista,
264 _offset: usize,
265 _limit: usize,
266 ) -> Result<Vec<(String, Record<CborValue>)>> {
267 Err(self.default_error("fetch_window", "can_fetch_window"))
268 }
269
270 // ---- Quicksearch -------------------------------------------------------
271
272 /// Apply a quicksearch filter — a single string the driver fans out across
273 /// the columns it considers searchable (typically those flagged
274 /// [`SEARCHABLE`](crate::flags::SEARCHABLE), but each driver decides).
275 ///
276 /// **Replace semantics**: calling `add_search` again wipes the previous
277 /// search filter before applying the new one. Default produces
278 /// `Unimplemented` (when `can_search: true`) or `Unsupported` (when
279 /// `can_search: false`).
280 fn add_search(&mut self, _text: &str) -> Result<()> {
281 Err(self.default_error("add_search", "can_search"))
282 }
283
284 /// Drop the search filter previously applied via
285 /// [`add_search`](Self::add_search). Default mirrors `add_search`.
286 fn clear_search(&mut self) -> Result<()> {
287 Err(self.default_error("clear_search", "can_search"))
288 }
289
290 // ---- Ordering ----------------------------------------------------------
291
292 /// Push a single ORDER BY clause onto the wrapped table.
293 ///
294 /// Vista's `add_order` is replace-semantics: the driver shell should clear
295 /// any previously-set order before pushing the new one. Default produces
296 /// `Unimplemented` (when `can_order: true`) or `Unsupported` (when
297 /// `can_order: false`).
298 fn add_order(&mut self, _field: &str, _dir: SortDirection) -> Result<()> {
299 Err(self.default_error("add_order", "can_order"))
300 }
301
302 /// Wipe every order clause. Default mirrors [`add_order`](Self::add_order).
303 fn clear_orders(&mut self) -> Result<()> {
304 Err(self.default_error("clear_orders", "can_order"))
305 }
306
307 // ---- Cloning -----------------------------------------------------------
308
309 /// Produce an independent copy of this shell, or `None` if the driver can't
310 /// be cloned cheaply. The copy must share the backing store / connection
311 /// (typically `Arc`) but own its own query state (conditions / order /
312 /// search) so a caller can narrow it — set an ORDER BY, add a WHERE — without
313 /// disturbing the original. This is how a consumer builds a per-view ordered
314 /// Vista to fetch from: `clone_shell()` → `add_order(...)` → `fetch_window`.
315 ///
316 /// Default `None`: drivers opt in only where a clone is genuinely cheap
317 /// (query state is small; the store is `Arc`-shared). Callers that get `None`
318 /// fall back to reading the shared shell and ordering client-side.
319 fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
320 None
321 }
322
323 // ---- References --------------------------------------------------------
324
325 /// Resolve a same-persistence relation using a known source row, returning
326 /// the related table as a new `Vista`.
327 ///
328 /// Drivers override by forwarding into the wrapped typed `Table`'s
329 /// `get_ref_from_row::<EmptyEntity>(relation, &native_row)` and then
330 /// wrapping the result back as a `Vista` through the driver's factory.
331 /// The default returns `Unimplemented`. Cross-persistence refs are
332 /// handled one layer up by `vantage-vista-factory`'s `VistaCatalog`,
333 /// never here.
334 fn get_ref(&self, relation: &str, _row: &Record<CborValue>) -> Result<Vista> {
335 Err(error!(
336 format!(
337 "get_ref not implemented for '{}'",
338 std::any::type_name::<Self>()
339 ),
340 method = "get_ref",
341 relation = relation,
342 source_type = std::any::type_name::<Self>()
343 )
344 .mark_unimplemented()
345 .traced())
346 }
347
348 /// Build the **bare** target of a same-persistence relation as a `Vista` —
349 /// the table a new related row would be inserted into, with no join
350 /// condition applied. Used by Vista's nested insert to reach a has-one /
351 /// has-many child's destination.
352 ///
353 /// Drivers override by forwarding into the wrapped typed `Table`'s
354 /// `get_ref_target::<EmptyEntity>(relation)` and wrapping the result back
355 /// through the driver's factory — the same path as [`get_ref`](Self::get_ref)
356 /// minus the row-derived condition. The default returns `Unimplemented`;
357 /// cross-persistence relations are rejected at the `Vista` layer before
358 /// this is reached.
359 fn get_ref_target(&self, relation: &str) -> Result<Vista> {
360 Err(error!(
361 format!(
362 "get_ref_target not implemented for '{}'",
363 std::any::type_name::<Self>()
364 ),
365 method = "get_ref_target",
366 relation = relation,
367 source_type = std::any::type_name::<Self>()
368 )
369 .mark_unimplemented()
370 .traced())
371 }
372
373 /// Contained (embedded-in-row) relations this shell exposes, keyed by name.
374 /// Default empty — only shells that model embedded objects/arrays override.
375 fn contained(&self) -> &IndexMap<String, ContainedSpec> {
376 static EMPTY: std::sync::OnceLock<IndexMap<String, ContainedSpec>> =
377 std::sync::OnceLock::new();
378 EMPTY.get_or_init(IndexMap::new)
379 }
380
381 /// Resolve a contained relation against a known parent `row`, returning the
382 /// embedded records as a sub-`Vista`. Writes to that sub-Vista patch the
383 /// host column of `row`'s record back through the shell. Default returns
384 /// `Unimplemented`; shells override to seed [`crate::build_contained_vista`]
385 /// with a writeback that patches the parent.
386 fn get_contained_ref(&self, relation: &str, _row: &Record<CborValue>) -> Result<Vista> {
387 Err(error!(
388 format!(
389 "get_contained_ref not implemented for '{}'",
390 std::any::type_name::<Self>()
391 ),
392 method = "get_contained_ref",
393 relation = relation,
394 source_type = std::any::type_name::<Self>()
395 )
396 .mark_unimplemented()
397 .traced())
398 }
399
400 /// Names + cardinalities of the shell's same-persistence references.
401 /// Derived from [`references`](Self::references) by default; impls
402 /// should rarely need to override.
403 fn get_ref_kinds(&self) -> Vec<(String, crate::reference::ReferenceKind)> {
404 self.references()
405 .iter()
406 .map(|(name, r)| (name.clone(), r.kind))
407 .collect()
408 }
409
410 // ---- Identity ----------------------------------------------------------
411
412 /// Short human label for the underlying driver (e.g. `"csv"`, `"sqlite"`,
413 /// `"postgres"`, `"mongodb"`). Used for diagnostics and CLI output.
414 /// Drivers should override; the default is a placeholder.
415 fn driver_name(&self) -> &'static str {
416 "unknown"
417 }
418
419 // ---- Scripting ---------------------------------------------------------
420
421 /// Contribute backend-specific vocabulary to a Rhai engine that
422 /// vantage-vista has already seeded with the conventional `Vista` verbs
423 /// (see the `rhai_conventional` module). Backends with an expression engine
424 /// (SurrealDB, SQL) override this to register `ident`/`==`/`fx`/graph
425 /// constructors plus a `with_condition(<backend expr>)` builder that routes
426 /// a boxed native condition through [`add_raw_condition`](Self::add_raw_condition).
427 ///
428 /// Default is a no-op: engine-less datasources (CSV/Mongo/REST) still get
429 /// the conventional verbs and only lose the vendor expression syntax —
430 /// graceful degradation, not all-or-nothing.
431 #[cfg(feature = "rhai")]
432 fn register_rhai_extensions(&self, _engine: &mut rhai::Engine) {}
433
434 // ---- Capability advertisement -----------------------------------------
435
436 fn capabilities(&self) -> &VistaCapabilities;
437
438 /// Look up a capability flag by name. Used by `default_error` to decide
439 /// between `Unsupported` and `Unimplemented`. Drivers don't normally
440 /// need to override this.
441 fn capability_flag(&self, name: &str) -> bool {
442 let caps = self.capabilities();
443 match name {
444 "can_count" => caps.can_count,
445 "can_insert" => caps.can_insert,
446 "can_update" => caps.can_update,
447 "can_delete" => caps.can_delete,
448 "can_subscribe" => caps.can_subscribe,
449 "can_invalidate" => caps.can_invalidate,
450 "can_order" => caps.can_order,
451 "can_search" => caps.can_search,
452 "can_set_page_size" => caps.can_set_page_size,
453 "can_fetch_page" => caps.can_fetch_page,
454 "can_fetch_next" => caps.can_fetch_next,
455 "can_traverse_to_record" => caps.can_traverse_to_record,
456 "can_traverse_to_set" => caps.can_traverse_to_set,
457 "can_build_ref_via_script" => caps.can_build_ref_via_script,
458 _ => false,
459 }
460 }
461
462 /// Build the standard error returned by default trait method impls.
463 ///
464 /// Picks the kind based on the capability flag: a `true` flag means the
465 /// driver advertised support but didn't override the method (placeholder
466 /// → `Unimplemented`); a `false` flag means the driver honestly doesn't
467 /// claim the op (caller should have checked → `Unsupported`).
468 ///
469 /// Only the `Unimplemented` kind traces at error level — it's a driver
470 /// bug. An `Unsupported` refusal is a legitimate answer to a caller
471 /// probing a capability (e.g. an exploratory data script calling
472 /// `set_page_size` on a cache-mode vista): the error value carries the
473 /// full message to the caller, so it logs at debug only.
474 fn default_error(&self, method: &str, capability: &str) -> VantageError {
475 let source_type = std::any::type_name::<Self>();
476 if self.capability_flag(capability) {
477 error!(
478 format!(
479 "'{}' is advertised as VistaCapability for '{}' but implementation for '{}' is missing",
480 capability, source_type, method
481 ),
482 method = method,
483 capability = capability,
484 source_type = source_type
485 )
486 .mark_unimplemented().traced()
487 } else {
488 error!(
489 format!(
490 "'{}' is not supported by '{}'; '{}' refused",
491 capability, source_type, method
492 ),
493 method = method,
494 capability = capability,
495 source_type = source_type
496 )
497 .mark_unsupported()
498 .traced_debug()
499 }
500 }
501}