// Copyright 2018-2026 the Deno authors. MIT license.

use std::borrow::Cow;
use std::cell::Cell;
use std::cell::RefCell;
use std::ffi::CStr;
use std::ffi::CString;
use std::ffi::c_char;
use std::ffi::c_void;
use std::path::Path;
use std::ptr::NonNull;
use std::ptr::null;
use std::rc::Rc;

use deno_core::FromV8;
use deno_core::GarbageCollected;
use deno_core::OpState;
use deno_core::convert::OptionUndefined;
use deno_core::cppgc;
use deno_core::op2;
use deno_core::v8;
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;
use rusqlite::ffi::sqlite3_db_filename;
use rusqlite::limits::Limit;

use super::Session;
use super::SqliteError;
use super::StatementSync;
use super::session::SessionOptions;
use super::sql_tag_store::SQLTagStore;
use super::statement::InnerStatementPtr;
use super::statement::check_error_code;
use super::statement::check_error_code2;
use super::validators;

const SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: i32 = 1005;
const SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE: i32 = 1021;
const MAX_SAFE_JS_INTEGER: i64 = 9_007_199_254_740_991;

const NUM_LIMITS: usize = 11;

/// Static mapping of JavaScript property names to SQLite limits.
/// Order matches SQLite limit constant values (0-10).
/// Keep in sync with LIMIT_NAMES in ext/node/polyfills/sqlite.ts.
const LIMIT_MAPPING: [(&str, Limit); NUM_LIMITS] = [
  ("length", Limit::SQLITE_LIMIT_LENGTH),
  ("sqlLength", Limit::SQLITE_LIMIT_SQL_LENGTH),
  ("column", Limit::SQLITE_LIMIT_COLUMN),
  ("exprDepth", Limit::SQLITE_LIMIT_EXPR_DEPTH),
  ("compoundSelect", Limit::SQLITE_LIMIT_COMPOUND_SELECT),
  ("vdbeOp", Limit::SQLITE_LIMIT_VDBE_OP),
  ("functionArg", Limit::SQLITE_LIMIT_FUNCTION_ARG),
  ("attach", Limit::SQLITE_LIMIT_ATTACHED),
  ("likePatternLength", Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH),
  ("variableNumber", Limit::SQLITE_LIMIT_VARIABLE_NUMBER),
  ("triggerDepth", Limit::SQLITE_LIMIT_TRIGGER_DEPTH),
];

struct DatabaseSyncOptions {
  open: bool,
  enable_foreign_key_constraints: bool,
  read_only: bool,
  allow_extension: bool,
  enable_double_quoted_string_literals: bool,
  use_big_int_arguments: bool,
  allow_bare_named_params: bool,
  return_arrays: bool,
  allow_unknown_named_params: bool,
  is_defensive_mode: bool,
  timeout: u64,
  initial_limits: [Option<i32>; NUM_LIMITS],
}

impl<'a> FromV8<'a> for DatabaseSyncOptions {
  type Error = validators::Error;

  fn from_v8(
    scope: &mut v8::PinScope<'a, '_>,
    value: v8::Local<'a, v8::Value>,
  ) -> Result<Self, Self::Error> {
    use validators::Error;

    if value.is_undefined() {
      return Ok(Self::default());
    }

    let Ok(obj) = v8::Local::<v8::Object>::try_from(value) else {
      return Err(Error::InvalidArgType(
        "The \"options\" argument must be an object.".into(),
      ));
    };

    let mut options = Self::default();

    v8_static_strings! {
      OPEN_STRING = "open",
      ENABLE_FOREIGN_KEY_CONSTRAINTS_STRING = "enableForeignKeyConstraints",
      READ_ONLY_STRING = "readOnly",
      ALLOW_EXTENSION_STRING = "allowExtension",
      ENABLE_DOUBLE_QUOTED_STRING_LITERALS_STRING = "enableDoubleQuotedStringLiterals",
      TIMEOUT_STRING = "timeout",
      READ_BIG_INTS = "readBigInts",
      RETURN_ARRAYS = "returnArrays",
      ALLOW_BARE_NAMED_PARAMS = "allowBareNamedParameters",
      ALLOW_UNKNOWN_NAMED_PARAMS = "allowUnknownNamedParameters",
      DEFENSIVE_STRING = "defensive",
      LIMITS_STRING = "limits",
    }

    let open_string = OPEN_STRING.v8_string(scope).unwrap();
    if let Some(open) = obj.get(scope, open_string.into())
      && !open.is_undefined()
    {
      options.open = v8::Local::<v8::Boolean>::try_from(open)
        .map_err(|_| {
          Error::InvalidArgType(
            "The \"options.open\" argument must be a boolean.".into(),
          )
        })?
        .is_true();
    }

    let read_only_string = READ_ONLY_STRING.v8_string(scope).unwrap();
    if let Some(read_only) = obj.get(scope, read_only_string.into())
      && !read_only.is_undefined()
    {
      options.read_only = v8::Local::<v8::Boolean>::try_from(read_only)
        .map_err(|_| {
          Error::InvalidArgType(
            "The \"options.readOnly\" argument must be a boolean.".into(),
          )
        })?
        .is_true();
    }

    let enable_foreign_key_constraints_string =
      ENABLE_FOREIGN_KEY_CONSTRAINTS_STRING
        .v8_string(scope)
        .unwrap();
    if let Some(enable_foreign_key_constraints) =
      obj.get(scope, enable_foreign_key_constraints_string.into())
      && !enable_foreign_key_constraints.is_undefined()
    {
      options.enable_foreign_key_constraints =
          v8::Local::<v8::Boolean>::try_from(enable_foreign_key_constraints)
            .map_err(|_| {
              Error::InvalidArgType("The \"options.enableForeignKeyConstraints\" argument must be a boolean.".into())
            })?
            .is_true();
    }

    let allow_extension_string =
      ALLOW_EXTENSION_STRING.v8_string(scope).unwrap();
    if let Some(allow_extension) = obj.get(scope, allow_extension_string.into())
      && !allow_extension.is_undefined()
    {
      options.allow_extension =
        v8::Local::<v8::Boolean>::try_from(allow_extension)
          .map_err(|_| {
            Error::InvalidArgType(
              "The \"options.allowExtension\" argument must be a boolean."
                .into(),
            )
          })?
          .is_true();
    }

    let enable_double_quoted_string_literals_string =
      ENABLE_DOUBLE_QUOTED_STRING_LITERALS_STRING
        .v8_string(scope)
        .unwrap();
    if let Some(enable_double_quoted_string_literals) =
      obj.get(scope, enable_double_quoted_string_literals_string.into())
      && !enable_double_quoted_string_literals.is_undefined()
    {
      options.enable_double_quoted_string_literals =
            v8::Local::<v8::Boolean>::try_from(enable_double_quoted_string_literals)
                .map_err(|_| {
                Error::InvalidArgType("The \"options.enableDoubleQuotedStringLiterals\" argument must be a boolean.".into())
                })?
                .is_true();
    }

    let timeout_string = TIMEOUT_STRING.v8_string(scope).unwrap();
    if let Some(timeout) = obj.get(scope, timeout_string.into())
      && !timeout.is_undefined()
    {
      let timeout = v8::Local::<v8::Integer>::try_from(timeout)
        .map_err(|_| {
          Error::InvalidArgType(
            "The \"options.timeout\" argument must be an integer.".into(),
          )
        })?
        .value();

      if timeout > 0 {
        options.timeout = timeout as u64;
      }
    }

    let read_big_ints_string = READ_BIG_INTS.v8_string(scope).unwrap();
    if let Some(read_big_ints) = obj.get(scope, read_big_ints_string.into())
      && !read_big_ints.is_undefined()
    {
      options.use_big_int_arguments =
        v8::Local::<v8::Boolean>::try_from(read_big_ints)
          .map_err(|_| {
            Error::InvalidArgType(
              "The \"options.readBigInts\" argument must be a boolean.".into(),
            )
          })?
          .is_true();
    }

    let return_arrays_string = RETURN_ARRAYS.v8_string(scope).unwrap();
    if let Some(return_arrays) = obj.get(scope, return_arrays_string.into())
      && !return_arrays.is_undefined()
    {
      options.return_arrays = v8::Local::<v8::Boolean>::try_from(return_arrays)
        .map_err(|_| {
          Error::InvalidArgType(
            "The \"options.returnArrays\" argument must be a boolean.".into(),
          )
        })?
        .is_true();
    }

    let allow_bare_named_params_string =
      ALLOW_BARE_NAMED_PARAMS.v8_string(scope).unwrap();
    if let Some(allow_bare_named_params) =
      obj.get(scope, allow_bare_named_params_string.into())
      && !allow_bare_named_params.is_undefined()
    {
      options.allow_bare_named_params =
        v8::Local::<v8::Boolean>::try_from(allow_bare_named_params)
          .map_err(|_| {
            Error::InvalidArgType("The \"options.allowBareNamedParameters\" argument must be a boolean.".into())
          })?
          .is_true();
    }

    let allow_unknown_named_params_string =
      ALLOW_UNKNOWN_NAMED_PARAMS.v8_string(scope).unwrap();
    if let Some(allow_unknown_named_params) =
      obj.get(scope, allow_unknown_named_params_string.into())
      && !allow_unknown_named_params.is_undefined()
    {
      options.allow_unknown_named_params =
        v8::Local::<v8::Boolean>::try_from(allow_unknown_named_params)
          .map_err(|_| {
            Error::InvalidArgType("The \"options.allowUnknownNamedParameters\" argument must be a boolean.".into())
          })?
          .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.".into(),
            )
          })?
          .is_true();
    }

    let limits_string = LIMITS_STRING.v8_string(scope).unwrap();
    if let Some(limits_value) = obj.get(scope, limits_string.into())
      && !limits_value.is_undefined()
    {
      let limits_obj = v8::Local::<v8::Object>::try_from(limits_value)
        .map_err(|_| {
          Error::InvalidArgType(
            "The \"options.limits\" argument must be an object.".into(),
          )
        })?;

      for (idx, &(js_name, _limit)) in LIMIT_MAPPING.iter().enumerate() {
        let key = v8::String::new(scope, js_name).ok_or({
          Error::InvalidArgType("Failed to create limit key string.".into())
        })?;

        if let Some(val) = limits_obj.get(scope, key.into())
          && !val.is_undefined()
        {
          let int_val =
            v8::Local::<v8::Int32>::try_from(val).map_err(|_| {
              Error::InvalidArgType(
                format!(
                  "The \"options.limits.{}\" argument must be an integer.",
                  js_name
                )
                .into(),
              )
            })?;
          let limit_val = int_val.value();
          if limit_val < 0 {
            return Err(Error::OutOfRange(
              format!(
                "The \"options.limits.{}\" argument must be non-negative.",
                js_name
              )
              .into(),
            ));
          }

          options.initial_limits[idx] = Some(limit_val);
        }
      }
    }

    Ok(options)
  }
}

impl Default for DatabaseSyncOptions {
  fn default() -> Self {
    DatabaseSyncOptions {
      open: true,
      enable_foreign_key_constraints: true,
      read_only: false,
      allow_extension: false,
      enable_double_quoted_string_literals: false,
      use_big_int_arguments: false,
      return_arrays: false,
      allow_bare_named_params: true,
      allow_unknown_named_params: false,
      is_defensive_mode: true,
      timeout: 0,
      initial_limits: [None; NUM_LIMITS],
    }
  }
}

struct AggregateFunctionOption<'a> {
  deterministic: bool,
  direct_only: bool,
  use_big_int_arguments: bool,
  varargs: bool,
  start: v8::Local<'a, v8::Value>,
  step: v8::Local<'a, v8::Function>,
  result: Option<v8::Local<'a, v8::Function>>,
  inverse: Option<v8::Local<'a, v8::Function>>,
}

impl<'a> AggregateFunctionOption<'a> {
  fn from_value(
    scope: &mut v8::PinScope<'a, '_>,
    value: v8::Local<'a, v8::Value>,
  ) -> Result<Self, validators::Error> {
    use validators::Error;

    if !value.is_object() {
      return Err(Error::InvalidArgType(
        "The \"options\" argument must be an object.".into(),
      ));
    }

    v8_static_strings! {
      DETERMINISTIC_STRING = "deterministic",
      DIRECT_ONLY_STRING = "directOnly",
      USE_BIG_INT_ARGUMENTS_STRING = "useBigIntArguments",
      VARARGS_STRING = "varargs",
      START_STRING = "start",
      STEP_STRING = "step",
      RESULT_STRING = "result",
      INVERSE_STRING = "inverse",
    }

    let start_key = START_STRING.v8_string(scope).unwrap();
    let start_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, start_key.into())
      .unwrap();
    if start_value.is_undefined() {
      return Err(Error::InvalidArgType("The \"options.start\" argument must be a function or a primitive value.".into()));
    }

    let step_key = STEP_STRING.v8_string(scope).unwrap();
    let step_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, step_key.into())
      .unwrap();
    let step_function = v8::Local::<v8::Function>::try_from(step_value)
      .map_err(|_| {
        Error::InvalidArgType(
          "The \"options.step\" argument must be a function.".into(),
        )
      })?;

    let result_key = RESULT_STRING.v8_string(scope).unwrap();
    let result_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, result_key.into())
      .unwrap();
    let result_function = if result_value.is_undefined() {
      None
    } else {
      let func =
        v8::Local::<v8::Function>::try_from(result_value).map_err(|_| {
          Error::InvalidArgType(
            "The \"options.result\" argument must be a function.".into(),
          )
        })?;
      Some(func)
    };

    let mut deterministic = false;
    let mut use_big_int_arguments = false;
    let mut varargs = false;
    let mut direct_only = false;

    let deterministic_key = DETERMINISTIC_STRING.v8_string(scope).unwrap();
    let deterministic_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, deterministic_key.into())
      .unwrap();
    if !deterministic_value.is_undefined() {
      if !deterministic_value.is_boolean() {
        return Err(Error::InvalidArgType(
          "The \"options.deterministic\" argument must be a boolean.".into(),
        ));
      }
      deterministic = deterministic_value.boolean_value(scope);
    }

    let use_bigint_key = USE_BIG_INT_ARGUMENTS_STRING.v8_string(scope).unwrap();
    let bigint_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, use_bigint_key.into())
      .unwrap();
    if !bigint_value.is_undefined() {
      if !bigint_value.is_boolean() {
        return Err(Error::InvalidArgType(
          "The \"options.useBigIntArguments\" argument must be a boolean."
            .into(),
        ));
      }
      use_big_int_arguments = bigint_value.boolean_value(scope);
    }

    let varargs_key = VARARGS_STRING.v8_string(scope).unwrap();
    let varargs_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, varargs_key.into())
      .unwrap();
    if !varargs_value.is_undefined() {
      if !varargs_value.is_boolean() {
        return Err(Error::InvalidArgType(
          "The \"options.varargs\" argument must be a boolean.".into(),
        ));
      }
      varargs = varargs_value.boolean_value(scope);
    }

    let direct_only_key = DIRECT_ONLY_STRING.v8_string(scope).unwrap();
    let direct_only_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, direct_only_key.into())
      .unwrap();
    if !direct_only_value.is_undefined() {
      if !direct_only_value.is_boolean() {
        return Err(Error::InvalidArgType(
          "The \"options.directOnly\" argument must be a boolean.".into(),
        ));
      }
      direct_only = direct_only_value.boolean_value(scope);
    }

    let inverse_key = INVERSE_STRING.v8_string(scope).unwrap();
    let inverse_value = v8::Local::<v8::Object>::try_from(value)
      .unwrap()
      .get(scope, inverse_key.into())
      .unwrap();
    let inverse_function = if inverse_value.is_undefined() {
      None
    } else {
      let func =
        v8::Local::<v8::Function>::try_from(inverse_value).map_err(|_| {
          Error::InvalidArgType(
            "The \"options.inverse\" argument must be a function.".into(),
          )
        })?;
      Some(func)
    };

    Ok(AggregateFunctionOption {
      deterministic,
      direct_only,
      use_big_int_arguments,
      varargs,
      start: start_value,
      step: step_function,
      result: result_function,
      inverse: inverse_function,
    })
  }
}

struct ApplyChangesetOptions<'a> {
  filter: Option<v8::Local<'a, v8::Value>>,
  on_conflict: Option<v8::Local<'a, v8::Value>>,
}

// Note: Can't use `FromV8` here because of lifetime issues with holding
// Local references.
impl<'a> ApplyChangesetOptions<'a> {
  fn from_value(
    scope: &mut v8::PinScope<'a, '_>,
    value: v8::Local<'a, v8::Value>,
  ) -> Result<Option<Self>, validators::Error> {
    use validators::Error;

    if value.is_undefined() {
      return Ok(None);
    }

    let obj = v8::Local::<v8::Object>::try_from(value).map_err(|_| {
      Error::InvalidArgType(
        "The \"options\" argument must be an object.".into(),
      )
    })?;

    let mut options = Self {
      filter: None,
      on_conflict: None,
    };

    v8_static_strings! {
      FILTER_STRING = "filter",
      ON_CONFLICT_STRING = "onConflict",
    }

    let filter_string = FILTER_STRING.v8_string(scope).unwrap();
    if let Some(filter) = obj.get(scope, filter_string.into())
      && !filter.is_undefined()
    {
      if !filter.is_function() {
        return Err(Error::InvalidArgType(
          "The \"options.filter\" argument must be a function.".into(),
        ));
      }

      options.filter = Some(filter);
    }

    let on_conflict_string = ON_CONFLICT_STRING.v8_string(scope).unwrap();
    if let Some(on_conflict) = obj.get(scope, on_conflict_string.into())
      && !on_conflict.is_undefined()
    {
      if !on_conflict.is_function() {
        return Err(Error::InvalidArgType(
          "The \"options.onConflict\" argument must be a function.".into(),
        ));
      }

      options.on_conflict = Some(on_conflict);
    }

    Ok(Some(options))
  }
}

pub struct DatabaseSync {
  pub conn: Rc<RefCell<Option<rusqlite::Connection>>>,
  statements: Rc<RefCell<Vec<InnerStatementPtr>>>,
  options: DatabaseSyncOptions,
  location: String,
  ignore_next_sqlite_error: Rc<Cell<bool>>,
  authorizer_data: Rc<RefCell<Option<*mut AuthorizerData>>>,
  // Non-zero while a user-defined callback is on the stack; close() refuses
  // to free the connection in that state to avoid a SQLite VDBE use-after-free.
  callback_depth: Rc<Cell<usize>>,
  // Whether ATTACH DATABASE is held disabled because the process lacks full
  // permissions for the database path. Used to stop the `limits.attach` setter
  // from raising the cap and bypassing the boundary.
  disable_attach: Rc<Cell<bool>>,
}

struct CallbackDepthGuard {
  depth: Rc<Cell<usize>>,
}

impl CallbackDepthGuard {
  fn new(depth: &Rc<Cell<usize>>) -> Self {
    depth.set(depth.get().saturating_add(1));
    Self {
      depth: Rc::clone(depth),
    }
  }
}

impl Drop for CallbackDepthGuard {
  fn drop(&mut self) {
    self.depth.set(self.depth.get().saturating_sub(1));
  }
}

// SAFETY: we're sure this can be GCed
unsafe impl GarbageCollected for DatabaseSync {
  fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}

  fn get_name(&self) -> &'static std::ffi::CStr {
    c"DatabaseSync"
  }
}

fn set_db_config(
  conn: &rusqlite::Connection,
  config: i32,
  value: bool,
) -> bool {
  // SAFETY: call to sqlite3_db_config is safe because the connection
  // handle is valid and the parameters are correct.
  unsafe {
    let mut set = 0;
    let r = libsqlite3_sys::sqlite3_db_config(
      conn.handle(),
      config,
      value as i32,
      &mut set,
    );

    if r != libsqlite3_sys::SQLITE_OK {
      panic!("Failed to set db config");
    }

    set == value as i32
  }
}

enum LimitCoercionError {
  NotANumber,
  NotAnInteger,
  Negative,
}

/// Coerce a v8 value to a valid SQLite limit integer.
/// Accepts non-negative integers and positive Infinity (mapped to i32::MAX).
fn coerce_limit_value(
  scope: &mut v8::PinScope<'_, '_>,
  value: v8::Local<v8::Value>,
) -> Result<i32, LimitCoercionError> {
  if !value.is_number() {
    return Err(LimitCoercionError::NotANumber);
  }
  let num_value = value.number_value(scope).unwrap_or(f64::NAN);
  if num_value.is_infinite() && num_value > 0.0 {
    return Ok(i32::MAX);
  }
  if !value.is_int32() {
    return Err(LimitCoercionError::NotAnInteger);
  }
  let int_val = value.int32_value(scope).unwrap_or(-1);
  if int_val < 0 {
    return Err(LimitCoercionError::Negative);
  }
  Ok(int_val)
}

/// Apply initial limits to a connection based on options
fn apply_initial_limits(
  conn: &rusqlite::Connection,
  options: &DatabaseSyncOptions,
  disable_attach: bool,
) -> Result<(), SqliteError> {
  for (idx, &(_js_name, limit)) in LIMIT_MAPPING.iter().enumerate() {
    if let Some(value) = options.initial_limits[idx] {
      // When the process lacks full permissions for the database path, the
      // attach limit is a security cap held at 0: ATTACH DATABASE can reach
      // files outside the database path, so it must not be raised through the
      // user-controlled `limits.attach` option. Reject raising it above 0,
      // mirroring the `db.limits.attach` setter. A value of 0 (keeping it
      // disabled) is still accepted.
      if disable_attach
        && matches!(limit, Limit::SQLITE_LIMIT_ATTACHED)
        && value > 0
      {
        return Err(SqliteError::AttachLimitDenied);
      }
      conn.set_limit(limit, value)?;
    }
  }
  Ok(())
}

// Returns the open connection together with `disable_attach`: whether the
// process lacks full permissions for the database path and so ATTACH DATABASE
// is held disabled. Callers store this so the `limits.attach` setter cannot
// later raise the cap and bypass the boundary.
fn open_db(
  state: &mut OpState,
  location: &str,
  options: &DatabaseSyncOptions,
) -> Result<(rusqlite::Connection, bool), SqliteError> {
  let perms = state.borrow::<PermissionsContainer>();
  let disable_attach = perms
    .check_has_all_permissions(Path::new(location))
    .is_err();

  if location == ":memory:" {
    let conn = rusqlite::Connection::open_in_memory()?;
    if disable_attach {
      assert!(set_db_config(
        &conn,
        SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE,
        false
      ));
      conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?;
    }

    if options.allow_extension {
      perms.check_ffi_all()?;
    } else {
      assert!(set_db_config(
        &conn,
        SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
        false
      ));
    }

    assert!(set_db_config(
      &conn,
      SQLITE_DBCONFIG_DEFENSIVE,
      options.is_defensive_mode,
    ));

    apply_initial_limits(&conn, options, disable_attach)?;

    return Ok((conn, disable_attach));
  }

  let location = perms
    .check_open(
      Cow::Borrowed(Path::new(location)),
      match options.read_only {
        true => OpenAccessKind::ReadNoFollow,
        false => OpenAccessKind::ReadWriteNoFollow,
      },
      Some("node:sqlite"),
    )?
    .into_path();

  if options.read_only {
    let conn = rusqlite::Connection::open_with_flags(
      location,
      rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
    )?;
    if disable_attach {
      assert!(set_db_config(
        &conn,
        SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE,
        false
      ));
      conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?;
    }

    if options.allow_extension {
      perms.check_ffi_all()?;
    } else {
      assert!(set_db_config(
        &conn,
        SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
        false
      ));
    }

    assert!(set_db_config(
      &conn,
      SQLITE_DBCONFIG_DEFENSIVE,
      options.is_defensive_mode,
    ));

    apply_initial_limits(&conn, options, disable_attach)?;

    return Ok((conn, disable_attach));
  }

  let conn = rusqlite::Connection::open(location)?;
  conn.busy_timeout(std::time::Duration::from_millis(options.timeout))?;

  if options.allow_extension {
    perms.check_ffi_all()?;
  } else {
    assert!(set_db_config(
      &conn,
      SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
      false
    ));
  }

  if disable_attach {
    conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?;
  }

  assert!(set_db_config(
    &conn,
    SQLITE_DBCONFIG_DEFENSIVE,
    options.is_defensive_mode,
  ));

  apply_initial_limits(&conn, options, disable_attach)?;

  Ok((conn, disable_attach))
}

fn is_open(
  scope: &mut v8::PinScope<'_, '_>,
  args: &v8::FunctionCallbackArguments,
) -> Result<(), SqliteError> {
  let this_ = args.this();
  let db = cppgc::try_unwrap_cppgc_object::<DatabaseSync>(scope, this_.into())
    .ok_or(SqliteError::AlreadyClosed)?;

  db.conn
    .borrow()
    .as_ref()
    .ok_or(SqliteError::AlreadyClosed)?;

  Ok(())
}

// Represents a single connection to a SQLite database.
#[op2]
impl DatabaseSync {
  // Constructs a new `DatabaseSync` instance.
  //
  // A SQLite database can be stored in a file or in memory. To
  // use a file-backed database, the `location` should be a path.
  // To use an in-memory database, the `location` should be special
  // name ":memory:".
  #[constructor]
  #[cppgc]
  fn new(
    state: &mut OpState,
    #[string] location: String,
    #[scoped] options: DatabaseSyncOptions,
  ) -> Result<DatabaseSync, SqliteError> {
    let mut disable_attach = false;
    let db = if options.open {
      let (db, da) = open_db(state, &location, &options)?;
      disable_attach = da;

      if options.enable_foreign_key_constraints {
        db.execute("PRAGMA foreign_keys = ON", [])?;
      } else {
        db.execute("PRAGMA foreign_keys = OFF", [])?;
      }

      set_db_config(
        &db,
        SQLITE_DBCONFIG_DQS_DDL,
        options.enable_double_quoted_string_literals,
      );
      set_db_config(
        &db,
        SQLITE_DBCONFIG_DQS_DML,
        options.enable_double_quoted_string_literals,
      );
      Some(db)
    } else {
      None
    };

    Ok(DatabaseSync {
      conn: Rc::new(RefCell::new(db)),
      statements: Rc::new(RefCell::new(Vec::new())),
      location,
      options,
      ignore_next_sqlite_error: Rc::new(Cell::new(false)),
      authorizer_data: Rc::new(RefCell::new(None)),
      callback_depth: Rc::new(Cell::new(0)),
      disable_attach: Rc::new(Cell::new(disable_attach)),
    })
  }

  // Opens the database specified by `location` of this instance.
  //
  // This method should only be used when the database is not opened
  // via the constructor. An exception is thrown if the database is
  // already opened.
  #[fast]
  #[undefined]
  fn open(&self, state: &mut OpState) -> Result<(), SqliteError> {
    if self.conn.borrow().is_some() {
      return Err(SqliteError::AlreadyOpen);
    }

    let (db, disable_attach) = open_db(state, &self.location, &self.options)?;
    if self.options.enable_foreign_key_constraints {
      db.execute("PRAGMA foreign_keys = ON", [])?;
    } else {
      db.execute("PRAGMA foreign_keys = OFF", [])?;
    }

    set_db_config(
      &db,
      SQLITE_DBCONFIG_DQS_DDL,
      self.options.enable_double_quoted_string_literals,
    );
    set_db_config(
      &db,
      SQLITE_DBCONFIG_DQS_DML,
      self.options.enable_double_quoted_string_literals,
    );

    *self.conn.borrow_mut() = Some(db);
    self.disable_attach.set(disable_attach);

    Ok(())
  }

  // Closes the database connection. An exception is thrown if the
  // database is not open.
  #[fast]
  #[undefined]
  fn close(&self) -> Result<(), SqliteError> {
    if self.conn.borrow().is_none() {
      return Err(SqliteError::AlreadyClosed);
    }

    // Refuse to close while a user-defined callback is on the SQLite stack —
    // freeing the in-flight statement would be a VDBE use-after-free.
    if self.callback_depth.get() > 0 {
      return Err(SqliteError::ActiveCallback);
    }

    // Finalize all prepared statements
    for stmt in self.statements.borrow_mut().drain(..) {
      match stmt.get() {
        None => continue,
        Some(ptr) => {
          // SAFETY: `ptr` is a valid statement handle.
          unsafe {
            libsqlite3_sys::sqlite3_finalize(ptr);
          };
          stmt.set(None);
        }
      };
    }

    {
      let mut authorizer_data = self.authorizer_data.borrow_mut();
      if let Some(data_ptr) = authorizer_data.take() {
        // SAFETY: data_ptr was allocated in authorizer setup.
        unsafe {
          release_authorizer_data(data_ptr);
        }
      }
    }

    let _ = self.conn.borrow_mut().take();

    Ok(())
  }

  // This method allows one or more SQL statements to be executed
  // without returning any results.
  //
  // This method is a wrapper around sqlite3_exec().
  #[fast]
  #[reentrant]
  #[validate(is_open)]
  #[undefined]
  fn exec(
    &self,
    #[validate(validators::sql_str)]
    #[string]
    sql: &str,
  ) -> Result<(), SqliteError> {
    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::InUse)?;

    if let Err(err) = db.execute_batch(sql) {
      if self.consume_ignore_next_sqlite_error() {
        return Ok(());
      }
      return Err(err.into());
    }

    Ok(())
  }

  // Compiles an SQL statement into a prepared statement.
  //
  // This method is a wrapper around `sqlite3_prepare_v2()`.
  #[reentrant]
  #[validate(is_open)]
  #[cppgc]
  fn prepare(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    #[validate(validators::sql_str)]
    #[string]
    sql: &str,
    #[varargs] args: Option<&v8::FunctionCallbackArguments>,
  ) -> Result<StatementSync, SqliteError> {
    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::InUse)?;

    // SAFETY: lifetime of the connection is guaranteed by reference
    // counting.
    let raw_handle = unsafe { db.handle() };

    let mut raw_stmt = std::ptr::null_mut();

    // SAFETY: `sql` points to a valid memory location and its length
    // is correct.
    let r = unsafe {
      libsqlite3_sys::sqlite3_prepare_v2(
        raw_handle,
        sql.as_ptr() as *const _,
        sql.len() as i32,
        &mut raw_stmt,
        std::ptr::null_mut(),
      )
    };
    check_error_code(r, raw_handle)?;

    let mut allow_unknown_named_params =
      self.options.allow_unknown_named_params;
    let mut use_big_ints = self.options.use_big_int_arguments;
    let mut return_arrays = self.options.return_arrays;
    let mut allow_bare_named_params = self.options.allow_bare_named_params;

    // Parse options object (second argument)
    if let Some(args) = args {
      // args[0] is already consumed as `sql`, so options is args[1]
      if args.length() > 1 {
        let options = args.get(1);
        if !options.is_undefined() && !options.is_null() {
          let options =
            v8::Local::<v8::Object>::try_from(options).map_err(|_| {
              SqliteError::Validation(validators::Error::InvalidArgType(
                "The \"options\" argument must be an object.".into(),
              ))
            })?;

          v8_static_strings! {
            ALLOW_UNKNOWN = "allowUnknownNamedParameters",
            READ_BIG_INTS = "readBigInts",
            RETURN_ARRAYS = "returnArrays",
            ALLOW_BARE = "allowBareNamedParameters",
          }

          macro_rules! parse_bool_opt {
            ($key:expr, $name:expr, $target:ident) => {
              let key = $key.v8_string(scope).unwrap().into();
              if let Some(val) = options.get(scope, key) {
                if !val.is_undefined() {
                  $target = v8::Local::<v8::Boolean>::try_from(val)
                    .map_err(|_| {
                      SqliteError::Validation(
                        validators::Error::InvalidArgType(
                          concat!(
                            "The \"",
                            $name,
                            "\" argument must be a boolean."
                          )
                          .into(),
                        ),
                      )
                    })?
                    .is_true();
                }
              }
            };
          }

          parse_bool_opt!(
            ALLOW_UNKNOWN,
            "options.allowUnknownNamedParameters",
            allow_unknown_named_params
          );
          parse_bool_opt!(READ_BIG_INTS, "options.readBigInts", use_big_ints);
          parse_bool_opt!(RETURN_ARRAYS, "options.returnArrays", return_arrays);
          parse_bool_opt!(
            ALLOW_BARE,
            "options.allowBareNamedParameters",
            allow_bare_named_params
          );
        }
      }
    }

    let stmt_cell = Rc::new(Cell::new(Some(raw_stmt)));
    self.statements.borrow_mut().push(stmt_cell.clone());

    Ok(StatementSync {
      inner: stmt_cell,
      db: self.conn.clone(),
      statements: Rc::clone(&self.statements),
      ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
      return_arrays: Cell::new(return_arrays),
      use_big_ints: Cell::new(use_big_ints),
      allow_bare_named_params: Cell::new(allow_bare_named_params),
      allow_unknown_named_params: Cell::new(allow_unknown_named_params),
      is_iter_finished: Cell::new(false),
      iter_generation: Cell::new(0),
      iter_contexts: RefCell::new(Vec::new()),
    })
  }

  #[fast]
  #[validate(is_open)]
  #[undefined]
  fn function<'a>(
    &self,
    scope: &mut v8::PinScope<'a, '_>,
    #[varargs] args: Option<&v8::FunctionCallbackArguments>,
  ) -> Result<(), SqliteError> {
    let Some(args) = args.filter(|args| args.length() > 0) else {
      return Err(
        validators::Error::InvalidArgType(
          "The \"name\" argument must be a string.".into(),
        )
        .into(),
      );
    };

    if !args.get(0).is_string() {
      return Err(
        validators::Error::InvalidArgType(
          "The \"name\" argument must be a string.".into(),
        )
        .into(),
      );
    }
    let name = args.get(0).to_rust_string_lossy(scope);

    let (options_value, function_value) = if args.length() < 3 {
      (None, args.get(1))
    } else {
      (Some(args.get(1)), args.get(2))
    };

    let Ok(function) = v8::Local::<v8::Function>::try_from(function_value)
    else {
      return Err(
        validators::Error::InvalidArgType(
          "The \"function\" argument must be a function.".into(),
        )
        .into(),
      );
    };

    let mut use_big_int_arguments = false;
    let mut varargs = false;
    let mut deterministic = false;
    let mut direct_only = false;

    if let Some(value) = options_value
      && !value.is_undefined()
    {
      if value.is_null() || !value.is_object() {
        return Err(
          validators::Error::InvalidArgType(
            "The \"options\" argument must be an object.".into(),
          )
          .into(),
        );
      }

      let options = v8::Local::<v8::Object>::try_from(value).unwrap();

      v8_static_strings! {
        USE_BIG_INT_ARGUMENTS = "useBigIntArguments",
        VARARGS = "varargs",
        DETERMINISTIC = "deterministic",
        DIRECT_ONLY = "directOnly",
      }

      let use_bigint_key = USE_BIG_INT_ARGUMENTS.v8_string(scope).unwrap();
      let bigint_value = options.get(scope, use_bigint_key.into()).unwrap();
      if !bigint_value.is_undefined() {
        if !bigint_value.is_boolean() {
          return Err(
            validators::Error::InvalidArgType(
              "The \"options.useBigIntArguments\" argument must be a boolean."
                .into(),
            )
            .into(),
          );
        }
        use_big_int_arguments = bigint_value.boolean_value(scope);
      }

      let varargs_key = VARARGS.v8_string(scope).unwrap();
      let varargs_value = options.get(scope, varargs_key.into()).unwrap();
      if !varargs_value.is_undefined() {
        if !varargs_value.is_boolean() {
          return Err(
            validators::Error::InvalidArgType(
              "The \"options.varargs\" argument must be a boolean.".into(),
            )
            .into(),
          );
        }
        varargs = varargs_value.boolean_value(scope);
      }

      let deterministic_key = DETERMINISTIC.v8_string(scope).unwrap();
      let deterministic_value =
        options.get(scope, deterministic_key.into()).unwrap();
      if !deterministic_value.is_undefined() {
        if !deterministic_value.is_boolean() {
          return Err(
            validators::Error::InvalidArgType(
              "The \"options.deterministic\" argument must be a boolean."
                .into(),
            )
            .into(),
          );
        }
        deterministic = deterministic_value.boolean_value(scope);
      }

      let direct_only_key = DIRECT_ONLY.v8_string(scope).unwrap();
      let direct_only_value =
        options.get(scope, direct_only_key.into()).unwrap();
      if !direct_only_value.is_undefined() {
        if !direct_only_value.is_boolean() {
          return Err(
            validators::Error::InvalidArgType(
              "The \"options.directOnly\" argument must be a boolean.".into(),
            )
            .into(),
          );
        }
        direct_only = direct_only_value.boolean_value(scope);
      }
    }

    v8_static_strings! {
      LENGTH = "length",
    }

    let argc = if varargs {
      -1
    } else {
      let length_key = LENGTH.v8_string(scope).unwrap();
      let length = function.get(scope, length_key.into()).unwrap();
      length.int32_value(scope).unwrap_or(0)
    };

    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::InUse)?;

    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let raw_handle = unsafe { db.handle() };
    let name_cstring = CString::new(name)?;

    let callback = v8::Global::new(scope, function).into_raw();
    let context =
      v8::Global::new(scope, scope.get_current_context()).into_raw();

    let data = Box::new(CustomFunctionData {
      callback,
      context,
      use_big_int_arguments,
      ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
      callback_depth: Rc::clone(&self.callback_depth),
    });
    let data_ptr = Box::into_raw(data);

    let mut text_rep = libsqlite3_sys::SQLITE_UTF8;
    if deterministic {
      text_rep |= libsqlite3_sys::SQLITE_DETERMINISTIC;
    }
    if direct_only {
      text_rep |= libsqlite3_sys::SQLITE_DIRECTONLY;
    }

    // SAFETY: `raw_handle` is a valid database handle.
    // `data_ptr` points to a valid memory location.
    // The v8 handles that are held in `CustomFunctionData` will be
    // dropped when the data is destroyed via `custom_function_destroy`.
    let result = unsafe {
      libsqlite3_sys::sqlite3_create_function_v2(
        raw_handle,
        name_cstring.as_ptr(),
        argc,
        text_rep,
        data_ptr as *mut c_void,
        Some(custom_function_handler),
        None,
        None,
        Some(custom_function_destroy),
      )
    };
    check_error_code(result, raw_handle)?;

    Ok(())
  }

  // Applies a changeset to the database.
  //
  // This method is a wrapper around `sqlite3changeset_apply()`.
  #[fast]
  #[reentrant]
  fn apply_changeset<'a>(
    &self,
    scope: &mut v8::PinScope<'a, '_>,
    #[validate(validators::changeset_buffer)]
    #[buffer]
    changeset: &[u8],
    options: v8::Local<'a, v8::Value>,
  ) -> Result<bool, SqliteError> {
    let options = ApplyChangesetOptions::from_value(scope, options)?;

    struct HandlerCtx<'a, 'b, 'c> {
      scope: &'a mut v8::PinScope<'b, 'c>,
      conflict: Option<v8::Local<'b, v8::Function>>,
      filter: Option<v8::Local<'b, v8::Function>>,
      has_caught: bool,
    }

    // Conflict handler callback for `sqlite3changeset_apply()`.
    unsafe extern "C" fn conflict_handler(
      p_ctx: *mut c_void,
      e_conflict: i32,
      _: *mut libsqlite3_sys::sqlite3_changeset_iter,
    ) -> i32 {
      #[allow(
        clippy::undocumented_unsafe_blocks,
        reason = "safety comment on the containing block"
      )]
      unsafe {
        let ctx = &mut *(p_ctx as *mut HandlerCtx);

        if let Some(conflict) = &mut ctx.conflict {
          let recv = v8::undefined(ctx.scope).into();
          let args = [v8::Integer::new(ctx.scope, e_conflict).into()];

          v8::tc_scope!(tc_scope, ctx.scope);

          let ret = conflict
            .call(tc_scope, recv, &args)
            .unwrap_or_else(|| v8::undefined(tc_scope).into());
          if tc_scope.has_caught() {
            tc_scope.rethrow();
            return libsqlite3_sys::SQLITE_CHANGESET_ABORT;
          }

          const INVALID_VALUE: i32 = -1;
          if !ret.is_int32() {
            return INVALID_VALUE;
          }

          let value = ret
            .int32_value(tc_scope)
            .unwrap_or(libsqlite3_sys::SQLITE_CHANGESET_ABORT);

          return value;
        }

        libsqlite3_sys::SQLITE_CHANGESET_ABORT
      }
    }

    // Filter handler callback for `sqlite3changeset_apply()`.
    unsafe extern "C" fn filter_handler(
      p_ctx: *mut c_void,
      z_tab: *const c_char,
    ) -> i32 {
      #[allow(
        clippy::undocumented_unsafe_blocks,
        reason = "safety comment on the containing block"
      )]
      unsafe {
        let ctx = &mut *(p_ctx as *mut HandlerCtx);

        // If we've already caught an exception, don't call the filter again
        // to avoid overwriting the original exception.
        if ctx.has_caught {
          return 0;
        }

        if let Some(filter) = &mut ctx.filter {
          let tab = CStr::from_ptr(z_tab).to_str().unwrap();

          let recv = v8::undefined(ctx.scope).into();
          let args = [v8::String::new(ctx.scope, tab).unwrap().into()];

          v8::tc_scope!(tc_scope, ctx.scope);

          let ret = filter
            .call(tc_scope, recv, &args)
            .unwrap_or_else(|| v8::undefined(tc_scope).into());
          if tc_scope.has_caught() {
            ctx.has_caught = true;
            tc_scope.rethrow();
            return 0;
          }

          return ret.boolean_value(tc_scope) as i32;
        }

        1
      }
    }

    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    // It is safe to use scope in the handlers because they are never
    // called after the call to `sqlite3changeset_apply()`.
    let mut ctx = HandlerCtx {
      scope,
      conflict: None,
      filter: None,
      has_caught: false,
    };

    if let Some(options) = options {
      if let Some(filter) = options.filter {
        let filter_cb: v8::Local<v8::Function> = filter
          .try_into()
          .map_err(|_| SqliteError::InvalidCallback("filter"))?;
        ctx.filter = Some(filter_cb);
      }

      if let Some(on_conflict) = options.on_conflict {
        let on_conflict_cb: v8::Local<v8::Function> = on_conflict
          .try_into()
          .map_err(|_| SqliteError::InvalidCallback("onConflict"))?;
        ctx.conflict = Some(on_conflict_cb);
      }
    }

    // SAFETY: lifetime of the connection is guaranteed by reference
    // counting.
    let raw_handle = unsafe { db.handle() };

    // Block close() while filter/onConflict callbacks may be on the stack.
    let _apply_depth_guard = CallbackDepthGuard::new(&self.callback_depth);

    // SAFETY: `changeset` points to a valid memory location and its
    // length is correct. `ctx` is stack allocated and its lifetime is
    // longer than the call to `sqlite3changeset_apply()`.
    unsafe {
      let r = libsqlite3_sys::sqlite3changeset_apply(
        raw_handle,
        changeset.len() as i32,
        changeset.as_ptr() as *mut _,
        Some(filter_handler),
        Some(conflict_handler),
        &mut ctx as *mut _ as *mut c_void,
      );

      if r == libsqlite3_sys::SQLITE_OK {
        return Ok(true);
      } else if r == libsqlite3_sys::SQLITE_ABORT {
        return Ok(false);
      }

      check_error_code2(r)?;

      Ok(false)
    }
  }

  // Loads a SQLite extension.
  //
  // This is a wrapper around `sqlite3_load_extension`. It requires FFI permission
  // to be granted and allowExtension must be set to true when opening the database.
  fn load_extension(
    &self,
    state: &mut OpState,
    #[validate(validators::path_str)]
    #[string]
    path: &str,
    #[string] entry_point: Option<String>,
  ) -> Result<(), SqliteError> {
    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    if !self.options.allow_extension {
      return Err(SqliteError::LoadExensionFailed(
        "Cannot load SQLite extensions when allowExtension is not enabled"
          .to_string(),
      ));
    }

    state.borrow::<PermissionsContainer>().check_ffi_all()?;

    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let raw_handle = unsafe { db.handle() };

    let path_cstring = std::ffi::CString::new(path.as_bytes())?;
    let entry_point_cstring =
      entry_point.map(|ep| std::ffi::CString::new(ep).unwrap_or_default());

    let entry_point_ptr = match &entry_point_cstring {
      Some(cstr) => cstr.as_ptr(),
      None => std::ptr::null(),
    };

    let mut err_msg: *mut c_char = std::ptr::null_mut();

    // SAFETY: Using sqlite3_load_extension with proper error handling
    let result = unsafe {
      let res = libsqlite3_sys::sqlite3_load_extension(
        raw_handle,
        path_cstring.as_ptr(),
        entry_point_ptr,
        &mut err_msg,
      );

      if res != libsqlite3_sys::SQLITE_OK {
        let error_message = if !err_msg.is_null() {
          let c_str = std::ffi::CStr::from_ptr(err_msg);
          let message = c_str.to_string_lossy().into_owned();
          libsqlite3_sys::sqlite3_free(err_msg as *mut _);
          message
        } else {
          format!("Failed to load extension with error code: {}", res)
        };

        return Err(SqliteError::LoadExensionFailed(error_message));
      }

      res
    };

    if result == libsqlite3_sys::SQLITE_OK {
      Ok(())
    } else {
      Err(SqliteError::LoadExensionFailed(
        "Unknown error loading SQLite extension".to_string(),
      ))
    }
  }

  // Creates and attaches a session to the database.
  //
  // This method is a wrapper around `sqlite3session_create()` and
  // `sqlite3session_attach()`.
  #[cppgc]
  fn create_session(
    &self,
    #[scoped] options: OptionUndefined<SessionOptions>,
  ) -> Result<Session, SqliteError> {
    let options = options.0;
    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    // SAFETY: lifetime of the connection is guaranteed by reference
    // counting.
    let raw_handle = unsafe { db.handle() };

    let mut raw_session = std::ptr::null_mut();
    let mut options = options;

    let z_db = options
      .as_mut()
      .and_then(|options| options.db.take())
      .map(|db| CString::new(db).unwrap())
      .unwrap_or_else(|| CString::new("main").unwrap());
    // SAFETY: `z_db` points to a valid c-string.
    let r = unsafe {
      libsqlite3_sys::sqlite3session_create(
        raw_handle,
        z_db.as_ptr() as *const _,
        &mut raw_session,
      )
    };

    if r != libsqlite3_sys::SQLITE_OK {
      return Err(SqliteError::SessionCreateFailed);
    }

    let table = options
      .as_mut()
      .and_then(|options| options.table.take())
      .map(|table| CString::new(table).unwrap());
    let z_table = table.as_ref().map(|table| table.as_ptr()).unwrap_or(null());
    let r =
      // SAFETY: `z_table` points to a valid c-string and `raw_session`
      // is a valid session handle.
      unsafe { libsqlite3_sys::sqlite3session_attach(raw_session, z_table) };

    if r != libsqlite3_sys::SQLITE_OK {
      return Err(SqliteError::SessionCreateFailed);
    }

    Ok(Session {
      inner: raw_session,
      freed: Cell::new(false),
      db: self.conn.clone(),
    })
  }

  fn location<'a>(
    &self,
    scope: &mut v8::PinScope<'a, '_>,
    name_value: v8::Local<'a, v8::Value>,
  ) -> Result<v8::Local<'a, v8::Value>, SqliteError> {
    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    let name = if !name_value.is_undefined() {
      if !name_value.is_string() {
        return Err(SqliteError::Validation(
          validators::Error::InvalidArgType(
            "The \"dbName\" argument must be a string.".into(),
          ),
        ));
      }
      name_value.to_rust_string_lossy(scope)
    } else {
      "main".to_string()
    };

    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let raw_handle = unsafe { db.handle() };
    let name_cstring = CString::new(name)?;

    // SAFETY: `raw_handle` is a valid sqlite3 pointer.
    let db_filename =
      unsafe { sqlite3_db_filename(raw_handle, name_cstring.as_ptr()) };
    if db_filename.is_null() {
      return Ok(v8::null(scope).into());
    }

    // SAFETY: `db_filename` is a valid C string pointer.
    let filename_cstr = unsafe { CStr::from_ptr(db_filename).to_bytes() };
    if filename_cstr.is_empty() {
      return Ok(v8::null(scope).into());
    }

    let filename = v8::String::new_from_utf8(
      scope,
      filename_cstr,
      v8::NewStringType::Normal,
    )
    .unwrap();

    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(())
  }

  // Sets an authorizer callback that SQLite will invoke whenever it attempts
  // to access data or modify the database schema through prepared statements.
  // This can be used to implement security policies, audit access, or restrict
  // certain operations.
  #[fast]
  #[validate(is_open)]
  #[undefined]
  fn set_authorizer<'a>(
    &self,
    scope: &mut v8::PinScope<'a, '_>,
    callback: v8::Local<'a, v8::Value>,
  ) -> Result<(), SqliteError> {
    let db = self.conn.borrow();
    let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let raw_handle = unsafe { conn.handle() };

    if callback.is_null() {
      // SAFETY: `raw_handle` is a valid database handle.
      let result = unsafe {
        libsqlite3_sys::sqlite3_set_authorizer(
          raw_handle,
          None,
          std::ptr::null_mut(),
        )
      };
      check_error_code(result, raw_handle)?;

      let mut authorizer_data = self.authorizer_data.borrow_mut();
      if let Some(old_data_ptr) = authorizer_data.take() {
        // SAFETY: data_ptr was allocated in authorizer setup.
        unsafe {
          release_authorizer_data(old_data_ptr);
        }
      }
      return Ok(());
    }

    let Ok(function) = v8::Local::<v8::Function>::try_from(callback) else {
      return Err(
        validators::Error::InvalidArgType(
          "The \"callback\" argument must be a function or null.".into(),
        )
        .into(),
      );
    };

    let callback_global = v8::Global::new(scope, function).into_raw();
    let context_global =
      v8::Global::new(scope, scope.get_current_context()).into_raw();

    let data = Box::new(AuthorizerData {
      callback: callback_global,
      context: context_global,
      ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
      callback_depth: Rc::clone(&self.callback_depth),
      active_callbacks: Cell::new(0),
      free_after_callback: Cell::new(false),
    });
    let data_ptr = Box::into_raw(data);

    // SAFETY: `raw_handle` is a valid database handle.
    // `data_ptr` points to a valid memory location.
    let result = unsafe {
      libsqlite3_sys::sqlite3_set_authorizer(
        raw_handle,
        Some(authorizer_callback),
        data_ptr as *mut c_void,
      )
    };

    if result != libsqlite3_sys::SQLITE_OK {
      // Clean up the data we allocated if setting the authorizer failed
      // SAFETY: we just allocated this data and it failed to be registered
      unsafe {
        free_authorizer_data(data_ptr);
      }
      check_error_code(result, raw_handle)?;
    }

    let mut authorizer_data = self.authorizer_data.borrow_mut();
    if let Some(old_data_ptr) = authorizer_data.replace(data_ptr) {
      // SAFETY: data_ptr was allocated in authorizer setup.
      unsafe {
        release_authorizer_data(old_data_ptr);
      }
    }

    Ok(())
  }

  #[getter]
  fn is_open(&self) -> bool {
    self.conn.borrow().is_some()
  }

  #[getter]
  fn is_transaction(&self) -> Result<bool, SqliteError> {
    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let res = unsafe { libsqlite3_sys::sqlite3_get_autocommit(db.handle()) };
    Ok(res == 0)
  }

  #[getter]
  #[cppgc]
  fn limits(&self) -> Result<DatabaseSyncLimits, SqliteError> {
    if self.conn.borrow().is_none() {
      return Err(SqliteError::AlreadyClosed);
    }
    Ok(DatabaseSyncLimits::create(
      Rc::clone(&self.conn),
      self.disable_attach.get(),
    ))
  }

  #[fast]
  #[validate(is_open)]
  fn aggregate<'a>(
    &self,
    scope: &mut v8::PinScope<'a, '_>,
    #[validate(validators::name_str)]
    #[string]
    name: &str,
    options: v8::Local<'a, v8::Value>,
  ) -> Result<(), SqliteError> {
    use std::cmp;

    let options = AggregateFunctionOption::from_value(scope, options)?;

    let mut argc = -1;
    if !options.varargs {
      v8_static_strings! {
        LENGTH = "length",
      }
      let length_key = LENGTH.v8_string(scope).unwrap();
      let step_fn_length = options
        .step
        .get(scope, length_key.into())
        .unwrap()
        .int32_value(scope)
        .unwrap();

      // Subtract 1 because the first argument is the aggregate value.
      argc = step_fn_length - 1;

      let inverse_fn_length = if let Some(inverse_fn) = &options.inverse {
        inverse_fn
          .get(scope, length_key.into())
          .unwrap()
          .int32_value(scope)
          .unwrap()
      } else {
        0
      };

      argc = cmp::max(argc, cmp::max(inverse_fn_length - 1, 0));
    }

    let mut text_rep = libsqlite3_sys::SQLITE_UTF8;
    if options.deterministic {
      text_rep |= libsqlite3_sys::SQLITE_DETERMINISTIC;
    }
    if options.direct_only {
      text_rep |= libsqlite3_sys::SQLITE_DIRECTONLY;
    }

    let db = self.conn.borrow();
    let db = db.as_ref().ok_or(SqliteError::InUse)?;

    let context =
      v8::Global::new(scope, scope.get_current_context()).into_raw();

    let start = v8::Global::new(scope, options.start).into_raw();

    let step_fn = v8::Global::new(scope, options.step).into_raw();

    let inverse_fn = options
      .inverse
      .map(|inv| v8::Global::new(scope, inv).into_raw());

    let final_fn = options
      .result
      .map(|res| v8::Global::new(scope, res).into_raw());

    let custom_aggregate = Box::new(CustomAggregate {
      context,
      use_big_int_arguments: options.use_big_int_arguments,
      start,
      step_fn,
      inverse_fn,
      final_fn,
      ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
      callback_depth: Rc::clone(&self.callback_depth),
    });

    let custom_aggregate_ptr = Box::into_raw(custom_aggregate);

    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let raw_handle = unsafe { db.handle() };
    let name_cstring = CString::new(name)?;

    let (value_callback, inverse_callback) = if options.inverse.is_some() {
      (
        Some(
          custom_aggregate_xvalue
            as unsafe extern "C" fn(*mut libsqlite3_sys::sqlite3_context),
        ),
        Some(
          custom_aggregate_xinverse
            as unsafe extern "C" fn(
              *mut libsqlite3_sys::sqlite3_context,
              i32,
              *mut *mut libsqlite3_sys::sqlite3_value,
            ),
        ),
      )
    } else {
      (None, None)
    };

    // SAFETY: `raw_handle` is a valid database handle.
    // The v8 handles stored in `CustomAggregate` are released in `custom_aggregate_xdestroy`.
    let r = unsafe {
      sqlite3_create_window_function(
        raw_handle,
        name_cstring.as_ptr(),
        argc,
        text_rep,
        custom_aggregate_ptr as *mut c_void,
        Some(custom_aggregate_xstep),
        Some(custom_aggregate_xfinal),
        value_callback,
        inverse_callback,
        Some(custom_aggregate_xdestroy),
      )
    };
    check_error_code(r, raw_handle)?;

    Ok(())
  }

  #[validate(is_open)]
  #[cppgc]
  fn create_tag_store(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    #[varargs] args: Option<&v8::FunctionCallbackArguments>,
  ) -> Result<SQLTagStore, SqliteError> {
    let capacity = if let Some(args) = args
      && args.length() > 0
    {
      let val = args.get(0);
      if val.is_number() {
        val.uint32_value(scope).unwrap_or(1000)
      } else {
        1000
      }
    } else {
      1000
    };

    let db_object = args.map(|a| a.this()).ok_or(SqliteError::AlreadyClosed)?;

    Ok(SQLTagStore::create(
      self.conn.clone(),
      self.statements.clone(),
      capacity,
      self.options.return_arrays,
      self.options.use_big_int_arguments,
      v8::Global::new(scope, db_object),
    ))
  }

  // Serializes the contents of the database into a Uint8Array. The database
  // can be either an in-memory database or a database opened from a file.
  //
  // This method is a wrapper around `sqlite3_serialize()`.
  #[validate(is_open)]
  fn serialize<'a>(
    &self,
    scope: &mut v8::PinScope<'a, '_>,
    db_name_value: v8::Local<'a, v8::Value>,
  ) -> Result<v8::Local<'a, v8::Value>, SqliteError> {
    let db = self.conn.borrow();
    let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    let db_name = if db_name_value.is_undefined() {
      "main".to_string()
    } else {
      let Ok(s) = v8::Local::<v8::String>::try_from(db_name_value) else {
        return Err(SqliteError::Validation(
          validators::Error::InvalidArgType(
            "The \"dbName\" argument must be a string.".into(),
          ),
        ));
      };
      s.to_rust_string_lossy(scope)
    };

    let name_cstring = CString::new(db_name)?;

    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let raw_handle = unsafe { conn.handle() };

    let mut size: libsqlite3_sys::sqlite3_int64 = 0;
    // SAFETY: `raw_handle` is a valid sqlite3 pointer; `name_cstring` is a
    // valid C string for the lifetime of this call; `size` is a valid out
    // pointer.
    let data = unsafe {
      libsqlite3_sys::sqlite3_serialize(
        raw_handle,
        name_cstring.as_ptr(),
        &mut size,
        0,
      )
    };

    if data.is_null() {
      return Err(SqliteError::SqliteSysError {
        message: "unable to serialize database".to_string(),
        errstr: "unable to serialize database".to_string(),
        errcode: libsqlite3_sys::SQLITE_ERROR as _,
      });
    }

    let len = size as usize;
    // SAFETY: `data` points to `len` bytes owned by SQLite and is valid for
    // reads of that length.
    let bytes = unsafe { std::slice::from_raw_parts(data, len) }.to_vec();
    // SAFETY: `data` was allocated by `sqlite3_serialize` and must be freed
    // with `sqlite3_free`.
    unsafe {
      libsqlite3_sys::sqlite3_free(data as *mut c_void);
    }

    let backing =
      v8::ArrayBuffer::new_backing_store_from_vec(bytes).make_shared();
    let ab = v8::ArrayBuffer::with_backing_store(scope, &backing);
    let view = v8::Uint8Array::new(scope, ab, 0, len).unwrap();
    Ok(view.into())
  }

  // Deserializes the given buffer into the database. Replaces the contents
  // of the named database (default `"main"`) with the contents of the
  // serialized buffer.
  //
  // This method is a wrapper around `sqlite3_deserialize()`.
  #[fast]
  #[validate(is_open)]
  #[undefined]
  fn deserialize<'a>(
    &self,
    scope: &mut v8::PinScope<'a, '_>,
    buffer_value: v8::Local<'a, v8::Value>,
    options_value: v8::Local<'a, v8::Value>,
  ) -> Result<(), SqliteError> {
    if !buffer_value.is_array_buffer_view() {
      return Err(SqliteError::Validation(validators::Error::InvalidArgType(
        "The \"serialized\" argument must be a TypedArray or a DataView."
          .into(),
      )));
    }

    let mut db_name = "main".to_string();
    let mut read_only = false;

    if !options_value.is_undefined() {
      let Ok(options_obj) = v8::Local::<v8::Object>::try_from(options_value)
      else {
        return Err(SqliteError::Validation(
          validators::Error::InvalidArgType(
            "The \"options\" argument must be an object.".into(),
          ),
        ));
      };

      v8_static_strings! {
        DB_NAME_STRING = "dbName",
        READ_ONLY_STRING = "readOnly",
      }

      let db_name_string = DB_NAME_STRING.v8_string(scope).unwrap();
      if let Some(name_val) = options_obj.get(scope, db_name_string.into())
        && !name_val.is_undefined()
      {
        let Ok(name_str) = v8::Local::<v8::String>::try_from(name_val) else {
          return Err(SqliteError::Validation(
            validators::Error::InvalidArgType(
              "The \"options.dbName\" argument must be a string.".into(),
            ),
          ));
        };
        db_name = name_str.to_rust_string_lossy(scope);
      }

      let read_only_string = READ_ONLY_STRING.v8_string(scope).unwrap();
      if let Some(read_only_val) =
        options_obj.get(scope, read_only_string.into())
        && !read_only_val.is_undefined()
      {
        let Ok(b) = v8::Local::<v8::Boolean>::try_from(read_only_val) else {
          return Err(SqliteError::Validation(
            validators::Error::InvalidArgType(
              "The \"options.readOnly\" argument must be a boolean.".into(),
            ),
          ));
        };
        read_only = b.is_true();
      }
    }

    let view: v8::Local<v8::ArrayBufferView> = buffer_value.try_into().unwrap();
    let byte_length = view.byte_length();
    let name_cstring = CString::new(db_name)?;

    // Per Node's contract, existing prepared statements are finalized before
    // deserialization is attempted, even if the operation subsequently fails.
    for stmt in self.statements.borrow_mut().drain(..) {
      if let Some(ptr) = stmt.get() {
        // SAFETY: `ptr` is a valid statement handle.
        unsafe {
          libsqlite3_sys::sqlite3_finalize(ptr);
        }
        stmt.set(None);
      }
    }

    let db = self.conn.borrow();
    let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
    // SAFETY: lifetime of the connection is guaranteed by reference counting.
    let raw_handle = unsafe { conn.handle() };

    // Allocate memory owned by SQLite, copy the input bytes in, and hand
    // ownership over via the FREEONCLOSE flag so SQLite frees it when the
    // database is closed or another deserialize replaces it.
    // sqlite3_malloc64(0) is allowed to return null; only treat null as an
    // error when we actually need a non-empty allocation.
    // SAFETY: ffi call with no preconditions other than a valid size.
    let buf = unsafe {
      libsqlite3_sys::sqlite3_malloc64(byte_length.max(1) as u64) as *mut u8
    };
    if buf.is_null() {
      return Err(SqliteError::SqliteSysError {
        message: "out of memory".to_string(),
        errstr: "out of memory".to_string(),
        errcode: libsqlite3_sys::SQLITE_NOMEM as _,
      });
    }

    if byte_length > 0 {
      let data = view.data() as *const u8;
      // SAFETY: `data` points to `byte_length` bytes; `buf` is a freshly
      // allocated buffer with the same capacity; the two regions do not
      // overlap.
      unsafe {
        std::ptr::copy_nonoverlapping(data, buf, byte_length);
      }
    }

    let mut flags: u32 = libsqlite3_sys::SQLITE_DESERIALIZE_FREEONCLOSE;
    if read_only {
      flags |= libsqlite3_sys::SQLITE_DESERIALIZE_READONLY;
    } else {
      flags |= libsqlite3_sys::SQLITE_DESERIALIZE_RESIZEABLE;
    }

    // SAFETY: `raw_handle` is a valid sqlite3 pointer; `name_cstring` is a
    // valid C string for the lifetime of this call; `buf` was allocated by
    // SQLite and ownership transfers via FREEONCLOSE.
    let r = unsafe {
      libsqlite3_sys::sqlite3_deserialize(
        raw_handle,
        name_cstring.as_ptr(),
        buf,
        byte_length as i64,
        byte_length as i64,
        flags,
      )
    };

    if r != libsqlite3_sys::SQLITE_OK {
      // sqlite3_deserialize frees `buf` itself on failure when FREEONCLOSE
      // is set.
      check_error_code(r, raw_handle)?;
      check_error_code2(r)?;
    }

    Ok(())
  }
}

#[repr(C)]
struct AggregateData {
  value: Option<NonNull<v8::Value>>,
  is_window: bool,
  initialized: bool,
}

struct CustomAggregate {
  context: NonNull<v8::Context>,
  use_big_int_arguments: bool,
  start: NonNull<v8::Value>,
  step_fn: NonNull<v8::Function>,
  inverse_fn: Option<NonNull<v8::Function>>,
  final_fn: Option<NonNull<v8::Function>>,
  ignore_next_sqlite_error: Rc<Cell<bool>>,
  callback_depth: Rc<Cell<usize>>,
}

enum AggregateStepKind {
  Step,
  Inverse,
}

unsafe fn get_aggregate_data(
  ctx: *mut libsqlite3_sys::sqlite3_context,
  custom_aggregate: &CustomAggregate,
  scope: &mut v8::PinScope<'_, '_>,
) -> Option<*mut AggregateData> {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    let agg_ptr = libsqlite3_sys::sqlite3_aggregate_context(
      ctx,
      std::mem::size_of::<AggregateData>() as i32,
    ) as *mut AggregateData;

    if agg_ptr.is_null() {
      sqlite_result_error(ctx, "Failed to allocate aggregate context");
      return None;
    }

    let agg = &mut *agg_ptr;
    if !agg.initialized {
      let start_local: v8::Local<v8::Value> =
        std::mem::transmute(custom_aggregate.start.as_ptr());

      let start_value = if start_local.is_function() {
        let start_fn: v8::Local<v8::Function> = start_local.try_into().unwrap();
        let recv = v8::null(scope).into();
        match start_fn.call(scope, recv, &[]) {
          Some(val) => val,
          None => {
            custom_aggregate.ignore_next_sqlite_error.set(true);
            sqlite_result_error(ctx, "");
            return None;
          }
        }
      } else {
        start_local
      };

      let global = v8::Global::new(scope, start_value);
      agg.value = Some(global.into_raw());
      agg.initialized = true;
      agg.is_window = false;
    }

    Some(agg_ptr)
  }
}

unsafe fn custom_aggregate_step_base(
  ctx: *mut libsqlite3_sys::sqlite3_context,
  argc: i32,
  argv: *mut *mut libsqlite3_sys::sqlite3_value,
  kind: AggregateStepKind,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    let data_ptr =
      libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomAggregate;

    if data_ptr.is_null() {
      sqlite_result_error(
        ctx,
        "Internal error: missing custom aggregate context",
      );
      return;
    }

    let data = &*data_ptr;
    // Block close() from freeing the in-flight statement under the VDBE.
    let _depth_guard = CallbackDepthGuard::new(&data.callback_depth);
    let context_local: v8::Local<v8::Context> =
      std::mem::transmute(data.context.as_ptr());

    v8::callback_scope!(unsafe cb_scope, context_local);
    v8::scope!(scope, cb_scope);
    v8::tc_scope!(tc_scope, scope);

    let Some(agg_ptr) = get_aggregate_data(ctx, data, tc_scope) else {
      if tc_scope.has_caught() {
        data.ignore_next_sqlite_error.set(true);
        sqlite_result_error(ctx, "");
        tc_scope.rethrow();
      }
      return;
    };
    let agg = &mut *agg_ptr;

    let step_fn = match kind {
      AggregateStepKind::Step => {
        let step_fn: v8::Local<v8::Function> =
          std::mem::transmute(data.step_fn.as_ptr());
        step_fn
      }
      AggregateStepKind::Inverse => {
        if let Some(inverse_ptr) = data.inverse_fn {
          let inverse_fn: v8::Local<v8::Function> =
            std::mem::transmute(inverse_ptr.as_ptr());
          inverse_fn
        } else {
          sqlite_result_error(ctx, "Internal error: inverse function not set");
          return;
        }
      }
    };

    let argc_len = usize::try_from(argc).unwrap_or(0);
    let args_slice = if argc_len == 0 {
      &[]
    } else {
      std::slice::from_raw_parts(argv, argc_len)
    };

    let current_value = if let Some(value_ptr) = agg.value {
      let global = v8::Global::from_raw(tc_scope, value_ptr);
      let local = v8::Local::new(tc_scope, &global);
      std::mem::forget(global);
      local
    } else {
      v8::undefined(tc_scope).into()
    };

    let mut js_args = Vec::with_capacity(args_slice.len() + 1);
    js_args.push(current_value);

    for &value_ptr in args_slice {
      if let Some(arg) =
        sqlite_value_to_v8(tc_scope, value_ptr, data.use_big_int_arguments)
      {
        js_args.push(arg);
      } else {
        data.ignore_next_sqlite_error.set(true);
        sqlite_result_error(ctx, "");
        tc_scope.rethrow();
        return;
      }
    }

    let recv = v8::undefined(tc_scope).into();
    let result = step_fn
      .call(tc_scope, recv, &js_args)
      .unwrap_or_else(|| v8::undefined(tc_scope).into());

    if tc_scope.has_caught() {
      data.ignore_next_sqlite_error.set(true);
      sqlite_result_error(ctx, "");
      tc_scope.rethrow();
      return;
    }

    if let Some(old_ptr) = agg.value {
      let _ = v8::Global::from_raw(tc_scope, old_ptr);
    }
    let global = v8::Global::new(tc_scope, result);
    agg.value = Some(global.into_raw());
  }
}

unsafe fn custom_aggregate_value_base(
  ctx: *mut libsqlite3_sys::sqlite3_context,
  is_final: bool,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    let data_ptr =
      libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomAggregate;
    if data_ptr.is_null() {
      sqlite_result_error(
        ctx,
        "Internal error: missing custom aggregate context",
      );
      return;
    }

    let data = &*data_ptr;
    // Block close() from freeing the in-flight statement under the VDBE.
    let _depth_guard = CallbackDepthGuard::new(&data.callback_depth);
    let context_local: v8::Local<v8::Context> =
      std::mem::transmute(data.context.as_ptr());

    v8::callback_scope!(unsafe cb_scope, context_local);
    v8::scope!(scope, cb_scope);
    v8::tc_scope!(tc_scope, scope);

    let Some(agg_ptr) = get_aggregate_data(ctx, data, tc_scope) else {
      if tc_scope.has_caught() {
        data.ignore_next_sqlite_error.set(true);
        sqlite_result_error(ctx, "");
        tc_scope.rethrow();
      }
      return;
    };
    let agg = &mut *agg_ptr;

    if !is_final {
      agg.is_window = true;
    } else if agg.is_window {
      if let Some(value_ptr) = agg.value.take() {
        let _ = v8::Global::from_raw(tc_scope, value_ptr);
      }
      return;
    }

    let current_value = if let Some(value_ptr) = agg.value {
      let global = v8::Global::from_raw(tc_scope, value_ptr);
      let local = v8::Local::new(tc_scope, &global);
      if is_final {
        agg.value = None;
      } else {
        std::mem::forget(global);
      }
      local
    } else {
      v8::undefined(tc_scope).into()
    };

    let result = if let Some(result_fn_ptr) = data.final_fn {
      let result_fn: v8::Local<v8::Function> =
        std::mem::transmute(result_fn_ptr.as_ptr());
      let ret = result_fn
        .call(tc_scope, v8::null(tc_scope).into(), &[current_value])
        .unwrap_or_else(|| v8::undefined(tc_scope).into());

      if tc_scope.has_caught() {
        data.ignore_next_sqlite_error.set(true);
        sqlite_result_error(ctx, "");
        tc_scope.rethrow();
        return;
      }
      ret
    } else {
      current_value
    };

    js_value_to_sqlite(tc_scope, ctx, result);
    if tc_scope.has_caught() {
      data.ignore_next_sqlite_error.set(true);
      sqlite_result_error(ctx, "");
      tc_scope.rethrow();
    }
  }
}

unsafe extern "C" fn custom_aggregate_xstep(
  ctx: *mut libsqlite3_sys::sqlite3_context,
  argc: i32,
  argv: *mut *mut libsqlite3_sys::sqlite3_value,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    custom_aggregate_step_base(ctx, argc, argv, AggregateStepKind::Step);
  }
}

unsafe extern "C" fn custom_aggregate_xinverse(
  ctx: *mut libsqlite3_sys::sqlite3_context,
  argc: i32,
  argv: *mut *mut libsqlite3_sys::sqlite3_value,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    custom_aggregate_step_base(ctx, argc, argv, AggregateStepKind::Inverse);
  }
}

unsafe extern "C" fn custom_aggregate_xfinal(
  ctx: *mut libsqlite3_sys::sqlite3_context,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    custom_aggregate_value_base(ctx, true);
  }
}

unsafe extern "C" fn custom_aggregate_xvalue(
  ctx: *mut libsqlite3_sys::sqlite3_context,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    custom_aggregate_value_base(ctx, false);
  }
}

unsafe extern "C" fn custom_aggregate_xdestroy(data: *mut c_void) {
  // SAFETY: `data` is a valid pointer to CustomAggregate.
  // We intentionally do not re-enter V8 here. SQLite may invoke this
  // destructor while the isolate is already tearing down, so creating a
  // callback scope can panic. The raw V8 handles are allowed to leak in that
  // case, matching the custom function destroy path.
  unsafe {
    let _ = Box::from_raw(data as *mut CustomAggregate);
  }
}

impl DatabaseSync {
  fn consume_ignore_next_sqlite_error(&self) -> bool {
    self.ignore_next_sqlite_error.replace(false)
  }
}

struct CustomFunctionData {
  callback: NonNull<v8::Function>,
  context: NonNull<v8::Context>,
  use_big_int_arguments: bool,
  ignore_next_sqlite_error: Rc<Cell<bool>>,
  callback_depth: Rc<Cell<usize>>,
}

struct AuthorizerData {
  callback: NonNull<v8::Function>,
  context: NonNull<v8::Context>,
  ignore_next_sqlite_error: Rc<Cell<bool>>,
  callback_depth: Rc<Cell<usize>>,
  // authorizer_callback holds shared references across reentrant JS calls.
  // Mutable callback-lifetime state must use interior mutability so reentrant
  // release/drop paths never create an aliasing &mut AuthorizerData.
  active_callbacks: Cell<usize>,
  free_after_callback: Cell<bool>,
}

struct AuthorizerCallbackGuard {
  data_ptr: *mut AuthorizerData,
  callback_depth: Rc<Cell<usize>>,
}

impl AuthorizerCallbackGuard {
  unsafe fn new(data_ptr: *mut AuthorizerData) -> Self {
    // SAFETY: caller guarantees `data_ptr` points to valid AuthorizerData.
    let data = unsafe { &*data_ptr };
    data
      .active_callbacks
      .set(data.active_callbacks.get().saturating_add(1));
    data
      .callback_depth
      .set(data.callback_depth.get().saturating_add(1));
    Self {
      data_ptr,
      callback_depth: Rc::clone(&data.callback_depth),
    }
  }
}

impl Drop for AuthorizerCallbackGuard {
  fn drop(&mut self) {
    // SAFETY: AuthorizerData cannot be freed while active_callbacks is non-zero.
    unsafe {
      let data = &*self.data_ptr;
      let active_callbacks = data.active_callbacks.get().saturating_sub(1);
      data.active_callbacks.set(active_callbacks);
      self
        .callback_depth
        .set(self.callback_depth.get().saturating_sub(1));
      if active_callbacks == 0 && data.free_after_callback.get() {
        free_authorizer_data(self.data_ptr);
      }
    }
  }
}

unsafe extern "C" fn custom_function_handler(
  ctx: *mut libsqlite3_sys::sqlite3_context,
  argc: i32,
  argv: *mut *mut libsqlite3_sys::sqlite3_value,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    let data_ptr =
      libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomFunctionData;
    if data_ptr.is_null() {
      sqlite_result_error(
        ctx,
        "Internal error: missing custom function context",
      );
      return;
    }

    let data = &*data_ptr;
    // Block close() from freeing the in-flight statement under the VDBE.
    let _depth_guard = CallbackDepthGuard::new(&data.callback_depth);
    let context_local: v8::Local<v8::Context> =
      std::mem::transmute(data.context.as_ptr());

    v8::callback_scope!(unsafe cb_scope, context_local);
    v8::scope!(scope, cb_scope);
    v8::tc_scope!(tc_scope, scope);

    let function_local: v8::Local<v8::Function> =
      std::mem::transmute(data.callback.as_ptr());

    let argc_len = usize::try_from(argc).unwrap_or(0);
    let args_slice = if argc_len == 0 {
      &[]
    } else {
      std::slice::from_raw_parts(argv, argc_len)
    };

    let mut js_args = Vec::with_capacity(args_slice.len());
    for &value_ptr in args_slice {
      if let Some(arg) =
        sqlite_value_to_v8(tc_scope, value_ptr, data.use_big_int_arguments)
      {
        js_args.push(arg);
      } else {
        data.ignore_next_sqlite_error.set(true);
        sqlite_result_error(ctx, "");
        tc_scope.rethrow();
        return;
      }
    }

    let recv = v8::undefined(tc_scope).into();
    let result = function_local.call(tc_scope, recv, &js_args);
    if tc_scope.has_caught() {
      data.ignore_next_sqlite_error.set(true);
      sqlite_result_error(ctx, "");
      tc_scope.rethrow();
      return;
    }

    if let Some(value) = result {
      js_value_to_sqlite(tc_scope, ctx, value);
      if tc_scope.has_caught() {
        data.ignore_next_sqlite_error.set(true);
        sqlite_result_error(ctx, "");
        tc_scope.rethrow();
      }
    }
  }
}

unsafe extern "C" fn custom_function_destroy(data: *mut c_void) {
  // SAFETY: `data` is a valid pointer to CustomFunctionData.
  // Drop the Box to free the raw pointers stored in it.
  // We intentionally do NOT convert the raw v8::Global handles back
  // via `Global::from_raw` because this callback can fire during
  // isolate teardown (e.g. sqlite3_close during GC) when the
  // isolate annex is already disposed, making scope creation unsafe.
  // The leaked Global pointers are harmless: the isolate is being
  // destroyed and will reclaim all V8 heap memory anyway.
  unsafe {
    let _ = Box::from_raw(data as *mut CustomFunctionData);
  }
}

fn sqlite_value_to_v8<'a>(
  scope: &mut v8::PinScope<'a, '_>,
  value: *mut libsqlite3_sys::sqlite3_value,
  use_big_int_arguments: bool,
) -> Option<v8::Local<'a, v8::Value>> {
  // SAFETY: `value` is a valid sqlite3_value pointer.
  unsafe {
    match libsqlite3_sys::sqlite3_value_type(value) {
      libsqlite3_sys::SQLITE_INTEGER => {
        let val = libsqlite3_sys::sqlite3_value_int64(value);
        if use_big_int_arguments {
          Some(v8::BigInt::new_from_i64(scope, val).into())
        } else if (-MAX_SAFE_JS_INTEGER..=MAX_SAFE_JS_INTEGER).contains(&val) {
          Some(v8::Number::new(scope, val as f64).into())
        } else {
          let msg = format!(
            "Value is too large to be represented as a JavaScript number: {}",
            val
          );
          throw_range_error(scope, &msg);
          None
        }
      }
      libsqlite3_sys::SQLITE_FLOAT => {
        let val = libsqlite3_sys::sqlite3_value_double(value);
        Some(v8::Number::new(scope, val).into())
      }
      libsqlite3_sys::SQLITE_TEXT => {
        let len = libsqlite3_sys::sqlite3_value_bytes(value) as usize;
        if len == 0 {
          let text =
            v8::String::new_from_utf8(scope, b"", v8::NewStringType::Normal)
              .unwrap();
          Some(text.into())
        } else {
          let ptr = libsqlite3_sys::sqlite3_value_text(value);
          let slice = std::slice::from_raw_parts(ptr, len);
          let text =
            v8::String::new_from_utf8(scope, slice, v8::NewStringType::Normal)
              .unwrap();
          Some(text.into())
        }
      }
      libsqlite3_sys::SQLITE_BLOB => {
        let len = libsqlite3_sys::sqlite3_value_bytes(value);
        if len == 0 {
          let ab = v8::ArrayBuffer::new(scope, 0);
          let view = v8::Uint8Array::new(scope, ab, 0, 0).unwrap();
          Some(view.into())
        } else {
          let ptr = libsqlite3_sys::sqlite3_value_blob(value) as *const u8;
          let slice = std::slice::from_raw_parts(ptr, len as usize);
          let backing =
            v8::ArrayBuffer::new_backing_store_from_vec(slice.to_vec())
              .make_shared();
          let ab = v8::ArrayBuffer::with_backing_store(scope, &backing);
          let view = v8::Uint8Array::new(scope, ab, 0, len as usize).unwrap();
          Some(view.into())
        }
      }
      libsqlite3_sys::SQLITE_NULL => Some(v8::null(scope).into()),
      _ => Some(v8::undefined(scope).into()),
    }
  }
}

fn js_value_to_sqlite(
  scope: &mut v8::PinScope<'_, '_>,
  ctx: *mut libsqlite3_sys::sqlite3_context,
  value: v8::Local<v8::Value>,
) {
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    if value.is_null_or_undefined() {
      libsqlite3_sys::sqlite3_result_null(ctx);
      return;
    }

    if value.is_number() {
      let number = value.number_value(scope).unwrap_or(0f64);
      libsqlite3_sys::sqlite3_result_double(ctx, number);
      return;
    }

    if value.is_string() {
      let text = value.to_rust_string_lossy(scope);
      libsqlite3_sys::sqlite3_result_text(
        ctx,
        text.as_ptr() as *const _,
        text.len() as i32,
        libsqlite3_sys::SQLITE_TRANSIENT(),
      );
      return;
    }

    if value.is_array_buffer_view() {
      let view: v8::Local<v8::ArrayBufferView> = value.try_into().unwrap();
      let mut data = view.data();
      let mut size = view.byte_length();
      if data.is_null() {
        static EMPTY: [u8; 0] = [];
        data = EMPTY.as_ptr() as *mut _;
        size = 0;
      }
      libsqlite3_sys::sqlite3_result_blob(
        ctx,
        data,
        size as i32,
        libsqlite3_sys::SQLITE_TRANSIENT(),
      );
      return;
    }

    if value.is_big_int() {
      let bigint: v8::Local<v8::BigInt> = value.try_into().unwrap();
      let (int_value, lossless) = bigint.i64_value();
      if !lossless {
        throw_range_error(scope, "BigInt value is too large for SQLite");
        sqlite_result_error(ctx, "");
        return;
      }
      libsqlite3_sys::sqlite3_result_int64(ctx, int_value);
      return;
    }

    if value.is_promise() {
      sqlite_result_error(
        ctx,
        "Asynchronous user-defined functions are not supported",
      );
      return;
    }

    sqlite_result_error(
      ctx,
      "Returned JavaScript value cannot be converted to a SQLite value",
    );
  }
}

fn sqlite_result_error(
  ctx: *mut libsqlite3_sys::sqlite3_context,
  message: &str,
) {
  let msg = CString::new(message).unwrap();
  // SAFETY: `ctx` is a valid sqlite3_context pointer.
  unsafe {
    libsqlite3_sys::sqlite3_result_error(
      ctx,
      msg.as_ptr(),
      msg.as_bytes().len() as i32,
    );
  }
}

macro_rules! nullable_cstring_to_v8_value {
  ($scope:expr, $ptr:expr) => {{
    if ($ptr).is_null() {
      v8::null($scope).into()
    } else {
      let cstr = CStr::from_ptr($ptr);
      v8::String::new_from_utf8(
        $scope,
        cstr.to_bytes(),
        v8::NewStringType::Normal,
      )
      .map(|s| s.into())
      .unwrap_or_else(|| v8::null($scope).into())
    }
  }};
}

unsafe extern "C" fn authorizer_callback(
  user_data: *mut c_void,
  action_code: i32,
  param1: *const c_char,
  param2: *const c_char,
  param3: *const c_char,
  param4: *const c_char,
) -> i32 {
  // SAFETY: `user_data` is a valid pointer to AuthorizerData.
  unsafe {
    let data_ptr = user_data as *mut AuthorizerData;
    if data_ptr.is_null() {
      return libsqlite3_sys::SQLITE_DENY;
    }

    let _callback_guard = AuthorizerCallbackGuard::new(data_ptr);
    let data = &*data_ptr;
    let context_local: v8::Local<v8::Context> =
      std::mem::transmute(data.context.as_ptr());

    v8::callback_scope!(unsafe cb_scope, context_local);
    v8::scope!(scope, cb_scope);

    let result: Option<v8::Local<'_, v8::Value>>;
    {
      v8::tc_scope!(tc_scope, scope);

      let function_local: v8::Local<v8::Function> =
        std::mem::transmute(data.callback.as_ptr());

      let action_code_js = v8::Integer::new(tc_scope, action_code).into();

      let param1_js: v8::Local<v8::Value> =
        nullable_cstring_to_v8_value!(tc_scope, param1);
      let param2_js: v8::Local<v8::Value> =
        nullable_cstring_to_v8_value!(tc_scope, param2);
      let param3_js: v8::Local<v8::Value> =
        nullable_cstring_to_v8_value!(tc_scope, param3);
      let param4_js: v8::Local<v8::Value> =
        nullable_cstring_to_v8_value!(tc_scope, param4);

      let js_args =
        [action_code_js, param1_js, param2_js, param3_js, param4_js];
      let recv = v8::undefined(tc_scope).into();
      result = function_local.call(tc_scope, recv, &js_args);

      if tc_scope.has_caught() {
        data.ignore_next_sqlite_error.set(true);
        tc_scope.rethrow();
        return libsqlite3_sys::SQLITE_DENY;
      }
    }

    let Some(value) = result else {
      data.ignore_next_sqlite_error.set(true);
      return libsqlite3_sys::SQLITE_DENY;
    };

    if !value.is_int32() {
      let message = v8::String::new(
        scope,
        "Authorizer callback must return an integer authorization code",
      )
      .unwrap();
      let err = v8::Exception::type_error(scope, message);
      data.ignore_next_sqlite_error.set(true);
      scope.throw_exception(err);
      return libsqlite3_sys::SQLITE_DENY;
    }

    let int_result = value
      .int32_value(scope)
      .unwrap_or(libsqlite3_sys::SQLITE_DENY);

    if int_result != libsqlite3_sys::SQLITE_OK
      && int_result != libsqlite3_sys::SQLITE_DENY
      && int_result != libsqlite3_sys::SQLITE_IGNORE
    {
      let message = v8::String::new(
        scope,
        "Authorizer callback returned a invalid authorization code",
      )
      .unwrap();
      let err = v8::Exception::range_error(scope, message);
      data.ignore_next_sqlite_error.set(true);
      scope.throw_exception(err);
      return libsqlite3_sys::SQLITE_DENY;
    }

    int_result
  }
}

unsafe fn free_authorizer_data(data_ptr: *mut AuthorizerData) {
  if data_ptr.is_null() {
    return;
  }
  // SAFETY: `data_ptr` is a valid pointer to AuthorizerData.
  // The v8 handles are properly dropped here.
  unsafe {
    let data = Box::from_raw(data_ptr);
    let context_local: v8::Local<v8::Context> =
      std::mem::transmute(data.context.as_ptr());

    v8::callback_scope!(unsafe cb_scope, context_local);
    v8::scope!(scope, cb_scope);

    let _ = v8::Global::from_raw(scope, data.callback);
    let _ = v8::Global::from_raw(scope, data.context);
  }
}

unsafe fn release_authorizer_data(data_ptr: *mut AuthorizerData) {
  if data_ptr.is_null() {
    return;
  }

  // SAFETY: `data_ptr` is a valid pointer to AuthorizerData.
  unsafe {
    let data = &*data_ptr;
    if data.active_callbacks.get() > 0 {
      data.free_after_callback.set(true);
    } else {
      free_authorizer_data(data_ptr);
    }
  }
}

fn throw_range_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
  let msg = v8::String::new(scope, message).unwrap();
  let error = v8::Exception::range_error(scope, msg);

  v8_static_strings!(CODE = "code", ERR_OUT_OF_RANGE = "ERR_OUT_OF_RANGE");
  let code_key = CODE.v8_string(scope).unwrap();
  let code_value = ERR_OUT_OF_RANGE.v8_string(scope).unwrap();
  let error_obj: v8::Local<v8::Object> = error.try_into().unwrap();
  error_obj
    .set(scope, code_key.into(), code_value.into())
    .unwrap();

  scope.throw_exception(error);
}

fn throw_type_error_with_code(
  scope: &mut v8::PinScope<'_, '_>,
  message: &str,
  code: &str,
) {
  let msg = v8::String::new(scope, message).unwrap();
  let error = v8::Exception::type_error(scope, msg);

  v8_static_strings!(CODE = "code");
  let code_key = CODE.v8_string(scope).unwrap();
  let code_value = v8::String::new(scope, code).unwrap();
  let error_obj: v8::Local<v8::Object> = error.try_into().unwrap();
  error_obj
    .set(scope, code_key.into(), code_value.into())
    .unwrap();

  scope.throw_exception(error);
}

// Throws a plain `Error` carrying `code`. Used for conditions that are not
// `TypeError`s or `RangeError`s, such as the ATTACH_DATABASE access boundary.
fn throw_error_with_code(
  scope: &mut v8::PinScope<'_, '_>,
  message: &str,
  code: &str,
) {
  let msg = v8::String::new(scope, message).unwrap();
  let error = v8::Exception::error(scope, msg);

  v8_static_strings!(CODE = "code");
  let code_key = CODE.v8_string(scope).unwrap();
  let code_value = v8::String::new(scope, code).unwrap();
  let error_obj: v8::Local<v8::Object> = error.try_into().unwrap();
  error_obj
    .set(scope, code_key.into(), code_value.into())
    .unwrap();

  scope.throw_exception(error);
}

/// Object representing SQLite database limits.
/// This is returned by DatabaseSync.limits getter.
pub struct DatabaseSyncLimits {
  conn: Rc<RefCell<Option<rusqlite::Connection>>>,
  // When true, ATTACH DATABASE is held disabled for this connection because the
  // process lacks full permissions for the database path. The `attach` setter
  // refuses to raise the cap above 0 in that state.
  disable_attach: bool,
}

// SAFETY: we're sure this can be GCed
unsafe impl GarbageCollected for DatabaseSyncLimits {
  fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}

  fn get_name(&self) -> &'static std::ffi::CStr {
    c"DatabaseSyncLimits"
  }
}

impl DatabaseSyncLimits {
  fn create(
    conn: Rc<RefCell<Option<rusqlite::Connection>>>,
    disable_attach: bool,
  ) -> Self {
    Self {
      conn,
      disable_attach,
    }
  }

  fn get_limit(&self, limit: Limit) -> Result<i32, SqliteError> {
    let conn = self.conn.borrow();
    let conn = conn.as_ref().ok_or(SqliteError::AlreadyClosed)?;
    conn.limit(limit).map_err(SqliteError::from)
  }

  // Uses manual v8 exception throwing instead of returning Err because
  // returning Err from a cppgc setter would produce a double-throw.
  fn set_limit_value(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    limit: Limit,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    let conn = self.conn.borrow();
    let conn = conn.as_ref().ok_or(SqliteError::AlreadyClosed)?;

    let new_value = match coerce_limit_value(scope, value) {
      Ok(v) => v,
      Err(
        LimitCoercionError::NotANumber | LimitCoercionError::NotAnInteger,
      ) => {
        throw_type_error_with_code(
          scope,
          "Limit value must be a non-negative integer or Infinity.",
          "ERR_INVALID_ARG_TYPE",
        );
        return Ok(());
      }
      Err(LimitCoercionError::Negative) => {
        throw_range_error(scope, "Limit value must be non-negative.");
        return Ok(());
      }
    };

    conn.set_limit(limit, new_value)?;
    Ok(())
  }
}

#[op2]
impl DatabaseSyncLimits {
  #[constructor]
  #[cppgc]
  fn new(_: bool) -> Result<DatabaseSyncLimits, SqliteError> {
    Err(SqliteError::InvalidConstructor)
  }

  #[getter]
  fn length(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_LENGTH)
  }

  #[setter]
  fn length(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_LENGTH, value)
  }

  #[getter]
  #[rename("sqlLength")]
  fn sql_length(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_SQL_LENGTH)
  }

  #[rename("sqlLength")]
  #[setter]
  fn sql_length(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_SQL_LENGTH, value)
  }

  #[getter]
  fn column(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_COLUMN)
  }

  #[setter]
  fn column(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_COLUMN, value)
  }

  #[getter]
  #[rename("exprDepth")]
  fn expr_depth(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_EXPR_DEPTH)
  }

  #[rename("exprDepth")]
  #[setter]
  fn expr_depth(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_EXPR_DEPTH, value)
  }

  #[getter]
  #[rename("compoundSelect")]
  fn compound_select(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_COMPOUND_SELECT)
  }

  #[rename("compoundSelect")]
  #[setter]
  fn compound_select(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_COMPOUND_SELECT, value)
  }

  #[getter]
  #[rename("vdbeOp")]
  fn vdbe_op(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_VDBE_OP)
  }

  #[rename("vdbeOp")]
  #[setter]
  fn vdbe_op(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_VDBE_OP, value)
  }

  #[getter]
  #[rename("functionArg")]
  fn function_arg(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_FUNCTION_ARG)
  }

  #[rename("functionArg")]
  #[setter]
  fn function_arg(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_FUNCTION_ARG, value)
  }

  #[getter]
  fn attach(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_ATTACHED)
  }

  #[setter]
  fn attach(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    // ATTACH DATABASE is held disabled for processes without full permissions
    // for the database path. Refuse to raise the cap above 0 so the limit
    // setter cannot re-enable attach and bypass that boundary. A value of 0
    // (keeping it disabled) is allowed and falls through to the normal setter.
    // Malformed values also fall through so set_limit_value reports the right
    // ERR_INVALID_ARG_* error.
    if self.disable_attach
      && matches!(coerce_limit_value(scope, value), Ok(v) if v > 0)
    {
      throw_error_with_code(
        scope,
        "Cannot raise the \"attach\" limit: ATTACH DATABASE is disabled \
         without full permissions for the database path.",
        "ERR_ACCESS_DENIED",
      );
      return Ok(());
    }
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_ATTACHED, value)
  }

  #[getter]
  #[rename("likePatternLength")]
  fn like_pattern_length(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH)
  }

  #[rename("likePatternLength")]
  #[setter]
  fn like_pattern_length(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH, value)
  }

  #[getter]
  #[rename("variableNumber")]
  fn variable_number(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER)
  }

  #[rename("variableNumber")]
  #[setter]
  fn variable_number(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_VARIABLE_NUMBER, value)
  }

  #[getter]
  #[rename("triggerDepth")]
  fn trigger_depth(&self) -> Result<i32, SqliteError> {
    self.get_limit(Limit::SQLITE_LIMIT_TRIGGER_DEPTH)
  }

  #[rename("triggerDepth")]
  #[setter]
  fn trigger_depth(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<v8::Value>,
  ) -> Result<(), SqliteError> {
    self.set_limit_value(scope, Limit::SQLITE_LIMIT_TRIGGER_DEPTH, value)
  }
}
