// 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
