Files
SnowLuma/.pnpm-store/v11/files/25/6b5004c46619acc6db7ff500940ae302b65fe7f4af3c63d6d46bf4c4be1bb02a163c4e97e2403c11ac3d6f06bedc991c139788a7dc8b32acd90fcf8ab2e958
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

1918 lines
67 KiB
Plaintext

// Sessions, changesets and serialize/deserialize (Deliverable 08).
// See src/session.h for the threading model and the preupdate-slot
// ownership story.
#include <cstring>
#include <string>
#include <vector>
#include <sqlite3.h>
#include <napi.h>
#include <uv.h>
#include "macros.h"
#include "convert.h"
#include "database.h"
#include "session.h"
using namespace node_sqlite3;
namespace node_sqlite3 {
// Free functions that need Database's protected state (the apply round
// trips read the integer mode and interrupt the handle), mirroring
// UserFunctionOps in src/function.cc: one auditable friendship grant.
struct SessionOps {
static int IntegerMode(Database* db) { return db->integer_mode; }
static sqlite3* Handle(Database* db) { return db->_handle; }
};
} // namespace node_sqlite3
namespace {
#define SESSION_BEGIN(type) \
assert(baton); \
assert(baton->session); \
assert(!baton->session->locked); \
assert(!baton->session->closed); \
assert(baton->session->inited); \
assert(baton->session->_handle != NULL); \
baton->session->locked = true; \
baton->session->db->pending++; \
auto env = baton->session->Env(); \
CREATE_WORK("sqlite3.Session." #type, Work_##type, Work_After##type);
// A view of a JS binary value (Buffer, typed array, DataView or
// ArrayBuffer), already offset by byteOffset. Valid only while the JS
// value stays reachable — callers copy immediately.
struct BytesView {
const void* data = NULL;
size_t length = 0;
};
bool GetBytesView(napi_env env, const Napi::Value& value,
const std::string& what, BytesView* out) {
// DataView first: napi's IsBuffer also answers true for DataViews,
// and routing one through Napi::Buffer fails.
if (value.IsDataView()) {
size_t bytes = 0;
void* data = NULL;
napi_get_dataview_info(env, value, &bytes, &data, NULL, NULL);
out->data = data;
out->length = bytes;
return true;
}
if (value.IsBuffer()) {
// Node Buffers and plain Uint8Arrays.
Napi::Buffer<char> buf = value.As<Napi::Buffer<char>>();
out->data = buf.Data();
out->length = buf.Length();
return true;
}
if (value.IsTypedArray()) {
napi_typedarray_type type;
size_t elements = 0;
void* data = NULL;
napi_get_typedarray_info(env, value, &type, &elements, &data,
NULL, NULL);
switch (type) {
case napi_int8_array:
case napi_uint8_array:
case napi_uint8_clamped_array:
out->data = data;
out->length = elements;
return true;
default:
Napi::TypeError::New(env, "Cannot " + what +
": only byte-sized typed arrays are accepted"
).ThrowAsJavaScriptException();
return false;
}
}
if (value.IsArrayBuffer()) {
Napi::ArrayBuffer ab = value.As<Napi::ArrayBuffer>();
out->data = ab.Data();
out->length = ab.ByteLength();
return true;
}
Napi::TypeError::New(env, "Cannot " + what +
": a Buffer, Uint8Array or ArrayBuffer is required"
).ThrowAsJavaScriptException();
return false;
}
// Wraps sqlite3_malloc'd bytes in a Uint8Array over an external
// ArrayBuffer whose finalizer calls sqlite3_free — the modern-type
// sibling of the zero-copy Buffer path in CellToJS (src/convert.cc).
// Falls back to a copy when external buffers are unavailable (e.g.
// sandboxed renderers). Takes ownership of `data` on every path.
Napi::Value WrapOwnedBytes(Napi::Env env, void* data, size_t length) {
napi_value ab = NULL;
napi_status st = napi_create_external_arraybuffer(env, data, length,
[](napi_env, void* external_data, void*) {
sqlite3_free(external_data);
},
NULL, &ab);
if (st == napi_ok) {
return Napi::Uint8Array::New(env, length,
Napi::ArrayBuffer(env, ab), 0);
}
Napi::ArrayBuffer copy = Napi::ArrayBuffer::New(env, length);
if (length > 0 && data != NULL) {
memcpy(copy.Data(), data, length);
}
sqlite3_free(data);
return Napi::Uint8Array::New(env, length, copy, 0);
}
// Runs one full iteration pass over a changeset buffer to prove it is
// parseable before handing it to sqlite3changeset_invert/concat, whose
// documented contract assumes a valid input ("the results are
// undefined" otherwise).
int ValidateChangeset(int n, const void* data) {
sqlite3_changeset_iter* iter = NULL;
int rc = sqlite3changeset_start(&iter, n, const_cast<void*>(data));
if (rc != SQLITE_OK) return rc;
while ((rc = sqlite3changeset_next(iter)) == SQLITE_ROW) {
}
if (rc == SQLITE_DONE) rc = SQLITE_OK;
sqlite3changeset_finalize(iter);
return rc;
}
const char* ChangesetOpString(int op) {
return sqlite_authorizer_string(op); // insert / update / delete
}
const char* ChangesetConflictString(int e_conflict) {
switch (e_conflict) {
case SQLITE_CHANGESET_DATA: return "data";
case SQLITE_CHANGESET_NOTFOUND: return "notFound";
case SQLITE_CHANGESET_CONFLICT: return "conflict";
case SQLITE_CHANGESET_CONSTRAINT: return "constraint";
case SQLITE_CHANGESET_FOREIGN_KEY: return "foreignKey";
default: return "unknown";
}
}
Napi::Value CellsToArray(Napi::Env env, std::vector<Cell>& cells,
int integer_mode, const std::string& what) {
Napi::Array out = Napi::Array::New(env, cells.size());
for (size_t i = 0; i < cells.size(); i++) {
out.Set(i, CellToJS(env, cells[i], integer_mode, what));
if (env.IsExceptionPending()) return env.Null();
}
return out;
}
// --- Changeset apply round trips --------------------------------------------
//
// sqlite3changeset_apply holds the connection mutex for its whole run
// and invokes xFilter/xConflict inside it. The JS handler forms
// therefore block the applying worker on the JS thread exactly like a
// user-defined function — with one difference the Database accounts for
// through js_apply_depth: the mutex is held for the entire apply, not
// just the callback, so every main-thread sqlite call on this
// connection must defer while one is in flight.
struct ApplyBaton;
// One xFilter or xConflict invocation. Everything the JS side reads is
// materialised BEFORE the round trip (the iterator's sqlite3_value
// pointers die with the callback); the answer is written back by the JS
// thread under the condition variable.
struct ApplyRoundTrip {
bool is_filter = false;
int op = 0;
int conflict = 0;
int n_col = 0;
std::string table;
std::vector<char> pk; // 1 where the column is a primary key
std::vector<Cell> conflict_row; // DATA / CONFLICT conflicts only
std::vector<Cell> old_row; // UPDATE / DELETE changes only
std::vector<Cell> new_row; // INSERT / UPDATE changes only
int answer = 0;
bool errored = false;
std::string error;
uv_mutex_t mutex;
uv_cond_t cond;
bool done = false;
void Init() {
uv_mutex_init(&mutex);
uv_cond_init(&cond);
}
void Destroy() {
uv_mutex_destroy(&mutex);
uv_cond_destroy(&cond);
}
};
struct ApplyBaton : Database::Baton {
int n = 0;
void* data = NULL; // private copy of the changeset bytes
int decision = SQLITE_CHANGESET_ABORT;
Napi::FunctionReference on_conflict;
Napi::FunctionReference on_filter;
napi_threadsafe_function tsfn = NULL;
ApplyRoundTrip rt;
// { rebase: true }: harvest the conflict-resolution rebase buffer
// alongside the apply (sqlite3changeset_apply_v2). The buffer records
// each OMIT/REPLACE decision so the same conflicts can be replayed
// onto other changesets via sqlite3.rebaseChangeset().
bool want_rebase = false;
int rebase_n = 0;
void* rebase = NULL; // sqlite3_malloc'd; freed here or moved to JS
int status = SQLITE_OK;
std::string message;
ApplyBaton(Database* db_, Napi::Function cb_) : Baton(db_, cb_) {
rt.Init();
}
virtual ~ApplyBaton() override {
if (data != NULL) sqlite3_free(data);
if (rebase != NULL) sqlite3_free(rebase);
// The tsfn is released in Work_AfterApplyChangeset. If that never
// ran (environment teardown) there is no sound place to release
// it from; teardown reclaims it.
rt.Destroy();
}
};
// Extracts an error message from a thrown JS value, leaving no pending
// exception behind (a hostile message getter can throw; anything pending
// after the reads is cleared and ignored).
std::string ApplyJsErrorMessage(Napi::Env env) {
napi_value pending = NULL;
napi_get_and_clear_last_exception(env, &pending);
if (pending == NULL) return "unknown error";
Napi::Value err(env, pending);
if (err.IsObject()) {
Napi::Value msg = err.As<Napi::Object>().Get("message");
if (!env.IsExceptionPending() && msg.IsString()) {
return msg.As<Napi::String>().Utf8Value();
}
napi_value unused = NULL;
napi_get_and_clear_last_exception(env, &unused);
}
if (err.IsString()) return err.As<Napi::String>().Utf8Value();
return "non-error value thrown";
}
// The tsfn call-js callback: runs the user's filter or conflict handler
// with the pre-materialised values and records the answer. No exception
// may escape into the tsfn dispatch machinery.
void ApplyCallJs(napi_env nenv, napi_value /*js_callback*/, void* /*context*/,
void* data) {
auto* baton = static_cast<ApplyBaton*>(data);
Napi::Env env(nenv);
Napi::HandleScope scope(env);
ApplyRoundTrip& rt = baton->rt;
Napi::Function fn = rt.is_filter
? baton->on_filter.Value() : baton->on_conflict.Value();
if (rt.is_filter) {
Napi::Value answer = fn.Call({ Napi::String::New(env, rt.table) });
if (env.IsExceptionPending()) {
rt.errored = true;
rt.error = "changeset filter threw: " +
ApplyJsErrorMessage(env);
}
else {
rt.answer = answer.ToBoolean().Value() ? 1 : 0;
}
}
else {
Napi::Object info = Napi::Object::New(env);
info.Set("op", Napi::String::New(env, ChangesetOpString(rt.op)));
info.Set("table", Napi::String::New(env, rt.table.c_str()));
info.Set("conflict",
Napi::String::New(env, ChangesetConflictString(rt.conflict)));
info.Set("columnCount", Napi::Number::New(env, rt.n_col));
Napi::Array pk = Napi::Array::New(env, rt.pk.size());
for (size_t i = 0; i < rt.pk.size(); i++) {
pk.Set(i, Napi::Boolean::New(env, rt.pk[i] != 0));
}
info.Set("primaryKey", pk);
const int integer_mode = SessionOps::IntegerMode(baton->db);
if (!rt.conflict_row.empty()) {
info.Set("conflictRow",
CellsToArray(env, rt.conflict_row, integer_mode,
"a conflicting row value"));
}
if (!rt.old_row.empty()) {
info.Set("oldRow", CellsToArray(env, rt.old_row, integer_mode,
"an old row value"));
}
if (!rt.new_row.empty()) {
info.Set("newRow", CellsToArray(env, rt.new_row, integer_mode,
"a new row value"));
}
if (env.IsExceptionPending()) {
rt.errored = true;
rt.error = ApplyJsErrorMessage(env);
}
else {
Napi::Value answer = fn.Call({ info });
if (env.IsExceptionPending()) {
rt.errored = true;
rt.error = "changeset conflict handler threw: " +
ApplyJsErrorMessage(env);
}
else if (answer.IsNumber()) {
int64_t v = answer.As<Napi::Number>().Int64Value();
if (v == SQLITE_CHANGESET_OMIT
|| v == SQLITE_CHANGESET_REPLACE
|| v == SQLITE_CHANGESET_ABORT) {
rt.answer = static_cast<int>(v);
}
else {
rt.errored = true;
rt.error = "changeset conflict handler returned an "
"unknown decision; use CHANGESET_OMIT, "
"CHANGESET_REPLACE or CHANGESET_ABORT";
}
}
else if (answer.IsString()) {
std::string s = answer.As<Napi::String>().Utf8Value();
if (s == "omit") rt.answer = SQLITE_CHANGESET_OMIT;
else if (s == "replace") rt.answer = SQLITE_CHANGESET_REPLACE;
else if (s == "abort") rt.answer = SQLITE_CHANGESET_ABORT;
else {
rt.errored = true;
rt.error = "changeset conflict handler returned '" +
s + "'; use 'omit', 'replace' or 'abort'";
}
}
else {
rt.errored = true;
rt.error = "changeset conflict handler must return "
"'omit', 'replace', 'abort' or a CHANGESET_* constant";
}
}
}
uv_mutex_lock(&rt.mutex);
rt.done = true;
uv_cond_signal(&rt.cond);
uv_mutex_unlock(&rt.mutex);
}
bool ApplyRoundTripRun(ApplyBaton* baton) {
napi_status st = napi_call_threadsafe_function(baton->tsfn, baton,
napi_tsfn_blocking);
if (st != napi_ok) {
baton->rt.errored = true;
baton->rt.error = "the JavaScript environment is shutting down; "
"the changeset apply cannot continue";
return false;
}
ApplyRoundTrip& rt = baton->rt;
uv_mutex_lock(&rt.mutex);
while (!rt.done) uv_cond_wait(&rt.cond, &rt.mutex);
uv_mutex_unlock(&rt.mutex);
return true;
}
void ApplyMaterialiseRows(ApplyRoundTrip& rt,
sqlite3_changeset_iter* iter) {
rt.conflict_row.clear();
rt.old_row.clear();
rt.new_row.clear();
if (rt.conflict == SQLITE_CHANGESET_DATA
|| rt.conflict == SQLITE_CHANGESET_CONFLICT) {
rt.conflict_row.resize(rt.n_col);
for (int i = 0; i < rt.n_col; i++) {
sqlite3_value* v = NULL;
if (sqlite3changeset_conflict(iter, i, &v) == SQLITE_OK
&& v != NULL) {
ValueToCell(&rt.conflict_row[i], v);
}
}
}
if (rt.op == SQLITE_UPDATE || rt.op == SQLITE_DELETE) {
rt.old_row.resize(rt.n_col);
for (int i = 0; i < rt.n_col; i++) {
sqlite3_value* v = NULL;
if (sqlite3changeset_old(iter, i, &v) == SQLITE_OK
&& v != NULL) {
ValueToCell(&rt.old_row[i], v);
}
}
}
if (rt.op == SQLITE_INSERT || rt.op == SQLITE_UPDATE) {
rt.new_row.resize(rt.n_col);
for (int i = 0; i < rt.n_col; i++) {
sqlite3_value* v = NULL;
if (sqlite3changeset_new(iter, i, &v) == SQLITE_OK
&& v != NULL) {
ValueToCell(&rt.new_row[i], v);
}
}
}
}
int ApplyFilterTrampoline(void* ctx, const char* z_tab) {
auto* baton = static_cast<ApplyBaton*>(ctx);
if (baton->tsfn == NULL || baton->on_filter.IsEmpty()) return 1;
ApplyRoundTrip& rt = baton->rt;
rt.is_filter = true;
rt.errored = false;
rt.done = false;
rt.table = z_tab != NULL ? z_tab : "";
ApplyRoundTripRun(baton);
if (rt.errored) {
// xFilter has no error channel; interrupting the connection makes
// the apply's statements fail, which rolls back its savepoint.
baton->status = SQLITE_INTERRUPT;
baton->message = rt.error;
sqlite3_interrupt(SessionOps::Handle(baton->db));
return 0;
}
return rt.answer;
}
int ApplyConflictTrampoline(void* ctx, int e_conflict,
sqlite3_changeset_iter* iter) {
auto* baton = static_cast<ApplyBaton*>(ctx);
if (baton->tsfn == NULL || baton->on_conflict.IsEmpty()) {
return baton->decision;
}
ApplyRoundTrip& rt = baton->rt;
rt.is_filter = false;
rt.errored = false;
rt.done = false;
rt.conflict = e_conflict;
const char* table = NULL;
int n_col = 0, op = 0, indirect = 0;
unsigned char* pk = NULL;
sqlite3changeset_op(iter, &table, &n_col, &op, &indirect);
sqlite3changeset_pk(iter, &pk, &n_col);
rt.op = op;
rt.n_col = n_col;
rt.table = table != NULL ? table : "";
rt.pk.clear();
if (pk != NULL) {
for (int i = 0; i < n_col; i++) {
rt.pk.push_back(pk[i] != 0);
}
}
else {
rt.pk.resize(n_col, 0);
}
ApplyMaterialiseRows(rt, iter);
ApplyRoundTripRun(baton);
if (rt.errored) {
baton->status = SQLITE_ABORT;
baton->message = rt.error;
return SQLITE_CHANGESET_ABORT;
}
if (rt.answer == SQLITE_CHANGESET_REPLACE
&& e_conflict != SQLITE_CHANGESET_DATA
&& e_conflict != SQLITE_CHANGESET_CONFLICT) {
// Returning REPLACE for any other conflict is SQLITE_MISUSE from
// sqlite; report it as the handler's mistake instead.
baton->status = SQLITE_ABORT;
baton->message = "changeset conflict handler returned 'replace' "
"for a '" + std::string(ChangesetConflictString(e_conflict)) +
"' conflict, where only 'omit' or 'abort' is legal";
return SQLITE_CHANGESET_ABORT;
}
return rt.answer;
}
// --- Serialize / deserialize batons ------------------------------------------
struct SerializeBaton : Database::Baton {
std::string database;
sqlite3_int64 size = 0;
void* data = NULL; // sqlite3_serialize output, sqlite3_malloc'd
int status = SQLITE_OK;
std::string message;
SerializeBaton(Database* db_, Napi::Function cb_, const char* database_) :
Baton(db_, cb_), database(database_) {}
virtual ~SerializeBaton() override {
if (data != NULL) sqlite3_free(data);
}
};
struct DeserializeBaton : Database::Baton {
int n = 0;
void* data = NULL; // private copy handed to sqlite3_deserialize
unsigned int flags = 0;
int status = SQLITE_OK;
std::string message;
DeserializeBaton(Database* db_, Napi::Function cb_) :
Baton(db_, cb_) {}
virtual ~DeserializeBaton() override {
// Nulled once ownership moved to sqlite (FREEONCLOSE frees it on
// both the success and the failure path inside Work_Deserialize).
// Still owned here only when the work never ran (the database
// closed while the call was queued).
if (data != NULL) sqlite3_free(data);
}
};
} // namespace
// --- Session ------------------------------------------------------------------
Napi::Object Session::Init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
auto napi_default_method = static_cast<napi_property_attributes>(
napi_writable | napi_configurable);
auto t = DefineClass(env, "Session", {
InstanceMethod("changeset", &Session::Changeset, napi_default_method),
InstanceMethod("patchset", &Session::Patchset, napi_default_method),
InstanceMethod("diff", &Session::Diff, napi_default_method),
InstanceMethod("close", &Session::Close, napi_default_method),
InstanceAccessor("closed", &Session::ClosedGetter, nullptr),
});
// Per-env (see Database::AddonData).
env.GetInstanceData<Database::AddonData>()->session_ctor =
Napi::Persistent(t);
exports.Set("Session", t);
return exports;
}
void Session::Process() {
if ((closed || !inited) && !queue.empty()) {
return CleanQueue();
}
while (inited && !locked && !closed && !queue.empty()) {
auto call = std::unique_ptr<Call>(queue.front());
queue.pop();
call->callback(call->baton);
}
}
void Session::Schedule(Work_Callback callback, Baton* baton) {
if (closed) {
queue.emplace(new Call(callback, baton));
CleanQueue();
}
else if (!inited || locked || !queue.empty()) {
queue.emplace(new Call(callback, baton));
}
else {
callback(baton);
}
}
template <class T> void Session::Error(T* baton) {
auto env = baton->session->Env();
Napi::HandleScope scope(env);
Session* session = baton->session;
assert(session->status != 0);
EXCEPTION(session->message, session->status, exception);
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { exception };
TRY_CATCH_CALL(session->Value(), cb, 1, argv);
}
else {
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(session->Value(), 2, info);
}
}
void Session::CleanQueue() {
auto env = this->Env();
Napi::HandleScope scope(env);
if (queue.empty()) return;
// Environment teardown (worker termination): failing the queued
// calls constructs JS on a dying environment, which is fatal. Drop
// them instead — reference cleanup still works there, so the
// batons are destroyed normally.
if (Database::EnvCannotRunJs(env)) {
while (!queue.empty()) {
auto call = std::unique_ptr<Call>(queue.front());
queue.pop();
delete call->baton;
}
return;
}
// Every queued call is failed rather than dropped: a silent skip is
// the failure mode this codebase exists to prevent. (Calls queued
// behind a failed create see this same MISUSE — the create's real
// error already went to its own callback.)
EXCEPTION("Session is already closed", SQLITE_MISUSE, exception);
Napi::Value argv[] = { exception };
bool called = false;
while (!queue.empty()) {
auto call = std::unique_ptr<Call>(queue.front());
queue.pop();
std::unique_ptr<Baton> baton(call->baton);
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
TRY_CATCH_CALL(Value(), cb, 1, argv);
called = true;
}
}
if (!called) {
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(Value(), 2, info);
}
}
// { Database db, String dbName, String table ("" = all), Boolean indirect,
// [Function callback] }
Session::Session(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Session>(info) {
auto env = info.Env();
if (!info.IsConstructCall()) {
Napi::TypeError::New(env, "Use the new operator to create new Session objects").ThrowAsJavaScriptException();
return;
}
if (info.Length() <= 0 || !Database::HasInstance(info[0])) {
Napi::TypeError::New(env, "Database object expected").ThrowAsJavaScriptException();
return;
}
else if (info.Length() <= 1 || !info[1].IsString()) {
Napi::TypeError::New(env, "Database name expected").ThrowAsJavaScriptException();
return;
}
else if (info.Length() <= 2 || !info[2].IsString()) {
Napi::TypeError::New(env, "Table name expected (empty string records all tables)").ThrowAsJavaScriptException();
return;
}
else if (info.Length() <= 3 || !info[3].IsBoolean()) {
Napi::TypeError::New(env, "Indirect flag expected").ThrowAsJavaScriptException();
return;
}
else if (info.Length() > 4 && !info[4].IsUndefined() && !info[4].IsFunction()) {
Napi::TypeError::New(env, "Callback expected").ThrowAsJavaScriptException();
return;
}
this->db = Napi::ObjectWrap<Database>::Unwrap(info[0].As<Napi::Object>());
this->db->Ref();
// The preupdate-hook slot is owned exclusively: a 'preupdate'
// listener and a session cannot coexist on one connection. Checked
// here (JS thread) and again in Work_BeginCreate under the exclusive
// gate, so a listener registered after construction but before the
// create dispatched still wins loudly.
if (this->db->hook_preupdate) {
this->db->Unref();
this->db = NULL;
Napi::Error::New(env,
"cannot create a session while a 'preupdate' listener is "
"registered on this connection: both use SQLite's single "
"preupdate hook, and creating the session would silently "
"stop the listener's events. Remove the listener first"
).ThrowAsJavaScriptException();
return;
}
auto dbName = info[1].As<Napi::String>().Utf8Value();
auto table = info[2].As<Napi::String>().Utf8Value();
bool indirect = info[3].As<Napi::Boolean>().Value();
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("db", info[1]));
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("table", info[2]));
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("indirect", info[3]));
// Tracked from construction (before the handle exists) so the
// 'preupdate' registration check cannot miss a session still being
// created. Untracked by the create-failure path, close and teardown.
this->db->live_sessions.push_back(this);
auto* baton = new CreateBaton(this->db, info[4].As<Napi::Function>(), this);
baton->dbName = dbName;
baton->table = table;
baton->indirect = indirect;
this->db->Schedule(Work_BeginCreate, baton, true);
}
void Session::Work_BeginCreate(Database::Baton* b) {
auto holder = std::unique_ptr<CreateBaton>(static_cast<CreateBaton*>(b));
auto* db = holder->db;
assert(db->exclusiveHeld);
assert(db->IsOpen());
assert(db->_handle);
assert(db->pending == 0);
if (db->hook_preupdate) {
// A 'preupdate' listener was registered after the constructor's
// check but before this dispatched. Fail the create loudly
// rather than displacing the listener's hook.
auto* session = holder->session;
session->status = SQLITE_MISUSE;
session->message = "cannot create a session while a 'preupdate' "
"listener is registered on this connection";
session->DetachFromDatabase();
Error(holder.get());
db->exclusiveHeld = false;
db->Process();
return;
}
// The async work (and its completion) own the baton from here;
// CREATE_WORK reads `baton`. pending holds the exclusive window closed
// until Work_AfterCreate releases it (Work_BeginClose's pattern).
CreateBaton* baton = holder.release();
db->pending++;
auto env = db->Env();
CREATE_WORK("sqlite3.Session.Create", Work_Create, Work_AfterCreate);
}
void Session::Work_Create(napi_env e, void* data) {
auto* baton = static_cast<CreateBaton*>(data);
Session* session = baton->session;
sqlite3_mutex* mtx = sqlite3_db_mutex(baton->db->_handle);
sqlite3_mutex_enter(mtx);
// Remembered for session.diff(), which needs the schema back.
session->db_name = baton->dbName;
int rc = sqlite3session_create(baton->db->_handle,
baton->dbName.c_str(), &session->_handle);
if (rc == SQLITE_OK) {
if (baton->indirect) {
sqlite3session_indirect(session->_handle, 1);
}
// A NULL table name records changes for every table with a
// primary key (tables attach lazily on first change).
rc = sqlite3session_attach(session->_handle,
baton->table.empty() ? NULL : baton->table.c_str());
}
if (rc != SQLITE_OK) {
session->message = std::string(sqlite3_errmsg(baton->db->_handle));
if (session->_handle != NULL) {
sqlite3session_delete(session->_handle);
session->_handle = NULL;
}
}
sqlite3_mutex_leave(mtx);
session->status = rc;
}
void Session::Work_AfterCreate(napi_env e, napi_status status, void* data) {
std::unique_ptr<CreateBaton> baton(static_cast<CreateBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* session = baton->session;
auto* db = baton->db;
auto env = session->Env();
Napi::HandleScope scope(env);
db->pending--;
// The exclusive create released the database either way.
db->exclusiveHeld = false;
if (session->status != SQLITE_OK) {
session->DetachFromDatabase();
Error(baton.get());
}
else {
session->inited = true;
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(session->Value(), cb, 1, argv);
}
}
db->Process();
session->Process();
}
void Session::EndCall() {
assert(locked);
assert(db->pending);
locked = false;
db->pending--;
Process();
db->Process();
}
Session::~Session() {
// GC safety net, mirroring ~Statement: every queued Baton holds a Ref
// on the session, so the queue is provably empty and CleanQueue
// cannot fire JS from this finalizer. sqlite3session_delete takes the
// connection mutex internally; the deferred path is required when a
// worker could be mid-round-trip holding that mutex while waiting for
// this very thread.
CleanQueue();
if (_handle != NULL) {
if (db != NULL && db->MayBlockOnWorkerRoundTrip()) {
db->Schedule(Database::Work_DeferredSessionDelete,
new SessionHandleBaton(db, _handle), true);
_handle = NULL;
}
else {
sqlite3session_delete(_handle);
_handle = NULL;
}
}
// Detached sessions have already released the database (and nulled
// the pointer); this covers the never-detached case only.
if (db != NULL) {
UntrackFromDatabase();
db->Unref();
}
}
void Session::UntrackFromDatabase() {
if (db == NULL) return;
auto& v = db->live_sessions;
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == this) {
v.erase(it);
return;
}
}
}
// JS thread, nothing in flight (Work_BeginClose / ~Database) or at a
// completion point where the handle is already gone. Idempotent.
// See Blob::DetachFromDatabase for why the back-pointer is nulled here
// and why owner_dying skips the Unref: at environment teardown ~Session
// can run after ~Database, and UntrackFromDatabase would then walk a
// destroyed std::vector.
void Session::DetachFromDatabase(bool owner_dying) {
if (db == NULL) return;
if (_handle != NULL) {
sqlite3session_delete(_handle);
_handle = NULL;
}
UntrackFromDatabase();
closed = true;
// See Blob::DetachFromDatabase for why this is confined to the
// owner-dying path.
if (owner_dying) db = NULL;
}
void Database::Work_DeferredSessionDelete(Baton* b) {
auto baton = std::unique_ptr<SessionHandleBaton>(
static_cast<SessionHandleBaton*>(b));
auto* db = baton->db;
sqlite3session_delete(baton->handle);
db->exclusiveHeld = false;
db->Process();
}
// --- changeset / patchset ------------------------------------------------------
Napi::Value Session::Changeset(const Napi::CallbackInfo& info) {
auto env = info.Env();
Session* session = this;
OPTIONAL_ARGUMENT_FUNCTION(0, callback);
auto* baton = new BufferBaton(session, callback);
baton->patch = false;
session->Schedule(Work_BeginChangeset, baton);
return info.This();
}
Napi::Value Session::Patchset(const Napi::CallbackInfo& info) {
auto env = info.Env();
Session* session = this;
OPTIONAL_ARGUMENT_FUNCTION(0, callback);
auto* baton = new BufferBaton(session, callback);
baton->patch = true;
session->Schedule(Work_BeginChangeset, baton);
return info.This();
}
void Session::Work_BeginChangeset(Baton* baton) {
SESSION_BEGIN(Changeset);
}
void Session::Work_Changeset(napi_env e, void* data) {
auto* baton = static_cast<BufferBaton*>(data);
Session* session = baton->session;
// Serialize against concurrent writes recording changes through the
// preupdate hook, which run under the same connection mutex.
sqlite3_mutex* mtx = sqlite3_db_mutex(session->db->_handle);
sqlite3_mutex_enter(mtx);
int rc = baton->patch
? sqlite3session_patchset(session->_handle, &baton->n, &baton->data)
: sqlite3session_changeset(session->_handle, &baton->n, &baton->data);
sqlite3_mutex_leave(mtx);
session->status = rc;
if (rc != SQLITE_OK) {
session->message =
std::string(sqlite3_errmsg(session->db->_handle));
}
}
void Session::Work_AfterChangeset(napi_env e, napi_status status, void* data) {
std::unique_ptr<BufferBaton> baton(static_cast<BufferBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* session = baton->session;
auto env = session->Env();
Napi::HandleScope scope(env);
// Runs the end-of-call bookkeeping on every exit path, including
// TRY_CATCH_CALL's early return when a JS callback throws.
Session::CallGuard session_call_guard__(session);
if (session->status != SQLITE_OK) {
Error(baton.get());
return;
}
Napi::Function cb = baton->callback.Value();
if (!IS_FUNCTION(cb)) {
// Nobody to hand the buffer to; the baton destructor frees it.
return;
}
// Ownership of the sqlite3_malloc'd buffer moves into the Uint8Array
// finalizer; the baton destructor must not also free it.
void* buffer = baton->data;
baton->data = NULL;
Napi::Value result = WrapOwnedBytes(env, buffer,
static_cast<size_t>(baton->n));
Napi::Value argv[] = { env.Null(), result };
TRY_CATCH_CALL(session->Value(), cb, 2, argv);
}
// --- close ----------------------------------------------------------------------
// diff(table, fromDb): records the differences between this session's
// table and the same table in the attached database `fromDb` into the
// session (sqlite3session_diff). Neither database is written; harvest the
// recorded changes with changeset().
Napi::Value Session::Diff(const Napi::CallbackInfo& info) {
auto env = info.Env();
Session* session = this;
REQUIRE_ARGUMENT_STRING(0, table);
REQUIRE_ARGUMENT_STRING(1, fromDb);
OPTIONAL_ARGUMENT_FUNCTION(2, callback);
// sqlite3session_diff compares the named schema's table against the
// session's own; the same schema on both sides is a no-op that reads
// like a bug at the call site.
if (fromDb == session->db_name) {
Napi::Error::New(env,
"session.diff() needs a different attached database than the "
"one the session records ('" + session->db_name + "'): it "
"records the changes that would bring that database's table up "
"to this one's"
).ThrowAsJavaScriptException();
return env.Null();
}
auto* baton = new DiffBaton(session, callback);
baton->table = table;
baton->fromDb = fromDb;
session->Schedule(Work_BeginDiff, baton);
return info.This();
}
void Session::Work_BeginDiff(Baton* baton) {
SESSION_BEGIN(Diff);
}
void Session::Work_Diff(napi_env e, void* data) {
auto* baton = static_cast<DiffBaton*>(data);
Session* session = baton->session;
// Same serialization as Work_Changeset: the session's hash tables are
// the state being appended to.
sqlite3_mutex* mtx = sqlite3_db_mutex(session->db->_handle);
sqlite3_mutex_enter(mtx);
// The session's own schema (recorded at create time) is the
// "current" side; the table is compared against the same table in
// the attached database `fromDb`. pzErrMsg is sqlite3_malloc'd.
char* err = NULL;
int rc = sqlite3session_diff(session->_handle,
baton->fromDb.c_str(), baton->table.c_str(), &err);
sqlite3_mutex_leave(mtx);
session->status = rc;
if (rc != SQLITE_OK) {
if (err != NULL) {
session->message = std::string(err);
sqlite3_free(err);
}
else {
session->message =
std::string(sqlite3_errmsg(session->db->_handle));
}
}
}
void Session::Work_AfterDiff(napi_env e, napi_status status, void* data) {
std::unique_ptr<DiffBaton> baton(static_cast<DiffBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* session = baton->session;
auto env = session->Env();
Napi::HandleScope scope(env);
Session::CallGuard session_call_guard__(session);
if (session->status != SQLITE_OK) {
Error(baton.get());
return;
}
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(session->Value(), cb, 1, argv);
}
}
Napi::Value Session::Close(const Napi::CallbackInfo& info) {
auto env = info.Env();
Session* session = this;
OPTIONAL_ARGUMENT_FUNCTION(0, callback);
if (session->closed) {
// Dispose contract: a second close is a benign no-op, and one
// after the database closed finds the handle already gone.
if (IS_FUNCTION(callback)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(session->Value(), callback, 1, argv, info.This());
}
return info.This();
}
auto* baton = new Baton(session, callback);
session->Schedule(Work_BeginClose, baton);
return info.This();
}
void Session::Work_BeginClose(Baton* baton) {
SESSION_BEGIN(Close);
}
void Session::Work_Close(napi_env e, void* data) {
auto* baton = static_cast<Baton*>(data);
Session* session = baton->session;
// Unlinks from the connection's session list (under the connection
// mutex, taken internally) and frees the recorded changes.
sqlite3session_delete(session->_handle);
session->_handle = NULL;
}
void Session::Work_AfterClose(napi_env e, napi_status status, void* data) {
std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* session = baton->session;
auto env = session->Env();
Napi::HandleScope scope(env);
Session::CallGuard session_call_guard__(session);
session->DetachFromDatabase();
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(session->Value(), cb, 1, argv);
}
}
Napi::Value Session::ClosedGetter(const Napi::CallbackInfo& info) {
return Napi::Boolean::New(info.Env(), closed);
}
// --- Database-side: live-session teardown and changeset apply ------------------
void Database::CloseLiveSessions(bool owner_dying) {
// Main thread, nothing in flight (Work_BeginClose / ~Database).
// DetachFromDatabase deletes the handle (taking the connection mutex
// internally — fine here) and untracks; iterating over a copy because
// it mutates the vector.
std::vector<Session*> sessions = live_sessions;
live_sessions.clear();
for (Session* session : sessions) {
session->DetachFromDatabase(owner_dying);
}
}
// _applyChangeset(data, decision, onConflict|null, onFilter|null,
// [callback], [wantRebase]). decision is one of the
// CHANGESET_* return constants used when no JS conflict handler is given;
// onConflict / onFilter, when non-null, make the blocking round trip from
// inside sqlite3changeset_apply. wantRebase harvests the rebase buffer
// (the apply's conflict decisions) and hands it to the callback as its
// second argument.
Napi::Value Database::ApplyChangeset(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto* db = this;
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected at least 1 argument").ThrowAsJavaScriptException();
return env.Null();
}
BytesView view;
if (!GetBytesView(env, info[0], "apply a changeset", &view)) {
return env.Null();
}
if (info.Length() < 2 || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Argument 1 must be a CHANGESET_* decision constant").ThrowAsJavaScriptException();
return env.Null();
}
auto* baton = new ApplyBaton(db, Napi::Function());
int64_t decision = info[1].As<Napi::Number>().Int64Value();
if (decision != SQLITE_CHANGESET_OMIT
&& decision != SQLITE_CHANGESET_REPLACE
&& decision != SQLITE_CHANGESET_ABORT) {
delete baton;
Napi::TypeError::New(env,
"conflict decision must be CHANGESET_ABORT, CHANGESET_OMIT or CHANGESET_REPLACE"
).ThrowAsJavaScriptException();
return env.Null();
}
baton->decision = static_cast<int>(decision);
auto read_handler = [&](size_t index, bool is_conflict) -> bool {
if (info.Length() <= index || info[index].IsNull()
|| info[index].IsUndefined()) {
return true;
}
if (!info[index].IsFunction()) {
Napi::TypeError::New(env,
"changeset handlers must be functions or null"
).ThrowAsJavaScriptException();
return false;
}
if (is_conflict) {
baton->on_conflict.Reset(info[index].As<Napi::Function>(), 1);
}
else {
baton->on_filter.Reset(info[index].As<Napi::Function>(), 1);
}
return true;
};
if (!read_handler(2, true) || !read_handler(3, false)) {
delete baton;
return env.Null();
}
Napi::Function callback;
if (info.Length() > 4 && !info[4].IsUndefined() && !info[4].IsNull()) {
if (!info[4].IsFunction()) {
delete baton;
Napi::TypeError::New(env, "Argument 4 must be a function").ThrowAsJavaScriptException();
return env.Null();
}
callback = info[4].As<Napi::Function>();
}
if (info.Length() > 5 && info[5].IsBoolean()) {
baton->want_rebase = info[5].As<Napi::Boolean>().Value();
}
// The changeset is applied from a private copy: the apply runs later
// on a worker, and a JS-side mutation of the source buffer mid-apply
// would otherwise be undefined behaviour inside sqlite's parser.
baton->n = static_cast<int>(view.length);
baton->data = sqlite3_malloc64(view.length);
if (baton->data == NULL && view.length > 0) {
delete baton;
Napi::Error::New(env, "out of memory").ThrowAsJavaScriptException();
return env.Null();
}
if (view.length > 0) {
memcpy(baton->data, view.data, view.length);
}
baton->callback.Reset(callback, 1);
if (!baton->on_conflict.IsEmpty() || !baton->on_filter.IsEmpty()) {
// The round-trip channel for the JS conflict/filter forms. Unref'd
// so it never keeps the event loop alive; released once the apply
// completes. While it exists, js_apply_depth keeps
// MayBlockOnWorkerRoundTrip truthful (the apply holds the
// connection mutex for its whole run).
Napi::Function noop = Napi::Function::New(env,
[](const Napi::CallbackInfo& info) {
return info.Env().Undefined();
});
napi_value resource_name = Napi::String::New(env,
"sqlite3.Database.ApplyChangeset");
napi_threadsafe_function tsfn = NULL;
napi_status st = napi_create_threadsafe_function(env, noop, NULL,
resource_name, 0, 1, NULL, NULL, NULL, ApplyCallJs, &tsfn);
if (st != napi_ok || tsfn == NULL) {
delete baton;
Napi::Error::New(env,
"cannot create the changeset round-trip channel"
).ThrowAsJavaScriptException();
return env.Null();
}
napi_unref_threadsafe_function(env, tsfn);
baton->tsfn = tsfn;
db->js_apply_depth++;
}
db->Schedule(Work_BeginApplyChangeset, baton);
return info.This();
}
void Database::Work_BeginApplyChangeset(Baton* baton) {
assert(baton->db->IsOpen());
assert(baton->db->_handle);
baton->db->pending++;
auto env = baton->db->Env();
CREATE_WORK("sqlite3.Database.ApplyChangeset", Work_ApplyChangeset,
Work_AfterApplyChangeset);
}
void Database::Work_ApplyChangeset(napi_env e, void* data) {
auto* baton = static_cast<ApplyBaton*>(data);
int rc;
if (baton->want_rebase) {
// apply_v2 with an output rebase buffer: every OMIT/REPLACE
// decision the conflict handling made is recorded so the same
// resolutions can be replayed onto later changesets (see
// sqlite3.rebaseChangeset). With no conflicts sqlite sets
// *ppRebase to NULL.
int rebase_n = 0;
void* rebase = NULL;
rc = sqlite3changeset_apply_v2(
baton->db->_handle,
baton->n,
baton->data,
ApplyFilterTrampoline,
ApplyConflictTrampoline,
baton,
&rebase,
&rebase_n,
0);
baton->rebase_n = rebase_n;
baton->rebase = rebase;
}
else {
rc = sqlite3changeset_apply(
baton->db->_handle,
baton->n,
baton->data,
ApplyFilterTrampoline,
ApplyConflictTrampoline,
baton);
}
// A handler error overrides the raw sqlite code as the reported
// cause of the (rolled-back) apply.
if (baton->status != SQLITE_OK) {
rc = baton->status;
}
else if (rc != SQLITE_OK) {
baton->message = std::string(sqlite3_errmsg(baton->db->_handle));
}
baton->status = rc;
}
void Database::Work_AfterApplyChangeset(napi_env e, napi_status status, void* data) {
std::unique_ptr<ApplyBaton> baton(static_cast<ApplyBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* db = baton->db;
auto env = db->Env();
Napi::HandleScope scope(env);
if (baton->tsfn != NULL) {
napi_release_threadsafe_function(baton->tsfn, napi_tsfn_release);
baton->tsfn = NULL;
assert(db->js_apply_depth > 0);
db->js_apply_depth--;
}
db->pending--;
db->Process();
Napi::Function cb = baton->callback.Value();
if (!IS_FUNCTION(cb)) {
if (baton->status != SQLITE_OK) {
EXCEPTION(baton->message, baton->status, exception);
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(db->Value(), 2, info);
}
return;
}
if (baton->status != SQLITE_OK) {
EXCEPTION(baton->message, baton->status, exception);
Napi::Value argv[] = { exception };
TRY_CATCH_CALL(db->Value(), cb, 1, argv);
return;
}
if (baton->want_rebase) {
// No conflicts were omitted or replaced: sqlite produces an empty
// buffer. Resolve null rather than a zero-length Uint8Array —
// "nothing to rebase against" reads better at the call site.
if (baton->rebase_n == 0) {
if (baton->rebase != NULL) {
sqlite3_free(baton->rebase);
baton->rebase = NULL;
}
Napi::Value argv[] = { env.Null(), env.Null() };
TRY_CATCH_CALL(db->Value(), cb, 2, argv);
return;
}
// WrapOwnedBytes takes ownership of the sqlite3_malloc'd buffer
// (zero-copy external ArrayBuffer, freed by its finalizer);
// NULLing the field keeps the baton destructor from double-freeing.
Napi::Value rebase = WrapOwnedBytes(env, baton->rebase,
static_cast<size_t>(baton->rebase_n));
baton->rebase = NULL;
Napi::Value argv[] = { env.Null(), rebase };
TRY_CATCH_CALL(db->Value(), cb, 2, argv);
return;
}
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(db->Value(), cb, 1, argv);
}
// --- serialize / deserialize -----------------------------------------------------
// _serializeToBytes(dbName, [callback]). Exclusive: the snapshot must not
// interleave with other work on the connection.
Napi::Value Database::SerializeToBytes(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto* db = this;
REQUIRE_ARGUMENT_STRING(0, database);
OPTIONAL_ARGUMENT_FUNCTION(1, callback);
auto* baton = new SerializeBaton(db, callback, database.c_str());
db->Schedule(Work_BeginSerializeToBytes, baton, true);
return info.This();
}
void Database::Work_BeginSerializeToBytes(Baton* baton) {
assert(baton->db->exclusiveHeld);
assert(baton->db->IsOpen());
assert(baton->db->_handle);
assert(baton->db->pending == 0);
baton->db->pending++;
auto env = baton->db->Env();
CREATE_WORK("sqlite3.Database.SerializeToBytes", Work_SerializeToBytes,
Work_AfterSerializeToBytes);
}
void Database::Work_SerializeToBytes(napi_env e, void* data) {
auto* baton = static_cast<SerializeBaton*>(data);
// sqlite3_serialize copies every page through the pager, and the
// pager reads through the WAL, so the snapshot carries every
// committed transaction whether or not its frames have been
// checkpointed into the main file. The one thing it cannot carry is
// the WAL format itself: a WAL database's header (bytes 18/19 =
// 0x02) demands WAL recovery on open, and a deserialized copy has no
// -wal file, so sqlite3_deserialize fails it with SQLITE_CANTOPEN at
// first use. Flip the image's file-format versions to 0x01 (rollback
// journal) — the workaround the sqlite3_deserialize documentation
// itself names. Only the copy is touched: with no NOCOPY flag sqlite
// never reads this buffer again, and the live database's journal
// mode is unaffected.
sqlite3_int64 size = 0;
unsigned char* bytes = sqlite3_serialize(baton->db->_handle,
baton->database.c_str(), &size, 0);
if (bytes == NULL && size > 0) {
baton->status = SQLITE_NOMEM;
baton->message = "out of memory serializing the database";
}
else if (bytes == NULL) {
baton->status = SQLITE_ERROR;
baton->message = std::string(sqlite3_errmsg(baton->db->_handle));
}
else {
// A WAL-mode image carries header bytes 18/19 = 0x02, which demands
// WAL recovery on open. A deserialized copy has no -wal file, so
// sqlite3_deserialize rejects it with SQLITE_CANTOPEN at first use;
// its documentation names this exact workaround: flip the image's
// file-format versions to 0x01 (rollback journal). Only a copy is
// touched — with no NOCOPY flag sqlite never reads this buffer
// again, and the live database's journal mode is unaffected.
if (size >= 20 && bytes[18] == 0x02 && bytes[19] == 0x02) {
bytes[18] = 0x01;
bytes[19] = 0x01;
}
baton->data = bytes;
baton->size = size;
}
}
void Database::Work_AfterSerializeToBytes(napi_env e, napi_status status, void* data) {
std::unique_ptr<SerializeBaton> baton(static_cast<SerializeBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* db = baton->db;
auto env = db->Env();
Napi::HandleScope scope(env);
db->pending--;
// The exclusive serialize released the database.
db->exclusiveHeld = false;
// Never call Value() on a default-constructed (empty)
// FunctionReference: undefined behaviour that fatals in practice on
// the raw no-callback form (see Work_AfterCheckpoint in
// src/database.cc). IsEmpty() is a plain member check.
if (baton->callback.IsEmpty()) {
if (baton->status != SQLITE_OK) {
EXCEPTION(baton->message, baton->status, exception);
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(db->Value(), 2, info);
}
db->Process();
return;
}
Napi::Function cb = baton->callback.Value();
if (!IS_FUNCTION(cb)) {
if (baton->status != SQLITE_OK) {
EXCEPTION(baton->message, baton->status, exception);
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(db->Value(), 2, info);
}
db->Process();
return;
}
if (baton->status != SQLITE_OK) {
EXCEPTION(baton->message, baton->status, exception);
Napi::Value argv[] = { exception };
TRY_CATCH_CALL(db->Value(), cb, 1, argv);
}
else {
void* buffer = baton->data;
baton->data = NULL; // ownership moves into the Uint8Array
Napi::Value result = WrapOwnedBytes(env, buffer,
static_cast<size_t>(baton->size));
Napi::Value argv[] = { env.Null(), result };
TRY_CATCH_CALL(db->Value(), cb, 2, argv);
}
db->Process();
}
// _deserialize(bytes, flags, [callback]). Exclusive, pending == 0: the
// schema image is replaced outright, so no work may be in flight. `flags`
// carries SQLITE_DESERIALIZE_RESIZEABLE / READONLY (FREEONCLOSE is always
// set — the copy is ours). The bytes are copied into sqlite3_malloc'd
// memory first: a JS-owned buffer cannot be handed over (the GC may move
// or collect it), and detaching it would trade that for a
// use-after-free. The copy is documented API behaviour.
Napi::Value Database::Deserialize(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto* db = this;
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected at least 1 argument").ThrowAsJavaScriptException();
return env.Null();
}
BytesView view;
if (!GetBytesView(env, info[0], "deserialize a database", &view)) {
return env.Null();
}
if (info.Length() < 2 || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Argument 1 must be a deserialize flag word").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Function callback;
if (info.Length() > 2 && !info[2].IsUndefined()) {
if (!info[2].IsFunction()) {
Napi::TypeError::New(env, "Argument 2 must be a function").ThrowAsJavaScriptException();
return env.Null();
}
callback = info[2].As<Napi::Function>();
}
auto* baton = new DeserializeBaton(db, callback);
baton->n = static_cast<int>(view.length);
baton->flags = static_cast<unsigned int>(
info[1].As<Napi::Number>().Uint32Value())
& (SQLITE_DESERIALIZE_RESIZEABLE | SQLITE_DESERIALIZE_READONLY);
baton->data = sqlite3_malloc64(view.length);
if (baton->data == NULL && view.length > 0) {
delete baton;
Napi::Error::New(env, "out of memory").ThrowAsJavaScriptException();
return env.Null();
}
if (view.length > 0) {
memcpy(baton->data, view.data, view.length);
}
db->Schedule(Work_BeginDeserialize, baton, true);
return info.This();
}
void Database::Work_BeginDeserialize(Baton* baton) {
assert(baton->db->exclusiveHeld);
assert(baton->db->IsOpen());
assert(baton->db->_handle);
assert(baton->db->pending == 0);
baton->db->pending++;
auto env = baton->db->Env();
CREATE_WORK("sqlite3.Database.Deserialize", Work_Deserialize,
Work_AfterDeserialize);
}
void Database::Work_Deserialize(napi_env e, void* data) {
auto* baton = static_cast<DeserializeBaton*>(data);
auto* db = baton->db;
int rc = sqlite3_deserialize(db->_handle, "main",
static_cast<unsigned char*>(baton->data), baton->n, baton->n,
SQLITE_DESERIALIZE_FREEONCLOSE | baton->flags);
// On failure with FREEONCLOSE sqlite frees the buffer itself; either
// way ownership left the baton.
baton->data = NULL;
if (rc != SQLITE_OK) {
baton->status = rc;
baton->message = std::string(sqlite3_errmsg(db->_handle));
return;
}
// sqlite3_deserialize accepts any bytes; corruption surfaces at first
// use. Probe the schema now so a corrupt input reports
// SQLITE_NOTADB from the deserialize call itself instead of from an
// unrelated later query.
sqlite3_stmt* stmt = NULL;
rc = sqlite3_prepare_v2(db->_handle,
"SELECT count(*) FROM sqlite_schema", -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) rc = SQLITE_OK;
sqlite3_finalize(stmt);
}
if (rc != SQLITE_OK) {
baton->status = rc;
baton->message = std::string(sqlite3_errmsg(db->_handle));
}
}
void Database::Work_AfterDeserialize(napi_env e, napi_status status, void* data) {
std::unique_ptr<DeserializeBaton> baton(static_cast<DeserializeBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* db = baton->db;
auto env = db->Env();
Napi::HandleScope scope(env);
db->pending--;
// The exclusive deserialize released the database.
db->exclusiveHeld = false;
Napi::Function cb = baton->callback.Value();
if (baton->status != SQLITE_OK) {
EXCEPTION(baton->message, baton->status, exception);
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { exception };
TRY_CATCH_CALL(db->Value(), cb, 1, argv);
}
else {
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(db->Value(), 2, info);
}
}
else if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(db->Value(), cb, 1, argv);
}
db->Process();
}
namespace node_sqlite3 {
// --- Module-level changeset helpers ----------------------------------------------
Napi::Value InvertChangeset(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException();
return env.Null();
}
BytesView view;
if (!GetBytesView(env, info[0], "invert a changeset", &view)) {
return env.Null();
}
// sqlite3changeset_invert documents its input as assumed-valid; prove
// parseability first so garbage is an error, not undefined behaviour.
int rc = ValidateChangeset(static_cast<int>(view.length), view.data);
if (rc != SQLITE_OK) {
EXCEPTION("the changeset is not parseable", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
int n_out = 0;
void* p_out = NULL;
rc = sqlite3changeset_invert(static_cast<int>(view.length),
const_cast<void*>(view.data), &n_out, &p_out);
if (rc != SQLITE_OK) {
EXCEPTION("cannot invert the changeset", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
return WrapOwnedBytes(env, p_out, static_cast<size_t>(n_out));
}
Napi::Value ConcatChangeset(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 2) {
Napi::TypeError::New(env, "Expected 2 arguments").ThrowAsJavaScriptException();
return env.Null();
}
BytesView a, b;
if (!GetBytesView(env, info[0], "concatenate a changeset", &a)) {
return env.Null();
}
if (!GetBytesView(env, info[1], "concatenate a changeset", &b)) {
return env.Null();
}
int rc = ValidateChangeset(static_cast<int>(a.length), a.data);
if (rc == SQLITE_OK) {
rc = ValidateChangeset(static_cast<int>(b.length), b.data);
}
if (rc != SQLITE_OK) {
EXCEPTION("one of the changesets is not parseable", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
int n_out = 0;
void* p_out = NULL;
rc = sqlite3changeset_concat(
static_cast<int>(a.length), const_cast<void*>(a.data),
static_cast<int>(b.length), const_cast<void*>(b.data),
&n_out, &p_out);
if (rc != SQLITE_OK) {
EXCEPTION("cannot concatenate the changesets", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
return WrapOwnedBytes(env, p_out, static_cast<size_t>(n_out));
}
// rebaseChangeset(changeset, rebase): rewrites a changeset against the
// conflict resolutions a previous applyChangeset(..., { rebase: true })
// harvested — sqlite3_rebaser_configure + sqlite3rebaser_apply. Pure
// memory function: no connection, no mutex, no worker. This is the
// client-server sync primitive no other JS driver exposes (rusqlite is
// the only binding anywhere with it).
Napi::Value RebaseChangeset(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 2) {
Napi::TypeError::New(env, "Expected 2 arguments").ThrowAsJavaScriptException();
return env.Null();
}
BytesView cs, rb;
if (!GetBytesView(env, info[0], "rebase a changeset", &cs)) {
return env.Null();
}
if (!GetBytesView(env, info[1], "rebase a changeset", &rb)) {
return env.Null();
}
int rc = ValidateChangeset(static_cast<int>(cs.length), cs.data);
if (rc != SQLITE_OK) {
EXCEPTION("the changeset is not parseable", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
sqlite3_rebaser* rebaser = NULL;
rc = sqlite3rebaser_create(&rebaser);
if (rc != SQLITE_OK) {
EXCEPTION("cannot create a changeset rebaser", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
rc = sqlite3rebaser_configure(rebaser,
static_cast<int>(rb.length), const_cast<void*>(rb.data));
if (rc != SQLITE_OK) {
sqlite3rebaser_delete(rebaser);
EXCEPTION("the rebase buffer is not parseable", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
int n_out = 0;
void* p_out = NULL;
rc = sqlite3rebaser_rebase(rebaser,
static_cast<int>(cs.length), const_cast<void*>(cs.data),
&n_out, &p_out);
sqlite3rebaser_delete(rebaser);
if (rc != SQLITE_OK) {
if (p_out != NULL) sqlite3_free(p_out);
EXCEPTION("cannot rebase the changeset", rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
return WrapOwnedBytes(env, p_out, static_cast<size_t>(n_out));
}
// --- ChangesetIter ----------------------------------------------------------------
namespace {
// The lazily created constructor for the iterator class; instances are
// not part of the public surface (the type is declared as an iterable in
// lib/native.d.ts).
} // namespace
Napi::Object ChangesetIter::Init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
auto napi_default_method = static_cast<napi_property_attributes>(
napi_writable | napi_configurable);
Napi::Function t = DefineClass(env, "ChangesetIter", {
InstanceMethod("next", &ChangesetIter::Next, napi_default_method),
});
// [Symbol.iterator] is defined per instance (in the constructor):
// DefineClass properties must be ClassPropertyDescriptor values,
// which have no symbol-keyed form in node-addon-api.
env.GetInstanceData<Database::AddonData>()->changeset_iter_ctor =
Napi::Persistent(t);
return exports;
}
// { Uint8Array data }
ChangesetIter::ChangesetIter(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<ChangesetIter>(info) {
auto env = info.Env();
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException();
return;
}
BytesView view;
if (!GetBytesView(env, info[0], "iterate a changeset", &view)) {
// GetBytesView already threw; keep the object inert.
return;
}
// The iterator parses lazily, so it walks a private copy: a JS-side
// mutation of the source buffer mid-iteration would otherwise be
// undefined behaviour.
bytes.assign(static_cast<const char*>(view.data), view.length);
int rc = sqlite3changeset_start(&_iter,
static_cast<int>(bytes.size()), bytes.data());
if (rc != SQLITE_OK) {
_iter = NULL;
Napi::Error::New(env, "the changeset is not parseable")
.ThrowAsJavaScriptException();
return;
}
// for..of drives [Symbol.iterator]() first; the iterator is its own
// iterable.
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value(
Napi::Symbol::WellKnown(env, "iterator"),
Napi::Function::New(env, [](const Napi::CallbackInfo& info) {
return info.This();
}), napi_default));
}
Napi::Value ChangesetIter::Next(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (_iter == NULL) {
Napi::Object done = Napi::Object::New(env);
done.Set("done", Napi::Boolean::New(env, true));
done.Set("value", env.Null());
return done;
}
int rc = sqlite3changeset_next(_iter);
if (rc == SQLITE_DONE) {
sqlite3changeset_finalize(_iter);
_iter = NULL;
Napi::Object done = Napi::Object::New(env);
done.Set("done", Napi::Boolean::New(env, true));
done.Set("value", env.Null());
return done;
}
if (rc != SQLITE_ROW) {
sqlite3changeset_finalize(_iter);
_iter = NULL;
EXCEPTION("the changeset is corrupt",
rc == SQLITE_OK ? SQLITE_CORRUPT : rc, exception);
exception.As<Napi::Error>().ThrowAsJavaScriptException();
return env.Null();
}
const char* table = NULL;
int n_col = 0, op = 0, indirect = 0;
unsigned char* pk = NULL;
sqlite3changeset_op(_iter, &table, &n_col, &op, &indirect);
sqlite3changeset_pk(_iter, &pk, &n_col);
// No database is involved, so there is no integer mode to honour;
// mixed keeps every value exact (number when safe, BigInt otherwise).
const int mixed = Database::INTEGER_MIXED;
Napi::Object value = Napi::Object::New(env);
value.Set("op", Napi::String::New(env, ChangesetOpString(op)));
value.Set("table", Napi::String::New(env, table != NULL ? table : ""));
value.Set("indirect", Napi::Boolean::New(env, indirect != 0));
Napi::Array pk_arr = Napi::Array::New(env, n_col);
for (int i = 0; i < n_col; i++) {
pk_arr.Set(i, Napi::Boolean::New(env,
pk != NULL && pk[i] != 0));
}
value.Set("primaryKey", pk_arr);
// UPDATE changesets carry old.* only for primary-key and modified
// columns; the untouched positions stay null in the array, which is
// exactly how sqlite encodes them.
auto fill = [&](const char* name, bool old) -> bool {
std::vector<Cell> cells;
cells.resize(n_col);
int filled = 0;
for (int i = 0; i < n_col; i++) {
sqlite3_value* v = NULL;
int grc = old
? sqlite3changeset_old(_iter, i, &v)
: sqlite3changeset_new(_iter, i, &v);
if (grc == SQLITE_OK && v != NULL) {
ValueToCell(&cells[i], v);
filled++;
}
}
if (filled == 0) return true;
value.Set(name, CellsToArray(env, cells, mixed,
std::string("a changeset ") + name + " value"));
return !env.IsExceptionPending();
};
if (op == SQLITE_UPDATE || op == SQLITE_DELETE) {
if (!fill("oldRow", true)) return env.Null();
}
if (op == SQLITE_INSERT || op == SQLITE_UPDATE) {
if (!fill("newRow", false)) return env.Null();
}
Napi::Object result = Napi::Object::New(env);
result.Set("done", Napi::Boolean::New(env, false));
result.Set("value", value);
return result;
}
Napi::Value IterateChangeset(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Function ctor = env.GetInstanceData<Database::AddonData>()
->changeset_iter_ctor.Value();
// The constructor throws (a pending TypeError/Error) on a bad input;
// propagate it.
Napi::Object iter = ctor.New({ info[0] });
if (env.IsExceptionPending()) return env.Null();
return iter;
}
} // namespace node_sqlite3