Files
Nathan Whitaker 98f9507aff fix(ext/node_sqlite): invalidate sessions on database close (#36633)
## Summary

- track sessions created by `DatabaseSync`
- close and invalidate active sessions before closing the database
connection
- keep sessions from an earlier connection closed after the database is
reopened

## Motivation

Session objects checked whether the database's shared connection state
was populated. Reopening the database populated that state again, so
sessions created for the previous connection could be treated as open.

Tying each session handle to the database's active-session list keeps
its lifecycle associated with the connection that created it.

## Testing

- `cargo check -p deno --lib`
- `cargo test unit_node::sqlite_test`
- `./tools/format.js ext/node_sqlite/database.rs
ext/node_sqlite/session.rs tests/unit_node/sqlite_test.ts`
- `./tools/lint.js`
2026-08-21 14:20:50 -07:00

220 lines
5.5 KiB
Rust

// Copyright 2018-2026 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::FromV8;
use deno_core::GarbageCollected;
use deno_core::op2;
use deno_core::v8;
use deno_core::v8_static_strings;
use rusqlite::ffi;
use super::SqliteError;
use super::validators;
#[derive(Default)]
pub struct SessionOptions {
pub table: Option<String>,
pub db: Option<String>,
}
impl FromV8<'_> for SessionOptions {
type Error = validators::Error;
fn from_v8(
scope: &mut v8::PinScope<'_, '_>,
value: v8::Local<v8::Value>,
) -> Result<Self, validators::Error> {
use validators::Error;
if value.is_undefined() {
return Ok(SessionOptions::default());
}
let obj = v8::Local::<v8::Object>::try_from(value).map_err(|_| {
Error::InvalidArgType(
"The \"options\" argument must be an object.".into(),
)
})?;
let mut options = SessionOptions::default();
v8_static_strings! {
TABLE_STRING = "table",
DB_STRING = "db",
}
let table_string = TABLE_STRING.v8_string(scope).unwrap();
if let Some(table_value) = obj.get(scope, table_string.into())
&& !table_value.is_undefined()
{
if !table_value.is_string() {
return Err(Error::InvalidArgType(
"The \"options.table\" argument must be a string.".into(),
));
}
let table =
v8::Local::<v8::String>::try_from(table_value).map_err(|_| {
Error::InvalidArgType(
"The \"options.table\" argument must be a string.".into(),
)
})?;
options.table = Some(table.to_rust_string_lossy(scope).to_string());
}
let db_string = DB_STRING.v8_string(scope).unwrap();
if let Some(db_value) = obj.get(scope, db_string.into())
&& !db_value.is_undefined()
{
if !db_value.is_string() {
return Err(Error::InvalidArgType(
"The \"options.db\" argument must be a string.".into(),
));
}
let db = v8::Local::<v8::String>::try_from(db_value).map_err(|_| {
Error::InvalidArgType(
"The \"options.db\" argument must be a string.".into(),
)
})?;
options.db = Some(db.to_rust_string_lossy(scope).to_string());
}
Ok(options)
}
}
pub struct Session {
pub(crate) inner: InnerSessionPtr,
// Hold a weak reference to the database.
pub(crate) db: Rc<RefCell<Option<rusqlite::Connection>>>,
pub(crate) sessions: Rc<RefCell<Vec<InnerSessionPtr>>>,
}
pub(crate) type InnerSessionPtr = Rc<Cell<Option<*mut ffi::sqlite3_session>>>;
// SAFETY: we're sure this can be GCed
unsafe impl GarbageCollected for Session {
fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}
fn get_name(&self) -> &'static std::ffi::CStr {
c"Session"
}
}
impl Drop for Session {
fn drop(&mut self) {
let _ = self.delete();
}
}
impl Session {
fn delete(&self) -> Result<(), SqliteError> {
let ptr = self.inner.take().ok_or(SqliteError::SessionClosed)?;
let mut sessions = self.sessions.borrow_mut();
if let Some(pos) = sessions
.iter()
.position(|session| Rc::ptr_eq(session, &self.inner))
{
sessions.remove(pos);
}
if self.db.borrow().is_none() {
return Ok(());
}
// Safety: `ptr` is a valid session. Double free is prevented by taking
// the pointer from `inner` before deleting it.
unsafe {
ffi::sqlite3session_delete(ptr);
}
Ok(())
}
}
#[op2]
impl Session {
#[constructor]
#[cppgc]
fn create(_: bool) -> Session {
unreachable!()
}
// Closes the session.
#[fast]
#[undefined]
fn close(&self) -> Result<(), SqliteError> {
if self.db.borrow().is_none() {
return Err(SqliteError::AlreadyClosed);
}
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.db.borrow().is_none() {
return Err(SqliteError::AlreadyClosed);
}
let ptr = self.inner.get().ok_or(SqliteError::SessionClosed)?;
session_buffer_op(ptr, 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.db.borrow().is_none() {
return Err(SqliteError::AlreadyClosed);
}
let ptr = self.inner.get().ok_or(SqliteError::SessionClosed)?;
session_buffer_op(ptr, 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)
}