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 statements, such as
an empty string or a lone comment.

Comment on lines +689 to +691

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is somewhat unnecessary, if a user is passing an empty SQL query then that's on them

### `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 statements.");
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 statements.");
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 statements', (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 statements/,
});
}
});

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 statements', () => {
const expectedError = {
code: 'ERR_INVALID_ARG_VALUE',
message: /contains no statements/,
};

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