diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 186e70784b94d0..22faccb6bad92d 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -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])` * `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 diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 6d35236dce0f82..78e928de206e9d 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -1147,6 +1147,92 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { return; } + std::optional return_arrays; + std::optional use_big_ints; + std::optional allow_bare_named_params; + std::optional 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 options = args[1].As(); + + Local 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 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 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 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()); sqlite3_stmt* s = nullptr; int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, 0); @@ -1155,6 +1241,20 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { BaseObjectPtr stmt = StatementSync::Create(env, BaseObjectPtr(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()); } diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 2641c9d4f1e8c5..27622a15dbf1bb 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -246,6 +246,7 @@ class StatementSync : public BaseObject { bool BindParams(const v8::FunctionCallbackInfo& args); bool BindValue(const v8::Local& value, const int index); + friend class DatabaseSync; friend class StatementSyncIterator; friend class SQLTagStore; friend class StatementExecutionHelper; diff --git a/test/parallel/test-sqlite-named-parameters.js b/test/parallel/test-sqlite-named-parameters.js index e1acd0f38fa2f7..db8f46e6b6ce5a 100644 --- a/test/parallel/test-sqlite-named-parameters.js +++ b/test/parallel/test-sqlite-named-parameters.js @@ -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'/, + }); + }); +}); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index 04494a02c692a8..62e95363f1c46a 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -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 }, + ); + }); +});