feat: stmt.scanStats()

This commit is contained in:
Fedor Indutny 2025-08-04 13:40:00 -07:00 committed by GitHub
parent d1646af2f3
commit 9e85ee2e91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 147 additions and 0 deletions

View File

@ -73,6 +73,7 @@
'SQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown',
],
'conditions': [
# Link with extension
['OS == "win"', {
'defines': [
'WIN32'
@ -96,6 +97,18 @@
]
},
}],
# Profiling
["\"<!(node -p \"require('../../package.json').version\")\".endswith(\"-profiling\")", {
'defines': [
'SQLITE_ENABLE_STMT_SCANSTATUS'
],
'direct_dependent_settings': {
'defines': [
'SQLITE_ENABLE_STMT_SCANSTATUS'
],
},
}],
],
'configurations': {
'Debug': {

View File

@ -39,6 +39,7 @@ const addon = bindings<{
cache: Array<SqliteValue<Options>> | undefined,
isGet: boolean,
): Array<SqliteValue<Options>>;
statementScanStats(stmt: NativeStatement): Array<ScanStats>;
statementClose(stmt: NativeStatement): void;
databaseOpen(path: string): NativeDatabase;
@ -225,6 +226,20 @@ class Statement<Options extends StatementOptions = object> {
return result as unknown as Array<Row>;
}
/**
* 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<ScanStats> {
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 PragmaOptions> = Options extends {
? RowType<{ pluck: true }> | undefined
: Array<RowType<object>>;
/**
* 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 }>;

View File

@ -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,108 @@ 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<uint32_t>(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<uint32_t>(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) {
auto env = info.Env();
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_);

View File

@ -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);