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/// 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    // ---- Quicksearch -------------------------------------------------------
251
252    /// Apply a quicksearch filter — a single string the driver fans out across
253    /// the columns it considers searchable (typically those flagged
254    /// [`SEARCHABLE`](crate::flags::SEARCHABLE), but each driver decides).
255    ///
256    /// **Replace semantics**: calling `add_search` again wipes the previous
257    /// search filter before applying the new one. Default produces
258    /// `Unimplemented` (when `can_search: true`) or `Unsupported` (when
259    /// `can_search: false`).
260    fn add_search(&mut self, _text: &str) -> Result<()> {
261        Err(self.default_error("add_search", "can_search"))
262    }
263
264    /// Drop the search filter previously applied via
265    /// [`add_search`](Self::add_search). Default mirrors `add_search`.
266    fn clear_search(&mut self) -> Result<()> {
267        Err(self.default_error("clear_search", "can_search"))
268    }
269
270    // ---- Ordering ----------------------------------------------------------
271
272    /// Push a single ORDER BY clause onto the wrapped table.
273    ///
274    /// Vista's `add_order` is replace-semantics: the driver shell should clear
275    /// any previously-set order before pushing the new one. Default produces
276    /// `Unimplemented` (when `can_order: true`) or `Unsupported` (when
277    /// `can_order: false`).
278    fn add_order(&mut self, _field: &str, _dir: SortDirection) -> Result<()> {
279        Err(self.default_error("add_order", "can_order"))
280    }
281
282    /// Wipe every order clause. Default mirrors [`add_order`](Self::add_order).
283    fn clear_orders(&mut self) -> Result<()> {
284        Err(self.default_error("clear_orders", "can_order"))
285    }
286
287    // ---- References --------------------------------------------------------
288
289    /// Resolve a same-persistence relation using a known source row, returning
290    /// the related table as a new `Vista`.
291    ///
292    /// Drivers override by forwarding into the wrapped typed `Table`'s
293    /// `get_ref_from_row::<EmptyEntity>(relation, &native_row)` and then
294    /// wrapping the result back as a `Vista` through the driver's factory.
295    /// The default returns `Unimplemented`. Cross-persistence refs are
296    /// handled one layer up by `vantage-vista-factory`'s `VistaCatalog`,
297    /// never here.
298    fn get_ref(&self, relation: &str, _row: &Record<CborValue>) -> Result<Vista> {
299        Err(error!(
300            format!(
301                "get_ref not implemented for '{}'",
302                std::any::type_name::<Self>()
303            ),
304            method = "get_ref",
305            relation = relation,
306            source_type = std::any::type_name::<Self>()
307        )
308        .mark_unimplemented()
309        .traced())
310    }
311
312    /// Build the **bare** target of a same-persistence relation as a `Vista` —
313    /// the table a new related row would be inserted into, with no join
314    /// condition applied. Used by Vista's nested insert to reach a has-one /
315    /// has-many child's destination.
316    ///
317    /// Drivers override by forwarding into the wrapped typed `Table`'s
318    /// `get_ref_target::<EmptyEntity>(relation)` and wrapping the result back
319    /// through the driver's factory — the same path as [`get_ref`](Self::get_ref)
320    /// minus the row-derived condition. The default returns `Unimplemented`;
321    /// cross-persistence relations are rejected at the `Vista` layer before
322    /// this is reached.
323    fn get_ref_target(&self, relation: &str) -> Result<Vista> {
324        Err(error!(
325            format!(
326                "get_ref_target not implemented for '{}'",
327                std::any::type_name::<Self>()
328            ),
329            method = "get_ref_target",
330            relation = relation,
331            source_type = std::any::type_name::<Self>()
332        )
333        .mark_unimplemented()
334        .traced())
335    }
336
337    /// Contained (embedded-in-row) relations this shell exposes, keyed by name.
338    /// Default empty — only shells that model embedded objects/arrays override.
339    fn contained(&self) -> &IndexMap<String, ContainedSpec> {
340        static EMPTY: std::sync::OnceLock<IndexMap<String, ContainedSpec>> =
341            std::sync::OnceLock::new();
342        EMPTY.get_or_init(IndexMap::new)
343    }
344
345    /// Resolve a contained relation against a known parent `row`, returning the
346    /// embedded records as a sub-`Vista`. Writes to that sub-Vista patch the
347    /// host column of `row`'s record back through the shell. Default returns
348    /// `Unimplemented`; shells override to seed [`crate::build_contained_vista`]
349    /// with a writeback that patches the parent.
350    fn get_contained_ref(&self, relation: &str, _row: &Record<CborValue>) -> Result<Vista> {
351        Err(error!(
352            format!(
353                "get_contained_ref not implemented for '{}'",
354                std::any::type_name::<Self>()
355            ),
356            method = "get_contained_ref",
357            relation = relation,
358            source_type = std::any::type_name::<Self>()
359        )
360        .mark_unimplemented()
361        .traced())
362    }
363
364    /// Names + cardinalities of the shell's same-persistence references.
365    /// Derived from [`references`](Self::references) by default; impls
366    /// should rarely need to override.
367    fn get_ref_kinds(&self) -> Vec<(String, crate::reference::ReferenceKind)> {
368        self.references()
369            .iter()
370            .map(|(name, r)| (name.clone(), r.kind))
371            .collect()
372    }
373
374    // ---- Identity ----------------------------------------------------------
375
376    /// Short human label for the underlying driver (e.g. `"csv"`, `"sqlite"`,
377    /// `"postgres"`, `"mongodb"`). Used for diagnostics and CLI output.
378    /// Drivers should override; the default is a placeholder.
379    fn driver_name(&self) -> &'static str {
380        "unknown"
381    }
382
383    // ---- Scripting ---------------------------------------------------------
384
385    /// Contribute backend-specific vocabulary to a Rhai engine that
386    /// vantage-vista has already seeded with the conventional `Vista` verbs
387    /// (see the `rhai_conventional` module). Backends with an expression engine
388    /// (SurrealDB, SQL) override this to register `ident`/`==`/`fx`/graph
389    /// constructors plus a `with_condition(<backend expr>)` builder that routes
390    /// a boxed native condition through [`add_raw_condition`](Self::add_raw_condition).
391    ///
392    /// Default is a no-op: engine-less datasources (CSV/Mongo/REST) still get
393    /// the conventional verbs and only lose the vendor expression syntax —
394    /// graceful degradation, not all-or-nothing.
395    #[cfg(feature = "rhai")]
396    fn register_rhai_extensions(&self, _engine: &mut rhai::Engine) {}
397
398    // ---- Capability advertisement -----------------------------------------
399
400    fn capabilities(&self) -> &VistaCapabilities;
401
402    /// Look up a capability flag by name. Used by `default_error` to decide
403    /// between `Unsupported` and `Unimplemented`. Drivers don't normally
404    /// need to override this.
405    fn capability_flag(&self, name: &str) -> bool {
406        let caps = self.capabilities();
407        match name {
408            "can_count" => caps.can_count,
409            "can_insert" => caps.can_insert,
410            "can_update" => caps.can_update,
411            "can_delete" => caps.can_delete,
412            "can_subscribe" => caps.can_subscribe,
413            "can_invalidate" => caps.can_invalidate,
414            "can_order" => caps.can_order,
415            "can_search" => caps.can_search,
416            "can_set_page_size" => caps.can_set_page_size,
417            "can_fetch_page" => caps.can_fetch_page,
418            "can_fetch_next" => caps.can_fetch_next,
419            "can_traverse_to_record" => caps.can_traverse_to_record,
420            "can_traverse_to_set" => caps.can_traverse_to_set,
421            "can_build_ref_via_script" => caps.can_build_ref_via_script,
422            _ => false,
423        }
424    }
425
426    /// Build the standard error returned by default trait method impls.
427    ///
428    /// Picks the kind based on the capability flag: a `true` flag means the
429    /// driver advertised support but didn't override the method (placeholder
430    /// → `Unimplemented`); a `false` flag means the driver honestly doesn't
431    /// claim the op (caller should have checked → `Unsupported`).
432    ///
433    /// Both kinds emit a `tracing::error!` at construction with `method`,
434    /// `capability`, `source_type`, and `vista_name` as structured fields.
435    fn default_error(&self, method: &str, capability: &str) -> VantageError {
436        let source_type = std::any::type_name::<Self>();
437        if self.capability_flag(capability) {
438            error!(
439                format!(
440                    "'{}' is advertised as VistaCapability for '{}' but implementation for '{}' is missing",
441                    capability, source_type, method
442                ),
443                method = method,
444                capability = capability,
445                source_type = source_type
446            )
447            .mark_unimplemented().traced()
448        } else {
449            error!(
450                format!(
451                    "'{}' is not supported by '{}'; '{}' refused",
452                    capability, source_type, method
453                ),
454                method = method,
455                capability = capability,
456                source_type = source_type
457            )
458            .mark_unsupported()
459            .traced()
460        }
461    }
462}