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:
parent
98339cf327
commit
28834a89bb
7 changed files with 250 additions and 5 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
124
ext/node/ops/sqlite/session.rs
Normal file
124
ext/node/ops/sqlite/session.rs
Normal 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)
|
||||
}
|
|
@ -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);");
|
||||
|
|
Loading…
Add table
Reference in a new issue