Skip to main content

vantage_vista/
shell.rs

1//! [`TableShell`]: the per-driver executor behind a [`Vista`].
2
3use async_trait::async_trait;
4use ciborium::Value as CborValue;
5use indexmap::IndexMap;
6use vantage_core::Result;
7#[cfg(feature = "rhai")]
8use vantage_rhai::rhai;
9use vantage_types::Record;
10
11use crate::{
12    capabilities::{Capability, VistaCapabilities, unsupported},
13    change::VistaChangeStream,
14    metadata::VistaMetadata,
15    page::{NativeCondition, Page, Preview},
16    query::Condition,
17    sort::SortDirection,
18    vista::Vista,
19};
20
21type Rec = Record<CborValue>;
22
23/// The driver side of a [`Vista`]: schema, reads, narrowing and writes over
24/// one table of one backend.
25///
26/// A shell owns its query state. `Vista` narrows an owned clone through the
27/// `&mut self` methods, so a shell's `Clone` must carry its current narrowing
28/// and own it independently, while sharing the store or connection. Every
29/// read and write runs over the shell's current narrowing.
30///
31/// A method with a default answers [`unsupported`] with the capability that
32/// names it; a shell that implements the method advertises that capability in
33/// [`capabilities`](Self::capabilities). See [`Capability`] for the mapping.
34///
35/// Ids are strings at this boundary, and values are CBOR: every driver's
36/// native id (Mongo `ObjectId`, Surreal `Thing`, …) stringifies here.
37#[async_trait]
38pub trait TableShell: CloneTableShell + Send + Sync + 'static {
39    // ---- Schema -------------------------------------------------------------
40
41    /// The set's columns, references and id column. `Vista` reads its
42    /// metadata vocabulary (`columns`, `column`, `id_column`, …) from here.
43    fn metadata(&self) -> &VistaMetadata;
44    fn capabilities(&self) -> &VistaCapabilities;
45    /// Short label for the driver (`"csv"`, `"sqlite"`, `"mongodb"`).
46    fn driver_name(&self) -> &'static str;
47
48    /// The query the next read would send, rendered without sending it.
49    /// Synchronous and performs no I/O: a value known only at fetch time
50    /// renders as a placeholder. Lossy is acceptable, wrong is not.
51    /// See [`Preview`] for what `query` and `note` each hold.
52    fn preview(&self) -> Preview;
53
54    // ---- Reads --------------------------------------------------------------
55
56    /// Every row of the set, keyed by id, in the set's order.
57    async fn list(&self) -> Result<IndexMap<String, Rec>>;
58    /// Row `id` when it is in the set.
59    async fn get(&self, id: &str) -> Result<Option<Rec>>;
60    async fn count(&self) -> Result<i64>;
61    /// Rows `[offset, offset + limit)` of the set, in its order.
62    ///
63    /// The leading window (`offset == 0`) is required of every shell whose
64    /// [`list`](Self::list) works: it is how a capped read and
65    /// `get_some_value` read. A shell that can't push a window to its
66    /// backend answers it with [`Page::from_listing`] over its listed rows.
67    /// [`Window`](Capability::Window) means any offset; `Vista` refuses
68    /// `offset > 0` without it before the shell is called.
69    async fn fetch_window(&self, offset: usize, limit: usize) -> Result<Page>;
70
71    /// Up to `limit` rows after `token` (`None` starts at the beginning) and
72    /// the token for the next call, `None` once the set is exhausted
73    /// ([`Cursor`](Capability::Cursor)). The token is driver-private; callers
74    /// hand it back unchanged. `Vista` refuses the call without `Cursor`
75    /// before the shell is called.
76    async fn fetch_cursor(
77        &self,
78        _token: Option<CborValue>,
79        _limit: usize,
80    ) -> Result<(Page, Option<CborValue>)> {
81        unsupported(Capability::Cursor)
82    }
83
84    /// Subscribe to changes on the set ([`Subscribe`](Capability::Subscribe)).
85    ///
86    /// Delivery is best-effort, and four promises hold for every driver:
87    /// 1. the stream may be coarser than the set (table-wide is valid);
88    /// 2. so a payload row may fall outside the set: either the driver
89    ///    reconciles (re-reads the id through the set, reporting `Deleted`)
90    ///    or the consumer must;
91    /// 3. [`Invalidated`](crate::VistaChange::Invalidated) means "re-read
92    ///    everything";
93    /// 4. the stream ending is normal; consumers resubscribe and reconcile.
94    ///
95    /// Advertise `Subscribe` only when the driver pushes with no further
96    /// setup, or the application declared the setup done: a stream that stays
97    /// silent can't be told apart from nothing happening. `Vista` refuses
98    /// the call without `Subscribe` before the shell is called.
99    async fn watch(&self) -> Result<VistaChangeStream> {
100        unsupported(Capability::Subscribe)
101    }
102
103    // ---- Narrowing ----------------------------------------------------------
104    //
105    // Error kinds matter here: `Vista::with_query` keeps a clause for
106    // local evaluation only when it is refused as `Unsupported` (or
107    // `Unimplemented`). A clause the shell could take but finds wrong (an
108    // unknown column, a value it can't convert) answers a normal error, which
109    // the caller sees. A shell that errors leaves its narrowing unchanged.
110
111    /// Narrow the set by `condition`. Every shell takes equality; `Vista`
112    /// refuses other operators unless the shell advertises
113    /// [`FilterOps`](Capability::FilterOps). An unknown column or a bad value
114    /// is a normal error, not `Unsupported`.
115    ///
116    /// A dotted column (`root.leaf`) names a field nested in a declared
117    /// root's value. The shell either evaluates it (narrowing to the rows
118    /// whose `root` holds a matching `leaf`) or rejects it with a normal
119    /// error. It never accepts the path and matches nothing.
120    fn add_condition(&mut self, condition: &Condition) -> Result<()>;
121
122    /// Narrow the set by a condition in the driver's own type
123    /// ([`NativeConditions`](Capability::NativeConditions)). A condition of
124    /// a type the driver doesn't take is an `IncorrectUsage` error.
125    fn add_native_condition(&mut self, _condition: NativeCondition) -> Result<()> {
126        unsupported(Capability::NativeConditions)
127    }
128
129    /// Replace the quicksearch text; `None` clears it
130    /// ([`Search`](Capability::Search)). Quicksearch reads the columns
131    /// flagged `Searchable`, or every column when none is
132    /// ([`VistaMetadata::search_columns`](crate::VistaMetadata::search_columns));
133    /// a shell may exclude columns it can't filter on (dotted paths, imported
134    /// columns). Text the backend can't express is a normal error, not
135    /// `Unsupported`.
136    fn set_search(&mut self, _text: Option<&str>) -> Result<()> {
137        unsupported(Capability::Search)
138    }
139
140    /// Replace the order; `None` clears it ([`Order`](Capability::Order)).
141    /// An unknown column is a normal error, not `Unsupported`.
142    fn set_order(&mut self, _order: Option<(&str, SortDirection)>) -> Result<()> {
143        unsupported(Capability::Order)
144    }
145
146    // ---- Writes -------------------------------------------------------------
147    //
148    // Writes follow the write contract on `vantage_dataset::WritableValueSet`
149    // over the shell's narrowing. A shell that keeps every write inside a
150    // narrowed set advertises `ConfineWrites`; `Vista` refuses writes through
151    // a narrowed handle whose shell doesn't.
152
153    /// Insert `record` as row `id`, or under an id the backend makes when
154    /// `id` is `None` (not retry-safe). Returns the id and the stored row.
155    /// An id already in the set returns the stored row untouched; an id held
156    /// outside the set is `Conflict`; a new row is conformed to the set's
157    /// equality conditions.
158    async fn insert(&self, _id: Option<&str>, _record: &Rec) -> Result<(String, Rec)> {
159        unsupported(Capability::Insert)
160    }
161
162    /// Replace row `id`, creating it when missing; an id held outside the set
163    /// is `Conflict`.
164    async fn replace(&self, _id: &str, _record: &Rec) -> Result<Rec> {
165        unsupported(Capability::Update)
166    }
167
168    /// Insert row `id`, or replace it when it exists, in one operation.
169    async fn upsert(&self, _id: &str, _record: &Rec) -> Result<Rec> {
170        unsupported(Capability::Update)
171    }
172
173    /// Merge `partial` into row `id`. A row outside the set or missing is
174    /// `NotFound`; a result that leaves the set is `Conflict`.
175    async fn patch(&self, _id: &str, _partial: &Rec) -> Result<Rec> {
176        unsupported(Capability::Update)
177    }
178
179    /// Remove row `id` when it is in the set; `Ok` whether it was in the set,
180    /// outside it, or missing.
181    async fn delete(&self, _id: &str) -> Result<()> {
182        unsupported(Capability::Delete)
183    }
184
185    /// Remove exactly the rows in the set.
186    async fn delete_all(&self) -> Result<()> {
187        unsupported(Capability::Delete)
188    }
189
190    /// Store `rows` in one all-or-nothing, driver-native operation and return
191    /// how many were newly inserted; an id already held counts zero
192    /// ([`Import`](Capability::Import)). A driver that can't make the batch
193    /// atomic, or can't tell new rows from existing ones, doesn't advertise
194    /// `Import`; callers then insert row by row.
195    async fn import(&self, _rows: &IndexMap<String, Rec>) -> Result<usize> {
196        unsupported(Capability::Import)
197    }
198
199    // ---- References ---------------------------------------------------------
200
201    /// The target of `relation` ([`Traverse`](Capability::Traverse)): with a
202    /// parent `row` (from this shell's set), the related rows; with `None`,
203    /// the bare target a new related row is inserted into. Contained
204    /// relations route through here too: from a row, the records it embeds;
205    /// with `None`, the contained schema with no records and no write
206    /// capabilities, since there is no parent row to write into.
207    ///
208    /// Narrow a target for a row through `Vista::add_condition` (or
209    /// `with_condition`, `with_native_condition`), never through the target
210    /// shell directly, so that the returned Vista's `query()` records the
211    /// join and `is_narrowed()` holds.
212    fn get_ref(&self, _relation: &str, _row: Option<&Rec>) -> Result<Vista> {
213        unsupported(Capability::Traverse)
214    }
215
216    // ---- Scripting ----------------------------------------------------------
217
218    /// Register backend vocabulary on a Rhai engine alongside vantage-vista's
219    /// `DataVocab`: expression constructors, a `with_condition(<expr>)` verb
220    /// routed through [`add_native_condition`](Self::add_native_condition),
221    /// and any per-evaluation constants. A backend with no expression engine
222    /// registers nothing; scripts still get the table handle verbs.
223    #[cfg(feature = "rhai")]
224    fn register_rhai_extensions(&self, _engine: &mut rhai::Engine) {}
225}
226
227/// Boxed cloning for [`TableShell`], so `Box<dyn TableShell>` is `Clone`.
228/// Every `TableShell + Clone` has it; a shell implements `Clone` and gets
229/// this for free. The trait is sealed: a clone must come from `Clone`, so
230/// it carries the shell's narrowing.
231pub trait CloneTableShell: sealed::Sealed {
232    #[doc(hidden)]
233    fn clone_boxed(&self) -> Box<dyn TableShell>;
234}
235
236mod sealed {
237    pub trait Sealed {}
238    impl<T: super::TableShell + Clone> Sealed for T {}
239}
240
241impl<T: TableShell + Clone> CloneTableShell for T {
242    fn clone_boxed(&self) -> Box<dyn TableShell> {
243        Box::new(self.clone())
244    }
245}
246
247impl Clone for Box<dyn TableShell> {
248    fn clone(&self) -> Self {
249        (**self).clone_boxed()
250    }
251}