Skip to content

Commit b46b699

Browse files
TrevorBurnhambyteforge38
authored andcommitted
sqlite: add virtual table support via createModule()
Expose SQLite's virtual table API through a new `database.createModule(name, options)` method, wrapping `sqlite3_create_module_v2()`. This enables read-only virtual tables backed by JavaScript data sources, usable either as an eponymous table (`SELECT * FROM module_name`) or via `CREATE VIRTUAL TABLE t USING module_name`. Hidden columns pass parameters using table-valued function syntax (`SELECT * FROM module_name(param1, param2)`). `options` accepts `columns`, `rows`, `directOnly`, and `useBigIntArguments`. Column types are validated against INTEGER, TEXT, REAL, BLOB, and ANY, and column names are quoted when building the `sqlite3_declare_vtab()` schema. Rebased from #61544, which was opened by byteforge38 and became inactive. Changes on top of that work: - xColumn reports the value each hidden column was constrained to, rather than NULL. SQLite treats xBestIndex's `omit` as a hint, so it may recheck a constraint it already handed to xFilter; against NULL that recheck rejected every row, and `gs(1, 3) WHERE start = 1` returned no rows. - xBestIndex lowers estimatedCost as it consumes constraints. With a constant cost the planner was free to pick the unconstrained plan and recheck afterwards, so a correlated parameter such as `FROM t, gs(t.a, t.a + 1)` also returned no rows. - Violations of the iteration protocol report a SQLite error instead of calling PropagateJSError with no JavaScript exception pending. That left `.all()` returning undefined and `exec()` reporting success. - xBestIndex passes the constrained hidden-column indices to xFilter through idxStr rather than an int bitmask, which previously aliased for parameter indices at or above the width of an int. - xFilter, xNext, and xColumn take a CallbackDepthGuard. Without it close() from inside rows(), an iterator's next(), or a row getter finalized the statement that SQLite was still stepping, crashing the process. - xClose calls the iterator's return() method so generator `finally` blocks run when SQLite stops stepping early, as it does for LIMIT or a `break` out of a for...of loop. It is skipped while tearing down from ~StatementSync or ~DatabaseSync, which run from garbage collection callbacks where JavaScript cannot be executed; an abandoned generator does not run `finally` in JavaScript either. It is also skipped when an error is already pending, so that error still reaches the caller. - VirtualTableModule holds a BaseObjectWeakPtr<DatabaseSync> to match UserDefinedFunction instead of a raw pointer. - createModule() rejects being called from an authorizer callback. - Documents that values yielded by rows() follow the usual conversion rules, so a number is stored as REAL and a BigInt as INTEGER even when a column declares INTEGER, since virtual tables do not apply column affinity to the values they return. Refs: #61544 Refs: #63826 Fixes: #61539 Co-authored-by: byteforge38 <stormcraft318@gmail.com> Signed-off-by: Trevor Burnham <trevorburnham@gmail.com> Assisted-by: Claude Opus 5 PR-URL: #65787 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e90ba99 commit b46b699

5 files changed

Lines changed: 1785 additions & 0 deletions

File tree

‎doc/api/sqlite.md‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,114 @@ console.log(allUsers);
860860
// ]
861861
```
862862

863+
### `database.createModule(name, options)`
864+
865+
<!-- YAML
866+
added: REPLACEME
867+
-->
868+
869+
* `name` {string} The name of the virtual table module. This name is used in
870+
`CREATE VIRTUAL TABLE ... USING name` statements and as an eponymous table
871+
name.
872+
* `options` {Object} Module configuration settings.
873+
* `columns` {Array} An array of column definitions. Each element is an object
874+
with the following properties:
875+
* `name` {string} The name of the column.
876+
* `type` {string} The declared type of the column. Must be one of
877+
`'INTEGER'`, `'TEXT'`, `'REAL'`, `'BLOB'`, or `'ANY'`.
878+
* `hidden` {boolean} If `true`, the column is hidden and acts as a
879+
parameter for table-valued function usage. **Default:** `false`.
880+
* `rows` {Function} A function called to produce rows when the virtual table
881+
is queried. The function receives values for hidden columns (parameters) as
882+
arguments, in the order they are defined. Must return an iterable (such as
883+
an array or generator) where each element is an array of column values.
884+
* `directOnly` {boolean} If `true`, the virtual table can only be used in
885+
top-level SQL statements and cannot be used inside triggers or views.
886+
**Default:** `false`.
887+
* `useBigIntArguments` {boolean} If `true`, integer parameters passed to
888+
`rows` are converted to `BigInt`s. **Default:** `false`.
889+
890+
Registers a virtual table module with the database. This method is a wrapper
891+
around [`sqlite3_create_module_v2()`][]. Virtual tables allow JavaScript code
892+
to provide the backing data for SQL tables. The registered module can be used
893+
in two ways:
894+
895+
* **Eponymous table**: Query the module name directly without creating a table
896+
(e.g., `SELECT * FROM module_name`).
897+
* **Named virtual table**: Use `CREATE VIRTUAL TABLE t USING module_name` to
898+
create a persistent virtual table.
899+
900+
Hidden columns can be used to pass parameters to the `rows` function using
901+
table-valued function syntax (e.g., `SELECT * FROM module_name(param1, param2)`).
902+
903+
Values yielded by `rows` follow the conversion rules in [Type conversion between
904+
JavaScript and SQLite][]: a {number} is stored as `REAL` and a {bigint} is
905+
stored as `INTEGER`, regardless of the column's declared `type`. Unlike an
906+
ordinary table, a virtual table does not apply column affinity to the values it
907+
returns, so yield a {bigint} when a column needs `INTEGER` storage:
908+
909+
```js
910+
db.createModule('counter', {
911+
columns: [{ name: 'value', type: 'INTEGER' }],
912+
*rows() {
913+
yield [1]; // typeof(value) is 'real'
914+
yield [2n]; // typeof(value) is 'integer'
915+
},
916+
});
917+
```
918+
919+
```cjs
920+
const { DatabaseSync } = require('node:sqlite');
921+
922+
const db = new DatabaseSync(':memory:');
923+
924+
db.createModule('generate_series', {
925+
columns: [
926+
{ name: 'value', type: 'INTEGER' },
927+
{ name: 'start', type: 'INTEGER', hidden: true },
928+
{ name: 'stop', type: 'INTEGER', hidden: true },
929+
{ name: 'step', type: 'INTEGER', hidden: true },
930+
],
931+
*rows(start, stop, step) {
932+
start ??= 0;
933+
stop ??= 10;
934+
step ??= 1;
935+
for (let i = start; i <= stop; i += step) {
936+
yield [i];
937+
}
938+
},
939+
});
940+
941+
console.log(db.prepare('SELECT * FROM generate_series(1, 5, 1)').all());
942+
// Prints: [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 } ]
943+
```
944+
945+
```mjs
946+
import { DatabaseSync } from 'node:sqlite';
947+
948+
const db = new DatabaseSync(':memory:');
949+
950+
db.createModule('generate_series', {
951+
columns: [
952+
{ name: 'value', type: 'INTEGER' },
953+
{ name: 'start', type: 'INTEGER', hidden: true },
954+
{ name: 'stop', type: 'INTEGER', hidden: true },
955+
{ name: 'step', type: 'INTEGER', hidden: true },
956+
],
957+
*rows(start, stop, step) {
958+
start ??= 0;
959+
stop ??= 10;
960+
step ??= 1;
961+
for (let i = start; i <= stop; i += step) {
962+
yield [i];
963+
}
964+
},
965+
});
966+
967+
console.log(db.prepare('SELECT * FROM generate_series(1, 5, 1)').all());
968+
// Prints: [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 } ]
969+
```
970+
863971
### `database.createSession([options])`
864972
865973
<!-- YAML
@@ -1968,6 +2076,7 @@ callback function to indicate what type of operation is being authorized.
19682076
[`sqlite3_column_origin_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
19692077
[`sqlite3_column_table_name()`]: https://www.sqlite.org/c3ref/column_database_name.html
19702078
[`sqlite3_create_function_v2()`]: https://www.sqlite.org/c3ref/create_function.html
2079+
[`sqlite3_create_module_v2()`]: https://www.sqlite.org/c3ref/create_module.html
19712080
[`sqlite3_create_window_function()`]: https://www.sqlite.org/c3ref/create_function.html
19722081
[`sqlite3_db_filename()`]: https://sqlite.org/c3ref/db_filename.html
19732082
[`sqlite3_deserialize()`]: https://sqlite.org/c3ref/deserialize.html

0 commit comments

Comments
 (0)