diff --git a/Cargo.toml b/Cargo.toml index cb9410ce982fca..200685c91bef6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -225,7 +225,7 @@ rand = "=0.8.5" rayon = "1.8.0" regex = "^1.7.0" reqwest = { version = "=0.12.5", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli", "socks", "json", "http2"] } # pinned because of https://github.com/seanmonstar/reqwest/pull/1955 -rusqlite = { version = "0.34.0", features = ["unlock_notify", "bundled", "session", "modern_sqlite", "limits"] } # "modern_sqlite": need sqlite >= 3.49.0 for some db configs +rusqlite = { version = "0.34.0", features = ["unlock_notify", "bundled", "session", "modern_sqlite", "limits", "backup"] } # "modern_sqlite": need sqlite >= 3.49.0 for some db configs rustls = { version = "0.23.11", default-features = false, features = ["logging", "std", "tls12", "aws_lc_rs"] } rustls-pemfile = "2" rustls-tokio-stream = "=0.5.0" diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 21695686edcf15..e6e368b7e2ad37 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -506,6 +506,8 @@ deno_core::extension!(deno_node, ops::inspector::op_inspector_disconnect, ops::inspector::op_inspector_emit_protocol_event, ops::inspector::op_inspector_enabled, + + ops::sqlite::op_node_database_backup, ], objects = [ ops::perf_hooks::EldHistogram, diff --git a/ext/node/ops/sqlite/backup.rs b/ext/node/ops/sqlite/backup.rs new file mode 100644 index 00000000000000..d6272d700476bf --- /dev/null +++ b/ext/node/ops/sqlite/backup.rs @@ -0,0 +1,41 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::ffi::c_int; +use std::time; + +use deno_core::op2; +use rusqlite::backup; +use rusqlite::Connection; +use serde::Deserialize; +use serde::Serialize; + +use super::DatabaseSync; +use super::SqliteError; + +const DEFAULT_BACKUP_RATE: c_int = 5; + +#[derive(Serialize, Deserialize)] +struct BackupOptions { + source: Option, + target: Option, + rate: Option, + // progress: fn(backup::Progress), +} + +#[op2] +#[serde] +pub fn op_node_database_backup( + #[cppgc] source_db: &DatabaseSync, + #[string] path: String, + #[serde] options: Option, +) -> std::result::Result<(), SqliteError> { + let src_conn_ref = source_db.conn.borrow(); + let src_conn = src_conn_ref.as_ref().ok_or(SqliteError::SessionClosed)?; + let path = std::path::Path::new(&path); + let mut dst_conn = Connection::open(path)?; + let rate = options + .and_then(|opts| opts.rate) + .unwrap_or(DEFAULT_BACKUP_RATE); + let backup = backup::Backup::new(src_conn, &mut dst_conn)?; + Ok(backup.run_to_completion(rate, time::Duration::from_millis(250), None)?) +} diff --git a/ext/node/ops/sqlite/database.rs b/ext/node/ops/sqlite/database.rs index 4a693788987f1d..79dfc6880afa1a 100644 --- a/ext/node/ops/sqlite/database.rs +++ b/ext/node/ops/sqlite/database.rs @@ -229,7 +229,7 @@ impl<'a> ApplyChangesetOptions<'a> { } pub struct DatabaseSync { - conn: Rc>>, + pub conn: Rc>>, statements: Rc>>, options: DatabaseSyncOptions, location: String, diff --git a/ext/node/ops/sqlite/mod.rs b/ext/node/ops/sqlite/mod.rs index 6ebe61ecace0d1..e2f85036c304a1 100644 --- a/ext/node/ops/sqlite/mod.rs +++ b/ext/node/ops/sqlite/mod.rs @@ -1,10 +1,12 @@ // Copyright 2018-2025 the Deno authors. MIT license. +mod backup; mod database; mod session; mod statement; mod validators; +pub use backup::op_node_database_backup; pub use database::DatabaseSync; pub use session::Session; pub use statement::StatementSync; diff --git a/ext/node/polyfills/sqlite.ts b/ext/node/polyfills/sqlite.ts index 0669ecc1eb3977..a0a526c87929e4 100644 --- a/ext/node/polyfills/sqlite.ts +++ b/ext/node/polyfills/sqlite.ts @@ -1,6 +1,80 @@ // Copyright 2018-2025 the Deno authors. MIT license. -import { DatabaseSync, StatementSync } from "ext:core/ops"; +import { + DatabaseSync, + op_node_database_backup, + StatementSync, +} from "ext:core/ops"; + +import { Buffer } from "node:buffer"; + +interface BackupOptions { + /** + * Name of the source database. This can be `'main'` (the default primary database) or any other + * database that have been added with [`ATTACH DATABASE`](https://www.sqlite.org/lang_attach.html) + * @default 'main' + */ + source?: string | undefined; + /** + * Name of the target database. This can be `'main'` (the default primary database) or any other + * database that have been added with [`ATTACH DATABASE`](https://www.sqlite.org/lang_attach.html) + * @default 'main' + */ + target?: string | undefined; + /** + * Number of pages to be transmitted in each batch of the backup. + * @default 100 + */ + rate?: number | undefined; + /** + * Callback function that will be called with the number of pages copied and the total number of + * pages. + */ + progress?: ((progressInfo: BackupProgressInfo) => void) | undefined; +} + +interface BackupProgressInfo { + totalPages: number; + remainingPages: number; +} + +/** + * This method makes a database backup. This method abstracts the + * [`sqlite3_backup_init()`](https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit), + * [`sqlite3_backup_step()`](https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep) + * and [`sqlite3_backup_finish()`](https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish) functions. + * + * The backed-up database can be used normally during the backup process. Mutations coming from the same connection - same + * `DatabaseSync` - object will be reflected in the backup right away. However, mutations from other connections will cause + * the backup process to restart. + * + * ```js + * import { backup, DatabaseSync } from 'node:sqlite'; + * + * const sourceDb = new DatabaseSync('source.db'); + * const totalPagesTransferred = await backup(sourceDb, 'backup.db', { + * rate: 1, // Copy one page at a time. + * progress: ({ totalPages, remainingPages }) => { + * console.log('Backup in progress', { totalPages, remainingPages }); + * }, + * }); + * + * console.log('Backup completed', totalPagesTransferred); + * ``` + * @param sourceDb The database to backup. The source database must be open. + * @param path The path where the backup will be created. If the file already exists, + * the contents will be overwritten. + * @param options Optional configuration for the backup. The + * following properties are supported: + * @returns A promise that resolves when the backup is completed and rejects if an error occurs. + */ +function backup( + sourceDb: DatabaseSync, + path: string | Buffer | URL, + options?: BackupOptions, +) { + op_node_database_backup(sourceDb, path, options); +} export const constants = { SQLITE_CHANGESET_OMIT: 0, @@ -14,9 +88,10 @@ export const constants = { SQLITE_CHANGESET_FOREIGN_KEY: 5, }; -export { DatabaseSync, StatementSync }; +export { backup, DatabaseSync, StatementSync }; export default { + backup, constants, DatabaseSync, StatementSync,