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
51 changes: 51 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
the values in `namedParameters` and `anonymousParameters`. See
[Binding parameters][].

### `statement.resetStats()`

<!-- YAML
added: REPLACEME
-->

Resets every counter reported by [`statement.stat()`][] back to zero, except
`memused`, which reports current memory usage and cannot be reset. This
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
measuring a specific workload without the counts accumulated by earlier
executions of the same prepared statement.

### `statement.run([namedParameters][, ...anonymousParameters])`

<!-- YAML
Expand Down Expand Up @@ -1322,6 +1334,43 @@ added: REPLACEME
Finalizes the prepared statement. If the prepared statement is already
finalized, then this is a no-op.

### `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 execution cycles started by the prepared statement.
* `'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.

The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
and passing either name throws `ERR_INVALID_ARG_VALUE`.

## Class: `SQLTagStore`

<!-- YAML
Expand Down Expand Up @@ -1840,6 +1889,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 All @@ -1848,6 +1898,7 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
[`statement.stat()`]: #statementstatcounter
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
[data types]: https://www.sqlite.org/datatype3.html
Expand Down
74 changes: 68 additions & 6 deletions src/node_sqlite.cc
Comment thread
geeksilva97 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
}
} // namespace

// Helper function to find limit info from JS property name
static constexpr const LimitInfo* GetLimitInfoFromName(std::string_view name) {
for (const auto& info : kLimitMapping) {
// Helper function to look up a mapping entry by its JS-facing name
template <typename T, size_t N>
static constexpr const T* FindByJsName(const std::array<T, N>& mapping,
std::string_view name) {
for (const auto& info : mapping) {
if (name == info.js_name) {
return &info;
}
Expand Down Expand Up @@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
Isolate* isolate = env->isolate();

Utf8Value prop_name(isolate, property);
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
const LimitInfo* limit_info =
FindByJsName(kLimitMapping, prop_name.ToStringView());

if (limit_info == nullptr) {
return Intercepted::kNo; // Unknown property, let default handling occur
Expand Down Expand Up @@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
Isolate* isolate = env->isolate();

Utf8Value prop_name(isolate, property);
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
const LimitInfo* limit_info =
FindByJsName(kLimitMapping, prop_name.ToStringView());

if (limit_info == nullptr) {
return Intercepted::kNo;
Expand Down Expand Up @@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(

Isolate* isolate = info.GetIsolate();
Utf8Value prop_name(isolate, property);
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
const LimitInfo* limit_info =
FindByJsName(kLimitMapping, prop_name.ToStringView());

if (!limit_info) {
return Intercepted::kNo;
Expand Down Expand Up @@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {

void StatementSync::InvalidateColumnNameCache() {
cached_column_names_.clear();
cached_column_names_reprepare_count_ = -1;
}

inline bool StatementSync::IsFinalized() {
Expand Down Expand Up @@ -3350,6 +3356,60 @@ 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 =
FindByJsName(kStatusMapping, 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_.get(), status_info->sqlite_status_id, false);
args.GetReturnValue().Set(Integer::New(isolate, value));
}

void StatementSync::ResetStats(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");

// sqlite3_stmt_status() resets a single counter per call, so every exposed
// counter is visited. The returned value is the pre-reset one and is unused.
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
// rather than an accumulated counter, and SQLite ignores the reset flag for
// it.
for (const auto& info : kStatusMapping) {
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
continue;
}
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
}

// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
// just zeroed. Without invalidating, a later re-prepare can make the counter
// match the cached generation again and the stale names would be reused.
stmt->InvalidateColumnNameCache();
}

void StatementSync::SetAllowBareNamedParameters(
const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
Expand Down Expand Up @@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
StatementSync::ExpandedSQLGetter);
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
SetProtoMethod(isolate,
tmpl,
"setAllowBareNamedParameters",
Expand Down
28 changes: 28 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ 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;
};

// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
#if SQLITE_VERSION_NUMBER >= 3038000
#define NODE_SQLITE_HAS_FILTER_STATUS 1
#endif

inline constexpr auto kStatusMapping = std::to_array<StatusInfo>({
{"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},
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},

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.

SQLITE_STMTSTATUS_FILTER_MISS and SQLITE_STMTSTATUS_FILTER_HIT was introduced in SQLite 3.38.0.

3.37.2: https://github.com/sqlite/sqlite/blob/version-3.37.2/src/sqlite.h.in
3.38.0: https://github.com/sqlite/sqlite/blob/version-3.38.0/src/sqlite.h.in

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.

So maybe we need to enforce a minimum SQLite version on --shared-sqlite

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What do u suggest? A CHECK or something?

@geeksilva97 geeksilva97 Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added some ifdefs. Let's see what the team says

#endif
{"memused", SQLITE_STMTSTATUS_MEMUSED},
Comment thread
geeksilva97 marked this conversation as resolved.
});

class DatabaseOpenConfiguration {
public:
explicit DatabaseOpenConfiguration(std::string&& location)
Expand Down Expand Up @@ -284,6 +310,8 @@ 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 ResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAllowBareNamedParameters(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAllowUnknownNamedParameters(
Expand Down
Loading
Loading