#include <cmath>
#include <cstring>
#include <cstdint>
#include <limits>
#include <napi.h>
#include <uv.h>

#include "macros.h"
#include "convert.h"
#include "database.h"
#include "statement.h"

using namespace node_sqlite3;

namespace {

// "parameter 3" / "parameter $name" for bind error messages.
template <class T>
std::string DescribeBindPosition(T pos) {
    if constexpr (std::is_integral_v<T>) {
        return "parameter " + std::to_string(static_cast<long long>(pos));
    } else {
        return std::string("parameter ") + pos;
    }
}

// Takes over a pending JS exception (e.g. the RangeError thrown by an
// integer-mode conversion) and returns it as a value suitable for an
// error-callback argument.
Napi::Value TakePendingError(Napi::Env env) {
    napi_value pending = NULL;
    napi_status st = napi_get_and_clear_last_exception(env, &pending);
    if (st != napi_ok || pending == NULL) {
        return Napi::Error::New(env, "integer value out of range").Value();
    }
    return Napi::Value(env, pending);
}

// Recognises the trailing `{ rowMode: 'array' }` options bag accepted by
// the synchronous read paths (GetSync/AllSync). Only the exact key
// `rowMode` is treated as an option, and the guard must mirror the bind
// path's named-object guard (arrays and binary views bind positionally,
// Date/RegExp are values): a plain object that owns `rowMode` could never
// have been a legal bind argument — named bind keys carry a sigil
// (`:name`/`@name`/`$name`) or are positional numbers — so recognising it
// costs nothing for any call that was legal before this option existed.
//
// Returns true when the value is the options bag (the caller must then
// exclude it from the bind arguments). Throws a TypeError for a rowMode
// value that is not 'object' or 'array'. A property read can throw (a
// Proxy trap); that surfaces as a pending exception and false.
bool ParseSyncReadOptions(const Napi::Value& value, int* row_mode) {
    if (!value.IsObject() || value.IsArray() || value.IsBuffer()
            || value.IsTypedArray() || value.IsDataView()
            || value.IsArrayBuffer() || value.IsDate()
            || OtherInstanceOf(value.As<Napi::Object>(), "RegExp")) {
        return false;
    }
    auto env = value.Env();
    Napi::Value mode = value.As<Napi::Object>().Get("rowMode");
    if (env.IsExceptionPending()) return false;
    if (mode.IsUndefined()) return false;
    if (!mode.IsString()) {
        Napi::TypeError::New(env,
            "rowMode must be 'object', 'array' or 'pluck'")
            .ThrowAsJavaScriptException();
        return true;
    }
    std::string requested = mode.As<Napi::String>().Utf8Value();
    if (requested == "array") {
        *row_mode = Statement::SYNC_ROW_ARRAY;
    } else if (requested == "object") {
        *row_mode = Statement::SYNC_ROW_OBJECT;
    } else if (requested == "pluck") {
        *row_mode = Statement::SYNC_ROW_PLUCK;
    } else {
        Napi::TypeError::New(env,
            "rowMode must be 'object', 'array' or 'pluck'")
            .ThrowAsJavaScriptException();
    }
    return true;
}

namespace {

// Pops the trailing callback (if any) and then a trailing
// `{ rowMode: 'object' | 'array' }` options bag off an asynchronous read
// call (Get/All/Each/Fetch). The bag sits between the bind parameters and
// the callback; a plain object owning `rowMode` could never have been a
// legal bind argument (named bind keys carry a sigil) — the same
// discriminator ParseSyncReadOptions uses. Returns the reduced argument
// count and hands the popped callback back for Bind's preset slot.
// Throws (pending exception) for an invalid rowMode value.
int PopAsyncReadOptions(const Napi::CallbackInfo& info,
        Napi::Function* callback, int* row_mode) {
    int end = info.Length();
    if (end > 0 && info[end - 1].IsFunction()) {
        *callback = info[end - 1].As<Napi::Function>();
        end--;
    }
    if (end > 0 && ParseSyncReadOptions(info[end - 1], row_mode)) end--;
    return end;
}

} // namespace

} // namespace

Napi::Object Statement::Init(Napi::Env env, Napi::Object exports) {
    Napi::HandleScope scope(env);

    // declare napi_default_method here as it is only available in Node v14.12.0+
    auto napi_default_method = static_cast<napi_property_attributes>(napi_writable | napi_configurable);

    auto t = DefineClass(env, "Statement", {
      InstanceMethod("bind", &Statement::Bind, napi_default_method),
      InstanceMethod("get", &Statement::Get, napi_default_method),
      InstanceMethod("run", &Statement::Run, napi_default_method),
      InstanceMethod("all", &Statement::All, napi_default_method),
      InstanceMethod("each", &Statement::Each, napi_default_method),
      InstanceMethod("reset", &Statement::Reset, napi_default_method),
      InstanceMethod("fetch", &Statement::Fetch, napi_default_method),
      InstanceMethod("finalize", &Statement::Finalize_, napi_default_method),
      InstanceMethod("getSync", &Statement::GetSync, napi_default_method),
      InstanceMethod("runSync", &Statement::RunSync, napi_default_method),
      InstanceMethod("allSync", &Statement::AllSync, napi_default_method),
      // Non-enumerable, like the prototype methods: enumerating the
      // prototype (sqlite3.verbose() does) must not invoke the getters
      // with the prototype as receiver.
      InstanceAccessor("lastID", &Statement::GetLastID, nullptr,
          static_cast<napi_property_attributes>(napi_configurable)),
      InstanceAccessor("lastIDBigInt", &Statement::GetLastIDBigInt, nullptr,
          static_cast<napi_property_attributes>(napi_configurable)),
      InstanceAccessor("changes", &Statement::GetChanges, nullptr,
          static_cast<napi_property_attributes>(napi_configurable)),
      InstanceAccessor("finalized", &Statement::FinalizedGetter, nullptr),
      // Introspection (Deliverable 07): snapshots taken at prepare time,
      // so the getters never race a worker.
      InstanceAccessor("readonly", &Statement::ReadonlyGetter, nullptr),
      InstanceAccessor("parameterCount", &Statement::ParameterCountGetter,
          nullptr),
      InstanceAccessor("parameterNames", &Statement::ParameterNamesGetter,
          nullptr),
      InstanceAccessor("columns", &Statement::ColumnsGetter, nullptr),
      InstanceMethod("status", &Statement::Status, napi_default_method),
      // SQL text accessors (Phase 6): expanded SQL with the last bound
      // values substituted, and normalized SQL with literals folded to `?`
      // for query-metric dashboards.
      InstanceAccessor("expandedSQL", &Statement::ExpandedSQLGetter, nullptr,
          static_cast<napi_property_attributes>(napi_configurable)),
      InstanceAccessor("normalizedSQL", &Statement::NormalizedSQLGetter,
          nullptr, static_cast<napi_property_attributes>(napi_configurable)),
      InstanceMethod("_setIntegerMode", &Statement::SetIntegerMode,
          napi_default_method),
    });

    // Per-env (see Database::AddonData): a worker thread is its own napi
    // env, so a file-static constructor here would be shared across
    // environments — and destroyed after the env at process exit.
    env.GetInstanceData<Database::AddonData>()->statement_ctor =
        Napi::Persistent(t);
    exports.Set("Statement", t);
    return exports;
}

// A Napi InstanceOf for Javascript Objects "Date" and "RegExp"
void Statement::Process() {
    if (finalized && !queue.empty()) {
        return CleanQueue();
    }

    while (prepared && !locked && !queue.empty()) {
        auto call = std::unique_ptr<Call>(queue.front());
        queue.pop();

        call->callback(call->baton);
    }
}

void Statement::Schedule(Work_Callback callback, Baton* baton) {
    if (finalized) {
        queue.emplace(new Call(callback, baton));
        CleanQueue();
    }
    else if (!prepared || locked) {
        queue.emplace(new Call(callback, baton));
    }
    else {
        callback(baton);
    }
}

template <class T> void Statement::Error(T* baton) {
    Statement* stmt = baton->stmt;

    auto env = stmt->Env();
    Napi::HandleScope scope(env);

    // Fail hard on logic errors.
    assert(stmt->status != 0);
    EXCEPTION(stmt->message, stmt->status, exception);
    // A user-defined function that threw during the step kept its JS error
    // on the database as the pending cause of exactly this failure.
    stmt->db->AttachPendingJsError(exception_obj);

    Napi::Function cb = baton->callback.Value();

    if (IS_FUNCTION(cb)) {
        Napi::Value argv[] = { exception };
        TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
    }
    else {
        Napi::Value argv[] = { Napi::String::New(env, "error"), exception };
        EMIT_EVENT(stmt->Value(), 2, argv);
    }
}

// { Database db, String sql, [Function callback], [Boolean sync] }
// The trailing boolean is used by Database#prepareSync: it prepares the
// statement inline instead of through the worker pool. The database must
// be fully idle for that to be safe.
Statement::Statement(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Statement>(info) {
    auto env = info.Env();
    int length = info.Length();

    if (length <= 0 || !Database::HasInstance(info[0])) {
        Napi::TypeError::New(env, "Database object expected").ThrowAsJavaScriptException();
        return;
    }
    else if (length <= 1 || !info[1].IsString()) {
        Napi::TypeError::New(env, "SQL query expected").ThrowAsJavaScriptException();
        return;
    }
    else if (length > 2 && !info[2].IsUndefined() && !info[2].IsFunction()) {
        Napi::TypeError::New(env, "Callback expected").ThrowAsJavaScriptException();
        return;
    }

    this->db = Napi::ObjectWrap<Database>::Unwrap(info[0].As<Napi::Object>());
    this->db->Ref();

    auto sql = info[1].As<Napi::String>();

    info.This().As<Napi::Object>().DefineProperty(Napi::PropertyDescriptor::Value("sql", sql, napi_default));

    std::string sql_str = sql.Utf8Value();
    // Preserve the historical NUL-truncation semantics of c_str().
    sql_str.resize(std::strlen(sql_str.c_str()));

    Statement* stmt = this;

    if (length > 3 && info[3].IsBoolean() && info[3].As<Napi::Boolean>().Value()) {
        // Synchronous prepare. The idle gate mirrors IdleForInline():
        // only DbState::Open (not Opening/Closing) with nothing in
        // flight or queued.
        if (db->db_state != Database::DbState::Open || db->pending > 0
                || !db->queue.empty()) {
            Napi::Error::New(env,
                "database is busy: sync methods require a fully idle database"
            ).ThrowAsJavaScriptException();
            return;
        }
        // A JavaScript progress callback fires inside prepare and step
        // alike (sqlite invokes the handler in both), and a round trip
        // from this thread would wait for itself. Unlike functions there
        // is no per-invocation error channel, so refuse up front — the
        // same contract as collations. The SharedArrayBuffer token form
        // has no such restriction.
        if (db->js_progress != NULL) {
            Napi::Error::New(env,
                "sync methods cannot be used while a JavaScript progress "
                "callback is registered on this connection: the callback "
                "would have to run JS on the thread that is blocked inside "
                "SQLite (a deadlock). Use the asynchronous API, or a "
                "cancellation token instead"
            ).ThrowAsJavaScriptException();
            return;
        }

        sqlite3_mutex* mtx = sqlite3_db_mutex(db->_handle);
        sqlite3_mutex_enter(mtx);
        {
            // Same guard as the *Sync methods: while this thread drives
            // sqlite, user callbacks reached from it must refuse their
            // round trip instead of deadlocking on this thread.
            Database::SyncSqliteGuard sync_guard(db);
            status = sqlite3_prepare_v2(db->_handle, sql_str.c_str(),
                sql_str.size(), &_handle, NULL);
            if (status != SQLITE_OK) {
                message = std::string(sqlite3_errmsg(db->_handle));
                error_offset = sqlite3_error_offset(db->_handle);
                _handle = NULL;
            }
            else {
                // Sync prepare: this is the JS thread, so take and
                // publish in one go.
                SnapshotMetadata();
                PublishMetadata();
            }
        }
        sqlite3_mutex_leave(mtx);

        if (status != SQLITE_OK) {
            ThrowStatementError(env);
            return;
        }
        prepared = true;
        locked = false; // no STATEMENT_END will run for this path
        return;
    }

    auto* baton = new PrepareBaton(this->db, info[2].As<Napi::Function>(), stmt);
    baton->sql = std::move(sql_str);
    this->db->Schedule(Work_BeginPrepare, baton);
}

void Statement::Work_BeginPrepare(Database::Baton* baton) {
    assert(baton->db->open);
    baton->db->pending++;

    auto env = baton->db->Env();
    CREATE_WORK("sqlite3.Statement.Prepare", Work_Prepare, Work_AfterPrepare);
}

void Statement::Work_Prepare(napi_env e, void* data) {
    STATEMENT_INIT(PrepareBaton);

    // In case preparing fails, we use a mutex to make sure we get the associated
    // error message.
    STATEMENT_MUTEX(mtx);
    sqlite3_mutex_enter(mtx);

    stmt->status = sqlite3_prepare_v2(
        baton->db->_handle,
        baton->sql.c_str(),
        baton->sql.size(),
        &stmt->_handle,
        NULL
    );

    if (stmt->status != SQLITE_OK) {
        stmt->message = std::string(sqlite3_errmsg(baton->db->_handle));
        // Byte offset of the failing token, read while the mutex is still
        // held and the error is current; -1 when this error has no
        // position (sqlite only produces offsets for prepare failures).
        stmt->error_offset = sqlite3_error_offset(baton->db->_handle);
        stmt->_handle = NULL;
    }
    else {
        stmt->SnapshotMetadata();
    }

    sqlite3_mutex_leave(mtx);
}

void Statement::EndCall() {
    assert(locked);
    assert(db->pending);
    locked = false;
    db->pending--;
    Process();
    db->Process();
}

void Statement::Work_AfterPrepare(napi_env e, napi_status status, void* data) {
    std::unique_ptr<PrepareBaton> baton(static_cast<PrepareBaton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    if (stmt->status != SQLITE_OK) {
        // Who hears about a failed prepare? Normally the prepare's own
        // callback, which every callback-style entry point supplies.
        //
        // Calls already queued behind the prepare settle no matter what:
        // they are what the caller is actually awaiting, and dropping them
        // in silence hangs exactly the abort-during-prepare window this
        // path exists for (sqlite3_interrupt() aborts a prepare just as
        // readily as a step). The prepare callback runs before the queue
        // so the error reaches its call-site context first — the JS layer
        // shares a once-token between the two slots, so the queued
        // delivery of the same error is a no-op there — and when there is
        // no callback the queue is settled before the failure is reported
        // on the statement's 'error' event, the documented surface for a
        // prepare given no callback of its own.
        EXCEPTION(stmt->message, stmt->status, exception);
        // Failed-prepare token position (sqlite3_error_offset), captured
        // on the worker while the error was current. Consumed here so no
        // later error inherits it.
        if (stmt->error_offset >= 0) {
            exception_obj.Set("offset",
                Napi::Number::New(env, stmt->error_offset));
            stmt->error_offset = -1;
        }
        // A user-defined function that threw during the step kept its JS
        // error on the database as the pending cause of exactly this
        // failure (Error() did this for the callback-only path).
        stmt->db->AttachPendingJsError(exception_obj);
        Napi::Function prepare_cb = baton->callback.Value();
        if (IS_FUNCTION(prepare_cb)) {
            Napi::Value argv[] = { exception };
            TRY_CATCH_CALL(stmt->Value(), prepare_cb, 1, argv);
            stmt->FailQueue(exception, false);
        }
        else if (!stmt->queue.empty()) {
            stmt->FailQueue(exception, false);
            Napi::Value info[] = {
                Napi::String::New(env, "error"), exception
            };
            EMIT_EVENT(stmt->Value(), 2, info);
        }
        else {
            Napi::Value info[] = {
                Napi::String::New(env, "error"), exception
            };
            EMIT_EVENT(stmt->Value(), 2, info);
        }
        stmt->Finalize_();
    }
    else {
        // Publish before `prepared`: both are JS-thread state, and this
        // is the first point at which JS can observe either.
        stmt->PublishMetadata();
        stmt->prepared = true;
        if (!baton->callback.IsEmpty() && baton->callback.Value().IsFunction()) {
            Napi::Function cb = baton->callback.Value();
            Napi::Value argv[] = { env.Null() };
            TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
        }
    }
}

template <class T> std::unique_ptr<Values::Field>
                   Statement::BindParameter(const Napi::Value source, T pos) {
    // The full dispatch lives in ConvertToField (src/convert.cc) since
    // Deliverable 06: statement binding and user-function results share one
    // converter so their type behaviour cannot drift. Only the position
    // naming and the position itself (index vs name) are bind-specific.
    if constexpr (std::is_integral_v<T>) {
        return ConvertToField(source, DescribeBindPosition(pos),
            FieldPos{ static_cast<int>(pos), "" });
    } else {
        return ConvertToField(source, DescribeBindPosition(pos),
            FieldPos{ 0, pos });
    }
}

template <class T> T* Statement::Bind(const Napi::CallbackInfo& info, int start,
        int last, Napi::Function preset_callback) {
    auto env = info.Env();
    Napi::HandleScope scope(env);

    if (last < 0) last = info.Length();
    Napi::Function callback = preset_callback;
    if (callback.IsEmpty() && last > start && info[last - 1].IsFunction()) {
        callback = info[last - 1].As<Napi::Function>();
        last--;
    }

    auto *baton = new T(this, callback);
    baton->bind_supplied = (start < last);

    if (start < last) {
        if (!ParseBindArguments(info, start, last, &baton->parameters)) {
            // BindParameter threw a TypeError/RangeError, or the shape was
            // malformed.
            delete baton;
            return NULL;
        }
    }

    return baton;
}

// The bind-argument shapes shared by every entry point: one array, N
// positional values, or one named-parameter object. Appends converted
// fields to `parameters` in bind order. Returns false with a pending
// TypeError/RangeError for an unsupported value or malformed shape —
// nothing is ever silently skipped or coerced.
//
// A member (not a free function) so it shares Statement::BindParameter and
// therefore the one JS->SQLite converter in src/convert.cc. The
// synchronous fast paths call this directly: they have no Baton to fill —
// a Baton exists for the async paths' queueing and cross-thread lifetimes,
// none of which a synchronous call has.
bool Statement::ParseBindArguments(const Napi::CallbackInfo& info, int start,
        int last, Parameters* parameters) {
    auto env = info.Env();

    if (info[start].IsArray()) {
        auto array = info[start].As<Napi::Array>();
        int length = array.Length();
        parameters->reserve(length);
        // Note: bind parameters start with 1.
        for (int i = 0, pos = 1; i < length; i++, pos++) {
            auto field = BindParameter((array).Get(i), i + 1);
            if (field == nullptr) {
                return false;
            }
            parameters->push_back(std::move(field));
        }
    }
    else if (!IsNamedParameterMap(info[start])) {
        // Parameters directly in array.
        // Note: bind parameters start with 1.
        parameters->reserve(last - start);
        for (int i = start, pos = 1; i < last; i++, pos++) {
            auto field = BindParameter(info[i], pos);
            if (field == nullptr) {
                return false;
            }
            parameters->push_back(std::move(field));
        }
    }
    else if (info[start].IsObject()) {
        auto object = info[start].As<Napi::Object>();
        auto array = object.GetPropertyNames();
        if (env.IsExceptionPending()) return false;
        int length = array.Length();
        parameters->reserve(length);
        // Reused across keys; see BindArgumentsDirect.
        std::string param_name;
        for (int i = 0; i < length; i++) {
            Napi::Value name = (array).Get(i);
            NamedKey key;
            if (!ResolveNamedKey(env, name, &key, &param_name)) return false;

            auto field = key.positional
                ? BindParameter((object).Get(name), key.index)
                : BindParameter((object).Get(name), key.name);
            if (field == nullptr) {
                return false;
            }
            parameters->push_back(std::move(field));
        }
    }
    else {
        return false;
    }

    return true;
}

bool Statement::Bind(Parameters&& parameters, bool supplied) {
    if (!supplied && parameters.empty()) {
        // A call with no bind argument re-steps the statement with its
        // previous bindings. Keep bound_payloads alive: the earlier
        // SQLITE_STATIC payloads are still referenced.
        return true;
    }

    sqlite3_reset(_handle);
    sqlite3_clear_bindings(_handle);

    // Hold the previous payloads until every parameter has been rebound.
    Parameters stale;
    stale.swap(bound_payloads);

    // Parameter count must match exactly. Too few used to silently bind
    // NULL; too many were silently ignored (or hit SQLITE_RANGE late in
    // the bind loop with an unhelpful message). Note that for ?N-style
    // SQL the count is the largest index, which is what the array form
    // supplies.
    int expected = sqlite3_bind_parameter_count(_handle);

    // Historical "accidental undefined" call shape: a parameter list made
    // up entirely of `undefined` against a statement with no parameters is
    // ignored, so generic wrappers forwarding an absent value keep working
    // (pinned by test/prepare.test.js). `undefined` against a statement
    // that does take parameters still binds NULL below.
    if (expected == 0 && !parameters.empty()) {
        bool all_undefined = true;
        for (auto& f : parameters) {
            if (!f->from_undefined) { all_undefined = false; break; }
        }
        if (all_undefined) return true;
    }

    if (static_cast<int>(parameters.size()) != expected) {
        // Bindings were cleared above, so no stale SQLITE_STATIC pointer
        // can dangle into the payloads we are about to release.
        status = SQLITE_RANGE;
        message = "supplied " + std::to_string(parameters.size()) +
            " parameter(s) but the statement takes " + std::to_string(expected);
        return false;
    }

    for (auto& field : parameters) {
        if (field == nullptr) {
            // Unreachable by construction: BindParameter either returns a
            // field or throws. Kept as a guard rather than an assert
            // because NDEBUG builds drop those.
            status = SQLITE_MISUSE;
            message = "internal error: unclassified bind parameter";
            return false;
        }

        unsigned int pos;
        if (field->index > 0) {
            pos = field->index;
        }
        else {
            pos = sqlite3_bind_parameter_index(_handle, field->name.c_str());
            if (pos == 0) {
                // The named parameter does not exist in the SQL — almost
                // always a typo'd key. Previously surfaced as a generic
                // SQLITE_RANGE from sqlite3_bind_* (or was silently
                // ignored when the value was skipped). Clear the partial
                // bindings from this call, like the bind-failure path.
                sqlite3_clear_bindings(_handle);
                bound_payloads.clear();
                status = SQLITE_RANGE;
                message = "unknown named parameter \"" + field->name + "\"";
                return false;
            }
        }

        switch (field->type) {
            case SQLITE_INTEGER: {
                status = sqlite3_bind_int64(_handle, pos,
                    (static_cast<Values::Integer*>(field.get()))->value);
            } break;
            case SQLITE_FLOAT: {
                status = sqlite3_bind_double(_handle, pos,
                    (static_cast<Values::Float*>(field.get()))->value);
            } break;
            case SQLITE_TEXT: {
                // SQLITE_STATIC is safe: the payload is moved into
                // bound_payloads below and stays alive until rebind/finalize.
                auto* f = static_cast<Values::Text*>(field.get());
                status = sqlite3_bind_text(_handle, pos,
                    f->value.c_str(), f->value.size(), SQLITE_STATIC);
                if (status == SQLITE_OK) bound_payloads.emplace_back(std::move(field));
            } break;
            case SQLITE_BLOB: {
                auto* f = static_cast<Values::Blob*>(field.get());
                status = sqlite3_bind_blob(_handle, pos,
                    f->value, static_cast<int>(f->length), SQLITE_STATIC);
                if (status == SQLITE_OK) bound_payloads.emplace_back(std::move(field));
            } break;
            case SQLITE_NULL: {
                status = sqlite3_bind_null(_handle, pos);
            } break;
        }

        if (status != SQLITE_OK) {
            // Clear every binding so no stale SQLITE_STATIC pointer can
            // dangle into the payloads we are about to release.
            sqlite3_clear_bindings(_handle);
            bound_payloads.clear();
            message = std::string(sqlite3_errmsg(db->_handle));
            return false;
        }
    }

    return true;
}

bool Statement::BindArgumentsDirect(const Napi::CallbackInfo& info,
        int start, int last, bool supplied) {
    auto env = info.Env();

    if (!supplied) {
        // A call with no bind argument re-steps the statement with its
        // previous bindings, so nothing here may touch them — including
        // bound_payloads, whose SQLITE_STATIC pointers are still live.
        return true;
    }

    // Resolve the argument shape and the values it carries, without
    // converting anything yet: the arity check has to run before the first
    // bind so a mismatched call cannot half-bind the statement.
    enum Shape { POSITIONAL, ARRAY, NAMED } shape = POSITIONAL;
    Napi::Array array;
    Napi::Object object;
    Napi::Array keys;
    int count = 0;

    if (start < last && info[start].IsArray()) {
        shape = ARRAY;
        array = info[start].As<Napi::Array>();
        count = static_cast<int>(array.Length());
    }
    else if (start < last && IsNamedParameterMap(info[start])) {
        shape = NAMED;
        object = info[start].As<Napi::Object>();
        keys = object.GetPropertyNames();
        if (env.IsExceptionPending()) return false;
        count = static_cast<int>(keys.Length());
    }
    else {
        count = last - start;
    }

    // Order matches the Field path: the statement is reset and its
    // bindings cleared before the checks, so a rejected call leaves no
    // stale binding behind either way.
    sqlite3_reset(_handle);
    sqlite3_clear_bindings(_handle);
    bound_payloads.clear();

    const int expected = sqlite3_bind_parameter_count(_handle);

    // Historical "accidental undefined" call shape: a parameter list made
    // up entirely of `undefined` against a statement with no parameters is
    // ignored, so generic wrappers forwarding an absent value keep
    // working. Only reachable when the statement takes no parameters, so
    // the extra pass costs nothing on the hot path.
    if (expected == 0 && count > 0) {
        bool all_undefined = true;
        for (int i = 0; i < count && all_undefined; i++) {
            Napi::Value value = shape == ARRAY ? array.Get(i)
                : shape == NAMED ? object.Get(keys.Get(i))
                : info[start + i];
            if (env.IsExceptionPending()) return false;
            if (!value.IsUndefined()) all_undefined = false;
        }
        if (all_undefined) return true;
    }

    if (count != expected) {
        status = SQLITE_RANGE;
        message = "supplied " + std::to_string(count) +
            " parameter(s) but the statement takes " +
            std::to_string(expected);
        return false;
    }

    // Reused across the keys of this call: a short parameter name fits in
    // the string's own storage, so the loop allocates nothing.
    std::string param_name;

    for (int i = 0; i < count; i++) {
        Napi::Value value;
        int pos = i + 1;
        // Only set for a genuinely named parameter; the subject is a
        // borrowed view either way and formats nothing unless it throws.
        bool named = false;

        if (shape == NAMED) {
            Napi::Value name = keys.Get(i);
            if (env.IsExceptionPending()) return false;
            NamedKey key;
            if (!ResolveNamedKey(env, name, &key, &param_name)) return false;
            if (key.positional) {
                pos = key.index;
            }
            else {
                named = true;
                pos = sqlite3_bind_parameter_index(_handle, key.name);
                if (pos == 0) {
                    // Almost always a typo'd key. Clear the partial
                    // bindings, like the bind-failure path below.
                    sqlite3_clear_bindings(_handle);
                    status = SQLITE_RANGE;
                    message = "unknown named parameter \"" + param_name
                        + "\"";
                    return false;
                }
            }
            value = object.Get(name);
        }
        else {
            value = shape == ARRAY ? array.Get(i) : info[start + i];
        }
        if (env.IsExceptionPending()) return false;

        int rc = SQLITE_OK;
        const BindSubject subject = named
            ? BindSubject(param_name.c_str()) : BindSubject(pos);
        if (!BindValueDirect(_handle, pos, value, subject, &rc, NULL)) {
            // BindValueDirect threw for an unsupported value.
            sqlite3_clear_bindings(_handle);
            return false;
        }
        if (rc != SQLITE_OK) {
            // Clear every binding so no partially bound statement is left
            // reachable.
            sqlite3_clear_bindings(_handle);
            status = rc;
            message = std::string(sqlite3_errmsg(db->_handle));
            return false;
        }
    }

    status = SQLITE_OK;
    return true;
}

Napi::Value Statement::Bind(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    auto baton = stmt->Bind<Baton>(info);
    if (baton == NULL) {
        // BindParameter already threw for unsupported values; the generic
        // message only covers malformed top-level argument shapes.
        if (!env.IsExceptionPending()) {
            Napi::TypeError::New(env, "Data type is not supported")
                .ThrowAsJavaScriptException();
        }
        return env.Null();
    }
    else {
        stmt->Schedule(Work_BeginBind, baton);
        return info.This();
    }
}

void Statement::Work_BeginBind(Baton* baton) {
    STATEMENT_BEGIN(Bind);
}

void Statement::Work_Bind(napi_env e, void* data) {
    STATEMENT_INIT(Baton);

    STATEMENT_MUTEX(mtx);
    sqlite3_mutex_enter(mtx);
    stmt->Bind(std::move(baton->parameters), baton->bind_supplied);
    sqlite3_mutex_leave(mtx);
}

void Statement::Work_AfterBind(napi_env e, napi_status status, void* data) {
    std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    if (stmt->status != SQLITE_OK) {
        Error(baton.get());
    }
    else {
        // Fire callbacks.
        Napi::Function cb = baton->callback.Value();
        if (IS_FUNCTION(cb)) {
            Napi::Value argv[] = { env.Null() };
            TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
        }
    }
}



Napi::Value Statement::Get(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    int row_mode = SYNC_ROW_OBJECT;
    Napi::Function callback;
    int end = PopAsyncReadOptions(info, &callback, &row_mode);
    if (env.IsExceptionPending()) return env.Null();

    RowBaton* baton = stmt->Bind<RowBaton>(info, 0, end, callback);
    if (baton == NULL) {
        if (!env.IsExceptionPending()) {
            Napi::TypeError::New(env, "Data type is not supported")
                .ThrowAsJavaScriptException();
        }
        return env.Null();
    }
    else {
        baton->row_mode = row_mode;
        stmt->Schedule(Work_BeginGet, baton);
        return info.This();
    }
}

void Statement::Work_BeginGet(Baton* baton) {
    STATEMENT_BEGIN(Get);
}

void Statement::Work_Get(napi_env e, void* data) {
    STATEMENT_INIT(RowBaton);

    if (stmt->status != SQLITE_DONE || baton->parameters.size()
            || baton->bind_supplied) {
        STATEMENT_MUTEX(mtx);
        sqlite3_mutex_enter(mtx);

        if (stmt->Bind(std::move(baton->parameters), baton->bind_supplied)) {
            stmt->status = sqlite3_step(stmt->_handle);

            if (!(stmt->status == SQLITE_ROW || stmt->status == SQLITE_DONE)) {
                stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
            }
        }

        sqlite3_mutex_leave(mtx);

        if (stmt->status == SQLITE_ROW) {
            // Acquire one result row before returning.
            GetRow(&baton->row, stmt->_handle, &baton->columns);
        }
    }
}

void Statement::Work_AfterGet(napi_env e, napi_status status, void* data) {
    std::unique_ptr<RowBaton> baton(static_cast<RowBaton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) {
        Error(baton.get());
    }
    else {
        // Fire callbacks.
        Napi::Function cb = baton->callback.Value();
        if (IS_FUNCTION(cb)) {
            if (stmt->status == SQLITE_ROW) {
                // Create the result array from the data we acquired.
                stmt->SyncColumnKeys(env, baton->columns);
                Napi::Value row = stmt->RowToJS(env, &baton->row,
                    baton->row_mode);
                if (env.IsExceptionPending()) {
                    // 'number' integer mode and an unsafe int64: deliver
                    // the RangeError to the callback instead of leaving a
                    // pending exception in the async completion.
                    Napi::Value argv[] = { TakePendingError(env) };
                    TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
                }
                else {
                    Napi::Value argv[] = { env.Null(), row };
                    TRY_CATCH_CALL(stmt->Value(), cb, 2, argv);
                }
            }
            else {
                Napi::Value argv[] = { env.Null() };
                TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
            }
        }
    }
}

Napi::Value Statement::Run(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    Baton* baton = stmt->Bind<RunBaton>(info);
    if (baton == NULL) {
        if (!env.IsExceptionPending()) {
            Napi::TypeError::New(env, "Data type is not supported")
                .ThrowAsJavaScriptException();
        }
        return env.Null();
    }
    else {
        stmt->Schedule(Work_BeginRun, baton);
        return info.This();
    }
}

void Statement::Work_BeginRun(Baton* baton) {
    STATEMENT_BEGIN(Run);
}

void Statement::Work_Run(napi_env e, void* data) {
    STATEMENT_INIT(RunBaton);

    STATEMENT_MUTEX(mtx);
    sqlite3_mutex_enter(mtx);

    // Make sure that we also reset when there are no parameters.
    if (!baton->parameters.size() && !baton->bind_supplied) {
        sqlite3_reset(stmt->_handle);
    }

    if (stmt->Bind(std::move(baton->parameters), baton->bind_supplied)) {
        stmt->status = sqlite3_step(stmt->_handle);

        if (!(stmt->status == SQLITE_ROW || stmt->status == SQLITE_DONE)) {
            stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
        }
        else {
            baton->inserted_id = sqlite3_last_insert_rowid(stmt->db->_handle);
            baton->changes = sqlite3_changes(stmt->db->_handle);
        }
    }

    sqlite3_mutex_leave(mtx);
}

void Statement::Work_AfterRun(napi_env e, napi_status status, void* data) {
    std::unique_ptr<RunBaton> baton(static_cast<RunBaton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) {
        Error(baton.get());
    }
    else {
        // Fire callbacks. lastID/changes are exposed through accessors
        // reading the stored members, so `this.lastID` inside the
        // callback applies the database's integer mode.
        stmt->RecordRunResult(baton->inserted_id, baton->changes);

        Napi::Function cb = baton->callback.Value();
        if (IS_FUNCTION(cb)) {
            Napi::Value argv[] = { env.Null() };
            TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
        }
    }
}

Napi::Value Statement::All(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    int row_mode = SYNC_ROW_OBJECT;
    Napi::Function callback;
    int end = PopAsyncReadOptions(info, &callback, &row_mode);
    if (env.IsExceptionPending()) return env.Null();

    RowsBaton* baton = stmt->Bind<RowsBaton>(info, 0, end, callback);
    if (baton == NULL) {
        if (!env.IsExceptionPending()) {
            Napi::TypeError::New(env, "Data type is not supported")
                .ThrowAsJavaScriptException();
        }
        return env.Null();
    }
    else {
        baton->row_mode = row_mode;
        stmt->Schedule(Work_BeginAll, baton);
        return info.This();
    }
}

void Statement::Work_BeginAll(Baton* baton) {
    STATEMENT_BEGIN(All);
}

void Statement::Work_All(napi_env e, void* data) {
    STATEMENT_INIT(RowsBaton);

    STATEMENT_MUTEX(mtx);
    sqlite3_mutex_enter(mtx);

    // Make sure that we also reset when there are no parameters.
    if (!baton->parameters.size() && !baton->bind_supplied) {
        sqlite3_reset(stmt->_handle);
    }

    if (stmt->Bind(std::move(baton->parameters), baton->bind_supplied)) {
        while ((stmt->status = sqlite3_step(stmt->_handle)) == SQLITE_ROW) {
            baton->rows.emplace_back();
            GetRow(&baton->rows.back(), stmt->_handle, &baton->columns);
        }
        if (stmt->status != SQLITE_DONE) {
            stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
        }
    }

    sqlite3_mutex_leave(mtx);
}

void Statement::Work_AfterAll(napi_env e, napi_status status, void* data) {
    std::unique_ptr<RowsBaton> baton(static_cast<RowsBaton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    if (stmt->status != SQLITE_DONE) {
        Error(baton.get());
        return;
    }

    // Fire callbacks.
    //
    // Note: conversion is deliberately a single synchronous pass. Spreading
    // it over several event-loop turns would release the statement lock
    // early and let later-queued operations report before this one, which
    // breaks the FIFO callback ordering that serialize() depends on.
    Napi::Function cb = baton->callback.Value();

    if (IS_FUNCTION(cb)) {
        if (baton->rows.size()) {
            // Create the result array from the data we acquired.
            Napi::Array result;
            const bool failed = !stmt->CellRowsToJS(env, baton->rows,
                baton->columns, baton->row_mode, &result);

            if (failed) {
                Napi::Value argv[] = { TakePendingError(env) };
                TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
            }
            else {
                Napi::Value argv[] = { env.Null(), result };
                TRY_CATCH_CALL(stmt->Value(), cb, 2, argv);
            }
        }
        else {
            // There were no result rows.
            Napi::Value argv[] = {
                env.Null(),
                Napi::Array::New(env, 0)
            };
            TRY_CATCH_CALL(stmt->Value(), cb, 2, argv);
        }
    }
}

Napi::Value Statement::Each(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    int last = info.Length();

    Napi::Function completed;
    if (last >= 2 && info[last - 1].IsFunction() && info[last - 2].IsFunction()) {
        completed = info[--last].As<Napi::Function>();
    }

    // Item callback, then an optional `{ rowMode }` bag before it (the
    // same shape Get/All accept; PopAsyncReadOptions cannot be reused
    // here because `last` no longer sits at the end of the call).
    int row_mode = SYNC_ROW_OBJECT;
    Napi::Function callback;
    if (last > 0 && info[last - 1].IsFunction()) {
        callback = info[--last].As<Napi::Function>();
    }
    if (last > 0 && ParseSyncReadOptions(info[last - 1], &row_mode)) last--;
    if (env.IsExceptionPending()) return env.Null();

    auto baton = stmt->Bind<EachBaton>(info, 0, last, callback);
    if (baton == NULL) {
        if (!env.IsExceptionPending()) {
            Napi::TypeError::New(env, "Data type is not supported")
                .ThrowAsJavaScriptException();
        }
        return env.Null();
    }
    else {
        baton->row_mode = row_mode;
        baton->completed.Reset(completed, 1);
        stmt->Schedule(Work_BeginEach, baton);
        return info.This();
    }
}

void Statement::Work_BeginEach(Baton* baton) {
    // Only create the Async object when we're actually going into
    // the event loop. This prevents dangling events.
    auto* each_baton = static_cast<EachBaton*>(baton);
    each_baton->async = new Async(each_baton->stmt, reinterpret_cast<uv_async_cb>(AsyncEach));
    each_baton->async->item_cb.Reset(each_baton->callback.Value(), 1);
    each_baton->async->completed_cb.Reset(each_baton->completed.Value(), 1);
    each_baton->async->row_mode = each_baton->row_mode;

    STATEMENT_BEGIN(Each);
}

void Statement::Work_Each(napi_env e, void* data) {
    STATEMENT_INIT(EachBaton);

    auto* async = baton->async;

    STATEMENT_MUTEX(mtx);

    // Make sure that we also reset when there are no parameters.
    if (!baton->parameters.size() && !baton->bind_supplied) {
        sqlite3_reset(stmt->_handle);
    }

    sqlite3_mutex_enter(mtx);
    bool bound = stmt->Bind(std::move(baton->parameters), baton->bind_supplied);
    sqlite3_mutex_leave(mtx);

    if (bound) {
        while (true) {
            // Reacquire per row rather than holding the connection mutex for
            // the whole result set: other statements on this database must
            // still make progress while a long each() streams.
            //
            // GetRow stays inside the lock, since sqlite3_column_* reads the
            // statement's own state and is not safe to run concurrently with
            // another thread using the same connection.
            sqlite3_mutex_enter(mtx);
            stmt->status = sqlite3_step(stmt->_handle);
            if (stmt->status != SQLITE_ROW) {
                if (stmt->status != SQLITE_DONE) {
                    stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
                }
                sqlite3_mutex_leave(mtx);
                break;
            }

            NODE_SQLITE3_MUTEX_LOCK(&async->mutex)
            async->data.emplace_back();
            GetRow(&async->data.back(), stmt->_handle, &async->columns);
            NODE_SQLITE3_MUTEX_UNLOCK(&async->mutex)
            sqlite3_mutex_leave(mtx);

            // uv_async_send already coalesces: it is a cheap atomic test when
            // a wakeup is pending, so signalling per row costs almost nothing
            // and keeps each() streaming instead of batching to the end.
            uv_async_send(&async->watcher);
        }
    }

    async->completed = true;
    uv_async_send(&async->watcher);
}

void Statement::CloseCallback(uv_handle_t* handle) {
    assert(handle != NULL);
    assert(handle->data != NULL);
    auto* async = static_cast<Async*>(handle->data);
    delete async;
}

void Statement::AsyncEach(uv_async_t* handle) {
    auto* async = static_cast<Async*>(handle->data);

    auto env = async->stmt->Env();
    Napi::HandleScope scope(env);

    while (true) {
        // Get the contents out of the data cache for us to process in the JS callback.
        Rows rows;
        Columns columns;
        NODE_SQLITE3_MUTEX_LOCK(&async->mutex)
        rows.swap(async->data);
        columns = async->columns;
        NODE_SQLITE3_MUTEX_UNLOCK(&async->mutex)

        if (rows.empty()) {
            break;
        }

        Napi::Function cb = async->item_cb.Value();
        if (IS_FUNCTION(cb)) {
            Napi::Value argv[2];
            argv[0] = env.Null();

            async->stmt->SyncColumnKeys(env, columns);
            std::vector<napi_value> keys;
            for (auto& row : rows) {
                // A scope per row, not per batch: each row is handed to the
                // callback and then unreachable, so the handles must not
                // accumulate for the length of the result set. The keys are
                // resolved inside it for the same reason CellRowsToJS
                // re-resolves per batch — handles die with their scope.
                Napi::HandleScope row_scope(env);
                async->stmt->ResolveColumnKeys(&keys);

                napi_value converted = NULL;
                if (!async->stmt->ConvertCellRow(env, &row, keys,
                        async->row_mode, &converted)) {
                    // 'number' integer mode and an unsafe int64: hand the
                    // RangeError to the item callback in place of the row.
                    argv[0] = TakePendingError(env);
                    TRY_CATCH_CALL(async->stmt->Value(), cb, 1, argv);
                    argv[0] = env.Null();
                    continue;
                }
                argv[1] = Napi::Value(env, converted);
                async->retrieved++;
                TRY_CATCH_CALL(async->stmt->Value(), cb, 2, argv);
            }
        }
    }

    Napi::Function cb = async->completed_cb.Value();
    if (async->completed) {
        if (!cb.IsEmpty() &&
                cb.IsFunction()) {
            Napi::Value argv[] = {
                env.Null(),
                Napi::Number::New(env, async->retrieved)
            };
            TRY_CATCH_CALL(async->stmt->Value(), cb, 2, argv);
        }
        uv_close(reinterpret_cast<uv_handle_t*>(handle), CloseCallback);
    }
}

void Statement::Work_AfterEach(napi_env e, napi_status status, void* data) {
    std::unique_ptr<EachBaton> baton(static_cast<EachBaton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    if (stmt->status != SQLITE_DONE) {
        Error(baton.get());
    }
}

Napi::Value Statement::Reset(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    OPTIONAL_ARGUMENT_FUNCTION(0, callback);

    auto* baton = new Baton(stmt, callback);
    stmt->Schedule(Work_BeginReset, baton);

    return info.This();
}

void Statement::Work_BeginReset(Baton* baton) {
    STATEMENT_BEGIN(Reset);
}

void Statement::Work_Reset(napi_env e, void* data) {
    STATEMENT_INIT(Baton);

    sqlite3_reset(stmt->_handle);
    stmt->status = SQLITE_OK;
}

void Statement::Work_AfterReset(napi_env e, napi_status status, void* data) {
    std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    // Fire callbacks.
    Napi::Function cb = baton->callback.Value();
    if (IS_FUNCTION(cb)) {
        Napi::Value argv[] = { env.Null() };
        TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
    }
}

// fetch(count, [params], [{rowMode}], [callback]): steps up to `count`
// rows and hands them back as one batch. Unlike all() the statement is
// deliberately NOT reset between calls, so successive fetches continue one
// cursor — this is the native half of the pull-based async iterator.
Napi::Value Statement::Fetch(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    REQUIRE_ARGUMENT_INTEGER(0, count);
    if (count < 1) {
        Napi::TypeError::New(env, "fetch count must be a positive integer")
            .ThrowAsJavaScriptException();
        return env.Null();
    }

    // The bag/callback scan mirrors PopAsyncReadOptions, offset by the
    // leading count argument.
    int end = info.Length();
    Napi::Function callback;
    if (end > 1 && info[end - 1].IsFunction()) {
        callback = info[--end].As<Napi::Function>();
    }
    int row_mode = SYNC_ROW_OBJECT;
    if (end > 1 && ParseSyncReadOptions(info[end - 1], &row_mode)) end--;
    if (env.IsExceptionPending()) return env.Null();

    FetchBaton* baton = stmt->Bind<FetchBaton>(info, 1, end, callback);
    if (baton == NULL) {
        if (!env.IsExceptionPending()) {
            Napi::TypeError::New(env, "Data type is not supported")
                .ThrowAsJavaScriptException();
        }
        return env.Null();
    }
    else {
        baton->count = count;
        baton->row_mode = row_mode;
        stmt->Schedule(Work_BeginFetch, baton);
        return info.This();
    }
}

void Statement::Work_BeginFetch(Baton* baton) {
    STATEMENT_BEGIN(Fetch);
}

void Statement::Work_Fetch(napi_env e, void* data) {
    STATEMENT_INIT(FetchBaton);

    // Mirrors Work_Get's guard: a cursor that already returned SQLITE_DONE
    // must not be stepped again, unless this call rebinds (which resets).
    if (stmt->status != SQLITE_DONE || baton->parameters.size()
            || baton->bind_supplied) {
        STATEMENT_MUTEX(mtx);
        sqlite3_mutex_enter(mtx);

        if (stmt->Bind(std::move(baton->parameters), baton->bind_supplied)) {
            int n = 0;
            while (n < baton->count
                    && (stmt->status = sqlite3_step(stmt->_handle)) == SQLITE_ROW) {
                baton->rows.emplace_back();
                GetRow(&baton->rows.back(), stmt->_handle, &baton->columns);
                n++;
            }
            if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) {
                stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
            }
        }

        sqlite3_mutex_leave(mtx);
    }

    baton->done = (stmt->status == SQLITE_DONE);
}

void Statement::Work_AfterFetch(napi_env e, napi_status status, void* data) {
    std::unique_ptr<FetchBaton> baton(static_cast<FetchBaton*>(data));
    AFTER_WORK_TEARDOWN_GUARD(baton);
    auto* stmt = baton->stmt;

    auto env = stmt->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.
    STATEMENT_END();

    if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) {
        Error(baton.get());
        return;
    }

    // Fire callbacks: (err, rows, done). `done` is true when the cursor is
    // exhausted, so the JS iterator can stop pulling without another round
    // trip. Row conversion is a single synchronous pass, like Work_AfterAll.
    Napi::Function cb = baton->callback.Value();

    if (IS_FUNCTION(cb)) {
        if (baton->rows.size()) {
            Napi::Array result;
            const bool failed = !stmt->CellRowsToJS(env, baton->rows,
                baton->columns, baton->row_mode, &result);

            if (failed) {
                Napi::Value argv[] = { TakePendingError(env) };
                TRY_CATCH_CALL(stmt->Value(), cb, 1, argv);
            }
            else {
                Napi::Value argv[] = {
                    env.Null(),
                    result,
                    Napi::Boolean::New(env, baton->done)
                };
                TRY_CATCH_CALL(stmt->Value(), cb, 3, argv);
            }
        }
        else {
            Napi::Value argv[] = {
                env.Null(),
                Napi::Array::New(env, 0),
                Napi::Boolean::New(env, baton->done)
            };
            TRY_CATCH_CALL(stmt->Value(), cb, 3, argv);
        }
    }
}

// --- Synchronous fast path ---------------------------------------------
//
// The gate is the whole safety story: JS is single-threaded, pending and
// locked/db-queue state only mutate on the main thread, and the gate
// requires every queue to be empty and no worker in flight. Between the
// check and the sqlite calls nothing else can therefore touch this
// connection, and no queued operation can be overtaken (FIFO intact).

bool Statement::IdleForInline() {
    // pending == 0 plus an empty queue is the actual "nothing running or
    // deferred" condition. exclusiveHeld is not consulted: it says the
    // last dispatched call was exclusive, not that anything is still
    // running (pending and the queue say that).
    return prepared && !locked && !finalized && queue.empty()
        && db->db_state == Database::DbState::Open
        && db->pending == 0 && db->queue.empty();
}

void Statement::ThrowStatementError(Napi::Env env) {
    EXCEPTION(message, status, exception);
    // Byte offset of the failing token when this error came from a failed
    // prepare (sqlite3_error_offset; -1 otherwise). Consumed here: a later
    // error must not inherit a stale prepare position.
    if (error_offset >= 0) {
        exception_obj.Set("offset", Napi::Number::New(env, error_offset));
        error_offset = -1;
    }
    db->AttachPendingJsError(exception_obj);
    exception.As<Napi::Error>().ThrowAsJavaScriptException();
}

bool Statement::SyncGate(Napi::Env env) {
    if (finalized) {
        Napi::Error::New(env, "Statement is already finalized")
            .ThrowAsJavaScriptException();
        return false;
    }
    if (sync_in_flight) {
        // A user-defined function invoked from this statement's sync call
        // is on the JS stack right now; re-entering the same VM is the one
        // re-entrancy rule SQLite has (node:sqlite enforces the same since
        // v26.8). Other statements on the connection remain usable.
        Napi::Error::New(env,
            "this statement is currently executing: a user-defined "
            "function it invoked cannot drive the same statement "
            "re-entrantly; use another statement or the asynchronous API"
        ).ThrowAsJavaScriptException();
        return false;
    }
    if (!IdleForInline()) {
        Napi::Error::New(env,
            "database is busy: sync methods require a fully idle database"
        ).ThrowAsJavaScriptException();
        return false;
    }
    // A JavaScript collation cannot run on this thread: the comparison
    // would need the JS thread, which is the one about to block inside
    // SQLite — and unlike functions, a collation callback has no way to
    // report an error, so refusing up front is the only sound answer.
    // (Functions are handled per-invocation, with a precise error.) The
    // JS progress callback is refused for the same reason — it fires
    // during step (and prepare) and has no error channel. The
    // SharedArrayBuffer token form works fine from the sync methods.
    if (!db->js_collations.empty() || db->js_progress != NULL) {
        Napi::Error::New(env,
            "sync methods cannot be used while a JavaScript collation or "
            "progress callback is registered on this connection: a "
            "comparison or progress check would have to run JS on the "
            "thread that is blocked inside SQLite (a deadlock), and "
            "SQLite provides no way to fail a single one. "
            "removeCollation()/db.progress() first, or use the "
            "asynchronous API"
        ).ThrowAsJavaScriptException();
        return false;
    }
    return true;
}

void Statement::SyncColumnKeysLive(Napi::Env env) {
    // The rooted keys describe a captured shape; they stay valid while the
    // statement's own shape counters say the live statement still has that
    // shape. sqlite3_stmt_status(SQLITE_STMTSTATUS_REPREPARE) is the same
    // integer node:sqlite compares — a transparent re-prepare (schema
    // change, including a column rename that changes the result labels)
    // moves it and forces a re-read of the live names. The counter belongs
    // to the sqlite3_stmt and a Statement prepares exactly once, so a
    // fresh prepare can never resurrect a stale cache.
    const int reprepares =
        sqlite3_stmt_status(_handle, SQLITE_STMTSTATUS_REPREPARE, 0);
    const int cols = sqlite3_column_count(_handle);
    if (sync_keys_valid && reprepares == sync_keys_reprepares
            && cols == sync_keys_cols) {
        return;
    }

    sync_columns.names.clear();
    sync_columns.names.reserve(cols);
    for (int i = 0; i < cols; i++) {
        const char* name = sqlite3_column_name(_handle, i);
        sync_columns.names.emplace_back(name != NULL ? name : "");
    }
    SyncColumnKeys(env, sync_columns);
    sync_keys_valid = true;
    sync_keys_reprepares = reprepares;
    sync_keys_cols = cols;
}

Napi::Value Statement::GetSync(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    int row_mode = SYNC_ROW_OBJECT;
    int end = info.Length();
    if (end > 0 && ParseSyncReadOptions(info[end - 1], &row_mode)) end--;
    if (env.IsExceptionPending()) return env.Null();
    if (!SyncGate(env)) return env.Null();
    if (end > 0 && info[end - 1].IsFunction()) {
        Napi::TypeError::New(env, "Sync methods do not take a callback")
            .ThrowAsJavaScriptException();
        return env.Null();
    }

    const bool bind_supplied = (end > 0);

    // While this thread is inside sqlite, a user-defined function invoked
    // by the statement runs directly on this thread (see src/function.cc);
    // the guards are what its re-entrancy checks test.
    Database::SyncSqliteGuard sync_guard(stmt->db);
    Statement::SyncStepGuard step_guard(stmt);

    // Mirrors Work_Get: step unless the cursor is already exhausted and
    // no new parameters were supplied.
    if (stmt->status != SQLITE_DONE || bind_supplied) {
        if (!stmt->BindArgumentsDirect(info, 0, end, bind_supplied)) {
            if (env.IsExceptionPending()) return env.Null();
            stmt->ThrowStatementError(env);
            return env.Null();
        }
        stmt->status = sqlite3_step(stmt->_handle);
        if (!(stmt->status == SQLITE_ROW || stmt->status == SQLITE_DONE)) {
            stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
            stmt->ThrowStatementError(env);
            return env.Null();
        }
    }

    if (stmt->status == SQLITE_ROW) {
        // Straight from the live statement: no Row, no per-cell string
        // copy. The async path still materialises a Row because it reads
        // on a worker thread; here the row is converted on the thread that
        // stepped it, so the intermediate was pure cost.
        stmt->SyncColumnKeysLive(env);
        if (row_mode == SYNC_ROW_ARRAY) {
            // The array shape carries no keys; only the names (for the
            // integer-mode RangeError) and their cache stay valid.
            return stmt->CurrentRowToJS(env, {}, row_mode);
        }
        std::vector<napi_value> keys;
        stmt->ResolveColumnKeys(&keys);
        // A RangeError from the integer mode propagates to the caller
        // with the pending exception.
        return stmt->CurrentRowToJS(env, keys, row_mode);
    }
    return env.Undefined();
}

Napi::Value Statement::RunSync(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    int end = info.Length();
    if (!SyncGate(env)) return env.Null();
    if (end > 0 && info[end - 1].IsFunction()) {
        Napi::TypeError::New(env, "Sync methods do not take a callback")
            .ThrowAsJavaScriptException();
        return env.Null();
    }

    const bool bind_supplied = (end > 0);

    Database::SyncSqliteGuard sync_guard(stmt->db);
    Statement::SyncStepGuard step_guard(stmt);

    // Mirrors Work_Run, including the explicit reset for parameterless
    // re-execution.
    if (!bind_supplied) {
        sqlite3_reset(stmt->_handle);
    }

    if (!stmt->BindArgumentsDirect(info, 0, end, bind_supplied)) {
        if (env.IsExceptionPending()) return env.Null();
        stmt->ThrowStatementError(env);
        return env.Null();
    }
    stmt->status = sqlite3_step(stmt->_handle);

    if (!(stmt->status == SQLITE_ROW || stmt->status == SQLITE_DONE)) {
        stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
        stmt->ThrowStatementError(env);
        return env.Null();
    }

    sqlite3_int64 inserted_id = sqlite3_last_insert_rowid(stmt->db->_handle);
    int changes = sqlite3_changes(stmt->db->_handle);

    stmt->RecordRunResult(inserted_id, changes);

    return info.This();
}

Napi::Value Statement::AllSync(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    int row_mode = SYNC_ROW_OBJECT;
    int end = info.Length();
    if (end > 0 && ParseSyncReadOptions(info[end - 1], &row_mode)) end--;
    if (env.IsExceptionPending()) return env.Null();
    if (!SyncGate(env)) return env.Null();
    if (end > 0 && info[end - 1].IsFunction()) {
        Napi::TypeError::New(env, "Sync methods do not take a callback")
            .ThrowAsJavaScriptException();
        return env.Null();
    }

    const bool bind_supplied = (end > 0);

    Database::SyncSqliteGuard sync_guard(stmt->db);
    Statement::SyncStepGuard step_guard(stmt);

    if (!bind_supplied) {
        sqlite3_reset(stmt->_handle);
    }

    if (!stmt->BindArgumentsDirect(info, 0, end, bind_supplied)) {
        if (env.IsExceptionPending()) return env.Null();
        stmt->ThrowStatementError(env);
        return env.Null();
    }

    // One pass: step and convert together, instead of materialising the
    // whole result set as Rows and walking it again. The old shape copied
    // every text and blob twice and allocated a Row plus a Cell per column
    // per row, none of which the caller ever saw.
    //
    // Rows are converted straight into `result` under batched handle
    // scopes — one HandleScope per kRowsPerScope rows instead of an
    // escapable scope per row. A stored value is rooted by the result
    // array itself, so closing a batch's scope cannot collect anything the
    // caller still needs; the batch only bounds how many dead handles a
    // large read holds at once.
    static constexpr int kRowsPerScope = 256;

    Napi::Array result(Napi::Array::New(env));
    std::vector<napi_value> keys;
    bool keys_ready = false;
    bool failed = false;
    uint32_t count = 0;
    int cols = 0;

    bool exhausted = false;
    while (!exhausted) {
        Napi::HandleScope batch(env);
        // Handles live only in the scope that created them: the resolved
        // key strings must be re-resolved once per batch, not once per
        // call — napi_get_reference_value per 256 rows is noise. The very
        // first batch resolves them on its first row instead, after the
        // keys have been built for this execution's shape.
        if (keys_ready && row_mode == SYNC_ROW_OBJECT) {
            stmt->ResolveColumnKeys(&keys);
        }
        for (int i = 0; i < kRowsPerScope; i++) {
            stmt->status = sqlite3_step(stmt->_handle);
            if (stmt->status != SQLITE_ROW) {
                exhausted = true;
                break;
            }
            if (!keys_ready) {
                // The result shape cannot change between the rows of one
                // execution, so the names and their keys are settled on
                // the first row and reused for every row after it.
                stmt->SyncColumnKeysLive(env);
                cols = static_cast<int>(sync_columns.names.size());
                keys_ready = true;
                if (row_mode == SYNC_ROW_OBJECT) {
                    stmt->ResolveColumnKeys(&keys);
                }
            }
            napi_value row = NULL;
            // A RangeError from the integer mode leaves a pending exception
            // and reports false; the offending value may be in any row, not
            // only the first.
            if (!stmt->ConvertCurrentRow(env, keys, row_mode, cols, &row)) {
                failed = true;
                exhausted = true;
                break;
            }
            napi_set_element(env, result, count++, row);
        }
    }
    if (failed) {
        return env.Null();
    }
    if (stmt->status != SQLITE_DONE) {
        stmt->message = std::string(sqlite3_errmsg(stmt->db->_handle));
        stmt->ThrowStatementError(env);
        return env.Null();
    }

    return result;
}

void Statement::SyncColumnKeys(Napi::Env env, const Columns& columns) {
    // Root one JS string per column and reuse it for every row of the batch
    // instead of re-creating a key per cell. Rebuilt only when the statement
    // was re-prepared with a different result shape.
    if (column_keys_source == columns.names) {
        return;
    }
    // The compiled row factories bake in the old names and arity, so they
    // die with the keys. This is the only place the shape changes, so it is
    // the only place they can go stale.
    ResetRowFactories(env);
    column_keys.clear();
    column_keys.reserve(columns.names.size());
    for (const auto& name : columns.names) {
        column_keys.emplace_back(Napi::Persistent(Napi::String::New(env, name)));
    }
    column_keys_source = columns.names;
}

Napi::Value Statement::Int64ToJS(Napi::Env env, sqlite3_int64 value,
        const std::string& what) {
    return ConvertInt64ToJS(env, value, EffectiveIntegerMode(), what);
}

void Statement::RecordRunResult(sqlite3_int64 id, int changes) {
    last_insert_id = id;
    last_changes = changes;
    has_run_result = true;
}

Napi::Value Statement::GetLastID(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (!has_run_result) return env.Undefined();
    return Int64ToJS(env, last_insert_id, "lastID");
}

Napi::Value Statement::GetLastIDBigInt(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (!has_run_result) return env.Undefined();
    return Napi::BigInt::New(env, static_cast<int64_t>(last_insert_id));
}

Napi::Value Statement::GetChanges(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (!has_run_result) return env.Undefined();
    return Napi::Number::New(env, last_changes);
}

Napi::Value Statement::FinalizedGetter(const Napi::CallbackInfo& info) {
    return Napi::Boolean::New(info.Env(), finalized);
}

// --- Introspection (Deliverable 07) ---------------------------------------

// Must be called on the thread that prepared, with the connection mutex
// held (both prepare paths already satisfy this). Reads only metadata
// that is fixed for the lifetime of the sqlite3_stmt, and writes only
// pending_meta — never the accessor-visible fields, which belong to the
// JS thread. PublishMetadata() hands it over.
void Statement::SnapshotMetadata() {
    Metadata snapshot;
    snapshot.readonly = sqlite3_stmt_readonly(_handle) != 0;

    snapshot.param_count = sqlite3_bind_parameter_count(_handle);
    snapshot.param_names.reserve(snapshot.param_count);
    for (int i = 1; i <= snapshot.param_count; i++) {
        const char* name = sqlite3_bind_parameter_name(_handle, i);
        snapshot.param_names.emplace_back(name != NULL ? name : "");
    }

    int cols = sqlite3_column_count(_handle);
    snapshot.columns.reserve(cols);
    for (int i = 0; i < cols; i++) {
        ColumnMeta meta;
        const char* name = sqlite3_column_name(_handle, i);
        meta.name = name != NULL ? name : "";
        const char* decl = sqlite3_column_decltype(_handle, i);
        if (decl != NULL) meta.decltype_ = decl;
        // The origin family requires SQLITE_ENABLE_COLUMN_METADATA
        // (enabled in deps/sqlite3.gyp since Deliverable 07).
#ifdef SQLITE_ENABLE_COLUMN_METADATA
        const char* db_name = sqlite3_column_database_name(_handle, i);
        if (db_name != NULL) meta.database = db_name;
        const char* table = sqlite3_column_table_name(_handle, i);
        if (table != NULL) meta.table = table;
        const char* origin = sqlite3_column_origin_name(_handle, i);
        if (origin != NULL) meta.origin = origin;
#endif
        snapshot.columns.push_back(std::move(meta));
    }

    pending_meta = std::move(snapshot);
}

// JS thread only. Making the snapshot visible is a single flag flip
// after the move, so an accessor either sees no snapshot or sees a
// complete one; there is no window in which meta_valid is set while the
// vectors are still being filled.
void Statement::PublishMetadata() {
    meta = std::move(pending_meta);
    meta_valid = true;
}

Napi::Value Statement::ReadonlyGetter(const Napi::CallbackInfo& info) {
    if (!meta_valid) return info.Env().Undefined();
    return Napi::Boolean::New(info.Env(), meta.readonly);
}

Napi::Value Statement::ParameterCountGetter(const Napi::CallbackInfo& info) {
    if (!meta_valid) return info.Env().Undefined();
    return Napi::Number::New(info.Env(), meta.param_count);
}

Napi::Value Statement::ParameterNamesGetter(const Napi::CallbackInfo& info) {
    if (!meta_valid) return info.Env().Undefined();
    auto env = info.Env();
    // A fully positional statement (every parameter a bare `?`) carries no
    // names at all: `undefined` says so plainly, instead of an array of
    // nulls that reads like a bug at the call site. Mixed statements
    // (named and positional) keep the array, with null at every positional
    // index so the index mapping stays honest. Zero parameters is simply
    // an empty array.
    bool any_named = false;
    for (const auto& name : meta.param_names) {
        if (!name.empty()) {
            any_named = true;
            break;
        }
    }
    if (!meta.param_names.empty() && !any_named) return env.Undefined();
    Napi::Array result = Napi::Array::New(env, meta.param_names.size());
    for (size_t i = 0; i < meta.param_names.size(); i++) {
        // Positional `?` parameters have no name: null keeps the index
        // mapping honest instead of shifting the array.
        result.Set(i, meta.param_names[i].empty()
            ? env.Null().As<Napi::Value>()
            : Napi::String::New(env, meta.param_names[i]).As<Napi::Value>());
    }
    return result;
}

Napi::Value Statement::ColumnsGetter(const Napi::CallbackInfo& info) {
    if (!meta_valid) return info.Env().Undefined();
    auto env = info.Env();
    Napi::Array result = Napi::Array::New(env, meta.columns.size());
    size_t i = 0;
    for (const auto& column : meta.columns) {
        Napi::Object col = Napi::Object::New(env);
        col.Set("name", Napi::String::New(env, column.name.c_str()));
        // Fields sqlite reports as NULL are omitted rather than nulled:
        // an absent declaredType says "the column has none", a null one
        // would read as "unknown".
        if (!column.decltype_.empty()) {
            col.Set("declaredType",
                Napi::String::New(env, column.decltype_.c_str()));
        }
        if (!column.database.empty()) {
            col.Set("database",
                Napi::String::New(env, column.database.c_str()));
        }
        if (!column.table.empty()) {
            col.Set("table", Napi::String::New(env, column.table.c_str()));
        }
        if (!column.origin.empty()) {
            col.Set("origin", Napi::String::New(env, column.origin.c_str()));
        }
        result.Set(i++, col);
    }
    return result;
}

// status(op, reset?): live sqlite3_stmt_status counters. Takes the
// connection mutex for the read; refuses while a JS round trip could be
// holding it (the mutex would be waiting on this very thread).
Napi::Value Statement::Status(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;

    REQUIRE_ARGUMENT_INTEGER(0, op);
    bool reset = false;
    if (info.Length() > 1 && !info[1].IsUndefined()) {
        if (!info[1].IsBoolean()) {
            Napi::TypeError::New(env, "reset flag must be a boolean")
                .ThrowAsJavaScriptException();
            return env.Null();
        }
        reset = info[1].As<Napi::Boolean>().Value();
    }

    if (finalized) {
        Napi::Error::New(env, "Statement is already finalized")
            .ThrowAsJavaScriptException();
        return env.Null();
    }
    if (!prepared) {
        Napi::Error::New(env,
            "Statement is not prepared yet").ThrowAsJavaScriptException();
        return env.Null();
    }
    if (db->MayBlockOnWorkerRoundTrip()) {
        Napi::Error::New(env,
            "statement.status() cannot run while a JavaScript function, "
            "collation or progress callback is mid-call on this "
            "connection; call it after the query completes"
        ).ThrowAsJavaScriptException();
        return env.Null();
    }

    if (db->_handle == NULL) {
        Napi::Error::New(env, "Database handle is closed")
            .ThrowAsJavaScriptException();
        return env.Null();
    }
    sqlite3_mutex* mtx = sqlite3_db_mutex(db->_handle);
    sqlite3_mutex_enter(mtx);
    int value = sqlite3_stmt_status(stmt->_handle, op, reset ? 1 : 0);
    sqlite3_mutex_leave(mtx);

    return Napi::Number::New(env, value);
}

// Shared prelude for the SQL accessors: refuse (with a thrown error) when
// the live statement cannot be safely read right now. Returns true when
// the caller may touch _handle.
bool Statement::SQLAccessorGate(Napi::Env env) {
    if (finalized) {
        Napi::Error::New(env, "Statement is already finalized")
            .ThrowAsJavaScriptException();
        return false;
    }
    if (!prepared) {
        Napi::Error::New(env,
            "Statement is not prepared yet").ThrowAsJavaScriptException();
        return false;
    }
    if (db->MayBlockOnWorkerRoundTrip()) {
        Napi::Error::New(env,
            "the SQL accessors cannot be read while a JavaScript function, "
            "collation or progress callback is mid-call on this "
            "connection; read them from a callback or after the query"
        ).ThrowAsJavaScriptException();
        return false;
    }
    if (db->_handle == NULL) {
        Napi::Error::New(env, "Database handle is closed")
            .ThrowAsJavaScriptException();
        return false;
    }
    return true;
}

// The statement's SQL with the most recent bound values substituted
// (sqlite3_expanded_sql). The result is sqlite3_malloc'd and freed here.
Napi::Value Statement::ExpandedSQLGetter(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (!SQLAccessorGate(env)) return env.Null();
    sqlite3_mutex* mtx = sqlite3_db_mutex(db->_handle);
    sqlite3_mutex_enter(mtx);
    char* text = sqlite3_expanded_sql(_handle);
    std::string copy = text != NULL ? text : "";
    sqlite3_free(text);
    sqlite3_mutex_leave(mtx);
    return Napi::String::New(env, copy);
}

// The statement's SQL with literals folded to `?`
// (sqlite3_normalized_sql; requires SQLITE_ENABLE_NORMALIZE). The string
// points into the statement itself and dies with the mutex leave.
Napi::Value Statement::NormalizedSQLGetter(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (!SQLAccessorGate(env)) return env.Null();
#ifdef SQLITE_ENABLE_NORMALIZE
    sqlite3_mutex* mtx = sqlite3_db_mutex(db->_handle);
    sqlite3_mutex_enter(mtx);
    const char* text = sqlite3_normalized_sql(_handle);
    std::string copy = text != NULL ? text : "";
    sqlite3_mutex_leave(mtx);
    return Napi::String::New(env, copy);
#else
    Napi::Error::New(env,
        "normalizedSQL requires a build with SQLITE_ENABLE_NORMALIZE")
        .ThrowAsJavaScriptException();
    return env.Null();
#endif
}

// _setIntegerMode(mode | null): per-statement integer-mode override.
// JS-thread only (all row conversion happens there), applied by the JS
// layer immediately after construction.
Napi::Value Statement::SetIntegerMode(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    REQUIRE_ARGUMENT_STRING(0, mode);
    int value;
    if (mode == "number") value = Database::INTEGER_NUMBER;
    else if (mode == "bigint") value = Database::INTEGER_BIGINT;
    else if (mode == "mixed") value = Database::INTEGER_MIXED;
    else {
        Napi::TypeError::New(env,
            "integer mode must be 'number', 'bigint' or 'mixed'")
            .ThrowAsJavaScriptException();
        return env.Null();
    }
    integer_mode_override = value;
    has_integer_override = true;
    return info.This();
}

bool Statement::ConvertCellRow(Napi::Env env, Row* row,
        const std::vector<napi_value>& keys, int row_mode, napi_value* out) {
    const int mode = EffectiveIntegerMode();
    const size_t key_count = keys.size();

    // Pluck serves the first column alone; no row container is built.
    if (row_mode == SYNC_ROW_PLUCK) {
        if (row->empty()) {
            napi_value undef = NULL;
            napi_get_undefined(env, &undef);
            *out = undef;
            return true;
        }
        bool raised = false;
        Napi::Value value = CellToJS(env, (*row)[0], mode,
            ValueOrigin(&column_keys_source, 0), true, &raised);
        if (raised) return false;
        *out = value;
        return true;
    }

    // Same one-call-per-row build as the synchronous path; see
    // ConvertCurrentRow for why the store loop below is the slow shape.
    napi_value factory = RowFactoryForShape(env, row_mode);
    if (factory != NULL && row->size() == column_keys_source.size()) {
        const int cols = static_cast<int>(row->size());
        std::vector<napi_value> cells(row->size());
        for (int i = 0; i < cols; i++) {
            bool raised = false;
            cells[i] = CellToJS(env, (*row)[i], mode,
                ValueOrigin(&column_keys_source, static_cast<size_t>(i)),
                true, &raised);
            if (raised) return false;
        }
        return CallRowFactory(env, factory, cells, cols, out);
    }

    // No factory (codegen unavailable, too many columns, or a shape
    // mismatch): the store loops, in the requested shape.
    if (row_mode == SYNC_ROW_ARRAY) {
        napi_value arr;
        napi_create_array_with_length(env, row->size(), &arr);
        size_t i = 0;
        for (auto& cell : *row) {
            bool raised = false;
            Napi::Value value = CellToJS(env, cell, mode,
                ValueOrigin(&column_keys_source, i), true, &raised);
            if (raised) return false;
            napi_set_element(env, arr, static_cast<uint32_t>(i), value);
            i++;
        }
        *out = arr;
        return true;
    }

    napi_value result;
    napi_create_object(env, &result);

    size_t i = 0;
    for (auto& cell : *row) {
        // The column description is passed by reference to the cached
        // names, not built here: it is only formatted if the conversion
        // raises the 'number'-mode RangeError. Building it eagerly was a
        // heap allocation per cell on every successful read.
        bool raised = false;
        Napi::Value value = CellToJS(env, cell, mode,
            ValueOrigin(&column_keys_source, i), true, &raised);
        if (raised) return false;

        // The keys always cover the row: both are derived from the same
        // sqlite3_column_count, and a mid-stream re-prepare refreshes them
        // together. The bound is kept so a shape change that slipped through
        // can never index out of range.
        if (i < key_count) {
            // Raw napi on already-resolved handles: the key references are
            // dereferenced once per batch by the caller, not once per cell.
            napi_set_property(env, result, keys[i], value);
        }
        i++;
    }

    *out = result;
    return true;
}

Napi::Value Statement::RowToJS(Napi::Env env, Row* row, int row_mode) {
    Napi::EscapableHandleScope scope(env);

    std::vector<napi_value> keys;
    ResolveColumnKeys(&keys);

    napi_value result = NULL;
    if (!ConvertCellRow(env, row, keys, row_mode, &result)) {
        return scope.Escape(env.Null());
    }
    return scope.Escape(Napi::Value(env, result));
}

bool Statement::CellRowsToJS(Napi::Env env, Rows& rows,
        const Columns& columns, int row_mode, Napi::Array* out) {
    SyncColumnKeys(env, columns);

    Napi::Array result(Napi::Array::New(env, rows.size()));
    *out = result;

    // One scope per batch rather than one per row. The handles a scope
    // creates die with it, so the resolved keys are re-resolved inside each
    // batch; the converted rows are safe because they are stored into
    // `result` — a rooted array in the caller's scope — before the batch
    // scope closes.
    const size_t kBatch = 256;
    std::vector<napi_value> keys;

    for (size_t start = 0; start < rows.size(); start += kBatch) {
        Napi::HandleScope batch(env);
        if (row_mode == SYNC_ROW_OBJECT) ResolveColumnKeys(&keys);

        const size_t end = std::min(start + kBatch, rows.size());
        for (size_t i = start; i < end; i++) {
            napi_value row = NULL;
            if (!ConvertCellRow(env, &rows[i], keys, row_mode, &row)) {
                // 'number' integer mode and an unsafe int64: the RangeError
                // is pending for the caller to deliver.
                return false;
            }
            napi_set_element(env, result, static_cast<uint32_t>(i), row);
        }
    }

    return true;
}

void Statement::ResetRowFactories(Napi::Env env) {
    for (int i = 0; i < 2; i++) {
        if (row_factory_[i] != NULL) {
            napi_delete_reference(env, row_factory_[i]);
            row_factory_[i] = NULL;
        }
    }
}

napi_value Statement::RowFactoryForShape(Napi::Env env, int row_mode) {
    const int slot = (row_mode == SYNC_ROW_ARRAY) ? 1 : 0;

    if (row_factory_[slot] != NULL) {
        napi_value cached = NULL;
        if (napi_get_reference_value(env, row_factory_[slot], &cached)
                == napi_ok && cached != NULL) {
            return cached;
        }
    }

    auto* addon = env.GetInstanceData<Database::AddonData>();
    if (addon == NULL || addon->row_factory_generator == NULL
            || addon->row_factory_unavailable) {
        return NULL;
    }

    // Compiling means calling JS, which is refused while an exception is
    // pending. Bail out without diagnosing anything: the caller's store
    // loop stays correct, and a pending exception here says nothing about
    // whether this realm can generate code.
    if (env.IsExceptionPending()) return NULL;

    const size_t cols = column_keys_source.size();
    if (cols == 0 || cols > static_cast<size_t>(kMaxFactoryColumns)) {
        return NULL;
    }

    napi_value generator = NULL;
    if (napi_get_reference_value(env, addon->row_factory_generator, &generator)
            != napi_ok || generator == NULL) {
        return NULL;
    }

    // The names go over as a JS array so the generated source is escaped by
    // JSON.stringify rather than by an escaper of our own.
    napi_value names = NULL;
    napi_create_array_with_length(env, cols, &names);
    for (size_t i = 0; i < cols; i++) {
        napi_value name = NULL;
        if (napi_create_string_utf8(env, column_keys_source[i].data(),
                column_keys_source[i].size(), &name) != napi_ok) {
            return NULL;
        }
        napi_set_element(env, names, static_cast<uint32_t>(i), name);
    }

    napi_value want_array = NULL;
    napi_get_boolean(env, row_mode == SYNC_ROW_ARRAY, &want_array);
    napi_value argv[] = { names, want_array };
    napi_value undef = NULL;
    napi_get_undefined(env, &undef);

    napi_value factory = NULL;
    const napi_status st = napi_call_function(env, undef, generator, 2, argv,
        &factory);
    if (st != napi_ok || factory == NULL) {
        // A realm that forbids code generation from strings (a CSP'd
        // renderer, --disallow-code-generation-from-strings) throws here.
        // That is a permanent property of the environment, so remember it
        // and never pay for the attempt again; the store loop stays
        // correct, only slower.
        if (env.IsExceptionPending()) {
            napi_value ignored = NULL;
            napi_get_and_clear_last_exception(env, &ignored);
        }
        addon->row_factory_unavailable = true;
        return NULL;
    }

    napi_valuetype type = napi_undefined;
    napi_typeof(env, factory, &type);
    if (type != napi_function) return NULL;

    napi_create_reference(env, factory, 1, &row_factory_[slot]);
    return factory;
}

bool Statement::CallRowFactory(Napi::Env env, napi_value factory,
        const std::vector<napi_value>& cells, int cols, napi_value* out) {
    napi_value undef = NULL;
    napi_get_undefined(env, &undef);
    return napi_call_function(env, undef, factory,
        static_cast<size_t>(cols), cells.data(), out) == napi_ok;
}

void Statement::ResolveColumnKeys(std::vector<napi_value>* out) {
    out->clear();
    out->reserve(column_keys.size());
    for (auto& key : column_keys) {
        out->push_back(key.Value());
    }
}

bool Statement::ConvertCurrentRow(Napi::Env env,
        const std::vector<napi_value>& keys, int row_mode, int cols,
        napi_value* out) {
    const int mode = EffectiveIntegerMode();
    const size_t key_count = keys.size();

    // Pluck serves the first column alone; no row container is built.
    if (row_mode == SYNC_ROW_PLUCK) {
        if (cols < 1) {
            napi_value undef = NULL;
            napi_get_undefined(env, &undef);
            *out = undef;
            return true;
        }
        bool raised = false;
        Napi::Value value = ColumnToJS(env, _handle, 0, mode,
            ValueOrigin(&column_keys_source, 0), &raised);
        if (raised) return false;
        *out = value;
        return true;
    }

    // The fast shape: convert the cells into a plain argument vector and
    // let a generated monomorphic function build the row in one call.
    // Profiling showed the per-column store loops below spend two thirds of
    // a read inside V8's generic property/element paths — a LookupIterator
    // and a map or elements-kind transition per column, which for objects
    // also reallocates the backing property array as it grows. One call
    // hands V8 the whole row at once, so it allocates the final shape
    // directly. See docs/performance.md.
    napi_value factory = RowFactoryForShape(env, row_mode);
    if (factory != NULL) {
        std::vector<napi_value> cells(static_cast<size_t>(cols));
        for (int i = 0; i < cols; i++) {
            bool raised = false;
            cells[i] = ColumnToJS(env, _handle, i, mode,
                ValueOrigin(&column_keys_source, static_cast<size_t>(i)),
                &raised);
            if (raised) return false;
        }
        return CallRowFactory(env, factory, cells, cols, out);
    }

    if (row_mode == SYNC_ROW_ARRAY) {
        // The bulk-reader shape: one pre-sized array per row, values in
        // result-column order. No property stores and no shape to
        // transition — duplicate column names keep every value instead of
        // collapsing, which is the point of the shape.
        napi_value row;
        napi_create_array_with_length(env,
            static_cast<size_t>(cols), &row);
        for (int i = 0; i < cols; i++) {
            bool raised = false;
            Napi::Value value = ColumnToJS(env, _handle, i, mode,
                ValueOrigin(&column_keys_source, static_cast<size_t>(i)),
                &raised);
            if (raised) return false;
            napi_set_element(env, row, static_cast<uint32_t>(i), value);
        }
        *out = row;
        return true;
    }

    napi_value result;
    napi_create_object(env, &result);

    for (int i = 0; i < cols; i++) {
        bool raised = false;
        Napi::Value value = ColumnToJS(env, _handle, i, mode,
            ValueOrigin(&column_keys_source, static_cast<size_t>(i)),
            &raised);
        if (raised) return false;
        // Same bound as RowToJS: keys and columns both derive from
        // sqlite3_column_count, so this cannot be exceeded in practice.
        if (static_cast<size_t>(i) < key_count) {
            // Raw napi on already-resolved handles: the key references are
            // dereferenced once per call by the caller, not once per cell.
            napi_set_property(env, result, keys[i], value);
        }
    }

    *out = result;
    return true;
}

Napi::Value Statement::CurrentRowToJS(Napi::Env env,
        const std::vector<napi_value>& keys, int row_mode) {
    Napi::EscapableHandleScope scope(env);

    napi_value row = NULL;
    if (!ConvertCurrentRow(env, keys, row_mode,
            sqlite3_column_count(_handle), &row)) {
        return scope.Escape(env.Null());
    }
    return scope.Escape(Napi::Value(env, row));
}

void Statement::GetRow(Row* row, sqlite3_stmt* stmt, Columns* columns) {
    int cols = sqlite3_column_count(stmt);

    // Captured once per execution; the result shape cannot change between
    // the rows of a single statement execution.
    columns->EnsureLoaded(stmt);

    row->clear();
    row->reserve(cols);

    for (int i = 0; i < cols; i++) {
        int type = sqlite3_column_type(stmt, i);

        switch (type) {
            case SQLITE_INTEGER: {
                Cell cell(SQLITE_INTEGER);
                cell.integer = sqlite3_column_int64(stmt, i);
                row->emplace_back(std::move(cell));
            }   break;
            case SQLITE_FLOAT: {
                Cell cell(SQLITE_FLOAT);
                cell.real = sqlite3_column_double(stmt, i);
                row->emplace_back(std::move(cell));
            }   break;
            case SQLITE_TEXT: {
                Cell cell(SQLITE_TEXT);
                const char* text = (const char*)sqlite3_column_text(stmt, i);
                int length = sqlite3_column_bytes(stmt, i);
                if (length > 0 && text != NULL) {
                    cell.str.assign(text, length);
                }
                row->emplace_back(std::move(cell));
            } break;
            case SQLITE_BLOB: {
                Cell cell(SQLITE_BLOB);
                const char* blob = (const char*)sqlite3_column_blob(stmt, i);
                int length = sqlite3_column_bytes(stmt, i);
                if (length > 0 && blob != NULL) {
                    cell.str.assign(blob, length);
                }
                row->emplace_back(std::move(cell));
            }   break;
            case SQLITE_NULL: {
                row->emplace_back(Cell(SQLITE_NULL));
            }   break;
            default:
                row->emplace_back(Cell(SQLITE_NULL));
        }
    }
}

Napi::Value Statement::Finalize_(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    Statement* stmt = this;
    OPTIONAL_ARGUMENT_FUNCTION(0, callback);

    // Refuse to finalize the statement sqlite is stepping right now: a
    // user callback invoked re-entrantly from this statement's own
    // getSync/runSync/allSync is on the stack, and sqlite3_finalize on a
    // live VM is a use-after-free. Other statements are unaffected.
    if (stmt->sync_in_flight) {
        Napi::Error::New(env,
            "this statement is currently executing: it cannot be "
            "finalized from a user-defined function it invoked; finalize "
            "it after the query completes"
        ).ThrowAsJavaScriptException();
        return env.Null();
    }

    auto *baton = new Baton(stmt, callback);
    stmt->Schedule(Finalize_, baton);

    return stmt->db->Value();
}

// Finalize-on-GC safety net: a collected statement that was never
// finalized is torn down here so a forgotten one degrades to "cleaned up
// at the next GC" instead of blocking close() forever. This runs in the
// ObjectWrap finalizer, so each step has to be safe there:
//
// - A statement with queued calls cannot reach this destructor: every
//   queued Baton holds a Ref on it. CleanQueue therefore sees an empty
//   queue and can never fire a JS callback from this context.
// - sqlite3_finalize is pure C API and takes the connection mutex
//   internally, so concurrent work on *other* statements of the same
//   database is safe; a collected statement has no work of its own in
//   flight.
// - The Database cannot outlive this call: this statement holds a Ref on
//   it, and its own handle cannot have been closed (sqlite3_close fails
//   with SQLITE_BUSY while this statement was outstanding).
// - db->Unref() is a reference-count operation, not a call into JS —
//   the same class of work node-addon-api itself does in finalizers.
//
// db is NULL only when the constructor threw before validation finished;
// then there is no handle, no Ref and nothing to release.
Statement::~Statement() {
    // The compiled row factories are strong references; drop them before
    // the env goes away. Safe on a torn-down env: napi_delete_reference
    // does not run JS.
    ResetRowFactories(Env());
    if (!finalized) {
        finalized = true;
        CleanQueue();
        // The guard: finalize can invoke an aggregate's xFinal (sqlite
        // calls it for incomplete aggregates), and this is the JS thread —
        // a blocking round trip from xFinal here would wait for itself.
        // MayBlockOnWorkerRoundTrip covers the second hazard: the mutex
        // itself, held by a worker waiting on a round trip this thread
        // must service.
        if (db && db->MayBlockOnWorkerRoundTrip() && _handle != NULL) {
            db->Schedule(Database::Work_DeferredHandleFinalize,
                new HandleFinalizeBaton(db, _handle), true);
            _handle = NULL;
        }
        else if (db) {
            Database::SyncSqliteGuard sync_guard(db);
            sqlite3_finalize(_handle);
            _handle = NULL;
        }
        else {
            sqlite3_finalize(_handle);
            _handle = NULL;
        }
        bound_payloads.clear();
        if (db) db->Unref();
    }
}

// Runs the finalize work of an original finalize baton (directly, or via
// the deferred wrapper below).
void Statement::FinishFinalizeBaton(Baton* baton) {
    std::unique_ptr<Statement::Baton> holder(baton);
    auto env = baton->stmt->Env();
    Napi::HandleScope scope(env);

    baton->stmt->Finalize_();

    // Fire callback in case there was one.
    Napi::Function cb = baton->callback.Value();
    if (IS_FUNCTION(cb)) {
        TRY_CATCH_CALL(baton->stmt->Value(), cb, 0, NULL);
    }
}

// Exclusive: dispatched only when pending == 0, so the connection mutex
// is free and the finalize cannot meet a worker blocked mid-round-trip.
void Database::Work_DeferredStatementFinalize(Baton* b) {
    auto baton = std::unique_ptr<Statement::DeferredFinalizeBaton>(
        static_cast<Statement::DeferredFinalizeBaton*>(b));
    auto* db = baton->db;

    Statement::Baton* inner = baton->inner;
    baton->inner = NULL; // ownership moves to FinishFinalizeBaton

    Statement::FinishFinalizeBaton(inner);

    db->exclusiveHeld = false;
    db->Process();
}

void Database::Work_DeferredHandleFinalize(Baton* b) {
    auto baton = std::unique_ptr<Statement::HandleFinalizeBaton>(
        static_cast<Statement::HandleFinalizeBaton*>(b));
    auto* db = baton->db;

    sqlite3_finalize(baton->handle);

    db->exclusiveHeld = false;
    db->Process();
}

void Statement::Finalize_(Baton* b) {
    Statement* stmt = b->stmt;
    // Finalizing on this thread takes the connection mutex inside
    // sqlite3_finalize. A worker blocked in a JS round trip holds that
    // mutex while waiting for this thread — finalizing now would deadlock
    // the two. Hand the work to the exclusive queue instead.
    if (stmt->db->MayBlockOnWorkerRoundTrip()) {
        auto* deferral = new DeferredFinalizeBaton(stmt->db, b);
        stmt->db->Schedule(Database::Work_DeferredStatementFinalize,
            deferral, true);
        return;
    }
    FinishFinalizeBaton(b);
}

void Statement::Finalize_() {
    assert(!finalized);
    finalized = true;
    CleanQueue();
    // Finalize returns the status code of the last operation. We already fired
    // error events in case those failed. The guard covers the xFinal an
    // incomplete aggregate fires from here — this runs on the JS thread,
    // where a blocking round trip would deadlock (see ~Statement).
    if (db->MayBlockOnWorkerRoundTrip() && _handle != NULL) {
        // Same hazard as Finalize_(Baton*): the mutex a round-tripping
        // worker holds. The handle is finalized from the exclusive queue;
        // the statement's own teardown proceeds (nothing else touches the
        // handle once finalized is set and _handle is cleared).
        db->Schedule(Database::Work_DeferredHandleFinalize,
            new HandleFinalizeBaton(db, _handle), true);
        _handle = NULL;
    }
    else {
        Database::SyncSqliteGuard sync_guard(db);
        sqlite3_finalize(_handle);
        _handle = NULL;
    }
    bound_payloads.clear();
    db->Unref();
}

// Hands `error` to every call still queued on this statement, settling
// each through whichever callback that kind of call settles through (see
// Baton::Fail). Emits an 'error' event when nothing took it, unless the
// caller reports the failure on the statement itself anyway.
//
// Callable from ~Statement: a queued Baton holds a Ref on the statement,
// so a collected statement provably has an empty queue and nothing is
// invoked from the finalizer.
void Statement::FailQueue(Napi::Value error, bool emit_if_unhandled) {
    auto env = this->Env();
    Napi::HandleScope scope(env);

    bool called = false;

    // Clear out the queue so that this object can get GC'ed.
    while (!queue.empty()) {
        auto call = std::unique_ptr<Call>(queue.front());
        queue.pop();

        auto baton = std::unique_ptr<Baton>(call->baton);
        if (baton->Fail(error)) called = true;
    }

    // When we couldn't call a callback function, emit an error on the
    // Statement object.
    if (!called && emit_if_unhandled) {
        Napi::Value info[] = { Napi::String::New(env, "error"), error };
        EMIT_EVENT(Value(), 2, info);
    }

}

void Statement::CleanQueue() {
    auto env = this->Env();
    Napi::HandleScope scope(env);

    // Environment teardown (worker termination): the error delivery
    // below constructs JS on a dying environment, which is fatal. Drop
    // the queued calls 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;
    }

    if (prepared && !queue.empty()) {
        // This statement has already been prepared and is now finalized.
        // Fire error for all remaining items in the queue.
        EXCEPTION("Statement is already finalized", SQLITE_MISUSE, exception);
        FailQueue(exception, true);
    }
    else while (!queue.empty()) {
        // Just delete all items in the queue; whatever is left here has
        // already been reported -- either by the prepare's own callback,
        // or by Work_AfterPrepare when there was none.
        auto call = std::unique_ptr<Call>(queue.front());
        queue.pop();
        // We don't call the actual callback, so we have to make sure that
        // the baton gets destroyed.
        delete call->baton;
    }
}
