Repository: denoland/deno
Pull request: fix(ext/node): Add sqlite allow unknown named params option

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->

Adds [statement.setAllowUnknownNamedParameters(enabled)](https://nodejs.org/api/sqlite.html#statementsetallowunknownnamedparametersenabled) to node:sqlite.

Closes #31196

This is my first commit to deno and I'm very new to rust so apologies in advance for any dumb mistakes!

I've mostly copied the implementation of `setAllowBareNamedParams` so hopefully it's mostly correct.

Changed files:
filename: ext/node/ops/sqlite/database.rs
patch: @@ -569,6 +569,7 @@ impl DatabaseSync {
       statements: Rc::clone(&self.statements),
       use_big_ints: Cell::new(false),
       allow_bare_named_params: Cell::new(true),
+      allow_unknown_named_params: Cell::new(false),
       is_iter_finished: false,
     })
   }

filename: ext/node/ops/sqlite/statement.rs
patch: @@ -70,6 +70,7 @@ pub struct StatementSync {
 
   pub use_big_ints: Cell<bool>,
   pub allow_bare_named_params: Cell<bool>,
+  pub allow_unknown_named_params: Cell<bool>,
 
   pub is_iter_finished: bool,
 }
@@ -487,6 +488,10 @@ impl StatementSync {
             }
 
             if r == 0 {
+              if self.allow_unknown_named_params.get() {
+                continue;
+              }
+
               return Err(SqliteError::UnknownNamedParameter(
                 key_c.into_string().unwrap(),
               ));
@@ -762,6 +767,16 @@ impl StatementSync {
     Ok(())
   }
 
+  #[fast]
+  #[undefined]
+  fn set_allow_unknown_named_parameters(
+    &self,
+    #[validate(validators::allow_unknown_named_params_bool)] enabled: bool,
+  ) -> Result<(), SqliteError> {
+    self.allow_unknown_named_params.set(enabled);
+    Ok(())
+  }
+
   #[fast]
   #[undefined]
   fn set_read_big_ints(

filename: ext/node/ops/sqlite/validators.rs
patch: @@ -101,6 +101,19 @@ pub(super) fn allow_bare_named_params_bool(
   ))
 }
 
+pub(super) fn allow_unknown_named_params_bool(
+  _: &mut v8::PinScope<'_, '_>,
+  value: v8::Local<v8::Value>,
+) -> Result<(), Error> {
+  if value.is_boolean() {
+    return Ok(());
+  }
+
+  Err(Error::InvalidArgType(
+    "The \"allowUnknownNamedParameters\" argument must be a boolean.",
+  ))
+}
+
 pub(super) fn read_big_ints_bool(
   _: &mut v8::PinScope<'_, '_>,
   value: v8::Local<v8::Value>,

filename: tests/unit_node/sqlite_test.ts
patch: @@ -257,6 +257,44 @@ Deno.test("[node/sqlite] query should handle mixed positional and named paramete
   db.close();
 });
 
+Deno.test("[node/sqlite] StatementSync unknown named parameters should throw", () => {
+  const db = new DatabaseSync(":memory:");
+  db.exec(
+    "CREATE TABLE foo (id INTEGER PRIMARY KEY, variable1 TEXT NOT NULL, variable2 INT NOT NULL)",
+  );
+
+  const stmt = db.prepare(
+    "INSERT INTO foo (variable1, variable2) VALUES (:variable1, :variable2) RETURNING id",
+  );
+
+  assertThrows(() =>
+    stmt.run(
+      { variable1: "bar", variable2: 1, variable3: "baz" },
+    )
+  );
+
+  db.close();
+});
+
+// https://github.com/denoland/deno/issues/31196
+Deno.test("[node/sqlite] StatementSync unknown named parameters can be ignored", () => {
+  const db = new DatabaseSync(":memory:");
+  db.exec(
+    "CREATE TABLE foo (id INTEGER PRIMARY KEY, variable1 TEXT NOT NULL, variable2 INT NOT NULL)",
+  );
+
+  const stmt = db.prepare(
+    "INSERT INTO foo (variable1, variable2) VALUES (:variable1, :variable2) RETURNING id",
+  );
+  stmt.setAllowUnknownNamedParameters(true);
+
+  const result = stmt.run({ variable1: "bar", variable2: 1, variable3: "baz" });
+
+  assertEquals(result, { lastInsertRowid: 1, changes: 1 });
+
+  db.close();
+});
+
 Deno.test("[node/sqlite] StatementSync#iterate", () => {
   const db = new DatabaseSync(":memory:");
   const stmt = db.prepare("SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3");