From a5603b392321a57834324d689b30bca49910b253 Mon Sep 17 00:00:00 2001 From: Fedor Indutny Date: Mon, 4 Aug 2025 13:06:00 -0700 Subject: [PATCH 1/2] feat: stmt.scanStats() --- deps/sqlcipher/sqlcipher.gyp | 13 +++++ lib/index.ts | 29 ++++++++++ src/addon.cc | 102 +++++++++++++++++++++++++++++++++++ src/addon.h | 1 + 4 files changed, 145 insertions(+) diff --git a/deps/sqlcipher/sqlcipher.gyp b/deps/sqlcipher/sqlcipher.gyp index 28a9458..9c0e32b 100755 --- a/deps/sqlcipher/sqlcipher.gyp +++ b/deps/sqlcipher/sqlcipher.gyp @@ -73,6 +73,7 @@ 'SQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown', ], 'conditions': [ + # Link with extension ['OS == "win"', { 'defines': [ 'WIN32' @@ -96,6 +97,18 @@ ] }, }], + + # Profiling + ["\"> | undefined, isGet: boolean, ): Array>; + statementScanStats(stmt: NativeStatement): Array; statementClose(stmt: NativeStatement): void; databaseOpen(path: string): NativeDatabase; @@ -225,6 +226,20 @@ class Statement { return result as unknown as Array; } + /** + * Report collected performance statics for the statement. + * + * @returns A list of objects describing the performance of the query. + * + * @see {@link https://www.sqlite.org/profile.html} + */ + public scanStats(): Array { + if (this.#native === undefined) { + throw new Error('Statement closed'); + } + return addon.statementScanStats(this.#native); + } + /** * Close the statement and release the used memory. */ @@ -302,6 +317,20 @@ export type PragmaResult = Options extends { ? RowType<{ pluck: true }> | undefined : Array>; +/** + * An entry of result array of `stmt.scanStats()` method. + * + * Value of `-1` indicates that the field is not available for a given entry. + */ +export type ScanStats = Readonly<{ + id: number; + parent: number; + cycles: number; + loops: number; + rows: number; + explain: string | null; +}>; + /** @internal */ type TransactionStatement = Statement<{ persistent: true; pluck: true }>; diff --git a/src/addon.cc b/src/addon.cc index e008a5a..0620f20 100644 --- a/src/addon.cc +++ b/src/addon.cc @@ -321,6 +321,8 @@ Napi::Object Statement::Init(Napi::Env env, Napi::Object exports) { exports["statementClose"] = Napi::Function::New(env, &Statement::Close); exports["statementRun"] = Napi::Function::New(env, &Statement::Run); exports["statementStep"] = Napi::Function::New(env, &Statement::Step); + exports["statementScanStats"] = + Napi::Function::New(env, &Statement::ScanStats); return exports; } @@ -562,6 +564,106 @@ Napi::Value Statement::Step(const Napi::CallbackInfo& info) { return result; } +// Only enabled on `-profiling` npm package versions + +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS +Napi::Value Statement::ScanStats(const Napi::CallbackInfo& info) { + auto env = info.Env(); + + auto stmt = FromExternal(info[0]); + if (stmt == nullptr) { + return Napi::Value(); + } + + sqlite3_int64 total_cycles = 0; + int r = sqlite3_stmt_scanstatus_v2(stmt->handle_, -1, SQLITE_SCANSTAT_NCYCLE, + SQLITE_SCANSTAT_COMPLEX, &total_cycles); + + if (r != SQLITE_OK) { + return stmt->db_->ThrowSqliteError(env, r); + } + + auto results = Napi::Array::New(env, 1); + + auto root = Napi::Object::New(env); + + root["id"] = 0; + root["parent"] = -1; + root["cycles"] = total_cycles; + root["loops"] = -1; + root["rows"] = -1; + root["explain"] = env.Null(); + results[static_cast(0)] = root; + + for (int idx = 0; r == SQLITE_OK; idx++) { + int id = 0; + int parent = 0; + sqlite3_int64 cycles = 0; + sqlite3_int64 loops = 0; + sqlite3_int64 rows = 0; + const char* explain = nullptr; + + r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_SELECTID, + SQLITE_SCANSTAT_COMPLEX, &id); + if (r != SQLITE_OK) { + break; + } + r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_PARENTID, + SQLITE_SCANSTAT_COMPLEX, &parent); + if (r != SQLITE_OK) { + break; + } + r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_NCYCLE, + SQLITE_SCANSTAT_COMPLEX, &cycles); + if (r != SQLITE_OK) { + break; + } + r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_NLOOP, + SQLITE_SCANSTAT_COMPLEX, &loops); + if (r != SQLITE_OK) { + break; + } + r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_NVISIT, + SQLITE_SCANSTAT_COMPLEX, &rows); + if (r != SQLITE_OK) { + break; + } + r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_EXPLAIN, + SQLITE_SCANSTAT_COMPLEX, &explain); + if (r != SQLITE_OK) { + break; + } + + auto result = Napi::Object::New(env); + + result["id"] = id; + result["parent"] = parent; + result["cycles"] = cycles; + result["loops"] = loops; + result["rows"] = rows; + if (explain == nullptr) { + result["explain"] = env.Null(); + } else { + result["explain"] = explain; + } + + results[static_cast(idx + 1)] = result; + } + + // SQLITE_ERROR is returned when `idx` is out of range + if (r != SQLITE_ERROR) { + return stmt->db_->ThrowSqliteError(env, r); + } + + return results; +} +#else // !SQLITE_ENABLE_STMT_SCANSTATUS +Napi::Value Statement::ScanStats(const Napi::CallbackInfo& info) { + NAPI_THROW(Napi::Error::New(env, "Not available in production builds"), + Napi::Value()); +} +#endif // !SQLITE_ENABLE_STMT_SCANSTATUS + bool Statement::BindParams(Napi::Env env, Napi::Value params) { int key_count = sqlite3_bind_parameter_count(handle_); diff --git a/src/addon.h b/src/addon.h index b854441..2d76f4b 100644 --- a/src/addon.h +++ b/src/addon.h @@ -131,6 +131,7 @@ class Statement { static Napi::Value Close(const Napi::CallbackInfo& info); static Napi::Value Run(const Napi::CallbackInfo& info); static Napi::Value Step(const Napi::CallbackInfo& info); + static Napi::Value ScanStats(const Napi::CallbackInfo& info); bool BindParams(Napi::Env env, Napi::Value params); From 60b1db6eecf1b15daa303328b95e551c2abba19e Mon Sep 17 00:00:00 2001 From: Fedor Indutny Date: Mon, 4 Aug 2025 13:08:02 -0700 Subject: [PATCH 2/2] fix --- src/addon.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/addon.cc b/src/addon.cc index 0620f20..8677909 100644 --- a/src/addon.cc +++ b/src/addon.cc @@ -659,6 +659,8 @@ Napi::Value Statement::ScanStats(const Napi::CallbackInfo& info) { } #else // !SQLITE_ENABLE_STMT_SCANSTATUS Napi::Value Statement::ScanStats(const Napi::CallbackInfo& info) { + auto env = info.Env(); + NAPI_THROW(Napi::Error::New(env, "Not available in production builds"), Napi::Value()); }