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