Skip to content
Draft
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
3 changes: 3 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,9 @@ added: v22.5.0
Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
around [`sqlite3_prepare_v2()`][].

Throws an `ERR_INVALID_ARG_VALUE` error if `sql` contains no statement, such as
an empty string or a lone comment.

### `database.createTagStore([maxSize])`

<!-- YAML
Expand Down
17 changes: 17 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,16 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, nullptr);

CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void());

// sqlite3_prepare_v2() reports success without producing a statement when
// the input holds no SQL, such as a comment. Such a statement can never be
// stepped, and tracking it would leave a dangling pointer in statements_
// because its destructor treats a null statement as already finalized.
if (s == nullptr) {
THROW_ERR_INVALID_ARG_VALUE(env, "The SQL query contains no statement.");
return;
}

BaseObjectPtr<StatementSync> stmt =
StatementSync::Create(env, BaseObjectPtr<DatabaseSync>(db), s);
db->statements_.insert(stmt.get());
Expand Down Expand Up @@ -3648,6 +3658,13 @@ BaseObjectPtr<StatementSync> SQLTagStore::PrepareStatement(
return BaseObjectPtr<StatementSync>();
}

// As in DatabaseSync::Prepare(), reject input that holds no SQL rather
// than caching a statement that can never be bound or stepped.
if (s == nullptr) {
THROW_ERR_INVALID_ARG_VALUE(env, "The SQL query contains no statement.");
return BaseObjectPtr<StatementSync>();
}

BaseObjectPtr<StatementSync> stmt_obj = StatementSync::Create(
env, BaseObjectPtr<DatabaseSync>(session->database_), s);

Expand Down
26 changes: 26 additions & 0 deletions test/parallel/test-sqlite-database-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,32 @@ suite('DatabaseSync.prototype.prepare()', () => {
message: /The "sql" argument must be a string/,
});
});

test('throws if sql contains no statement', (t) => {
using db = new DatabaseSync(nextDb());

for (const sql of ['', ' ', ';', '-- comment', '/* comment */']) {
t.assert.throws(() => {
db.prepare(sql);
}, {
code: 'ERR_INVALID_ARG_VALUE',
message: /contains no statement/,
});
}
});

test('prepares statements that contain comments', (t) => {
using db = new DatabaseSync(nextDb());
const queries = [
'-- lead\nSELECT 1 AS v',
'SELECT 1 AS v -- trail',
'SELECT /* mid */ 1 AS v',
];

for (const sql of queries) {
t.assert.strictEqual(db.prepare(sql).get().v, 1);
}
});
});

suite('DatabaseSync.prototype.exec()', () => {
Expand Down
23 changes: 23 additions & 0 deletions test/parallel/test-sqlite-template-tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,29 @@ test('sql error messages are descriptive', () => {
});
});

test('rejects SQL that contains no statement', () => {
const expectedError = {
code: 'ERR_INVALID_ARG_VALUE',
message: /contains no statement/,
};

for (const method of ['run', 'get', 'all', 'iterate']) {
assert.throws(() => {
// eslint-disable-next-line no-unused-expressions
sql[method]`-- comment`;
}, expectedError);

assert.throws(() => {
// eslint-disable-next-line no-unused-expressions
sql[method]``;
}, expectedError);
}

// A rejected statement must not be cached, so a later valid query with the
// same tag store still works.
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
});

test('a tag store keeps the database alive by itself', () => {
const sql = new DatabaseSync(':memory:').createTagStore();

Expand Down