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}