Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ exposed by this class execute synchronously.
<!-- YAML
added: v22.5.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/62241
description: Add `verbose` option.
- version:
- v25.5.0
- v24.14.0
Expand Down Expand Up @@ -180,6 +183,13 @@ changes:
* `likePatternLength` {number} Maximum length of a LIKE pattern.
* `variableNumber` {number} Maximum number of SQL variables.
* `triggerDepth` {number} Maximum trigger recursion depth.
* `verbose` {Function} An optional callback function that is invoked for
every SQL statement executed against the database. The callback receives
the expanded SQL string (with bound parameter values substituted) as its
only argument. If expansion fails, the source SQL (with unsubstituted
placeholders) is passed instead. This is useful for logging and debugging.
This option is a wrapper around [`sqlite3_trace_v2()`][].
**Default:** `undefined`.

Constructs a new `DatabaseSync` instance.

Expand Down Expand Up @@ -1566,6 +1576,7 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3_load_extension()`]: https://www.sqlite.org/c3ref/load_extension.html
[`sqlite3_prepare_v2()`]: https://www.sqlite.org/c3ref/prepare.html
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
[`sqlite3_trace_v2()`]: https://www.sqlite.org/c3ref/trace_v2.html
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@
V(url_string, "url") \
V(username_string, "username") \
V(value_string, "value") \
V(verbose_string, "verbose") \
V(verify_error_string, "verifyError") \
V(version_string, "version") \
V(windows_hide_string, "windowsHide") \
Expand Down
75 changes: 75 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,15 @@ bool DatabaseSync::Open() {
env()->isolate(), this, load_extension_ret, SQLITE_OK, false);
}

{
Local<Value> cb =
object()->GetInternalField(kVerboseCallback).template As<Value>();
if (cb->IsFunction()) {
sqlite3_trace_v2(
connection_, SQLITE_TRACE_STMT, DatabaseSync::TraceCallback, this);
}
}

return true;
}

Expand Down Expand Up @@ -1339,6 +1348,23 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
}
}
}

// Parse verbose option
Local<Value> verbose_v;
if (!options->Get(env->context(), env->verbose_string())
.ToLocal(&verbose_v)) {
return;
}
if (!verbose_v->IsUndefined() && !verbose_v->IsNull()) {
if (!verbose_v->IsFunction()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.verbose\" argument must be a function.");
return;
}
args.This()->SetInternalField(kVerboseCallback,
verbose_v.As<Function>());
}
}

new DatabaseSync(
Expand Down Expand Up @@ -2390,6 +2416,55 @@ int DatabaseSync::AuthorizerCallback(void* user_data,
return int_result;
}

int DatabaseSync::TraceCallback(unsigned int type,
void* user_data,
void* p,
void* x) {
if (type != SQLITE_TRACE_STMT) {
return 0;
}

DatabaseSync* db = static_cast<DatabaseSync*>(user_data);
Environment* env = db->env();
Isolate* isolate = env->isolate();
HandleScope handle_scope(isolate);
Local<Context> context = env->context();

Local<Value> cb =
db->object()->GetInternalField(kVerboseCallback).template As<Value>();

if (!cb->IsFunction()) {
return 0;
}

char* expanded = sqlite3_expanded_sql(static_cast<sqlite3_stmt*>(p));
Local<Value> sql_string;
if (expanded != nullptr) {
bool ok = String::NewFromUtf8(isolate, expanded).ToLocal(&sql_string);
sqlite3_free(expanded);
if (!ok) {
return 0;
}
} else {
// Fallback to source SQL if expanded is unavailable
const char* source = sqlite3_sql(static_cast<sqlite3_stmt*>(p));
if (source == nullptr || !String::NewFromUtf8(isolate, source)
.ToLocal(&sql_string)) {
return 0;
}
}

Local<Function> callback = cb.As<Function>();
MaybeLocal<Value> retval =
callback->Call(context, Undefined(isolate), 1, &sql_string);

if (retval.IsEmpty()) {
db->SetIgnoreNextSQLiteError(true);
}

return 0;
}

StatementSync::StatementSync(Environment* env,
Local<Object> object,
BaseObjectPtr<DatabaseSync> db,
Expand Down
5 changes: 5 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ class DatabaseSync : public BaseObject {
enum InternalFields {
kAuthorizerCallback = BaseObject::kInternalFieldCount,
kLimitsObject,
kVerboseCallback,
kInternalFieldCount
};

Expand Down Expand Up @@ -202,6 +203,10 @@ class DatabaseSync : public BaseObject {
const char* param2,
const char* param3,
const char* param4);
static int TraceCallback(unsigned int type,
void* user_data,
void* p,
void* x);
void FinalizeStatements();
void RemoveBackup(BackupJob* backup);
void AddBackup(BackupJob* backup);
Expand Down
134 changes: 134 additions & 0 deletions test/parallel/test-sqlite-verbose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use strict';

const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();

const assert = require('node:assert');
const { DatabaseSync } = require('node:sqlite');
const { suite, it } = require('node:test');

suite('DatabaseSync verbose option', () => {
it('callback receives SQL string for exec() statements', (t) => {
const calls = [];
const db = new DatabaseSync(':memory:', {
verbose: (sql) => calls.push(sql),
});

db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');

assert.strictEqual(calls.length, 2);
assert.strictEqual(calls[0], 'CREATE TABLE t (x INTEGER)');
assert.strictEqual(calls[1], 'INSERT INTO t VALUES (1)');
db.close();
});

it('callback receives SQL string for prepared statement execution', (t) => {
let calls = [];
const db = new DatabaseSync(':memory:', {
verbose: (sql) => calls.push(sql),
});

db.exec('CREATE TABLE t (x INTEGER)');
calls = []; // reset after setup

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

assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0], 'INSERT INTO t VALUES (42.0)');
db.close();
});

it('callback receives SQL string for SELECT statements', () => {
let calls = [];
const db = new DatabaseSync(':memory:', {
verbose: (sql) => calls.push(sql),
});

db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');
calls = []; // reset after setup

const stmt = db.prepare('SELECT x FROM t WHERE x = ?');
stmt.get(1);

assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0], 'SELECT x FROM t WHERE x = 1.0');
db.close();
});

it('callback receives SQL string for UPDATE statements', () => {
let calls = [];
const db = new DatabaseSync(':memory:', {
verbose: (sql) => calls.push(sql),
});

db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');
calls = []; // reset after setup

const stmt = db.prepare('UPDATE t SET x = ? WHERE x = ?');
stmt.run(2, 1);

assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0], 'UPDATE t SET x = 2.0 WHERE x = 1.0');
db.close();
});

it('callback receives SQL string for DELETE statements', () => {
let calls = [];
const db = new DatabaseSync(':memory:', {
verbose: (sql) => calls.push(sql),
});

db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');
calls = []; // reset after setup

const stmt = db.prepare('DELETE FROM t WHERE x = ?');
stmt.run(1);

assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0], 'DELETE FROM t WHERE x = 1.0');
db.close();
});

it('falls back to source SQL when expansion fails', () => {
let calls = [];

const db = new DatabaseSync(':memory:', {
verbose: (sql) => calls.push(sql),
limits: { length: 1000 },
});

db.exec('CREATE TABLE t (x TEXT)');
calls = []; // reset after setup

const stmt = db.prepare('INSERT INTO t VALUES (?)');

const longValue = 'a'.repeat(977);
stmt.run(longValue);

assert.strictEqual(calls.length, 1);
// Falls back to source SQL with unexpanded '?' placeholder
assert.strictEqual(calls[0], 'INSERT INTO t VALUES (?)');
db.close();
});

it('invalid type for verbose throws ERR_INVALID_ARG_TYPE', () => {
assert.throws(() => {
new DatabaseSync(':memory:', { verbose: 'not-a-function' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.verbose" argument must be a function\./,
});

assert.throws(() => {
new DatabaseSync(':memory:', { verbose: 42 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.verbose" argument must be a function\./,
});
});
});
Loading