sqlite: add sqlite prepare options args

PR-URL: https://github.com/nodejs/node/pull/61311
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Claudio Wunder <cwunder@gnome.org>
This commit is contained in:
Guilherme Araújo authored and GitHub committed 2026-01-19 23:27:03 +00:00
1 parent bc9ffc32bd
commit efa7621132
5 files changed
+457 -1

No files matched your search

+11 -1
View File
@@ -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
+100
View File
@@ -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
->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);
@@ -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());
}
+1
View File
@@ -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;
@@ -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'/,
});
});
});
+245
View File
@@ -609,3 +609,248 @@ suite('StatementSync.prototype.setAllowBareNamedParameters()', () => {
});
});
});
suite('options.readBigInts', () => {
test('BigInts are returned when input is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
INSERT INTO data (key, val) VALUES (1, 42);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT val FROM data', { readBigInts: true });
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
});
test('numbers are returned when input is false', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
INSERT INTO data (key, val) VALUES (1, 42);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT val FROM data', { readBigInts: false });
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
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('SELECT val FROM data', { readBigInts: 'true' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.readBigInts" argument must be a boolean/,
});
});
test('setReadBigInts can override prepare option', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
INSERT INTO data (key, val) VALUES (1, 42);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT val FROM data', { readBigInts: true });
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
t.assert.strictEqual(query.setReadBigInts(false), undefined);
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
});
});
suite('options.returnArrays', () => {
test('arrays are returned when input is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare(
'SELECT key, val FROM data WHERE key = 1',
{ returnArrays: true }
);
t.assert.deepStrictEqual(query.get(), [1, 'one']);
});
test('objects are returned when input is false', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare(
'SELECT key, val FROM data WHERE key = 1',
{ returnArrays: false }
);
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;'
);
t.assert.strictEqual(setup, undefined);
t.assert.throws(() => {
db.prepare('SELECT key, val FROM data', { returnArrays: 'true' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.returnArrays" argument must be a boolean/,
});
});
test('setReturnArrays can override prepare option', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare(
'SELECT key, val FROM data WHERE key = 1',
{ returnArrays: true }
);
t.assert.deepStrictEqual(query.get(), [1, 'one']);
t.assert.strictEqual(query.setReturnArrays(false), undefined);
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
});
test('all() returns arrays when input is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
INSERT INTO data (key, val) VALUES (2, 'two');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare(
'SELECT key, val FROM data ORDER BY key',
{ returnArrays: true }
);
t.assert.deepStrictEqual(query.all(), [
[1, 'one'],
[2, 'two'],
]);
});
test('iterate() returns arrays when input is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
INSERT INTO data (key, val) VALUES (1, 'one');
INSERT INTO data (key, val) VALUES (2, 'two');
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare(
'SELECT key, val FROM data ORDER BY key',
{ returnArrays: true }
);
t.assert.deepStrictEqual(query.iterate().toArray(), [
[1, 'one'],
[2, 'two'],
]);
});
});
suite('options.allowBareNamedParameters', () => {
test('bare named parameters are allowed when input is true', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare(
'INSERT INTO data (key, val) VALUES ($k, $v)',
{ allowBareNamedParameters: true }
);
t.assert.deepStrictEqual(
stmt.run({ k: 1, v: 2 }),
{ changes: 1, lastInsertRowid: 1 },
);
});
test('bare named parameters throw when input is false', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare(
'INSERT INTO data (key, val) VALUES ($k, $v)',
{ allowBareNamedParameters: false }
);
t.assert.throws(() => {
stmt.run({ k: 1, v: 2 });
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter 'k'/,
});
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
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)',
{ allowBareNamedParameters: 'true' }
);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.allowBareNamedParameters" argument must be a boolean/,
});
});
test('setAllowBareNamedParameters can override prepare option', (t) => {
const db = new DatabaseSync(nextDb());
t.after(() => { db.close(); });
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare(
'INSERT INTO data (key, val) VALUES ($k, $v)',
{ allowBareNamedParameters: false }
);
t.assert.throws(() => {
stmt.run({ k: 1, v: 2 });
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter 'k'/,
});
t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined);
t.assert.deepStrictEqual(
stmt.run({ k: 2, v: 4 }),
{ changes: 1, lastInsertRowid: 2 },
);
});
});