Skip to content

Commit ece384c

Browse files
authored
fix(ext/node): implement DatabaseSync#applyChangeset() (#27967)
https://nodejs.org/api/sqlite.html#databaseapplychangesetchangeset-options ```js const sourceDb = new DatabaseSync(':memory:'); const targetDb = new DatabaseSync(':memory:'); sourceDb.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); targetDb.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); const session = sourceDb.createSession(); const insert = sourceDb.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); insert.run(1, 'hello'); insert.run(2, 'world'); const changeset = session.changeset(); targetDb.applyChangeset(changeset); // Now that the changeset has been applied, targetDb contains the same data as sourceDb. ```
1 parent bc85548 commit ece384c

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

ext/node/ops/sqlite/database.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
use std::cell::Cell;
44
use std::cell::RefCell;
5+
use std::ffi::c_char;
6+
use std::ffi::c_void;
7+
use std::ffi::CStr;
58
use std::ffi::CString;
69
use std::ptr::null;
710
use std::rc::Rc;
811

912
use deno_core::op2;
13+
use deno_core::serde_v8;
14+
use deno_core::v8;
1015
use deno_core::GarbageCollected;
1116
use deno_core::OpState;
1217
use deno_permissions::PermissionsContainer;
@@ -41,6 +46,13 @@ impl Default for DatabaseSyncOptions {
4146
}
4247
}
4348

49+
#[derive(Deserialize)]
50+
#[serde(rename_all = "camelCase")]
51+
struct ApplyChangesetOptions<'a> {
52+
filter: Option<serde_v8::Value<'a>>,
53+
on_conflict: Option<serde_v8::Value<'a>>,
54+
}
55+
4456
pub struct DatabaseSync {
4557
conn: Rc<RefCell<Option<rusqlite::Connection>>>,
4658
options: DatabaseSyncOptions,
@@ -197,6 +209,119 @@ impl DatabaseSync {
197209
})
198210
}
199211

212+
// Applies a changeset to the database.
213+
//
214+
// This method is a wrapper around `sqlite3changeset_apply()`.
215+
#[reentrant]
216+
fn apply_changeset<'a>(
217+
&self,
218+
scope: &mut v8::HandleScope<'a>,
219+
#[buffer] changeset: &[u8],
220+
#[serde] options: Option<ApplyChangesetOptions<'a>>,
221+
) -> Result<bool, SqliteError> {
222+
struct HandlerCtx<'a, 'b> {
223+
scope: &'a mut v8::HandleScope<'b>,
224+
confict: Option<v8::Local<'b, v8::Function>>,
225+
filter: Option<v8::Local<'b, v8::Function>>,
226+
}
227+
228+
// Conflict handler callback for `sqlite3changeset_apply()`.
229+
unsafe extern "C" fn conflict_handler(
230+
p_ctx: *mut c_void,
231+
e_conflict: i32,
232+
_: *mut libsqlite3_sys::sqlite3_changeset_iter,
233+
) -> i32 {
234+
let ctx = &mut *(p_ctx as *mut HandlerCtx);
235+
236+
if let Some(conflict) = &mut ctx.confict {
237+
let recv = v8::undefined(ctx.scope).into();
238+
let args = [v8::Integer::new(ctx.scope, e_conflict).into()];
239+
240+
let ret = conflict.call(ctx.scope, recv, &args).unwrap();
241+
return ret
242+
.int32_value(ctx.scope)
243+
.unwrap_or(libsqlite3_sys::SQLITE_CHANGESET_ABORT);
244+
}
245+
246+
libsqlite3_sys::SQLITE_CHANGESET_ABORT
247+
}
248+
249+
// Filter handler callback for `sqlite3changeset_apply()`.
250+
unsafe extern "C" fn filter_handler(
251+
p_ctx: *mut c_void,
252+
z_tab: *const c_char,
253+
) -> i32 {
254+
let ctx = &mut *(p_ctx as *mut HandlerCtx);
255+
256+
if let Some(filter) = &mut ctx.filter {
257+
let tab = CStr::from_ptr(z_tab).to_str().unwrap();
258+
259+
let recv = v8::undefined(ctx.scope).into();
260+
let args = [v8::String::new(ctx.scope, tab).unwrap().into()];
261+
262+
let ret = filter.call(ctx.scope, recv, &args).unwrap();
263+
return ret.boolean_value(ctx.scope) as i32;
264+
}
265+
266+
1
267+
}
268+
269+
let db = self.conn.borrow();
270+
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
271+
272+
// It is safe to use scope in the handlers because they are never
273+
// called after the call to `sqlite3changeset_apply()`.
274+
let mut ctx = HandlerCtx {
275+
scope,
276+
confict: None,
277+
filter: None,
278+
};
279+
280+
if let Some(options) = options {
281+
if let Some(filter) = options.filter {
282+
let filter_cb: v8::Local<v8::Function> = filter
283+
.v8_value
284+
.try_into()
285+
.map_err(|_| SqliteError::InvalidCallback("filter"))?;
286+
ctx.filter = Some(filter_cb);
287+
}
288+
289+
if let Some(on_conflict) = options.on_conflict {
290+
let on_conflict_cb: v8::Local<v8::Function> = on_conflict
291+
.v8_value
292+
.try_into()
293+
.map_err(|_| SqliteError::InvalidCallback("onConflict"))?;
294+
ctx.confict = Some(on_conflict_cb);
295+
}
296+
}
297+
298+
// SAFETY: lifetime of the connection is guaranteed by reference
299+
// counting.
300+
let raw_handle = unsafe { db.handle() };
301+
302+
// SAFETY: `changeset` points to a valid memory location and its
303+
// length is correct. `ctx` is stack allocated and its lifetime is
304+
// longer than the call to `sqlite3changeset_apply()`.
305+
unsafe {
306+
let r = libsqlite3_sys::sqlite3changeset_apply(
307+
raw_handle,
308+
changeset.len() as i32,
309+
changeset.as_ptr() as *mut _,
310+
Some(filter_handler),
311+
Some(conflict_handler),
312+
&mut ctx as *mut _ as *mut c_void,
313+
);
314+
315+
if r == libsqlite3_sys::SQLITE_OK {
316+
return Ok(true);
317+
} else if r == libsqlite3_sys::SQLITE_ABORT {
318+
return Ok(false);
319+
}
320+
321+
Err(SqliteError::ChangesetApplyFailed)
322+
}
323+
}
324+
200325
// Creates and attaches a session to the database.
201326
//
202327
// This method is a wrapper around `sqlite3session_create()` and

ext/node/ops/sqlite/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,10 @@ pub enum SqliteError {
5858
#[class(range)]
5959
#[error("The value of column {0} is too large to be represented as a JavaScript number: {1}")]
6060
NumberTooLarge(i32, i64),
61+
#[class(generic)]
62+
#[error("Failed to apply changeset")]
63+
ChangesetApplyFailed,
64+
#[class(type)]
65+
#[error("Invalid callback: {0}")]
66+
InvalidCallback(&'static str),
6167
}

tests/unit_node/sqlite_test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,32 @@ Deno.test({
152152
}
153153
},
154154
});
155+
156+
Deno.test("[node/sqlite] applyChangeset across databases", () => {
157+
const sourceDb = new DatabaseSync(":memory:");
158+
const targetDb = new DatabaseSync(":memory:");
159+
160+
sourceDb.exec("CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)");
161+
targetDb.exec("CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)");
162+
163+
const session = sourceDb.createSession();
164+
165+
const insert = sourceDb.prepare(
166+
"INSERT INTO data (key, value) VALUES (?, ?)",
167+
);
168+
insert.run(1, "hello");
169+
insert.run(2, "world");
170+
171+
const changeset = session.changeset();
172+
targetDb.applyChangeset(changeset, {
173+
filter(e) {
174+
return e === "data";
175+
},
176+
});
177+
178+
const stmt = targetDb.prepare("SELECT * FROM data");
179+
assertEquals(stmt.all(), [
180+
{ key: 1, value: "hello", __proto__: null },
181+
{ key: 2, value: "world", __proto__: null },
182+
]);
183+
});

0 commit comments

Comments
 (0)