Repository: denoland/deno
Pull request: fix(ext/node): enable defensive option on `DatabaseSync`

Allows https://github.com/nodejs/node/blob/v24.12.0/test/parallel/test-sqlite-config.js to pass

Changed files:
filename: ext/node/ops/sqlite/database.rs
patch: @@ -23,6 +23,7 @@ use deno_core::v8_static_strings;
 use deno_permissions::OpenAccessKind;
 use deno_permissions::PermissionsContainer;
 use rusqlite::ffi as libsqlite3_sys;
+use rusqlite::ffi::SQLITE_DBCONFIG_DEFENSIVE;
 use rusqlite::ffi::SQLITE_DBCONFIG_DQS_DDL;
 use rusqlite::ffi::SQLITE_DBCONFIG_DQS_DML;
 use rusqlite::ffi::sqlite3_create_window_function;
@@ -53,6 +54,7 @@ struct DatabaseSyncOptions {
   allow_bare_named_params: bool,
   return_arrays: bool,
   allow_unknown_named_params: bool,
+  is_defensive_mode: bool,
   timeout: u64,
 }
 
@@ -88,6 +90,7 @@ impl<'a> FromV8<'a> for DatabaseSyncOptions {
       RETURN_ARRAYS = "returnArrays",
       ALLOW_BARE_NAMED_PARAMS = "allowBareNamedParameters",
       ALLOW_UNKNOWN_NAMED_PARAMS = "allowUnknownNamedParameters",
+      DEFENSIVE_STRING = "defensive",
     }
 
     let open_string = OPEN_STRING.v8_string(scope).unwrap();
@@ -243,6 +246,20 @@ impl<'a> FromV8<'a> for DatabaseSyncOptions {
           .is_true();
     }
 
+    let defensive_string = DEFENSIVE_STRING.v8_string(scope).unwrap();
+    if let Some(is_defensive_mode) = obj.get(scope, defensive_string.into())
+      && !is_defensive_mode.is_undefined()
+    {
+      options.is_defensive_mode =
+        v8::Local::<v8::Boolean>::try_from(is_defensive_mode)
+          .map_err(|_| {
+            Error::InvalidArgType(
+              "The \"options.defensive\" argument must be a boolean.",
+            )
+          })?
+          .is_true();
+    }
+
     Ok(options)
   }
 }
@@ -259,6 +276,7 @@ impl Default for DatabaseSyncOptions {
       return_arrays: false,
       allow_bare_named_params: true,
       allow_unknown_named_params: false,
+      is_defensive_mode: false,
       timeout: 0,
     }
   }
@@ -564,6 +582,12 @@ fn open_db(
       ));
     }
 
+    assert!(set_db_config(
+      &conn,
+      SQLITE_DBCONFIG_DEFENSIVE,
+      options.is_defensive_mode,
+    ));
+
     return Ok(conn);
   }
 
@@ -602,6 +626,12 @@ fn open_db(
       ));
     }
 
+    assert!(set_db_config(
+      &conn,
+      SQLITE_DBCONFIG_DEFENSIVE,
+      options.is_defensive_mode,
+    ));
+
     return Ok(conn);
   }
 
@@ -622,6 +652,12 @@ fn open_db(
     conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?;
   }
 
+  assert!(set_db_config(
+    &conn,
+    SQLITE_DBCONFIG_DEFENSIVE,
+    options.is_defensive_mode,
+  ));
+
   Ok(conn)
 }
 
@@ -1356,6 +1392,18 @@ impl DatabaseSync {
     Ok(filename.into())
   }
 
+  #[fast]
+  #[validate(is_open)]
+  fn enable_defensive(
+    &self,
+    #[validate(validators::active_bool)] is_enabled: bool,
+  ) -> Result<(), SqliteError> {
+    let db = self.conn.borrow();
+    let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
+    assert!(set_db_config(conn, SQLITE_DBCONFIG_DEFENSIVE, is_enabled));
+    Ok(())
+  }
+
   #[getter]
   fn is_open(&self) -> bool {
     self.conn.borrow().is_some()

filename: ext/node/ops/sqlite/validators.rs
patch: @@ -146,3 +146,16 @@ pub(super) fn return_arrays_bool(
     "The \"returnArrays\" argument must be a boolean.",
   ))
 }
+
+pub(super) fn active_bool(
+  _: &mut v8::PinScope<'_, '_>,
+  value: v8::Local<v8::Value>,
+) -> Result<(), Error> {
+  if value.is_boolean() {
+    return Ok(());
+  }
+
+  Err(Error::InvalidArgType(
+    "The \"active\" argument must be a boolean.",
+  ))
+}

filename: tests/node_compat/config.jsonc
patch: @@ -1131,6 +1131,7 @@
     "parallel/test-socket-write-after-fin-error.js": {},
     // TODO(bartlomieju): disabled during work on `node:inspector`, this test didn't actualy run before
     // "parallel/test-source-map-enable.js": {},
+    "parallel/test-sqlite-config.js": {},
     "parallel/test-sqlite-custom-functions.js": {},
     "parallel/test-sqlite-data-types.js": {},
     "parallel/test-sqlite-database-sync-dispose.js": {},