Skip to main content

deno_node_sqlite/
session.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::cell::Cell;
4use std::cell::RefCell;
5use std::ffi::c_void;
6use std::rc::Rc;
7
8use deno_core::FromV8;
9use deno_core::GarbageCollected;
10use deno_core::op2;
11use deno_core::v8;
12use deno_core::v8_static_strings;
13use rusqlite::ffi;
14
15use super::SqliteError;
16use super::validators;
17
18#[derive(Default)]
19pub struct SessionOptions {
20  pub table: Option<String>,
21  pub db: Option<String>,
22}
23
24impl FromV8<'_> for SessionOptions {
25  type Error = validators::Error;
26
27  fn from_v8(
28    scope: &mut v8::PinScope<'_, '_>,
29    value: v8::Local<v8::Value>,
30  ) -> Result<Self, validators::Error> {
31    use validators::Error;
32
33    if value.is_undefined() {
34      return Ok(SessionOptions::default());
35    }
36
37    let obj = v8::Local::<v8::Object>::try_from(value).map_err(|_| {
38      Error::InvalidArgType("The \"options\" argument must be an object.")
39    })?;
40
41    let mut options = SessionOptions::default();
42
43    v8_static_strings! {
44      TABLE_STRING = "table",
45      DB_STRING = "db",
46    }
47
48    let table_string = TABLE_STRING.v8_string(scope).unwrap();
49    if let Some(table_value) = obj.get(scope, table_string.into())
50      && !table_value.is_undefined()
51    {
52      if !table_value.is_string() {
53        return Err(Error::InvalidArgType(
54          "The \"options.table\" argument must be a string.",
55        ));
56      }
57      let table =
58        v8::Local::<v8::String>::try_from(table_value).map_err(|_| {
59          Error::InvalidArgType(
60            "The \"options.table\" argument must be a string.",
61          )
62        })?;
63      options.table = Some(table.to_rust_string_lossy(scope).to_string());
64    }
65
66    let db_string = DB_STRING.v8_string(scope).unwrap();
67    if let Some(db_value) = obj.get(scope, db_string.into())
68      && !db_value.is_undefined()
69    {
70      if !db_value.is_string() {
71        return Err(Error::InvalidArgType(
72          "The \"options.db\" argument must be a string.",
73        ));
74      }
75      let db = v8::Local::<v8::String>::try_from(db_value).map_err(|_| {
76        Error::InvalidArgType("The \"options.db\" argument must be a string.")
77      })?;
78      options.db = Some(db.to_rust_string_lossy(scope).to_string());
79    }
80
81    Ok(options)
82  }
83}
84
85pub struct Session {
86  pub(crate) inner: *mut ffi::sqlite3_session,
87  pub(crate) freed: Cell<bool>,
88
89  // Hold a weak reference to the database.
90  pub(crate) db: Rc<RefCell<Option<rusqlite::Connection>>>,
91}
92
93// SAFETY: we're sure this can be GCed
94unsafe impl GarbageCollected for Session {
95  fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}
96
97  fn get_name(&self) -> &'static std::ffi::CStr {
98    c"Session"
99  }
100}
101
102impl Drop for Session {
103  fn drop(&mut self) {
104    let _ = self.delete();
105  }
106}
107
108impl Session {
109  fn delete(&self) -> Result<(), SqliteError> {
110    if self.freed.get() {
111      return Err(SqliteError::SessionClosed);
112    }
113
114    self.freed.set(true);
115    if self.db.borrow().is_none() {
116      return Ok(());
117    }
118    // Safety: `self.inner` is a valid session. double free is
119    // prevented by `freed` flag.
120    unsafe {
121      ffi::sqlite3session_delete(self.inner);
122    }
123
124    Ok(())
125  }
126}
127
128#[op2]
129impl Session {
130  #[constructor]
131  #[cppgc]
132  fn create(_: bool) -> Session {
133    unreachable!()
134  }
135
136  // Closes the session.
137  #[fast]
138  #[undefined]
139  fn close(&self) -> Result<(), SqliteError> {
140    if self.db.borrow().is_none() {
141      return Err(SqliteError::AlreadyClosed);
142    }
143
144    self.delete()
145  }
146
147  // Retrieves a changeset containing all changes since the changeset
148  // was created. Can be called multiple times.
149  //
150  // This method is a wrapper around `sqlite3session_changeset()`.
151  #[buffer]
152  fn changeset(&self) -> Result<Box<[u8]>, SqliteError> {
153    if self.db.borrow().is_none() {
154      return Err(SqliteError::AlreadyClosed);
155    }
156    if self.freed.get() {
157      return Err(SqliteError::SessionClosed);
158    }
159
160    session_buffer_op(self.inner, ffi::sqlite3session_changeset)
161  }
162
163  // Similar to the method above, but generates a more compact patchset.
164  //
165  // This method is a wrapper around `sqlite3session_patchset()`.
166  #[buffer]
167  fn patchset(&self) -> Result<Box<[u8]>, SqliteError> {
168    if self.db.borrow().is_none() {
169      return Err(SqliteError::AlreadyClosed);
170    }
171    if self.freed.get() {
172      return Err(SqliteError::SessionClosed);
173    }
174
175    session_buffer_op(self.inner, ffi::sqlite3session_patchset)
176  }
177}
178
179fn session_buffer_op(
180  s: *mut ffi::sqlite3_session,
181  f: unsafe extern "C" fn(
182    *mut ffi::sqlite3_session,
183    *mut i32,
184    *mut *mut c_void,
185  ) -> i32,
186) -> Result<Box<[u8]>, SqliteError> {
187  let mut n_buffer = 0;
188  let mut p_buffer = std::ptr::null_mut();
189
190  // Safety: `s` is a valid session and the buffer is allocated
191  // by sqlite3 and will be freed later.
192  let r = unsafe { f(s, &mut n_buffer, &mut p_buffer) };
193  if r != ffi::SQLITE_OK {
194    return Err(SqliteError::SessionChangesetFailed);
195  }
196
197  if n_buffer == 0 {
198    return Ok(Default::default());
199  }
200
201  // Safety: n_buffer is the size of the buffer.
202  let buffer = unsafe {
203    std::slice::from_raw_parts(p_buffer as *const u8, n_buffer as usize)
204  }
205  .to_vec()
206  .into_boxed_slice();
207
208  // Safety: free sqlite allocated buffer, we copied it into the JS buffer.
209  unsafe {
210    ffi::sqlite3_free(p_buffer);
211  }
212
213  Ok(buffer)
214}