mirror of
https://github.com/aphrody-labs/deno.git
synced 2026-10-14 06:19:28 +00:00
Allows https://github.com/nodejs/node/blob/v25.8.1/test/parallel/test-sqlite-limits.js test to pass. Most implementation logics were written by Claude --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
218 lines
6.3 KiB
Rust
218 lines
6.3 KiB
Rust
// Copyright 2018-2026 the Deno authors. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::cell::RefCell;
|
|
use std::ffi::c_int;
|
|
use std::rc::Rc;
|
|
|
|
use deno_core::OpState;
|
|
use deno_core::op2;
|
|
use deno_core::v8;
|
|
use deno_core::v8_static_strings;
|
|
use deno_permissions::OpenAccessKind;
|
|
use deno_permissions::PermissionsContainer;
|
|
use rusqlite::Connection;
|
|
use rusqlite::backup;
|
|
|
|
use super::DatabaseSync;
|
|
use super::SqliteError;
|
|
use super::validators;
|
|
|
|
const DEFAULT_BACKUP_RATE: c_int = 100;
|
|
|
|
struct BackupOptions<'a> {
|
|
source: String,
|
|
target: String,
|
|
rate: i32,
|
|
progress: Option<v8::Local<'a, v8::Function>>,
|
|
}
|
|
|
|
impl<'a> Default for BackupOptions<'a> {
|
|
fn default() -> Self {
|
|
BackupOptions {
|
|
source: "main".to_string(),
|
|
target: "main".to_string(),
|
|
rate: DEFAULT_BACKUP_RATE,
|
|
progress: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> BackupOptions<'a> {
|
|
fn from_value(
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
value: v8::Local<'a, v8::Value>,
|
|
) -> Result<Self, validators::Error> {
|
|
let mut options = BackupOptions::default();
|
|
|
|
if value.is_undefined() {
|
|
return Ok(options);
|
|
}
|
|
|
|
let Ok(obj) = v8::Local::<v8::Object>::try_from(value) else {
|
|
return Err(validators::Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
));
|
|
};
|
|
|
|
v8_static_strings! {
|
|
SOURCE_STRING = "source",
|
|
TARGET_STRING = "target",
|
|
RATE_STRING = "rate",
|
|
PROGRESS_STRING = "progress",
|
|
}
|
|
|
|
let source_string = SOURCE_STRING.v8_string(scope).unwrap();
|
|
if let Some(source_val) = obj.get(scope, source_string.into())
|
|
&& !source_val.is_undefined()
|
|
{
|
|
let source_str =
|
|
v8::Local::<v8::String>::try_from(source_val).map_err(|_| {
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.source\" argument must be a string.".into(),
|
|
)
|
|
})?;
|
|
options.source = source_str.to_rust_string_lossy(scope);
|
|
}
|
|
|
|
let target_string = TARGET_STRING.v8_string(scope).unwrap();
|
|
if let Some(target_val) = obj.get(scope, target_string.into())
|
|
&& !target_val.is_undefined()
|
|
{
|
|
let target_str =
|
|
v8::Local::<v8::String>::try_from(target_val).map_err(|_| {
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.target\" argument must be a string.".into(),
|
|
)
|
|
})?;
|
|
options.target = target_str.to_rust_string_lossy(scope);
|
|
}
|
|
|
|
let rate_string = RATE_STRING.v8_string(scope).unwrap();
|
|
if let Some(rate_val) = obj.get(scope, rate_string.into())
|
|
&& !rate_val.is_undefined()
|
|
{
|
|
let rate_int = v8::Local::<v8::Integer>::try_from(rate_val)
|
|
.map_err(|_| {
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.rate\" argument must be an integer.".into(),
|
|
)
|
|
})?
|
|
.value();
|
|
|
|
options.rate = i32::try_from(rate_int).map_err(|_| {
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.rate\" argument must be an integer.".into(),
|
|
)
|
|
})?;
|
|
}
|
|
|
|
let progress_string = PROGRESS_STRING.v8_string(scope).unwrap();
|
|
if let Some(progress_val) = obj.get(scope, progress_string.into())
|
|
&& !progress_val.is_undefined()
|
|
{
|
|
let progress_fn = v8::Local::<v8::Function>::try_from(progress_val)
|
|
.map_err(|_| {
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.progress\" argument must be a function.".into(),
|
|
)
|
|
})?;
|
|
options.progress = Some(progress_fn);
|
|
}
|
|
|
|
Ok(options)
|
|
}
|
|
}
|
|
|
|
#[op2(reentrant, stack_trace)]
|
|
#[smi]
|
|
pub fn op_node_database_backup<'a>(
|
|
state: Rc<RefCell<OpState>>,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
#[cppgc] source_db: &DatabaseSync,
|
|
#[string] path: &str,
|
|
options: v8::Local<'a, v8::Value>,
|
|
) -> Result<Option<i32>, SqliteError> {
|
|
use rusqlite::backup::StepResult::Busy;
|
|
use rusqlite::backup::StepResult::Done;
|
|
use rusqlite::backup::StepResult::Locked;
|
|
use rusqlite::backup::StepResult::More;
|
|
|
|
let options = BackupOptions::from_value(scope, options)?;
|
|
let src_conn_ref = source_db.conn.borrow();
|
|
let src_conn = src_conn_ref.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
let checked_path = {
|
|
let mut state = state.borrow_mut();
|
|
let permissions = state.borrow_mut::<PermissionsContainer>();
|
|
permissions.check_open(
|
|
Cow::Borrowed(std::path::Path::new(path)),
|
|
OpenAccessKind::Write,
|
|
Some("node:sqlite.backup"),
|
|
)?
|
|
};
|
|
|
|
let mut dst_conn = Connection::open(checked_path).map_err(|e| match e {
|
|
rusqlite::Error::SqliteFailure(err, Some(msg)) => {
|
|
let message = if err.extended_code == rusqlite::ffi::SQLITE_CANTOPEN {
|
|
"unable to open database file".to_string()
|
|
} else {
|
|
msg
|
|
};
|
|
SqliteError::SqliteSysError {
|
|
message: message.clone(),
|
|
errstr: message,
|
|
errcode: err.extended_code as _,
|
|
}
|
|
}
|
|
other_err => SqliteError::from(other_err),
|
|
})?;
|
|
let backup = backup::Backup::new_with_names(
|
|
src_conn,
|
|
options.source.as_str(),
|
|
&mut dst_conn,
|
|
options.target.as_str(),
|
|
)?;
|
|
|
|
v8_static_strings! {
|
|
TOTAL_PAGES_STRING = "totalPages",
|
|
REMAINING_PAGES_STRING = "remainingPages",
|
|
}
|
|
|
|
loop {
|
|
let step_result = backup.step(options.rate)?;
|
|
if let Some(progress_fn) = options.progress {
|
|
let progress = backup.progress();
|
|
if progress.remaining != 0 {
|
|
let js_progress_obj = v8::Object::new(scope);
|
|
let total_pages_string = TOTAL_PAGES_STRING.v8_string(scope).unwrap();
|
|
let remaining_pages_string =
|
|
REMAINING_PAGES_STRING.v8_string(scope).unwrap();
|
|
|
|
let total_pages_js = v8::Integer::new(scope, progress.pagecount);
|
|
let remaining_pages_js = v8::Integer::new(scope, progress.remaining);
|
|
js_progress_obj.set(
|
|
scope,
|
|
total_pages_string.into(),
|
|
total_pages_js.into(),
|
|
);
|
|
js_progress_obj.set(
|
|
scope,
|
|
remaining_pages_string.into(),
|
|
remaining_pages_js.into(),
|
|
);
|
|
|
|
let recv = v8::null(scope).into();
|
|
let res = progress_fn.call(scope, recv, &[js_progress_obj.into()]);
|
|
if res.is_none() {
|
|
// JS exception occurred in progress callback
|
|
return Ok(None);
|
|
}
|
|
}
|
|
}
|
|
match step_result {
|
|
Done => return Ok(Some(backup.progress().pagecount)),
|
|
More | Busy | Locked | _ => continue,
|
|
}
|
|
}
|
|
}
|