Skip to content

Commit 2ac7a27

Browse files
committed
sqlite: add verbose option
1 parent 86282b5 commit 2ac7a27

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@
370370
V(url_string, "url") \
371371
V(username_string, "username") \
372372
V(value_string, "value") \
373+
V(verbose_string, "verbose") \
373374
V(verify_error_string, "verifyError") \
374375
V(version_string, "version") \
375376
V(windows_hide_string, "windowsHide") \

src/node_sqlite.cc

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,15 @@ bool DatabaseSync::Open() {
974974
env()->isolate(), this, load_extension_ret, SQLITE_OK, false);
975975
}
976976

977+
{
978+
Local<Value> cb =
979+
object()->GetInternalField(kVerboseCallback).template As<Value>();
980+
if (cb->IsFunction()) {
981+
sqlite3_trace_v2(
982+
connection_, SQLITE_TRACE_STMT, DatabaseSync::TraceCallback, this);
983+
}
984+
}
985+
977986
return true;
978987
}
979988

@@ -1339,6 +1348,23 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
13391348
}
13401349
}
13411350
}
1351+
1352+
// Parse verbose option
1353+
Local<Value> verbose_v;
1354+
if (!options->Get(env->context(), env->verbose_string())
1355+
.ToLocal(&verbose_v)) {
1356+
return;
1357+
}
1358+
if (!verbose_v->IsUndefined() && !verbose_v->IsNull()) {
1359+
if (!verbose_v->IsFunction()) {
1360+
THROW_ERR_INVALID_ARG_TYPE(
1361+
env->isolate(),
1362+
"The \"options.verbose\" argument must be a function.");
1363+
return;
1364+
}
1365+
args.This()->SetInternalField(kVerboseCallback,
1366+
verbose_v.As<Function>());
1367+
}
13421368
}
13431369

13441370
new DatabaseSync(
@@ -2391,6 +2417,55 @@ int DatabaseSync::AuthorizerCallback(void* user_data,
23912417
return int_result;
23922418
}
23932419

2420+
int DatabaseSync::TraceCallback(unsigned int type,
2421+
void* user_data,
2422+
void* p,
2423+
void* x) {
2424+
if (type != SQLITE_TRACE_STMT) {
2425+
return 0;
2426+
}
2427+
2428+
DatabaseSync* db = static_cast<DatabaseSync*>(user_data);
2429+
Environment* env = db->env();
2430+
Isolate* isolate = env->isolate();
2431+
HandleScope handle_scope(isolate);
2432+
Local<Context> context = env->context();
2433+
2434+
Local<Value> cb =
2435+
db->object()->GetInternalField(kVerboseCallback).template As<Value>();
2436+
2437+
if (!cb->IsFunction()) {
2438+
return 0;
2439+
}
2440+
2441+
char* expanded = sqlite3_expanded_sql(static_cast<sqlite3_stmt*>(p));
2442+
Local<Value> sql_string;
2443+
if (expanded != nullptr) {
2444+
bool ok = String::NewFromUtf8(isolate, expanded).ToLocal(&sql_string);
2445+
sqlite3_free(expanded);
2446+
if (!ok) {
2447+
return 0;
2448+
}
2449+
} else {
2450+
// Fallback to source SQL if expanded is unavailable
2451+
const char* source = sqlite3_sql(static_cast<sqlite3_stmt*>(p));
2452+
if (source == nullptr || !String::NewFromUtf8(isolate, source)
2453+
.ToLocal(&sql_string)) {
2454+
return 0;
2455+
}
2456+
}
2457+
2458+
Local<Function> callback = cb.As<Function>();
2459+
MaybeLocal<Value> retval =
2460+
callback->Call(context, Undefined(isolate), 1, &sql_string);
2461+
2462+
if (retval.IsEmpty()) {
2463+
db->SetIgnoreNextSQLiteError(true);
2464+
}
2465+
2466+
return 0;
2467+
}
2468+
23942469
StatementSync::StatementSync(Environment* env,
23952470
Local<Object> object,
23962471
BaseObjectPtr<DatabaseSync> db,

src/node_sqlite.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ class DatabaseSync : public BaseObject {
165165
enum InternalFields {
166166
kAuthorizerCallback = BaseObject::kInternalFieldCount,
167167
kLimitsObject,
168+
kVerboseCallback,
168169
kInternalFieldCount
169170
};
170171

@@ -202,6 +203,10 @@ class DatabaseSync : public BaseObject {
202203
const char* param2,
203204
const char* param3,
204205
const char* param4);
206+
static int TraceCallback(unsigned int type,
207+
void* user_data,
208+
void* p,
209+
void* x);
205210
void FinalizeStatements();
206211
void RemoveBackup(BackupJob* backup);
207212
void AddBackup(BackupJob* backup);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use strict';
2+
3+
const { skipIfSQLiteMissing } = require('../common');
4+
skipIfSQLiteMissing();
5+
6+
const assert = require('node:assert');
7+
const { DatabaseSync } = require('node:sqlite');
8+
const { suite, it } = require('node:test');
9+
10+
suite('DatabaseSync verbose option', () => {
11+
it('callback receives SQL string for exec() statements', (t) => {
12+
const calls = [];
13+
const db = new DatabaseSync(':memory:', {
14+
verbose: (sql) => calls.push(sql),
15+
});
16+
17+
db.exec('CREATE TABLE t (x INTEGER)');
18+
db.exec('INSERT INTO t VALUES (1)');
19+
20+
assert.strictEqual(calls.length, 2);
21+
assert.strictEqual(calls[0], 'CREATE TABLE t (x INTEGER)');
22+
assert.strictEqual(calls[1], 'INSERT INTO t VALUES (1)');
23+
db.close();
24+
});
25+
26+
it('callback receives SQL string for prepared statement execution', (t) => {
27+
let calls = [];
28+
const db = new DatabaseSync(':memory:', {
29+
verbose: (sql) => calls.push(sql),
30+
});
31+
32+
db.exec('CREATE TABLE t (x INTEGER)');
33+
calls = []; // reset after setup
34+
35+
const stmt = db.prepare('INSERT INTO t VALUES (?)');
36+
stmt.run(42);
37+
38+
assert.strictEqual(calls.length, 1);
39+
assert.strictEqual(calls[0], 'INSERT INTO t VALUES (42.0)');
40+
db.close();
41+
});
42+
43+
it('falls back to source SQL when expansion fails', () => {
44+
let calls = [];
45+
46+
const db = new DatabaseSync(':memory:', {
47+
verbose: (sql) => calls.push(sql),
48+
limits: { length: 1000 },
49+
});
50+
51+
db.exec('CREATE TABLE t (x TEXT)');
52+
calls = []; // reset after setup
53+
54+
const stmt = db.prepare('INSERT INTO t VALUES (?)');
55+
56+
const longValue = 'a'.repeat(977);
57+
stmt.run(longValue);
58+
59+
assert.strictEqual(calls.length, 1);
60+
// Falls back to source SQL with unexpanded '?' placeholder
61+
assert.strictEqual(calls[0], 'INSERT INTO t VALUES (?)');
62+
db.close();
63+
});
64+
65+
it('invalid type for verbose throws ERR_INVALID_ARG_TYPE', () => {
66+
assert.throws(() => {
67+
new DatabaseSync(':memory:', { verbose: 'not-a-function' });
68+
}, {
69+
code: 'ERR_INVALID_ARG_TYPE',
70+
message: /The "options\.verbose" argument must be a function\./,
71+
});
72+
73+
assert.throws(() => {
74+
new DatabaseSync(':memory:', { verbose: 42 });
75+
}, {
76+
code: 'ERR_INVALID_ARG_TYPE',
77+
message: /The "options\.verbose" argument must be a function\./,
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)