Skip to content

Commit 35e30e2

Browse files
authored
feat: add v2.2.0 node:sqlite support (#557)
* chore: add dependencies * chore: update `Cargo.lock` * feat: v2.2.0 `node:sqlite` support * stamp: perm check
1 parent 40de73d commit 35e30e2

File tree

11 files changed

+629
-6
lines changed

11 files changed

+629
-6
lines changed

Cargo.lock

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ regex = "^1.7.0"
155155
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
156156
reqwest_v011 = { package = "reqwest", version = "0.11", features = ["stream", "json", "multipart"] }
157157
ring = "^0.17.14"
158+
rusqlite = { version = "0.32.0", features = ["unlock_notify", "bundled"] }
158159
rustls = { version = "0.23.11", default-features = false, features = ["logging", "std", "tls12", "ring"] }
159160
rustls-pemfile = "2"
160161
rustls-tokio-stream = "=0.3.0"

ext/node/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ rand.workspace = true
5353
regex.workspace = true
5454
ring.workspace = true
5555
rsa.workspace = true
56+
rusqlite.workspace = true
5657
sec1.workspace = true
5758
serde.workspace = true
5859
sha1.workspace = true
@@ -81,6 +82,7 @@ home = "0.5.9"
8182
idna = "1.0.3"
8283
ipnetwork = "0.20.0"
8384
k256 = "0.13.1"
85+
libsqlite3-sys = "0.30.1"
8486
md-5 = { version = "0.10.5", features = ["oid"] }
8587
md4 = "0.10.2"
8688
memchr = "2.7.4"

ext/node/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,9 @@ deno_core::extension!(deno_node,
431431
ops::inspector::op_inspector_enabled,
432432
],
433433
objects = [
434-
ops::perf_hooks::EldHistogram
434+
ops::perf_hooks::EldHistogram,
435+
ops::sqlite::DatabaseSync,
436+
ops::sqlite::StatementSync
435437
],
436438
esm_entry_point = "ext:deno_node/02_init.js",
437439
esm = [
@@ -655,6 +657,7 @@ deno_core::extension!(deno_node,
655657
"node:readline" = "readline.ts",
656658
"node:readline/promises" = "readline/promises.ts",
657659
"node:repl" = "repl.ts",
660+
"node:sqlite" = "sqlite.ts",
658661
"node:stream" = "stream.ts",
659662
"node:stream/consumers" = "stream/consumers.mjs",
660663
"node:stream/promises" = "stream/promises.mjs",

ext/node/ops/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod os;
1313
pub mod perf_hooks;
1414
pub mod process;
1515
pub mod require;
16+
pub mod sqlite;
1617
pub mod tls;
1718
pub mod util;
1819
pub mod v8;

ext/node/ops/sqlite/database.rs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
use std::cell::Cell;
4+
use std::cell::RefCell;
5+
use std::rc::Rc;
6+
7+
use deno_core::anyhow;
8+
use deno_core::anyhow::anyhow;
9+
use deno_core::op2;
10+
use deno_core::GarbageCollected;
11+
use deno_core::OpState;
12+
use deno_permissions::PermissionsContainer;
13+
use serde::Deserialize;
14+
15+
use super::StatementSync;
16+
17+
#[derive(Deserialize)]
18+
#[serde(rename_all = "camelCase")]
19+
struct DatabaseSyncOptions {
20+
#[serde(default = "true_fn")]
21+
open: bool,
22+
#[serde(default = "true_fn")]
23+
enable_foreign_key_constraints: bool,
24+
read_only: bool,
25+
}
26+
27+
fn true_fn() -> bool {
28+
true
29+
}
30+
31+
impl Default for DatabaseSyncOptions {
32+
fn default() -> Self {
33+
DatabaseSyncOptions {
34+
open: true,
35+
enable_foreign_key_constraints: true,
36+
read_only: false,
37+
}
38+
}
39+
}
40+
41+
pub struct DatabaseSync {
42+
conn: Rc<RefCell<Option<rusqlite::Connection>>>,
43+
options: DatabaseSyncOptions,
44+
location: String,
45+
}
46+
47+
impl GarbageCollected for DatabaseSync {}
48+
49+
fn open_db(
50+
state: &mut OpState,
51+
readonly: bool,
52+
location: &str,
53+
) -> Result<rusqlite::Connection, anyhow::Error> {
54+
if location == ":memory:" {
55+
return Ok(rusqlite::Connection::open_in_memory()?);
56+
}
57+
58+
state
59+
.borrow::<PermissionsContainer>()
60+
.check_read_with_api_name(location, Some("node:sqlite"))?;
61+
62+
if readonly {
63+
return Ok(rusqlite::Connection::open_with_flags(
64+
location,
65+
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
66+
)?);
67+
}
68+
69+
state
70+
.borrow::<PermissionsContainer>()
71+
.check_write_with_api_name(location, Some("node:sqlite"))?;
72+
73+
Ok(rusqlite::Connection::open(location)?)
74+
}
75+
76+
// Represents a single connection to a SQLite database.
77+
#[op2]
78+
impl DatabaseSync {
79+
// Constructs a new `DatabaseSync` instance.
80+
//
81+
// A SQLite database can be stored in a file or in memory. To
82+
// use a file-backed database, the `location` should be a path.
83+
// To use an in-memory database, the `location` should be special
84+
// name ":memory:".
85+
#[constructor]
86+
#[cppgc]
87+
fn new(
88+
state: &mut OpState,
89+
#[string] location: String,
90+
#[serde] options: Option<DatabaseSyncOptions>,
91+
) -> Result<DatabaseSync, anyhow::Error> {
92+
let options = options.unwrap_or_default();
93+
94+
let db = if options.open {
95+
let db = open_db(state, options.read_only, &location)?;
96+
97+
if options.enable_foreign_key_constraints {
98+
db.execute("PRAGMA foreign_keys = ON", [])?;
99+
}
100+
Some(db)
101+
} else {
102+
None
103+
};
104+
105+
Ok(DatabaseSync {
106+
conn: Rc::new(RefCell::new(db)),
107+
location,
108+
options,
109+
})
110+
}
111+
112+
// Opens the database specified by `location` of this instance.
113+
//
114+
// This method should only be used when the database is not opened
115+
// via the constructor. An exception is thrown if the database is
116+
// already opened.
117+
#[fast]
118+
fn open(&self, state: &mut OpState) -> Result<(), anyhow::Error> {
119+
if self.conn.borrow().is_some() {
120+
return Err(anyhow!("Database is already open"));
121+
}
122+
123+
let db = open_db(state, self.options.read_only, &self.location)?;
124+
if self.options.enable_foreign_key_constraints {
125+
db.execute("PRAGMA foreign_keys = ON", [])?;
126+
}
127+
128+
*self.conn.borrow_mut() = Some(db);
129+
130+
Ok(())
131+
}
132+
133+
// Closes the database connection. An exception is thrown if the
134+
// database is not open.
135+
#[fast]
136+
fn close(&self) -> Result<(), anyhow::Error> {
137+
if self.conn.borrow().is_none() {
138+
return Err(anyhow!("Database is already closed"));
139+
}
140+
141+
*self.conn.borrow_mut() = None;
142+
Ok(())
143+
}
144+
145+
// This method allows one or more SQL statements to be executed
146+
// without returning any results.
147+
//
148+
// This method is a wrapper around sqlite3_exec().
149+
#[fast]
150+
fn exec(&self, #[string] sql: &str) -> Result<(), anyhow::Error> {
151+
let db = self.conn.borrow();
152+
let db = db.as_ref().ok_or(anyhow!("Database is already in use"))?;
153+
154+
let mut stmt = db.prepare_cached(sql)?;
155+
stmt.raw_execute()?;
156+
157+
Ok(())
158+
}
159+
160+
// Compiles an SQL statement into a prepared statement.
161+
//
162+
// This method is a wrapper around `sqlite3_prepare_v2()`.
163+
#[cppgc]
164+
fn prepare(
165+
&self,
166+
#[string] sql: &str,
167+
) -> Result<StatementSync, anyhow::Error> {
168+
let db = self.conn.borrow();
169+
let db = db.as_ref().ok_or(anyhow!("Database is already in use"))?;
170+
171+
// SAFETY: lifetime of the connection is guaranteed by reference
172+
// counting.
173+
let raw_handle = unsafe { db.handle() };
174+
175+
let mut raw_stmt = std::ptr::null_mut();
176+
177+
// SAFETY: `sql` points to a valid memory location and its length
178+
// is correct.
179+
let r = unsafe {
180+
libsqlite3_sys::sqlite3_prepare_v2(
181+
raw_handle,
182+
sql.as_ptr() as *const _,
183+
sql.len() as i32,
184+
&mut raw_stmt,
185+
std::ptr::null_mut(),
186+
)
187+
};
188+
189+
if r != libsqlite3_sys::SQLITE_OK {
190+
return Err(anyhow!("Failed to prepare statement"));
191+
}
192+
193+
Ok(StatementSync {
194+
inner: raw_stmt,
195+
db: self.conn.clone(),
196+
use_big_ints: Cell::new(false),
197+
})
198+
}
199+
}

ext/node/ops/sqlite/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
mod database;
4+
mod statement;
5+
6+
pub use database::DatabaseSync;
7+
pub use statement::StatementSync;
8+
9+
// #[derive(Debug, thiserror::Error, deno_error::JsError)]
10+
// pub enum SqliteError {
11+
// #[class(generic)]
12+
// #[error(transparent)]
13+
// SqliteError(#[from] rusqlite::Error),
14+
// #[class(generic)]
15+
// #[error("Database is already in use")]
16+
// InUse,
17+
// #[class(generic)]
18+
// #[error("Failed to step statement")]
19+
// FailedStep,
20+
// #[class(generic)]
21+
// #[error("Failed to bind parameter. {0}")]
22+
// FailedBind(&'static str),
23+
// #[class(generic)]
24+
// #[error("Unknown column type")]
25+
// UnknownColumnType,
26+
// #[class(generic)]
27+
// #[error("Failed to get SQL")]
28+
// GetSqlFailed,
29+
// #[class(generic)]
30+
// #[error("Database is already closed")]
31+
// AlreadyClosed,
32+
// #[class(generic)]
33+
// #[error("Database is already open")]
34+
// AlreadyOpen,
35+
// #[class(generic)]
36+
// #[error("Failed to prepare statement")]
37+
// PrepareFailed,
38+
// #[class(generic)]
39+
// #[error("Invalid constructor")]
40+
// InvalidConstructor,
41+
// }

0 commit comments

Comments
 (0)