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 fsqlite_error::{FrankenError, Result};
16use fsqlite_types::SqliteValue;
17use fsqlite_types::cx::Cx;
18
19// ---------------------------------------------------------------------------
20// Query planner types
21// ---------------------------------------------------------------------------
22
23/// Comparison operator for an index constraint.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum ConstraintOp {
26    Eq,
27    Gt,
28    Le,
29    Lt,
30    Ge,
31    Match,
32    Like,
33    Glob,
34    Regexp,
35    Ne,
36    IsNot,
37    IsNotNull,
38    IsNull,
39    Is,
40}
41
42/// A single constraint from the WHERE clause that the planner is considering.
43#[derive(Debug, Clone)]
44pub struct IndexConstraint {
45    /// Column index (0-based; `-1` for rowid).
46    pub column: i32,
47    /// The comparison operator.
48    pub op: ConstraintOp,
49    /// Whether the planner considers this constraint usable.
50    pub usable: bool,
51}
52
53/// A single ORDER BY term from the query.
54#[derive(Debug, Clone)]
55pub struct IndexOrderBy {
56    /// Column index (0-based).
57    pub column: i32,
58    /// `true` if descending, `false` if ascending.
59    pub desc: bool,
60}
61
62/// Per-constraint usage information set by `best_index`.
63#[derive(Debug, Clone, Default)]
64pub struct IndexConstraintUsage {
65    /// 1-based index into the `args` array passed to `filter`.
66    /// 0 means this constraint is not consumed by the vtab.
67    pub argv_index: i32,
68    /// If `true`, the vtab guarantees this constraint is satisfied and
69    /// the core need not double-check it.
70    pub omit: bool,
71}
72
73/// Information exchanged between the query planner and virtual table
74/// during index selection.
75///
76/// The planner fills `constraints` and `order_by`. The vtab fills
77/// `constraint_usage`, `idx_num`, `idx_str`, `order_by_consumed`,
78/// `estimated_cost`, and `estimated_rows`.
79#[derive(Debug, Clone)]
80pub struct IndexInfo {
81    /// WHERE clause constraints the planner is considering.
82    pub constraints: Vec<IndexConstraint>,
83    /// ORDER BY terms from the query.
84    pub order_by: Vec<IndexOrderBy>,
85    /// How each constraint maps to filter arguments (vtab fills this).
86    pub constraint_usage: Vec<IndexConstraintUsage>,
87    /// Integer identifier for the chosen index strategy.
88    pub idx_num: i32,
89    /// Optional string identifier for the chosen index strategy.
90    pub idx_str: Option<String>,
91    /// Whether the vtab guarantees the output is already sorted.
92    pub order_by_consumed: bool,
93    /// Estimated cost of the scan (lower is better).
94    pub estimated_cost: f64,
95    /// Estimated number of rows returned.
96    pub estimated_rows: i64,
97}
98
99impl IndexInfo {
100    /// Create a new `IndexInfo` with the given constraints and order-by terms.
101    #[must_use]
102    pub fn new(constraints: Vec<IndexConstraint>, order_by: Vec<IndexOrderBy>) -> Self {
103        let usage_len = constraints.len();
104        Self {
105            constraints,
106            order_by,
107            constraint_usage: vec![IndexConstraintUsage::default(); usage_len],
108            idx_num: 0,
109            idx_str: None,
110            order_by_consumed: false,
111            estimated_cost: 1_000_000.0,
112            estimated_rows: 1_000_000,
113        }
114    }
115}
116
117// ---------------------------------------------------------------------------
118// Column context
119// ---------------------------------------------------------------------------
120
121/// A context object passed to [`VirtualTableCursor::column`] for writing
122/// the column value.
123///
124/// Analogous to C SQLite's `sqlite3_context*` used with `sqlite3_result_*`.
125#[derive(Debug, Default)]
126pub struct ColumnContext {
127    value: Option<SqliteValue>,
128}
129
130impl ColumnContext {
131    /// Create a new empty column context.
132    #[must_use]
133    pub fn new() -> Self {
134        Self { value: None }
135    }
136
137    /// Set the value for this column.
138    pub fn set_value(&mut self, val: SqliteValue) {
139        self.value = Some(val);
140    }
141
142    /// Take the value out of this context, leaving `None`.
143    pub fn take_value(&mut self) -> Option<SqliteValue> {
144        self.value.take()
145    }
146}
147
148// ---------------------------------------------------------------------------
149// VirtualTable trait
150// ---------------------------------------------------------------------------
151
152/// A virtual table module.
153///
154/// Virtual tables expose external data sources as SQL tables. This trait
155/// covers the full lifecycle: creation, connection, scanning, mutation,
156/// and destruction.
157///
158/// This trait is **open** (user-implementable). The `Sized` bound on
159/// constructor methods (`create`, `connect`) allows the trait to be used
160/// as `dyn VirtualTable<Cursor = C>` for other methods.
161///
162/// # Default Implementations
163///
164/// Most methods have sensible defaults. At minimum, you must implement
165/// `connect`, `best_index`, and `open`.
166#[allow(clippy::missing_errors_doc)]
167pub trait VirtualTable: Send + Sync {
168    /// The cursor type for scanning this virtual table.
169    type Cursor: VirtualTableCursor;
170
171    /// Called for `CREATE VIRTUAL TABLE`.
172    ///
173    /// May create backing storage. Default delegates to `connect`
174    /// (suitable for eponymous virtual tables).
175    fn create(cx: &Cx, args: &[&str]) -> Result<Self>
176    where
177        Self: Sized,
178    {
179        Self::connect(cx, args)
180    }
181
182    /// Called for subsequent opens of an existing virtual table.
183    fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
184    where
185        Self: Sized;
186
187    /// Inform the query planner about available indexes and their costs.
188    fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
189
190    /// Open a new scan cursor.
191    fn open(&self) -> Result<Self::Cursor>;
192
193    /// Drop a virtual table instance (opposite of `connect`).
194    fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
195        Ok(())
196    }
197
198    /// Called for `DROP VIRTUAL TABLE` — destroy backing storage.
199    ///
200    /// Default delegates to `disconnect`.
201    fn destroy(&mut self, cx: &Cx) -> Result<()> {
202        self.disconnect(cx)
203    }
204
205    /// INSERT/UPDATE/DELETE on the virtual table.
206    ///
207    /// - `args[0]`: old rowid (`None` for INSERT)
208    /// - `args[1]`: new rowid
209    /// - `args[2..]`: column values
210    ///
211    /// Returns the new rowid for INSERT, `None` for UPDATE/DELETE.
212    ///
213    /// Default returns [`FrankenError::ReadOnly`] (read-only virtual tables).
214    fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
215        Err(FrankenError::ReadOnly)
216    }
217
218    /// Begin a virtual table transaction.
219    fn begin(&mut self, _cx: &Cx) -> Result<()> {
220        Ok(())
221    }
222
223    /// Sync a virtual table transaction (phase 1 of 2PC).
224    fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
225        Ok(())
226    }
227
228    /// Commit a virtual table transaction.
229    fn commit(&mut self, _cx: &Cx) -> Result<()> {
230        Ok(())
231    }
232
233    /// Roll back a virtual table transaction.
234    fn rollback(&mut self, _cx: &Cx) -> Result<()> {
235        Ok(())
236    }
237
238    /// Rename the virtual table.
239    ///
240    /// Default returns [`FrankenError::Unsupported`].
241    fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
242        Err(FrankenError::Unsupported)
243    }
244
245    /// Create a savepoint at level `n`.
246    fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
247        Ok(())
248    }
249
250    /// Release savepoint at level `n`.
251    fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
252        Ok(())
253    }
254
255    /// Roll back to savepoint at level `n`.
256    fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
257        Ok(())
258    }
259}
260
261// ---------------------------------------------------------------------------
262// VirtualTableCursor trait
263// ---------------------------------------------------------------------------
264
265/// A cursor for scanning a virtual table.
266///
267/// Cursors are `Send` but **NOT** `Sync` — they are single-threaded
268/// scan objects bound to a specific filter invocation.
269///
270/// # Lifecycle
271///
272/// 1. [`filter`](Self::filter) begins a scan with planner-chosen parameters.
273/// 2. Iterate: check [`eof`](Self::eof), read [`column`](Self::column)/[`rowid`](Self::rowid), advance with [`next`](Self::next).
274/// 3. The cursor is dropped when the scan is complete.
275#[allow(clippy::missing_errors_doc)]
276pub trait VirtualTableCursor: Send {
277    /// Begin a scan with the filter parameters chosen by `best_index`.
278    fn filter(
279        &mut self,
280        cx: &Cx,
281        idx_num: i32,
282        idx_str: Option<&str>,
283        args: &[SqliteValue],
284    ) -> Result<()>;
285
286    /// Advance to the next row.
287    fn next(&mut self, cx: &Cx) -> Result<()>;
288
289    /// Whether the cursor has moved past the last row.
290    fn eof(&self) -> bool;
291
292    /// Write the value of column `col` into `ctx`.
293    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
294
295    /// Return the rowid of the current row.
296    fn rowid(&self) -> Result<i64>;
297}
298
299// ---------------------------------------------------------------------------
300// Tests
301// ---------------------------------------------------------------------------
302
303#[cfg(test)]
304#[allow(clippy::too_many_lines)]
305mod tests {
306    use super::*;
307
308    // -- Mock: generate_series(start, stop) virtual table --
309
310    struct GenerateSeries {
311        destroyed: bool,
312    }
313
314    struct GenerateSeriesCursor {
315        start: i64,
316        stop: i64,
317        current: i64,
318    }
319
320    impl VirtualTable for GenerateSeries {
321        type Cursor = GenerateSeriesCursor;
322
323        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
324            Ok(Self { destroyed: false })
325        }
326
327        fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
328            info.estimated_cost = 10.0;
329            info.estimated_rows = 100;
330            info.idx_num = 1;
331
332            // Mark constraint 0 as consumed, mapped to filter arg 1.
333            if !info.constraints.is_empty() && info.constraints[0].usable {
334                info.constraint_usage[0].argv_index = 1;
335                info.constraint_usage[0].omit = true;
336            }
337            Ok(())
338        }
339
340        fn open(&self) -> Result<GenerateSeriesCursor> {
341            Ok(GenerateSeriesCursor {
342                start: 0,
343                stop: 0,
344                current: 0,
345            })
346        }
347
348        fn destroy(&mut self, _cx: &Cx) -> Result<()> {
349            self.destroyed = true;
350            Ok(())
351        }
352    }
353
354    impl VirtualTableCursor for GenerateSeriesCursor {
355        fn filter(
356            &mut self,
357            _cx: &Cx,
358            _idx_num: i32,
359            _idx_str: Option<&str>,
360            args: &[SqliteValue],
361        ) -> Result<()> {
362            self.start = args.first().map_or(1, SqliteValue::to_integer);
363            self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
364            self.current = self.start;
365            Ok(())
366        }
367
368        fn next(&mut self, _cx: &Cx) -> Result<()> {
369            self.current += 1;
370            Ok(())
371        }
372
373        fn eof(&self) -> bool {
374            self.current > self.stop
375        }
376
377        fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
378            ctx.set_value(SqliteValue::Integer(self.current));
379            Ok(())
380        }
381
382        fn rowid(&self) -> Result<i64> {
383            Ok(self.current)
384        }
385    }
386
387    // -- Mock: read-only vtab for default update test --
388
389    struct ReadOnlyVtab;
390
391    struct ReadOnlyCursor;
392
393    impl VirtualTable for ReadOnlyVtab {
394        type Cursor = ReadOnlyCursor;
395
396        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
397            Ok(Self)
398        }
399
400        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
401            Ok(())
402        }
403
404        fn open(&self) -> Result<ReadOnlyCursor> {
405            Ok(ReadOnlyCursor)
406        }
407    }
408
409    impl VirtualTableCursor for ReadOnlyCursor {
410        fn filter(
411            &mut self,
412            _cx: &Cx,
413            _idx_num: i32,
414            _idx_str: Option<&str>,
415            _args: &[SqliteValue],
416        ) -> Result<()> {
417            Ok(())
418        }
419
420        fn next(&mut self, _cx: &Cx) -> Result<()> {
421            Ok(())
422        }
423
424        fn eof(&self) -> bool {
425            true
426        }
427
428        fn column(&self, _ctx: &mut ColumnContext, _col: i32) -> Result<()> {
429            Ok(())
430        }
431
432        fn rowid(&self) -> Result<i64> {
433            Ok(0)
434        }
435    }
436
437    // -- Mock: writable vtab for insert test --
438
439    struct WritableVtab {
440        rows: Vec<(i64, Vec<SqliteValue>)>,
441        next_rowid: i64,
442    }
443
444    struct WritableCursor {
445        rows: Vec<(i64, Vec<SqliteValue>)>,
446        pos: usize,
447    }
448
449    impl VirtualTable for WritableVtab {
450        type Cursor = WritableCursor;
451
452        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
453            Ok(Self {
454                rows: Vec::new(),
455                next_rowid: 1,
456            })
457        }
458
459        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
460            Ok(())
461        }
462
463        fn open(&self) -> Result<WritableCursor> {
464            Ok(WritableCursor {
465                rows: self.rows.clone(),
466                pos: 0,
467            })
468        }
469
470        fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
471            // args[0] = old rowid (Null for INSERT)
472            if args[0].is_null() {
473                // INSERT
474                let rowid = self.next_rowid;
475                self.next_rowid += 1;
476                let cols: Vec<SqliteValue> = args[2..].to_vec();
477                self.rows.push((rowid, cols));
478                return Ok(Some(rowid));
479            }
480            Ok(None)
481        }
482    }
483
484    impl VirtualTableCursor for WritableCursor {
485        fn filter(
486            &mut self,
487            _cx: &Cx,
488            _idx_num: i32,
489            _idx_str: Option<&str>,
490            _args: &[SqliteValue],
491        ) -> Result<()> {
492            self.pos = 0;
493            Ok(())
494        }
495
496        fn next(&mut self, _cx: &Cx) -> Result<()> {
497            self.pos += 1;
498            Ok(())
499        }
500
501        fn eof(&self) -> bool {
502            self.pos >= self.rows.len()
503        }
504
505        fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
506            #[allow(clippy::cast_sign_loss)]
507            let col_idx = col as usize;
508            if let Some((_, cols)) = self.rows.get(self.pos) {
509                if let Some(val) = cols.get(col_idx) {
510                    ctx.set_value(val.clone());
511                }
512            }
513            Ok(())
514        }
515
516        fn rowid(&self) -> Result<i64> {
517            self.rows
518                .get(self.pos)
519                .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
520        }
521    }
522
523    // -- Tests --
524
525    #[test]
526    fn test_vtab_create_vs_connect() {
527        let cx = Cx::new();
528
529        // create delegates to connect by default.
530        let vtab = GenerateSeries::create(&cx, &[]).unwrap();
531        assert!(!vtab.destroyed);
532
533        // connect works directly.
534        let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
535        assert!(!vtab2.destroyed);
536    }
537
538    #[test]
539    fn test_vtab_best_index_populates_info() {
540        let cx = Cx::new();
541        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
542
543        let mut info = IndexInfo::new(
544            vec![IndexConstraint {
545                column: 0,
546                op: ConstraintOp::Gt,
547                usable: true,
548            }],
549            vec![],
550        );
551
552        vtab.best_index(&mut info).unwrap();
553
554        assert_eq!(info.idx_num, 1);
555        assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
556        assert_eq!(info.estimated_rows, 100);
557        assert_eq!(info.constraint_usage[0].argv_index, 1);
558        assert!(info.constraint_usage[0].omit);
559    }
560
561    #[test]
562    fn test_vtab_cursor_filter_next_eof() {
563        let cx = Cx::new();
564        let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
565        let mut cursor = vtab.open().unwrap();
566
567        cursor
568            .filter(
569                &cx,
570                0,
571                None,
572                &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
573            )
574            .unwrap();
575
576        let mut values = Vec::new();
577        while !cursor.eof() {
578            let mut ctx = ColumnContext::new();
579            cursor.column(&mut ctx, 0).unwrap();
580            let rowid = cursor.rowid().unwrap();
581            values.push((rowid, ctx.take_value().unwrap()));
582            cursor.next(&cx).unwrap();
583        }
584
585        assert_eq!(values.len(), 3);
586        assert_eq!(values[0], (1, SqliteValue::Integer(1)));
587        assert_eq!(values[1], (2, SqliteValue::Integer(2)));
588        assert_eq!(values[2], (3, SqliteValue::Integer(3)));
589    }
590
591    #[test]
592    fn test_vtab_update_insert() {
593        let cx = Cx::new();
594        let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
595
596        // INSERT: args[0] = Null (no old rowid), args[1] = new rowid (ignored),
597        // args[2..] = column values
598        let result = vtab
599            .update(
600                &cx,
601                &[
602                    SqliteValue::Null,
603                    SqliteValue::Null,
604                    SqliteValue::Text("hello".to_owned()),
605                ],
606            )
607            .unwrap();
608
609        assert_eq!(result, Some(1));
610        assert_eq!(vtab.rows.len(), 1);
611        assert_eq!(vtab.rows[0].0, 1);
612    }
613
614    #[test]
615    fn test_vtab_update_readonly_default() {
616        let cx = Cx::new();
617        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
618        let err = vtab.update(&cx, &[SqliteValue::Null]).unwrap_err();
619        assert!(matches!(err, FrankenError::ReadOnly));
620    }
621
622    #[test]
623    fn test_vtab_destroy_vs_disconnect() {
624        let cx = Cx::new();
625
626        // Default: destroy delegates to disconnect (both no-ops for ReadOnlyVtab).
627        let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
628        vtab.disconnect(&cx).unwrap();
629        vtab.destroy(&cx).unwrap();
630
631        // Custom destroy sets a flag.
632        let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
633        assert!(!vtab.destroyed);
634        vtab.destroy(&cx).unwrap();
635        assert!(vtab.destroyed);
636    }
637
638    #[test]
639    fn test_vtab_cursor_send_but_not_sync() {
640        fn assert_send<T: Send>() {}
641        assert_send::<GenerateSeriesCursor>();
642
643        // VirtualTableCursor is Send but NOT Sync.
644        // We can't directly test "not Sync" at runtime, but we can verify
645        // the trait bound: VirtualTableCursor: Send (not Send + Sync).
646        // The type GenerateSeriesCursor IS actually Sync by coincidence
647        // (all fields are i64), but the trait doesn't require it.
648        // The key point: the trait signature says Send, not Send + Sync.
649    }
650
651    #[test]
652    fn test_column_context_lifecycle() {
653        let mut ctx = ColumnContext::new();
654        assert!(ctx.take_value().is_none());
655
656        ctx.set_value(SqliteValue::Integer(42));
657        assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
658
659        // After take, it's None again.
660        assert!(ctx.take_value().is_none());
661    }
662
663    #[test]
664    fn test_index_info_new() {
665        let info = IndexInfo::new(
666            vec![
667                IndexConstraint {
668                    column: 0,
669                    op: ConstraintOp::Eq,
670                    usable: true,
671                },
672                IndexConstraint {
673                    column: 1,
674                    op: ConstraintOp::Gt,
675                    usable: false,
676                },
677            ],
678            vec![IndexOrderBy {
679                column: 0,
680                desc: false,
681            }],
682        );
683
684        assert_eq!(info.constraints.len(), 2);
685        assert_eq!(info.order_by.len(), 1);
686        assert_eq!(info.constraint_usage.len(), 2);
687        assert_eq!(info.idx_num, 0);
688        assert!(info.idx_str.is_none());
689        assert!(!info.order_by_consumed);
690    }
691}