Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ regex = "^1.7.0"
reqwest = { version = "0.12.5", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli", "socks", "json", "http2"] } # pinned because of https://github.com/seanmonstar/reqwest/pull/1955
reqwest_v011 = { package = "reqwest", version = "0.11", features = ["stream", "json", "multipart"] }
ring = "^0.17.14"
rusqlite = { version = "0.32.0", features = ["unlock_notify", "bundled"] }
rustls = { version = "0.23.11", default-features = false, features = ["logging", "std", "tls12", "ring"] }
rustls-pemfile = "2"
rustls-tokio-stream = "=0.3.0"
Expand Down
2 changes: 2 additions & 0 deletions ext/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ rand.workspace = true
regex.workspace = true
ring.workspace = true
rsa.workspace = true
rusqlite.workspace = true
sec1.workspace = true
serde.workspace = true
sha1.workspace = true
Expand Down Expand Up @@ -81,6 +82,7 @@ home = "0.5.9"
idna = "1.0.3"
ipnetwork = "0.20.0"
k256 = "0.13.1"
libsqlite3-sys = "0.30.1"
md-5 = { version = "0.10.5", features = ["oid"] }
md4 = "0.10.2"
memchr = "2.7.4"
Expand Down
5 changes: 4 additions & 1 deletion ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,9 @@ deno_core::extension!(deno_node,
ops::inspector::op_inspector_enabled,
],
objects = [
ops::perf_hooks::EldHistogram
ops::perf_hooks::EldHistogram,
ops::sqlite::DatabaseSync,
ops::sqlite::StatementSync
],
esm_entry_point = "ext:deno_node/02_init.js",
esm = [
Expand Down Expand Up @@ -655,6 +657,7 @@ deno_core::extension!(deno_node,
"node:readline" = "readline.ts",
"node:readline/promises" = "readline/promises.ts",
"node:repl" = "repl.ts",
"node:sqlite" = "sqlite.ts",
"node:stream" = "stream.ts",
"node:stream/consumers" = "stream/consumers.mjs",
"node:stream/promises" = "stream/promises.mjs",
Expand Down
1 change: 1 addition & 0 deletions ext/node/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod os;
pub mod perf_hooks;
pub mod process;
pub mod require;
pub mod sqlite;
pub mod tls;
pub mod util;
pub mod v8;
Expand Down
199 changes: 199 additions & 0 deletions ext/node/ops/sqlite/database.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright 2018-2025 the Deno authors. MIT license.

use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;

use deno_core::anyhow;
use deno_core::anyhow::anyhow;
use deno_core::op2;
use deno_core::GarbageCollected;
use deno_core::OpState;
use deno_permissions::PermissionsContainer;
use serde::Deserialize;

use super::StatementSync;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DatabaseSyncOptions {
#[serde(default = "true_fn")]
open: bool,
#[serde(default = "true_fn")]
enable_foreign_key_constraints: bool,
read_only: bool,
}

fn true_fn() -> bool {
true
}

impl Default for DatabaseSyncOptions {
fn default() -> Self {
DatabaseSyncOptions {
open: true,
enable_foreign_key_constraints: true,
read_only: false,
}
}
}

pub struct DatabaseSync {
conn: Rc<RefCell<Option<rusqlite::Connection>>>,
options: DatabaseSyncOptions,
location: String,
}

impl GarbageCollected for DatabaseSync {}

fn open_db(
state: &mut OpState,
readonly: bool,
location: &str,
) -> Result<rusqlite::Connection, anyhow::Error> {
if location == ":memory:" {
return Ok(rusqlite::Connection::open_in_memory()?);
}

state
.borrow::<PermissionsContainer>()
.check_read_with_api_name(location, Some("node:sqlite"))?;

if readonly {
return Ok(rusqlite::Connection::open_with_flags(
location,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)?);
}

state
.borrow::<PermissionsContainer>()
.check_write_with_api_name(location, Some("node:sqlite"))?;

Ok(rusqlite::Connection::open(location)?)
}

// 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,
#[serde] options: Option<DatabaseSyncOptions>,
) -> Result<DatabaseSync, anyhow::Error> {
let options = options.unwrap_or_default();

let db = if options.open {
let db = open_db(state, options.read_only, &location)?;

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

Ok(DatabaseSync {
conn: Rc::new(RefCell::new(db)),
location,
options,
})
}

// 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]
fn open(&self, state: &mut OpState) -> Result<(), anyhow::Error> {
if self.conn.borrow().is_some() {
return Err(anyhow!("Database is already open"));
}

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

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

Ok(())
}

// Closes the database connection. An exception is thrown if the
// database is not open.
#[fast]
fn close(&self) -> Result<(), anyhow::Error> {
if self.conn.borrow().is_none() {
return Err(anyhow!("Database is already closed"));
}

*self.conn.borrow_mut() = None;
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]
fn exec(&self, #[string] sql: &str) -> Result<(), anyhow::Error> {
let db = self.conn.borrow();
let db = db.as_ref().ok_or(anyhow!("Database is already in use"))?;

let mut stmt = db.prepare_cached(sql)?;
stmt.raw_execute()?;

Ok(())
}

// Compiles an SQL statement into a prepared statement.
//
// This method is a wrapper around `sqlite3_prepare_v2()`.
#[cppgc]
fn prepare(
&self,
#[string] sql: &str,
) -> Result<StatementSync, anyhow::Error> {
let db = self.conn.borrow();
let db = db.as_ref().ok_or(anyhow!("Database is already in use"))?;

// 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(),
)
};

if r != libsqlite3_sys::SQLITE_OK {
return Err(anyhow!("Failed to prepare statement"));
}

Ok(StatementSync {
inner: raw_stmt,
db: self.conn.clone(),
use_big_ints: Cell::new(false),
})
}
}
41 changes: 41 additions & 0 deletions ext/node/ops/sqlite/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2018-2025 the Deno authors. MIT license.

mod database;
mod statement;

pub use database::DatabaseSync;
pub use statement::StatementSync;

// #[derive(Debug, thiserror::Error, deno_error::JsError)]
// pub enum SqliteError {
// #[class(generic)]
// #[error(transparent)]
// SqliteError(#[from] rusqlite::Error),
// #[class(generic)]
// #[error("Database is already in use")]
// InUse,
// #[class(generic)]
// #[error("Failed to step statement")]
// FailedStep,
// #[class(generic)]
// #[error("Failed to bind parameter. {0}")]
// FailedBind(&'static str),
// #[class(generic)]
// #[error("Unknown column type")]
// UnknownColumnType,
// #[class(generic)]
// #[error("Failed to get SQL")]
// GetSqlFailed,
// #[class(generic)]
// #[error("Database is already closed")]
// AlreadyClosed,
// #[class(generic)]
// #[error("Database is already open")]
// AlreadyOpen,
// #[class(generic)]
// #[error("Failed to prepare statement")]
// PrepareFailed,
// #[class(generic)]
// #[error("Invalid constructor")]
// InvalidConstructor,
// }
Loading