Repository: nodejs/node
Pull request: sqlite: add sqlite prepare options args

Fixes #61235

Changed files:
filename: doc/api/sqlite.md
patch: @@ -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

filename: src/node_sqlite.cc
patch: @@ -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());
 }
 

filename: src/node_sqlite.h
patch: @@ -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;

filename: test/parallel/test-sqlite-named-parameters.js
patch: @@ -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'/,
+    });
+  });
+});

filename: test/parallel/test-sqlite-statement-sync.js
patch: @@ -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 },
+    );
+  });
+});