// 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 #include #include #include #include #include #include #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 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 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 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(cell.str.size()), SQLITE_TRANSIENT); break; case SQLITE_BLOB: sqlite3_result_blob(ctx, cell.str.empty() ? "" : cell.str.data(), static_cast(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().Get("message"); if (!env.IsExceptionPending()) { if (text != NULL && msg.IsString()) { *text += ": " + msg.As().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().Get("message"); if (!env.IsExceptionPending() && msg.IsString()) { call->error = msg.As().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(field.get())->value; break; case SQLITE_FLOAT: out->type = SQLITE_FLOAT; out->real = static_cast(field.get())->value; break; case SQLITE_TEXT: out->type = SQLITE_TEXT; out->str = std::move(static_cast(field.get())->value); break; case SQLITE_BLOB: { auto* f = static_cast(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(); uint32_t len = arr.Length(); if (len > ncols) len = static_cast(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(); 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(); 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::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().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 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().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 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(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(); // 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().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(); // 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(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(module->columns.size())) { return -1; } const std::string& name = module->columns[static_cast(col)]; for (size_t p = 0; p < module->params.size(); p++) { if (module->params[p] == name) { return static_cast(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(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(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(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 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(p)]) continue; taken[static_cast(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(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(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(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(cursor->args.size())) { ValueToCell(&cursor->args[static_cast(which)], argv[consumed]); cursor->has_arg[static_cast(which)] = true; } consumed++; p = (*end == ',') ? end + 1 : end; } } return PullBatch(cursor, true); } static int Next(sqlite3_vtab_cursor* cursor_base) { auto* cursor = reinterpret_cast(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(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(cursor_base); const size_t ncols = cursor->instance->module->columns.size(); if (col < 0 || static_cast(col) >= ncols || cursor->pos >= cursor->rows.size()) { sqlite3_result_null(ctx); return SQLITE_OK; } ApplyCellToResult(ctx, cursor->rows[cursor->pos][static_cast(col)]); return SQLITE_OK; } static int Rowid(sqlite3_vtab_cursor* cursor_base, sqlite3_int64* rowid) { auto* cursor = reinterpret_cast(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(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(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().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(); 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().Utf8Value()); } auto params = info[2].As(); 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().Utf8Value()); } if (info.Length() > 3 && info[3].IsFunction()) { module->has_factory = true; module->factory.Reset(info[3].As(), 1); } else if (info.Length() > 4 && info[4].IsFunction()) { module->rows.Reset(info[4].As(), 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(static_cast(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(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 refs; refs.swap(pending_vtab_refs); std::vector 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