Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,39 @@ added: v22.5.0
The source SQL text of the prepared statement. This property is a
wrapper around [`sqlite3_sql()`][].

### `statement.stat(counter)`

<!-- YAML
added: REPLACEME
-->

* `counter` {string} The name of the counter to read. One of:

* `'fullscanStep'` The number of times SQLite has stepped forward in a table
as part of a full table scan.
* `'sort'` The number of sort operations that have occurred.
* `'autoindex'` The number of rows inserted into transient indices that were
created automatically to help joins run faster.
* `'vmStep'` The number of virtual machine operations executed by the
prepared statement.
* `'reprepare'` The number of times the statement has been automatically
reprepared due to schema changes or changes to bound parameters.
* `'run'` The number of times the statement has run to completion.
* `'filterMiss'` The number of times the Bloom filter returned a result that
required the join step to be processed as normal.
* `'filterHit'` The number of times a join step was bypassed because a Bloom
filter returned not-found.
* `'memused'` The approximate number of bytes of heap memory used to store
the prepared statement.

* Returns: {number} The current value of the requested counter.

Returns one of the runtime counters that SQLite tracks for this prepared
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
not reset the counter. Asserting that a statement does not perform a full table
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
against degenerate performance.

## Class: `SQLTagStore`

<!-- YAML
Expand Down Expand Up @@ -1689,6 +1722,7 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
Expand Down
39 changes: 39 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@ static constexpr const LimitInfo* GetLimitInfoFromName(std::string_view name) {
return nullptr;
}

static constexpr const StatusInfo* GetStatusInfoFromName(
std::string_view name) {
for (const auto& info : kStatusMapping) {
if (name == info.js_name) {
return &info;
}
}
return nullptr;
}

inline MaybeLocal<Object> CreateSQLiteError(Isolate* isolate,
const char* message) {
Local<String> js_msg;
Expand Down Expand Up @@ -3202,6 +3212,34 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result);
}

void StatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, stmt->IsFinalized(), "statement has been finalized");
Isolate* isolate = env->isolate();

if (!args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(isolate,
"The \"counter\" argument must be a string.");
return;
}

Utf8Value counter(isolate, args[0].As<String>());
const StatusInfo* status_info = GetStatusInfoFromName(counter.ToStringView());
if (status_info == nullptr) {
THROW_ERR_INVALID_ARG_VALUE(
isolate, "The \"counter\" argument is not a valid statistic name.");
return;
}

// The reset flag is always false; the counter is read without being cleared.
int value = sqlite3_stmt_status(
stmt->statement_, status_info->sqlite_status_id, false);
args.GetReturnValue().Set(Integer::New(isolate, value));
}

void StatementSync::SetAllowBareNamedParameters(
const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
Expand Down Expand Up @@ -3617,6 +3655,7 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
StatementSync::ExpandedSQLGetter);
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
SetProtoMethod(isolate,
tmpl,
"setAllowBareNamedParameters",
Expand Down
19 changes: 19 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,24 @@ static_assert(
CheckLimitIndices(),
"Each kLimitMapping entry's sqlite_limit_id must match its index");

// Mapping from JavaScript counter names to SQLite statement status constants
struct StatusInfo {
std::string_view js_name;
int sqlite_status_id;
};

inline constexpr std::array<StatusInfo, 9> kStatusMapping = {{
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
{"sort", SQLITE_STMTSTATUS_SORT},
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
{"run", SQLITE_STMTSTATUS_RUN},
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
{"memused", SQLITE_STMTSTATUS_MEMUSED},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{"memused", SQLITE_STMTSTATUS_MEMUSED},
{"memUsed", SQLITE_STMTSTATUS_MEMUSED},

}};

class DatabaseOpenConfiguration {
public:
explicit DatabaseOpenConfiguration(std::string&& location)
Expand Down Expand Up @@ -272,6 +290,7 @@ class StatementSync : public BaseObject {
static void SourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ExpandedSQLGetter(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void Stat(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAllowBareNamedParameters(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAllowUnknownNamedParameters(
Expand Down
92 changes: 92 additions & 0 deletions test/parallel/test-sqlite-statement-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,98 @@ suite('StatementSync.prototype.expandedSQL', () => {
});
});

suite('StatementSync.prototype.stat()', () => {
const counters = [
'fullscanStep', 'sort', 'autoindex', 'vmStep', 'reprepare',
'run', 'filterMiss', 'filterHit', 'memused',
];

test('returns a number for every valid counter', (t) => {
using db = new DatabaseSync(nextDb());
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
const stmt = db.prepare('SELECT * FROM data');
for (const counter of counters) {
t.assert.strictEqual(typeof stmt.stat(counter), 'number');
}
});

test('counts virtual machine steps and runs after execution', (t) => {
using db = new DatabaseSync(nextDb());
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
for (let i = 1; i <= 5; i++) {
insert.run(i, `val-${i}`);
}
const stmt = db.prepare('SELECT * FROM data');
t.assert.strictEqual(stmt.stat('run'), 0);
t.assert.strictEqual(stmt.stat('vmStep'), 0);
stmt.all();
t.assert.strictEqual(stmt.stat('run'), 1);
t.assert.ok(stmt.stat('vmStep') > 0);
t.assert.ok(stmt.stat('memused') > 0);
});

test('detects full table scans', (t) => {
using db = new DatabaseSync(nextDb());
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
for (let i = 1; i <= 10; i++) {
insert.run(i, `val-${i}`);
}

// Filtering on a non-indexed column forces a full table scan.
const scan = db.prepare('SELECT * FROM data WHERE val = ?');
scan.all('val-5');
t.assert.ok(scan.stat('fullscanStep') > 0);

// Filtering on the primary key uses the index; no full scan occurs.
const indexed = db.prepare('SELECT * FROM data WHERE key = ?');
indexed.all(5);
t.assert.strictEqual(indexed.stat('fullscanStep'), 0);
});

test('reading a counter does not reset it', (t) => {
using db = new DatabaseSync(nextDb());
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
const stmt = db.prepare('SELECT * FROM data');
stmt.all();
const first = stmt.stat('run');
t.assert.strictEqual(stmt.stat('run'), first);
});

test('throws if the counter argument is not a string', (t) => {
using db = new DatabaseSync(nextDb());
const stmt = db.prepare('SELECT 1');
t.assert.throws(() => stmt.stat(), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "counter" argument must be a string/,
});
t.assert.throws(() => stmt.stat(42), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "counter" argument must be a string/,
});
});

test('throws if the counter name is unknown', (t) => {
using db = new DatabaseSync(nextDb());
const stmt = db.prepare('SELECT 1');
t.assert.throws(() => stmt.stat('nope'), {
code: 'ERR_INVALID_ARG_VALUE',
message: /The "counter" argument is not a valid statistic name/,
});
});

test('throws if the statement is finalized', (t) => {
const db = new DatabaseSync(nextDb());
const stmt = db.prepare('SELECT 1');
db.close();
t.assert.throws(() => stmt.stat('run'), {
code: 'ERR_INVALID_STATE',
message: /statement has been finalized/,
});
});
});

suite('StatementSync.prototype.setReadBigInts()', () => {
test('BigInts support can be toggled', (t) => {
const db = new DatabaseSync(nextDb());
Expand Down
Loading