Files
SnowLuma/.pnpm-store/v11/files/89/28ca6c224adfe57c59356fe93c561cdd0151c9c2926f2e214b8356c1f1109250e185148baab95c338aeaaa1c08d6b16fe01f2ce24bcb586013b0c711e9b935
T
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

1026 lines
48 KiB
Plaintext

#ifndef NODE_SQLITE3_SRC_DATABASE_H
#define NODE_SQLITE3_SRC_DATABASE_H
#include <assert.h>
#include <atomic>
#include <string>
#include <queue>
#include <vector>
#include <sqlite3.h>
#include <napi.h>
#include "async.h"
#include "macros.h"
using namespace Napi;
namespace node_sqlite3 {
class Database;
struct JsFunc;
struct VtabModule;
struct FunctionBaton;
struct RemoveFunctionBaton;
struct UserFunctionOps;
struct SessionOps;
class Session;
class Blob;
// One queued 'preupdate' event (Deliverable 08); defined in
// src/session.h, which can see the Cell type from src/convert.h.
struct PreupdateInfo;
class Database : public Napi::ObjectWrap<Database> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
// The declarative authorizer: a rule list evaluated in C++ (a JS
// callback would need a blocking round trip from inside
// sqlite3_prepare, on whatever thread is preparing).
struct AuthRule {
int action = -1; // -1 matches any authorizer action
int verdict; // SQLITE_OK / SQLITE_DENY / SQLITE_IGNORE
std::string arg1; // table, index, pragma name…
std::string arg2; // column, new name…
std::string database;
std::string trigger;
// An unspecified field (null/undefined in the rule row) matches
// anything; an explicitly-passed empty string matches only an
// empty argument, which D07's wildcard-"" encoding could not
// express.
bool arg1_any = true;
bool arg2_any = true;
bool database_any = true;
bool trigger_any = true;
};
struct AuthPolicy {
int default_decision = SQLITE_OK;
// Deny rules first, then ignore, then allow: a deny can never be
// rescued by a later allow, which is the safe reading for a
// sandbox (the JS layer orders them this way).
std::vector<AuthRule> rules;
};
// How INTEGER columns and lastID are converted to JS
// (configure('integerMode', mode)). 'number' throws a RangeError on
// values outside the safe-integer range rather than truncating;
// 'bigint' always returns BigInt; 'mixed' picks per value.
enum IntegerMode {
INTEGER_NUMBER = 0,
INTEGER_BIGINT = 1,
INTEGER_MIXED = 2
};
// The connection's lifecycle. Replaces the former overloaded `locked`
// bool, which meant "an exclusive call is in flight" while busy and
// "this object is dead" after a successful close — every consumer had
// to know which, and forgetting the tombstone reading was a bug class
// of its own.
enum class DbState {
Opening, // constructor ran, sqlite3_open not yet completed
Open, // handle live, no close in flight
Closing, // sqlite3_close in flight on the worker
Closed // close completed (or the object was never opened);
// terminal for scheduling purposes
};
// Per-environment constructor references, one slot per exported
// class. These must live in instance data rather than in file
// statics: node-addon-api deletes instance data at env teardown, while
// a static Napi::Reference is destroyed at process exit — after the
// env is gone — so its napi_delete_reference lands on a dead
// environment (a segfault at exit, seen on musl). Instance data is
// also per-env, which a static is not: every worker thread gets its
// own napi env, and a static would hand one environment a constructor
// belonging to another. Database::Init allocates the block (it runs
// first in RegisterModule); each class's Init fills in its own slot.
struct AddonData {
Napi::FunctionReference database_ctor;
Napi::FunctionReference statement_ctor;
Napi::FunctionReference backup_ctor;
Napi::FunctionReference session_ctor;
Napi::FunctionReference blob_ctor;
Napi::FunctionReference changeset_iter_ctor;
// Cached verdict of EnvCannotRunJs: once an environment refuses
// JS mutation it is being torn down and never comes back, so the
// probe never needs repeating. probe_object/probe_key are the
// reusable probe pair (built once on a live env) so the per-
// completion cost is two reference reads and one set_property.
bool cannot_run_js = false;
napi_ref probe_object = NULL;
napi_ref probe_key = NULL;
// The JS row-factory generator, registered once at module load by
// lib/sqlite3.js. Given the result column names it compiles a
// monomorphic function that builds one row from its arguments, so
// a row costs one napi_call_function instead of one V8 property
// store per column. NULL when the JS half never registered it, or
// when the environment forbids code generation from strings — both
// fall back to the per-cell store loop.
napi_ref row_factory_generator = NULL;
// Set once the generator has refused (a CSP/no-codegen realm), so
// the refusal is not retried per statement.
bool row_factory_unavailable = false;
// The global Date and RegExp constructors, looked up once per
// environment. The bind paths ask "is this argument a named
// parameter map, or a lone Date/RegExp value?" on every call, and
// reading them off the global each time cost a napi_get_global
// plus a property get keyed by a freshly built JS string —
// per call, to answer a question whose answer cannot change.
// These hold the *current* environment's constructors, which is
// exactly what the uncached lookup returned.
napi_ref date_ctor = NULL;
napi_ref regexp_ctor = NULL;
// Object.prototype, for recognising a plain object literal in one
// step. See IsNamedParameterMap.
napi_ref object_prototype = NULL;
};
// True when this environment can no longer accept JS mutation — it
// is being torn down. A terminated worker has its remaining
// async-work completions delivered while the isolate is unwinding,
// where any JS construction, throw or checked property operation is
// fatal (node-addon-api's error path calls napi_throw, which fatals
// in turn); those completions must bail out instead. Probed with a
// real property operation on a per-env cached pair — the cheap calls
// (get_global, typeof, reference get/create/delete) still succeed on
// a dying env; property access is where it is refused.
//
// A refused probe has exactly two causes: the isolate is terminating,
// or an exception is already pending. `pending_means_alive` says
// which one the caller can be in, and the two callers genuinely
// differ:
//
// * Synchronous callers (CleanQueue, reached while a throwing
// JS callback unwinds) legitimately run with an exception
// pending on a perfectly healthy env — pass true.
// * Async-work completions enter a fresh callback scope with no
// exception pending on a healthy env, so a refusal there means
// the isolate is terminating — pass false. Treating it as alive
// is what let a terminated worker with several queued
// completions still fatal in Work_AfterAll: V8's termination
// exception is pending, so the probe was refused *and* reported
// alive, and the handler walked straight into the throw.
//
// The verdict is cached in AddonData (teardown never reverses), but
// only when it was reached without a pending exception: if the
// premise above is ever wrong, the cost is one dropped completion
// rather than a permanently dead connection.
static inline bool EnvCannotRunJs(napi_env e,
bool pending_means_alive = true) {
auto env = Napi::Env(e);
auto* data = env.GetInstanceData<AddonData>();
if (data != nullptr && data->cannot_run_js) return true;
napi_value obj = nullptr;
napi_value key = nullptr;
bool refused;
if (data != nullptr && data->probe_object != NULL) {
// Hot path: the reusable probe pair, two reference reads.
bool has = false;
refused = napi_get_reference_value(e, data->probe_object, &obj) != napi_ok
|| napi_get_reference_value(e, data->probe_key, &key) != napi_ok
|| napi_has_property(e, obj, key, &has) != napi_ok;
} else {
refused = napi_create_object(e, &obj) != napi_ok
|| napi_create_string_utf8(e, "sqlite3_probe", 13, &key) != napi_ok
|| napi_set_property(e, obj, key, key) != napi_ok;
if (!refused && data != nullptr) {
// Keep the pair for every later probe on this env. If
// the reference creation fails the pair is simply not
// cached; the probe above already ran.
napi_ref obj_ref = NULL;
napi_ref key_ref = NULL;
if (napi_create_reference(e, obj, 1, &obj_ref) == napi_ok
&& napi_create_reference(e, key, 1, &key_ref) == napi_ok) {
data->probe_object = obj_ref;
data->probe_key = key_ref;
}
}
}
if (!refused) return false;
bool pending = false;
napi_is_exception_pending(e, &pending);
if (pending && pending_means_alive) return false;
// Only a refusal with no exception pending is provably terminal;
// see the note above on why the pending case is not cached.
if (!pending && data != nullptr) data->cannot_run_js = true;
return true;
}
static inline bool HasInstance(Napi::Value val) {
return HasInstanceIn(val, &AddonData::database_ctor);
}
// InstanceOf against a constructor held in this env's AddonData.
// Shared by every class's HasInstance (Statement, Backup, Session,
// Blob): one instance-data slot per env holds all of them, so
// cross-class argument validation does not collide the way a single
// SetInstanceData slot did. False, not an error, when the class is not
// initialized in this env.
static inline bool HasInstanceIn(Napi::Value val,
Napi::FunctionReference AddonData::*slot) {
auto env = val.Env();
Napi::HandleScope scope(env);
auto* data = env.GetInstanceData<AddonData>();
if (data == nullptr || (data->*slot).IsEmpty()) return false;
if (!val.IsObject()) return false;
return val.As<Napi::Object>().InstanceOf((data->*slot).Value());
}
struct Baton {
napi_async_work request = NULL;
Database* db;
Napi::FunctionReference callback;
int status;
std::string message;
// Payload for SetBusyTimeout (the only scheduled call that needs
// one); other work uses the fields of its derived baton.
int timeout = 0;
Baton(Database* db_, Napi::Function cb_) :
db(db_), status(SQLITE_OK) {
db->Ref();
if (!cb_.IsUndefined() && cb_.IsFunction()) {
callback.Reset(cb_, 1);
}
}
virtual ~Baton() {
if (request) napi_delete_async_work(db->Env(), request);
db->Unref();
callback.Reset();
}
};
struct OpenBaton : Baton {
std::string filename;
int mode;
OpenBaton(Database* db_, Napi::Function cb_, const char* filename_, int mode_) :
Baton(db_, cb_), filename(filename_), mode(mode_) {}
virtual ~OpenBaton() override = default;
};
struct ExecBaton : Baton {
std::string sql;
ExecBaton(Database* db_, Napi::Function cb_, const char* sql_) :
Baton(db_, cb_), sql(sql_) {}
virtual ~ExecBaton() override = default;
};
struct LoadExtensionBaton : Baton {
std::string filename;
LoadExtensionBaton(Database* db_, Napi::Function cb_, const char* filename_) :
Baton(db_, cb_), filename(filename_) {}
virtual ~LoadExtensionBaton() override = default;
};
struct LimitBaton : Baton {
int id;
int value;
LimitBaton(Database* db_, Napi::Function cb_, int id_, int value_) :
Baton(db_, cb_), id(id_), value(value_) {}
virtual ~LimitBaton() override = default;
};
// Deliverable 07: hook registration carries the wanted state (the JS
// layer calls configure(type, true) for every addListener, so a toggle
// here would uninstall a hook on the second listener).
struct HookBaton : Baton {
bool enable = false;
HookBaton(Database* db_, Napi::Function cb_, bool enable_) :
Baton(db_, cb_), enable(enable_) {}
virtual ~HookBaton() override = default;
};
struct CheckpointBaton : Baton {
std::string database;
int mode;
int log_frames = 0;
int ckpt_frames = 0;
CheckpointBaton(Database* db_, Napi::Function cb_,
const char* database_, int mode_) :
Baton(db_, cb_), database(database_), mode(mode_) {}
virtual ~CheckpointBaton() override = default;
};
struct TableInfoBaton : Baton {
struct Column {
int cid;
std::string name;
std::string type;
bool not_null;
std::string dflt;
int pk;
std::string collate;
bool autoinc;
};
std::string database;
std::string table;
std::vector<Column> columns;
TableInfoBaton(Database* db_, Napi::Function cb_,
const char* database_, const char* table_) :
Baton(db_, cb_), database(database_), table(table_) {}
virtual ~TableInfoBaton() override = default;
};
struct DbConfigBaton : Baton {
int op;
int value;
int previous = 0;
DbConfigBaton(Database* db_, Napi::Function cb_, int op_, int value_) :
Baton(db_, cb_), op(op_), value(value_) {}
virtual ~DbConfigBaton() override = default;
};
// Authorizer registration. Owns `policy` until the exclusive handler
// installs it (or the schedule fails and the baton destructor frees
// it).
struct AuthBaton : Baton {
AuthPolicy* policy = NULL;
bool remove = false;
AuthBaton(Database* db_, Napi::Function cb_) : Baton(db_, cb_) {}
virtual ~AuthBaton() override { delete policy; }
};
// ATTACH-gate registration (Deliverable 11). Carries the enabled flag
// and the allowlist until the exclusive handler installs them.
struct AttachGateBaton : Baton {
bool enable = false;
std::vector<std::string> allow;
AttachGateBaton(Database* db_, Napi::Function cb_) : Baton(db_, cb_) {}
virtual ~AttachGateBaton() override = default;
};
// Cancellation-token registration. Owns the Int32Array reference (and
// the captured flag pointer) until the exclusive handler installs it.
struct ProgressFlagBaton : Baton {
int period;
std::atomic<int32_t>* flag = NULL;
Napi::Reference<Napi::Value> buffer;
ProgressFlagBaton(Database* db_, Napi::Function cb_, int period_) :
Baton(db_, cb_), period(period_) {}
virtual ~ProgressFlagBaton() override = default;
};
typedef void (*Work_Callback)(Baton* baton);
struct Call {
Call(Work_Callback cb_, Baton* baton_, bool exclusive_ = false) :
callback(cb_), exclusive(exclusive_), baton(baton_) {};
Work_Callback callback;
bool exclusive;
Baton* baton;
};
struct ProfileInfo {
std::string sql;
sqlite3_int64 nsecs;
};
struct UpdateInfo {
// Which connection event this queued item delivers. change,
// commit and rollback share one ordered channel so a
// transaction's change events always reach JS before its commit
// (or rollback) event: three separate uv_async_t watchers would
// be drained in handle-init order, not send order, whenever two
// were pending in the same loop wakeup.
enum Kind { kChange, kCommit, kRollback } kind = kChange;
int type = 0; // kChange only (SQLITE_INSERT/…)
std::string database;
std::string table;
sqlite3_int64 rowid = 0;
};
struct WalInfo {
std::string database;
int pages;
};
// The sqlite handle exists and has not been closed. Note that Closing
// counts as open: work scheduled during a close waits behind it and is
// only failed (or runs) once the close's outcome is known. This is the
// old `open` bool's exact semantics.
bool IsOpen() { return db_state == DbState::Open || db_state == DbState::Closing; }
// Terminal: a close completed. The old `locked` tombstone.
bool IsClosed() { return db_state == DbState::Closed; }
// The raw mode value for cross-file users (src/vtab.cc's JS-thread
// half converts generator arguments with it).
int IntegerMode() const { return integer_mode; }
typedef Async<std::string, Database> AsyncTrace;
typedef Async<ProfileInfo, Database> AsyncProfile;
typedef Async<UpdateInfo, Database> AsyncUpdate;
typedef Async<WalInfo, Database> AsyncWal;
typedef Async<PreupdateInfo, Database> AsyncPreupdate;
friend class Statement;
friend class Backup;
friend class Session;
friend class Blob;
friend struct UserFunctionOps;
friend struct SessionOps;
friend struct VtabOps;
// Marks that the JavaScript thread is itself inside a sqlite call on
// this connection and therefore cannot service the ThreadSafeFunction
// round trip a user-defined function needs. Set by every main-thread
// sqlite-driving path (the *Sync methods, the synchronous prepare, and
// every sqlite3_finalize — an aggregate's xFinal can fire from any of
// them). Main-thread-only state; the worker callbacks read it as the
// deadlock refusal test.
unsigned sync_sqlite_depth = 0;
struct SyncSqliteGuard {
Database* db;
explicit SyncSqliteGuard(Database* db_) : db(db_) { db->sync_sqlite_depth++; }
~SyncSqliteGuard() { db->sync_sqlite_depth--; }
SyncSqliteGuard(const SyncSqliteGuard&) = delete;
SyncSqliteGuard& operator=(const SyncSqliteGuard&) = delete;
};
// Drains the database queue on every exit path from a Work_After*
// handler, including TRY_CATCH_CALL's early return when a JS
// callback throws. The database-level completions (open/exec/close/
// loadExtension) call Process() as their last statement; a throwing
// completion callback used to skip it, leaving everything queued
// behind the exclusive call undispatched forever — every later
// query on the connection never settled. Same discipline as
// Statement::CallGuard, which runs the statement-side bookkeeping
// on every exit path for the same reason.
//
// When the handler exits through a throwing callback the exception
// is still pending, and Process() cannot deliver JS under it —
// napi_call_function is refused while an exception is pending, so
// the Closed-state branch (which fails queued batons inline) would
// bail out of its first callback and strand the rest. The guard
// lifts the pending exception for the drain and re-arms it
// afterwards, so the uncaught-exception delivery the runtime
// performs is unchanged.
struct ProcessGuard {
Database* db;
explicit ProcessGuard(Database* db_) : db(db_) {}
~ProcessGuard() {
auto env = db->Env();
napi_value pending = nullptr;
bool had_pending = false;
napi_is_exception_pending(env, &had_pending);
if (had_pending) {
napi_get_and_clear_last_exception(env, &pending);
}
db->Process();
// Re-arm the lifted exception — unless the drain itself
// threw, in which case that newer exception wins and the
// lifted one is dropped (one exception still surfaces).
if (pending != nullptr) {
bool still_clean = false;
napi_is_exception_pending(env, &still_clean);
if (!still_clean) napi_throw(env, pending);
}
}
ProcessGuard(const ProcessGuard&) = delete;
ProcessGuard& operator=(const ProcessGuard&) = delete;
};
Database(const Napi::CallbackInfo& info);
// Records `err` — a value thrown by a user callback on the JS thread —
// as the `cause` the next SQLite error built for this connection will
// carry (AttachPendingJsError consumes it). Public because the virtual
// table machinery reaches it from free functions on the JS thread; the
// user-function path sets the slot directly (it is a friend).
void SetPendingJsError(Napi::Value err);
~Database() {
RemoveCallbacks();
RemoveUserFunctions();
RemoveAuthorizer();
RemoveProgressHandler();
RemovePreupdateHook();
// owner_dying: this Database is mid-destruction, so the
// detaching objects must not call back into it.
CloseLiveSessions(true);
CloseLiveBlobs(true);
sqlite3_close(_handle);
_handle = NULL;
db_state = DbState::Closed;
// After sqlite3_close every virtual table instance is
// disconnected, so the module holders (and their sqlite3_module
// structs) can be freed. See src/vtab.cc — last, because
// RemoveVtabs drains through the queue this mutex guards.
RemoveVtabs();
uv_mutex_destroy(&vtab_refs_mutex);
}
protected:
WORK_DEFINITION(Open);
WORK_DEFINITION(Exec);
WORK_DEFINITION(Close);
WORK_DEFINITION(LoadExtension);
void Schedule(Work_Callback callback, Baton* baton, bool exclusive = false);
void Process();
Napi::Value Wait(const Napi::CallbackInfo& info);
static void Work_Wait(Baton* baton);
Napi::Value Serialize(const Napi::CallbackInfo& info);
Napi::Value Parallelize(const Napi::CallbackInfo& info);
Napi::Value Configure(const Napi::CallbackInfo& info);
Napi::Value Interrupt(const Napi::CallbackInfo& info);
// Delivers the profile ('profile' event) batons already queued for
// this connection, synchronously. A finished statement's timing is
// handed over on the worker thread and dispatched to JS on a later
// loop turn, so a caller that has just awaited a query has not seen
// its span yet; lib/sqlite3.js drains through this before dropping a
// subscribeQueries() subscriber, and exposes it as
// sqlite3.flushQuerySpans().
Napi::Value FlushProfile(const Napi::CallbackInfo& info);
/** Current integerMode as a string: 'number' | 'bigint' | 'mixed'. */
Napi::Value IntegerModeGetter(const Napi::CallbackInfo& info);
// sync_sqlite_depth > 0, for the JS-side re-entrancy refusals.
Napi::Value InSyncCallGetter(const Napi::CallbackInfo& info);
// Read-only snapshot of the connection's scheduling state, computed
// on read from the authoritative fields; diagnostics and tests consume
// it. The statement cache's hot guard reads the individual accessors
// below instead (the per-call object construction measured +46% on
// db.getSync cached in bench, Deliverable 05).
Napi::Value StateGetter(const Napi::CallbackInfo& info);
// Individual read-only views of the same authoritative fields, for
// hot paths that must not pay for the state object's construction.
Napi::Value ClosingGetter(const Napi::CallbackInfo& info);
Napi::Value LockedGetter(const Napi::CallbackInfo& info);
Napi::Value SerializedGetter(const Napi::CallbackInfo& info);
Napi::Value PendingGetter(const Napi::CallbackInfo& info);
Napi::Value QueuedGetter(const Napi::CallbackInfo& info);
// True while an exclusive operation (exec/close/wait/loadExtension) is
// running or waiting in the database queue. Deprecated in favour of
// state; kept for one minor version as an alias.
Napi::Value QueueBusy(const Napi::CallbackInfo& info);
static void SetBusyTimeout(Baton* baton);
static void SetLimit(Baton* baton);
static void SetWalAutocheckpoint(Baton* baton);
// --- Live sqlite introspection (Phase 1/6): transaction state, db
// status counters, memory release, run-time limit reads and
// attached-file paths. All follow the db.changes reader pattern (see
// LiveReadGate): refuse while a worker round trip could hold the
// connection mutex, serialize on it otherwise.
bool LiveReadGate(Napi::Env env, const char* who);
Napi::Value InTransactionGetter(const Napi::CallbackInfo& info);
Napi::Value TxnStateGetter(const Napi::CallbackInfo& info);
Napi::Value DbStatus(const Napi::CallbackInfo& info);
Napi::Value ReleaseMemory(const Napi::CallbackInfo& info);
Napi::Value GetLimit(const Napi::CallbackInfo& info);
Napi::Value DbLocation(const Napi::CallbackInfo& info);
// Deferred main-thread sqlite work (see MayBlockOnWorkerRoundTrip):
// exclusive, so each dispatches only once pending == 0 and the
// connection mutex is provably free. Definitions in statement.cc.
static void Work_DeferredStatementFinalize(Baton* baton);
static void Work_DeferredHandleFinalize(Baton* baton);
// Same deferral for a collected Session's native handle. Definition
// in src/session.cc.
static void Work_DeferredSessionDelete(Baton* baton);
static void RegisterTraceCallback(Baton* baton);
static void UpdateTraceMask(Database* db, sqlite3* handle);
static int TraceV2Callback(unsigned int type, void* ctx, void* p, void* x);
static void TraceCallback(Database* db, std::string* sql);
static void RegisterProfileCallback(Baton* baton);
static void ProfileCallback(Database* db, ProfileInfo* info);
static void RegisterUpdateCallback(Baton* baton);
static void UpdateCallback(void* db, int type, const char* database, const char* table, sqlite3_int64 rowid);
static void UpdateCallback(Database* db, UpdateInfo* info);
// --- Transaction hooks (commit / rollback), the WAL hook, the
// declarative authorizer and the progress handler (Deliverable 07).
// All registration goes through the exclusive queue with the
// MayBlockOnWorkerRoundTrip deferral — every sqlite3_*_hook /
// sqlite3_set_authorizer / sqlite3_progress_handler call takes the
// connection mutex.
static void RegisterCommitCallback(Baton* baton);
static void RegisterRollbackCallback(Baton* baton);
static void RegisterWalCallback(Baton* baton);
// The sqlite hooks. Advisory by design: the commit/rollback/wal
// return values are ignored (always "proceed"), because a veto would
// block the committing worker on the JS thread — the D06 deadlock
// shape, on the commit path. A NULL channel (hooks removed, close in
// progress) means drop the event: sqlite itself may still fire a hook
// during the implicit rollback of sqlite3_close.
static int CommitHook(void* ctx);
static void RollbackHook(void* ctx);
static int WalHook(void* ctx, sqlite3* handle, const char* database,
int pages);
static void TxnCallback(Database* db, UpdateInfo* info);
static void WalCallback(Database* db, WalInfo* info);
// Creates the shared channel on first need / releases it when no
// transaction hook is left. Exclusive contexts only.
static void EnsureTxnChannel(Database* db);
static void MaybeDropTxnChannel(Database* db);
Napi::Value SetAuthorizer(const Napi::CallbackInfo& info);
static void Work_SetAuthorizer(Baton* baton);
// Runs on whatever thread is preparing. auth_policy is only ever
// swapped by the exclusive handler (pending == 0, so no prepare is
// running) and freed after the swap, so the pointer read here is
// stable for the whole call.
static int AuthorizerCallback(void* ctx, int action, const char* arg1,
const char* arg2, const char* database, const char* trigger);
void RemoveAuthorizer();
// --- ATTACH gate (Deliverable 11). SQLite exposes exactly one
// authorizer slot per connection, and the declarative authorizer above
// occupies it whenever a policy is installed — so the gate is not a
// second authorizer but a pre-filter evaluated inside
// AuthorizerCallback: while attach_gate is set, every SQLITE_ATTACH
// action (which is also what VACUUM INTO fires for its output file)
// is denied unless the target filename matches the allowlist. The
// allowlist is populated by the JS layer, which permission-checks each
// entry against Node's permission model at declare time — the C side
// cannot query it, and a JS callback here would put JavaScript on the
// prepare path. Matching is lexical (exact string, separator-
// normalised, or joined with the process cwd); symlinks are not
// resolved, so a mismatched spelling is denied — fail-closed.
// attach_gate/attach_allow follow auth_policy's lifetime discipline:
// written only by the exclusive handler at pending == 0 (no prepare
// can be reading them), cleared in Work_BeginClose and ~Database.
Napi::Value SetAttachGate(const Napi::CallbackInfo& info);
static void Work_SetAttachGate(Baton* baton);
static bool AttachTargetAllowed(const Database* db, const char* arg1);
// --- Preupdate event (Deliverable 08). One preupdate hook slot
// exists per connection and is shared with the session extension:
// sqlite3session_create installs its own hook, displacing ours.
// Ownership is therefore exclusive — see the note in src/session.h —
// and enforced on the JS thread at pending == 0 in both directions.
// The trampoline materialises old/new rows eagerly (the
// sqlite3_preupdate_old/new values die with the callback) and defers
// the JS event through the shared channel; the JS-thread half
// converts with the connection's integer mode.
static void RegisterPreupdateCallback(Baton* baton);
static void PreupdateTrampoline(void* ctx, sqlite3* handle, int op,
const char* database, const char* table, sqlite3_int64 key1,
sqlite3_int64 key2);
static void PreupdateCallback(Database* db, PreupdateInfo* info);
void RemovePreupdateHook();
// Progress handler: either an atomic flag inside a SharedArrayBuffer
// (db.cancellationToken(); zero per-invocation cost, cancellable from
// any thread) or a JS callback making the D06 blocking round trip
// (observability; documented as slow). One sqlite3_progress_handler
// slot per connection, so the two forms replace each other.
enum class ProgressMode { None, Flag, Callback };
Napi::Value SetProgressFlag(const Napi::CallbackInfo& info);
Napi::Value SetProgressCallback(const Napi::CallbackInfo& info);
static void Work_SetProgressFlag(Baton* baton);
static void Work_SetProgressCallback(Baton* baton);
// The sqlite handler. Flag mode is a relaxed atomic load; callback
// mode round-trips to the JS thread and aborts the statement when the
// callback returns truthy or throws. Defined in src/function.cc with
// the round-trip machinery. ApplyProgressHandler (re)installs or
// removes the sqlite hook from the current state; exclusive contexts
// only.
static int ProgressHandler(void* ctx);
static void ApplyProgressHandler(Database* db);
void RemoveProgressHandler();
// Frees the JS-callback holder, releasing its JS references and its
// database ref exactly like JsFuncDestroy does for registered
// functions (no sqlite registration exists to call xDestroy for us).
void DropJsProgressHolder();
// WAL checkpoints (sqlite3_wal_checkpoint_v2), table metadata
// (PRAGMA table_info + sqlite3_table_column_metadata) and the safe
// sqlite3_db_config subset. Scheduled work; the sqlite calls run on
// the worker pool like statement work.
Napi::Value Checkpoint(const Napi::CallbackInfo& info);
static void Work_BeginCheckpoint(Baton* baton);
static void Work_Checkpoint(napi_env env, void* data);
static void Work_AfterCheckpoint(napi_env env, napi_status status, void* data);
Napi::Value TableInfo(const Napi::CallbackInfo& info);
static void Work_BeginTableInfo(Baton* baton);
static void Work_TableInfo(napi_env env, void* data);
static void Work_AfterTableInfo(napi_env env, napi_status status, void* data);
Napi::Value DbConfig(const Napi::CallbackInfo& info);
static void Work_BeginDbConfig(Baton* baton);
static void Work_DbConfig(napi_env env, void* data);
static void Work_AfterDbConfig(napi_env env, napi_status status, void* data);
// --- Changeset apply, serialize/deserialize and session/blob
// tracking (Deliverable 08). Definitions in src/session.cc and
// src/blob.cc. _applyChangeset runs on the worker pool like
// statement work (it is one big savepoint-wrapped write); the JS
// conflict/filter forms round-trip through a per-apply
// ThreadSafeFunction (js_apply_depth keeps MayBlockOnWorkerRoundTrip
// truthful while any are in flight). _serializeToBytes and
// _deserialize are exclusive: a snapshot must not interleave with
// writes, and deserialize replaces the schema image outright.
Napi::Value ApplyChangeset(const Napi::CallbackInfo& info);
static void Work_BeginApplyChangeset(Baton* baton);
static void Work_ApplyChangeset(napi_env env, void* data);
static void Work_AfterApplyChangeset(napi_env env, napi_status status, void* data);
Napi::Value SerializeToBytes(const Napi::CallbackInfo& info);
static void Work_BeginSerializeToBytes(Baton* baton);
static void Work_SerializeToBytes(napi_env env, void* data);
static void Work_AfterSerializeToBytes(napi_env env, napi_status status, void* data);
Napi::Value Deserialize(const Napi::CallbackInfo& info);
static void Work_BeginDeserialize(Baton* baton);
static void Work_Deserialize(napi_env env, void* data);
static void Work_AfterDeserialize(napi_env env, napi_status status, void* data);
// JS-thread, nothing in flight (Work_BeginClose / ~Database): delete
// every tracked session handle / close every tracked blob handle so
// sqlite3_close cannot leave them dangling. Both are idempotent.
void CloseLiveSessions(bool owner_dying = false);
void CloseLiveBlobs(bool owner_dying = false);
// sqlite3_changes64 / sqlite3_total_changes64, subject to the
// connection's integer mode.
Napi::Value ChangesGetter(const Napi::CallbackInfo& info);
Napi::Value TotalChangesGetter(const Napi::CallbackInfo& info);
void RemoveCallbacks();
// True when a main-thread sqlite call on this connection could block
// on the connection mutex: a JS function, collation or progress
// callback is registered, or a JavaScript virtual table is registered,
// and statement work is in flight; or a changeset apply carrying JS
// conflict/filter handlers is queued or in flight (the apply holds the
// connection mutex for its whole run, and its handlers block on this
// thread) — a worker may be sitting inside a round trip holding that
// mutex while it waits for this very thread. Callers on the JS thread
// must defer their sqlite call (the exclusive queue runs it once
// nothing is in flight) instead of touching the handle. Without
// registered callbacks in-flight work never waits on the JS thread, so
// the mutex is only ever held briefly and blocking on it is fine —
// which is why every pre-existing path is unchanged.
//
// js_vtabs belongs in this set for exactly the same reason as
// js_functions: a worker inside xFilter/xNext waits on the JS thread
// for the generator's next batch while holding the connection mutex.
// Leaving it out deadlocked two concurrent queries against a
// db.table() (or db.values()) table: the first query's completion
// handler called sqlite3_finalize inline, which wants the mutex the
// second query's worker is holding while it waits for this thread.
bool MayBlockOnWorkerRoundTrip() {
return (!(js_functions.empty() && js_collations.empty()
&& js_vtabs.empty()
&& js_progress == NULL && js_apply_depth == 0))
&& pending > 0;
}
// --- User-defined functions, aggregates, window functions and
// collations (Deliverable 06). Implementation in src/function.cc.
// JS-visible entry points. They validate argument types and schedule
// the registration through the exclusive queue; the JS layer
// (lib/sqlite3.js) wraps them with option parsing and the statement
// cache flush.
Napi::Value RegisterUserFunction(const Napi::CallbackInfo& info);
Napi::Value RegisterUserAggregate(const Napi::CallbackInfo& info);
Napi::Value RegisterUserCollation(const Napi::CallbackInfo& info);
Napi::Value RemoveUserFunction(const Napi::CallbackInfo& info);
Napi::Value RemoveUserCollation(const Napi::CallbackInfo& info);
// Registration handlers. Exclusive: they touch the connection mutex
// (sqlite3_create_function_v2 / sqlite3_create_collation take it), and
// a worker blocked mid-round-trip inside sqlite3_step holds that mutex
// while waiting for the JS thread — so a registration dispatched while
// anything is in flight would deadlock the JS thread on the mutex.
// Waiting for pending == 0 (the exclusive semantic) guarantees the
// mutex is free.
static void Work_RegisterFunction(Baton* baton);
static void Work_RegisterAggregate(Baton* baton);
static void Work_RegisterCollation(Baton* baton);
static void Work_RemoveFunction(Baton* baton);
static void Work_RemoveCollation(Baton* baton);
// sqlite invokes these from whatever thread is stepping the statement.
static void JsScalarFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv);
static void JsAggregateStep(sqlite3_context* ctx, int argc, sqlite3_value** argv);
static void JsAggregateFinal(sqlite3_context* ctx);
static void JsAggregateValue(sqlite3_context* ctx);
static void JsAggregateInverse(sqlite3_context* ctx, int argc, sqlite3_value** argv);
static int JsCollation(void* ctx, int len1, const void* d1, int len2, const void* d2);
// xDestroy for every registration. Runs on the JS thread by
// construction (see src/function.cc).
static void JsFuncDestroy(void* data);
// Unregisters and frees every user function and collation. Called from
// Work_BeginClose and ~Database, both main-thread with nothing in
// flight. Registrations sqlite refuses to drop (SQLITE_BUSY with a
// suspended cursor — which also makes the close fail) are kept alive
// with their holders, so the connection stays consistent.
void RemoveUserFunctions();
// Defined in src/function.cc beside the channel they serve: creates
// the shared ThreadSafeFunction on demand (the progress callback form
// needs it without any SQL function being registered), and reports a
// registration failure on the connection's 'error' event.
bool EnsureJsChannel();
void ReportRegistrationFailure(int rc);
// --- JavaScript virtual tables (Phase 4). Implementation in
// src/vtab.cc; same lifecycle as the user functions above — exclusive
// registration, refuse-from-sync-callbacks, teardown at close.
Napi::Value RegisterVtab(const Napi::CallbackInfo& info);
Napi::Value RemoveVtab(const Napi::CallbackInfo& info);
static void Work_RegisterVtab(Baton* baton);
static void Work_RemoveVtab(Baton* baton);
// Drops every module registration and frees the holders once no
// instance can remain (Work_BeginClose orders before the actual
// sqlite3_close, which disconnects instances).
void RemoveVtabs();
// Creates the vtab round-trip channel on demand; reports on 'error'.
bool EnsureVtabChannel();
void ReleaseVtabChannelIfIdle();
// Releases the callback channel when no registration is left. Only
// called from live-loop contexts (the removal handlers, Work_BeginClose)
// — never from a destructor, where it could re-enter the channel's own
// teardown and hang the process at exit.
void ReleaseJsChannelIfIdle();
// Attaches the most recently thrown JS error from a user function as
// `cause` on `err`, consuming it. The slot is set from the
// ThreadSafeFunction callback while a worker is blocked mid-round-trip;
// the step failure it causes is always the next error built for this
// connection, which is where this is called from.
void AttachPendingJsError(Napi::Object err);
protected:
sqlite3* _handle = NULL;
DbState db_state = DbState::Opening;
// The sqlite3_open_v2 flags this connection was opened with, recorded
// in Work_Open. The ATTACH gate reads SQLITE_OPEN_URI from it: without
// that flag a 'file:…' target is an ordinary filename, not a URI.
int open_mode = 0;
// Set when Work_Open failed: the connection then lands in DbState::Closed
// (it will never become usable), and Process()'s closed-state drain
// fails work queued behind the failed open with THIS error rather than
// the generic "Database handle is closed" — the caller queued against a
// database that never existed, and the open failure is the error that
// explains it. Before Deliverable 11 that work sat stranded in the
// queue forever: Process() never dispatched from the Opening state.
bool open_failed = false;
std::string open_error_message;
int open_error_status = SQLITE_OK;
// True only while an exclusive call (exec/close/wait/loadExtension)
// holds the database: set when it is dispatched, cleared when it
// completes. The old sticky `locked` flag stayed true after an
// exclusive call finished (and forever after close); db.state exposes
// this one honestly.
bool exclusiveHeld = false;
unsigned int pending = 0;
bool serialize = false;
// See IntegerMode above. Read by Statement when converting rows and
// lastID; only ever mutated on the main thread by configure().
int integer_mode = INTEGER_NUMBER;
std::queue<Call*> queue;
AsyncTrace* debug_trace = NULL;
AsyncProfile* debug_profile = NULL;
// Shared, ordered channel for the change/commit/rollback events (see
// UpdateInfo::Kind). The per-hook installed flags say which sqlite
// hooks feed it.
AsyncUpdate* txn_event = NULL;
bool hook_change = false;
bool hook_commit = false;
bool hook_rollback = false;
AsyncWal* wal_event = NULL;
// --- Preupdate event and session/blob tracking (Deliverable 08) ---
//
// Lifetime answers (the checklist items these must settle):
//
// - preupdate_event is an Async channel exactly like txn_event:
// created by the exclusive registration handler, finished in
// RemovePreupdateHook (Work_BeginClose, ~Database). The sqlite
// hook is uninstalled before the channel is finished; a NULL
// channel inside the trampoline means "drop the event" (removed /
// closing), the same contract the update hook has.
// - live_sessions / live_blobs are JS-thread-only registries of the
// wrappers holding native handles. Population: session/blob
// construction (before the handle exists, so the preupdate-slot
// ownership check cannot miss one still being created) and blob
// open-completion. Teardown: each object's close-completion, the
// create/open failure paths, and CloseLiveSessions/CloseLiveBlobs
// (Work_BeginClose, ~Database — main thread, pending == 0).
// Disposing twice is a benign no-op (the handle is NULL the
// second time); disposing after the database closed finds the
// handle already NULL; and unlike the progress slot these
// objects do not displace one another, so "disposed after
// something else took its place" cannot occur.
// - js_apply_depth counts the changeset applies with JS
// conflict/filter handlers that are queued or in flight
// (JS-thread writes: the entry point increments, the
// after-handler decrements), and is read by
// MayBlockOnWorkerRoundTrip. A count, not a flag: nothing
// serialises applies, so two can overlap, and a flag cleared by
// the first to finish would declare the connection safe while the
// second still held the mutex inside sqlite3changeset_apply —
// which is precisely the deadlock MayBlockOnWorkerRoundTrip
// exists to prevent.
AsyncPreupdate* preupdate_event = NULL;
bool hook_preupdate = false;
std::vector<Session*> live_sessions;
std::vector<Blob*> live_blobs;
int js_apply_depth = 0;
// --- Authorizer and progress handler (Deliverable 07) ---
//
// Lifetime answers (the checklist items these must settle):
//
// - auth_policy is owned by the Database, swapped only inside the
// exclusive registration handler — which by definition runs at
// pending == 0, when no prepare (the only context the authorizer
// fires in) can be executing on any thread. The old policy is
// freed after the sqlite3_set_authorizer swap, never under a
// concurrent callback. Cleared in Work_BeginClose and ~Database,
// both main-thread with nothing in flight.
// - The progress SharedArrayBuffer reference is a Napi persistent
// rooted on the main thread (created by the registration handler,
// reset in RemoveProgressHandler). The C handler dereferences
// progress_flag only while the sqlite hook is installed, and
// RemoveProgressHandler unregisters the hook before resetting the
// reference — at pending == 0, exclusive, so no step/prepare can
// be inside the handler. At environment teardown the reference is
// reclaimed with everything else; no JS runs.
// - js_progress (the callback form) is a JsFunc holder exactly like
// a registered function: JS-thread-only state, swept by the same
// exclusive removal paths, and its round trips ride the shared
// js_channel (see src/function.cc).
AuthPolicy* auth_policy = NULL;
// The ATTACH gate pre-filter state (see SetAttachGate above). Same
// access discipline as auth_policy: JS-thread writes inside the
// exclusive handler at pending == 0, reads from whatever thread is
// preparing.
bool attach_gate = false;
std::vector<std::string> attach_allow;
ProgressMode progress_mode = ProgressMode::None;
int progress_period = 0;
std::atomic<int32_t>* progress_flag = NULL;
Napi::Reference<Napi::Value> progress_buffer;
JsFunc* js_progress = NULL;
// --- User-defined function state (Deliverable 06) ---
// Registered callbacks, owned. Main-thread-only access: population
// happens in the exclusive registration handlers, teardown in
// RemoveUserFunctions and the same handlers' failure paths.
std::vector<JsFunc*> js_functions;
std::vector<JsFunc*> js_collations;
// One raw ThreadSafeFunction per database carrying every user-function
// round trip. Created lazily by the first registration, Unref'd (so it
// never keeps the event loop alive) and released only when the last
// registration is gone and nothing can be in flight.
napi_threadsafe_function js_channel = NULL;
// --- JavaScript virtual tables (Phase 4; see src/vtab.cc) ----------
// The live registrations, owned here. A holder leaves this list when
// sqlite drops its last reference to the module (VtabOps::ModuleDestroy)
// and is freed on the JS thread through pending_vtab_modules; what is
// still here at ~Database was never handed to sqlite.
std::vector<VtabModule*> js_vtabs;
napi_threadsafe_function vtab_channel = NULL;
// Instance rows-references queued by xDisconnect, cursor iterators
// queued by xClose and dead module holders queued by xDestroy (any
// thread) for JS-thread deletion — napi reference work is main-thread
// work.
uv_mutex_t vtab_refs_mutex;
std::vector<napi_ref> pending_vtab_refs;
std::vector<VtabModule*> pending_vtab_modules;
void QueueVtabRef(napi_ref ref);
void QueueVtabModule(VtabModule* module);
void DrainVtabRefs();
// The JS error thrown inside a user function that caused the current
// step failure: attached as `cause` on the SQLite error the statement
// then reports. JS-thread-only (set in the channel callback, consumed
// by the error builders).
Napi::Reference<Napi::Value> pending_js_error;
};
}
#endif