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// VirtualTable trait
233// ---------------------------------------------------------------------------
234
235/// A virtual table module.
236///
237/// Virtual tables expose external data sources as SQL tables. This trait
238/// covers the full lifecycle: creation, connection, scanning, mutation,
239/// and destruction.
240///
241/// This trait is **open** (user-implementable). The `Sized` bound on
242/// constructor methods (`create`, `connect`) allows the trait to be used
243/// as `dyn VirtualTable<Cursor = C>` for other methods.
244///
245/// # Default Implementations
246///
247/// Most methods have sensible defaults. At minimum, you must implement
248/// `connect`, `best_index`, and `open`.
249#[allow(clippy::missing_errors_doc)]
250pub trait VirtualTable: Send + Sync {
251    /// The cursor type for scanning this virtual table.
252    type Cursor: VirtualTableCursor;
253
254    /// Called for `CREATE VIRTUAL TABLE`.
255    ///
256    /// May create backing storage. Default delegates to `connect`
257    /// (suitable for eponymous virtual tables).
258    fn create(cx: &Cx, args: &[&str]) -> Result<Self>
259    where
260        Self: Sized,
261    {
262        Self::connect(cx, args)
263    }
264
265    /// Called for subsequent opens of an existing virtual table.
266    fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
267    where
268        Self: Sized;
269
270    /// Inform the query planner about available indexes and their costs.
271    fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
272
273    /// Open a new scan cursor.
274    fn open(&self) -> Result<Self::Cursor>;
275
276    /// Drop a virtual table instance (opposite of `connect`).
277    fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
278        Ok(())
279    }
280
281    /// Called for `DROP VIRTUAL TABLE` — destroy backing storage.
282    ///
283    /// Default delegates to `disconnect`.
284    fn destroy(&mut self, cx: &Cx) -> Result<()> {
285        self.disconnect(cx)
286    }
287
288    /// INSERT/UPDATE/DELETE on the virtual table.
289    ///
290    /// - `args[0]`: old rowid (`None` for INSERT)
291    /// - `args[1]`: new rowid
292    /// - `args[2..]`: column values
293    ///
294    /// Returns the new rowid for INSERT, `None` for UPDATE/DELETE.
295    ///
296    /// Default returns [`FrankenError::ReadOnly`] (read-only virtual tables).
297    fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
298        Err(FrankenError::ReadOnly)
299    }
300
301    /// Begin a virtual table transaction.
302    fn begin(&mut self, _cx: &Cx) -> Result<()> {
303        Ok(())
304    }
305
306    /// Sync a virtual table transaction (phase 1 of 2PC).
307    fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
308        Ok(())
309    }
310
311    /// Commit a virtual table transaction.
312    fn commit(&mut self, _cx: &Cx) -> Result<()> {
313        Ok(())
314    }
315
316    /// Roll back a virtual table transaction.
317    fn rollback(&mut self, _cx: &Cx) -> Result<()> {
318        Ok(())
319    }
320
321    /// Rename the virtual table.
322    ///
323    /// Default returns [`FrankenError::Unsupported`].
324    fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
325        Err(FrankenError::Unsupported)
326    }
327
328    /// Create a savepoint at level `n`.
329    fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
330        Ok(())
331    }
332
333    /// Release savepoint at level `n`.
334    fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
335        Ok(())
336    }
337
338    /// Roll back to savepoint at level `n`.
339    fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
340        Ok(())
341    }
342}
343
344// ---------------------------------------------------------------------------
345// VirtualTableCursor trait
346// ---------------------------------------------------------------------------
347
348/// A cursor for scanning a virtual table.
349///
350/// Cursors are `Send` but **NOT** `Sync` — they are single-threaded
351/// scan objects bound to a specific filter invocation.
352///
353/// # Lifecycle
354///
355/// 1. [`filter`](Self::filter) begins a scan with planner-chosen parameters.
356/// 2. Iterate: check [`eof`](Self::eof), read [`column`](Self::column)/[`rowid`](Self::rowid), advance with [`next`](Self::next).
357/// 3. The cursor is dropped when the scan is complete.
358#[allow(clippy::missing_errors_doc)]
359pub trait VirtualTableCursor: Send {
360    /// Begin a scan with the filter parameters chosen by `best_index`.
361    fn filter(
362        &mut self,
363        cx: &Cx,
364        idx_num: i32,
365        idx_str: Option<&str>,
366        args: &[SqliteValue],
367    ) -> Result<()>;
368
369    /// Advance to the next row.
370    fn next(&mut self, cx: &Cx) -> Result<()>;
371
372    /// Whether the cursor has moved past the last row.
373    fn eof(&self) -> bool;
374
375    /// Write the value of column `col` into `ctx`.
376    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
377
378    /// Return the rowid of the current row.
379    fn rowid(&self) -> Result<i64>;
380}
381
382// ---------------------------------------------------------------------------
383// Module factory & type erasure
384// ---------------------------------------------------------------------------
385
386/// A type-erased virtual table module factory.
387///
388/// Registered with the connection via `register_module("name", factory)`.
389/// When `CREATE VIRTUAL TABLE ... USING name(args)` is executed, the
390/// factory's `create` method is called to produce a concrete vtab instance.
391#[allow(clippy::missing_errors_doc)]
392pub trait VtabModuleFactory: Send + Sync {
393    /// Create a new virtual table instance for `CREATE VIRTUAL TABLE`.
394    fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>>;
395
396    /// Connect to an existing virtual table (subsequent opens).
397    fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
398        self.create(cx, args)
399    }
400
401    /// Column names and affinities for the virtual table schema.
402    fn column_info(&self, _args: &[&str]) -> Vec<(String, char)> {
403        Vec::new()
404    }
405}
406
407/// A type-erased virtual table instance.
408#[allow(clippy::missing_errors_doc)]
409pub trait ErasedVtabInstance: Send + Sync {
410    /// Return this instance as `Any` for downcasting to concrete extension types.
411    fn as_any(&self) -> &dyn Any;
412    /// Return this instance as mutable `Any` for downcasting to concrete extension types.
413    fn as_any_mut(&mut self) -> &mut dyn Any;
414    /// Open a new scan cursor.
415    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
416    /// INSERT/UPDATE/DELETE on the virtual table.
417    fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
418    /// Begin a virtual table transaction.
419    fn begin(&mut self, cx: &Cx) -> Result<()>;
420    /// Sync a virtual table transaction.
421    fn sync_txn(&mut self, cx: &Cx) -> Result<()>;
422    /// Commit a virtual table transaction.
423    fn commit(&mut self, cx: &Cx) -> Result<()>;
424    /// Roll back a virtual table transaction.
425    fn rollback(&mut self, cx: &Cx) -> Result<()>;
426    /// Create a savepoint at level `n`.
427    fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()>;
428    /// Release savepoint at level `n`.
429    fn release(&mut self, cx: &Cx, n: i32) -> Result<()>;
430    /// Roll back to savepoint at level `n`.
431    fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()>;
432    /// Destroy the virtual table.
433    fn destroy(&mut self, cx: &Cx) -> Result<()>;
434    /// Rename the virtual table.
435    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
436    /// Inform the query planner about available indexes.
437    fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
438}
439
440/// A type-erased virtual table cursor.
441#[allow(clippy::missing_errors_doc)]
442pub trait ErasedVtabCursor: Send {
443    /// Begin a scan with filter parameters.
444    fn erased_filter(
445        &mut self,
446        cx: &Cx,
447        idx_num: i32,
448        idx_str: Option<&str>,
449        args: &[SqliteValue],
450    ) -> Result<()>;
451    /// Advance to the next row.
452    fn erased_next(&mut self, cx: &Cx) -> Result<()>;
453    /// Whether the cursor has moved past the last row.
454    fn erased_eof(&self) -> bool;
455    /// Write the value of column `col` into `ctx`.
456    fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
457    /// Return the rowid of the current row.
458    fn erased_rowid(&self) -> Result<i64>;
459}
460
461/// Blanket `ErasedVtabCursor` for any concrete cursor.
462impl<C: VirtualTableCursor + 'static> ErasedVtabCursor for C {
463    fn erased_filter(
464        &mut self,
465        cx: &Cx,
466        idx_num: i32,
467        idx_str: Option<&str>,
468        args: &[SqliteValue],
469    ) -> Result<()> {
470        VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
471    }
472    fn erased_next(&mut self, cx: &Cx) -> Result<()> {
473        VirtualTableCursor::next(self, cx)
474    }
475    fn erased_eof(&self) -> bool {
476        VirtualTableCursor::eof(self)
477    }
478    fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
479        VirtualTableCursor::column(self, ctx, col)
480    }
481    fn erased_rowid(&self) -> Result<i64> {
482        VirtualTableCursor::rowid(self)
483    }
484}
485
486/// Blanket `ErasedVtabInstance` for any concrete `VirtualTable`.
487impl<T: VirtualTable + 'static> ErasedVtabInstance for T
488where
489    T::Cursor: 'static,
490{
491    fn as_any(&self) -> &dyn Any {
492        self
493    }
494
495    fn as_any_mut(&mut self) -> &mut dyn Any {
496        self
497    }
498
499    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
500        let cursor = VirtualTable::open(self)?;
501        Ok(Box::new(cursor))
502    }
503    fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
504        VirtualTable::update(self, cx, args)
505    }
506    fn begin(&mut self, cx: &Cx) -> Result<()> {
507        VirtualTable::begin(self, cx)
508    }
509    fn sync_txn(&mut self, cx: &Cx) -> Result<()> {
510        VirtualTable::sync_txn(self, cx)
511    }
512    fn commit(&mut self, cx: &Cx) -> Result<()> {
513        VirtualTable::commit(self, cx)
514    }
515    fn rollback(&mut self, cx: &Cx) -> Result<()> {
516        VirtualTable::rollback(self, cx)
517    }
518    fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()> {
519        VirtualTable::savepoint(self, cx, n)
520    }
521    fn release(&mut self, cx: &Cx, n: i32) -> Result<()> {
522        VirtualTable::release(self, cx, n)
523    }
524    fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()> {
525        VirtualTable::rollback_to(self, cx, n)
526    }
527    fn destroy(&mut self, cx: &Cx) -> Result<()> {
528        VirtualTable::destroy(self, cx)
529    }
530    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
531        VirtualTable::rename(self, cx, new_name)
532    }
533    fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
534        VirtualTable::best_index(self, info)
535    }
536}
537
538/// Create a `VtabModuleFactory` from a `VirtualTable` type.
539pub fn module_factory_from<T>() -> impl VtabModuleFactory
540where
541    T: VirtualTable + 'static,
542    T::Cursor: 'static,
543{
544    struct Factory<T: Send + Sync>(std::marker::PhantomData<T>);
545
546    impl<T: VirtualTable + 'static> VtabModuleFactory for Factory<T>
547    where
548        T::Cursor: 'static,
549    {
550        fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
551            let vtab = T::create(cx, args)?;
552            Ok(Box::new(vtab))
553        }
554        fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
555            let vtab = T::connect(cx, args)?;
556            Ok(Box::new(vtab))
557        }
558    }
559
560    Factory::<T>(std::marker::PhantomData)
561}
562
563// ---------------------------------------------------------------------------
564// Tests
565// ---------------------------------------------------------------------------
566
567#[cfg(test)]
568#[allow(clippy::too_many_lines)]
569mod tests {
570    use super::*;
571
572    // -- Mock: generate_series(start, stop) virtual table --
573
574    struct GenerateSeries {
575        destroyed: bool,
576    }
577
578    struct GenerateSeriesCursor {
579        start: i64,
580        stop: i64,
581        current: i64,
582    }
583
584    impl VirtualTable for GenerateSeries {
585        type Cursor = GenerateSeriesCursor;
586
587        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
588            Ok(Self { destroyed: false })
589        }
590
591        fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
592            info.estimated_cost = 10.0;
593            info.estimated_rows = 100;
594            info.idx_num = 1;
595
596            // Mark constraint 0 as consumed, mapped to filter arg 1.
597            if !info.constraints.is_empty() && info.constraints[0].usable {
598                info.constraint_usage[0].argv_index = 1;
599                info.constraint_usage[0].omit = true;
600            }
601            Ok(())
602        }
603
604        fn open(&self) -> Result<GenerateSeriesCursor> {
605            Ok(GenerateSeriesCursor {
606                start: 0,
607                stop: 0,
608                current: 0,
609            })
610        }
611
612        fn destroy(&mut self, _cx: &Cx) -> Result<()> {
613            self.destroyed = true;
614            Ok(())
615        }
616    }
617
618    impl VirtualTableCursor for GenerateSeriesCursor {
619        fn filter(
620            &mut self,
621            _cx: &Cx,
622            _idx_num: i32,
623            _idx_str: Option<&str>,
624            args: &[SqliteValue],
625        ) -> Result<()> {
626            self.start = args.first().map_or(1, SqliteValue::to_integer);
627            self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
628            self.current = self.start;
629            Ok(())
630        }
631
632        fn next(&mut self, _cx: &Cx) -> Result<()> {
633            self.current += 1;
634            Ok(())
635        }
636
637        fn eof(&self) -> bool {
638            self.current > self.stop
639        }
640
641        fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
642            ctx.set_value(SqliteValue::Integer(self.current));
643            Ok(())
644        }
645
646        fn rowid(&self) -> Result<i64> {
647            Ok(self.current)
648        }
649    }
650
651    // -- Mock: read-only vtab for default update test --
652
653    struct ReadOnlyVtab;
654
655    struct ReadOnlyCursor;
656
657    impl VirtualTable for ReadOnlyVtab {
658        type Cursor = ReadOnlyCursor;
659
660        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
661            Ok(Self)
662        }
663
664        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
665            Ok(())
666        }
667
668        fn open(&self) -> Result<ReadOnlyCursor> {
669            Ok(ReadOnlyCursor)
670        }
671    }
672
673    impl VirtualTableCursor for ReadOnlyCursor {
674        fn filter(
675            &mut self,
676            _cx: &Cx,
677            _idx_num: i32,
678            _idx_str: Option<&str>,
679            _args: &[SqliteValue],
680        ) -> Result<()> {
681            Ok(())
682        }
683
684        fn next(&mut self, _cx: &Cx) -> Result<()> {
685            Ok(())
686        }
687
688        fn eof(&self) -> bool {
689            true
690        }
691
692        fn column(&self, _ctx: &mut ColumnContext, _col: i32) -> Result<()> {
693            Ok(())
694        }
695
696        fn rowid(&self) -> Result<i64> {
697            Ok(0)
698        }
699    }
700
701    // -- Mock: writable vtab for insert test --
702
703    struct WritableVtab {
704        rows: Vec<(i64, Vec<SqliteValue>)>,
705        next_rowid: i64,
706    }
707
708    struct WritableCursor {
709        rows: Vec<(i64, Vec<SqliteValue>)>,
710        pos: usize,
711    }
712
713    impl VirtualTable for WritableVtab {
714        type Cursor = WritableCursor;
715
716        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
717            Ok(Self {
718                rows: Vec::new(),
719                next_rowid: 1,
720            })
721        }
722
723        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
724            Ok(())
725        }
726
727        fn open(&self) -> Result<WritableCursor> {
728            Ok(WritableCursor {
729                rows: self.rows.clone(),
730                pos: 0,
731            })
732        }
733
734        fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
735            // args[0] = old rowid (Null for INSERT)
736            if args[0].is_null() {
737                // INSERT
738                let rowid = self.next_rowid;
739                self.next_rowid += 1;
740                let cols: Vec<SqliteValue> = args[2..].to_vec();
741                self.rows.push((rowid, cols));
742                return Ok(Some(rowid));
743            }
744            Ok(None)
745        }
746    }
747
748    impl VirtualTableCursor for WritableCursor {
749        fn filter(
750            &mut self,
751            _cx: &Cx,
752            _idx_num: i32,
753            _idx_str: Option<&str>,
754            _args: &[SqliteValue],
755        ) -> Result<()> {
756            self.pos = 0;
757            Ok(())
758        }
759
760        fn next(&mut self, _cx: &Cx) -> Result<()> {
761            self.pos += 1;
762            Ok(())
763        }
764
765        fn eof(&self) -> bool {
766            self.pos >= self.rows.len()
767        }
768
769        fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
770            #[allow(clippy::cast_sign_loss)]
771            let col_idx = col as usize;
772            if let Some((_, cols)) = self.rows.get(self.pos) {
773                if let Some(val) = cols.get(col_idx) {
774                    ctx.set_value(val.clone());
775                }
776            }
777            Ok(())
778        }
779
780        fn rowid(&self) -> Result<i64> {
781            self.rows
782                .get(self.pos)
783                .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
784        }
785    }
786
787    #[derive(Debug, Clone, PartialEq, Eq)]
788    struct HookSnapshot {
789        version: i32,
790    }
791
792    struct HookAwareVtab {
793        version: i32,
794        syncs: usize,
795        tx_state: TransactionalVtabState<HookSnapshot>,
796    }
797
798    impl VirtualTable for HookAwareVtab {
799        type Cursor = ReadOnlyCursor;
800
801        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
802            Ok(Self {
803                version: 7,
804                syncs: 0,
805                tx_state: TransactionalVtabState::default(),
806            })
807        }
808
809        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
810            Ok(())
811        }
812
813        fn open(&self) -> Result<Self::Cursor> {
814            Ok(ReadOnlyCursor)
815        }
816
817        fn begin(&mut self, _cx: &Cx) -> Result<()> {
818            self.tx_state.begin(HookSnapshot {
819                version: self.version,
820            });
821            Ok(())
822        }
823
824        fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
825            self.syncs += 1;
826            Ok(())
827        }
828
829        fn savepoint(&mut self, _cx: &Cx, n: i32) -> Result<()> {
830            self.tx_state.savepoint(
831                n,
832                HookSnapshot {
833                    version: self.version,
834                },
835            );
836            Ok(())
837        }
838
839        fn release(&mut self, _cx: &Cx, n: i32) -> Result<()> {
840            self.tx_state.release(n);
841            Ok(())
842        }
843
844        fn rollback_to(&mut self, _cx: &Cx, n: i32) -> Result<()> {
845            if let Some(snapshot) = self.tx_state.rollback_to(n) {
846                self.version = snapshot.version;
847            }
848            Ok(())
849        }
850
851        fn commit(&mut self, _cx: &Cx) -> Result<()> {
852            self.tx_state.commit();
853            Ok(())
854        }
855
856        fn rollback(&mut self, _cx: &Cx) -> Result<()> {
857            if let Some(snapshot) = self.tx_state.rollback() {
858                self.version = snapshot.version;
859            }
860            Ok(())
861        }
862    }
863
864    // -- Tests --
865
866    #[test]
867    fn test_vtab_create_vs_connect() {
868        let cx = Cx::new();
869
870        // create delegates to connect by default.
871        let vtab = GenerateSeries::create(&cx, &[]).unwrap();
872        assert!(!vtab.destroyed);
873
874        // connect works directly.
875        let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
876        assert!(!vtab2.destroyed);
877    }
878
879    #[test]
880    fn test_vtab_best_index_populates_info() {
881        let cx = Cx::new();
882        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
883
884        let mut info = IndexInfo::new(
885            vec![IndexConstraint {
886                column: 0,
887                op: ConstraintOp::Gt,
888                usable: true,
889            }],
890            vec![],
891        );
892
893        VirtualTable::best_index(&vtab, &mut info).unwrap();
894
895        assert_eq!(info.idx_num, 1);
896        assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
897        assert_eq!(info.estimated_rows, 100);
898        assert_eq!(info.constraint_usage[0].argv_index, 1);
899        assert!(info.constraint_usage[0].omit);
900    }
901
902    #[test]
903    fn test_vtab_cursor_filter_next_eof() {
904        let cx = Cx::new();
905        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
906        let mut cursor = vtab.open().unwrap();
907
908        cursor
909            .filter(
910                &cx,
911                0,
912                None,
913                &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
914            )
915            .unwrap();
916
917        let mut values = Vec::new();
918        while !cursor.eof() {
919            let mut ctx = ColumnContext::new();
920            cursor.column(&mut ctx, 0).unwrap();
921            let rowid = cursor.rowid().unwrap();
922            values.push((rowid, ctx.take_value().unwrap()));
923            cursor.next(&cx).unwrap();
924        }
925
926        assert_eq!(values.len(), 3);
927        assert_eq!(values[0], (1, SqliteValue::Integer(1)));
928        assert_eq!(values[1], (2, SqliteValue::Integer(2)));
929        assert_eq!(values[2], (3, SqliteValue::Integer(3)));
930    }
931
932    #[test]
933    fn test_vtab_update_insert() {
934        let cx = Cx::new();
935        let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
936
937        // INSERT: args[0] = Null (no old rowid), args[1] = new rowid (ignored),
938        // args[2..] = column values
939        let result = VirtualTable::update(
940            &mut vtab,
941            &cx,
942            &[
943                SqliteValue::Null,
944                SqliteValue::Null,
945                SqliteValue::Text("hello".into()),
946            ],
947        )
948        .unwrap();
949
950        assert_eq!(result, Some(1));
951        assert_eq!(vtab.rows.len(), 1);
952        assert_eq!(vtab.rows[0].0, 1);
953    }
954
955    #[test]
956    fn test_vtab_update_readonly_default() {
957        let cx = Cx::new();
958        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
959        let err = VirtualTable::update(&mut vtab, &cx, &[SqliteValue::Null]).unwrap_err();
960        assert!(matches!(err, FrankenError::ReadOnly));
961    }
962
963    #[test]
964    fn test_vtab_destroy_vs_disconnect() {
965        let cx = Cx::new();
966
967        // Default: destroy delegates to disconnect (both no-ops for ReadOnlyVtab).
968        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
969        VirtualTable::disconnect(&mut vtab, &cx).unwrap();
970        VirtualTable::destroy(&mut vtab, &cx).unwrap();
971
972        // Custom destroy sets a flag.
973        let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
974        assert!(!vtab.destroyed);
975        VirtualTable::destroy(&mut vtab, &cx).unwrap();
976        assert!(vtab.destroyed);
977    }
978
979    #[test]
980    fn test_vtab_cursor_send_but_not_sync() {
981        fn assert_send<T: Send>() {}
982        assert_send::<GenerateSeriesCursor>();
983
984        // VirtualTableCursor is Send but NOT Sync.
985        // We can't directly test "not Sync" at runtime, but we can verify
986        // the trait bound: VirtualTableCursor: Send (not Send + Sync).
987        // The type GenerateSeriesCursor IS actually Sync by coincidence
988        // (all fields are i64), but the trait doesn't require it.
989        // The key point: the trait signature says Send, not Send + Sync.
990    }
991
992    #[test]
993    fn test_column_context_lifecycle() {
994        let mut ctx = ColumnContext::new();
995        assert!(ctx.take_value().is_none());
996
997        ctx.set_value(SqliteValue::Integer(42));
998        assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
999
1000        // After take, it's None again.
1001        assert!(ctx.take_value().is_none());
1002    }
1003
1004    #[test]
1005    fn test_index_info_new() {
1006        let info = IndexInfo::new(
1007            vec![
1008                IndexConstraint {
1009                    column: 0,
1010                    op: ConstraintOp::Eq,
1011                    usable: true,
1012                },
1013                IndexConstraint {
1014                    column: 1,
1015                    op: ConstraintOp::Gt,
1016                    usable: false,
1017                },
1018            ],
1019            vec![IndexOrderBy {
1020                column: 0,
1021                desc: false,
1022            }],
1023        );
1024
1025        assert_eq!(info.constraints.len(), 2);
1026        assert_eq!(info.order_by.len(), 1);
1027        assert_eq!(info.constraint_usage.len(), 2);
1028        assert_eq!(info.idx_num, 0);
1029        assert!(info.idx_str.is_none());
1030        assert!(!info.order_by_consumed);
1031    }
1032
1033    #[test]
1034    fn test_transactional_vtab_state_tracks_savepoints() {
1035        let mut state = TransactionalVtabState::default();
1036
1037        state.begin(1_i32);
1038        state.savepoint(0, 2);
1039        state.savepoint(1, 3);
1040        assert_eq!(state.rollback_to(1), Some(3));
1041        state.release(1);
1042        assert_eq!(state.rollback(), Some(1));
1043        assert_eq!(state.rollback(), None);
1044    }
1045
1046    #[test]
1047    fn test_transactional_vtab_state_uses_base_for_late_enlistment() {
1048        let mut state = TransactionalVtabState::default();
1049
1050        state.begin(7_i32);
1051        state.savepoint(2, 9);
1052
1053        assert_eq!(state.rollback_to(1), Some(7));
1054        assert_eq!(state.rollback(), Some(7));
1055    }
1056
1057    #[test]
1058    fn test_erased_vtab_instance_forwards_transaction_hooks() {
1059        let cx = Cx::new();
1060        let mut erased: Box<dyn ErasedVtabInstance> =
1061            Box::new(HookAwareVtab::connect(&cx, &[]).unwrap());
1062
1063        erased.begin(&cx).unwrap();
1064        {
1065            let hook = erased
1066                .as_any_mut()
1067                .downcast_mut::<HookAwareVtab>()
1068                .expect("hook-aware vtab");
1069            hook.version = 9;
1070        }
1071        erased.savepoint(&cx, 0).unwrap();
1072        {
1073            let hook = erased
1074                .as_any_mut()
1075                .downcast_mut::<HookAwareVtab>()
1076                .expect("hook-aware vtab");
1077            hook.version = 11;
1078        }
1079        erased.rollback_to(&cx, 0).unwrap();
1080        erased.release(&cx, 0).unwrap();
1081        erased.sync_txn(&cx).unwrap();
1082        erased.rollback(&cx).unwrap();
1083
1084        let hook = erased
1085            .as_any_mut()
1086            .downcast_mut::<HookAwareVtab>()
1087            .expect("hook-aware vtab");
1088        assert_eq!(hook.version, 7);
1089        assert_eq!(hook.syncs, 1);
1090    }
1091}