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