Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
ContributorAuthor

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?

@geeksilva97geeksilva97Aug 11, 2026

Copy link
Copy Markdown
ContributorAuthor

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 DownExpand 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