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