Skip to main content

vantage_vista/mocks/
mock_shell.rs

1//! In-memory `TableShell` for tests and examples.
2
3use async_trait::async_trait;
4use ciborium::Value as CborValue;
5use indexmap::IndexMap;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use vantage_core::Result;
9use vantage_types::Record;
10
11use crate::{
12    build_contained_vista,
13    capabilities::VistaCapabilities,
14    column::Column,
15    contained::ContainedWriteback,
16    metadata::VistaMetadata,
17    reference::{ContainedSpec, Reference},
18    sort::SortDirection,
19    source::TableShell,
20    vista::Vista,
21};
22
23#[derive(Clone)]
24pub struct MockShell {
25    data: Arc<Mutex<IndexMap<String, Record<CborValue>>>>,
26    next_auto_id: Arc<Mutex<i64>>,
27    filters: Arc<Mutex<Vec<(String, CborValue)>>>,
28    order: Arc<Mutex<Option<(String, SortDirection)>>>,
29    search: Arc<Mutex<Option<String>>>,
30    capabilities: VistaCapabilities,
31    metadata: VistaMetadata,
32    /// Per-relation target stores, so `get_ref` can resolve a foreign-key
33    /// reference to another in-memory shell — the mock analogue of a driver's
34    /// `with_one`/`with_many`. Registered via [`Self::with_ref_target`].
35    ref_targets: IndexMap<String, MockShell>,
36    /// While set, every read returns `Err` — the in-memory analogue of a
37    /// source that is temporarily unreachable (a 503). Shared across clones
38    /// (incl. narrowed `get_ref` results) so a test can flip it from any handle.
39    fail_reads: Arc<AtomicBool>,
40    /// While set, the store keys every record by `<prefix><id>` — the
41    /// in-memory analogue of a driver that owns its key space and qualifies
42    /// what a caller hands it. See [`Self::with_id_prefix`].
43    id_prefix: Option<String>,
44}
45
46impl MockShell {
47    pub fn new() -> Self {
48        Self {
49            data: Arc::new(Mutex::new(IndexMap::new())),
50            next_auto_id: Arc::new(Mutex::new(1)),
51            filters: Arc::new(Mutex::new(Vec::new())),
52            order: Arc::new(Mutex::new(None)),
53            search: Arc::new(Mutex::new(None)),
54            capabilities: VistaCapabilities {
55                can_count: true,
56                can_insert: true,
57                can_update: true,
58                can_delete: true,
59                can_order: true,
60                can_search: true,
61                ..VistaCapabilities::default()
62            },
63            metadata: VistaMetadata::new(),
64            ref_targets: IndexMap::new(),
65            fail_reads: Arc::new(AtomicBool::new(false)),
66            id_prefix: None,
67        }
68    }
69
70    /// Register the target store for a foreign-key relation declared in
71    /// `metadata` (via [`VistaMetadata::with_reference`]). `get_ref(relation,
72    /// row)` then returns this store narrowed by `foreign_key == parent_row[id]`
73    /// — the in-memory analogue of a driver resolving a `with_one`/`with_many`.
74    pub fn with_ref_target(mut self, relation: impl Into<String>, target: MockShell) -> Self {
75        self.ref_targets.insert(relation.into(), target);
76        self
77    }
78
79    /// Flip read failure on/off. While `true`, `list`/`get`/`count` return
80    /// `Err`. Shared across clones (incl. narrowed `get_ref` results) via `Arc`,
81    /// so a handle kept before the shell is registered/boxed can fail the live
82    /// dataset mid-run — simulating a source that goes offline.
83    pub fn set_fail_reads(&self, fail: bool) {
84        self.fail_reads.store(fail, Ordering::SeqCst);
85    }
86
87    /// Clone sharing the data, fail toggle, ref targets, and metadata, but with
88    /// FRESH condition state — so narrowing a `get_ref` result doesn't pollute
89    /// the registered target shell's filters/order/search.
90    fn narrowed_clone(&self) -> Self {
91        Self {
92            data: self.data.clone(),
93            next_auto_id: self.next_auto_id.clone(),
94            filters: Arc::new(Mutex::new(Vec::new())),
95            order: Arc::new(Mutex::new(None)),
96            search: Arc::new(Mutex::new(None)),
97            capabilities: self.capabilities.clone(),
98            metadata: self.metadata.clone(),
99            ref_targets: self.ref_targets.clone(),
100            fail_reads: self.fail_reads.clone(),
101            id_prefix: self.id_prefix.clone(),
102        }
103    }
104
105    pub fn with_capabilities(mut self, capabilities: VistaCapabilities) -> Self {
106        self.capabilities = capabilities;
107        self
108    }
109
110    pub fn with_metadata(mut self, metadata: VistaMetadata) -> Self {
111        self.metadata = metadata;
112        self
113    }
114
115    /// Qualify every id with `prefix`, the way a driver that owns its key
116    /// space does: a record inserted as `abc` is stored — and returned —
117    /// as `client:abc`. An id that already carries the prefix passes
118    /// through, so a caller can address a row in either form.
119    pub fn with_id_prefix(mut self, prefix: impl Into<String>) -> Self {
120        self.id_prefix = Some(prefix.into());
121        self
122    }
123
124    /// The store key for `id` under [`Self::with_id_prefix`].
125    fn key(&self, id: &str) -> String {
126        match &self.id_prefix {
127            Some(prefix) if !id.starts_with(prefix.as_str()) => format!("{prefix}{id}"),
128            _ => id.to_string(),
129        }
130    }
131
132    /// Seed a record with an explicit id.
133    pub fn with_record(self, id: impl Into<String>, record: Record<CborValue>) -> Self {
134        self.data.lock().unwrap().insert(id.into(), record);
135        self
136    }
137
138    // ---- Live dataset mutation ---------------------------------------------
139    //
140    // The store is `Arc<Mutex<…>>`, so a clone of this shell taken *before*
141    // it is boxed into a `Vista` keeps a handle to the same rows. These
142    // by-ref helpers let a test or example mutate the dataset mid-run —
143    // simulating an upstream that changed between reads — and have the next
144    // `list`/`get`/refresh observe it. They are additive and opt-in; an
145    // untouched shell behaves exactly as before.
146
147    /// Insert or replace a record by id through the shared store.
148    pub fn set_record(&self, id: impl Into<String>, record: Record<CborValue>) {
149        self.data.lock().unwrap().insert(id.into(), record);
150    }
151
152    /// Overwrite a single field of an existing record (read-modify-write).
153    /// No-op if the record is absent.
154    pub fn set_field(&self, id: &str, field: &str, value: CborValue) {
155        if let Some(rec) = self.data.lock().unwrap().get_mut(id) {
156            rec.insert(field.to_string(), value);
157        }
158    }
159
160    /// Remove a record by id. No-op if absent.
161    pub fn remove_record(&self, id: &str) {
162        self.data.lock().unwrap().shift_remove(id);
163    }
164
165    /// Drop every record.
166    pub fn clear_records(&self) {
167        self.data.lock().unwrap().clear();
168    }
169
170    /// Number of records currently held. Companion to the live-mutation
171    /// helpers above, for tests/examples that grow or shrink the store at
172    /// runtime and want to assert on its size without an async `list`.
173    pub fn len(&self) -> usize {
174        self.data.lock().unwrap().len()
175    }
176
177    /// Whether the store holds no records.
178    pub fn is_empty(&self) -> bool {
179        self.len() == 0
180    }
181
182    /// Snapshot one record by id, straight off the store — no conditions, no
183    /// `fail_reads` guard. Companion to the live-mutation helpers: an effect
184    /// or test reading what it is about to mutate is inspecting its own
185    /// store, not querying a source.
186    pub fn get_record(&self, id: &str) -> Option<Record<CborValue>> {
187        self.data.lock().unwrap().get(id).cloned()
188    }
189
190    /// Ids currently held, in store order. Same store-side view as
191    /// [`Self::get_record`].
192    pub fn record_ids(&self) -> Vec<String> {
193        self.data.lock().unwrap().keys().cloned().collect()
194    }
195
196    /// Return `Err` while `fail_reads` is set — see [`Self::set_fail_reads`].
197    fn guard_reads(&self) -> Result<()> {
198        if self.fail_reads.load(Ordering::SeqCst) {
199            return Err(vantage_core::error!("mock source read failed (injected)"));
200        }
201        Ok(())
202    }
203
204    fn matches_filters(&self, record: &Record<CborValue>) -> bool {
205        self.filters
206            .lock()
207            .unwrap()
208            .iter()
209            .all(|(field, expected)| record.get(field) == Some(expected))
210    }
211
212    fn matches_search(&self, record: &Record<CborValue>) -> bool {
213        let guard = self.search.lock().unwrap();
214        let Some(needle) = guard.as_deref() else {
215            return true;
216        };
217        let needle_lc = needle.to_lowercase();
218        record.values().any(|v| match v {
219            CborValue::Text(s) => s.to_lowercase().contains(&needle_lc),
220            _ => false,
221        })
222    }
223
224    fn next_auto_id(&self) -> String {
225        let mut next = self.next_auto_id.lock().unwrap();
226        let id = next.to_string();
227        *next += 1;
228        id
229    }
230}
231
232impl Default for MockShell {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238#[async_trait]
239impl TableShell for MockShell {
240    fn columns(&self) -> &IndexMap<String, Column> {
241        &self.metadata.columns
242    }
243
244    fn references(&self) -> &IndexMap<String, Reference> {
245        &self.metadata.references
246    }
247
248    fn contained(&self) -> &IndexMap<String, ContainedSpec> {
249        &self.metadata.contained
250    }
251
252    /// Resolve a contained relation against `row`, with a writeback that patches
253    /// the parent record's host column directly in this mock's store — the
254    /// in-memory analogue of a driver patching its row.
255    fn get_contained_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
256        let spec = self.metadata.contained.get(relation).ok_or_else(|| {
257            vantage_core::error!("unknown contained relation", relation = relation)
258        })?;
259        let host_value = row.get(&spec.host_column).cloned();
260
261        let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
262        let parent_id = match row.get(id_field) {
263            Some(CborValue::Text(s)) => s.clone(),
264            _ => {
265                return Err(vantage_core::error!(
266                    "contained traversal requires the parent row's id",
267                    relation = relation
268                ));
269            }
270        };
271
272        let data = self.data.clone();
273        let host_column = spec.host_column.clone();
274        let writeback: ContainedWriteback = Arc::new(move |collection: CborValue| {
275            let data = data.clone();
276            let host_column = host_column.clone();
277            let parent_id = parent_id.clone();
278            Box::pin(async move {
279                let mut store = data.lock().unwrap();
280                if let Some(record) = store.get_mut(&parent_id) {
281                    record.insert(host_column, collection);
282                }
283                Ok(())
284            })
285        });
286
287        build_contained_vista(spec, host_value.as_ref(), writeback, None)
288    }
289
290    /// Resolve a foreign-key relation to its registered target store, narrowed
291    /// by `foreign_key == parent_row[id]`. This is a pure descriptor operation
292    /// (no read), so it succeeds even while the target's `fail_reads` is set —
293    /// the failure surfaces later, at load time, on the returned Vista.
294    fn get_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
295        let reference = self
296            .metadata
297            .references
298            .get(relation)
299            .ok_or_else(|| vantage_core::error!("unknown relation", relation = relation))?;
300        let target = self.ref_targets.get(relation).ok_or_else(|| {
301            vantage_core::error!("no ref target registered for relation", relation = relation)
302        })?;
303        let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
304        let parent_val = row.get(id_field).cloned().ok_or_else(|| {
305            vantage_core::error!(
306                "parent row missing id for traversal",
307                relation = relation,
308                id_field = id_field
309            )
310        })?;
311        let narrowed = target.narrowed_clone();
312        narrowed
313            .filters
314            .lock()
315            .unwrap()
316            .push((reference.foreign_key.clone(), parent_val));
317        Ok(Vista::new(reference.target.clone(), Box::new(narrowed)))
318    }
319
320    fn id_column(&self) -> Option<&str> {
321        self.metadata.id_column.as_deref()
322    }
323
324    async fn list_vista_values(
325        &self,
326        _vista: &Vista,
327    ) -> Result<IndexMap<String, Record<CborValue>>> {
328        self.guard_reads()?;
329        let data = self.data.lock().unwrap();
330        let mut rows: Vec<(String, Record<CborValue>)> = data
331            .iter()
332            .filter(|(_, record)| self.matches_filters(record) && self.matches_search(record))
333            .map(|(k, v)| (k.clone(), v.clone()))
334            .collect();
335        if let Some((field, dir)) = self.order.lock().unwrap().clone() {
336            rows.sort_by(|a, b| {
337                let lhs = a.1.get(&field);
338                let rhs = b.1.get(&field);
339                let ord = cbor_cmp(lhs, rhs);
340                match dir {
341                    SortDirection::Ascending => ord,
342                    SortDirection::Descending => ord.reverse(),
343                }
344            });
345        }
346        Ok(rows.into_iter().collect())
347    }
348
349    /// Windowed read that honours the shell's current `add_order` — the mock's
350    /// analogue of a driver serving an ordered `[offset, limit)` page. Gated by
351    /// `can_fetch_window` like every driver.
352    ///
353    /// Clones only the returned window. The obvious implementation — list
354    /// everything, then slice — copies the whole store per window, which on a
355    /// large mock (200k rows) turns every "fast" page into a near-second stall
356    /// and made the shaped-faker latency knobs meaningless. Filtering and
357    /// ordering work over references; records are cloned after the slice.
358    async fn fetch_window(
359        &self,
360        _vista: &Vista,
361        offset: usize,
362        limit: usize,
363    ) -> Result<Vec<(String, Record<CborValue>)>> {
364        self.guard_reads()?;
365        let data = self.data.lock().unwrap();
366        let matches = |record: &Record<CborValue>| {
367            self.matches_filters(record) && self.matches_search(record)
368        };
369        let order = self.order.lock().unwrap().clone();
370        Ok(match order {
371            Some((field, dir)) => {
372                let mut refs: Vec<(&String, &Record<CborValue>)> =
373                    data.iter().filter(|(_, r)| matches(r)).collect();
374                refs.sort_by(|a, b| {
375                    let ord = cbor_cmp(a.1.get(&field), b.1.get(&field));
376                    match dir {
377                        SortDirection::Ascending => ord,
378                        SortDirection::Descending => ord.reverse(),
379                    }
380                });
381                refs.into_iter()
382                    .skip(offset)
383                    .take(limit)
384                    .map(|(k, v)| (k.clone(), v.clone()))
385                    .collect()
386            }
387            None => data
388                .iter()
389                .filter(|(_, r)| matches(r))
390                .skip(offset)
391                .take(limit)
392                .map(|(k, v)| (k.clone(), v.clone()))
393                .collect(),
394        })
395    }
396
397    /// Share the backing store, but give the copy its **own** query state
398    /// (filters / order / search) — the `clone_shell` contract. `MockShell`'s
399    /// derived `Clone` shares those (they're `Arc<Mutex<_>>`), so it can't be
400    /// used here: narrowing the clone (e.g. `add_order` in
401    /// `Dio::fetch_window_ordered`) must not reach back and reorder the original.
402    /// `data` / `next_auto_id` / `fail_reads` stay shared (the store).
403    fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
404        Some(Box::new(MockShell {
405            data: self.data.clone(),
406            next_auto_id: self.next_auto_id.clone(),
407            filters: Arc::new(Mutex::new(self.filters.lock().unwrap().clone())),
408            order: Arc::new(Mutex::new(self.order.lock().unwrap().clone())),
409            search: Arc::new(Mutex::new(self.search.lock().unwrap().clone())),
410            capabilities: self.capabilities.clone(),
411            metadata: self.metadata.clone(),
412            ref_targets: self.ref_targets.clone(),
413            fail_reads: self.fail_reads.clone(),
414            id_prefix: self.id_prefix.clone(),
415        }))
416    }
417
418    async fn get_vista_value(
419        &self,
420        _vista: &Vista,
421        id: &String,
422    ) -> Result<Option<Record<CborValue>>> {
423        self.guard_reads()?;
424        Ok(self.data.lock().unwrap().get(&self.key(id)).cloned())
425    }
426
427    async fn get_vista_some_value(
428        &self,
429        _vista: &Vista,
430    ) -> Result<Option<(String, Record<CborValue>)>> {
431        self.guard_reads()?;
432        let data = self.data.lock().unwrap();
433        Ok(data
434            .iter()
435            .find(|(_, record)| self.matches_filters(record))
436            .map(|(k, v)| (k.clone(), v.clone())))
437    }
438
439    /// Store the record and report it back carrying the key it landed
440    /// under. The key goes into the vista's id column, not a literal
441    /// `id` field: a caller that reads the id back off the returned
442    /// record — a cache settling a create, for one — reads the column the
443    /// metadata declares, so a mock configured with a different id column
444    /// must answer in that column too.
445    async fn insert_vista_value(
446        &self,
447        _vista: &Vista,
448        id: &String,
449        record: &Record<CborValue>,
450    ) -> Result<Record<CborValue>> {
451        let key = self.key(id);
452        let mut data = self.data.lock().unwrap();
453        if data.contains_key(&key) {
454            return Err(vantage_core::error!("Record already exists", id = id));
455        }
456        let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
457        let mut stored = record.clone();
458        stored.insert(id_field.to_string(), CborValue::Text(key.clone()));
459        data.insert(key, stored.clone());
460        Ok(stored)
461    }
462
463    async fn replace_vista_value(
464        &self,
465        _vista: &Vista,
466        id: &String,
467        record: &Record<CborValue>,
468    ) -> Result<Record<CborValue>> {
469        let key = self.key(id);
470        let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
471        let mut data = self.data.lock().unwrap();
472        let mut stored = record.clone();
473        stored.insert(id_field.to_string(), CborValue::Text(key.clone()));
474        data.insert(key, stored.clone());
475        Ok(stored)
476    }
477
478    async fn patch_vista_value(
479        &self,
480        _vista: &Vista,
481        id: &String,
482        partial: &Record<CborValue>,
483    ) -> Result<Record<CborValue>> {
484        let key = self.key(id);
485        let mut data = self.data.lock().unwrap();
486        let existing = data
487            .get_mut(&key)
488            .ok_or_else(|| vantage_core::error!("Record not found", id = id))?;
489        for (k, v) in partial {
490            existing.insert(k.clone(), v.clone());
491        }
492        Ok(existing.clone())
493    }
494
495    async fn delete_vista_value(&self, _vista: &Vista, id: &String) -> Result<()> {
496        let key = self.key(id);
497        let mut data = self.data.lock().unwrap();
498        if data.shift_remove(&key).is_none() {
499            Err(vantage_core::error!("Record not found", id = id))
500        } else {
501            Ok(())
502        }
503    }
504
505    async fn delete_vista_all_values(&self, _vista: &Vista) -> Result<()> {
506        self.data.lock().unwrap().clear();
507        Ok(())
508    }
509
510    /// Insert and report the id the record is addressable by. That is the
511    /// store key, prefix included: the caller uses this id to read the row
512    /// back, so it must be the same id the by-id insert path stores under.
513    async fn insert_vista_return_id_value(
514        &self,
515        vista: &Vista,
516        record: &Record<CborValue>,
517    ) -> Result<String> {
518        let id = match record.get("id") {
519            Some(CborValue::Text(s)) if !s.is_empty() => s.clone(),
520            Some(CborValue::Integer(i)) => i128::from(*i).to_string(),
521            _ => self.next_auto_id(),
522        };
523        self.insert_vista_value(vista, &id, record).await?;
524        Ok(self.key(&id))
525    }
526
527    /// Count without materializing: `list_vista_values` clones every matching
528    /// record just to take a length, which turns each counted window fetch on
529    /// a large store into a full-store copy (1.8 s against a 200k-row mock).
530    /// Counting only needs the predicates.
531    async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
532        let _ = vista;
533        self.guard_reads()?;
534        let data = self.data.lock().unwrap();
535        Ok(data
536            .values()
537            .filter(|record| self.matches_filters(record) && self.matches_search(record))
538            .count() as i64)
539    }
540
541    fn capabilities(&self) -> &VistaCapabilities {
542        &self.capabilities
543    }
544
545    fn driver_name(&self) -> &'static str {
546        "mock"
547    }
548
549    /// The mock has no query language, so it reports the narrowing state a real
550    /// driver would translate: which filters, order and search are pending.
551    /// Enough for tests to assert that a builder verb reached the shell.
552    fn preview_query(&self, vista: &Vista) -> serde_json::Value {
553        let filters: Vec<String> = self
554            .filters
555            .lock()
556            .unwrap()
557            .iter()
558            .map(|(field, value)| format!("{field} = {value:?}"))
559            .collect();
560        let order = self.order.lock().unwrap().as_ref().map(|(col, dir)| {
561            let dir = match dir {
562                SortDirection::Ascending => "asc",
563                SortDirection::Descending => "desc",
564            };
565            format!("{col} {dir}")
566        });
567        serde_json::json!({
568            "driver": "mock",
569            "table": vista.name(),
570            "filters": filters,
571            "order": order,
572            "search": self.search.lock().unwrap().clone(),
573        })
574    }
575
576    fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
577        self.filters
578            .lock()
579            .unwrap()
580            .push((field.to_string(), value.clone()));
581        Ok(())
582    }
583
584    fn add_order(&mut self, field: &str, dir: SortDirection) -> Result<()> {
585        *self.order.lock().unwrap() = Some((field.to_string(), dir));
586        Ok(())
587    }
588
589    fn clear_orders(&mut self) -> Result<()> {
590        *self.order.lock().unwrap() = None;
591        Ok(())
592    }
593
594    fn add_search(&mut self, text: &str) -> Result<()> {
595        *self.search.lock().unwrap() = Some(text.to_string());
596        Ok(())
597    }
598
599    fn clear_search(&mut self) -> Result<()> {
600        *self.search.lock().unwrap() = None;
601        Ok(())
602    }
603}
604
605/// Total-order comparator for the CBOR scalars MockShell records carry.
606/// Falls back to lexical ordering of CBOR-as-text for mixed-or-unknown
607/// types, which keeps sort deterministic without claiming semantic
608/// equivalence between heterogeneous values.
609fn cbor_cmp(a: Option<&CborValue>, b: Option<&CborValue>) -> std::cmp::Ordering {
610    use std::cmp::Ordering;
611    match (a, b) {
612        (None, None) => Ordering::Equal,
613        (None, _) => Ordering::Less,
614        (_, None) => Ordering::Greater,
615        (Some(lhs), Some(rhs)) => match (lhs, rhs) {
616            (CborValue::Text(l), CborValue::Text(r)) => l.cmp(r),
617            (CborValue::Integer(l), CborValue::Integer(r)) => i128::from(*l).cmp(&i128::from(*r)),
618            (CborValue::Bool(l), CborValue::Bool(r)) => l.cmp(r),
619            _ => format!("{lhs:?}").cmp(&format!("{rhs:?}")),
620        },
621    }
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use crate::{Column, Reference, ReferenceKind, Vista, VistaMetadata};
628    use vantage_dataset::{InsertableValueSet, ReadableValueSet, WritableValueSet};
629
630    fn cbor_text(s: &str) -> CborValue {
631        CborValue::Text(s.into())
632    }
633
634    fn record(pairs: &[(&str, CborValue)]) -> Record<CborValue> {
635        let mut r = Record::new();
636        for (k, v) in pairs {
637            r.insert((*k).to_string(), v.clone());
638        }
639        r
640    }
641
642    fn build_user_vista(source: MockShell) -> Vista {
643        let metadata = VistaMetadata::new()
644            .with_column(Column::new("id", "String").with_flag("id"))
645            .with_column(Column::new("name", "String").with_flag("title"))
646            .with_column(Column::new("email", "String").hidden())
647            .with_column(Column::new("vip_flag", "bool"))
648            .with_id_column("id")
649            .with_reference(Reference::new(
650                "orders",
651                "orders",
652                ReferenceKind::HasMany,
653                "user_id",
654            ));
655        Vista::new("users", Box::new(source.with_metadata(metadata)))
656    }
657
658    #[test]
659    fn metadata_accessors_round_trip() {
660        let vista = build_user_vista(MockShell::new());
661
662        assert_eq!(vista.name(), "users");
663        assert_eq!(vista.get_id_column(), Some("id"));
664        assert_eq!(vista.get_title_columns(), vec!["name"]);
665        assert_eq!(
666            vista.get_column_names(),
667            vec!["id", "name", "email", "vip_flag"]
668        );
669        assert!(vista.get_column("email").unwrap().is_hidden());
670        assert!(!vista.get_column("name").unwrap().is_hidden());
671        assert_eq!(vista.get_references(), vec!["orders".to_string()]);
672        assert_eq!(
673            vista.get_reference("orders").unwrap().foreign_key,
674            "user_id"
675        );
676
677        let caps = vista.capabilities();
678        assert!(caps.can_count && caps.can_insert && caps.can_update && caps.can_delete);
679        assert!(!caps.can_subscribe);
680    }
681
682    #[tokio::test]
683    async fn list_values_returns_seeded_rows() {
684        let source = MockShell::new()
685            .with_record(
686                "1",
687                record(&[("id", cbor_text("1")), ("name", cbor_text("Alice"))]),
688            )
689            .with_record(
690                "2",
691                record(&[("id", cbor_text("2")), ("name", cbor_text("Bob"))]),
692            );
693        let vista = build_user_vista(source);
694
695        let rows = vista.list_values().await.unwrap();
696        assert_eq!(rows.len(), 2);
697        assert!(rows.contains_key("1"));
698        assert_eq!(rows["2"].get("name"), Some(&cbor_text("Bob")));
699
700        let alice = vista.get_value("1").await.unwrap().unwrap();
701        assert_eq!(alice.get("name"), Some(&cbor_text("Alice")));
702
703        assert_eq!(vista.get_count().await.unwrap(), 2);
704    }
705
706    #[tokio::test]
707    async fn add_condition_eq_filters_list_and_count() {
708        let source = MockShell::new()
709            .with_record(
710                "1",
711                record(&[
712                    ("name", cbor_text("Alice")),
713                    ("vip_flag", CborValue::Bool(true)),
714                ]),
715            )
716            .with_record(
717                "2",
718                record(&[
719                    ("name", cbor_text("Bob")),
720                    ("vip_flag", CborValue::Bool(false)),
721                ]),
722            )
723            .with_record(
724                "3",
725                record(&[
726                    ("name", cbor_text("Carol")),
727                    ("vip_flag", CborValue::Bool(true)),
728                ]),
729            );
730        let mut vista = build_user_vista(source);
731        vista
732            .add_condition_eq("vip_flag", CborValue::Bool(true))
733            .unwrap();
734
735        let rows = vista.list_values().await.unwrap();
736        assert_eq!(rows.len(), 2);
737        assert!(rows.contains_key("1"));
738        assert!(rows.contains_key("3"));
739        assert_eq!(vista.get_count().await.unwrap(), 2);
740    }
741
742    #[tokio::test]
743    async fn writable_value_set_round_trip() {
744        let vista = build_user_vista(MockShell::new());
745
746        // insert_value with explicit id
747        let inserted = vista
748            .insert_value("alice", &record(&[("name", cbor_text("Alice"))]))
749            .await
750            .unwrap();
751        assert_eq!(inserted.get("id"), Some(&cbor_text("alice")));
752
753        // duplicate insert_value fails
754        let dup = vista.insert_value("alice", &record(&[])).await;
755        assert!(dup.is_err());
756
757        // replace_value upserts
758        vista
759            .replace_value("alice", &record(&[("name", cbor_text("Alicia"))]))
760            .await
761            .unwrap();
762        let renamed = vista.get_value("alice").await.unwrap().unwrap();
763        assert_eq!(renamed.get("name"), Some(&cbor_text("Alicia")));
764
765        // patch_value merges
766        vista
767            .patch_value(
768                "alice",
769                &record(&[("email", cbor_text("alice@example.com"))]),
770            )
771            .await
772            .unwrap();
773        let patched = vista.get_value("alice").await.unwrap().unwrap();
774        assert_eq!(patched.get("name"), Some(&cbor_text("Alicia")));
775        assert_eq!(patched.get("email"), Some(&cbor_text("alice@example.com")));
776
777        // delete
778        vista.delete("alice").await.unwrap();
779        assert!(vista.get_value("alice").await.unwrap().is_none());
780
781        // delete_all
782        vista
783            .insert_value("a", &record(&[("name", cbor_text("A"))]))
784            .await
785            .unwrap();
786        vista
787            .insert_value("b", &record(&[("name", cbor_text("B"))]))
788            .await
789            .unwrap();
790        vista.delete_all().await.unwrap();
791        assert_eq!(vista.list_values().await.unwrap().len(), 0);
792    }
793
794    #[tokio::test]
795    async fn default_get_value_with_row_ignores_row_and_delegates() {
796        // A driver that does not override `get_vista_value_with_row` must behave
797        // exactly like `get_value` — the extra `row` is ignored.
798        let source = MockShell::new().with_record(
799            "x",
800            record(&[("id", cbor_text("x")), ("name", cbor_text("Xavier"))]),
801        );
802        let vista = build_user_vista(source);
803
804        let mut row: Record<CborValue> = Record::new();
805        row.insert("extra".into(), cbor_text("ignored"));
806
807        let got = vista.get_value_with_row("x", &row).await.unwrap().unwrap();
808        assert_eq!(got.get("name"), Some(&cbor_text("Xavier")));
809    }
810
811    #[tokio::test]
812    async fn insertable_value_set_assigns_ids() {
813        let vista = build_user_vista(MockShell::new());
814
815        // record without id → mock generates one
816        let auto_id = vista
817            .insert_return_id_value(&record(&[("name", cbor_text("Bob"))]))
818            .await
819            .unwrap();
820        assert_eq!(auto_id, "1");
821
822        // record with explicit string id → preserved
823        let explicit = vista
824            .insert_return_id_value(&record(&[
825                ("id", cbor_text("alice")),
826                ("name", cbor_text("Alice")),
827            ]))
828            .await
829            .unwrap();
830        assert_eq!(explicit, "alice");
831
832        assert_eq!(vista.get_count().await.unwrap(), 2);
833    }
834}