Files
SnowLuma/.pnpm-store/v11/files/be/a2813757d852e1c1225286c8582350600348d72aa007f3428235fbaa1dde449e88509f438c17c7ff6cee1693534ea1be60d72d9fbf26e21fd7f0acaa0d409d
TanTan 2a69b23b47
Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

1286 lines
48 KiB
Plaintext

// JavaScript virtual tables (Phase 4). See src/vtab.h for the design.
//
// JavaScript is reached from xFilter (invoke the generator, take the first
// batch of rows) and from xNext when a cursor runs out of buffered rows —
// never from the prepare path, which is pure C++. Rows are pulled in
// growing batches (kVtabFirstBatch..kVtabMaxBatch) instead of draining the
// iterator, so `SELECT … LIMIT 3` over an unbounded generator stops after
// one batch instead of running forever, and a table larger than memory
// streams. On the sync path each pull is a direct re-entrant call (Phase 2
// machinery, same guard: sync_sqlite_depth > 0 implies the JS thread); on
// a worker it blocks on the per-database vtab channel like a user
// function.
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <sqlite3.h>
#include <napi.h>
#include <uv.h>
#include "macros.h"
#include "convert.h"
#include "database.h"
#include "vtab.h"
using namespace node_sqlite3;
namespace {
// One xFilter (or factory-instantiation) round trip: the worker fills the
// request half, the JS thread runs the generator (or factory) and fills
// `rows`, then whoever waits is signalled. Direct sync calls fill the same
// struct inline without the channel.
// How many rows one pull asks the generator for. The first batch is small
// so a LIMIT 1 query does not run a generator 1024 times; later batches
// grow so a full scan pays one round trip per 1024 rows.
const size_t kVtabFirstBatch = 64;
const size_t kVtabMaxBatch = 1024;
struct VtabCall {
// kCreate instantiates a factory module; kOpen invokes the generator
// and takes the first batch; kPull takes the next batch from the
// iterator a kOpen left on the cursor.
enum Kind { kCreate, kOpen, kPull };
Database* db;
VtabModule* module;
Kind kind = kOpen;
// Factory instantiation: the CREATE VIRTUAL TABLE argument strings,
// and the definition's rows generator is published as a raw napi_ref.
std::vector<std::string> create_args;
napi_ref factory_rows = NULL;
// xFilter: the hidden-parameter values, and the generator to invoke —
// the instance's factory-produced one (a raw napi_ref, because
// xDisconnect must be able to hand it back to the JS thread from any
// thread), or the module's (a FunctionReference read on this, the JS,
// thread).
napi_ref instance_rows = NULL;
Napi::FunctionReference* module_rows = NULL;
std::vector<Cell> params;
// Which entries of `params` the query actually supplied: an
// unconstrained HIDDEN parameter reaches the generator as `undefined`
// (SQL NULL is a value someone passed on purpose).
std::vector<bool> params_supplied;
// The live iterator: produced by kOpen, consumed by every kPull, owned
// by the cursor in between (a raw napi_ref for the same reason as
// instance_rows: xClose can run on a worker).
napi_ref iterator = NULL;
size_t want = kVtabFirstBatch;
bool exhausted = false;
// Result half.
Rows rows_out;
bool errored = false;
std::string error;
uv_mutex_t mutex;
uv_cond_t cond;
bool done = false;
VtabCall(Database* db_, VtabModule* module_) : db(db_), module(module_) {
uv_mutex_init(&mutex);
uv_cond_init(&cond);
}
~VtabCall() {
uv_mutex_destroy(&mutex);
uv_cond_destroy(&cond);
}
};
void DisposeVtabCall(VtabCall* call) {
delete call;
}
// Applies one materialised Cell to a cursor column result slot (same
// mapping ApplyCell uses for function results in src/function.cc).
void ApplyCellToResult(sqlite3_context* ctx, const Cell& cell) {
switch (cell.type) {
case SQLITE_INTEGER:
sqlite3_result_int64(ctx, cell.integer);
break;
case SQLITE_FLOAT:
sqlite3_result_double(ctx, cell.real);
break;
case SQLITE_TEXT:
sqlite3_result_text(ctx, cell.str.data(),
static_cast<int>(cell.str.size()), SQLITE_TRANSIENT);
break;
case SQLITE_BLOB:
sqlite3_result_blob(ctx,
cell.str.empty() ? "" : cell.str.data(),
static_cast<int>(cell.str.size()), SQLITE_TRANSIENT);
break;
default:
sqlite3_result_null(ctx);
}
}
// Consumes the pending JS exception: appends its `message` to `text` (when
// it has one) and keeps the thrown value on the database as the `cause` of
// the step failure this is about to produce — the same contract a throwing
// user-defined function gets (src/function.cc SetCallError), so
// `err.cause` means the same thing whichever kind of callback threw.
static void CaptureVtabThrow(Napi::Env env, Database* db, std::string* text) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
if (pending == NULL) return;
Napi::Value err(env, pending);
if (err.IsObject()) {
Napi::Value msg = err.As<Napi::Object>().Get("message");
if (!env.IsExceptionPending()) {
if (text != NULL && msg.IsString()) {
*text += ": " + msg.As<Napi::String>().Utf8Value();
}
}
else {
// Reading .message threw (a hostile getter); the value is
// still worth carrying as the cause.
napi_value stray = NULL;
napi_get_and_clear_last_exception(env, &stray);
}
}
db->SetPendingJsError(err);
}
// Converts one yielded JS value into a Cell via the shared bind converter
// (strict marshalling: no [object Object], no silent coercion). Returns
// false with the call marked errored.
bool YieldedValueToCell(Napi::Env env, Database* db, VtabCall* call,
Napi::Value value, const std::string& what, Cell* out) {
auto field = ConvertToField(value, what);
if (field == nullptr) {
call->errored = true;
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
Napi::Value err(env, pending);
if (err.IsObject()) {
Napi::Value msg = err.As<Napi::Object>().Get("message");
if (!env.IsExceptionPending() && msg.IsString()) {
call->error = msg.As<Napi::String>().Utf8Value();
return false;
}
napi_get_and_clear_last_exception(env, &pending);
}
call->error = "a row yielded by virtual table module '" +
call->module->name + "' holds an unsupported value";
return false;
}
switch (field->type) {
case SQLITE_INTEGER:
out->type = SQLITE_INTEGER;
out->integer = static_cast<Values::Integer*>(field.get())->value;
break;
case SQLITE_FLOAT:
out->type = SQLITE_FLOAT;
out->real = static_cast<Values::Float*>(field.get())->value;
break;
case SQLITE_TEXT:
out->type = SQLITE_TEXT;
out->str = std::move(static_cast<Values::Text*>(field.get())->value);
break;
case SQLITE_BLOB: {
auto* f = static_cast<Values::Blob*>(field.get());
out->type = SQLITE_BLOB;
out->str.assign(f->value, f->length);
break;
}
default:
out->type = SQLITE_NULL;
}
return true;
}
// Reads one row-shaped yielded value into `row`. Returns false with the
// call marked errored.
bool YieldedRowToCells(Napi::Env env, Database* db, VtabCall* call,
Napi::Value yielded, size_t ncols, Row* row) {
VtabModule* module = call->module;
const std::string where = "row " +
std::to_string(call->rows_out.size() + 1) +
" of the virtual table '" + module->name + "'";
if (yielded.IsArray()) {
Napi::Array arr = yielded.As<Napi::Array>();
uint32_t len = arr.Length();
if (len > ncols) len = static_cast<uint32_t>(ncols);
for (uint32_t i = 0; i < len; i++) {
Napi::Value v = arr.Get(i);
if (env.IsExceptionPending()) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
}
if (!YieldedValueToCell(env, db, call, v, where, &(*row)[i])) {
return false;
}
}
return true;
}
if (yielded.IsObject() && !yielded.IsFunction()) {
Napi::Object obj = yielded.As<Napi::Object>();
for (size_t i = 0; i < ncols; i++) {
Napi::Value v = obj.Get(module->columns[i]);
if (env.IsExceptionPending()) {
call->errored = true;
call->error = "a row yielded by virtual table '" +
module->name + "' has a hostile property getter";
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
return false;
}
if (!YieldedValueToCell(env, db, call, v, where, &(*row)[i])) {
return false;
}
}
return true;
}
// A bare value: the single-column-table convenience.
if (ncols == 1) {
return YieldedValueToCell(env, db, call, yielded, where, &(*row)[0]);
}
call->errored = true;
call->error = "a row yielded by virtual table '" + module->name +
"' must be an array or an object (the table has " +
std::to_string(ncols) + " columns)";
return false;
}
// Pulls up to call->want rows out of `iter`, appending them to
// call->rows_out and setting call->exhausted when the iterator is done.
void PullRows(Napi::Env env, Database* db, VtabCall* call, Napi::Object iter) {
VtabModule* module = call->module;
const size_t ncols = module->columns.size();
Napi::Value next_v = iter.Get("next");
if (env.IsExceptionPending() || !next_v.IsFunction()) {
call->errored = true;
call->error = "the rows generator of virtual table '" +
module->name + "' did not return an iterable";
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
return;
}
Napi::Function next_fn = next_v.As<Napi::Function>();
while (call->rows_out.size() < call->want) {
Napi::Value step = next_fn.Call(iter, {});
if (env.IsExceptionPending()) {
call->errored = true;
call->error = "the rows generator of virtual table '" +
module->name + "' threw";
CaptureVtabThrow(env, db, &call->error);
return;
}
if (!step.IsObject()) {
call->errored = true;
call->error = "the rows generator of virtual table '" +
module->name + "' produced a malformed iteration result";
return;
}
Napi::Object result = step.As<Napi::Object>();
Napi::Value done_v = result.Get("done");
if (env.IsExceptionPending() || !done_v.IsBoolean()) {
call->errored = true;
call->error = "the rows generator of virtual table '" +
module->name + "' produced a malformed iteration result";
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
return;
}
if (done_v.As<Napi::Boolean>().Value()) {
call->exhausted = true;
return;
}
Napi::Value yielded = result.Get("value");
if (env.IsExceptionPending()) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
}
Row row(ncols);
if (!YieldedRowToCells(env, db, call, yielded, ncols, &row)) return;
call->rows_out.emplace_back(std::move(row));
}
}
// The JS-thread half of one VtabCall.
void ExecuteVtabCallOnJsThread(napi_env nenv, VtabCall* call) {
Napi::Env env(nenv);
Napi::HandleScope scope(env);
Database* db = call->db;
VtabModule* module = call->module;
if (module->dead) {
call->errored = true;
call->error = "virtual table module '" + module->name +
"' was removed while a query against it was in flight";
return;
}
if (call->kind == VtabCall::kCreate) {
// Factory instantiation: run the factory with the CREATE VIRTUAL
// TABLE argument strings; the definition it returns supplies this
// instance's rows generator.
Napi::Function factory = module->factory.IsEmpty()
? Napi::Function() : module->factory.Value();
if (factory.IsEmpty()) {
call->errored = true;
call->error = "virtual table module '" + module->name +
"' has no factory";
return;
}
std::vector<napi_value> argv;
argv.reserve(call->create_args.size());
for (const auto& arg : call->create_args) {
argv.push_back(Napi::String::New(env, arg));
}
Napi::Value definition = factory.Call(env.Undefined(), argv);
if (env.IsExceptionPending()) {
call->errored = true;
call->error = "the factory of virtual table module '" +
module->name + "' threw";
CaptureVtabThrow(env, db, &call->error);
return;
}
Napi::Value rows = definition.IsObject()
? definition.As<Napi::Object>().Get("rows")
: env.Undefined();
if (env.IsExceptionPending()) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
}
if (!rows.IsFunction()) {
call->errored = true;
call->error = "the factory of virtual table module '" +
module->name + "' must return a definition with a rows " +
"generator function";
return;
}
napi_create_reference(env, rows, 1, &call->factory_rows);
return;
}
if (call->kind == VtabCall::kPull) {
// The cursor's iterator, mid-scan: take the next batch.
napi_value iter_v = NULL;
if (call->iterator != NULL) {
napi_get_reference_value(env, call->iterator, &iter_v);
}
if (iter_v == NULL) {
call->errored = true;
call->error = "the rows iterator of virtual table '" +
module->name + "' disappeared mid-scan";
return;
}
PullRows(env, db, call, Napi::Object(env, iter_v));
return;
}
// kOpen (xFilter): convert the parameters, invoke the generator, take
// its iterator and the first batch of rows.
Napi::Function rows_fn;
if (call->instance_rows != NULL) {
napi_value fn = NULL;
napi_get_reference_value(env, call->instance_rows, &fn);
if (fn != NULL) rows_fn = Napi::Function(env, fn);
}
else if (call->module_rows != NULL && !call->module_rows->IsEmpty()) {
rows_fn = call->module_rows->Value();
}
if (rows_fn.IsEmpty()) {
call->errored = true;
call->error = "virtual table module '" + module->name +
"' has no rows generator";
return;
}
const int integer_mode = db->IntegerMode();
std::vector<napi_value> argv;
argv.reserve(call->params.size());
for (size_t i = 0; i < call->params.size(); i++) {
const bool supplied = i < call->params_supplied.size()
&& call->params_supplied[i];
argv.push_back(supplied
? CellToJS(env, call->params[i], integer_mode,
"argument " + std::to_string(i + 1) +
" of the virtual table '" + module->name + "'")
: static_cast<napi_value>(env.Undefined()));
if (env.IsExceptionPending()) {
call->errored = true;
call->error = "cannot pass an argument of the virtual table '" +
module->name + "' to JavaScript";
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
return;
}
}
Napi::Value iterable_v = rows_fn.Call(env.Undefined(), argv);
if (env.IsExceptionPending() || !iterable_v.IsObject()) {
call->errored = true;
if (env.IsExceptionPending()) {
call->error = "the rows generator of virtual table '" +
module->name + "' threw";
CaptureVtabThrow(env, db, &call->error);
}
else {
call->error = "the rows generator of virtual table '" +
module->name + "' did not return an iterable";
}
return;
}
Napi::Object iterable = iterable_v.As<Napi::Object>();
// for..of drives [Symbol.iterator]() first.
Napi::Value iter_fn = iterable.Get(
Napi::Symbol::WellKnown(env, "iterator"));
if (env.IsExceptionPending() || !iter_fn.IsFunction()) {
call->errored = true;
call->error = "the rows generator of virtual table '" +
module->name + "' did not return an iterable";
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
return;
}
Napi::Value iter_v = iter_fn.As<Napi::Function>().Call(iterable, {});
if (env.IsExceptionPending() || !iter_v.IsObject()) {
call->errored = true;
call->error = "the rows generator of virtual table '" +
module->name + "' threw";
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
return;
}
Napi::Object iter = iter_v.As<Napi::Object>();
// The cursor keeps the iterator alive between pulls; xClose hands the
// reference back to the JS thread (Database::QueueVtabRef).
if (napi_create_reference(env, iter, 1, &call->iterator) != napi_ok) {
call->errored = true;
call->error = "cannot retain the rows iterator of virtual table '" +
module->name + "'";
return;
}
PullRows(env, db, call, iter);
}
// The tsfn dispatch: runs the call, then wakes the worker (a direct sync
// call never goes through here).
void VtabCallJs(napi_env nenv, napi_value /*jsCallback*/, void* /*context*/,
void* data) {
if (data == NULL) return;
VtabCall* call = static_cast<VtabCall*>(data);
ExecuteVtabCallOnJsThread(nenv, call);
// No exception may escape into the tsfn dispatch machinery; always
// clear a stray one (the check must be napi_is_exception_pending —
// napi_get_and_clear_last_exception alone reports a non-NULL "last
// exception" even after every exception has been handled).
bool stray = false;
napi_is_exception_pending(nenv, &stray);
if (stray) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(nenv, &pending);
if (!call->errored) {
call->errored = true;
call->error = "internal error while invoking a virtual table's " +
std::string("rows generator");
}
}
uv_mutex_lock(&call->mutex);
call->done = true;
uv_cond_signal(&call->cond);
uv_mutex_unlock(&call->mutex);
}
} // namespace
namespace node_sqlite3 {
// The sqlite3_module callbacks. Pure C++ except xConnect's factory half
// and xFilter's generator half, which round-trip (or run directly on the
// sync path).
struct VtabOps {
// Builds the CREATE TABLE declaration handed to sqlite3_declare_vtab.
// The parameters are a subset of the columns (the better-sqlite3
// contract): `parameters: ['n']` marks the column `n` HIDDEN, which is
// what turns the module into a table-valued function.
//
// Parameters are declared `BLOB HIDDEN`, not bare `HIDDEN`. sqlite parses
// a vtab declaration as ordinary CREATE TABLE text and only *afterwards*
// strips the `hidden` token out of the type it recorded (see
// sqlite3VtabCallConnect) — the affinity was already computed from the
// full type string, and a type naming none of INT/CHAR/CLOB/TEXT/BLOB/
// REAL/FLOA/DOUB gets NUMERIC affinity. So `"n" HIDDEN` gave parameter
// columns NUMERIC affinity while every other column had none: on that
// column text '2' compared equal to the integer 2 and '2' sorted below
// '10', so a table-valued function over a text domain (a version, a
// hash) silently compared its parameter numerically. `BLOB` names the
// no-affinity rule explicitly and leaves the HIDDEN token where sqlite
// looks for it (last, space-separated).
static std::string BuildDeclareSql(const VtabModule* module) {
std::string sql = "CREATE TABLE x(";
for (size_t i = 0; i < module->columns.size(); i++) {
if (i > 0) sql += ",";
sql += "\"" + module->columns[i] + "\"";
for (const auto& param : module->params) {
if (param == module->columns[i]) {
sql += " BLOB HIDDEN";
break;
}
}
}
sql += ")";
return sql;
}
// Column index -> parameter position, or -1: the map xBestIndex/xFilter
// agree on. A parameter is whichever column the name names.
static int ParamIndex(const VtabModule* module, int col) {
if (col < 0 || col >= static_cast<int>(module->columns.size())) {
return -1;
}
const std::string& name = module->columns[static_cast<size_t>(col)];
for (size_t p = 0; p < module->params.size(); p++) {
if (module->params[p] == name) {
return static_cast<int>(p);
}
}
return -1;
}
static int Connect(sqlite3* handle, void* aux, int argc,
const char* const* argv, sqlite3_vtab** vtab_out,
char** pzErr) {
auto* module = static_cast<VtabModule*>(aux);
Database* db = module->db;
// Factory modules: the instance gets its own rows generator from the
// factory (one round trip / direct call). Eponymous modules use the
// registered generator directly.
auto* instance = new VtabInstance();
instance->module = module;
if (module->has_factory) {
VtabCall* call = new VtabCall(db, module);
call->kind = VtabCall::kCreate;
// argv[3..] are the CREATE VIRTUAL TABLE arguments, as SQL text.
for (int i = 3; i < argc; i++) {
call->create_args.emplace_back(
argv[i] != NULL ? argv[i] : "");
}
if (db->sync_sqlite_depth > 0) {
ExecuteVtabCallOnJsThread(db->Env(), call);
// Same always-clear rule as VtabCallJs: no exception may
// escape into sqlite's C frames from the direct path.
bool stray = false;
napi_is_exception_pending(db->Env(), &stray);
if (stray) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(db->Env(), &pending);
if (!call->errored) {
call->errored = true;
call->error = "internal error while invoking a virtual "
"table factory";
}
}
}
else {
napi_status st = napi_call_threadsafe_function(
db->vtab_channel, call, napi_tsfn_blocking);
if (st != napi_ok) {
call->errored = true;
call->error = "the JavaScript environment is shutting down";
}
else {
uv_mutex_lock(&call->mutex);
while (!call->done) uv_cond_wait(&call->cond, &call->mutex);
uv_mutex_unlock(&call->mutex);
}
}
if (call->errored) {
std::string err = call->error;
DisposeVtabCall(call);
delete instance;
*vtab_out = NULL;
if (pzErr != NULL) *pzErr = sqlite3_mprintf("%s", err.c_str());
return SQLITE_ERROR;
}
instance->rows_ref = call->factory_rows;
call->factory_rows = NULL;
DisposeVtabCall(call);
}
int rc = sqlite3_declare_vtab(handle, BuildDeclareSql(module).c_str());
if (rc != SQLITE_OK) {
// Still on the creating thread's responsibility: hand the ref to
// the queue rather than deleting it here (this can run on a
// worker during CREATE VIRTUAL TABLE).
db->QueueVtabRef(instance->rows_ref);
delete instance;
*vtab_out = NULL;
return rc;
}
*vtab_out = &instance->base;
return SQLITE_OK;
}
static int Disconnect(sqlite3_vtab* vtab) {
auto* instance = reinterpret_cast<VtabInstance*>(vtab);
// Runs on whatever thread dropped the table or closed the database
// (a worker, for close). napi_delete_reference is not callable there,
// so the reference is queued and deleted on the JS thread — see
// Database::DrainVtabRefs.
instance->module->db->QueueVtabRef(instance->rows_ref);
delete instance;
return SQLITE_OK;
}
// Factory modules are writable-shaped too in sqlite's eyes, but v1 is
// read-only: no xUpdate slot is registered at all.
static int BestIndex(sqlite3_vtab* vtab, sqlite3_index_info* info) {
auto* instance = reinterpret_cast<VtabInstance*>(vtab);
VtabModule* module = instance->module;
// Equality constraints on the hidden parameters are the table-valued
// function's arguments. sqlite requires the argvIndex values to be
// 1..N with no gaps and no duplicates (it fails the statement with
// "xBestIndex malfunction" otherwise), so they are handed out in the
// order the constraints are consumed and the argv position -> declared
// parameter mapping travels to xFilter in idxStr. Assigning
// `parameter position + 1` instead — which is what this did — broke
// `WHERE b = ?` on a later parameter and any duplicate constraint on
// one parameter.
//
// Nothing is omitted. `aConstraintUsage[i].omit = 1` promises sqlite
// the virtual table applied the constraint itself, and sqlite then
// drops it from the WHERE clause entirely — but the constraint value
// is only *delivered* to the generator as an argument, and a generator
// is free to ignore it. A generator that writes its own values into a
// parameter's column therefore defeated the query silently:
// `WHERE n = 1` returned every row it yielded, `WHERE n IN (1,2)`
// concatenated one unfiltered scan per IN value, and a join on that
// column multiplied rows. Leaving omit at 0 costs one comparison per
// row against the column value xColumn reports and makes *any*
// generator correct — including the well-behaved shape, where xFilter
// fills the HIDDEN columns the generator left NULL with the argument
// (ApplyCursorArgs), so the re-check sees exactly what the caller
// passed and passes.
std::string mapping;
int argv_n = 0;
std::vector<bool> taken(module->params.size(), false);
for (int i = 0; i < info->nConstraint; i++) {
const auto& c = info->aConstraint[i];
if (!c.usable || c.op != SQLITE_INDEX_CONSTRAINT_EQ) continue;
int p = ParamIndex(module, c.iColumn);
if (p < 0 || taken[static_cast<size_t>(p)]) continue;
taken[static_cast<size_t>(p)] = true;
info->aConstraintUsage[i].argvIndex = ++argv_n;
if (!mapping.empty()) mapping += ",";
mapping += std::to_string(p);
}
if (argv_n > 0) {
info->idxStr = sqlite3_mprintf("%s", mapping.c_str());
if (info->idxStr == NULL) return SQLITE_NOMEM;
info->needToFreeIdxStr = 1;
}
info->estimatedCost = argv_n > 0 ? 10.0 : 1000000.0;
info->estimatedRows = argv_n > 0 ? 10 : 1000000;
info->idxNum = argv_n;
return SQLITE_OK;
}
static int Open(sqlite3_vtab* vtab, sqlite3_vtab_cursor** cursor_out) {
auto* instance = reinterpret_cast<VtabInstance*>(vtab);
auto* cursor = new VtabCursor();
cursor->base.pVtab = vtab;
cursor->instance = instance;
*cursor_out = &cursor->base;
return SQLITE_OK;
}
static int Close(sqlite3_vtab_cursor* cursor_base) {
auto* cursor = reinterpret_cast<VtabCursor*>(cursor_base);
// Runs on whichever thread stepped the statement, so the iterator
// reference is handed to the JS thread (napi_delete_reference is not
// callable from a worker). An abandoned scan leaves the generator
// suspended and never resumes it — see src/vtab.h.
if (cursor->iterator != NULL) {
cursor->instance->module->db->QueueVtabRef(cursor->iterator);
cursor->iterator = NULL;
}
delete cursor;
return SQLITE_OK;
}
static bool RunVtabCall(VtabCall* call, VtabInstance* instance) {
Database* db = instance->module->db;
if (db->sync_sqlite_depth > 0) {
// Direct re-entrant call: this is the JS thread inside a *Sync
// call (Phase 2 machinery). Same always-clear rule as VtabCallJs.
ExecuteVtabCallOnJsThread(db->Env(), call);
bool stray = false;
napi_is_exception_pending(db->Env(), &stray);
if (stray) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(db->Env(), &pending);
if (!call->errored) {
call->errored = true;
call->error =
"internal error while invoking a virtual table's rows " +
std::string("generator");
}
}
}
else {
napi_status st = napi_call_threadsafe_function(
db->vtab_channel, call, napi_tsfn_blocking);
if (st != napi_ok) {
call->errored = true;
call->error =
"the JavaScript environment is shutting down; the rows " +
std::string("generator of virtual table '") +
instance->module->name + "' cannot run";
return false;
}
uv_mutex_lock(&call->mutex);
while (!call->done) uv_cond_wait(&call->cond, &call->mutex);
uv_mutex_unlock(&call->mutex);
}
return !call->errored;
}
// Reports a failed pull on the vtab instance, replacing any previous
// message (sqlite frees zErrMsg when it imports it, but never assume).
static void SetVtabError(VtabInstance* instance, const std::string& message) {
if (instance->base.zErrMsg != NULL) {
sqlite3_free(instance->base.zErrMsg);
}
instance->base.zErrMsg = sqlite3_mprintf("%s", message.c_str());
}
// Fills the HIDDEN parameter columns a row left NULL with the argument
// xFilter received for them (see BestIndex).
static void ApplyCursorArgs(VtabCursor* cursor, Row* row) {
VtabModule* module = cursor->instance->module;
for (size_t p = 0; p < cursor->args.size(); p++) {
if (!cursor->has_arg[p]) continue;
// The column this parameter names.
for (size_t col = 0; col < module->columns.size(); col++) {
if (module->columns[col] != module->params[p]) continue;
if (col < row->size() && (*row)[col].type == SQLITE_NULL) {
(*row)[col] = cursor->args[p];
}
break;
}
}
}
// One batch: kOpen for the first (xFilter), kPull afterwards (xNext).
static int PullBatch(VtabCursor* cursor, bool first) {
auto* instance = cursor->instance;
VtabModule* module = instance->module;
VtabCall* call = new VtabCall(module->db, module);
call->kind = first ? VtabCall::kOpen : VtabCall::kPull;
call->want = cursor->batch;
if (first) {
// Factory instances carry their own generator; eponymous instances
// use the module's.
if (instance->rows_ref != NULL) {
call->instance_rows = instance->rows_ref;
}
else {
call->module_rows = &module->rows;
}
call->params = cursor->args;
call->params_supplied = cursor->has_arg;
}
else {
call->iterator = cursor->iterator;
// The rows about to be replaced have all been delivered.
cursor->produced += cursor->rows.size();
}
bool ok = RunVtabCall(call, instance);
if (first && call->iterator != NULL) {
// Taken even on failure: the reference exists and must be freed.
cursor->iterator = call->iterator;
call->iterator = NULL;
}
if (!ok) {
std::string err = call->error;
DisposeVtabCall(call);
cursor->rows.clear();
cursor->pos = 0;
cursor->exhausted = true;
SetVtabError(instance, err);
return SQLITE_ERROR;
}
cursor->rows = std::move(call->rows_out);
cursor->pos = 0;
cursor->exhausted = call->exhausted;
DisposeVtabCall(call);
if (!cursor->args.empty()) {
for (auto& row : cursor->rows) ApplyCursorArgs(cursor, &row);
}
// Grow the next batch: a scan pays one round trip per kVtabMaxBatch
// rows, a LIMIT query only the small first one.
if (cursor->batch < kVtabMaxBatch) {
cursor->batch = cursor->batch * 2 < kVtabMaxBatch
? cursor->batch * 2 : kVtabMaxBatch;
}
return SQLITE_OK;
}
static int Filter(sqlite3_vtab_cursor* cursor_base, int /*idxNum*/,
const char* idxStr, int argc, sqlite3_value** argv) {
auto* cursor = reinterpret_cast<VtabCursor*>(cursor_base);
auto* instance = cursor->instance;
VtabModule* module = instance->module;
// A cursor can be re-filtered (a correlated subquery re-runs it): drop
// the previous scan's iterator first.
if (cursor->iterator != NULL) {
module->db->QueueVtabRef(cursor->iterator);
cursor->iterator = NULL;
}
cursor->rows.clear();
cursor->pos = 0;
cursor->exhausted = false;
cursor->batch = kVtabFirstBatch;
cursor->produced = 0;
// idxStr maps argv positions onto declared parameters (BestIndex).
cursor->args.assign(module->params.size(), Cell());
cursor->has_arg.assign(module->params.size(), false);
int consumed = 0;
if (idxStr != NULL) {
const char* p = idxStr;
while (*p != '\0' && consumed < argc) {
char* end = NULL;
long which = strtol(p, &end, 10);
if (end == p) break;
if (which >= 0 && which < static_cast<long>(cursor->args.size())) {
ValueToCell(&cursor->args[static_cast<size_t>(which)],
argv[consumed]);
cursor->has_arg[static_cast<size_t>(which)] = true;
}
consumed++;
p = (*end == ',') ? end + 1 : end;
}
}
return PullBatch(cursor, true);
}
static int Next(sqlite3_vtab_cursor* cursor_base) {
auto* cursor = reinterpret_cast<VtabCursor*>(cursor_base);
cursor->pos++;
if (cursor->pos < cursor->rows.size() || cursor->exhausted) {
return SQLITE_OK;
}
// The batch is spent and the generator has more: pull the next one.
return PullBatch(cursor, false);
}
static int Eof(sqlite3_vtab_cursor* cursor_base) {
auto* cursor = reinterpret_cast<VtabCursor*>(cursor_base);
// xFilter and xNext always leave a row buffered unless the generator
// is done, so an empty remainder means end of table.
return cursor->pos >= cursor->rows.size() ? 1 : 0;
}
static int Column(sqlite3_vtab_cursor* cursor_base,
sqlite3_context* ctx, int col) {
auto* cursor = reinterpret_cast<VtabCursor*>(cursor_base);
const size_t ncols = cursor->instance->module->columns.size();
if (col < 0 || static_cast<size_t>(col) >= ncols
|| cursor->pos >= cursor->rows.size()) {
sqlite3_result_null(ctx);
return SQLITE_OK;
}
ApplyCellToResult(ctx,
cursor->rows[cursor->pos][static_cast<size_t>(col)]);
return SQLITE_OK;
}
static int Rowid(sqlite3_vtab_cursor* cursor_base, sqlite3_int64* rowid) {
auto* cursor = reinterpret_cast<VtabCursor*>(cursor_base);
// Position in the whole scan, not in the current batch: a per-batch
// counter would hand out the same rowid to every batch.
*rowid = static_cast<sqlite3_int64>(cursor->produced + cursor->pos) + 1;
return SQLITE_OK;
}
// xDestroy for the sqlite3_create_module_v2 registration. sqlite calls it
// once the registration has been replaced or dropped *and* every instance
// created against it has been disconnected (sqlite3VtabModuleUnref), which
// is exactly when the holder may die — including at sqlite3_close, which
// unrefs every module. Waiting for ~Database instead (what this used to
// do) meant a dropped module's generator closure — and everything it
// captured, an entire db.values() array — stayed alive for the life of the
// connection.
//
// It can run on a worker (close, or a DROP dispatched there), so the
// holder is handed to the JS thread, which frees it (and its
// FunctionReferences) from Database::DrainVtabRefs.
static void ModuleDestroy(void* aux) {
auto* module = static_cast<VtabModule*>(aux);
module->dead = true;
Database* db = module->db;
auto& live = db->js_vtabs;
for (auto it = live.begin(); it != live.end(); ++it) {
if (*it == module) {
live.erase(it);
break;
}
}
db->QueueVtabModule(module);
}
static void ReportVtabError(Database* db, const std::string& message,
int rc = SQLITE_ERROR) {
Napi::Env env = db->Env();
if (env.IsExceptionPending()) return;
Napi::HandleScope scope(env);
Napi::Error err = Napi::Error::New(env, message);
err.Value().As<Napi::Object>().Set("errno", Napi::Number::New(env, rc));
Napi::Value info[] = { Napi::String::New(env, "error"), err.Value() };
EMIT_EVENT(db->Value(), 2, info);
}
// Builds the sqlite3_module for one registration. Eponymous-only modules
// leave xCreate NULL (the table exists by name immediately and cannot be
// CREATE VIRTUAL TABLE'd); factory modules set both xCreate and xConnect.
static sqlite3_module* MakeModule(bool has_factory) {
sqlite3_module* m = new sqlite3_module();
// iVersion 0 keeps the modern fields unread (no xShadowName etc.).
m->iVersion = 0;
m->xCreate = has_factory ? Connect : NULL;
m->xConnect = Connect;
m->xBestIndex = BestIndex;
m->xDisconnect = Disconnect;
m->xDestroy = has_factory ? Disconnect : NULL;
m->xOpen = Open;
m->xClose = Close;
m->xFilter = Filter;
m->xNext = Next;
m->xEof = Eof;
m->xColumn = Column;
m->xRowid = Rowid;
m->xUpdate = NULL;
m->xBegin = NULL;
m->xSync = NULL;
m->xCommit = NULL;
m->xRollback = NULL;
m->xFindFunction = NULL;
m->xRename = NULL;
m->xSavepoint = NULL;
m->xRelease = NULL;
m->xRollbackTo = NULL;
m->xShadowName = NULL;
return m;
}
static bool EnsureChannel(Database* db) {
if (db->vtab_channel != NULL) return true;
Napi::Env env = db->Env();
Napi::Function noop = Napi::Function::New(env,
[](const Napi::CallbackInfo& info) {
return info.Env().Undefined();
});
napi_value resource_name = Napi::String::New(env,
"sqlite3.Database.Vtab");
napi_threadsafe_function tsfn = NULL;
napi_status st = napi_create_threadsafe_function(env, noop, NULL,
resource_name, 0, 1, NULL, NULL, NULL, VtabCallJs, &tsfn);
if (st != napi_ok || tsfn == NULL) return false;
napi_unref_threadsafe_function(env, tsfn);
db->vtab_channel = tsfn;
return true;
}
static void ReleaseChannelIfIdle(Database* db) {
if (db->vtab_channel != NULL && db->js_vtabs.empty()) {
napi_release_threadsafe_function(db->vtab_channel,
napi_tsfn_release);
db->vtab_channel = NULL;
}
}
}; // struct VtabOps
} // namespace node_sqlite3
namespace node_sqlite3 {
// --- JS-visible entry points -----------------------------------------------
// The registration baton: the module holder travels to the exclusive
// handler through it.
struct VtabBaton : Database::Baton {
VtabModule* module = NULL;
explicit VtabBaton(Database* db_, VtabModule* module_) :
Baton(db_, Napi::Function()), module(module_) {}
virtual ~VtabBaton() override = default;
};
// The stub a removed module is replaced with: every prepare against the
// name fails loudly instead of silently using stale JavaScript.
static int RefusedConnect(sqlite3*, void*, int, const char* const*,
sqlite3_vtab** vtab_out, char** pzErr) {
*vtab_out = NULL;
if (pzErr != NULL) {
*pzErr = sqlite3_mprintf(
"this virtual table module was removed with db.removeTable()");
}
return SQLITE_ERROR;
}
// _registerVtab(name, columns[], params[], factory|null, rows|null).
// The JS layer validates the definition, flushes the statement cache and
// refuses while a sync-invoked callback is on the stack.
Napi::Value Database::RegisterVtab(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto* db = this;
REQUIRE_ARGUMENT_STRING(0, name);
if (info.Length() < 2 || !info[1].IsArray()) {
Napi::TypeError::New(env,
"Argument 1 must be the column name array").ThrowAsJavaScriptException();
return env.Null();
}
if (info.Length() < 3 || !info[2].IsArray()) {
Napi::TypeError::New(env,
"Argument 2 must be the parameter name array").ThrowAsJavaScriptException();
return env.Null();
}
if (db->sync_sqlite_depth > 0) {
Napi::Error::New(env,
"virtual tables cannot be registered from inside a JavaScript "
"callback invoked by a synchronous method on this connection"
).ThrowAsJavaScriptException();
return env.Null();
}
auto* module = new VtabModule();
module->db = db;
module->name = name;
auto cols = info[1].As<Napi::Array>();
module->columns.reserve(cols.Length());
for (uint32_t i = 0; i < cols.Length(); i++) {
Napi::Value c = cols.Get(i);
if (!c.IsString()) {
delete module;
Napi::TypeError::New(env, "column names must be strings")
.ThrowAsJavaScriptException();
return env.Null();
}
module->columns.push_back(c.As<Napi::String>().Utf8Value());
}
auto params = info[2].As<Napi::Array>();
module->params.reserve(params.Length());
for (uint32_t i = 0; i < params.Length(); i++) {
Napi::Value p = params.Get(i);
if (!p.IsString()) {
delete module;
Napi::TypeError::New(env, "parameter names must be strings")
.ThrowAsJavaScriptException();
return env.Null();
}
module->params.push_back(p.As<Napi::String>().Utf8Value());
}
if (info.Length() > 3 && info[3].IsFunction()) {
module->has_factory = true;
module->factory.Reset(info[3].As<Napi::Function>(), 1);
}
else if (info.Length() > 4 && info[4].IsFunction()) {
module->rows.Reset(info[4].As<Napi::Function>(), 1);
}
else {
delete module;
Napi::TypeError::New(env,
"a virtual table definition requires a rows generator (or a "
"factory function)").ThrowAsJavaScriptException();
return env.Null();
}
db->Schedule(Work_RegisterVtab, new VtabBaton(db, module), true);
return info.This();
}
// _removeVtab(name).
Napi::Value Database::RemoveVtab(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto* db = this;
REQUIRE_ARGUMENT_STRING(0, name);
if (db->sync_sqlite_depth > 0) {
Napi::Error::New(env,
"virtual tables cannot be removed from inside a JavaScript "
"callback invoked by a synchronous method on this connection"
).ThrowAsJavaScriptException();
return env.Null();
}
auto* baton = new Baton(db, Napi::Function());
baton->message = name;
db->Schedule(Work_RemoveVtab, baton, true);
return info.This();
}
void Database::Work_RegisterVtab(Baton* b) {
auto baton = std::unique_ptr<VtabBaton>(static_cast<VtabBaton*>(b));
auto* db = baton->db;
auto* module = baton->module;
assert(db->IsOpen());
assert(db->_handle);
assert(db->pending == 0);
// A same-name re-registration replaces the module: sqlite disconnects
// the old registration's instances and unrefs the old Module, which
// calls VtabOps::ModuleDestroy — the holder is freed then, not here,
// because a stepping VM may still hold an instance created against it
// and its sqlite3_module must outlive that.
for (auto* existing : db->js_vtabs) {
if (existing->name == module->name) existing->dead = true;
}
if (!VtabOps::EnsureChannel(db)) {
VtabOps::ReportVtabError(db, "cannot create the virtual table "
"round-trip channel", SQLITE_NOMEM);
delete module;
db->exclusiveHeld = false;
db->Process();
return;
}
module->module = VtabOps::MakeModule(module->has_factory);
int rc = sqlite3_create_module_v2(db->_handle, module->name.c_str(),
module->module, module, VtabOps::ModuleDestroy);
if (rc != SQLITE_OK) {
VtabOps::ReportVtabError(db,
"cannot register virtual table module '" + module->name +
"': " + std::string(sqlite3_errmsg(db->_handle)), rc);
delete module->module;
module->module = NULL;
delete module;
}
else {
db->js_vtabs.push_back(module);
}
db->exclusiveHeld = false;
db->Process();
}
void Database::Work_RemoveVtab(Baton* b) {
auto baton = std::unique_ptr<Baton>(b);
auto* db = baton->db;
assert(db->IsOpen());
assert(db->_handle);
assert(db->pending == 0);
// sqlite3_drop_modules works from a keep-list (it would also drop the
// built-in fts5/rtree/... modules), so removal replaces the
// registration with a stub whose xConnect refuses. sqlite unrefs (and
// disconnects the instances of) the module being replaced.
static sqlite3_module refused_module = []() {
sqlite3_module m = {};
m.iVersion = 0;
m.xConnect = RefusedConnect;
return m;
}();
bool found = false;
for (auto* module : db->js_vtabs) {
if (module->name == baton->message && !module->dead) {
found = true;
module->dead = true;
}
}
int rc = SQLITE_OK;
if (found) {
rc = sqlite3_create_module(db->_handle, baton->message.c_str(),
&refused_module, NULL);
if (rc != SQLITE_OK) {
VtabOps::ReportVtabError(db,
"cannot remove virtual table module '" + baton->message +
"': " + std::string(sqlite3_errmsg(db->_handle)), rc);
}
}
// The holder is freed by VtabOps::ModuleDestroy once sqlite has
// disconnected the last instance created against it; until then the
// dead flag fails any round trip that could still reach one.
VtabOps::ReleaseChannelIfIdle(db);
db->exclusiveHeld = false;
db->Process();
}
void Database::RemoveVtabs() {
// ~Database, after sqlite3_close: every module was unrefed by the
// close, so VtabOps::ModuleDestroy has already queued each holder —
// draining frees them. Whatever is left in js_vtabs was never handed
// to sqlite (a failed sqlite3_create_module, or a connection that
// never opened), so it is ours to free.
DrainVtabRefs();
for (auto* module : js_vtabs) {
delete module->module;
module->module = NULL;
delete module;
}
js_vtabs.clear();
}
void Database::QueueVtabRef(napi_ref ref) {
if (ref == NULL) return;
uv_mutex_lock(&vtab_refs_mutex);
pending_vtab_refs.push_back(ref);
uv_mutex_unlock(&vtab_refs_mutex);
}
void Database::QueueVtabModule(VtabModule* module) {
if (module == NULL) return;
uv_mutex_lock(&vtab_refs_mutex);
pending_vtab_modules.push_back(module);
uv_mutex_unlock(&vtab_refs_mutex);
}
void Database::DrainVtabRefs() {
// JS thread only: deleting the queued instance references and module
// holders is napi work (a holder owns FunctionReferences), which is why
// xDisconnect and xDestroy queued them instead.
uv_mutex_lock(&vtab_refs_mutex);
std::vector<napi_ref> refs;
refs.swap(pending_vtab_refs);
std::vector<VtabModule*> modules;
modules.swap(pending_vtab_modules);
uv_mutex_unlock(&vtab_refs_mutex);
for (napi_ref ref : refs) {
napi_delete_reference(Env(), ref);
}
for (VtabModule* module : modules) {
// The sqlite3_module struct dies with the holder: sqlite has
// dropped its last reference to both by now.
delete module->module;
module->module = NULL;
delete module;
}
}
bool Database::EnsureVtabChannel() {
return VtabOps::EnsureChannel(this);
}
void Database::ReleaseVtabChannelIfIdle() {
VtabOps::ReleaseChannelIfIdle(this);
}
} // namespace node_sqlite3