Files
SnowLuma/.pnpm-store/v11/files/ea/134e5efa4fa21df924bbdbee275cd24894993585a447c83312bde325900f70377b4d68991f0b41a2d2e1b7652615220df0232a59c071e464c479e98b84cd92
TanTan 2a69b23b47
Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

2725 lines
100 KiB
Plaintext

#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;
}
}