Repository: nodejs/node
Pull request: New SQlite database options

This introduces new SQLite database options that can be set at the database connection level.

cc @geeksilva97 

Changed files:
filename: doc/api/sqlite.md
patch: @@ -97,6 +97,10 @@ exposed by this class execute synchronously.
 
 <!-- YAML
 added: v22.5.0
+changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/58697
+    description: Add new SQLite database options.
 -->
 
 * `path` {string | Buffer | URL} The path of the database. A SQLite database can be
@@ -126,6 +130,14 @@ added: v22.5.0
   * `timeout` {number} The [busy timeout][] in milliseconds. This is the maximum amount of
     time that SQLite will wait for a database lock to be released before
     returning an error. **Default:** `0`.
+  * `readBigInts` {boolean} If `true`, integer fields are read as JavaScript `BigInt` values. If `false`,
+    integer fields are read as JavaScript numbers. **Default:** `false`.
+  * `returnArrays` {boolean} If `true`, query results are returned as arrays instead of objects.
+    **Default:** `false`.
+  * `allowBareNamedParameters` {boolean} If `true`, allows binding named parameters without the prefix
+    character (e.g., `foo` instead of `:foo`). **Default:** `true`.
+  * `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters are ignored when binding.
+    If `false`, an exception is thrown for unknown named parameters. **Default:** `false`.
 
 Constructs a new `DatabaseSync` instance.
 

filename: src/env_properties.h
patch: @@ -72,6 +72,8 @@
   V(ack_string, "ack")                                                         \
   V(address_string, "address")                                                 \
   V(aliases_string, "aliases")                                                 \
+  V(allow_bare_named_params_string, "allowBareNamedParameters")                \
+  V(allow_unknown_named_params_string, "allowUnknownNamedParameters")          \
   V(alpn_callback_string, "ALPNCallback")                                      \
   V(args_string, "args")                                                       \
   V(asn1curve_string, "asn1Curve")                                             \
@@ -324,6 +326,7 @@
   V(raw_string, "raw")                                                         \
   V(read_host_object_string, "_readHostObject")                                \
   V(readable_string, "readable")                                               \
+  V(read_bigints_string, "readBigInts")                                        \
   V(reason_string, "reason")                                                   \
   V(refresh_string, "refresh")                                                 \
   V(regexp_string, "regexp")                                                   \

filename: src/node_sqlite.cc
patch: @@ -966,6 +966,66 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
 
       open_config.set_timeout(timeout_v.As<Int32>()->Value());
     }
+
+    Local<Value> read_bigints_v;
+    if (options->Get(env->context(), env->read_bigints_string())
+            .ToLocal(&read_bigints_v)) {
+      if (!read_bigints_v->IsUndefined()) {
+        if (!read_bigints_v->IsBoolean()) {
+          THROW_ERR_INVALID_ARG_TYPE(
+              env->isolate(),
+              R"(The "options.readBigInts" argument must be a boolean.)");
+          return;
+        }
+        open_config.set_use_big_ints(read_bigints_v.As<Boolean>()->Value());
+      }
+    }
+
+    Local<Value> return_arrays_v;
+    if (options->Get(env->context(), env->return_arrays_string())
+            .ToLocal(&return_arrays_v)) {
+      if (!return_arrays_v->IsUndefined()) {
+        if (!return_arrays_v->IsBoolean()) {
+          THROW_ERR_INVALID_ARG_TYPE(
+              env->isolate(),
+              R"(The "options.returnArrays" argument must be a boolean.)");
+          return;
+        }
+        open_config.set_return_arrays(return_arrays_v.As<Boolean>()->Value());
+      }
+    }
+
+    Local<Value> allow_bare_named_params_v;
+    if (options->Get(env->context(), env->allow_bare_named_params_string())
+            .ToLocal(&allow_bare_named_params_v)) {
+      if (!allow_bare_named_params_v->IsUndefined()) {
+        if (!allow_bare_named_params_v->IsBoolean()) {
+          THROW_ERR_INVALID_ARG_TYPE(
+              env->isolate(),
+              R"(The "options.allowBareNamedParameters" )"
+              "argument must be a boolean.");
+          return;
+        }
+        open_config.set_allow_bare_named_params(
+            allow_bare_named_params_v.As<Boolean>()->Value());
+      }
+    }
+
+    Local<Value> allow_unknown_named_params_v;
+    if (options->Get(env->context(), env->allow_unknown_named_params_string())
+            .ToLocal(&allow_unknown_named_params_v)) {
+      if (!allow_unknown_named_params_v->IsUndefined()) {
+        if (!allow_unknown_named_params_v->IsBoolean()) {
+          THROW_ERR_INVALID_ARG_TYPE(
+              env->isolate(),
+              R"(The "options.allowUnknownNamedParameters" )"
+              "argument must be a boolean.");
+          return;
+        }
+        open_config.set_allow_unknown_named_params(
+            allow_unknown_named_params_v.As<Boolean>()->Value());
+      }
+    }
   }
 
   new DatabaseSync(
@@ -1772,12 +1832,11 @@ StatementSync::StatementSync(Environment* env,
     : BaseObject(env, object), db_(std::move(db)) {
   MakeWeak();
   statement_ = stmt;
-  // In the future, some of these options could be set at the database
-  // connection level and inherited by statements to reduce boilerplate.
-  return_arrays_ = false;
-  use_big_ints_ = false;
-  allow_bare_named_params_ = true;
-  allow_unknown_named_params_ = false;
+  use_big_ints_ = db_->use_big_ints();
+  return_arrays_ = db_->return_arrays();
+  allow_bare_named_params_ = db_->allow_bare_named_params();
+  allow_unknown_named_params_ = db_->allow_unknown_named_params();
+
   bare_named_params_ = std::nullopt;
 }
 

filename: src/node_sqlite.h
patch: @@ -39,12 +39,40 @@ class DatabaseOpenConfiguration {
 
   inline int get_timeout() { return timeout_; }
 
+  inline void set_use_big_ints(bool flag) { use_big_ints_ = flag; }
+
+  inline bool get_use_big_ints() const { return use_big_ints_; }
+
+  inline void set_return_arrays(bool flag) { return_arrays_ = flag; }
+
+  inline bool get_return_arrays() const { return return_arrays_; }
+
+  inline void set_allow_bare_named_params(bool flag) {
+    allow_bare_named_params_ = flag;
+  }
+
+  inline bool get_allow_bare_named_params() const {
+    return allow_bare_named_params_;
+  }
+
+  inline void set_allow_unknown_named_params(bool flag) {
+    allow_unknown_named_params_ = flag;
+  }
+
+  inline bool get_allow_unknown_named_params() const {
+    return allow_unknown_named_params_;
+  }
+
  private:
   std::string location_;
   bool read_only_ = false;
   bool enable_foreign_keys_ = true;
   bool enable_dqs_ = false;
   int timeout_ = 0;
+  bool use_big_ints_ = false;
+  bool return_arrays_ = false;
+  bool allow_bare_named_params_ = true;
+  bool allow_unknown_named_params_ = false;
 };
 
 class StatementSync;
@@ -82,6 +110,14 @@ class DatabaseSync : public BaseObject {
   void FinalizeBackups();
   void UntrackStatement(StatementSync* statement);
   bool IsOpen();
+  bool use_big_ints() const { return open_config_.get_use_big_ints(); }
+  bool return_arrays() const { return open_config_.get_return_arrays(); }
+  bool allow_bare_named_params() const {
+    return open_config_.get_allow_bare_named_params();
+  }
+  bool allow_unknown_named_params() const {
+    return open_config_.get_allow_unknown_named_params();
+  }
   sqlite3* Connection();
 
   // In some situations, such as when using custom functions, it is possible

filename: test/parallel/test-sqlite-database-sync.js
patch: @@ -174,6 +174,113 @@ suite('DatabaseSync() constructor', () => {
     t.after(() => { db.close(); });
     db.exec('SELECT "foo";');
   });
+
+  test('throws if options.readBigInts is provided but is not a boolean', (t) => {
+    t.assert.throws(() => {
+      new DatabaseSync('foo', { readBigInts: 42 });
+    }, {
+      code: 'ERR_INVALID_ARG_TYPE',
+      message: 'The "options.readBigInts" argument must be a boolean.',
+    });
+  });
+
+  test('allows reading big integers', (t) => {
+    const dbPath = nextDb();
+    const db = new DatabaseSync(dbPath, { readBigInts: true });
+    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');
+    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
+
+    const insert = db.prepare('INSERT INTO data (key) VALUES (?)');
+    t.assert.deepStrictEqual(
+      insert.run(20),
+      { changes: 1n, lastInsertRowid: 20n },
+    );
+  });
+
+  test('throws if options.returnArrays is provided but is not a boolean', (t) => {
+    t.assert.throws(() => {
+      new DatabaseSync('foo', { returnArrays: 42 });
+    }, {
+      code: 'ERR_INVALID_ARG_TYPE',
+      message: 'The "options.returnArrays" argument must be a boolean.',
+    });
+  });
+
+  test('allows returning arrays', (t) => {
+    const dbPath = nextDb();
+    const db = new DatabaseSync(dbPath, { returnArrays: true });
+    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 WHERE key = 1');
+    t.assert.deepStrictEqual(query.get(), [1, 'one']);
+  });
+
+  test('throws if options.allowBareNamedParameters is provided but is not a boolean', (t) => {
+    t.assert.throws(() => {
+      new DatabaseSync('foo', { allowBareNamedParameters: 42 });
+    }, {
+      code: 'ERR_INVALID_ARG_TYPE',
+      message: 'The "options.allowBareNamedParameters" argument must be a boolean.',
+    });
+  });
+
+  test('throws if bare named parameters are used when option is false', (t) => {
+    const dbPath = nextDb();
+    const db = new DatabaseSync(dbPath, { allowBareNamedParameters: false });
+    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)');
+    t.assert.throws(() => {
+      stmt.run({ k: 2, v: 4 });
+    }, {
+      code: 'ERR_INVALID_STATE',
+      message: /Unknown named parameter 'k'/,
+    });
+  });
+
+  test('throws if options.allowUnknownNamedParameters is provided but is not a boolean', (t) => {
+    t.assert.throws(() => {
+      new DatabaseSync('foo', { allowUnknownNamedParameters: 42 });
+    }, {
+      code: 'ERR_INVALID_ARG_TYPE',
+      message: 'The "options.allowUnknownNamedParameters" argument must be a boolean.',
+    });
+  });
+
+  test('allows unknown named parameters', (t) => {
+    const dbPath = nextDb();
+    const db = new DatabaseSync(dbPath, { allowUnknownNamedParameters: true });
+    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.deepStrictEqual(
+      stmt.run(params),
+      { changes: 1, lastInsertRowid: 1 },
+    );
+  });
 });
 
 suite('DatabaseSync.prototype.open()', () => {