mirror of
https://github.com/denoland/deno.git
synced 2025-02-13 01:06:00 -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",
|
"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]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.70.1"
|
version = "0.70.1"
|
||||||
|
@ -4819,6 +4839,12 @@ dependencies = [
|
||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazycell"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.168"
|
version = "0.2.168"
|
||||||
|
@ -4886,6 +4912,7 @@ version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bindgen 0.69.5",
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
|
@ -4910,7 +4937,7 @@ version = "1.48.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca8dfd1a173826d193e3b955e07c22765829890f62c677a59c4a410cb4f47c01"
|
checksum = "ca8dfd1a173826d193e3b955e07c22765829890f62c677a59c4a410cb4f47c01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen",
|
"bindgen 0.70.1",
|
||||||
"libloading 0.8.5",
|
"libloading 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -8689,7 +8716,7 @@ version = "130.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a511192602f7b435b0a241c1947aa743eb7717f20a9195f4b5e8ed1952e01db1"
|
checksum = "a511192602f7b435b0a241c1947aa743eb7717f20a9195f4b5e8ed1952e01db1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen",
|
"bindgen 0.70.1",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"fslock",
|
"fslock",
|
||||||
"gzip-header",
|
"gzip-header",
|
||||||
|
|
|
@ -184,7 +184,7 @@ rand = "=0.8.5"
|
||||||
regex = "^1.7.0"
|
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 = { 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"
|
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 = { version = "0.23.11", default-features = false, features = ["logging", "std", "tls12", "ring"] }
|
||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
rustls-tokio-stream = "=0.3.0"
|
rustls-tokio-stream = "=0.3.0"
|
||||||
|
|
|
@ -443,6 +443,7 @@ deno_core::extension!(deno_node,
|
||||||
objects = [
|
objects = [
|
||||||
ops::perf_hooks::EldHistogram,
|
ops::perf_hooks::EldHistogram,
|
||||||
ops::sqlite::DatabaseSync,
|
ops::sqlite::DatabaseSync,
|
||||||
|
ops::sqlite::Session,
|
||||||
ops::sqlite::StatementSync
|
ops::sqlite::StatementSync
|
||||||
],
|
],
|
||||||
esm_entry_point = "ext:deno_node/02_init.js",
|
esm_entry_point = "ext:deno_node/02_init.js",
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::ptr::null;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
|
@ -10,6 +12,8 @@ use deno_core::OpState;
|
||||||
use deno_permissions::PermissionsContainer;
|
use deno_permissions::PermissionsContainer;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::session::SessionOptions;
|
||||||
|
use super::Session;
|
||||||
use super::SqliteError;
|
use super::SqliteError;
|
||||||
use super::StatementSync;
|
use super::StatementSync;
|
||||||
|
|
||||||
|
@ -192,4 +196,62 @@ impl DatabaseSync {
|
||||||
use_big_ints: Cell::new(false),
|
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.
|
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
|
mod session;
|
||||||
mod statement;
|
mod statement;
|
||||||
|
|
||||||
pub use database::DatabaseSync;
|
pub use database::DatabaseSync;
|
||||||
use deno_permissions::PermissionCheckError;
|
pub use session::Session;
|
||||||
pub use statement::StatementSync;
|
pub use statement::StatementSync;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, deno_error::JsError)]
|
#[derive(Debug, thiserror::Error, deno_error::JsError)]
|
||||||
pub enum SqliteError {
|
pub enum SqliteError {
|
||||||
#[class(inherit)]
|
#[class(inherit)]
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Permission(#[from] PermissionCheckError),
|
Permission(#[from] deno_permissions::PermissionCheckError),
|
||||||
#[class(generic)]
|
#[class(generic)]
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SqliteError(#[from] rusqlite::Error),
|
SqliteError(#[from] rusqlite::Error),
|
||||||
|
@ -40,6 +41,15 @@ pub enum SqliteError {
|
||||||
#[error("Failed to prepare statement")]
|
#[error("Failed to prepare statement")]
|
||||||
PrepareFailed,
|
PrepareFailed,
|
||||||
#[class(generic)]
|
#[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")]
|
#[error("Invalid constructor")]
|
||||||
InvalidConstructor,
|
InvalidConstructor,
|
||||||
#[class(generic)]
|
#[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");
|
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", () => {
|
Deno.test("[node/sqlite] StatementSync integer too large", () => {
|
||||||
const db = new DatabaseSync(":memory:");
|
const db = new DatabaseSync(":memory:");
|
||||||
db.exec("CREATE TABLE data(key INTEGER PRIMARY KEY);");
|
db.exec("CREATE TABLE data(key INTEGER PRIMARY KEY);");
|
||||||
|
|
Loading…
Add table
Reference in a new issue