Skip to content

Commit 45be4bd

Browse files
committed
sqlite: reject statement-less SQL in prepare()
Apply the same check to DatabaseSync::Prepare() so that statement-less SQL is rejected at preparation instead of on first use. This matches SQLite's own oo1 JavaScript API, which throws when the SQL contains no statements rather than exposing the C API's null statement pointer. Previously db.prepare('-- comment') returned a StatementSync whose statement_ was null. Every method on it threw "statement has been finalized", which was misleading because nothing had been finalized, and the object was still inserted into statements_. Since IsFinalized() is true for a null statement, its destructor skipped UntrackStatement() and left a dangling pointer in the set that a later close() would finalize. Refs: #65157 (comment) Refs: https://sqlite.org/wasm/doc/trunk/api-oo1.md Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
1 parent fea9a8e commit 45be4bd

3 files changed

Lines changed: 42 additions & 3 deletions

File tree

doc/api/sqlite.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,10 @@ console.log(query.get());
668668

669669
<!-- YAML
670670
added: v22.5.0
671+
changes:
672+
- version: REPLACEME
673+
pr-url: https://github.com/nodejs/node/pull/65157
674+
description: Throw `ERR_INVALID_ARG_VALUE` if `sql` contains no statements.
671675
-->
672676

673677
* `sql` {string} A SQL string to compile to a prepared statement.

src/node_sqlite.cc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,16 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
15751575
int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, nullptr);
15761576

15771577
CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void());
1578+
1579+
// sqlite3_prepare_v2() reports success without producing a statement when
1580+
// the input holds no SQL, such as a comment. Such a statement can never be
1581+
// stepped, and tracking it would leave a dangling pointer in statements_
1582+
// because its destructor treats a null statement as already finalized.
1583+
if (s == nullptr) {
1584+
THROW_ERR_INVALID_ARG_VALUE(env, "The SQL query contains no statements.");
1585+
return;
1586+
}
1587+
15781588
BaseObjectPtr<StatementSync> stmt =
15791589
StatementSync::Create(env, BaseObjectPtr<DatabaseSync>(db), s);
15801590
db->statements_.insert(stmt.get());
@@ -3648,9 +3658,8 @@ BaseObjectPtr<StatementSync> SQLTagStore::PrepareStatement(
36483658
return BaseObjectPtr<StatementSync>();
36493659
}
36503660

3651-
// sqlite3_prepare_v2() reports success without producing a statement when
3652-
// the input holds no SQL, such as a comment. Such a statement cannot be
3653-
// bound or executed, so reject it instead of caching it.
3661+
// As in DatabaseSync::Prepare(), reject input that holds no SQL rather
3662+
// than caching a statement that can never be bound or stepped.
36543663
if (s == nullptr) {
36553664
THROW_ERR_INVALID_ARG_VALUE(env, "The SQL query contains no statements.");
36563665
return BaseObjectPtr<StatementSync>();

test/parallel/test-sqlite-database-sync.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,32 @@ suite('DatabaseSync.prototype.prepare()', () => {
397397
message: /The "sql" argument must be a string/,
398398
});
399399
});
400+
401+
test('throws if sql contains no statements', (t) => {
402+
using db = new DatabaseSync(nextDb());
403+
404+
for (const sql of ['', ' ', ';', '-- comment', '/* comment */']) {
405+
t.assert.throws(() => {
406+
db.prepare(sql);
407+
}, {
408+
code: 'ERR_INVALID_ARG_VALUE',
409+
message: /contains no statements/,
410+
});
411+
}
412+
});
413+
414+
test('prepares statements that contain comments', (t) => {
415+
using db = new DatabaseSync(nextDb());
416+
const queries = [
417+
'-- lead\nSELECT 1 AS v',
418+
'SELECT 1 AS v -- trail',
419+
'SELECT /* mid */ 1 AS v',
420+
];
421+
422+
for (const sql of queries) {
423+
t.assert.strictEqual(db.prepare(sql).get().v, 1);
424+
}
425+
});
400426
});
401427

402428
suite('DatabaseSync.prototype.exec()', () => {

0 commit comments

Comments
 (0)