Normative notes for the virtual-table interface (RecursiveAI.OneLite.VirtualTables), built on SQLite's documented virtual-table
model (vtab, c3ref/index_info, c3ref/c_index_constraint_eq). Results follow the pinned SQLite release; plans are OneLite's own.
Two kinds of module
| user module | built-in module | |
|---|---|---|
| contract | IVirtualTableModule / IVirtualTable / IVirtualTableCursor, synchronous | BuiltinModule / VTab / VCursor, resumable |
| registered | Connection.RegisterModule(name, module) | static table VirtualModules (rtree, rtree_i32, dbstat) |
| state | whatever the module keeps; it may run SQL on VirtualTableContext.Connection (sync driver) | shadow tables in the database file, read and written through ShadowTree |
| transactions | ITransactionalVirtualTable hooks | none needed: the pager gives atomicity, rollback and savepoints |
| creation | Create runs inside CREATE VIRTUAL TABLE (VtCreate) | the compiler emits the shadow tables from BuiltinModule.ShadowTables as ordinary DDL in the same program |
Built-in modules follow the suspend-point rules of the engine: every method that can touch a page returns ModStatus.Ok or
ModStatus.Pending; on Pending the VM does not move pc and later calls the same method with the same arguments, so a module keeps
progress in fields, never in locals. Errors are thrown as OneLiteException.
Life of a table
CREATE VIRTUAL TABLE [IF NOT EXISTS] [schema.]name USING module(arg, ...). Text of the statement is stored insqlite_schema(typetable,rootpage0) first, then the module's shadow tables follow in module order, each a normaltablerow. Errors:no such module: X;table N already exists.- A catalog entry becomes a connected
VTablazily (first name resolution).Connectmust callDeclareTable("CREATE TABLE x(...)")once; a column whose declared type ends inHIDDENis hidden (not in*, not in the defaultINSERTcolumn list, settable as a table-function argument or by an equality). Not declaring fails withvtable constructor did not declare schema: N. - The instance is dropped when the connection's schema is invalidated (DDL, schema cookie change, rollback), except while it still owes the current transaction a Commit or Rollback.
DROP TABLEcallsDestroyon the table, deletes the table's own row and (built-in) its shadow tables' rows and frees their pages.- The built-in table-valued functions
generate_series,json_each,json_tree,jsonb_eachandjsonb_treeare eponymous-only modules (Engine/VirtualTables/FunctionModules.cs) on this interface: their arguments andWHERE json = xstyle equalities on the hidden columns reachFilterthroughBestIndex(IndexNumberbit k = hidden column k), andCREATE VIRTUAL TABLE ... USING generate_seriesfails withno such module. A user module registered withIsEponymousworks the same way:FROM f(1, 2)fills the first hidden columns. - An eponymous module (
IsEponymous/BuiltinModule.Eponymous, e.g.dbstat) can be named in FROM withoutCREATE VIRTUAL TABLE, with its hidden columns as table-function arguments.
Planning: BestIndex
For each FROM item the planner collects the WHERE (and, for inner joins, ON) terms of the form column OP expr, with the column on
the left (a term written 5 < x is offered as x > 5), and calls BestIndex once at the item's place in the loop nest.
Constraints[i]:Column(declared position; -1 forrowid),Op(ConstraintOp, the documented numeric codes: EQ 2, GT 4, LE 8, LT 16, GE 32, MATCH 64, LIKE 65, GLOB 66, REGEXP 67, NE 68, ISNOT 69, ISNOTNULL 70, ISNULL 71, IS 72),Usable: true whenexprreads only constants, bound parameters, columns of outer queries, subqueries whose correlated columns come from items placed before this one, and FROM items placed before this one (WHERE v MATCH (SELECT q FROM t),ON r.id = t.rid).BETWEEN(as>=and<=),INlists andLIMITare offered to modules that accept extended constraints (vec0does).- Arguments of a table-valued call (
FROM t(a, b)) are offered as equalities on the first hidden columns, always usable. - Outputs:
ConstraintUsage[i].ArgvIndex(0 = unused; else 1-based, unique, no gaps, usable constraints only) andOmit(the engine does not test the term again; otherwise it still tests it on every returned row, so a plan can only change speed, never the answer, unlessOmitis set);IndexNumber,IndexString,EstimatedCost(default 5e98),EstimatedRows(25),ScanIsUnique. A malformed answer fails the statement withxBestIndex malfunction. ThrowingResultCode.Constraintfails withno query solution. OrderBy/OrderByConsumed: for a SELECT over one virtual table (no join, grouping, aggregate, window or DISTINCT), the ORDER BY terms are offered when every one is a plain column of the table (orrowid) without COLLATE or NULLS FIRST/LAST. If the module setsOrderByConsumedno sorter runs and the module's order is the result order, ties included; otherwise the engine sorts as usual. The planner asks twice (once to decide about the sorter, once when planning the loop) and a module must give the same answer; a different one fails withxBestIndex malfunction. The built-in modules (R*Tree, dbstat, vec) are offered the order and do not consume it: the pinned shell sorts their results too (checked for dbstat).- Join order. The planner asks a virtual table in the nest for its plan with what the items before it make usable. A plan that takes an
argument ranks like an indexed equality (50), one that returns a single row (
ScanIsUniqueorEstimatedRows <= 1) like a rowid lookup (100), and a module that throws (a required constraint is not usable yet) is placed later. Table-valued calls and modules with hidden columns wait for the items their arguments read.
Scanning
VtFilter (opens the cursor on first use, passes IndexNumber, IndexString and the arguments, in ArgvIndex order, as Mem values
exactly as evaluated: no affinity applied), then Eof is checked; VNext calls Next then checks Eof; VColumn/VRowid read the
current row. A Column call for a hidden column reads what the module returns. Cursors are disposed when the statement ends or is reset.
Writing
INSERT, UPDATE and DELETE on a table whose module is writable (IWritableVirtualTable; built-in: IsWritable) read the rows they
change first (a drained scan, as for views), then call Update once per row with the documented xUpdate argument layout:
[oldRowid, newRowid, col0, col1, ...] (all columns, hidden included), a delete passes [oldRowid]; an insert has oldRowid NULL and
newRowid NULL unless rowid was named. changes() counts each call; an insert sets last_insert_rowid() to the returned rowid. A table
that is not writable fails with table N may not be modified. RETURNING, upserts and UPDATE ... FROM on a virtual table are unsupported.
Transactions and savepoints (user modules)
A module that implements ITransactionalVirtualTable gets Begin before its first write in a transaction, then at commit Sync (an
exception rolls the transaction back) and Commit, or on rollback Rollback. ISavepointVirtualTable also gets Savepoint(n),
Release(n) and RollbackTo(n) with n the 1-based depth of the SQL savepoint. A failing statement inside an explicit transaction does not
call RollbackTo (the statement's own undo is the module's concern). Built-in modules need none of this: their pages are in the pager.
Shadow tables
Shadow tables are ordinary tables: queryable and, as in the reference, writable by SQL (which can corrupt the module's structure).
ShadowTree is the built-in modules' access to one: a rowid B-tree cursor on BTree.Table of the table's current root (re-resolved from the
catalog at the start of every scan or update, because auto-vacuum can move roots).
R*Tree (Extensions/RTree)
rtree: coordinates are float32 (lower bounds rounded down, upper bounds rounded up to the next float32), the first column an INTEGER
rowid; rtree_i32: int32 coordinates. 1 to 5 dimensions (3 to 11 columns), then +aux columns (stored in _rowid columns a0, a1, ...;
at most 100 columns in all). Shadow tables, created in this order: %_rowid(rowid INTEGER PRIMARY KEY,nodeno,a0,...),
%_node(nodeno INTEGER PRIMARY KEY,data), %_parent(nodeno INTEGER PRIMARY KEY,parentnode); %_node starts with node 1 (the root)
zero-filled. A node's data is pageSize - 64 bytes: bytes 0-1 the tree depth (root only, big-endian), bytes 2-3 the cell count, then
cells of 8 + 4 * 2 * dims bytes: rowid (or child node number) big-endian int64, then min,max per dimension as big-endian float32 or
int32. See docs/rtree.md for the algorithm and the observed behaviour.
dbstat (Engine/VirtualTables/DbStat.cs)
Eponymous dbstat with the documented columns name, path, pageno, pagetype, ncell, payload, unused, mx_payload, pgoffset, pgsize and hidden
schema, aggregate; results equal the pinned shell's. See docs/rtree.md ("dbstat").