#ifndef NODE_SQLITE3_SRC_STATEMENT_H #define NODE_SQLITE3_SRC_STATEMENT_H #include #include #include #include #include #include #include #include #include "convert.h" #include "database.h" #include "threading.h" using namespace Napi; namespace node_sqlite3 { class Statement : public Napi::ObjectWrap { 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 param_names; // "" entries: unnamed (?) std::vector 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 inline std::unique_ptr 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 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 (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& 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& 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& 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* 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& 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 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 column_keys_source; std::vector> 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 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 queue; std::string message; }; } #endif