0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-02-12 16:59:32 -05:00

fix(ext/node): implement SQLite Session API (#27909)

https://nodejs.org/api/sqlite.html#class-session

---------

Signed-off-by: Divy Srivastava <dj.srivastava23@gmail.com>
This commit is contained in:
Divy Srivastava 2025-02-04 21:59:13 +05:30 committed by GitHub
parent 98339cf327
commit 28834a89bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 250 additions and 5 deletions

31
Cargo.lock generated
View file

@ -511,6 +511,26 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"lazy_static",
"lazycell",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.87",
]
[[package]]
name = "bindgen"
version = "0.70.1"
@ -4819,6 +4839,12 @@ dependencies = [
"spin",
]
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.168"
@ -4886,6 +4912,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"bindgen 0.69.5",
"cc",
"pkg-config",
"vcpkg",
@ -4910,7 +4937,7 @@ version = "1.48.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8dfd1a173826d193e3b955e07c22765829890f62c677a59c4a410cb4f47c01"
dependencies = [
"bindgen",
"bindgen 0.70.1",
"libloading 0.8.5",
]
@ -8689,7 +8716,7 @@ version = "130.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a511192602f7b435b0a241c1947aa743eb7717f20a9195f4b5e8ed1952e01db1"
dependencies = [
"bindgen",
"bindgen 0.70.1",
"bitflags 2.6.0",
"fslock",
"gzip-header",

View file

@ -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"

View file

@ -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",

View file

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

View file

@ -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),
@ -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)]

View file

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

View file

@ -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);");