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/// Policy returned by a module when the core asks whether a table name is a
246/// shadow table of a virtual table instance.
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub struct ShadowTablePolicy {
249    /// Whether the table is ordinary or shadow-owned.
250    pub kind: ShadowTableKind,
251}
252
253impl ShadowTablePolicy {
254    /// Policy for an ordinary, non-shadow table.
255    #[must_use]
256    pub const fn ordinary() -> Self {
257        Self {
258            kind: ShadowTableKind::Ordinary,
259        }
260    }
261
262    /// Policy for a module-owned shadow table.
263    #[must_use]
264    pub const fn owned_shadow() -> Self {
265        Self {
266            kind: ShadowTableKind::Shadow,
267        }
268    }
269
270    /// Whether the table is module-owned shadow state.
271    #[must_use]
272    pub const fn is_shadow(self) -> bool {
273        matches!(self.kind, ShadowTableKind::Shadow)
274    }
275}
276
277impl Default for ShadowTablePolicy {
278    fn default() -> Self {
279        Self::ordinary()
280    }
281}
282
283/// Lifecycle shape a module exposes to the connection/catalog layer.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
285pub enum VtabLifecyclePolicy {
286    /// `create` and `connect` are effectively the same operation.
287    #[default]
288    Simple,
289    /// The module distinguishes create-time and connect-time lifecycle.
290    SeparateCreateAndConnect,
291}
292
293/// Integrity surface advertised by a module.
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
295pub enum VtabIntegrityPolicy {
296    /// No module-specific integrity entry point is exposed.
297    #[default]
298    None,
299    /// Integrity checks are module-defined and may inspect shadow state.
300    ShadowAware,
301}
302
303/// Defensive/risk metadata analogous to SQLite's vtab safety flags.
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
305pub struct VtabRiskLevel {
306    /// Safe to invoke in defensive contexts.
307    pub innocuous: bool,
308    /// Must not be invoked from schema or trigger contexts.
309    pub direct_only: bool,
310    /// May consult objects outside the current schema.
311    pub uses_all_schemas: bool,
312}
313
314impl VtabRiskLevel {
315    /// Risk profile for an innocuous module.
316    #[must_use]
317    pub const fn innocuous() -> Self {
318        Self {
319            innocuous: true,
320            direct_only: false,
321            uses_all_schemas: false,
322        }
323    }
324}
325
326/// Module-level metadata that future catalog and defensive checks can consult
327/// without hard-coding FTS5-specific behavior in unrelated layers.
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329pub struct VtabModuleMetadata {
330    /// Whether the module owns any shadow tables.
331    pub owns_shadow_tables: bool,
332    /// Whether create/connect semantics differ.
333    pub lifecycle: VtabLifecyclePolicy,
334    /// Whether the module exposes integrity hooks.
335    pub integrity: VtabIntegrityPolicy,
336    /// Defensive-execution metadata.
337    pub risk: VtabRiskLevel,
338}
339
340impl VtabModuleMetadata {
341    /// Metadata for ordinary modules with no shadow-table contract.
342    #[must_use]
343    pub const fn ordinary() -> Self {
344        Self {
345            owns_shadow_tables: false,
346            lifecycle: VtabLifecyclePolicy::Simple,
347            integrity: VtabIntegrityPolicy::None,
348            risk: VtabRiskLevel::innocuous(),
349        }
350    }
351
352    /// Metadata for a shadow-owning module.
353    #[must_use]
354    pub const fn shadow_owning(
355        lifecycle: VtabLifecyclePolicy,
356        integrity: VtabIntegrityPolicy,
357        risk: VtabRiskLevel,
358    ) -> Self {
359        Self {
360            owns_shadow_tables: true,
361            lifecycle,
362            integrity,
363            risk,
364        }
365    }
366}
367
368impl Default for VtabModuleMetadata {
369    fn default() -> Self {
370        Self::ordinary()
371    }
372}
373
374// ---------------------------------------------------------------------------
375// VirtualTable trait
376// ---------------------------------------------------------------------------
377
378/// A virtual table module.
379///
380/// Virtual tables expose external data sources as SQL tables. This trait
381/// covers the full lifecycle: creation, connection, scanning, mutation,
382/// and destruction.
383///
384/// This trait is **open** (user-implementable). The `Sized` bound on
385/// constructor methods (`create`, `connect`) allows the trait to be used
386/// as `dyn VirtualTable<Cursor = C>` for other methods.
387///
388/// # Default Implementations
389///
390/// Most methods have sensible defaults. At minimum, you must implement
391/// `connect`, `best_index`, and `open`.
392#[allow(clippy::missing_errors_doc)]
393pub trait VirtualTable: Send + Sync {
394    /// The cursor type for scanning this virtual table.
395    type Cursor: VirtualTableCursor;
396
397    /// Static metadata for the module as a whole.
398    fn module_metadata(_args: &[&str]) -> VtabModuleMetadata
399    where
400        Self: Sized,
401    {
402        VtabModuleMetadata::ordinary()
403    }
404
405    /// Determine whether `table_name` is a module-owned shadow table for the
406    /// virtual table instance named `vtab_name`.
407    fn shadow_table_policy(_vtab_name: &str, _table_name: &str) -> ShadowTablePolicy
408    where
409        Self: Sized,
410    {
411        ShadowTablePolicy::ordinary()
412    }
413
414    /// Called for `CREATE VIRTUAL TABLE`.
415    ///
416    /// May create backing storage. Default delegates to `connect`
417    /// (suitable for eponymous virtual tables).
418    fn create(cx: &Cx, args: &[&str]) -> Result<Self>
419    where
420        Self: Sized,
421    {
422        Self::connect(cx, args)
423    }
424
425    /// Called for subsequent opens of an existing virtual table.
426    fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
427    where
428        Self: Sized;
429
430    /// Inform the query planner about available indexes and their costs.
431    fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
432
433    /// Open a new scan cursor.
434    fn open(&self) -> Result<Self::Cursor>;
435
436    /// Drop a virtual table instance (opposite of `connect`).
437    fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
438        Ok(())
439    }
440
441    /// Called for `DROP VIRTUAL TABLE` — destroy backing storage.
442    ///
443    /// Default delegates to `disconnect`.
444    fn destroy(&mut self, cx: &Cx) -> Result<()> {
445        self.disconnect(cx)
446    }
447
448    /// INSERT/UPDATE/DELETE on the virtual table.
449    ///
450    /// - `args[0]`: old rowid (`None` for INSERT)
451    /// - `args[1]`: new rowid
452    /// - `args[2..]`: column values
453    ///
454    /// Returns the new rowid for INSERT, `None` for UPDATE/DELETE.
455    ///
456    /// Default returns [`FrankenError::ReadOnly`] (read-only virtual tables).
457    fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
458        Err(FrankenError::ReadOnly)
459    }
460
461    /// Begin a virtual table transaction.
462    fn begin(&mut self, _cx: &Cx) -> Result<()> {
463        Ok(())
464    }
465
466    /// Sync a virtual table transaction (phase 1 of 2PC).
467    fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
468        Ok(())
469    }
470
471    /// Commit a virtual table transaction.
472    fn commit(&mut self, _cx: &Cx) -> Result<()> {
473        Ok(())
474    }
475
476    /// Roll back a virtual table transaction.
477    fn rollback(&mut self, _cx: &Cx) -> Result<()> {
478        Ok(())
479    }
480
481    /// Rename the virtual table.
482    ///
483    /// Default returns [`FrankenError::Unsupported`].
484    fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
485        Err(FrankenError::Unsupported)
486    }
487
488    /// Create a savepoint at level `n`.
489    fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
490        Ok(())
491    }
492
493    /// Release savepoint at level `n`.
494    fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
495        Ok(())
496    }
497
498    /// Roll back to savepoint at level `n`.
499    fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
500        Ok(())
501    }
502}
503
504// ---------------------------------------------------------------------------
505// VirtualTableCursor trait
506// ---------------------------------------------------------------------------
507
508/// A cursor for scanning a virtual table.
509///
510/// Cursors are `Send` but **NOT** `Sync` — they are single-threaded
511/// scan objects bound to a specific filter invocation.
512///
513/// # Lifecycle
514///
515/// 1. [`filter`](Self::filter) begins a scan with planner-chosen parameters.
516/// 2. Iterate: check [`eof`](Self::eof), read [`column`](Self::column)/[`rowid`](Self::rowid), advance with [`next`](Self::next).
517/// 3. The cursor is dropped when the scan is complete.
518#[allow(clippy::missing_errors_doc)]
519pub trait VirtualTableCursor: Send {
520    /// Begin a scan with the filter parameters chosen by `best_index`.
521    fn filter(
522        &mut self,
523        cx: &Cx,
524        idx_num: i32,
525        idx_str: Option<&str>,
526        args: &[SqliteValue],
527    ) -> Result<()>;
528
529    /// Advance to the next row.
530    fn next(&mut self, cx: &Cx) -> Result<()>;
531
532    /// Whether the cursor has moved past the last row.
533    fn eof(&self) -> bool;
534
535    /// Write the value of column `col` into `ctx`.
536    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
537
538    /// Return the rowid of the current row.
539    fn rowid(&self) -> Result<i64>;
540}
541
542// ---------------------------------------------------------------------------
543// Module factory & type erasure
544// ---------------------------------------------------------------------------
545
546/// A type-erased virtual table module factory.
547///
548/// Registered with the connection via `register_module("name", factory)`.
549/// When `CREATE VIRTUAL TABLE ... USING name(args)` is executed, the
550/// factory's `create` method is called to produce a concrete vtab instance.
551#[allow(clippy::missing_errors_doc)]
552pub trait VtabModuleFactory: Send + Sync {
553    /// Create a new virtual table instance for `CREATE VIRTUAL TABLE`.
554    fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>>;
555
556    /// Connect to an existing virtual table (subsequent opens).
557    fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
558        self.create(cx, args)
559    }
560
561    /// Column names and affinities for the virtual table schema.
562    fn column_info(&self, _args: &[&str]) -> Vec<(String, char)> {
563        Vec::new()
564    }
565
566    /// Static metadata for the module as a whole.
567    fn module_metadata(&self, _args: &[&str]) -> VtabModuleMetadata {
568        VtabModuleMetadata::ordinary()
569    }
570
571    /// Determine whether `table_name` is a module-owned shadow table for the
572    /// virtual table instance named `vtab_name`.
573    fn shadow_table_policy(&self, _vtab_name: &str, _table_name: &str) -> ShadowTablePolicy {
574        ShadowTablePolicy::ordinary()
575    }
576}
577
578/// A type-erased virtual table instance.
579#[allow(clippy::missing_errors_doc)]
580pub trait ErasedVtabInstance: Send + Sync {
581    /// Return this instance as `Any` for downcasting to concrete extension types.
582    fn as_any(&self) -> &dyn Any;
583    /// Return this instance as mutable `Any` for downcasting to concrete extension types.
584    fn as_any_mut(&mut self) -> &mut dyn Any;
585    /// Open a new scan cursor.
586    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
587    /// INSERT/UPDATE/DELETE on the virtual table.
588    fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
589    /// Begin a virtual table transaction.
590    fn begin(&mut self, cx: &Cx) -> Result<()>;
591    /// Sync a virtual table transaction.
592    fn sync_txn(&mut self, cx: &Cx) -> Result<()>;
593    /// Commit a virtual table transaction.
594    fn commit(&mut self, cx: &Cx) -> Result<()>;
595    /// Roll back a virtual table transaction.
596    fn rollback(&mut self, cx: &Cx) -> Result<()>;
597    /// Create a savepoint at level `n`.
598    fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()>;
599    /// Release savepoint at level `n`.
600    fn release(&mut self, cx: &Cx, n: i32) -> Result<()>;
601    /// Roll back to savepoint at level `n`.
602    fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()>;
603    /// Destroy the virtual table.
604    fn destroy(&mut self, cx: &Cx) -> Result<()>;
605    /// Rename the virtual table.
606    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
607    /// Inform the query planner about available indexes.
608    fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
609}
610
611/// A type-erased virtual table cursor.
612#[allow(clippy::missing_errors_doc)]
613pub trait ErasedVtabCursor: Send {
614    /// Begin a scan with filter parameters.
615    fn erased_filter(
616        &mut self,
617        cx: &Cx,
618        idx_num: i32,
619        idx_str: Option<&str>,
620        args: &[SqliteValue],
621    ) -> Result<()>;
622    /// Advance to the next row.
623    fn erased_next(&mut self, cx: &Cx) -> Result<()>;
624    /// Whether the cursor has moved past the last row.
625    fn erased_eof(&self) -> bool;
626    /// Write the value of column `col` into `ctx`.
627    fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
628    /// Return the rowid of the current row.
629    fn erased_rowid(&self) -> Result<i64>;
630}
631
632/// Blanket `ErasedVtabCursor` for any concrete cursor.
633impl<C: VirtualTableCursor + 'static> ErasedVtabCursor for C {
634    fn erased_filter(
635        &mut self,
636        cx: &Cx,
637        idx_num: i32,
638        idx_str: Option<&str>,
639        args: &[SqliteValue],
640    ) -> Result<()> {
641        VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
642    }
643    fn erased_next(&mut self, cx: &Cx) -> Result<()> {
644        VirtualTableCursor::next(self, cx)
645    }
646    fn erased_eof(&self) -> bool {
647        VirtualTableCursor::eof(self)
648    }
649    fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
650        VirtualTableCursor::column(self, ctx, col)
651    }
652    fn erased_rowid(&self) -> Result<i64> {
653        VirtualTableCursor::rowid(self)
654    }
655}
656
657/// Blanket `ErasedVtabInstance` for any concrete `VirtualTable`.
658impl<T: VirtualTable + 'static> ErasedVtabInstance for T
659where
660    T::Cursor: 'static,
661{
662    fn as_any(&self) -> &dyn Any {
663        self
664    }
665
666    fn as_any_mut(&mut self) -> &mut dyn Any {
667        self
668    }
669
670    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
671        let cursor = VirtualTable::open(self)?;
672        Ok(Box::new(cursor))
673    }
674    fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
675        VirtualTable::update(self, cx, args)
676    }
677    fn begin(&mut self, cx: &Cx) -> Result<()> {
678        VirtualTable::begin(self, cx)
679    }
680    fn sync_txn(&mut self, cx: &Cx) -> Result<()> {
681        VirtualTable::sync_txn(self, cx)
682    }
683    fn commit(&mut self, cx: &Cx) -> Result<()> {
684        VirtualTable::commit(self, cx)
685    }
686    fn rollback(&mut self, cx: &Cx) -> Result<()> {
687        VirtualTable::rollback(self, cx)
688    }
689    fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()> {
690        VirtualTable::savepoint(self, cx, n)
691    }
692    fn release(&mut self, cx: &Cx, n: i32) -> Result<()> {
693        VirtualTable::release(self, cx, n)
694    }
695    fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()> {
696        VirtualTable::rollback_to(self, cx, n)
697    }
698    fn destroy(&mut self, cx: &Cx) -> Result<()> {
699        VirtualTable::destroy(self, cx)
700    }
701    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
702        VirtualTable::rename(self, cx, new_name)
703    }
704    fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
705        VirtualTable::best_index(self, info)
706    }
707}
708
709/// Create a `VtabModuleFactory` from a `VirtualTable` type.
710pub fn module_factory_from<T>() -> impl VtabModuleFactory
711where
712    T: VirtualTable + 'static,
713    T::Cursor: 'static,
714{
715    struct Factory<T: Send + Sync>(std::marker::PhantomData<T>);
716
717    impl<T: VirtualTable + 'static> VtabModuleFactory for Factory<T>
718    where
719        T::Cursor: 'static,
720    {
721        fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
722            let vtab = T::create(cx, args)?;
723            Ok(Box::new(vtab))
724        }
725        fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
726            let vtab = T::connect(cx, args)?;
727            Ok(Box::new(vtab))
728        }
729
730        fn module_metadata(&self, args: &[&str]) -> VtabModuleMetadata {
731            T::module_metadata(args)
732        }
733
734        fn shadow_table_policy(&self, vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
735            T::shadow_table_policy(vtab_name, table_name)
736        }
737    }
738
739    Factory::<T>(std::marker::PhantomData)
740}
741
742// ---------------------------------------------------------------------------
743// Tests
744// ---------------------------------------------------------------------------
745
746#[cfg(test)]
747#[allow(clippy::too_many_lines)]
748mod tests {
749    use super::*;
750
751    // -- Mock: generate_series(start, stop) virtual table --
752
753    struct GenerateSeries {
754        destroyed: bool,
755    }
756
757    struct GenerateSeriesCursor {
758        start: i64,
759        stop: i64,
760        current: i64,
761    }
762
763    impl VirtualTable for GenerateSeries {
764        type Cursor = GenerateSeriesCursor;
765
766        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
767            Ok(Self { destroyed: false })
768        }
769
770        fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
771            info.estimated_cost = 10.0;
772            info.estimated_rows = 100;
773            info.idx_num = 1;
774
775            // Mark constraint 0 as consumed, mapped to filter arg 1.
776            if !info.constraints.is_empty() && info.constraints[0].usable {
777                info.constraint_usage[0].argv_index = 1;
778                info.constraint_usage[0].omit = true;
779            }
780            Ok(())
781        }
782
783        fn open(&self) -> Result<GenerateSeriesCursor> {
784            Ok(GenerateSeriesCursor {
785                start: 0,
786                stop: 0,
787                current: 0,
788            })
789        }
790
791        fn destroy(&mut self, _cx: &Cx) -> Result<()> {
792            self.destroyed = true;
793            Ok(())
794        }
795    }
796
797    impl VirtualTableCursor for GenerateSeriesCursor {
798        fn filter(
799            &mut self,
800            _cx: &Cx,
801            _idx_num: i32,
802            _idx_str: Option<&str>,
803            args: &[SqliteValue],
804        ) -> Result<()> {
805            self.start = args.first().map_or(1, SqliteValue::to_integer);
806            self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
807            self.current = self.start;
808            Ok(())
809        }
810
811        fn next(&mut self, _cx: &Cx) -> Result<()> {
812            self.current += 1;
813            Ok(())
814        }
815
816        fn eof(&self) -> bool {
817            self.current > self.stop
818        }
819
820        fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
821            if self.eof() {
822                ctx.set_value(SqliteValue::Null);
823                return Ok(());
824            }
825            ctx.set_value(SqliteValue::Integer(self.current));
826            Ok(())
827        }
828
829        fn rowid(&self) -> Result<i64> {
830            Ok(if self.eof() { 0 } else { self.current })
831        }
832    }
833
834    // -- Mock: read-only vtab for default update test --
835
836    struct ReadOnlyVtab;
837
838    struct ReadOnlyCursor;
839
840    impl VirtualTable for ReadOnlyVtab {
841        type Cursor = ReadOnlyCursor;
842
843        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
844            Ok(Self)
845        }
846
847        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
848            Ok(())
849        }
850
851        fn open(&self) -> Result<ReadOnlyCursor> {
852            Ok(ReadOnlyCursor)
853        }
854    }
855
856    impl VirtualTableCursor for ReadOnlyCursor {
857        fn filter(
858            &mut self,
859            _cx: &Cx,
860            _idx_num: i32,
861            _idx_str: Option<&str>,
862            _args: &[SqliteValue],
863        ) -> Result<()> {
864            Ok(())
865        }
866
867        fn next(&mut self, _cx: &Cx) -> Result<()> {
868            Ok(())
869        }
870
871        fn eof(&self) -> bool {
872            true
873        }
874
875        fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
876            ctx.set_value(SqliteValue::Null);
877            Ok(())
878        }
879
880        fn rowid(&self) -> Result<i64> {
881            Ok(0)
882        }
883    }
884
885    // -- Mock: writable vtab for insert test --
886
887    struct WritableVtab {
888        rows: Vec<(i64, Vec<SqliteValue>)>,
889        next_rowid: i64,
890    }
891
892    struct WritableCursor {
893        rows: Vec<(i64, Vec<SqliteValue>)>,
894        pos: usize,
895    }
896
897    impl VirtualTable for WritableVtab {
898        type Cursor = WritableCursor;
899
900        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
901            Ok(Self {
902                rows: Vec::new(),
903                next_rowid: 1,
904            })
905        }
906
907        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
908            Ok(())
909        }
910
911        fn open(&self) -> Result<WritableCursor> {
912            Ok(WritableCursor {
913                rows: self.rows.clone(),
914                pos: 0,
915            })
916        }
917
918        fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
919            // args[0] = old rowid (Null for INSERT)
920            if args[0].is_null() {
921                // INSERT
922                let rowid = self.next_rowid;
923                self.next_rowid += 1;
924                let cols: Vec<SqliteValue> = args[2..].to_vec();
925                self.rows.push((rowid, cols));
926                return Ok(Some(rowid));
927            }
928            Ok(None)
929        }
930    }
931
932    impl VirtualTableCursor for WritableCursor {
933        fn filter(
934            &mut self,
935            _cx: &Cx,
936            _idx_num: i32,
937            _idx_str: Option<&str>,
938            _args: &[SqliteValue],
939        ) -> Result<()> {
940            self.pos = 0;
941            Ok(())
942        }
943
944        fn next(&mut self, _cx: &Cx) -> Result<()> {
945            self.pos += 1;
946            Ok(())
947        }
948
949        fn eof(&self) -> bool {
950            self.pos >= self.rows.len()
951        }
952
953        fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
954            if self.eof() {
955                ctx.set_value(SqliteValue::Null);
956                return Ok(());
957            }
958
959            #[allow(clippy::cast_sign_loss)]
960            let col_idx = col as usize;
961            if let Some((_, cols)) = self.rows.get(self.pos) {
962                if let Some(val) = cols.get(col_idx) {
963                    ctx.set_value(val.clone());
964                    return Ok(());
965                }
966            }
967            ctx.set_value(SqliteValue::Null);
968            Ok(())
969        }
970
971        fn rowid(&self) -> Result<i64> {
972            self.rows
973                .get(self.pos)
974                .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
975        }
976    }
977
978    struct ShadowOwningVtab;
979
980    impl VirtualTable for ShadowOwningVtab {
981        type Cursor = ReadOnlyCursor;
982
983        fn module_metadata(_args: &[&str]) -> VtabModuleMetadata {
984            VtabModuleMetadata::shadow_owning(
985                VtabLifecyclePolicy::SeparateCreateAndConnect,
986                VtabIntegrityPolicy::ShadowAware,
987                VtabRiskLevel {
988                    innocuous: false,
989                    direct_only: true,
990                    uses_all_schemas: false,
991                },
992            )
993        }
994
995        fn shadow_table_policy(vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
996            let Some((owner, suffix)) = table_name.rsplit_once('_') else {
997                return ShadowTablePolicy::ordinary();
998            };
999
1000            if owner == vtab_name
1001                && matches!(suffix, "config" | "content" | "data" | "docsize" | "idx")
1002            {
1003                return ShadowTablePolicy::owned_shadow();
1004            }
1005
1006            ShadowTablePolicy::ordinary()
1007        }
1008
1009        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1010            Ok(Self)
1011        }
1012
1013        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1014            Ok(())
1015        }
1016
1017        fn open(&self) -> Result<Self::Cursor> {
1018            Ok(ReadOnlyCursor)
1019        }
1020    }
1021
1022    #[derive(Debug, Clone, PartialEq, Eq)]
1023    struct HookSnapshot {
1024        version: i32,
1025    }
1026
1027    struct HookAwareVtab {
1028        version: i32,
1029        syncs: usize,
1030        tx_state: TransactionalVtabState<HookSnapshot>,
1031    }
1032
1033    impl VirtualTable for HookAwareVtab {
1034        type Cursor = ReadOnlyCursor;
1035
1036        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1037            Ok(Self {
1038                version: 7,
1039                syncs: 0,
1040                tx_state: TransactionalVtabState::default(),
1041            })
1042        }
1043
1044        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1045            Ok(())
1046        }
1047
1048        fn open(&self) -> Result<Self::Cursor> {
1049            Ok(ReadOnlyCursor)
1050        }
1051
1052        fn begin(&mut self, _cx: &Cx) -> Result<()> {
1053            self.tx_state.begin(HookSnapshot {
1054                version: self.version,
1055            });
1056            Ok(())
1057        }
1058
1059        fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
1060            self.syncs += 1;
1061            Ok(())
1062        }
1063
1064        fn savepoint(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1065            self.tx_state.savepoint(
1066                n,
1067                HookSnapshot {
1068                    version: self.version,
1069                },
1070            );
1071            Ok(())
1072        }
1073
1074        fn release(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1075            self.tx_state.release(n);
1076            Ok(())
1077        }
1078
1079        fn rollback_to(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1080            if let Some(snapshot) = self.tx_state.rollback_to(n) {
1081                self.version = snapshot.version;
1082            }
1083            Ok(())
1084        }
1085
1086        fn commit(&mut self, _cx: &Cx) -> Result<()> {
1087            self.tx_state.commit();
1088            Ok(())
1089        }
1090
1091        fn rollback(&mut self, _cx: &Cx) -> Result<()> {
1092            if let Some(snapshot) = self.tx_state.rollback() {
1093                self.version = snapshot.version;
1094            }
1095            Ok(())
1096        }
1097    }
1098
1099    // -- Tests --
1100
1101    #[test]
1102    fn test_vtab_create_vs_connect() {
1103        let cx = Cx::new();
1104
1105        // create delegates to connect by default.
1106        let vtab = GenerateSeries::create(&cx, &[]).unwrap();
1107        assert!(!vtab.destroyed);
1108
1109        // connect works directly.
1110        let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
1111        assert!(!vtab2.destroyed);
1112    }
1113
1114    #[test]
1115    fn test_vtab_best_index_populates_info() {
1116        let cx = Cx::new();
1117        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1118
1119        let mut info = IndexInfo::new(
1120            vec![IndexConstraint {
1121                column: 0,
1122                op: ConstraintOp::Gt,
1123                usable: true,
1124            }],
1125            vec![],
1126        );
1127
1128        VirtualTable::best_index(&vtab, &mut info).unwrap();
1129
1130        assert_eq!(info.idx_num, 1);
1131        assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
1132        assert_eq!(info.estimated_rows, 100);
1133        assert_eq!(info.constraint_usage[0].argv_index, 1);
1134        assert!(info.constraint_usage[0].omit);
1135    }
1136
1137    #[test]
1138    fn test_vtab_cursor_filter_next_eof() {
1139        let cx = Cx::new();
1140        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1141        let mut cursor = vtab.open().unwrap();
1142
1143        cursor
1144            .filter(
1145                &cx,
1146                0,
1147                None,
1148                &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
1149            )
1150            .unwrap();
1151
1152        let mut values = Vec::new();
1153        while !cursor.eof() {
1154            let mut ctx = ColumnContext::new();
1155            cursor.column(&mut ctx, 0).unwrap();
1156            let rowid = cursor.rowid().unwrap();
1157            values.push((rowid, ctx.take_value().unwrap()));
1158            cursor.next(&cx).unwrap();
1159        }
1160
1161        assert_eq!(values.len(), 3);
1162        assert_eq!(values[0], (1, SqliteValue::Integer(1)));
1163        assert_eq!(values[1], (2, SqliteValue::Integer(2)));
1164        assert_eq!(values[2], (3, SqliteValue::Integer(3)));
1165    }
1166
1167    #[test]
1168    fn test_generate_series_cursor_past_end_returns_null_and_zero_rowid() {
1169        let cx = Cx::new();
1170        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1171        let mut cursor = vtab.open().unwrap();
1172
1173        cursor
1174            .filter(
1175                &cx,
1176                0,
1177                None,
1178                &[SqliteValue::Integer(1), SqliteValue::Integer(1)],
1179            )
1180            .unwrap();
1181        cursor.next(&cx).unwrap();
1182        assert!(cursor.eof());
1183
1184        let mut ctx = ColumnContext::new();
1185        cursor.column(&mut ctx, 0).unwrap();
1186        assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1187        assert_eq!(cursor.rowid().unwrap(), 0);
1188    }
1189
1190    #[test]
1191    fn test_writable_cursor_missing_column_returns_null() {
1192        let cx = Cx::new();
1193        let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1194        VirtualTable::update(
1195            &mut vtab,
1196            &cx,
1197            &[
1198                SqliteValue::Null,
1199                SqliteValue::Null,
1200                SqliteValue::Text("hello".into()),
1201            ],
1202        )
1203        .unwrap();
1204
1205        let mut cursor = vtab.open().unwrap();
1206        cursor.filter(&cx, 0, None, &[]).unwrap();
1207
1208        let mut ctx = ColumnContext::new();
1209        cursor.column(&mut ctx, 3).unwrap();
1210        assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1211
1212        cursor.next(&cx).unwrap();
1213        assert!(cursor.eof());
1214        cursor.column(&mut ctx, 0).unwrap();
1215        assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1216        assert_eq!(cursor.rowid().unwrap(), 0);
1217    }
1218
1219    #[test]
1220    fn test_vtab_update_insert() {
1221        let cx = Cx::new();
1222        let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1223
1224        // INSERT: args[0] = Null (no old rowid), args[1] = new rowid (ignored),
1225        // args[2..] = column values
1226        let result = VirtualTable::update(
1227            &mut vtab,
1228            &cx,
1229            &[
1230                SqliteValue::Null,
1231                SqliteValue::Null,
1232                SqliteValue::Text("hello".into()),
1233            ],
1234        )
1235        .unwrap();
1236
1237        assert_eq!(result, Some(1));
1238        assert_eq!(vtab.rows.len(), 1);
1239        assert_eq!(vtab.rows[0].0, 1);
1240    }
1241
1242    #[test]
1243    fn test_vtab_update_readonly_default() {
1244        let cx = Cx::new();
1245        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1246        let err = VirtualTable::update(&mut vtab, &cx, &[SqliteValue::Null]).unwrap_err();
1247        assert!(matches!(err, FrankenError::ReadOnly));
1248    }
1249
1250    #[test]
1251    fn test_vtab_destroy_vs_disconnect() {
1252        let cx = Cx::new();
1253
1254        // Default: destroy delegates to disconnect (both no-ops for ReadOnlyVtab).
1255        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1256        VirtualTable::disconnect(&mut vtab, &cx).unwrap();
1257        VirtualTable::destroy(&mut vtab, &cx).unwrap();
1258
1259        // Custom destroy sets a flag.
1260        let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1261        assert!(!vtab.destroyed);
1262        VirtualTable::destroy(&mut vtab, &cx).unwrap();
1263        assert!(vtab.destroyed);
1264    }
1265
1266    #[test]
1267    fn test_vtab_cursor_send_but_not_sync() {
1268        fn assert_send<T: Send>() {}
1269        assert_send::<GenerateSeriesCursor>();
1270
1271        // VirtualTableCursor is Send but NOT Sync.
1272        // We can't directly test "not Sync" at runtime, but we can verify
1273        // the trait bound: VirtualTableCursor: Send (not Send + Sync).
1274        // The type GenerateSeriesCursor IS actually Sync by coincidence
1275        // (all fields are i64), but the trait doesn't require it.
1276        // The key point: the trait signature says Send, not Send + Sync.
1277    }
1278
1279    #[test]
1280    fn test_column_context_lifecycle() {
1281        let mut ctx = ColumnContext::new();
1282        assert!(ctx.take_value().is_none());
1283
1284        ctx.set_value(SqliteValue::Integer(42));
1285        assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
1286
1287        // After take, it's None again.
1288        assert!(ctx.take_value().is_none());
1289    }
1290
1291    #[test]
1292    fn test_index_info_new() {
1293        let info = IndexInfo::new(
1294            vec![
1295                IndexConstraint {
1296                    column: 0,
1297                    op: ConstraintOp::Eq,
1298                    usable: true,
1299                },
1300                IndexConstraint {
1301                    column: 1,
1302                    op: ConstraintOp::Gt,
1303                    usable: false,
1304                },
1305            ],
1306            vec![IndexOrderBy {
1307                column: 0,
1308                desc: false,
1309            }],
1310        );
1311
1312        assert_eq!(info.constraints.len(), 2);
1313        assert_eq!(info.order_by.len(), 1);
1314        assert_eq!(info.constraint_usage.len(), 2);
1315        assert_eq!(info.idx_num, 0);
1316        assert!(info.idx_str.is_none());
1317        assert!(!info.order_by_consumed);
1318    }
1319
1320    #[test]
1321    fn test_transactional_vtab_state_tracks_savepoints() {
1322        let mut state = TransactionalVtabState::default();
1323
1324        state.begin(1_i32);
1325        state.savepoint(0, 2);
1326        state.savepoint(1, 3);
1327        assert_eq!(state.rollback_to(1), Some(3));
1328        state.release(1);
1329        assert_eq!(state.rollback(), Some(1));
1330        assert_eq!(state.rollback(), None);
1331    }
1332
1333    #[test]
1334    fn test_transactional_vtab_state_uses_base_for_late_enlistment() {
1335        let mut state = TransactionalVtabState::default();
1336
1337        state.begin(7_i32);
1338        state.savepoint(2, 9);
1339
1340        assert_eq!(state.rollback_to(1), Some(7));
1341        assert_eq!(state.rollback(), Some(7));
1342    }
1343
1344    #[test]
1345    fn test_shadow_table_policy_defaults_to_ordinary() {
1346        let policy = ReadOnlyVtab::shadow_table_policy("docs", "docs_data");
1347        assert_eq!(policy, ShadowTablePolicy::ordinary());
1348        assert!(!policy.is_shadow());
1349    }
1350
1351    #[test]
1352    fn test_shadow_owning_module_metadata_is_forwarded_by_factory() {
1353        let factory = module_factory_from::<ShadowOwningVtab>();
1354        let metadata = factory.module_metadata(&[]);
1355
1356        assert!(metadata.owns_shadow_tables);
1357        assert_eq!(
1358            metadata.lifecycle,
1359            VtabLifecyclePolicy::SeparateCreateAndConnect
1360        );
1361        assert_eq!(metadata.integrity, VtabIntegrityPolicy::ShadowAware);
1362        assert!(metadata.risk.direct_only);
1363        assert!(!metadata.risk.innocuous);
1364    }
1365
1366    #[test]
1367    fn test_shadow_owning_module_matches_owned_shadow_tables() {
1368        let factory = module_factory_from::<ShadowOwningVtab>();
1369
1370        let owned = factory.shadow_table_policy("docs", "docs_data");
1371        let other_owner = factory.shadow_table_policy("docs", "posts_data");
1372        let unrelated = factory.shadow_table_policy("docs", "docs_segments");
1373
1374        assert_eq!(owned.kind, ShadowTableKind::Shadow);
1375        assert!(!other_owner.is_shadow());
1376        assert!(!unrelated.is_shadow());
1377    }
1378
1379    #[test]
1380    fn test_erased_vtab_instance_forwards_transaction_hooks() {
1381        let cx = Cx::new();
1382        let mut erased: Box<dyn ErasedVtabInstance> =
1383            Box::new(HookAwareVtab::connect(&cx, &[]).unwrap());
1384
1385        erased.begin(&cx).unwrap();
1386        {
1387            let hook = erased
1388                .as_any_mut()
1389                .downcast_mut::<HookAwareVtab>()
1390                .expect("hook-aware vtab");
1391            hook.version = 9;
1392        }
1393        erased.savepoint(&cx, 0).unwrap();
1394        {
1395            let hook = erased
1396                .as_any_mut()
1397                .downcast_mut::<HookAwareVtab>()
1398                .expect("hook-aware vtab");
1399            hook.version = 11;
1400        }
1401        erased.rollback_to(&cx, 0).unwrap();
1402        erased.release(&cx, 0).unwrap();
1403        erased.sync_txn(&cx).unwrap();
1404        erased.rollback(&cx).unwrap();
1405
1406        let hook = erased
1407            .as_any_mut()
1408            .downcast_mut::<HookAwareVtab>()
1409            .expect("hook-aware vtab");
1410        assert_eq!(hook.version, 7);
1411        assert_eq!(hook.syncs, 1);
1412    }
1413}