#include <cmath>
#include <cstring>
#include <limits>

#include <napi.h>

#include "convert.h"

namespace node_sqlite3 {

namespace {

// Element size in bytes for each typed-array kind.
inline size_t TypedArrayElementSize(napi_typedarray_type type) {
    switch (type) {
        case napi_int8_array:
        case napi_uint8_array:
        case napi_uint8_clamped_array:      return 1;
        case napi_int16_array:
        case napi_uint16_array:             return 2;
        case napi_int32_array:
        case napi_uint32_array:
        case napi_float32_array:            return 4;
        case napi_float64_array:
        case napi_bigint64_array:
        case napi_biguint64_array:          return 8;
        default:                            return 0;
    }
}

// Human-readable type name for "unsupported type" errors. Never throws;
// falls back to a generic name when the constructor is inaccessible
// (e.g. a Proxy whose get trap throws).
std::string BindTypeName(const Napi::Value& source) {
    auto env = source.Env();
    switch (source.Type()) {
        case napi_symbol:   return "Symbol";
        case napi_function: return "Function";
        case napi_external: return "External";
        default: break;
    }
    if (source.IsObject()) {
        Napi::Object obj = source.As<Napi::Object>();
        Napi::Value ctor = obj.Get("constructor");
        if (!env.IsExceptionPending() && ctor.IsObject()) {
            Napi::Value name = ctor.As<Napi::Object>().Get("name");
            if (!env.IsExceptionPending() && name.IsString()) {
                std::string s = name.As<Napi::String>().Utf8Value();
                if (!s.empty()) return s;
            }
        }
        return "Object";
    }
    return "value";
}

void ThrowUnsupportedBindType(const Napi::Value& source,
        const std::string& subject) {
    auto env = source.Env();
    std::string msg = "Cannot bind " + subject +
        ": unsupported type " + BindTypeName(source) +
        ". Serialize it explicitly (e.g. JSON.stringify) before binding.";
    Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
}

// Range of doubles that convert to int64 without undefined behaviour.
// The upper bound is inclusive: JS cannot express 2^63-1, so the double
// 2^63 is the rounded form of it and clamps to INT64_MAX.
const double kInt64MinAsDouble = -9223372036854775808.0;   // -(2^63)
const double kInt64MaxAsDouble = 9223372036854775808.0;    //   2^63

} // namespace

// Resolves one global constructor, caching it per environment.
//
// The uncached form read `env.Global().Get("RegExp")` on every call: a
// napi_get_global, a JS string built from the C literal, and a property
// get — to answer a question whose answer is fixed for the lifetime of
// the environment. The bind paths ask it for every named-parameter call,
// where it was one of the largest single costs.
//
// The reference holds the current environment's constructor, which is
// precisely what the uncached lookup returned; caching changes nothing
// about which objects match.
static Napi::Function GlobalCtor(Napi::Env env, napi_ref* slot,
        const char* name) {
    if (*slot != NULL) {
        napi_value cached = NULL;
        if (napi_get_reference_value(env, *slot, &cached) == napi_ok
                && cached != NULL) {
            return Napi::Function(env, cached);
        }
    }
    Napi::Value ctor = env.Global().Get(name);
    if (!ctor.IsFunction()) return Napi::Function();
    // A weak reference would let the constructor be collected out from
    // under us; these live as long as the environment does.
    napi_create_reference(env, ctor, 1, slot);
    return ctor.As<Napi::Function>();
}

bool IsNamedParameterMap(Napi::Value source) {
    if (!source.IsObject() || source.IsArray()) return false;

    auto env = source.Env();
    auto* addon = env.GetInstanceData<Database::AddonData>();

    // Fast path: a plain object literal — which is what a named-parameter
    // map almost always is. Its prototype is Object.prototype, and none
    // of the shapes rejected below share that prototype, so one
    // comparison settles all six of them.
    napi_value proto = NULL;
    if (napi_get_prototype(env, source, &proto) == napi_ok && proto != NULL) {
        napi_value object_proto = NULL;
        if (addon->object_prototype == NULL) {
            Napi::Value base = env.Global().Get("Object");
            if (base.IsFunction()) {
                Napi::Value p = base.As<Napi::Object>().Get("prototype");
                if (p.IsObject()) {
                    napi_create_reference(env, p, 1,
                        &addon->object_prototype);
                }
            }
        }
        if (addon->object_prototype != NULL
                && napi_get_reference_value(env, addon->object_prototype,
                    &object_proto) == napi_ok) {
            bool same = false;
            if (napi_strict_equals(env, proto, object_proto, &same) == napi_ok
                    && same) {
                return true;
            }
        }
    }

    // Anything else: the explicit checks. Cheap predicates first; IsDate
    // matches across realms, and the RegExp lookup only runs once the
    // value is known to be an object. Binary views bind positionally,
    // like the other non-map shapes.
    return !source.IsBuffer() && !source.IsTypedArray()
        && !source.IsDataView() && !source.IsArrayBuffer()
        && !source.IsDate()
        && !OtherInstanceOf(source.As<Napi::Object>(), "RegExp");
}

bool ResolveNamedKey(Napi::Env env, Napi::Value key, NamedKey* out,
        std::string* storage) {
    // Long enough that no realistic parameter name reaches the fallback,
    // so the whole classification is a single napi call.
    char stack[128];
    size_t written = 0;
    const size_t capacity = sizeof(stack);
    bool have_text = false;

    if (key.IsString()
            && napi_get_value_string_utf8(env, key, stack, capacity, &written)
                == napi_ok
            && written + 1 < capacity) {
        storage->assign(stack, written);
        have_text = true;
    }

    if (have_text) {
        const char c = storage->empty() ? '\0' : (*storage)[0];
        const bool could_be_numeric = c == '\0'
            || (c >= '0' && c <= '9')
            || c == '-' || c == '+' || c == '.'
            || c == ' ' || c == '\t' || c == '\n' || c == '\r'
            || c == '\f' || c == '\v';
        if (!could_be_numeric) {
            out->positional = false;
            out->name = storage->c_str();
            return true;
        }
    }

    // Either an unusual key, or one that really might be a number: fall
    // back to the coercion, which is the definitive test.
    Napi::Number num = key.ToNumber();
    if (env.IsExceptionPending()) return false;
    // Bind positions are 1-based, so a key reading as zero or a negative
    // number selects nothing. Such a key is treated as a name, which is
    // what it literally is, and reported as an unknown one — the async
    // path cannot represent a zero position either, so this is also what
    // keeps the two implementations wording it the same way.
    if (num.Int32Value() == num.DoubleValue() && num.Int32Value() > 0) {
        out->positional = true;
        out->index = num.Int32Value();
        return true;
    }
    if (!have_text) {
        *storage = key.As<Napi::String>().Utf8Value();
        if (env.IsExceptionPending()) return false;
    }
    out->positional = false;
    out->name = storage->c_str();
    return true;
}

// True for a plain-object instance of the named global constructor
// ("Date", "RegExp"), matching JS instanceof across realms.
bool OtherInstanceOf(Napi::Object source, const char* object_type) {
    auto env = source.Env();
    auto* addon = env.GetInstanceData<Database::AddonData>();
    Napi::Function ctor;
    if (strncmp(object_type, "Date", 4) == 0) {
        ctor = GlobalCtor(env, &addon->date_ctor, "Date");
    } else if (strncmp(object_type, "RegExp", 6) == 0) {
        ctor = GlobalCtor(env, &addon->regexp_ctor, "RegExp");
    }
    if (ctor.IsEmpty()) return false;
    return source.InstanceOf(ctor);
}

std::unique_ptr<Values::Field> ConvertToField(const Napi::Value source,
        const std::string& subject, const FieldPos& pos) {
    // Exhaustive dispatch. Order matters for the hot path: cheap primitive
    // checks run before the object checks (InstanceOf lookups hit the
    // global object). Every JS type either maps to a field or throws —
    // returning nullptr therefore always implies a pending exception, so
    // nothing can silently skip a value. The constructors carry the bind
    // position (index or name) through to Bind(Parameters&&, bool).
#define MAKE_FIELD(kind, ...)     (pos.index > 0         ? std::make_unique<Values::kind>(pos.index, __VA_ARGS__)         : std::make_unique<Values::kind>(pos.name, __VA_ARGS__))
    // One napi_typeof up front, then compare against it. Each of
    // node-addon-api's IsNumber()/IsString()/... calls napi_typeof itself,
    // so an if-else chain over them pays that ABI call once per arm it
    // tries — six times over for a value that turns out to be a BigInt.
    napi_valuetype vtype = napi_undefined;
    if (napi_typeof(source.Env(), source, &vtype) != napi_ok) {
        return nullptr;
    }
    if (vtype == napi_number) {
        double val = source.As<Napi::Number>().DoubleValue();
        // Number.isInteger within the int64 range binds as INTEGER (64-bit,
        // not the old Int32 round-trip). NaN and ±Infinity fail the
        // trunc/finiteness test and bind as REAL (NaN becomes NULL, per
        // sqlite's bind_double semantics).
        if (std::isfinite(val) && val == std::trunc(val)
                && val >= kInt64MinAsDouble && val < kInt64MaxAsDouble) {
            return MAKE_FIELD(Integer, static_cast<int64_t>(val));
        }
        if (val == kInt64MaxAsDouble) {
            // 2^63 as a double is the rounded form of 2^63-1: clamp so the
            // top of the int64 range stays reachable from JS numbers.
            return MAKE_FIELD(Integer, INT64_MAX);
        }
        return MAKE_FIELD(Float, val);
    }
    else if (vtype == napi_string) {
        std::string val = source.As<Napi::String>().Utf8Value();
        return MAKE_FIELD(Text, val.length(), val.c_str());
    }
    else if (vtype == napi_boolean) {
        return MAKE_FIELD(Integer, source.As<Napi::Boolean>().Value() ? 1 : 0);
    }
    else if (vtype == napi_null) {
        return pos.index > 0
            ? std::make_unique<Values::Null>(pos.index)
            : std::make_unique<Values::Null>(pos.name);
    }
    else if (vtype == napi_undefined) {
        // Binds as NULL, matching null: object shorthand
        // { $x: obj.maybeMissing } is a common call shape. Typo'd property
        // names are caught by the named-parameter and arity checks in
        // Bind(Parameters&&, bool), not by rejecting undefined.
        auto field = pos.index > 0
            ? std::make_unique<Values::Null>(pos.index)
            : std::make_unique<Values::Null>(pos.name);
        field->from_undefined = true;
        return field;
    }
    else if (vtype == napi_bigint) {
        bool lossless = false;
        int64_t val = source.As<Napi::BigInt>().Int64Value(&lossless);
        if (!lossless) {
            std::string digits = source.ToString().Utf8Value();
            Napi::RangeError::New(source.Env(),
                "Cannot bind " + subject + ": BigInt " +
                digits + " is outside the signed 64-bit integer range"
            ).ThrowAsJavaScriptException();
            return nullptr;
        }
        return MAKE_FIELD(Integer, val);
    }
    else if (vtype != napi_object) {
        // Symbols, functions, external values.
        ThrowUnsupportedBindType(source, subject);
        return nullptr;
    }
    else if (source.IsDataView()) {
        // Must be tested before IsBuffer(): napi_is_buffer() also answers
        // true for DataViews, and routing one through Napi::Buffer fails
        // ("Invalid argument"). data arrives already offset by
        // byte_offset, like the typed-array call.
        size_t bytes = 0;
        void* data = NULL;
        napi_get_dataview_info(source.Env(), source, &bytes, &data,
            NULL, NULL);
        if (bytes > static_cast<size_t>(std::numeric_limits<int>::max())) {
            Napi::RangeError::New(source.Env(),
                "Cannot bind " + subject + ": DataView of " +
                std::to_string(bytes) + " bytes exceeds the bind size limit"
            ).ThrowAsJavaScriptException();
            return nullptr;
        }
        return MAKE_FIELD(Blob, bytes, data);
    }
    else if (source.IsBuffer()) {
        // Node Buffers and plain Uint8Arrays: Data() and Length() honour
        // byteOffset for both.
        Napi::Buffer<char> buffer = source.As<Napi::Buffer<char>>();
        if (buffer.Length() > static_cast<size_t>(std::numeric_limits<int>::max())) {
            // Buffers can exceed 2 GB on 64-bit Node; sqlite3_bind_blob takes
            // an int, so without this the length narrows to a negative number.
            Napi::RangeError::New(source.Env(),
                "Cannot bind " + subject + ": Buffer of " +
                std::to_string(buffer.Length()) + " bytes exceeds the bind size limit"
            ).ThrowAsJavaScriptException();
            return nullptr;
        }
        return MAKE_FIELD(Blob, buffer.Length(), buffer.Data());
    }
    else if (source.IsTypedArray()) {
        // Any other typed array (Uint16Array, Float64Array, ...): raw
        // element count times the element size, with the data pointer
        // already offset by byteOffset per napi_get_typedarray_info.
        napi_typedarray_type type;
        size_t elements = 0;
        void* data = NULL;
        napi_get_typedarray_info(source.Env(), source, &type,
            &elements, &data, NULL, NULL);
        size_t bytes = elements * TypedArrayElementSize(type);
        if (bytes > static_cast<size_t>(std::numeric_limits<int>::max())) {
            Napi::RangeError::New(source.Env(),
                "Cannot bind " + subject + ": typed array of " +
                std::to_string(bytes) + " bytes exceeds the bind size limit"
            ).ThrowAsJavaScriptException();
            return nullptr;
        }
        return MAKE_FIELD(Blob, bytes, data);
    }
    else if (source.IsArrayBuffer()) {
        Napi::ArrayBuffer buffer = source.As<Napi::ArrayBuffer>();
        if (buffer.ByteLength() > static_cast<size_t>(std::numeric_limits<int>::max())) {
            Napi::RangeError::New(source.Env(),
                "Cannot bind " + subject + ": ArrayBuffer exceeds the bind size limit"
            ).ThrowAsJavaScriptException();
            return nullptr;
        }
        return MAKE_FIELD(Blob, buffer.ByteLength(), buffer.Data());
    }
    else if (source.IsDate()) {
        // Documented v8/v9 behaviour: epoch milliseconds as REAL. Opt-in
        // TEXT binding is deliberately out of scope for D02.
        return MAKE_FIELD(Float, source.As<Napi::Date>().ValueOf());
    }
    else if (source.IsObject()) {
        if (OtherInstanceOf(source.As<Napi::Object>(), "RegExp")) {
            std::string val = source.ToString().Utf8Value();
            return MAKE_FIELD(Text, val.length(), val.c_str());
        }
        // Plain objects, arrays, Maps, class instances: refused. The old
        // behaviour bound the literal string "[object Object]".
        ThrowUnsupportedBindType(source, subject);
        return nullptr;
    }
    // Symbols, functions, anything else.
    ThrowUnsupportedBindType(source, subject);
    return nullptr;
#undef MAKE_FIELD
}

std::string BindSubject::Describe() const {
    if (name != nullptr) return std::string("parameter ") + name;
    return "parameter " + std::to_string(index);
}

bool BindValueDirect(sqlite3_stmt* stmt, int pos, const Napi::Value source,
        const BindSubject& subject, int* rc, bool* from_undefined) {
    auto env = source.Env();

    // One napi_typeof, then a switch. node-addon-api's IsNumber()/
    // IsString()/... each call napi_typeof, so an if-else chain over them
    // pays that ABI call once per arm it tries.
    napi_valuetype type = napi_undefined;
    if (napi_typeof(env, source, &type) != napi_ok) return false;

    switch (type) {
        case napi_number: {
            const double val = source.As<Napi::Number>().DoubleValue();
            // Number.isInteger within the int64 range binds as INTEGER;
            // NaN and +/-Infinity fail the finiteness test and bind REAL.
            if (std::isfinite(val) && val == std::trunc(val)
                    && val >= kInt64MinAsDouble && val < kInt64MaxAsDouble) {
                *rc = sqlite3_bind_int64(stmt, pos, static_cast<int64_t>(val));
                return true;
            }
            if (val == kInt64MaxAsDouble) {
                // 2^63 as a double is the rounded form of 2^63-1: clamp so
                // the top of the range stays reachable from a JS number.
                *rc = sqlite3_bind_int64(stmt, pos, INT64_MAX);
                return true;
            }
            *rc = sqlite3_bind_double(stmt, pos, val);
            return true;
        }
        case napi_string: {
            // Two napi calls (length, then write) is the documented way to
            // read a string of unknown length; the payload is malloc'd
            // once and handed to SQLite rather than copied into a
            // std::string and then copied again.
            size_t length = 0;
            if (napi_get_value_string_utf8(env, source, NULL, 0, &length)
                    != napi_ok) {
                return false;
            }
            char* buffer = static_cast<char*>(malloc(length + 1));
            if (buffer == NULL) {
                Napi::Error::New(env, "Cannot bind " + subject.Describe() +
                    ": out of memory").ThrowAsJavaScriptException();
                return false;
            }
            size_t written = 0;
            if (napi_get_value_string_utf8(env, source, buffer, length + 1,
                    &written) != napi_ok) {
                free(buffer);
                return false;
            }
            *rc = sqlite3_bind_text64(stmt, pos, buffer,
                static_cast<sqlite3_uint64>(written), free, SQLITE_UTF8);
            return true;
        }
        case napi_boolean: {
            *rc = sqlite3_bind_int64(stmt, pos,
                source.As<Napi::Boolean>().Value() ? 1 : 0);
            return true;
        }
        case napi_null: {
            *rc = sqlite3_bind_null(stmt, pos);
            return true;
        }
        case napi_undefined: {
            // Binds as NULL, like the Field path; the caller uses
            // `from_undefined` for the zero-parameter call shape.
            if (from_undefined != NULL) *from_undefined = true;
            *rc = sqlite3_bind_null(stmt, pos);
            return true;
        }
        case napi_bigint: {
            bool lossless = false;
            const int64_t val =
                source.As<Napi::BigInt>().Int64Value(&lossless);
            if (!lossless) {
                const std::string digits = source.ToString().Utf8Value();
                Napi::RangeError::New(env,
                    "Cannot bind " + subject.Describe() + ": BigInt " +
                    digits + " is outside the signed 64-bit integer range"
                ).ThrowAsJavaScriptException();
                return false;
            }
            *rc = sqlite3_bind_int64(stmt, pos, val);
            return true;
        }
        case napi_object:
            break;      // handled below
        default:
            // Symbols, functions, external values.
            ThrowUnsupportedBindType(source, subject.Describe());
            return false;
    }

    // Binary views. SQLITE_TRANSIENT: SQLite copies before returning, so
    // the JS-owned bytes need not outlive this call.
    const void* data = NULL;
    size_t bytes = 0;
    const char* kind = NULL;
    if (source.IsDataView()) {
        // Before IsBuffer(): napi_is_buffer() also answers true for a
        // DataView, and routing one through Napi::Buffer fails.
        napi_get_dataview_info(env, source, &bytes,
            const_cast<void**>(&data), NULL, NULL);
        kind = "DataView";
    }
    else if (source.IsBuffer()) {
        // Node Buffers and plain Uint8Arrays: Data() and Length() honour
        // byteOffset for both.
        Napi::Buffer<char> buffer = source.As<Napi::Buffer<char>>();
        data = buffer.Data();
        bytes = buffer.Length();
        kind = "Buffer";
    }
    else if (source.IsTypedArray()) {
        napi_typedarray_type ta_type;
        size_t elements = 0;
        napi_get_typedarray_info(env, source, &ta_type, &elements,
            const_cast<void**>(&data), NULL, NULL);
        bytes = elements * TypedArrayElementSize(ta_type);
        kind = "typed array";
    }
    else if (source.IsArrayBuffer()) {
        Napi::ArrayBuffer buffer = source.As<Napi::ArrayBuffer>();
        data = buffer.Data();
        bytes = buffer.ByteLength();
        kind = "ArrayBuffer";
    }

    if (kind != NULL) {
        if (bytes > static_cast<size_t>(std::numeric_limits<int>::max())) {
            // Views can exceed 2 GB on 64-bit Node; the wording matches
            // the Field path per shape.
            const std::string what = std::strcmp(kind, "ArrayBuffer") == 0
                ? "ArrayBuffer exceeds the bind size limit"
                : std::string(kind) + " of " + std::to_string(bytes) +
                    " bytes exceeds the bind size limit";
            Napi::RangeError::New(env,
                "Cannot bind " + subject.Describe() + ": " + what
            ).ThrowAsJavaScriptException();
            return false;
        }
        // A zero-length view can carry a NULL data pointer, and
        // sqlite3_bind_blob64(NULL, 0) binds SQL NULL rather than an empty
        // blob. The Field path always had a real allocation behind it, so
        // an empty Buffer round-tripped as an empty blob; keep that by
        // handing SQLite a non-NULL pointer it will read zero bytes from.
        *rc = sqlite3_bind_blob64(stmt, pos, data != NULL ? data : "",
            static_cast<sqlite3_uint64>(bytes), SQLITE_TRANSIENT);
        return true;
    }

    if (source.IsDate()) {
        // Documented v8/v9 behaviour: epoch milliseconds as REAL.
        *rc = sqlite3_bind_double(stmt, pos,
            source.As<Napi::Date>().ValueOf());
        return true;
    }
    if (OtherInstanceOf(source.As<Napi::Object>(), "RegExp")) {
        const std::string val = source.ToString().Utf8Value();
        *rc = sqlite3_bind_text64(stmt, pos, val.c_str(),
            static_cast<sqlite3_uint64>(val.length()),
            SQLITE_TRANSIENT, SQLITE_UTF8);
        return true;
    }
    // Plain objects, arrays, Maps, class instances: refused.
    ThrowUnsupportedBindType(source, subject.Describe());
    return false;
}

void ValueToCell(Cell* cell, sqlite3_value* value) {
    cell->str.clear();
    cell->integer = 0;
    cell->real = 0.;
    switch (sqlite3_value_type(value)) {
        case SQLITE_INTEGER: {
            cell->type = SQLITE_INTEGER;
            cell->integer = sqlite3_value_int64(value);
        }   break;
        case SQLITE_FLOAT: {
            cell->type = SQLITE_FLOAT;
            cell->real = sqlite3_value_double(value);
        }   break;
        case SQLITE_TEXT: {
            cell->type = SQLITE_TEXT;
            // sqlite3_value_text first, then sqlite3_value_bytes: the docs
            // require that order so the UTF-8 conversion happens before the
            // length is read.
            const char* text =
                reinterpret_cast<const char*>(sqlite3_value_text(value));
            int length = sqlite3_value_bytes(value);
            if (length > 0 && text != NULL) {
                cell->str.assign(text, length);
            }
        } break;
        case SQLITE_BLOB: {
            cell->type = SQLITE_BLOB;
            const void* blob = sqlite3_value_blob(value);
            int length = sqlite3_value_bytes(value);
            if (length > 0 && blob != NULL) {
                cell->str.assign(static_cast<const char*>(blob), length);
            }
        } break;
        default: {
            cell->type = SQLITE_NULL;
        }
    }
}

std::string ValueOrigin::Describe() const {
    if (literal != nullptr) return *literal;
    if (literal_cstr != nullptr) return std::string(literal_cstr);
    if (column_names != nullptr && index < column_names->size()) {
        return "column '" + (*column_names)[index] + "'";
    }
    return "result column " + std::to_string(index);
}

Napi::Value ConvertInt64ToJS(Napi::Env env, sqlite3_int64 value,
        int integer_mode, const ValueOrigin& origin) {
    // A single range compare on the int64 — deliberately not a call into
    // JS: this runs per integer cell.
    const bool safe = value >= -(1LL << 53) + 1 && value < (1LL << 53);
    switch (integer_mode) {
        case Database::INTEGER_BIGINT:
            return Napi::BigInt::New(env, static_cast<int64_t>(value));
        case Database::INTEGER_MIXED:
            if (!safe)
                return Napi::BigInt::New(env, static_cast<int64_t>(value));
            return Napi::Number::New(env, static_cast<double>(value));
        default:
            if (safe) return Napi::Number::New(env, static_cast<double>(value));
            // The default throws instead of truncating: a silent double
            // conversion is exactly the corruption this mode guards
            // against. The callback-free sync paths surface this directly;
            // async completions deliver it to the user callback.
            Napi::RangeError::New(env,
                "Integer " + std::to_string(value) + " in " +
                    origin.Describe() +
                " is outside the safe integer range (-(2^53-1) .. 2^53-1); "
                "configure('integerMode', 'bigint' | 'mixed') to read it "
                "exactly"
            ).ThrowAsJavaScriptException();
            return env.Null();
    }
}

Napi::Value ColumnToJS(Napi::Env env, sqlite3_stmt* stmt, int column,
        int integer_mode, const ValueOrigin& origin, bool* raised) {
    switch (sqlite3_column_type(stmt, column)) {
        case SQLITE_INTEGER: {
            // The one branch that can raise (the 'number'-mode RangeError
            // for an unsafe int64): report it through `raised` so the row
            // loop stays free of napi_is_exception_pending calls.
            const sqlite3_int64 value = sqlite3_column_int64(stmt, column);
            const bool safe = value >= -(1LL << 53) + 1 && value < (1LL << 53);
            if (integer_mode == Database::INTEGER_NUMBER && !safe) {
                if (raised != NULL) *raised = true;
            }
            return ConvertInt64ToJS(env, value, integer_mode, origin);
        }
        case SQLITE_FLOAT: {
            return Napi::Number::New(env,
                sqlite3_column_double(stmt, column));
        }
        case SQLITE_TEXT: {
            const char* text = reinterpret_cast<const char*>(
                sqlite3_column_text(stmt, column));
            const int length = sqlite3_column_bytes(stmt, column);
            if (text == NULL || length <= 0) {
                return Napi::String::New(env, "", 0);
            }
            return Napi::String::New(env, text, static_cast<size_t>(length));
        }
        case SQLITE_BLOB: {
            const char* blob = reinterpret_cast<const char*>(
                sqlite3_column_blob(stmt, column));
            const int length = sqlite3_column_bytes(stmt, column);
            if (blob == NULL || length <= 0) {
                return Napi::Buffer<char>::Copy(env, "", 0);
            }
            return Napi::Buffer<char>::Copy(env, blob,
                static_cast<size_t>(length));
        }
        default: {
            // SQLITE_NULL, and anything unexpected, as the Cell path does.
            return env.Null();
        }
    }
}

Napi::Value CellToJS(Napi::Env env, Cell& cell, int integer_mode,
        const ValueOrigin& origin, bool move_payload, bool* raised) {
    switch (cell.type) {
        case SQLITE_INTEGER: {
            // The one branch that can raise; see ColumnToJS for why the
            // failure travels in a bool rather than through the env.
            const bool safe = cell.integer >= -(1LL << 53) + 1
                && cell.integer < (1LL << 53);
            if (integer_mode == Database::INTEGER_NUMBER && !safe) {
                if (raised != NULL) *raised = true;
            }
            return ConvertInt64ToJS(env, cell.integer, integer_mode, origin);
        }
        case SQLITE_FLOAT: {
            return Napi::Number::New(env, cell.real);
        }
        case SQLITE_TEXT: {
            return Napi::String::New(env, cell.str.data(), cell.str.size());
        }
        case SQLITE_BLOB: {
            // Zero-copy for large blobs: transfer ownership of the bytes
            // to the Buffer finalizer. Small blobs are cheaper to copy
            // (external-buffer bookkeeping outweighs the memcpy), and the
            // copy fallback also covers environments without external
            // buffer support (e.g. sandboxed renderers).
            if (move_payload && cell.str.size() >= 4096) {
                auto* payload = new std::string(std::move(cell.str));
                napi_value buf = NULL;
                napi_status st = napi_create_external_buffer(env,
                    payload->size(), &(*payload)[0],
                    [](napi_env, void*, void* hint) {
                        delete static_cast<std::string*>(hint);
                    },
                    payload, &buf);
                if (st == napi_ok) {
                    return Napi::Buffer<char>(env, buf);
                }
                // No external-buffer support (e.g. sandboxed renderers):
                // fall back to a copy from the payload we still hold.
                Napi::Buffer<char> copy = Napi::Buffer<char>::Copy(env,
                    payload->data(), payload->size());
                delete payload;
                return copy;
            }
            return Napi::Buffer<char>::Copy(env,
                cell.str.data(), cell.str.size());
        }
        case SQLITE_NULL: {
            return env.Null();
        }
        default:
            return env.Null();
    }
}

} // namespace node_sqlite3
