Skip to main content

fsqlite_func/
vtab.rs

1//! Virtual table and cursor traits (§9.3).
2//!
3//! Virtual tables expose external data sources as SQL tables. They follow
4//! the SQLite xCreate/xConnect/xBestIndex/xFilter/xNext protocol.
5//!
6//! These traits are **open** (user-implementable). Extension authors
7//! implement them to create custom virtual table modules.
8//!
9//! # Cx on I/O Methods
10//!
11//! Methods that perform I/O accept `&Cx` for cancellation and deadline
12//! propagation. Lightweight accessors (`eof`, `column`, `rowid`) do not
13//! require `&Cx` since they operate on already-fetched row data.
14
15use std::any::Any;
16
17use fsqlite_error::{FrankenError, Result};
18use fsqlite_types::SqliteValue;
19use fsqlite_types::cx::Cx;
20
21// ---------------------------------------------------------------------------
22// Query planner types
23// ---------------------------------------------------------------------------
24
25/// Comparison operator for an index constraint.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum ConstraintOp {
28    Eq,
29    Gt,
30    Le,
31    Lt,
32    Ge,
33    Match,
34    Like,
35    Glob,
36    Regexp,
37    Ne,
38    IsNot,
39    IsNotNull,
40    IsNull,
41    Is,
42}
43
44/// A single constraint from the WHERE clause that the planner is considering.
45#[derive(Debug, Clone)]
46pub struct IndexConstraint {
47    /// Column index (0-based; `-1` for rowid).
48    pub column: i32,
49    /// The comparison operator.
50    pub op: ConstraintOp,
51    /// Whether the planner considers this constraint usable.
52    pub usable: bool,
53}
54
55/// A single ORDER BY term from the query.
56#[derive(Debug, Clone)]
57pub struct IndexOrderBy {
58    /// Column index (0-based).
59    pub column: i32,
60    /// `true` if descending, `false` if ascending.
61    pub desc: bool,
62}
63
64/// Per-constraint usage information set by `best_index`.
65#[derive(Debug, Clone, Default)]
66pub struct IndexConstraintUsage {
67    /// 1-based index into the `args` array passed to `filter`.
68    /// 0 means this constraint is not consumed by the vtab.
69    pub argv_index: i32,
70    /// If `true`, the vtab guarantees this constraint is satisfied and
71    /// the core need not double-check it.
72    pub omit: bool,
73}
74
75/// Information exchanged between the query planner and virtual table
76/// during index selection.
77///
78/// The planner fills `constraints` and `order_by`. The vtab fills
79/// `constraint_usage`, `idx_num`, `idx_str`, `order_by_consumed`,
80/// `estimated_cost`, and `estimated_rows`.
81#[derive(Debug, Clone)]
82pub struct IndexInfo {
83    /// WHERE clause constraints the planner is considering.
84    pub constraints: Vec<IndexConstraint>,
85    /// ORDER BY terms from the query.
86    pub order_by: Vec<IndexOrderBy>,
87    /// How each constraint maps to filter arguments (vtab fills this).
88    pub constraint_usage: Vec<IndexConstraintUsage>,
89    /// Integer identifier for the chosen index strategy.
90    pub idx_num: i32,
91    /// Optional string identifier for the chosen index strategy.
92    pub idx_str: Option<String>,
93    /// Whether the vtab guarantees the output is already sorted.
94    pub order_by_consumed: bool,
95    /// Estimated cost of the scan (lower is better).
96    pub estimated_cost: f64,
97    /// Estimated number of rows returned.
98    pub estimated_rows: i64,
99}
100
101impl IndexInfo {
102    /// Create a new `IndexInfo` with the given constraints and order-by terms.
103    #[must_use]
104    pub fn new(constraints: Vec<IndexConstraint>, order_by: Vec<IndexOrderBy>) -> Self {
105        let usage_len = constraints.len();
106        Self {
107            constraints,
108            order_by,
109            constraint_usage: vec![IndexConstraintUsage::default(); usage_len],
110            idx_num: 0,
111            idx_str: None,
112            order_by_consumed: false,
113            estimated_cost: 1_000_000.0,
114            estimated_rows: 1_000_000,
115        }
116    }
117}
118
119// ---------------------------------------------------------------------------
120// Column context
121// ---------------------------------------------------------------------------
122
123/// A context object passed to [`VirtualTableCursor::column`] for writing
124/// the column value.
125///
126/// Analogous to C SQLite's `sqlite3_context*` used with `sqlite3_result_*`.
127#[derive(Debug, Default)]
128pub struct ColumnContext {
129    value: Option<SqliteValue>,
130}
131
132impl ColumnContext {
133    /// Create a new empty column context.
134    #[must_use]
135    pub fn new() -> Self {
136        Self { value: None }
137    }
138
139    /// Set the value for this column.
140    pub fn set_value(&mut self, val: SqliteValue) {
141        self.value = Some(val);
142    }
143
144    /// Take the value out of this context, leaving `None`.
145    pub fn take_value(&mut self) -> Option<SqliteValue> {
146        self.value.take()
147    }
148}
149
150/// Snapshot-backed transaction/savepoint state for mutable virtual tables.
151///
152/// Virtual table implementations that keep their authoritative state in memory
153/// can use this helper to participate in connection-level `BEGIN`/`COMMIT`/
154/// `ROLLBACK` and savepoint recovery without wiring their own savepoint stack.
155#[derive(Debug, Clone)]
156pub struct TransactionalVtabState<S: Clone> {
157    base_snapshot: Option<S>,
158    savepoints: Vec<(i32, S)>,
159}
160
161impl<S: Clone> Default for TransactionalVtabState<S> {
162    fn default() -> Self {
163        Self {
164            base_snapshot: None,
165            savepoints: Vec::new(),
166        }
167    }
168}
169
170impl<S: Clone> TransactionalVtabState<S> {
171    /// Mark the start of a virtual-table transaction.
172    pub fn begin(&mut self, snapshot: S) {
173        if self.base_snapshot.is_none() {
174            self.base_snapshot = Some(snapshot);
175            self.savepoints.clear();
176        }
177    }
178
179    /// Drop all transactional snapshots after a successful commit.
180    pub fn commit(&mut self) {
181        self.base_snapshot = None;
182        self.savepoints.clear();
183    }
184
185    /// Return the transaction-begin snapshot for a full rollback.
186    pub fn rollback(&mut self) -> Option<S> {
187        let snapshot = self.base_snapshot.take();
188        self.savepoints.clear();
189        snapshot
190    }
191
192    /// Record the current state at savepoint `level`.
193    pub fn savepoint(&mut self, level: i32, snapshot: S) {
194        if self.base_snapshot.is_none() {
195            return;
196        }
197        self.savepoints.retain(|(existing, _)| *existing < level);
198        self.savepoints.push((level, snapshot));
199    }
200
201    /// Drop savepoint snapshots at `level` and deeper.
202    pub fn release(&mut self, level: i32) {
203        if self.base_snapshot.is_none() {
204            return;
205        }
206        self.savepoints.retain(|(existing, _)| *existing < level);
207    }
208
209    /// Return the snapshot recorded for savepoint `level`, keeping that
210    /// savepoint active and discarding deeper ones.
211    ///
212    /// If the virtual table joined the transaction after outer savepoints were
213    /// already active, SQLite only gives it a snapshot for the current level.
214    /// Falling back to the transaction-begin snapshot lets `ROLLBACK TO` an
215    /// older savepoint restore the correct pre-transaction state.
216    pub fn rollback_to(&mut self, level: i32) -> Option<S> {
217        self.base_snapshot.as_ref()?;
218        let snapshot = self
219            .savepoints
220            .iter()
221            .rfind(|(existing, _)| *existing == level)
222            .map(|(_, snapshot)| snapshot.clone())
223            .or_else(|| self.base_snapshot.clone());
224        if snapshot.is_some() {
225            self.savepoints.retain(|(existing, _)| *existing <= level);
226        }
227        snapshot
228    }
229}
230
231// ---------------------------------------------------------------------------
232// Module metadata
233// ---------------------------------------------------------------------------
234
235/// Classification for a schema object named by a virtual-table module.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
237pub enum ShadowTableKind {
238    /// The name is not a module-owned shadow table.
239    #[default]
240    Ordinary,
241    /// The name is a module-owned shadow table.
242    Shadow,
243}
244
245/// Access decision for a shadow-table operation class.
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
247pub enum ShadowTableAccess {
248    /// The operation is permitted by the module policy.
249    #[default]
250    Allow,
251    /// The operation is rejected by the module policy.
252    Deny,
253}
254
255impl ShadowTableAccess {
256    /// Whether the access decision permits the operation.
257    #[must_use]
258    pub const fn is_allowed(self) -> bool {
259        matches!(self, Self::Allow)
260    }
261}
262
263/// Policy returned by a module when the core asks whether a table name is a
264/// shadow table of a virtual table instance.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct ShadowTablePolicy {
267    /// Whether the table is ordinary or shadow-owned.
268    pub kind: ShadowTableKind,
269    /// User-authored INSERT/UPDATE/DELETE against this schema object.
270    pub direct_dml: ShadowTableAccess,
271    /// User-authored schema changes such as CREATE TRIGGER on this object.
272    pub schema_ddl: ShadowTableAccess,
273    /// Writes performed by the owning module while applying its own lifecycle.
274    pub module_internal_write: ShadowTableAccess,
275}
276
277impl ShadowTablePolicy {
278    /// Policy for an ordinary, non-shadow table.
279    #[must_use]
280    pub const fn ordinary() -> Self {
281        Self {
282            kind: ShadowTableKind::Ordinary,
283            direct_dml: ShadowTableAccess::Allow,
284            schema_ddl: ShadowTableAccess::Allow,
285            module_internal_write: ShadowTableAccess::Allow,
286        }
287    }
288
289    /// Policy for a module-owned shadow table.
290    #[must_use]
291    pub const fn owned_shadow() -> Self {
292        Self {
293            kind: ShadowTableKind::Shadow,
294            direct_dml: ShadowTableAccess::Deny,
295            schema_ddl: ShadowTableAccess::Deny,
296            module_internal_write: ShadowTableAccess::Allow,
297        }
298    }
299
300    /// Whether the table is module-owned shadow state.
301    #[must_use]
302    pub const fn is_shadow(self) -> bool {
303        matches!(self.kind, ShadowTableKind::Shadow)
304    }
305
306    /// Whether user-authored INSERT/UPDATE/DELETE should be accepted.
307    #[must_use]
308    pub const fn allows_direct_dml(self) -> bool {
309        self.direct_dml.is_allowed()
310    }
311
312    /// Whether user-authored schema DDL such as CREATE TRIGGER should be accepted.
313    #[must_use]
314    pub const fn allows_schema_ddl(self) -> bool {
315        self.schema_ddl.is_allowed()
316    }
317
318    /// Whether the owning module may write this object internally.
319    #[must_use]
320    pub const fn allows_module_internal_write(self) -> bool {
321        self.module_internal_write.is_allowed()
322    }
323}
324
325impl Default for ShadowTablePolicy {
326    fn default() -> Self {
327        Self::ordinary()
328    }
329}
330
331/// Lifecycle shape a module exposes to the connection/catalog layer.
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
333pub enum VtabLifecyclePolicy {
334    /// `create` and `connect` are effectively the same operation.
335    #[default]
336    Simple,
337    /// The module distinguishes create-time and connect-time lifecycle.
338    SeparateCreateAndConnect,
339}
340
341/// Integrity surface advertised by a module.
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
343pub enum VtabIntegrityPolicy {
344    /// No module-specific integrity entry point is exposed.
345    #[default]
346    None,
347    /// Integrity checks are module-defined and may inspect shadow state.
348    ShadowAware,
349}
350
351/// Defensive/risk metadata analogous to SQLite's vtab safety flags.
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
353pub struct VtabRiskLevel {
354    /// Safe to invoke in defensive contexts.
355    pub innocuous: bool,
356    /// Must not be invoked from schema or trigger contexts.
357    pub direct_only: bool,
358    /// May consult objects outside the current schema.
359    pub uses_all_schemas: bool,
360}
361
362impl VtabRiskLevel {
363    /// Risk profile for an innocuous module.
364    #[must_use]
365    pub const fn innocuous() -> Self {
366        Self {
367            innocuous: true,
368            direct_only: false,
369            uses_all_schemas: false,
370        }
371    }
372}
373
374/// Module-level metadata that future catalog and defensive checks can consult
375/// without hard-coding FTS5-specific behavior in unrelated layers.
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub struct VtabModuleMetadata {
378    /// Whether the module owns any shadow tables.
379    pub owns_shadow_tables: bool,
380    /// Whether create/connect semantics differ.
381    pub lifecycle: VtabLifecyclePolicy,
382    /// Whether the module exposes integrity hooks.
383    pub integrity: VtabIntegrityPolicy,
384    /// Defensive-execution metadata.
385    pub risk: VtabRiskLevel,
386}
387
388impl VtabModuleMetadata {
389    /// Metadata for ordinary modules with no shadow-table contract.
390    #[must_use]
391    pub const fn ordinary() -> Self {
392        Self {
393            owns_shadow_tables: false,
394            lifecycle: VtabLifecyclePolicy::Simple,
395            integrity: VtabIntegrityPolicy::None,
396            risk: VtabRiskLevel::innocuous(),
397        }
398    }
399
400    /// Metadata for a shadow-owning module.
401    #[must_use]
402    pub const fn shadow_owning(
403        lifecycle: VtabLifecyclePolicy,
404        integrity: VtabIntegrityPolicy,
405        risk: VtabRiskLevel,
406    ) -> Self {
407        Self {
408            owns_shadow_tables: true,
409            lifecycle,
410            integrity,
411            risk,
412        }
413    }
414}
415
416impl Default for VtabModuleMetadata {
417    fn default() -> Self {
418        Self::ordinary()
419    }
420}
421
422// ---------------------------------------------------------------------------
423// VirtualTable trait
424// ---------------------------------------------------------------------------
425
426/// A virtual table module.
427///
428/// Virtual tables expose external data sources as SQL tables. This trait
429/// covers the full lifecycle: creation, connection, scanning, mutation,
430/// and destruction.
431///
432/// This trait is **open** (user-implementable). The `Sized` bound on
433/// constructor methods (`create`, `connect`) allows the trait to be used
434/// as `dyn VirtualTable<Cursor = C>` for other methods.
435///
436/// # Default Implementations
437///
438/// Most methods have sensible defaults. At minimum, you must implement
439/// `connect`, `best_index`, and `open`.
440#[allow(clippy::missing_errors_doc)]
441pub trait VirtualTable: Send + Sync {
442    /// The cursor type for scanning this virtual table.
443    type Cursor: VirtualTableCursor;
444
445    /// Static metadata for the module as a whole.
446    fn module_metadata(_args: &[&str]) -> VtabModuleMetadata
447    where
448        Self: Sized,
449    {
450        VtabModuleMetadata::ordinary()
451    }
452
453    /// Determine whether `table_name` is a module-owned shadow table for the
454    /// virtual table instance named `vtab_name`.
455    fn shadow_table_policy(_vtab_name: &str, _table_name: &str) -> ShadowTablePolicy
456    where
457        Self: Sized,
458    {
459        ShadowTablePolicy::ordinary()
460    }
461
462    /// Called for `CREATE VIRTUAL TABLE`.
463    ///
464    /// May create backing storage. Default delegates to `connect`
465    /// (suitable for eponymous virtual tables).
466    fn create(cx: &Cx, args: &[&str]) -> Result<Self>
467    where
468        Self: Sized,
469    {
470        Self::connect(cx, args)
471    }
472
473    /// Called for subsequent opens of an existing virtual table.
474    fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
475    where
476        Self: Sized;
477
478    /// Inform the query planner about available indexes and their costs.
479    fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
480
481    /// Open a new scan cursor.
482    fn open(&self) -> Result<Self::Cursor>;
483
484    /// Drop a virtual table instance (opposite of `connect`).
485    fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
486        Ok(())
487    }
488
489    /// Called for `DROP VIRTUAL TABLE` — destroy backing storage.
490    ///
491    /// Default delegates to `disconnect`.
492    fn destroy(&mut self, cx: &Cx) -> Result<()> {
493        self.disconnect(cx)
494    }
495
496    /// INSERT/UPDATE/DELETE on the virtual table.
497    ///
498    /// - `args[0]`: old rowid (`None` for INSERT)
499    /// - `args[1]`: new rowid
500    /// - `args[2..]`: column values
501    ///
502    /// Returns the new rowid for INSERT, `None` for UPDATE/DELETE.
503    ///
504    /// Default returns [`FrankenError::ReadOnly`] (read-only virtual tables).
505    fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
506        Err(FrankenError::ReadOnly)
507    }
508
509    /// Begin a virtual table transaction.
510    fn begin(&mut self, _cx: &Cx) -> Result<()> {
511        Ok(())
512    }
513
514    /// Sync a virtual table transaction (phase 1 of 2PC).
515    fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
516        Ok(())
517    }
518
519    /// Commit a virtual table transaction.
520    fn commit(&mut self, _cx: &Cx) -> Result<()> {
521        Ok(())
522    }
523
524    /// Roll back a virtual table transaction.
525    fn rollback(&mut self, _cx: &Cx) -> Result<()> {
526        Ok(())
527    }
528
529    /// Rename the virtual table.
530    ///
531    /// Default returns [`FrankenError::Unsupported`].
532    fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
533        Err(FrankenError::Unsupported)
534    }
535
536    /// Create a savepoint at level `n`.
537    fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
538        Ok(())
539    }
540
541    /// Release savepoint at level `n`.
542    fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
543        Ok(())
544    }
545
546    /// Roll back to savepoint at level `n`.
547    fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
548        Ok(())
549    }
550}
551
552// ---------------------------------------------------------------------------
553// VirtualTableCursor trait
554// ---------------------------------------------------------------------------
555
556/// A cursor for scanning a virtual table.
557///
558/// Cursors are `Send` but **NOT** `Sync` — they are single-threaded
559/// scan objects bound to a specific filter invocation.
560///
561/// # Lifecycle
562///
563/// 1. [`filter`](Self::filter) begins a scan with planner-chosen parameters.
564/// 2. Iterate: check [`eof`](Self::eof), read [`column`](Self::column)/[`rowid`](Self::rowid), advance with [`next`](Self::next).
565/// 3. The cursor is dropped when the scan is complete.
566#[allow(clippy::missing_errors_doc)]
567pub trait VirtualTableCursor: Send {
568    /// Begin a scan with the filter parameters chosen by `best_index`.
569    fn filter(
570        &mut self,
571        cx: &Cx,
572        idx_num: i32,
573        idx_str: Option<&str>,
574        args: &[SqliteValue],
575    ) -> Result<()>;
576
577    /// Advance to the next row.
578    fn next(&mut self, cx: &Cx) -> Result<()>;
579
580    /// Whether the cursor has moved past the last row.
581    fn eof(&self) -> bool;
582
583    /// Write the value of column `col` into `ctx`.
584    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
585
586    /// Return the rowid of the current row.
587    fn rowid(&self) -> Result<i64>;
588}
589
590// ---------------------------------------------------------------------------
591// Module factory & type erasure
592// ---------------------------------------------------------------------------
593
594/// A type-erased virtual table module factory.
595///
596/// Registered with the connection via `register_module("name", factory)`.
597/// When `CREATE VIRTUAL TABLE ... USING name(args)` is executed, the
598/// factory's `create` method is called to produce a concrete vtab instance.
599#[allow(clippy::missing_errors_doc)]
600pub trait VtabModuleFactory: Send + Sync {
601    /// Create a new virtual table instance for `CREATE VIRTUAL TABLE`.
602    fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>>;
603
604    /// Connect to an existing virtual table (subsequent opens).
605    fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
606        self.create(cx, args)
607    }
608
609    /// Column names and affinities for the virtual table schema.
610    fn column_info(&self, _args: &[&str]) -> Vec<(String, char)> {
611        Vec::new()
612    }
613
614    /// Static metadata for the module as a whole.
615    fn module_metadata(&self, _args: &[&str]) -> VtabModuleMetadata {
616        VtabModuleMetadata::ordinary()
617    }
618
619    /// Determine whether `table_name` is a module-owned shadow table for the
620    /// virtual table instance named `vtab_name`.
621    fn shadow_table_policy(&self, _vtab_name: &str, _table_name: &str) -> ShadowTablePolicy {
622        ShadowTablePolicy::ordinary()
623    }
624}
625
626/// A type-erased virtual table instance.
627#[allow(clippy::missing_errors_doc)]
628pub trait ErasedVtabInstance: Send + Sync {
629    /// Return this instance as `Any` for downcasting to concrete extension types.
630    fn as_any(&self) -> &dyn Any;
631    /// Return this instance as mutable `Any` for downcasting to concrete extension types.
632    fn as_any_mut(&mut self) -> &mut dyn Any;
633    /// Open a new scan cursor.
634    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
635    /// INSERT/UPDATE/DELETE on the virtual table.
636    fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
637    /// Begin a virtual table transaction.
638    fn begin(&mut self, cx: &Cx) -> Result<()>;
639    /// Sync a virtual table transaction.
640    fn sync_txn(&mut self, cx: &Cx) -> Result<()>;
641    /// Commit a virtual table transaction.
642    fn commit(&mut self, cx: &Cx) -> Result<()>;
643    /// Roll back a virtual table transaction.
644    fn rollback(&mut self, cx: &Cx) -> Result<()>;
645    /// Create a savepoint at level `n`.
646    fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()>;
647    /// Release savepoint at level `n`.
648    fn release(&mut self, cx: &Cx, n: i32) -> Result<()>;
649    /// Roll back to savepoint at level `n`.
650    fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()>;
651    /// Destroy the virtual table.
652    fn destroy(&mut self, cx: &Cx) -> Result<()>;
653    /// Rename the virtual table.
654    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
655    /// Inform the query planner about available indexes.
656    fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
657}
658
659/// A type-erased virtual table cursor.
660#[allow(clippy::missing_errors_doc)]
661pub trait ErasedVtabCursor: Send {
662    /// Begin a scan with filter parameters.
663    fn erased_filter(
664        &mut self,
665        cx: &Cx,
666        idx_num: i32,
667        idx_str: Option<&str>,
668        args: &[SqliteValue],
669    ) -> Result<()>;
670    /// Advance to the next row.
671    fn erased_next(&mut self, cx: &Cx) -> Result<()>;
672    /// Whether the cursor has moved past the last row.
673    fn erased_eof(&self) -> bool;
674    /// Write the value of column `col` into `ctx`.
675    fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
676    /// Return the rowid of the current row.
677    fn erased_rowid(&self) -> Result<i64>;
678}
679
680/// Blanket `ErasedVtabCursor` for any concrete cursor.
681impl<C: VirtualTableCursor + 'static> ErasedVtabCursor for C {
682    fn erased_filter(
683        &mut self,
684        cx: &Cx,
685        idx_num: i32,
686        idx_str: Option<&str>,
687        args: &[SqliteValue],
688    ) -> Result<()> {
689        VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
690    }
691    fn erased_next(&mut self, cx: &Cx) -> Result<()> {
692        VirtualTableCursor::next(self, cx)
693    }
694    fn erased_eof(&self) -> bool {
695        VirtualTableCursor::eof(self)
696    }
697    fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
698        VirtualTableCursor::column(self, ctx, col)
699    }
700    fn erased_rowid(&self) -> Result<i64> {
701        VirtualTableCursor::rowid(self)
702    }
703}
704
705/// Blanket `ErasedVtabInstance` for any concrete `VirtualTable`.
706impl<T: VirtualTable + 'static> ErasedVtabInstance for T
707where
708    T::Cursor: 'static,
709{
710    fn as_any(&self) -> &dyn Any {
711        self
712    }
713
714    fn as_any_mut(&mut self) -> &mut dyn Any {
715        self
716    }
717
718    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
719        let cursor = VirtualTable::open(self)?;
720        Ok(Box::new(cursor))
721    }
722    fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
723        VirtualTable::update(self, cx, args)
724    }
725    fn begin(&mut self, cx: &Cx) -> Result<()> {
726        VirtualTable::begin(self, cx)
727    }
728    fn sync_txn(&mut self, cx: &Cx) -> Result<()> {
729        VirtualTable::sync_txn(self, cx)
730    }
731    fn commit(&mut self, cx: &Cx) -> Result<()> {
732        VirtualTable::commit(self, cx)
733    }
734    fn rollback(&mut self, cx: &Cx) -> Result<()> {
735        VirtualTable::rollback(self, cx)
736    }
737    fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()> {
738        VirtualTable::savepoint(self, cx, n)
739    }
740    fn release(&mut self, cx: &Cx, n: i32) -> Result<()> {
741        VirtualTable::release(self, cx, n)
742    }
743    fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()> {
744        VirtualTable::rollback_to(self, cx, n)
745    }
746    fn destroy(&mut self, cx: &Cx) -> Result<()> {
747        VirtualTable::destroy(self, cx)
748    }
749    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
750        VirtualTable::rename(self, cx, new_name)
751    }
752    fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
753        VirtualTable::best_index(self, info)
754    }
755}
756
757/// Create a `VtabModuleFactory` from a `VirtualTable` type.
758pub fn module_factory_from<T>() -> impl VtabModuleFactory
759where
760    T: VirtualTable + 'static,
761    T::Cursor: 'static,
762{
763    struct Factory<T: Send + Sync>(std::marker::PhantomData<T>);
764
765    impl<T: VirtualTable + 'static> VtabModuleFactory for Factory<T>
766    where
767        T::Cursor: 'static,
768    {
769        fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
770            let vtab = T::create(cx, args)?;
771            Ok(Box::new(vtab))
772        }
773        fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
774            let vtab = T::connect(cx, args)?;
775            Ok(Box::new(vtab))
776        }
777
778        fn module_metadata(&self, args: &[&str]) -> VtabModuleMetadata {
779            T::module_metadata(args)
780        }
781
782        fn shadow_table_policy(&self, vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
783            T::shadow_table_policy(vtab_name, table_name)
784        }
785    }
786
787    Factory::<T>(std::marker::PhantomData)
788}
789
790// ---------------------------------------------------------------------------
791// Tests
792// ---------------------------------------------------------------------------
793
794#[cfg(test)]
795#[allow(clippy::too_many_lines)]
796mod tests {
797    use super::*;
798
799    // -- Mock: generate_series(start, stop) virtual table --
800
801    struct GenerateSeries {
802        destroyed: bool,
803    }
804
805    struct GenerateSeriesCursor {
806        start: i64,
807        stop: i64,
808        current: i64,
809    }
810
811    impl VirtualTable for GenerateSeries {
812        type Cursor = GenerateSeriesCursor;
813
814        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
815            Ok(Self { destroyed: false })
816        }
817
818        fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
819            info.estimated_cost = 10.0;
820            info.estimated_rows = 100;
821            info.idx_num = 1;
822
823            // Mark constraint 0 as consumed, mapped to filter arg 1.
824            if !info.constraints.is_empty() && info.constraints[0].usable {
825                info.constraint_usage[0].argv_index = 1;
826                info.constraint_usage[0].omit = true;
827            }
828            Ok(())
829        }
830
831        fn open(&self) -> Result<GenerateSeriesCursor> {
832            Ok(GenerateSeriesCursor {
833                start: 0,
834                stop: 0,
835                current: 0,
836            })
837        }
838
839        fn destroy(&mut self, _cx: &Cx) -> Result<()> {
840            self.destroyed = true;
841            Ok(())
842        }
843    }
844
845    impl VirtualTableCursor for GenerateSeriesCursor {
846        fn filter(
847            &mut self,
848            _cx: &Cx,
849            _idx_num: i32,
850            _idx_str: Option<&str>,
851            args: &[SqliteValue],
852        ) -> Result<()> {
853            self.start = args.first().map_or(1, SqliteValue::to_integer);
854            self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
855            self.current = self.start;
856            Ok(())
857        }
858
859        fn next(&mut self, _cx: &Cx) -> Result<()> {
860            self.current += 1;
861            Ok(())
862        }
863
864        fn eof(&self) -> bool {
865            self.current > self.stop
866        }
867
868        fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
869            if self.eof() {
870                ctx.set_value(SqliteValue::Null);
871                return Ok(());
872            }
873            ctx.set_value(SqliteValue::Integer(self.current));
874            Ok(())
875        }
876
877        fn rowid(&self) -> Result<i64> {
878            Ok(if self.eof() { 0 } else { self.current })
879        }
880    }
881
882    // -- Mock: read-only vtab for default update test --
883
884    struct ReadOnlyVtab;
885
886    struct ReadOnlyCursor;
887
888    impl VirtualTable for ReadOnlyVtab {
889        type Cursor = ReadOnlyCursor;
890
891        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
892            Ok(Self)
893        }
894
895        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
896            Ok(())
897        }
898
899        fn open(&self) -> Result<ReadOnlyCursor> {
900            Ok(ReadOnlyCursor)
901        }
902    }
903
904    impl VirtualTableCursor for ReadOnlyCursor {
905        fn filter(
906            &mut self,
907            _cx: &Cx,
908            _idx_num: i32,
909            _idx_str: Option<&str>,
910            _args: &[SqliteValue],
911        ) -> Result<()> {
912            Ok(())
913        }
914
915        fn next(&mut self, _cx: &Cx) -> Result<()> {
916            Ok(())
917        }
918
919        fn eof(&self) -> bool {
920            true
921        }
922
923        fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
924            ctx.set_value(SqliteValue::Null);
925            Ok(())
926        }
927
928        fn rowid(&self) -> Result<i64> {
929            Ok(0)
930        }
931    }
932
933    // -- Mock: writable vtab for insert test --
934
935    struct WritableVtab {
936        rows: Vec<(i64, Vec<SqliteValue>)>,
937        next_rowid: i64,
938    }
939
940    struct WritableCursor {
941        rows: Vec<(i64, Vec<SqliteValue>)>,
942        pos: usize,
943    }
944
945    impl VirtualTable for WritableVtab {
946        type Cursor = WritableCursor;
947
948        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
949            Ok(Self {
950                rows: Vec::new(),
951                next_rowid: 1,
952            })
953        }
954
955        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
956            Ok(())
957        }
958
959        fn open(&self) -> Result<WritableCursor> {
960            Ok(WritableCursor {
961                rows: self.rows.clone(),
962                pos: 0,
963            })
964        }
965
966        fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
967            // args[0] = old rowid (Null for INSERT)
968            if args[0].is_null() {
969                // INSERT
970                let rowid = self.next_rowid;
971                self.next_rowid += 1;
972                let cols: Vec<SqliteValue> = args[2..].to_vec();
973                self.rows.push((rowid, cols));
974                return Ok(Some(rowid));
975            }
976            Ok(None)
977        }
978    }
979
980    impl VirtualTableCursor for WritableCursor {
981        fn filter(
982            &mut self,
983            _cx: &Cx,
984            _idx_num: i32,
985            _idx_str: Option<&str>,
986            _args: &[SqliteValue],
987        ) -> Result<()> {
988            self.pos = 0;
989            Ok(())
990        }
991
992        fn next(&mut self, _cx: &Cx) -> Result<()> {
993            self.pos += 1;
994            Ok(())
995        }
996
997        fn eof(&self) -> bool {
998            self.pos >= self.rows.len()
999        }
1000
1001        fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
1002            if self.eof() {
1003                ctx.set_value(SqliteValue::Null);
1004                return Ok(());
1005            }
1006
1007            #[allow(clippy::cast_sign_loss)]
1008            let col_idx = col as usize;
1009            if let Some((_, cols)) = self.rows.get(self.pos) {
1010                if let Some(val) = cols.get(col_idx) {
1011                    ctx.set_value(val.clone());
1012                    return Ok(());
1013                }
1014            }
1015            ctx.set_value(SqliteValue::Null);
1016            Ok(())
1017        }
1018
1019        fn rowid(&self) -> Result<i64> {
1020            self.rows
1021                .get(self.pos)
1022                .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
1023        }
1024    }
1025
1026    struct ShadowOwningVtab;
1027
1028    impl VirtualTable for ShadowOwningVtab {
1029        type Cursor = ReadOnlyCursor;
1030
1031        fn module_metadata(_args: &[&str]) -> VtabModuleMetadata {
1032            VtabModuleMetadata::shadow_owning(
1033                VtabLifecyclePolicy::SeparateCreateAndConnect,
1034                VtabIntegrityPolicy::ShadowAware,
1035                VtabRiskLevel {
1036                    innocuous: false,
1037                    direct_only: true,
1038                    uses_all_schemas: false,
1039                },
1040            )
1041        }
1042
1043        fn shadow_table_policy(vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
1044            let Some((owner, suffix)) = table_name.rsplit_once('_') else {
1045                return ShadowTablePolicy::ordinary();
1046            };
1047
1048            if owner == vtab_name
1049                && matches!(suffix, "config" | "content" | "data" | "docsize" | "idx")
1050            {
1051                return ShadowTablePolicy::owned_shadow();
1052            }
1053
1054            ShadowTablePolicy::ordinary()
1055        }
1056
1057        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1058            Ok(Self)
1059        }
1060
1061        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1062            Ok(())
1063        }
1064
1065        fn open(&self) -> Result<Self::Cursor> {
1066            Ok(ReadOnlyCursor)
1067        }
1068    }
1069
1070    #[derive(Debug, Clone, PartialEq, Eq)]
1071    struct HookSnapshot {
1072        version: i32,
1073    }
1074
1075    struct HookAwareVtab {
1076        version: i32,
1077        syncs: usize,
1078        tx_state: TransactionalVtabState<HookSnapshot>,
1079    }
1080
1081    impl VirtualTable for HookAwareVtab {
1082        type Cursor = ReadOnlyCursor;
1083
1084        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1085            Ok(Self {
1086                version: 7,
1087                syncs: 0,
1088                tx_state: TransactionalVtabState::default(),
1089            })
1090        }
1091
1092        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1093            Ok(())
1094        }
1095
1096        fn open(&self) -> Result<Self::Cursor> {
1097            Ok(ReadOnlyCursor)
1098        }
1099
1100        fn begin(&mut self, _cx: &Cx) -> Result<()> {
1101            self.tx_state.begin(HookSnapshot {
1102                version: self.version,
1103            });
1104            Ok(())
1105        }
1106
1107        fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
1108            self.syncs += 1;
1109            Ok(())
1110        }
1111
1112        fn savepoint(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1113            self.tx_state.savepoint(
1114                n,
1115                HookSnapshot {
1116                    version: self.version,
1117                },
1118            );
1119            Ok(())
1120        }
1121
1122        fn release(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1123            self.tx_state.release(n);
1124            Ok(())
1125        }
1126
1127        fn rollback_to(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1128            if let Some(snapshot) = self.tx_state.rollback_to(n) {
1129                self.version = snapshot.version;
1130            }
1131            Ok(())
1132        }
1133
1134        fn commit(&mut self, _cx: &Cx) -> Result<()> {
1135            self.tx_state.commit();
1136            Ok(())
1137        }
1138
1139        fn rollback(&mut self, _cx: &Cx) -> Result<()> {
1140            if let Some(snapshot) = self.tx_state.rollback() {
1141                self.version = snapshot.version;
1142            }
1143            Ok(())
1144        }
1145    }
1146
1147    // -- Tests --
1148
1149    #[test]
1150    fn test_vtab_create_vs_connect() {
1151        let cx = Cx::new();
1152
1153        // create delegates to connect by default.
1154        let vtab = GenerateSeries::create(&cx, &[]).unwrap();
1155        assert!(!vtab.destroyed);
1156
1157        // connect works directly.
1158        let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
1159        assert!(!vtab2.destroyed);
1160    }
1161
1162    #[test]
1163    fn test_vtab_best_index_populates_info() {
1164        let cx = Cx::new();
1165        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1166
1167        let mut info = IndexInfo::new(
1168            vec![IndexConstraint {
1169                column: 0,
1170                op: ConstraintOp::Gt,
1171                usable: true,
1172            }],
1173            vec![],
1174        );
1175
1176        VirtualTable::best_index(&vtab, &mut info).unwrap();
1177
1178        assert_eq!(info.idx_num, 1);
1179        assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
1180        assert_eq!(info.estimated_rows, 100);
1181        assert_eq!(info.constraint_usage[0].argv_index, 1);
1182        assert!(info.constraint_usage[0].omit);
1183    }
1184
1185    #[test]
1186    fn test_vtab_cursor_filter_next_eof() {
1187        let cx = Cx::new();
1188        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1189        let mut cursor = vtab.open().unwrap();
1190
1191        cursor
1192            .filter(
1193                &cx,
1194                0,
1195                None,
1196                &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
1197            )
1198            .unwrap();
1199
1200        let mut values = Vec::new();
1201        while !cursor.eof() {
1202            let mut ctx = ColumnContext::new();
1203            cursor.column(&mut ctx, 0).unwrap();
1204            let rowid = cursor.rowid().unwrap();
1205            values.push((rowid, ctx.take_value().unwrap()));
1206            cursor.next(&cx).unwrap();
1207        }
1208
1209        assert_eq!(values.len(), 3);
1210        assert_eq!(values[0], (1, SqliteValue::Integer(1)));
1211        assert_eq!(values[1], (2, SqliteValue::Integer(2)));
1212        assert_eq!(values[2], (3, SqliteValue::Integer(3)));
1213    }
1214
1215    #[test]
1216    fn test_generate_series_cursor_past_end_returns_null_and_zero_rowid() {
1217        let cx = Cx::new();
1218        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1219        let mut cursor = vtab.open().unwrap();
1220
1221        cursor
1222            .filter(
1223                &cx,
1224                0,
1225                None,
1226                &[SqliteValue::Integer(1), SqliteValue::Integer(1)],
1227            )
1228            .unwrap();
1229        cursor.next(&cx).unwrap();
1230        assert!(cursor.eof());
1231
1232        let mut ctx = ColumnContext::new();
1233        cursor.column(&mut ctx, 0).unwrap();
1234        assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1235        assert_eq!(cursor.rowid().unwrap(), 0);
1236    }
1237
1238    #[test]
1239    fn test_writable_cursor_missing_column_returns_null() {
1240        let cx = Cx::new();
1241        let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1242        VirtualTable::update(
1243            &mut vtab,
1244            &cx,
1245            &[
1246                SqliteValue::Null,
1247                SqliteValue::Null,
1248                SqliteValue::Text("hello".into()),
1249            ],
1250        )
1251        .unwrap();
1252
1253        let mut cursor = vtab.open().unwrap();
1254        cursor.filter(&cx, 0, None, &[]).unwrap();
1255
1256        let mut ctx = ColumnContext::new();
1257        cursor.column(&mut ctx, 3).unwrap();
1258        assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1259
1260        cursor.next(&cx).unwrap();
1261        assert!(cursor.eof());
1262        cursor.column(&mut ctx, 0).unwrap();
1263        assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1264        assert_eq!(cursor.rowid().unwrap(), 0);
1265    }
1266
1267    #[test]
1268    fn test_vtab_update_insert() {
1269        let cx = Cx::new();
1270        let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1271
1272        // INSERT: args[0] = Null (no old rowid), args[1] = new rowid (ignored),
1273        // args[2..] = column values
1274        let result = VirtualTable::update(
1275            &mut vtab,
1276            &cx,
1277            &[
1278                SqliteValue::Null,
1279                SqliteValue::Null,
1280                SqliteValue::Text("hello".into()),
1281            ],
1282        )
1283        .unwrap();
1284
1285        assert_eq!(result, Some(1));
1286        assert_eq!(vtab.rows.len(), 1);
1287        assert_eq!(vtab.rows[0].0, 1);
1288    }
1289
1290    #[test]
1291    fn test_vtab_update_readonly_default() {
1292        let cx = Cx::new();
1293        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1294        let err = VirtualTable::update(&mut vtab, &cx, &[SqliteValue::Null]).unwrap_err();
1295        assert!(matches!(err, FrankenError::ReadOnly));
1296    }
1297
1298    #[test]
1299    fn test_vtab_destroy_vs_disconnect() {
1300        let cx = Cx::new();
1301
1302        // Default: destroy delegates to disconnect (both no-ops for ReadOnlyVtab).
1303        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1304        VirtualTable::disconnect(&mut vtab, &cx).unwrap();
1305        VirtualTable::destroy(&mut vtab, &cx).unwrap();
1306
1307        // Custom destroy sets a flag.
1308        let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1309        assert!(!vtab.destroyed);
1310        VirtualTable::destroy(&mut vtab, &cx).unwrap();
1311        assert!(vtab.destroyed);
1312    }
1313
1314    #[test]
1315    fn test_vtab_cursor_send_but_not_sync() {
1316        fn assert_send<T: Send>() {}
1317        assert_send::<GenerateSeriesCursor>();
1318
1319        // VirtualTableCursor is Send but NOT Sync.
1320        // We can't directly test "not Sync" at runtime, but we can verify
1321        // the trait bound: VirtualTableCursor: Send (not Send + Sync).
1322        // The type GenerateSeriesCursor IS actually Sync by coincidence
1323        // (all fields are i64), but the trait doesn't require it.
1324        // The key point: the trait signature says Send, not Send + Sync.
1325    }
1326
1327    #[test]
1328    fn test_column_context_lifecycle() {
1329        let mut ctx = ColumnContext::new();
1330        assert!(ctx.take_value().is_none());
1331
1332        ctx.set_value(SqliteValue::Integer(42));
1333        assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
1334
1335        // After take, it's None again.
1336        assert!(ctx.take_value().is_none());
1337    }
1338
1339    #[test]
1340    fn test_index_info_new() {
1341        let info = IndexInfo::new(
1342            vec![
1343                IndexConstraint {
1344                    column: 0,
1345                    op: ConstraintOp::Eq,
1346                    usable: true,
1347                },
1348                IndexConstraint {
1349                    column: 1,
1350                    op: ConstraintOp::Gt,
1351                    usable: false,
1352                },
1353            ],
1354            vec![IndexOrderBy {
1355                column: 0,
1356                desc: false,
1357            }],
1358        );
1359
1360        assert_eq!(info.constraints.len(), 2);
1361        assert_eq!(info.order_by.len(), 1);
1362        assert_eq!(info.constraint_usage.len(), 2);
1363        assert_eq!(info.idx_num, 0);
1364        assert!(info.idx_str.is_none());
1365        assert!(!info.order_by_consumed);
1366    }
1367
1368    #[test]
1369    fn test_transactional_vtab_state_tracks_savepoints() {
1370        let mut state = TransactionalVtabState::default();
1371
1372        state.begin(1_i32);
1373        state.savepoint(0, 2);
1374        state.savepoint(1, 3);
1375        assert_eq!(state.rollback_to(1), Some(3));
1376        state.release(1);
1377        assert_eq!(state.rollback(), Some(1));
1378        assert_eq!(state.rollback(), None);
1379    }
1380
1381    #[test]
1382    fn test_transactional_vtab_state_uses_base_for_late_enlistment() {
1383        let mut state = TransactionalVtabState::default();
1384
1385        state.begin(7_i32);
1386        state.savepoint(2, 9);
1387
1388        assert_eq!(state.rollback_to(1), Some(7));
1389        assert_eq!(state.rollback(), Some(7));
1390    }
1391
1392    #[test]
1393    fn test_shadow_table_policy_defaults_to_ordinary() {
1394        let policy = ReadOnlyVtab::shadow_table_policy("docs", "docs_data");
1395        assert_eq!(policy, ShadowTablePolicy::ordinary());
1396        assert!(!policy.is_shadow());
1397        assert!(policy.allows_direct_dml());
1398        assert!(policy.allows_schema_ddl());
1399        assert!(policy.allows_module_internal_write());
1400    }
1401
1402    #[test]
1403    fn test_owned_shadow_policy_blocks_user_dml_and_schema_ddl() {
1404        let policy = ShadowTablePolicy::owned_shadow();
1405
1406        assert!(policy.is_shadow());
1407        assert!(!policy.allows_direct_dml());
1408        assert!(!policy.allows_schema_ddl());
1409        assert!(policy.allows_module_internal_write());
1410    }
1411
1412    #[test]
1413    fn test_shadow_owning_module_metadata_is_forwarded_by_factory() {
1414        let factory = module_factory_from::<ShadowOwningVtab>();
1415        let metadata = factory.module_metadata(&[]);
1416
1417        assert!(metadata.owns_shadow_tables);
1418        assert_eq!(
1419            metadata.lifecycle,
1420            VtabLifecyclePolicy::SeparateCreateAndConnect
1421        );
1422        assert_eq!(metadata.integrity, VtabIntegrityPolicy::ShadowAware);
1423        assert!(metadata.risk.direct_only);
1424        assert!(!metadata.risk.innocuous);
1425    }
1426
1427    #[test]
1428    fn test_shadow_owning_module_matches_owned_shadow_tables() {
1429        let factory = module_factory_from::<ShadowOwningVtab>();
1430
1431        let owned = factory.shadow_table_policy("docs", "docs_data");
1432        let other_owner = factory.shadow_table_policy("docs", "posts_data");
1433        let unrelated = factory.shadow_table_policy("docs", "docs_segments");
1434
1435        assert_eq!(owned.kind, ShadowTableKind::Shadow);
1436        assert!(!owned.allows_direct_dml());
1437        assert!(!owned.allows_schema_ddl());
1438        assert!(owned.allows_module_internal_write());
1439        assert!(!other_owner.is_shadow());
1440        assert!(!unrelated.is_shadow());
1441        assert!(unrelated.allows_direct_dml());
1442    }
1443
1444    #[test]
1445    fn test_erased_vtab_instance_forwards_transaction_hooks() {
1446        let cx = Cx::new();
1447        let mut erased: Box<dyn ErasedVtabInstance> =
1448            Box::new(HookAwareVtab::connect(&cx, &[]).unwrap());
1449
1450        erased.begin(&cx).unwrap();
1451        {
1452            let hook = erased
1453                .as_any_mut()
1454                .downcast_mut::<HookAwareVtab>()
1455                .expect("hook-aware vtab");
1456            hook.version = 9;
1457        }
1458        erased.savepoint(&cx, 0).unwrap();
1459        {
1460            let hook = erased
1461                .as_any_mut()
1462                .downcast_mut::<HookAwareVtab>()
1463                .expect("hook-aware vtab");
1464            hook.version = 11;
1465        }
1466        erased.rollback_to(&cx, 0).unwrap();
1467        erased.release(&cx, 0).unwrap();
1468        erased.sync_txn(&cx).unwrap();
1469        erased.rollback(&cx).unwrap();
1470
1471        let hook = erased
1472            .as_any_mut()
1473            .downcast_mut::<HookAwareVtab>()
1474            .expect("hook-aware vtab");
1475        assert_eq!(hook.version, 7);
1476        assert_eq!(hook.syncs, 1);
1477    }
1478}