vantage_vista/source.rs
1use std::pin::Pin;
2#[cfg(feature = "rhai")]
3use vantage_rhai::rhai;
4
5use async_trait::async_trait;
6use ciborium::Value as CborValue;
7use futures_core::Stream;
8use indexmap::IndexMap;
9use vantage_core::{Result, VantageError, error};
10use vantage_types::Record;
11
12use crate::{
13 aggregate::AggregateSpec,
14 capabilities::VistaCapabilities,
15 column::Column,
16 reference::{ContainedSpec, Reference},
17 sort::SortDirection,
18 vista::Vista,
19};
20
21/// A single change observed on the underlying set by a live subscription.
22///
23/// This is the *push* counterpart to `list_vista_values`: a driver whose
24/// backend can stream changes (SurrealDB LIVE, Postgres `LISTEN/NOTIFY`,
25/// a Mongo change stream) emits one of these per affected row. The `value`
26/// carried on `Inserted`/`Updated` is the record in the **same projected,
27/// id-keyed shape** the driver returns from `list_vista_values`, so consumers
28/// can drop it straight into a cache without re-reading.
29#[derive(Debug, Clone)]
30pub enum VistaChange {
31 /// A row entered the set.
32 Inserted {
33 id: String,
34 value: Record<CborValue>,
35 },
36 /// An existing row's contents changed.
37 Updated {
38 id: String,
39 value: Record<CborValue>,
40 },
41 /// A row left the set.
42 Deleted { id: String },
43 /// "Something changed, but I can't say what" — a coarse invalidation. The
44 /// consumer should reconcile by re-reading the whole set. This is what a
45 /// payload-less push (Postgres `LISTEN/NOTIFY`) can offer; drivers that
46 /// carry the row (SurrealDB LIVE) emit the fine-grained variants instead.
47 Invalidated,
48}
49
50impl VistaChange {
51 /// The id of the affected row, or `None` for a coarse [`Invalidated`](Self::Invalidated).
52 pub fn id(&self) -> Option<&str> {
53 match self {
54 VistaChange::Inserted { id, .. }
55 | VistaChange::Updated { id, .. }
56 | VistaChange::Deleted { id } => Some(id),
57 VistaChange::Invalidated => None,
58 }
59 }
60}
61
62/// A stream of [`VistaChange`]s from a live subscription. `'static` and `Send`
63/// so it can be handed to a background task.
64pub type VistaChangeStream = Pin<Box<dyn Stream<Item = Result<VistaChange>> + Send>>;
65
66/// Per-driver executor for a `Vista`.
67///
68/// Implementations live in driver crates (vantage-sqlite, vantage-mongodb,
69/// vantage-aws, etc.). Each method receives `&Vista` so the driver can read
70/// the current condition state, columns, and other metadata.
71///
72/// `Id = String` and `Value = ciborium::Value` at this boundary, so every
73/// driver's native id (Mongo `ObjectId`, Surreal `Thing`, …) stringifies
74/// here. Methods are named with the `_vista_` infix to mirror
75/// `TableSource`'s `_table_` convention; `Vista`'s `ValueSet` impls
76/// delegate by stripping the infix.
77///
78/// `id: &String` (rather than `&str`) is intentional: the upstream
79/// `vantage_dataset::ValueSet` trait family fixes `Id = String` and uses
80/// `&Self::Id` in its signatures, so impls receive `&String` and forward
81/// it through unchanged.
82#[async_trait]
83#[allow(clippy::ptr_arg)]
84pub trait TableShell: Send + Sync + 'static {
85 // ---- Schema --------------------------------------------------------------
86 //
87 // The shell owns the schema. `Vista` is a thin wrapper that forwards its
88 // metadata accessors here. No defaults — every impl must answer (an empty
89 // schema is a deliberate choice the impl declares explicitly).
90
91 fn columns(&self) -> &IndexMap<String, Column>;
92
93 fn references(&self) -> &IndexMap<String, Reference>;
94
95 fn id_column(&self) -> Option<&str>;
96
97 // ---- ReadableValueSet delegates ----------------------------------------
98
99 async fn list_vista_values(&self, vista: &Vista)
100 -> Result<IndexMap<String, Record<CborValue>>>;
101
102 async fn get_vista_value(
103 &self,
104 vista: &Vista,
105 id: &String,
106 ) -> Result<Option<Record<CborValue>>>;
107
108 /// Fetch one record by id, with the caller's existing (cheap) record
109 /// available to drivers that can use it (e.g. a cmd detail script reading
110 /// list-pass columns). The default ignores `row` and delegates to
111 /// [`get_vista_value`](Self::get_vista_value); only drivers that benefit
112 /// override it.
113 async fn get_vista_value_with_row(
114 &self,
115 vista: &Vista,
116 id: &String,
117 _row: &Record<CborValue>,
118 ) -> Result<Option<Record<CborValue>>> {
119 self.get_vista_value(vista, id).await
120 }
121
122 async fn get_vista_some_value(
123 &self,
124 vista: &Vista,
125 ) -> Result<Option<(String, Record<CborValue>)>>;
126
127 /// Default implementation wraps `list_vista_values`. Drivers with native
128 /// streaming (cursor-based queries, paginated REST APIs) override.
129 #[allow(clippy::type_complexity)]
130 fn stream_vista_values<'a>(
131 &'a self,
132 vista: &'a Vista,
133 ) -> Pin<Box<dyn Stream<Item = Result<(String, Record<CborValue>)>> + Send + 'a>>
134 where
135 Self: Sync,
136 {
137 Box::pin(async_stream::stream! {
138 match self.list_vista_values(vista).await {
139 Ok(map) => {
140 for item in map {
141 yield Ok(item);
142 }
143 }
144 Err(e) => yield Err(e),
145 }
146 })
147 }
148
149 // ---- WritableValueSet delegates ----------------------------------------
150 //
151 // Default impls return a typed VantageError via `default_error` — drivers
152 // override only what they actually support. The matching `VistaCapabilities`
153 // flag must be set to `true` for any method the driver implements; if the
154 // flag is `true` but the trait method falls through to the default,
155 // `default_error` produces an `Unimplemented`-kind error (placeholder
156 // detected). If the flag is `false`, it produces `Unsupported`. Both are
157 // emitted as tracing events at construction.
158
159 async fn insert_vista_value(
160 &self,
161 _vista: &Vista,
162 _id: &String,
163 _record: &Record<CborValue>,
164 ) -> Result<Record<CborValue>> {
165 Err(self.default_error("insert_vista_value", "can_insert"))
166 }
167
168 async fn replace_vista_value(
169 &self,
170 _vista: &Vista,
171 _id: &String,
172 _record: &Record<CborValue>,
173 ) -> Result<Record<CborValue>> {
174 Err(self.default_error("replace_vista_value", "can_update"))
175 }
176
177 /// Insert row `id`, or replace it if it exists. The default replaces and
178 /// falls back to insert on `NotFound`; stores with a native upsert override it.
179 async fn upsert_vista_value(
180 &self,
181 vista: &Vista,
182 id: &String,
183 record: &Record<CborValue>,
184 ) -> Result<Record<CborValue>> {
185 match self.replace_vista_value(vista, id, record).await {
186 Err(e) if e.is_not_found() => self.insert_vista_value(vista, id, record).await,
187 other => other,
188 }
189 }
190
191 async fn patch_vista_value(
192 &self,
193 _vista: &Vista,
194 _id: &String,
195 _partial: &Record<CborValue>,
196 ) -> Result<Record<CborValue>> {
197 Err(self.default_error("patch_vista_value", "can_update"))
198 }
199
200 async fn delete_vista_value(&self, _vista: &Vista, _id: &String) -> Result<()> {
201 Err(self.default_error("delete_vista_value", "can_delete"))
202 }
203
204 async fn delete_vista_all_values(&self, _vista: &Vista) -> Result<()> {
205 Err(self.default_error("delete_vista_all_values", "can_delete"))
206 }
207
208 // ---- InsertableValueSet delegate ---------------------------------------
209
210 async fn insert_vista_return_id_value(
211 &self,
212 _vista: &Vista,
213 _record: &Record<CborValue>,
214 ) -> Result<String> {
215 Err(self.default_error("insert_vista_return_id_value", "can_insert"))
216 }
217
218 // ---- Bulk import -------------------------------------------------------
219
220 /// Store `records` (id → record) in one driver-native operation —
221 /// SQL COPY, Surreal batch insert — and return how many were
222 /// **newly inserted**. An id the table already held counts zero,
223 /// whether the driver's batch skips it or upserts it: the number
224 /// travels to a user as "n records created", and the per-record
225 /// fallback in `Dio::import_values` counts the same way. A driver
226 /// that cannot tell new from existing must not advertise
227 /// [`can_import`](VistaCapabilities::can_import).
228 ///
229 /// All-or-nothing is the other half of the contract: a driver that
230 /// cannot make the batch atomic must not advertise it either; the
231 /// caller then falls back to per-record inserts where partial
232 /// progress is honest and reportable.
233 async fn import_vista_values(
234 &self,
235 _vista: &Vista,
236 _records: &IndexMap<String, Record<CborValue>>,
237 ) -> Result<usize> {
238 Err(self.default_error("import_vista_values", "can_import"))
239 }
240
241 // ---- Aggregates --------------------------------------------------------
242
243 /// Default impl falls back to `list_vista_values` — drivers with native
244 /// count (`SELECT COUNT(*)`, etc.) override.
245 async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
246 Ok(self.list_vista_values(vista).await?.len() as i64)
247 }
248
249 /// Derive a **new vista** that reduces this one — the driver's equivalent
250 /// of selecting from a subquery.
251 ///
252 /// An aggregation is not a value, it is a different set: `count(*)` yields
253 /// one row, `GROUP BY` yields one per group, and either can then be
254 /// conditioned, ordered or counted like any other set. Returning a
255 /// [`Vista`] is what lets every consumer keep the single shape it already
256 /// handles instead of growing a scalar special case.
257 ///
258 /// `Ok(None)` means *this driver cannot answer this request* — not that
259 /// the result is empty. The caller then reduces locally, which is a
260 /// different question (the rows it holds, not every row that matches) with
261 /// a different answer, so "can't" must never collapse into a number.
262 ///
263 /// **Narrow before aggregating.** Conditions belong to the source, applied
264 /// with `add_eq_condition` before this call — the order SQL uses, where the
265 /// filter is the inner query and the aggregate selects from its result.
266 /// Narrowing reports its own failure, so a driver is never handed a filter
267 /// it would silently ignore.
268 ///
269 /// **The returned vista's capabilities describe the DERIVED set, not the
270 /// source.** In particular it must not advertise condition support unless
271 /// the driver really implements it: adding a condition to an aggregate is
272 /// `HAVING`, a different operation over different values, and inheriting
273 /// the source's flag would promise a filter that silently does nothing.
274 /// An aggregator holding its entire output in memory is the exception —
275 /// it can filter what it produced, and may say so.
276 ///
277 /// This is construction, not a query — nothing is fetched until someone
278 /// lists the returned vista.
279 fn aggregate_vista(&self, _vista: &Vista, _spec: &AggregateSpec) -> Result<Option<Vista>> {
280 Ok(None)
281 }
282
283 // ---- Conditions --------------------------------------------------------
284
285 /// Translate `field == value` into the driver's native condition type and
286 /// apply it to the wrapped table. The default impl returns `Unimplemented`
287 /// — every driver is expected to override.
288 ///
289 /// `value` is the universal CBOR carrier; the driver picks the appropriate
290 /// translation (e.g. `cbor_to_bson` for Mongo, `cbor → AnyCsvType` for CSV).
291 fn add_eq_condition(&mut self, _field: &str, _value: &CborValue) -> Result<()> {
292 Err(error!(
293 format!(
294 "add_eq_condition not implemented for '{}'",
295 std::any::type_name::<Self>()
296 ),
297 method = "add_eq_condition",
298 source_type = std::any::type_name::<Self>()
299 )
300 .mark_unimplemented()
301 .traced())
302 }
303
304 /// Translate `field <op> value` into the driver's native condition and
305 /// apply it. The default routes `Eq` to [`add_eq_condition`](Self::add_eq_condition)
306 /// (so every driver gets equality for free) and returns `Unimplemented`
307 /// for every richer operator. Drivers whose query language expresses the
308 /// operators (SQL, SurrealDB) override this and advertise
309 /// [`can_filter_operators`](crate::VistaCapabilities::can_filter_operators);
310 /// consumers that see `false` skip the call and filter locally instead.
311 fn add_op_condition(
312 &mut self,
313 field: &str,
314 op: crate::FilterOp,
315 value: &CborValue,
316 ) -> Result<()> {
317 match op {
318 crate::FilterOp::Eq => self.add_eq_condition(field, value),
319 _ => Err(error!(
320 format!(
321 "add_op_condition operator {:?} not implemented for '{}'",
322 op,
323 std::any::type_name::<Self>()
324 ),
325 method = "add_op_condition",
326 operator = format!("{:?}", op),
327 source_type = std::any::type_name::<Self>()
328 )
329 .mark_unimplemented()
330 .traced()),
331 }
332 }
333
334 /// Push a driver-native condition into the wrapped table. The
335 /// caller boxes the condition as `dyn Any` and the driver
336 /// downcasts to its own `T::Condition`. Used by YAML-driven
337 /// relation traversal, where the factory constructs a
338 /// `DeferredFn`-bearing condition outside the value-set surface
339 /// (which only accepts scalar eq) and pushes it through this
340 /// channel. Default is `Unimplemented`.
341 fn add_raw_condition(
342 &mut self,
343 _condition: Box<dyn std::any::Any + Send + Sync>,
344 ) -> Result<()> {
345 Err(error!(
346 format!(
347 "add_raw_condition not implemented for '{}'",
348 std::any::type_name::<Self>()
349 ),
350 method = "add_raw_condition",
351 source_type = std::any::type_name::<Self>()
352 )
353 .mark_unimplemented()
354 .traced())
355 }
356
357 // ---- Pagination --------------------------------------------------------
358
359 /// Declare how many records constitute one page. Used by both
360 /// [`fetch_page`](Self::fetch_page) and [`fetch_next`](Self::fetch_next).
361 /// Default returns `default_error("set_page_size", "can_set_page_size")`.
362 fn set_page_size(&mut self, _size: usize) -> Result<()> {
363 Err(self.default_error("set_page_size", "can_set_page_size"))
364 }
365
366 /// Fetch a specific page (1-based) using offset-style pagination. The
367 /// per-page count comes from the most recent
368 /// [`set_page_size`](Self::set_page_size).
369 ///
370 /// Drivers without random-access pagination (DynamoDB, most token-paginated
371 /// REST APIs) leave the default in place, which produces `Unsupported`.
372 /// Callers should branch on `vista.capabilities().can_fetch_page` first.
373 async fn fetch_page(
374 &self,
375 _vista: &Vista,
376 _page: usize,
377 ) -> Result<Vec<(String, Record<CborValue>)>> {
378 Err(self.default_error("fetch_page", "can_fetch_page"))
379 }
380
381 /// Cursor-style chain fetch. Pass `None` on the first call; pass the
382 /// previous call's returned token on subsequent calls. Returned token is
383 /// `None` when the result set is exhausted.
384 ///
385 /// The token is **driver-private** — its shape is whatever the backend
386 /// finds convenient (DynamoDB `LastEvaluatedKey` as a CBOR map, REST
387 /// `nextToken` as `CborValue::Text`, offset-based as `CborValue::Integer`).
388 /// Consumers treat it as opaque and round-trip it back unchanged.
389 ///
390 /// Default returns `default_error("fetch_next", "can_fetch_next")`.
391 async fn fetch_next(
392 &self,
393 _vista: &Vista,
394 _token: Option<CborValue>,
395 ) -> Result<(Vec<(String, Record<CborValue>)>, Option<CborValue>)> {
396 Err(self.default_error("fetch_next", "can_fetch_next"))
397 }
398
399 /// Fetch the half-open row window `[offset, offset + limit)` in the
400 /// source's natural order. Offset-style like [`fetch_page`](Self::fetch_page)
401 /// but addressed by absolute row index rather than page number, so it
402 /// maps directly onto a diorama `on_load_chunk` `Range<usize>` — which
403 /// is *not* guaranteed page-aligned. This is the primitive a paged,
404 /// lazily-loaded grid drives on scroll.
405 ///
406 /// Drivers leave the default in place (producing `Unsupported`) until
407 /// they implement it; callers branch on
408 /// `vista.capabilities().can_fetch_window` first. Default returns
409 /// `default_error("fetch_window", "can_fetch_window")`.
410 async fn fetch_window(
411 &self,
412 _vista: &Vista,
413 _offset: usize,
414 _limit: usize,
415 ) -> Result<Vec<(String, Record<CborValue>)>> {
416 Err(self.default_error("fetch_window", "can_fetch_window"))
417 }
418
419 /// [`fetch_window`](Self::fetch_window), plus the grand total of matching
420 /// rows when this fetch already learned it.
421 ///
422 /// Paged sources typically report the total in every response envelope,
423 /// alongside the window's rows. A caller needing both — a lazily-loaded
424 /// grid sizing its scrollbar — would otherwise pay a second round trip for
425 /// a number the first reply already carried.
426 ///
427 /// Drivers that can answer override this. The default delegates and
428 /// reports `None`, so no existing driver changes and no caller is told a
429 /// total exists when it doesn't. `None` means "this fetch didn't say",
430 /// never "zero".
431 async fn fetch_window_counted(
432 &self,
433 vista: &Vista,
434 offset: usize,
435 limit: usize,
436 ) -> Result<(Vec<(String, Record<CborValue>)>, Option<i64>)> {
437 Ok((self.fetch_window(vista, offset, limit).await?, None))
438 }
439
440 // ---- Quicksearch -------------------------------------------------------
441
442 /// Apply a quicksearch filter — a single string the driver fans out across
443 /// the columns it considers searchable (typically those flagged
444 /// [`SEARCHABLE`](crate::flags::SEARCHABLE), but each driver decides).
445 ///
446 /// **Replace semantics**: calling `add_search` again wipes the previous
447 /// search filter before applying the new one. Default produces
448 /// `Unimplemented` (when `can_search: true`) or `Unsupported` (when
449 /// `can_search: false`).
450 fn add_search(&mut self, _text: &str) -> Result<()> {
451 Err(self.default_error("add_search", "can_search"))
452 }
453
454 /// Drop the search filter previously applied via
455 /// [`add_search`](Self::add_search). Default mirrors `add_search`.
456 fn clear_search(&mut self) -> Result<()> {
457 Err(self.default_error("clear_search", "can_search"))
458 }
459
460 // ---- Ordering ----------------------------------------------------------
461
462 /// Push a single ORDER BY clause onto the wrapped table.
463 ///
464 /// Vista's `add_order` is replace-semantics: the driver shell should clear
465 /// any previously-set order before pushing the new one. Default produces
466 /// `Unimplemented` (when `can_order: true`) or `Unsupported` (when
467 /// `can_order: false`).
468 fn add_order(&mut self, _field: &str, _dir: SortDirection) -> Result<()> {
469 Err(self.default_error("add_order", "can_order"))
470 }
471
472 /// Wipe every order clause. Default mirrors [`add_order`](Self::add_order).
473 fn clear_orders(&mut self) -> Result<()> {
474 Err(self.default_error("clear_orders", "can_order"))
475 }
476
477 // ---- Cloning -----------------------------------------------------------
478
479 /// Produce an independent copy of this shell, or `None` if the driver can't
480 /// be cloned cheaply. The copy must share the backing store / connection
481 /// (typically `Arc`) but own its own query state (conditions / order /
482 /// search) so a caller can narrow it — set an ORDER BY, add a WHERE — without
483 /// disturbing the original. This is how a consumer builds a per-view ordered
484 /// Vista to fetch from: `clone_shell()` → `add_order(...)` → `fetch_window`.
485 ///
486 /// Default `None`: drivers opt in only where a clone is genuinely cheap
487 /// (query state is small; the store is `Arc`-shared). Callers that get `None`
488 /// fall back to reading the shared shell and ordering client-side.
489 fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
490 None
491 }
492
493 // ---- References --------------------------------------------------------
494
495 /// Resolve a same-persistence relation using a known source row, returning
496 /// the related table as a new `Vista`.
497 ///
498 /// Drivers override by forwarding into the wrapped typed `Table`'s
499 /// `get_ref_from_row::<EmptyEntity>(relation, &native_row)` and then
500 /// wrapping the result back as a `Vista` through the driver's factory.
501 /// The default returns `Unimplemented`. Cross-persistence refs are
502 /// handled one layer up by `vantage-vista-factory`'s `VistaCatalog`,
503 /// never here.
504 fn get_ref(&self, relation: &str, _row: &Record<CborValue>) -> Result<Vista> {
505 Err(error!(
506 format!(
507 "get_ref not implemented for '{}'",
508 std::any::type_name::<Self>()
509 ),
510 method = "get_ref",
511 relation = relation,
512 source_type = std::any::type_name::<Self>()
513 )
514 .mark_unimplemented()
515 .traced())
516 }
517
518 /// Build the **bare** target of a same-persistence relation as a `Vista` —
519 /// the table a new related row would be inserted into, with no join
520 /// condition applied. Used by Vista's nested insert to reach a has-one /
521 /// has-many child's destination.
522 ///
523 /// Drivers override by forwarding into the wrapped typed `Table`'s
524 /// `get_ref_target_erased(relation)` and wrapping the result back
525 /// through the driver's factory — the same path as [`get_ref`](Self::get_ref)
526 /// minus the row-derived condition. The default returns `Unimplemented`;
527 /// cross-persistence relations are rejected at the `Vista` layer before
528 /// this is reached.
529 fn get_ref_target(&self, relation: &str) -> Result<Vista> {
530 Err(error!(
531 format!(
532 "get_ref_target not implemented for '{}'",
533 std::any::type_name::<Self>()
534 ),
535 method = "get_ref_target",
536 relation = relation,
537 source_type = std::any::type_name::<Self>()
538 )
539 .mark_unimplemented()
540 .traced())
541 }
542
543 /// Contained (embedded-in-row) relations this shell exposes, keyed by name.
544 /// Default empty — only shells that model embedded objects/arrays override.
545 fn contained(&self) -> &IndexMap<String, ContainedSpec> {
546 static EMPTY: std::sync::OnceLock<IndexMap<String, ContainedSpec>> =
547 std::sync::OnceLock::new();
548 EMPTY.get_or_init(IndexMap::new)
549 }
550
551 /// Resolve a contained relation against a known parent `row`, returning the
552 /// embedded records as a sub-`Vista`. Writes to that sub-Vista patch the
553 /// host column of `row`'s record back through the shell. Default returns
554 /// `Unimplemented`; shells override to seed [`crate::build_contained_vista`]
555 /// with a writeback that patches the parent.
556 fn get_contained_ref(&self, relation: &str, _row: &Record<CborValue>) -> Result<Vista> {
557 Err(error!(
558 format!(
559 "get_contained_ref not implemented for '{}'",
560 std::any::type_name::<Self>()
561 ),
562 method = "get_contained_ref",
563 relation = relation,
564 source_type = std::any::type_name::<Self>()
565 )
566 .mark_unimplemented()
567 .traced())
568 }
569
570 /// Names + cardinalities of the shell's same-persistence references.
571 /// Derived from [`references`](Self::references) by default; impls
572 /// should rarely need to override.
573 fn get_ref_kinds(&self) -> Vec<(String, crate::reference::ReferenceKind)> {
574 self.references()
575 .iter()
576 .map(|(name, r)| (name.clone(), r.kind))
577 .collect()
578 }
579
580 // ---- Identity ----------------------------------------------------------
581
582 /// Short human label for the underlying driver (e.g. `"csv"`, `"sqlite"`,
583 /// `"postgres"`, `"mongodb"`). Used for diagnostics and CLI output.
584 /// Drivers should override; the default is a placeholder.
585 fn driver_name(&self) -> &'static str {
586 "unknown"
587 }
588
589 // ---- Preview -----------------------------------------------------------
590
591 /// Render the query this shell would send, **without sending it**.
592 ///
593 /// Free-form and driver-shaped: SQL answers with `sql`, GraphQL with a
594 /// `query` document, a cmd datasource with the argv it would spawn. The
595 /// only convention is a `driver` key naming the driver, so a reader can
596 /// tell what the rest of the object means.
597 ///
598 /// There is no default and no `Result`. No default, so a new driver is
599 /// forced to answer rather than inheriting silence; no `Result`, because
600 /// "there is no query" is an answer, not a failure — a driver that
601 /// generates its rows in-process says so in a `note` and is done.
602 ///
603 /// # Contract
604 ///
605 /// **Synchronous, and performs no I/O.** That is what makes preview safe
606 /// to expose where fetching is not. A value known only at fetch time (an
607 /// unresolved foreign-key narrowing) renders as a placeholder; it is never
608 /// awaited.
609 ///
610 /// **Lossy is acceptable, wrong is not.** A preview that omits a detail
611 /// should say so in a `note`. A preview showing a filter the driver would
612 /// not actually send is a bug.
613 fn preview_query(&self, vista: &Vista) -> serde_json::Value;
614
615 // ---- Scripting ---------------------------------------------------------
616
617 /// Contribute backend-specific vocabulary to a Rhai engine, registered
618 /// before vantage-vista's `DataVocab`. Backends with an expression engine
619 /// (SurrealDB, SQL) override this to register `ident`/`==`/`fx`/graph
620 /// constructors plus a `with_condition(<backend expr>)` verb on the table
621 /// handle (see `Handle::with_base_vista`) that routes a boxed native
622 /// condition through [`add_raw_condition`](Self::add_raw_condition).
623 ///
624 /// Default is a no-op: engine-less datasources (CSV/Mongo/REST) still get
625 /// the table handle verbs and only lose the vendor expression syntax.
626 #[cfg(feature = "rhai")]
627 fn register_rhai_extensions(&self, _engine: &mut rhai::Engine) {}
628
629 /// Vendor constants a script sees beside `row`/`self` — the per-evaluation
630 /// half of [`register_rhai_extensions`](Self::register_rhai_extensions).
631 /// SurrealDB pushes `me`, the current-record anchor. A value here is a
632 /// scope variable, never an engine hook, so it composes with the host's
633 /// own resolver.
634 ///
635 /// Default: `env` unchanged.
636 #[cfg(feature = "rhai")]
637 fn rhai_env(&self, env: vantage_rhai::Env) -> vantage_rhai::Env {
638 env
639 }
640
641 // ---- Live subscription -------------------------------------------------
642
643 /// Subscribe to changes on the set and stream them as [`VistaChange`]s.
644 ///
645 /// Drivers whose backend can push changes (SurrealDB LIVE, Postgres
646 /// `LISTEN/NOTIFY`) override this and advertise
647 /// [`can_subscribe`](VistaCapabilities::can_subscribe). The row-bearing
648 /// variants carry the record in the same projected shape as
649 /// [`list_vista_values`](Self::list_vista_values), so a consumer can apply
650 /// them to a cache directly; [`VistaChange::Invalidated`] carries nothing at
651 /// all and means "re-read the set". The default produces `Unimplemented` (when
652 /// `can_subscribe: true`) or `Unsupported` (when `false`); callers branch on
653 /// `vista.capabilities().can_subscribe` first.
654 ///
655 /// # The subscription contract
656 ///
657 /// Callers always pass the full `vista`, and drivers deliver on a best-effort
658 /// basis. A consumer must not need to know whether a given backend filters
659 /// row-, select- or table-wide — that is what keeps consumer code identical
660 /// across drivers, and lets a driver tighten its scope later without
661 /// breaking anyone. Four promises hold for every implementation:
662 ///
663 /// 1. **The stream may be coarser than the vista.** Subscribing table-wide
664 /// and letting the consumer discard what it doesn't want is a valid
665 /// implementation; `vista` is a hint about what's interesting, not a
666 /// filter the driver is obliged to apply.
667 /// 2. **Payload rows may fall outside the vista's conditions**, precisely
668 /// because of (1). Either the driver reconciles (SurrealDB re-reads each
669 /// notified id *through* the vista's conditions, so a row that no longer
670 /// matches surfaces as [`VistaChange::Deleted`]) or the consumer must.
671 /// Never assume an `Inserted`/`Updated` row belongs in the set.
672 /// 3. **[`VistaChange::Invalidated`] means "re-read everything".** It carries
673 /// no id and implies nothing about how much changed — a driver with no row
674 /// payload to offer may emit it for every single write.
675 /// 4. **Stream end is normal, not an error.** Connections drop and sessions
676 /// expire; consumers resubscribe (with backoff) and reconcile the gap. A
677 /// driver need not reconnect internally.
678 ///
679 /// Delivery is not guaranteed even while subscribed — see
680 /// [`can_subscribe`](VistaCapabilities::can_subscribe).
681 async fn watch_vista(&self, _vista: &Vista) -> Result<VistaChangeStream> {
682 Err(self.default_error("watch_vista", "can_subscribe"))
683 }
684
685 // ---- Capability advertisement -----------------------------------------
686
687 fn capabilities(&self) -> &VistaCapabilities;
688
689 /// Look up a capability flag by name. Used by `default_error` to decide
690 /// between `Unsupported` and `Unimplemented`. Drivers don't normally
691 /// need to override this.
692 fn capability_flag(&self, name: &str) -> bool {
693 let caps = self.capabilities();
694 match name {
695 "can_count" => caps.can_count,
696 "can_insert" => caps.can_insert,
697 "can_update" => caps.can_update,
698 "can_delete" => caps.can_delete,
699 "can_import" => caps.can_import,
700 "can_subscribe" => caps.can_subscribe,
701 "can_invalidate" => caps.can_invalidate,
702 "can_order" => caps.can_order,
703 "can_search" => caps.can_search,
704 "can_set_page_size" => caps.can_set_page_size,
705 "can_fetch_page" => caps.can_fetch_page,
706 "can_fetch_next" => caps.can_fetch_next,
707 "can_fetch_window" => caps.can_fetch_window,
708 "can_traverse_to_record" => caps.can_traverse_to_record,
709 "can_traverse_to_set" => caps.can_traverse_to_set,
710 "can_build_ref_via_script" => caps.can_build_ref_via_script,
711 "can_traverse_in_columns" => caps.can_traverse_in_columns,
712 _ => false,
713 }
714 }
715
716 /// Build the standard error returned by default trait method impls.
717 ///
718 /// Picks the kind based on the capability flag: a `true` flag means the
719 /// driver advertised support but didn't override the method (placeholder
720 /// → `Unimplemented`); a `false` flag means the driver honestly doesn't
721 /// claim the op (caller should have checked → `Unsupported`).
722 ///
723 /// Only the `Unimplemented` kind traces at error level — it's a driver
724 /// bug. An `Unsupported` refusal is a legitimate answer to a caller
725 /// probing a capability (e.g. an exploratory data script calling
726 /// `set_page_size` on a cache-mode vista): the error value carries the
727 /// full message to the caller, so it logs at debug only.
728 fn default_error(&self, method: &str, capability: &str) -> VantageError {
729 let source_type = std::any::type_name::<Self>();
730 if self.capability_flag(capability) {
731 error!(
732 format!(
733 "'{}' is advertised as VistaCapability for '{}' but implementation for '{}' is missing",
734 capability, source_type, method
735 ),
736 method = method,
737 capability = capability,
738 source_type = source_type
739 )
740 .mark_unimplemented().traced()
741 } else {
742 error!(
743 format!(
744 "'{}' is not supported by '{}'; '{}' refused",
745 capability, source_type, method
746 ),
747 method = method,
748 capability = capability,
749 source_type = source_type
750 )
751 .mark_unsupported()
752 .traced_debug()
753 }
754 }
755}