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}