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