Skip to content

Commit

Permalink
fix(ext/node): implement SQLite Session API (#27909)
Browse files Browse the repository at this point in the history
https://nodejs.org/api/sqlite.html#class-session

---------

Signed-off-by: Divy Srivastava <[email protected]>
  • Loading branch information
littledivy authored Feb 4, 2025
1 parent 98339cf commit 28834a8
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 5 deletions.
31 changes: 29 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ rand = "=0.8.5"
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
ring = "^0.17.0"
rusqlite = { version = "0.32.0", features = ["unlock_notify", "bundled"] }
rusqlite = { version = "0.32.0", features = ["unlock_notify", "bundled", "session"] }
rustls = { version = "0.23.11", default-features = false, features = ["logging", "std", "tls12", "ring"] }
rustls-pemfile = "2"
rustls-tokio-stream = "=0.3.0"
Expand Down
1 change: 1 addition & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ deno_core::extension!(deno_node,
objects = [
ops::perf_hooks::EldHistogram,
ops::sqlite::DatabaseSync,
ops::sqlite::Session,
ops::sqlite::StatementSync
],
esm_entry_point = "ext:deno_node/02_init.js",
Expand Down
62 changes: 62 additions & 0 deletions ext/node/ops/sqlite/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

use std::cell::Cell;
use std::cell::RefCell;
use std::ffi::CString;
use std::ptr::null;
use std::rc::Rc;

use deno_core::op2;
Expand All @@ -10,6 +12,8 @@ use deno_core::OpState;
use deno_permissions::PermissionsContainer;
use serde::Deserialize;

use super::session::SessionOptions;
use super::Session;
use super::SqliteError;
use super::StatementSync;

Expand Down Expand Up @@ -192,4 +196,62 @@ impl DatabaseSync {
use_big_ints: Cell::new(false),
})
}

// Creates and attaches a session to the database.
//
// This method is a wrapper around `sqlite3session_create()` and
// `sqlite3session_attach()`.
#[cppgc]
fn create_session(
&self,
#[serde] options: Option<SessionOptions>,
) -> Result<Session, 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 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(),
})
}
}
14 changes: 12 additions & 2 deletions ext/node/ops/sqlite/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
// Copyright 2018-2025 the Deno authors. MIT license.

mod database;
mod session;
mod statement;

pub use database::DatabaseSync;
use deno_permissions::PermissionCheckError;
pub use session::Session;
pub use statement::StatementSync;

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum SqliteError {
#[class(inherit)]
#[error(transparent)]
Permission(#[from] PermissionCheckError),
Permission(#[from] deno_permissions::PermissionCheckError),
#[class(generic)]
#[error(transparent)]
SqliteError(#[from] rusqlite::Error),
Expand Down Expand Up @@ -40,6 +41,15 @@ pub enum SqliteError {
#[error("Failed to prepare statement")]
PrepareFailed,
#[class(generic)]
#[error("Failed to create session")]
SessionCreateFailed,
#[class(generic)]
#[error("Failed to retrieve changeset")]
SessionChangesetFailed,
#[class(generic)]
#[error("Session is already closed")]
SessionClosed,
#[class(generic)]
#[error("Invalid constructor")]
InvalidConstructor,
#[class(generic)]
Expand Down
124 changes: 124 additions & 0 deletions ext/node/ops/sqlite/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2018-2025 the Deno authors. MIT license.

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

use deno_core::op2;
use deno_core::GarbageCollected;
use libsqlite3_sys as ffi;
use serde::Deserialize;

use super::SqliteError;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionOptions {
pub table: Option<String>,
pub db: Option<String>,
}

pub struct Session {
pub(crate) inner: *mut ffi::sqlite3_session,
pub(crate) freed: Cell<bool>,

// Hold a strong reference to the database.
pub(crate) _db: Rc<RefCell<Option<rusqlite::Connection>>>,
}

impl GarbageCollected for Session {}

impl Drop for Session {
fn drop(&mut self) {
let _ = self.delete();
}
}

impl Session {
fn delete(&self) -> Result<(), SqliteError> {
if self.freed.get() {
return Err(SqliteError::SessionClosed);
}

self.freed.set(true);
// Safety: `self.inner` is a valid session. double free is
// prevented by `freed` flag.
unsafe {
ffi::sqlite3session_delete(self.inner);
}

Ok(())
}
}

#[op2]
impl Session {
// Closes the session.
#[fast]
fn close(&self) -> Result<(), SqliteError> {
self.delete()
}

// Retrieves a changeset containing all changes since the changeset
// was created. Can be called multiple times.
//
// This method is a wrapper around `sqlite3session_changeset()`.
#[buffer]
fn changeset(&self) -> Result<Box<[u8]>, SqliteError> {
if self.freed.get() {
return Err(SqliteError::SessionClosed);
}

session_buffer_op(self.inner, ffi::sqlite3session_changeset)
}

// Similar to the method above, but generates a more compact patchset.
//
// This method is a wrapper around `sqlite3session_patchset()`.
#[buffer]
fn patchset(&self) -> Result<Box<[u8]>, SqliteError> {
if self.freed.get() {
return Err(SqliteError::SessionClosed);
}

session_buffer_op(self.inner, ffi::sqlite3session_patchset)
}
}

fn session_buffer_op(
s: *mut ffi::sqlite3_session,
f: unsafe extern "C" fn(
*mut ffi::sqlite3_session,
*mut i32,
*mut *mut c_void,
) -> i32,
) -> Result<Box<[u8]>, SqliteError> {
let mut n_buffer = 0;
let mut p_buffer = std::ptr::null_mut();

// Safety: `s` is a valid session and the buffer is allocated
// by sqlite3 and will be freed later.
let r = unsafe { f(s, &mut n_buffer, &mut p_buffer) };
if r != ffi::SQLITE_OK {
return Err(SqliteError::SessionChangesetFailed);
}

if n_buffer == 0 {
return Ok(Default::default());
}

// Safety: n_buffer is the size of the buffer.
let buffer = unsafe {
std::slice::from_raw_parts(p_buffer as *const u8, n_buffer as usize)
}
.to_vec()
.into_boxed_slice();

// Safety: free sqlite allocated buffer, we copied it into the JS buffer.
unsafe {
ffi::sqlite3_free(p_buffer);
}

Ok(buffer)
}
21 changes: 21 additions & 0 deletions tests/unit_node/sqlite_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,27 @@ Deno.test("[node/sqlite] StatementSync read bigints are supported", () => {
assertEquals(stmt.expandedSQL, "SELECT * FROM data");
});

Deno.test("[node/sqlite] createSession and changesets", () => {
const db = new DatabaseSync(":memory:");
const session = db.createSession();

db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
db.exec("INSERT INTO test (name) VALUES ('foo')");

assert(session.changeset() instanceof Uint8Array);
assert(session.patchset() instanceof Uint8Array);

assert(session.changeset().byteLength > 0);
assert(session.patchset().byteLength > 0);

session.close();

// Use after close shoud throw.
assertThrows(() => session.changeset(), Error, "Session is already closed");
// Close after close should throw.
assertThrows(() => session.close(), Error, "Session is already closed");
});

Deno.test("[node/sqlite] StatementSync integer too large", () => {
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE data(key INTEGER PRIMARY KEY);");
Expand Down

0 comments on commit 28834a8

Please sign in to comment.