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
590 lines
25 KiB
Plaintext
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
|