Skip to main content

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}