Files
SnowLuma/.pnpm-store/v11/files/ff/98dfb188b0146185da66d546a687293e03627252c71342b231374d90e37386b9b9ed7f645aac3cee47b2a1dd27833ba69312cc316767b9a87c47ad56b09171
T
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) Skipped
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

590 lines
25 KiB
Plaintext

#ifndef NODE_SQLITE3_SRC_STATEMENT_H
#define NODE_SQLITE3_SRC_STATEMENT_H
#include <cstdlib>
#include <cstring>
#include <string>
#include <queue>
#include <vector>
#include <sqlite3.h>
#include <napi.h>
#include <uv.h>
#include "convert.h"
#include "database.h"
#include "threading.h"
using namespace Napi;
namespace node_sqlite3 {
class Statement : public Napi::ObjectWrap<Statement> {
public:
// Row shape requested from the synchronous read paths. SYNC_ROW_OBJECT
// is the historical default (plain objects, result-column order,
// last-duplicate-wins); SYNC_ROW_ARRAY is the bulk-reader opt-in
// (`{ rowMode: 'array' }`), which skips the per-cell property stores
// entirely — napi_set_element on a pre-sized array has no shape to
// transition, which is what makes it the fastest row we can build.
// SYNC_ROW_PLUCK (`{ rowMode: 'pluck' }`) serves only the first result
// column — better-sqlite3's pluck(), as a per-call option.
enum SyncRowMode {
SYNC_ROW_OBJECT = 0,
SYNC_ROW_ARRAY = 1,
SYNC_ROW_PLUCK = 2,
};
static Napi::Object Init(Napi::Env env, Napi::Object exports);
static Napi::Value New(const Napi::CallbackInfo& info);
// Cross-class argument validation (e.g. "is this a Statement?"),
// against this env's constructor — see Database::HasInstanceIn.
static inline bool HasInstance(Napi::Value val) {
return Database::HasInstanceIn(val,
&Database::AddonData::statement_ctor);
}
friend class Database;
// Runs the finalize work of an original finalize baton (directly, or
// via the deferred wrappers below).
struct Baton;
static void FinishFinalizeBaton(Baton* baton);
struct Baton {
napi_async_work request = NULL;
Statement* stmt;
Napi::FunctionReference callback;
Parameters parameters;
// True when the call site passed a bind argument at all (possibly
// an empty array/object). False means "re-step with the previous
// bindings", which must skip the parameter-count check.
bool bind_supplied = false;
Baton(Statement* stmt_, Napi::Function cb_) : stmt(stmt_) {
stmt->Ref();
callback.Reset(cb_, 1);
}
// Delivers `error` to whichever callback settles this call, used
// by CleanQueue when the statement is torn down with work still
// queued. Returns false when the call has no callback to settle,
// so the caller can fall back to an 'error' event. For most calls
// that callback is `callback`; each() overrides this, because
// there `callback` is the per-row callback and firing it would
// hand the caller a phantom row.
virtual bool Fail(Napi::Value error) {
Napi::Function cb = callback.Value();
if (cb.IsEmpty() || !cb.IsFunction()) return false;
Napi::Value argv[] = { error };
// A throwing callback still counts as settled: it ran.
TRY_CATCH_CALL(stmt->Value(), cb, 1, argv, true);
return true;
}
virtual ~Baton() {
parameters.clear();
if (request) napi_delete_async_work(stmt->Env(), request);
stmt->Unref();
callback.Reset();
}
};
struct RowBaton : Baton {
RowBaton(Statement* stmt_, Napi::Function cb_) :
Baton(stmt_, cb_) {}
Row row;
Columns columns;
// Row shape requested through a trailing `{ rowMode: ... }` options
// bag on the asynchronous read paths (Phase 1 ergonomics parity):
// the same SYNC_ROW_* values the synchronous paths use.
int row_mode = SYNC_ROW_OBJECT;
virtual ~RowBaton() override = default;
};
struct RunBaton : Baton {
RunBaton(Statement* stmt_, Napi::Function cb_) :
Baton(stmt_, cb_), inserted_id(0), changes(0) {}
sqlite3_int64 inserted_id;
int changes;
virtual ~RunBaton() override = default;
};
struct RowsBaton : Baton {
RowsBaton(Statement* stmt_, Napi::Function cb_) :
Baton(stmt_, cb_) {}
Rows rows;
Columns columns;
// See RowBaton::row_mode.
int row_mode = SYNC_ROW_OBJECT;
virtual ~RowsBaton() override = default;
};
// fetch(count, ...): like all(), but steps at most `count` rows so a
// pull-based iterator can apply backpressure. The statement is not
// reset between fetches; the cursor keeps its position.
struct FetchBaton : RowsBaton {
FetchBaton(Statement* stmt_, Napi::Function cb_) :
RowsBaton(stmt_, cb_), count(1), done(false) {}
int count;
// True when sqlite3_step returned SQLITE_DONE: the cursor is
// exhausted and only a rebind can produce more rows.
bool done;
virtual ~FetchBaton() override = default;
};
struct Async;
struct EachBaton : Baton {
Napi::FunctionReference completed;
Async* async; // Isn't deleted when the baton is deleted.
// See RowBaton::row_mode (published into the Async watcher).
int row_mode = SYNC_ROW_OBJECT;
EachBaton(Statement* stmt_, Napi::Function cb_) :
Baton(stmt_, cb_) {}
// When each() was given a completion handler that handler settles
// the call, and takes (err, rowCount). Without one the per-row
// callback is the only thing the caller passed, so the error goes
// there instead — never to both, and never a row-shaped call with
// no error.
virtual bool Fail(Napi::Value error) override {
Napi::Function done = completed.Value();
if (!done.IsEmpty() && done.IsFunction()) {
Napi::Value argv[] = {
error, Napi::Number::New(error.Env(), 0)
};
TRY_CATCH_CALL(stmt->Value(), done, 2, argv, true);
return true;
}
return Baton::Fail(error);
}
virtual ~EachBaton() override {
completed.Reset();
}
};
struct PrepareBaton : Database::Baton {
Statement* stmt;
std::string sql;
PrepareBaton(Database* db_, Napi::Function cb_, Statement* stmt_) :
Baton(db_, cb_), stmt(stmt_) {
stmt->Ref();
}
virtual ~PrepareBaton() override {
stmt->Unref();
if (db->IsClosed()) {
// The database handle was closed before the statement could be
// prepared.
stmt->Finalize_();
}
}
};
typedef void (*Work_Callback)(Baton* baton);
struct Call {
Call(Work_Callback cb_, Baton* baton_) : callback(cb_), baton(baton_) {};
Work_Callback callback;
Baton* baton;
};
// Deferral wrappers for the main-thread finalize paths (see
// Database::MayBlockOnWorkerRoundTrip): they carry the original work
// through the database's exclusive queue so the sqlite3_finalize runs
// on this thread only once no worker can hold the connection mutex.
struct DeferredFinalizeBaton : Database::Baton {
Statement::Baton* inner;
DeferredFinalizeBaton(Database* db_, Statement::Baton* inner_) :
Baton(db_, Napi::Function()), inner(inner_) {}
virtual ~DeferredFinalizeBaton() override {
delete inner;
}
};
struct HandleFinalizeBaton : Database::Baton {
sqlite3_stmt* handle;
HandleFinalizeBaton(Database* db_, sqlite3_stmt* handle_) :
Baton(db_, Napi::Function()), handle(handle_) {}
virtual ~HandleFinalizeBaton() override = default;
};
struct Async {
uv_async_t watcher;
Statement* stmt;
Rows data;
// Column names for the rows currently in `data`. Written by the
// worker thread and read by the main thread, both under the mutex
// below, so a mid-stream re-prepare cannot be missed.
Columns columns;
// Row shape for the delivered rows (EachBaton::row_mode), set once
// before the work starts and read on the JS thread only.
int row_mode = SYNC_ROW_OBJECT;
NODE_SQLITE3_MUTEX_t;
bool completed;
int retrieved;
// Store the callbacks here because we don't have
// access to the baton in the async callback.
Napi::FunctionReference item_cb;
Napi::FunctionReference completed_cb;
Async(Statement* st, uv_async_cb async_cb) :
stmt(st), completed(false), retrieved(0) {
watcher.data = this;
NODE_SQLITE3_MUTEX_INIT
stmt->Ref();
uv_loop_t *loop;
napi_get_uv_event_loop(stmt->Env(), &loop);
uv_async_init(loop, &watcher, async_cb);
}
~Async() {
stmt->Unref();
item_cb.Reset();
completed_cb.Reset();
NODE_SQLITE3_MUTEX_DESTROY
}
};
Statement(const Napi::CallbackInfo& info);
// Finalize-on-GC safety net: tears down a collected statement that
// was never finalized. Runs in the ObjectWrap finalizer, so it must
// not call into JS; see the definition in statement.cc for why each
// step is safe there.
~Statement();
WORK_DEFINITION(Bind)
WORK_DEFINITION(Get)
WORK_DEFINITION(Run)
WORK_DEFINITION(All)
WORK_DEFINITION(Each)
WORK_DEFINITION(Reset)
WORK_DEFINITION(Fetch)
Napi::Value Finalize_(const Napi::CallbackInfo& info);
// End-of-call bookkeeping for an asynchronous statement operation.
void EndCall();
// Runs EndCall() on every exit path from a Work_After* handler. This
// matters because TRY_CATCH_CALL returns early when a JS callback
// throws: doing the bookkeeping inline would then be skipped, leaving
// `locked` set and db->pending elevated forever. The statement's queue
// would never drain again and the sync fast path's idle gate could
// never pass, so one throwing callback would permanently disable
// getSync/runSync/allSync on that connection.
struct CallGuard {
Statement* stmt;
explicit CallGuard(Statement* s) : stmt(s) {}
~CallGuard() { stmt->EndCall(); }
CallGuard(const CallGuard&) = delete;
CallGuard& operator=(const CallGuard&) = delete;
};
// Opt-in synchronous fast path. Only legal when the database is fully
// idle (no worker in flight, nothing queued); otherwise throws.
Napi::Value GetSync(const Napi::CallbackInfo& info);
Napi::Value RunSync(const Napi::CallbackInfo& info);
Napi::Value AllSync(const Napi::CallbackInfo& info);
// Mode-aware accessors for the result of the last run(). lastID throws
// a RangeError in 'number' mode when the rowid is not a safe integer;
// lastIDBigInt is exact in every mode. changes is always a safe number.
Napi::Value GetLastID(const Napi::CallbackInfo& info);
Napi::Value GetLastIDBigInt(const Napi::CallbackInfo& info);
Napi::Value GetChanges(const Napi::CallbackInfo& info);
// True once the statement has been finalized: explicitly, after a
// failed prepare, or by the GC safety net. Operations on a finalized
// statement fail with SQLITE_MISUSE.
Napi::Value FinalizedGetter(const Napi::CallbackInfo& info);
// --- Introspection (Deliverable 07). readonly/parameterCount/
// parameterNames/columns serve a snapshot taken once, under the
// connection mutex, so the accessors never touch the sqlite3_stmt*
// and cannot race a stepping worker or a round-tripping JS callback.
// The sqlite3_stmt result shape does not change across a transparent
// re-prepare, so the snapshot holds for the statement's lifetime.
// status() reads live counters and takes the mutex instead, refusing
// while a JS round trip could hold it.
//
// Taking the snapshot and publishing it are deliberately separate.
// The async prepare runs on a libuv worker, and the accessors run on
// the JS thread: had the worker written the accessor-visible fields
// directly, a getter called during the prepare window would walk a
// std::vector mid-push_back — a dangling read, not merely a stale
// one. So SnapshotMetadata() (preparing thread, mutex held) fills
// pending_meta, and PublishMetadata() (JS thread only) moves it into
// place. The napi async-work completion that calls PublishMetadata()
// is ordered after the worker, so the move needs no atomics.
struct ColumnMeta {
std::string name;
std::string decltype_; // empty = none (expression/alias columns)
std::string database; // SQLITE_ENABLE_COLUMN_METADATA only
std::string table;
std::string origin;
};
struct Metadata {
bool readonly = false;
int param_count = 0;
std::vector<std::string> param_names; // "" entries: unnamed (?)
std::vector<ColumnMeta> columns;
};
void SnapshotMetadata();
void PublishMetadata();
Napi::Value ReadonlyGetter(const Napi::CallbackInfo& info);
Napi::Value ParameterCountGetter(const Napi::CallbackInfo& info);
Napi::Value ParameterNamesGetter(const Napi::CallbackInfo& info);
Napi::Value ColumnsGetter(const Napi::CallbackInfo& info);
Napi::Value Status(const Napi::CallbackInfo& info);
// SQL text accessors (Phase 6): the statement's SQL with bound values
// substituted (sqlite3_expanded_sql) and with literals normalized to
// `?` (sqlite3_normalized_sql, needs SQLITE_ENABLE_NORMALIZE). Both
// read the live statement, so they refuse while a worker round trip
// could hold the connection mutex.
Napi::Value ExpandedSQLGetter(const Napi::CallbackInfo& info);
Napi::Value NormalizedSQLGetter(const Napi::CallbackInfo& info);
// Shared refusal prelude of the two SQL accessors: true when the live
// statement may be read right now.
bool SQLAccessorGate(Napi::Env env);
// Per-statement integer-mode override (Phase 6 parity): 0 = none
// (follow the connection's mode), otherwise one of INTEGER_NUMBER /
// INTEGER_BIGINT / INTEGER_MIXED. Applied by the JS layer right after
// construction, so it is only ever read on the JS thread (all row
// conversion happens there).
Napi::Value SetIntegerMode(const Napi::CallbackInfo& info);
int EffectiveIntegerMode() const {
return has_integer_override ? integer_mode_override
: db->integer_mode;
}
protected:
static void Work_BeginPrepare(Database::Baton* baton);
static void Work_Prepare(napi_env env, void* data);
static void Work_AfterPrepare(napi_env env, napi_status status, void* data);
static void AsyncEach(uv_async_t* handle);
static void CloseCallback(uv_handle_t* handle);
static void Finalize_(Baton* baton);
void Finalize_();
template <class T> inline std::unique_ptr<Values::Field> BindParameter(const Napi::Value source, T pos);
// Binds [start, end) of the call into a baton, popping a trailing
// callback into the baton unless `preset_callback` carries one
// (Get/All/Each/Fetch pop it themselves when a `{ rowMode }` options
// bag sits between the bind parameters and the callback).
template <class T> T* Bind(const Napi::CallbackInfo& info, int start = 0,
int end = -1, Napi::Function preset_callback = Napi::Function());
// The bind-argument shapes (one array / N positional / one named
// object), shared by Bind<T> (into a Baton, for the queued async
// paths) and called directly by the synchronous fast paths, which
// have no Baton to fill. Returns false with a pending exception on
// unsupported values or a malformed shape.
bool ParseBindArguments(const Napi::CallbackInfo& info, int start,
int last, Parameters* parameters);
bool Bind(Parameters&& parameters, bool supplied);
// The synchronous counterpart of ParseBindArguments + Bind: applies the
// call's bind arguments straight onto the statement, with no
// Parameters vector and no heap Values::Field per parameter.
//
// Accepts the same three argument shapes, performs the same arity and
// named-parameter checks, produces the same error text, and leaves the
// statement in the same state on failure. The asynchronous paths keep
// the Field route: they read the arguments on the JS thread and apply
// them on a worker, so there the materialisation is the hand-off.
bool BindArgumentsDirect(const Napi::CallbackInfo& info, int start,
int last, bool supplied);
static void GetRow(Row* row, sqlite3_stmt* stmt, Columns* columns);
// Rebuilds the rooted JS key strings if `columns` differs from the set
// they were built from. Call once per batch, before RowToJS.
void SyncColumnKeys(Napi::Env env, const Columns& columns);
Napi::Value RowToJS(Napi::Env env, Row* row,
int row_mode = SYNC_ROW_OBJECT);
// The scopeless core of RowToJS, and the asynchronous counterpart of
// ConvertCurrentRow: converts one already-materialised Row into the
// caller's HandleScope. Returns false when the 'number'-mode RangeError
// left a pending exception. Callers must store `*out` into a rooted JS
// object before their scope closes.
bool ConvertCellRow(Napi::Env env, Row* row,
const std::vector<napi_value>& keys, int row_mode, napi_value* out);
// Converts a whole materialised result set into a JS array, resolving
// the column keys once and opening one HandleScope per batch of rows
// rather than one per row.
//
// This is the shared tail of every asynchronous read completion
// (all/fetch, and their promise forms). Those paths must materialise
// Cells — the rows are read on a worker thread and converted later on
// the JS thread — but the *conversion* is a plain synchronous pass and
// has no reason to cost more per row than the synchronous paths do.
//
// Returns false when a row raised the RangeError, leaving it pending
// for the caller to deliver to the callback.
bool CellRowsToJS(Napi::Env env, Rows& rows, const Columns& columns,
int row_mode, Napi::Array* out);
// The synchronous counterpart of RowToJS: builds the row object from
// the live statement, with no intermediate Row. Requires the column
// keys to have been synced for the current result shape. `row_mode`
// picks the row shape (object or array); the array shape never reads
// `keys`.
Napi::Value CurrentRowToJS(Napi::Env env,
const std::vector<napi_value>& keys, int row_mode = SYNC_ROW_OBJECT);
// The scopeless core of CurrentRowToJS: converts the current row into
// the caller's HandleScope and reports via the return value whether it
// completed (false = the 'number'-mode RangeError left a pending
// exception). Callers must store `*out` into a rooted JS object before
// their scope closes — which the synchronous paths do immediately,
// into the result array they are building.
bool ConvertCurrentRow(Napi::Env env, const std::vector<napi_value>& keys,
int row_mode, int cols, napi_value* out);
// Resolves the cached column-key references into raw napi_values once
// per call, so a multi-row read does not re-dereference them per cell.
void ResolveColumnKeys(std::vector<napi_value>* out);
// Returns the compiled row factory for the current result shape, or
// NULL when this build/realm/shape cannot use one (see
// AddonData::row_factory_generator, and kMaxFactoryColumns). Compiled
// on first use per shape and dropped whenever the column keys are
// rebuilt, so a mid-stream re-prepare cannot reuse a stale shape.
napi_value RowFactoryForShape(Napi::Env env, int row_mode);
// Drops the compiled factories. Called from the two places that
// invalidate the column keys.
void ResetRowFactories(Napi::Env env);
// Builds one row by calling the shape's factory with the cells as
// arguments — one napi_call_function instead of one V8 store per
// column. `cells` must already hold `cols` converted values.
bool CallRowFactory(Napi::Env env, napi_value factory,
const std::vector<napi_value>& cells, int cols, napi_value* out);
// Above this column count the generated function is not worth it (and
// approaches V8's parameter limit): those shapes keep the store loop.
static const int kMaxFactoryColumns = 256;
// Compiled factories for this statement's current result shape,
// indexed by SyncRowMode. NULL until first used for that mode.
napi_ref row_factory_[2] = { NULL, NULL };
// Converts an int64 cell/rowid according to the database's integer
// mode. Throws a RangeError in 'number' mode for unsafe values;
// callers must check env.IsExceptionPending() afterwards.
Napi::Value Int64ToJS(Napi::Env env, sqlite3_int64 value, const std::string& what);
// Stores the result of a completed run() for the lastID/lastIDBigInt/
// changes accessors.
void RecordRunResult(sqlite3_int64 id, int changes);
void Schedule(Work_Callback callback, Baton* baton);
void Process();
void CleanQueue();
template <class T> static void Error(T* baton);
// True when nothing anywhere on this database is in flight or queued,
// so sqlite can be driven from the main thread without racing the
// worker pool or breaking FIFO ordering.
bool IdleForInline();
// The synchronous methods' shared safety gate: not finalized, fully
// idle, no JavaScript collation or progress handler that would have to
// run on the thread blocked inside SQLite. Throws; false means the
// caller must return env.Null().
bool SyncGate(Napi::Env env);
// RAII marker for "this statement's VM is currently executing on the
// JS thread": a user-defined function invoked from the sync path can
// call back into the synchronous methods, but not into *this*
// statement — re-entering a stepping VM is the one hard rule SQLite
// has (node:sqlite enforces the same since v26.8). Other statements
// on the same connection stay legal: the connection mutex is
// recursive, so a nested step just re-enters it.
struct SyncStepGuard {
Statement* stmt;
explicit SyncStepGuard(Statement* s) : stmt(s) { stmt->sync_in_flight = true; }
~SyncStepGuard() { stmt->sync_in_flight = false; }
SyncStepGuard(const SyncStepGuard&) = delete;
SyncStepGuard& operator=(const SyncStepGuard&) = delete;
};
// Throws the pending status/message as a JS error with errno/code.
void ThrowStatementError(Napi::Env env);
void FailQueue(Napi::Value error, bool emit_if_unhandled = true);
protected:
// NULL when the constructor threw before validation completed; every
// destructor path gates on it.
Database* db = NULL;
sqlite3_stmt* _handle = NULL;
int status = SQLITE_OK;
bool prepared = false;
bool locked = true;
bool finalized = false;
// True while this statement's VM is executing inside a *Sync call on
// the JS thread (see SyncStepGuard): a re-entrant sync call from a
// user-defined function must refuse rather than corrupt the VDBE.
bool sync_in_flight = false;
// Byte offset of the failing token of the most recent failed prepare,
// from sqlite3_error_offset() (-1 when the last error was not a
// prepare error or there was no error). Attached to the thrown
// SqliteError as `offset` (bun:sqlite exposes the same as byteOffset;
// no other Node driver has it). Written under the connection mutex
// wherever a prepare fails; read on the JS thread when the error is
// built, which the async-work completion ordering serializes.
int error_offset = -1;
// Per-statement integer-mode override (see SetIntegerMode).
int integer_mode_override = 0;
bool has_integer_override = false;
// Result of the most recent run(), exposed through the lastID,
// lastIDBigInt and changes accessors.
sqlite3_int64 last_insert_id = 0;
int last_changes = 0;
bool has_run_result = false;
// Payloads of the currently SQLITE_STATIC-bound text/blob parameters.
// Owned until the next rebind or finalize so sqlite never sees a
// dangling pointer when a statement is stepped again without rebinding.
Parameters bound_payloads;
// Rooted JS key strings used for row conversion, plus the column names
// they were built from so a schema change can invalidate them.
std::vector<std::string> column_keys_source;
std::vector<Napi::Reference<Napi::String>> column_keys;
// Shape tracking for the synchronous read paths' key cache: a sync
// call re-reads the live column names only when the statement's shape
// has moved (transparent re-prepare counter or column count changed).
// One sqlite3_stmt_status call per call replaces the fresh heap
// vector<string> of names the old per-call Columns captured.
void SyncColumnKeysLive(Napi::Env env);
Columns sync_columns;
bool sync_keys_valid = false;
int sync_keys_reprepares = -1;
int sync_keys_cols = -1;
// Introspection snapshot. pending_meta is written by the preparing
// thread; meta and meta_valid are touched only by the JS thread, and
// meta_valid gates the accessors (the async prepare window leaves
// them undefined). See the note above PublishMetadata().
Metadata pending_meta;
Metadata meta;
bool meta_valid = false;
std::queue<Call*> queue;
std::string message;
};
}
#endif