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) Skipped
1026 lines
48 KiB
Plaintext
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
|