Skip to content

Commit eef70d7

Browse files
committed
sqlite: add verbose option
1 parent d0fa608 commit eef70d7

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@
376376
V(url_string, "url") \
377377
V(username_string, "username") \
378378
V(value_string, "value") \
379+
V(verbose_string, "verbose") \
379380
V(verify_error_string, "verifyError") \
380381
V(version_string, "version") \
381382
V(windows_hide_string, "windowsHide") \

src/node_sqlite.cc

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

996+
{
997+
Local<Value> cb =
998+
object()->GetInternalField(kVerboseCallback).template As<Value>();
999+
if (cb->IsFunction()) {
1000+
sqlite3_trace_v2(
1001+
connection_, SQLITE_TRACE_STMT, DatabaseSync::TraceCallback, this);
1002+
}
1003+
}
1004+
9961005
return true;
9971006
}
9981007

@@ -1358,6 +1367,23 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
13581367
}
13591368
}
13601369
}
1370+
1371+
// Parse verbose option
1372+
Local<Value> verbose_v;
1373+
if (!options->Get(env->context(), env->verbose_string())
1374+
.ToLocal(&verbose_v)) {
1375+
return;
1376+
}
1377+
if (!verbose_v->IsUndefined() && !verbose_v->IsNull()) {
1378+
if (!verbose_v->IsFunction()) {
1379+
THROW_ERR_INVALID_ARG_TYPE(
1380+
env->isolate(),
1381+
"The \"options.verbose\" argument must be a function.");
1382+
return;
1383+
}
1384+
args.This()->SetInternalField(kVerboseCallback,
1385+
verbose_v.As<Function>());
1386+
}
13611387
}
13621388

13631389
new DatabaseSync(
@@ -2540,6 +2566,55 @@ int DatabaseSync::AuthorizerCallback(void* user_data,
25402566
return int_result;
25412567
}
25422568

2569+
int DatabaseSync::TraceCallback(unsigned int type,
2570+
void* user_data,
2571+
void* p,
2572+
void* x) {
2573+
if (type != SQLITE_TRACE_STMT) {
2574+
return 0;
2575+
}
2576+
2577+
DatabaseSync* db = static_cast<DatabaseSync*>(user_data);
2578+
Environment* env = db->env();
2579+
Isolate* isolate = env->isolate();
2580+
HandleScope handle_scope(isolate);
2581+
Local<Context> context = env->context();
2582+
2583+
Local<Value> cb =
2584+
db->object()->GetInternalField(kVerboseCallback).template As<Value>();
2585+
2586+
if (!cb->IsFunction()) {
2587+
return 0;
2588+
}
2589+
2590+
char* expanded = sqlite3_expanded_sql(static_cast<sqlite3_stmt*>(p));
2591+
Local<Value> sql_string;
2592+
if (expanded != nullptr) {
2593+
bool ok = String::NewFromUtf8(isolate, expanded).ToLocal(&sql_string);
2594+
sqlite3_free(expanded);
2595+
if (!ok) {
2596+
return 0;
2597+
}
2598+
} else {
2599+
// Fallback to source SQL if expanded is unavailable
2600+
const char* source = sqlite3_sql(static_cast<sqlite3_stmt*>(p));
2601+
if (source == nullptr || !String::NewFromUtf8(isolate, source)
2602+
.ToLocal(&sql_string)) {
2603+
return 0;
2604+
}
2605+
}
2606+
2607+
Local<Function> callback = cb.As<Function>();
2608+
MaybeLocal<Value> retval =
2609+
callback->Call(context, Undefined(isolate), 1, &sql_string);
2610+
2611+
if (retval.IsEmpty()) {
2612+
db->SetIgnoreNextSQLiteError(true);
2613+
}
2614+
2615+
return 0;
2616+
}
2617+
25432618
StatementSync::StatementSync(Environment* env,
25442619
Local<Object> object,
25452620
BaseObjectPtr<DatabaseSync> db,

src/node_sqlite.h

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

@@ -205,6 +206,10 @@ class DatabaseSync : public BaseObject {
205206
const char* param2,
206207
const char* param3,
207208
const char* param4);
209+
static int TraceCallback(unsigned int type,
210+
void* user_data,
211+
void* p,
212+
void* x);
208213
void FinalizeStatements();
209214
void RemoveBackup(BackupJob* backup);
210215
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)