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