Skip to content
Open
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
12 changes: 11 additions & 1 deletion doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,13 +453,23 @@ Opens the database specified in the `path` argument of the `DatabaseSync`
constructor. This method should only be used when the database is not opened via
the constructor. An exception is thrown if the database is already open.

### `database.prepare(sql)`
### `database.prepare(sql[, options])`

<!-- YAML
added: v22.5.0
-->

* `sql` {string} A SQL string to compile to a prepared statement.
* `options` {Object} Optional configuration for the prepared statement.
* `readBigInts` {boolean} If `true`, integer fields are read as `BigInt`s.
**Default:** inherited from database options or `false`.
* `returnArrays` {boolean} If `true`, results are returned as arrays.
**Default:** inherited from database options or `false`.
* `allowBareNamedParameters` {boolean} If `true`, allows binding named
parameters without the prefix character. **Default:** inherited from
database options or `true`.
* `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters
are ignored. **Default:** inherited from database options or `false`.
* Returns: {StatementSync} The prepared statement.

Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
Expand Down
100 changes: 100 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,92 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
return;
}

std::optional<bool> return_arrays;
std::optional<bool> use_big_ints;
std::optional<bool> allow_bare_named_params;
std::optional<bool> allow_unknown_named_params;

if (args.Length() > 1 && !args[1]->IsUndefined()) {
if (!args[1]->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"options\" argument must be an object.");
return;
}
Local<Object> options = args[1].As<Object>();

Local<Value> return_arrays_v;
if (!options
Copy link
Member

@ovflowd ovflowd Jan 14, 2026

Choose a reason for hiding this comment

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

ooc, are you eagerly returning if you're unable to allocate a string? (I'm assuming that's what this is doing)

Copy link
Member Author

@araujogui araujogui Jan 14, 2026

Choose a reason for hiding this comment

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

If an API method returns a MaybeLocal<>, the API method can potentially fail either because an exception is thrown, or because an exception is pending, e.g. because a previous API call threw an exception that hasn't been caught yet, or because a TerminateExecution exception was thrown. In that case, an empty MaybeLocal is returned.

https://v8.github.io/api/head/classv8_1_1MaybeLocal.html

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically, when it fails, an exception is already pending, so we return early to let it propagate.

->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "returnArrays"))
.ToLocal(&return_arrays_v)) {
return;
}
if (!return_arrays_v->IsUndefined()) {
if (!return_arrays_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.returnArrays\" argument must be a boolean.");
return;
}
return_arrays = return_arrays_v->IsTrue();
}

Local<Value> read_big_ints_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "readBigInts"))
.ToLocal(&read_big_ints_v)) {
return;
}
if (!read_big_ints_v->IsUndefined()) {
if (!read_big_ints_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.readBigInts\" argument must be a boolean.");
return;
}
use_big_ints = read_big_ints_v->IsTrue();
}

Local<Value> allow_bare_named_params_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(),
"allowBareNamedParameters"))
.ToLocal(&allow_bare_named_params_v)) {
return;
}
if (!allow_bare_named_params_v->IsUndefined()) {
if (!allow_bare_named_params_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.allowBareNamedParameters\" argument must be a "
"boolean.");
return;
}
allow_bare_named_params = allow_bare_named_params_v->IsTrue();
}

Local<Value> allow_unknown_named_params_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(),
"allowUnknownNamedParameters"))
.ToLocal(&allow_unknown_named_params_v)) {
return;
}
if (!allow_unknown_named_params_v->IsUndefined()) {
if (!allow_unknown_named_params_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.allowUnknownNamedParameters\" argument must be a "
"boolean.");
return;
}
allow_unknown_named_params = allow_unknown_named_params_v->IsTrue();
}
}

Utf8Value sql(env->isolate(), args[0].As<String>());
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, 0);
Expand All @@ -1155,6 +1241,20 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
BaseObjectPtr<StatementSync> stmt =
StatementSync::Create(env, BaseObjectPtr<DatabaseSync>(db), s);
db->statements_.insert(stmt.get());

if (return_arrays.has_value()) {
stmt->return_arrays_ = return_arrays.value();
}
if (use_big_ints.has_value()) {
stmt->use_big_ints_ = use_big_ints.value();
}
if (allow_bare_named_params.has_value()) {
stmt->allow_bare_named_params_ = allow_bare_named_params.value();
}
if (allow_unknown_named_params.has_value()) {
stmt->allow_unknown_named_params_ = allow_unknown_named_params.value();
}

args.GetReturnValue().Set(stmt->object());
}

Expand Down
1 change: 1 addition & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class StatementSync : public BaseObject {
bool BindParams(const v8::FunctionCallbackInfo<v8::Value>& args);
bool BindValue(const v8::Local<v8::Value>& value, const int index);

friend class DatabaseSync;
friend class StatementSyncIterator;
friend class SQLTagStore;
friend class StatementExecutionHelper;
Expand Down
100 changes: 100 additions & 0 deletions test/parallel/test-sqlite-named-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,103 @@ suite('StatementSync.prototype.setAllowUnknownNamedParameters()', () => {
});
});
});

suite('options.allowUnknownNamedParameters', () => {
test('unknown named parameters are allowed when input is true', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare(
'INSERT INTO data (key, val) VALUES ($k, $v)',
{ allowUnknownNamedParameters: true }
);
const params = { $a: 1, $b: 2, $k: 42, $y: 25, $v: 84, $z: 99 };
t.assert.deepStrictEqual(
stmt.run(params),
{ changes: 1, lastInsertRowid: 1 },
);
});

test('unknown named parameters throw when input is false', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare(
'INSERT INTO data (key, val) VALUES ($k, $v)',
{ allowUnknownNamedParameters: false }
);
const params = { $a: 1, $b: 2, $k: 42, $y: 25, $v: 84, $z: 99 };
t.assert.throws(() => {
stmt.run(params);
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter '\$a'/,
});
});

test('unknown named parameters throws error by default', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
const params = { $a: 1, $b: 2, $k: 42, $y: 25, $v: 84, $z: 99 };
t.assert.throws(() => {
stmt.run(params);
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter '\$a'/,
});
});

test('throws when option is not a boolean', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
t.assert.throws(() => {
db.prepare(
'INSERT INTO data (key, val) VALUES ($k, $v)',
{ allowUnknownNamedParameters: 'true' }
);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.allowUnknownNamedParameters" argument must be a boolean/,
});
});

test('setAllowUnknownNamedParameters can override prepare option', (t) => {
const db = new DatabaseSync(':memory:');
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare(
'INSERT INTO data (key, val) VALUES ($k, $v)',
{ allowUnknownNamedParameters: true }
);
const params = { $a: 1, $b: 2, $k: 42, $y: 25, $v: 84, $z: 99 };
t.assert.deepStrictEqual(
stmt.run(params),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.strictEqual(stmt.setAllowUnknownNamedParameters(false), undefined);
t.assert.throws(() => {
stmt.run(params);
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter '\$a'/,
});
});
});
Loading
Loading