#ifndef NODE_SQLITE3_SRC_DATABASE_H #define NODE_SQLITE3_SRC_DATABASE_H #include #include #include #include #include #include #include #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 { 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 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(); 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(); if (data == nullptr || (data->*slot).IsEmpty()) return false; if (!val.IsObject()) return false; return val.As().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 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 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* flag = NULL; Napi::Reference 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 AsyncTrace; typedef Async AsyncProfile; typedef Async AsyncUpdate; typedef Async AsyncWal; typedef Async 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 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 live_sessions; std::vector 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 attach_allow; ProgressMode progress_mode = ProgressMode::None; int progress_period = 0; std::atomic* progress_flag = NULL; Napi::Reference 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 js_functions; std::vector 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 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 pending_vtab_refs; std::vector 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 pending_js_error; }; } #endif