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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
61 changes: 61 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,66 @@ 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 a {string} containing the expanded
SQL with bound parameter values substituted. If expansion fails, the source
SQL with unsubstituted placeholders is used instead.

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

function onQuery(sql) {
console.log(sql);
}

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

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

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

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

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

function onQuery(sql) {
console.log(sql);
}

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

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

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

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

## `sqlite.constants`

<!-- YAML
Expand Down Expand Up @@ -1546,6 +1606,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