Skip to content
42 changes: 42 additions & 0 deletions benchmark/sqlite/sqlite-trace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
const common = require('../common.js');
const sqlite = require('node:sqlite');
const dc = require('diagnostics_channel');
const assert = require('assert');

const bench = common.createBenchmark(main, {
n: [1e5],
mode: ['none', 'subscribed', 'unsubscribed'],
});

function main(conf) {
const { n, mode } = conf;

const db = new sqlite.DatabaseSync(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
const insert = db.prepare('INSERT INTO t VALUES (?)');

let subscriber;
if (mode === 'subscribed') {
subscriber = () => {};
dc.subscribe('sqlite.db.query', subscriber);
} else if (mode === 'unsubscribed') {
subscriber = () => {};
dc.subscribe('sqlite.db.query', subscriber);
dc.unsubscribe('sqlite.db.query', subscriber);
}
// mode === 'none': no subscription ever made

let result;
bench.start();
for (let i = 0; i < n; i++) {
result = insert.run(i);
}
bench.end(n);

if (mode === 'subscribed') {
dc.unsubscribe('sqlite.db.query', subscriber);
}

assert.ok(result !== undefined);
}
67 changes: 67 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,72 @@ const totalPagesTransferred = await backup(sourceDb, 'backup.db', {
console.log('Backup completed', totalPagesTransferred);
```

## Diagnostics channel

<!-- YAML
added: REPLACEME
-->

The `node:sqlite` module publishes SQL trace events on the
[`diagnostics_channel`][] channel `sqlite.db.query`. This allows subscribers
to observe every SQL statement executed against any `DatabaseSync` instance
without modifying the database code itself. Tracing is zero-cost when there
are no subscribers.

### Channel `sqlite.db.query`

The message published to this channel is an {Object} with the following
properties:

* `sql` {string} The expanded SQL with bound parameter values substituted.
If expansion fails, the source SQL with unsubstituted placeholders is used
instead.
* `database` {DatabaseSync} The `DatabaseSync` instance that executed the
statement.
* `duration` {number} The estimated statement run time in nanoseconds.

```cjs
const dc = require('node:diagnostics_channel');
const { DatabaseSync } = require('node:sqlite');

function onQuery({ sql, database, duration }) {
console.log(sql, duration);
}

dc.subscribe('sqlite.db.query', onQuery);

const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
// Logs: CREATE TABLE t (x INTEGER) <duration>

const stmt = db.prepare('INSERT INTO t VALUES (?)');
stmt.run(42);
// Logs: INSERT INTO t VALUES (42.0) <duration>

dc.unsubscribe('sqlite.db.query', onQuery);
```

```mjs
import dc from 'node:diagnostics_channel';
import { DatabaseSync } from 'node:sqlite';

function onQuery({ sql, database, duration }) {
console.log(sql, duration);
}

dc.subscribe('sqlite.db.query', onQuery);

const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
// Logs: CREATE TABLE t (x INTEGER) <duration>

const stmt = db.prepare('INSERT INTO t VALUES (?)');
stmt.run(42);
// Logs: INSERT INTO t VALUES (42.0) <duration>

dc.unsubscribe('sqlite.db.query', onQuery);
```

## `sqlite.constants`

<!-- YAML
Expand Down Expand Up @@ -1546,6 +1612,7 @@ callback function to indicate what type of operation is being authorized.
[`database.applyChangeset()`]: #databaseapplychangesetchangeset-options
[`database.createTagStore()`]: #databasecreatetagstoremaxsize
[`database.setAuthorizer()`]: #databasesetauthorizercallback
[`diagnostics_channel`]: diagnostics_channel.md
[`sqlite3_backup_finish()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
[`sqlite3_backup_init()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
[`sqlite3_backup_step()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
Expand Down
8 changes: 8 additions & 0 deletions lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,19 @@ function markActive(channel) {
ObjectSetPrototypeOf(channel, ActiveChannel.prototype);
channel._subscribers = [];
channel._stores = new SafeMap();

// Notify native modules that this channel just got its first subscriber.
if (channel._index !== undefined)
dc_binding.notifyChannelActive(channel._index);
}

function maybeMarkInactive(channel) {
// When there are no more active subscribers or bound, restore to fast prototype.
if (!channel._subscribers.length && !channel._stores.size) {
// Notify native modules that this channel just lost its last subscriber.
if (channel._index !== undefined)
dc_binding.notifyChannelInactive(channel._index);

// eslint-disable-next-line no-use-before-define
ObjectSetPrototypeOf(channel, Channel.prototype);
channel._subscribers = undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/base_object_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ namespace node {
#define UNSERIALIZABLE_BINDING_TYPES(V) \
V(http2_binding_data, http2::BindingData) \
V(http_parser_binding_data, http_parser::BindingData) \
V(quic_binding_data, quic::BindingData)
V(quic_binding_data, quic::BindingData) \
V(sqlite_binding_data, sqlite::BindingData)

// List of (non-binding) BaseObjects that are serializable in the snapshot.
// The first argument should match what the type passes to
Expand Down
2 changes: 2 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
V(crypto_rsa_pss_string, "rsa-pss") \
V(cwd_string, "cwd") \
V(data_string, "data") \
V(database_string, "database") \
V(default_is_true_string, "defaultIsTrue") \
V(defensive_string, "defensive") \
V(deserialize_info_string, "deserializeInfo") \
Expand Down Expand Up @@ -338,6 +339,7 @@
V(source_map_url_string, "sourceMapURL") \
V(source_url_string, "sourceURL") \
V(specifier_string, "specifier") \
V(sql_string, "sql") \
V(stack_string, "stack") \
V(start_string, "start") \
V(state_string, "state") \
Expand Down
28 changes: 28 additions & 0 deletions src/node_diagnostics_channel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,38 @@ void BindingData::Deserialize(Local<Context> context,
CHECK_NOT_NULL(binding);
}

void BindingData::SetChannelStatusCallback(uint32_t index,
ChannelStatusCallback cb) {
channel_status_callbacks_[index] = std::move(cb);
}

void BindingData::NotifyChannelActive(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
BindingData* binding = realm->GetBindingData<BindingData>();
if (binding == nullptr) return;
uint32_t index = args[0].As<v8::Uint32>()->Value();
auto it = binding->channel_status_callbacks_.find(index);
if (it != binding->channel_status_callbacks_.end()) it->second(true);
}
Comment on lines +135 to +142
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

NotifyChannelActive() reads args[0] as a Uint32 without validating args.Length() or the value type. Since this is callable from JS, please add the same kind of CHECK(args[0]->IsUint32())/length validation used by other binding entry points (e.g., GetOrCreateChannelIndex, LinkNativeChannel) to avoid crashes on misuse.

Copilot uses AI. Check for mistakes.

void BindingData::NotifyChannelInactive(
const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
BindingData* binding = realm->GetBindingData<BindingData>();
if (binding == nullptr) return;
uint32_t index = args[0].As<v8::Uint32>()->Value();
auto it = binding->channel_status_callbacks_.find(index);
if (it != binding->channel_status_callbacks_.end()) it->second(false);
}
Comment on lines +144 to +152
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

NotifyChannelInactive() reads args[0] as a Uint32 without validating args.Length() or the value type. Add CHECK() validation (or equivalent) for argument count/type to keep the binding robust and consistent with the other diagnostics_channel binding methods.

Copilot uses AI. Check for mistakes.

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
SetMethod(
isolate, target, "getOrCreateChannelIndex", GetOrCreateChannelIndex);
SetMethod(isolate, target, "linkNativeChannel", LinkNativeChannel);
SetMethod(isolate, target, "notifyChannelActive", NotifyChannelActive);
SetMethod(isolate, target, "notifyChannelInactive", NotifyChannelInactive);
}

void BindingData::CreatePerContextProperties(Local<Object> target,
Expand All @@ -148,6 +174,8 @@ void BindingData::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(GetOrCreateChannelIndex);
registry->Register(LinkNativeChannel);
registry->Register(NotifyChannelActive);
registry->Register(NotifyChannelInactive);
}

Channel::Channel(Environment* env,
Expand Down
10 changes: 10 additions & 0 deletions src/node_diagnostics_channel.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <cinttypes>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
Expand Down Expand Up @@ -53,6 +54,14 @@ class BindingData : public SnapshotableObject {
static void LinkNativeChannel(
const v8::FunctionCallbackInfo<v8::Value>& args);

using ChannelStatusCallback = std::function<void(bool is_active)>;
void SetChannelStatusCallback(uint32_t index, ChannelStatusCallback cb);

static void NotifyChannelActive(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void NotifyChannelInactive(
const v8::FunctionCallbackInfo<v8::Value>& args);

static void CreatePerIsolateProperties(IsolateData* isolate_data,
v8::Local<v8::ObjectTemplate> target);
static void CreatePerContextProperties(v8::Local<v8::Object> target,
Expand All @@ -63,6 +72,7 @@ class BindingData : public SnapshotableObject {

private:
InternalFieldInfo* internal_field_info_ = nullptr;
std::unordered_map<uint32_t, ChannelStatusCallback> channel_status_callbacks_;
};

class Channel : public BaseObject {
Expand Down
Loading
Loading