story-kit: merge 260_refactor_upgrade_libsqlite3_sys
This commit is contained in:
Vendored
+442
@@ -0,0 +1,442 @@
|
||||
//! Online SQLite backup API.
|
||||
//!
|
||||
//! Alternatively, you can create a backup with a simple
|
||||
//! [`VACUUM INTO <backup_path>`](https://sqlite.org/lang_vacuum.html#vacuuminto).
|
||||
//!
|
||||
//! To create a [`Backup`], you must have two distinct [`Connection`]s - one
|
||||
//! for the source (which can be used while the backup is running) and one for
|
||||
//! the destination (which cannot). A [`Backup`] handle exposes three methods:
|
||||
//! [`step`](Backup::step) will attempt to back up a specified number of pages,
|
||||
//! [`progress`](Backup::progress) gets the current progress of the backup as of
|
||||
//! the last call to [`step`](Backup::step), and
|
||||
//! [`run_to_completion`](Backup::run_to_completion) will attempt to back up the
|
||||
//! entire source database, allowing you to specify how many pages are backed up
|
||||
//! at a time and how long the thread should sleep between chunks of pages.
|
||||
//!
|
||||
//! The following example is equivalent to "Example 2: Online Backup of a
|
||||
//! Running Database" from [SQLite's Online Backup API
|
||||
//! documentation](https://www.sqlite.org/backup.html).
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use rusqlite::{backup, Connection, Result};
|
||||
//! # use std::path::Path;
|
||||
//! # use std::time;
|
||||
//!
|
||||
//! fn backup_db<P: AsRef<Path>>(
|
||||
//! src: &Connection,
|
||||
//! dst: P,
|
||||
//! progress: fn(backup::Progress),
|
||||
//! ) -> Result<()> {
|
||||
//! let mut dst = Connection::open(dst)?;
|
||||
//! let backup = backup::Backup::new(src, &mut dst)?;
|
||||
//! backup.run_to_completion(5, time::Duration::from_millis(250), Some(progress))
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
|
||||
use std::ffi::c_int;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::ffi;
|
||||
|
||||
use crate::error::error_from_handle;
|
||||
use crate::{Connection, Name, Result, MAIN_DB};
|
||||
|
||||
impl Connection {
|
||||
/// Back up the `name` database to the given
|
||||
/// destination path.
|
||||
///
|
||||
/// If `progress` is not `None`, it will be called periodically
|
||||
/// until the backup completes.
|
||||
///
|
||||
/// For more fine-grained control over the backup process (e.g.,
|
||||
/// to sleep periodically during the backup or to back up to an
|
||||
/// already-open database connection), see the `backup` module.
|
||||
///
|
||||
/// # Failure
|
||||
///
|
||||
/// Will return `Err` if the destination path cannot be opened
|
||||
/// or if the backup fails.
|
||||
pub fn backup<N: Name, P: AsRef<Path>>(
|
||||
&self,
|
||||
name: N,
|
||||
dst_path: P,
|
||||
progress: Option<fn(Progress)>,
|
||||
) -> Result<()> {
|
||||
use self::StepResult::{Busy, Done, Locked, More};
|
||||
let mut dst = Self::open(dst_path)?;
|
||||
let backup = Backup::new_with_names(self, name, &mut dst, MAIN_DB)?;
|
||||
|
||||
let mut r = More;
|
||||
while r == More {
|
||||
r = backup.step(100)?;
|
||||
if let Some(f) = progress {
|
||||
f(backup.progress());
|
||||
}
|
||||
}
|
||||
|
||||
match r {
|
||||
Done => Ok(()),
|
||||
Busy => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_BUSY) }),
|
||||
Locked => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_LOCKED) }),
|
||||
More => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore the given source path into the
|
||||
/// `name` database. If `progress` is not `None`, it will be
|
||||
/// called periodically until the restore completes.
|
||||
///
|
||||
/// For more fine-grained control over the restore process (e.g.,
|
||||
/// to sleep periodically during the restore or to restore from an
|
||||
/// already-open database connection), see the `backup` module.
|
||||
///
|
||||
/// # Failure
|
||||
///
|
||||
/// Will return `Err` if the destination path cannot be opened
|
||||
/// or if the restore fails.
|
||||
pub fn restore<N: Name, P: AsRef<Path>, F: Fn(Progress)>(
|
||||
&mut self,
|
||||
name: N,
|
||||
src_path: P,
|
||||
progress: Option<F>,
|
||||
) -> Result<()> {
|
||||
use self::StepResult::{Busy, Done, Locked, More};
|
||||
let src = Self::open(src_path)?;
|
||||
let restore = Backup::new_with_names(&src, MAIN_DB, self, name)?;
|
||||
|
||||
let mut r = More;
|
||||
let mut busy_count = 0_i32;
|
||||
'restore_loop: while r == More || r == Busy {
|
||||
r = restore.step(100)?;
|
||||
if let Some(ref f) = progress {
|
||||
f(restore.progress());
|
||||
}
|
||||
if r == Busy {
|
||||
busy_count += 1;
|
||||
if busy_count >= 3 {
|
||||
break 'restore_loop;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
match r {
|
||||
Done => Ok(()),
|
||||
Busy => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_BUSY) }),
|
||||
Locked => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_LOCKED) }),
|
||||
More => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible successful results of calling
|
||||
/// [`Backup::step`].
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum StepResult {
|
||||
/// The backup is complete.
|
||||
Done,
|
||||
|
||||
/// The step was successful but there are still more pages that need to be
|
||||
/// backed up.
|
||||
More,
|
||||
|
||||
/// The step failed because appropriate locks could not be acquired. This is
|
||||
/// not a fatal error - the step can be retried.
|
||||
Busy,
|
||||
|
||||
/// The step failed because the source connection was writing to the
|
||||
/// database. This is not a fatal error - the step can be retried.
|
||||
Locked,
|
||||
}
|
||||
|
||||
/// Struct specifying the progress of a backup.
|
||||
///
|
||||
/// The percentage completion can be calculated as `(pagecount - remaining) /
|
||||
/// pagecount`. The progress of a backup is as of the last call to
|
||||
/// [`step`](Backup::step) - if the source database is modified after a call to
|
||||
/// [`step`](Backup::step), the progress value will become outdated and
|
||||
/// potentially incorrect.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Progress {
|
||||
/// Number of pages in the source database that still need to be backed up.
|
||||
pub remaining: c_int,
|
||||
/// Total number of pages in the source database.
|
||||
pub pagecount: c_int,
|
||||
}
|
||||
|
||||
/// A handle to an online backup.
|
||||
pub struct Backup<'a, 'b> {
|
||||
phantom_from: PhantomData<&'a Connection>,
|
||||
to: &'b Connection,
|
||||
b: *mut ffi::sqlite3_backup,
|
||||
}
|
||||
|
||||
impl Backup<'_, '_> {
|
||||
/// Attempt to create a new handle that will allow backups from `from` to
|
||||
/// `to`. Note that `to` is a `&mut` - this is because SQLite forbids any
|
||||
/// API calls on the destination of a backup while the backup is taking
|
||||
/// place.
|
||||
///
|
||||
/// # Failure
|
||||
///
|
||||
/// Will return `Err` if the underlying `sqlite3_backup_init` call returns
|
||||
/// `NULL`.
|
||||
#[inline]
|
||||
pub fn new<'a, 'b>(from: &'a Connection, to: &'b mut Connection) -> Result<Backup<'a, 'b>> {
|
||||
Backup::new_with_names(from, MAIN_DB, to, MAIN_DB)
|
||||
}
|
||||
|
||||
/// Attempt to create a new handle that will allow backups from the
|
||||
/// `from_name` database of `from` to the `to_name` database of `to`. Note
|
||||
/// that `to` is a `&mut` - this is because SQLite forbids any API calls on
|
||||
/// the destination of a backup while the backup is taking place.
|
||||
///
|
||||
/// # Failure
|
||||
///
|
||||
/// Will return `Err` if the underlying `sqlite3_backup_init` call returns
|
||||
/// `NULL`.
|
||||
pub fn new_with_names<'a, 'b, F: Name, T: Name>(
|
||||
from: &'a Connection,
|
||||
from_name: F,
|
||||
to: &'b mut Connection,
|
||||
to_name: T,
|
||||
) -> Result<Backup<'a, 'b>> {
|
||||
let to_name = to_name.as_cstr()?;
|
||||
let from_name = from_name.as_cstr()?;
|
||||
|
||||
let to_db = to.db.borrow_mut().db;
|
||||
|
||||
let b = unsafe {
|
||||
let b = ffi::sqlite3_backup_init(
|
||||
to_db,
|
||||
to_name.as_ptr(),
|
||||
from.db.borrow_mut().db,
|
||||
from_name.as_ptr(),
|
||||
);
|
||||
if b.is_null() {
|
||||
return Err(error_from_handle(to_db, ffi::sqlite3_errcode(to_db)));
|
||||
}
|
||||
b
|
||||
};
|
||||
|
||||
Ok(Backup {
|
||||
phantom_from: PhantomData,
|
||||
to,
|
||||
b,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the progress of the backup as of the last call to
|
||||
/// [`step`](Backup::step).
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn progress(&self) -> Progress {
|
||||
unsafe {
|
||||
Progress {
|
||||
remaining: ffi::sqlite3_backup_remaining(self.b),
|
||||
pagecount: ffi::sqlite3_backup_pagecount(self.b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to back up the given number of pages. If `num_pages` is
|
||||
/// negative, will attempt to back up all remaining pages. This will hold a
|
||||
/// lock on the source database for the duration, so it is probably not
|
||||
/// what you want for databases that are currently active (see
|
||||
/// [`run_to_completion`](Backup::run_to_completion) for a better
|
||||
/// alternative).
|
||||
///
|
||||
/// # Failure
|
||||
///
|
||||
/// Will return `Err` if the underlying `sqlite3_backup_step` call returns
|
||||
/// an error code other than `DONE`, `OK`, `BUSY`, or `LOCKED`. `BUSY` and
|
||||
/// `LOCKED` are transient errors and are therefore returned as possible
|
||||
/// `Ok` values.
|
||||
#[inline]
|
||||
pub fn step(&self, num_pages: c_int) -> Result<StepResult> {
|
||||
use self::StepResult::{Busy, Done, Locked, More};
|
||||
|
||||
let rc = unsafe { ffi::sqlite3_backup_step(self.b, num_pages) };
|
||||
match rc {
|
||||
ffi::SQLITE_DONE => Ok(Done),
|
||||
ffi::SQLITE_OK => Ok(More),
|
||||
ffi::SQLITE_BUSY => Ok(Busy),
|
||||
ffi::SQLITE_LOCKED => Ok(Locked),
|
||||
_ => self.to.decode_result(rc).map(|_| More),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to run the entire backup. Will call
|
||||
/// [`step(pages_per_step)`](Backup::step) as many times as necessary,
|
||||
/// sleeping for `pause_between_pages` between each call to give the
|
||||
/// source database time to process any pending queries. This is a
|
||||
/// direct implementation of "Example 2: Online Backup of a Running
|
||||
/// Database" from [SQLite's Online Backup API documentation](https://www.sqlite.org/backup.html).
|
||||
///
|
||||
/// If `progress` is not `None`, it will be called after each step with the
|
||||
/// current progress of the backup. Note that is possible the progress may
|
||||
/// not change if the step returns `Busy` or `Locked` even though the
|
||||
/// backup is still running.
|
||||
///
|
||||
/// # Failure
|
||||
///
|
||||
/// Will return `Err` if any of the calls to [`step`](Backup::step) return
|
||||
/// `Err`.
|
||||
pub fn run_to_completion(
|
||||
&self,
|
||||
pages_per_step: c_int,
|
||||
pause_between_pages: Duration,
|
||||
progress: Option<fn(Progress)>,
|
||||
) -> Result<()> {
|
||||
use self::StepResult::{Busy, Done, Locked, More};
|
||||
|
||||
assert!(pages_per_step > 0, "pages_per_step must be positive");
|
||||
|
||||
loop {
|
||||
let r = self.step(pages_per_step)?;
|
||||
if let Some(progress) = progress {
|
||||
progress(self.progress());
|
||||
}
|
||||
match r {
|
||||
More | Busy | Locked => thread::sleep(pause_between_pages),
|
||||
Done => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Backup<'_, '_> {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::sqlite3_backup_finish(self.b) };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
||||
use wasm_bindgen_test::wasm_bindgen_test as test;
|
||||
|
||||
use super::{Backup, Progress};
|
||||
use crate::{Connection, Result, MAIN_DB, TEMP_DB};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg_attr(
|
||||
all(target_family = "wasm", target_os = "unknown"),
|
||||
ignore = "no filesystem on this platform"
|
||||
)]
|
||||
#[test]
|
||||
fn backup_to_path() -> Result<()> {
|
||||
let src = Connection::open_in_memory()?;
|
||||
src.execute_batch("CREATE TABLE foo AS SELECT 42 AS x")?;
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let path = temp_dir.path().join("test.db3");
|
||||
|
||||
fn progress(_: Progress) {}
|
||||
|
||||
src.backup(MAIN_DB, path.as_path(), Some(progress))?;
|
||||
|
||||
let mut dst = Connection::open_in_memory()?;
|
||||
dst.restore(MAIN_DB, path, Some(progress))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backup() -> Result<()> {
|
||||
let src = Connection::open_in_memory()?;
|
||||
let sql = "BEGIN;
|
||||
CREATE TABLE foo(x INTEGER);
|
||||
INSERT INTO foo VALUES(42);
|
||||
END;";
|
||||
src.execute_batch(sql)?;
|
||||
|
||||
let mut dst = Connection::open_in_memory()?;
|
||||
|
||||
{
|
||||
let backup = Backup::new(&src, &mut dst)?;
|
||||
backup.step(-1)?;
|
||||
}
|
||||
|
||||
assert_eq!(42, dst.one_column::<i64, _>("SELECT x FROM foo", [])?);
|
||||
|
||||
src.execute_batch("INSERT INTO foo VALUES(43)")?;
|
||||
|
||||
{
|
||||
let backup = Backup::new(&src, &mut dst)?;
|
||||
backup.run_to_completion(5, Duration::from_millis(250), None)?;
|
||||
}
|
||||
|
||||
let the_answer: i64 = dst.one_column("SELECT SUM(x) FROM foo", [])?;
|
||||
assert_eq!(42 + 43, the_answer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backup_temp() -> Result<()> {
|
||||
let src = Connection::open_in_memory()?;
|
||||
let sql = "BEGIN;
|
||||
CREATE TEMPORARY TABLE foo(x INTEGER);
|
||||
INSERT INTO foo VALUES(42);
|
||||
END;";
|
||||
src.execute_batch(sql)?;
|
||||
|
||||
let mut dst = Connection::open_in_memory()?;
|
||||
|
||||
{
|
||||
let backup = Backup::new_with_names(&src, TEMP_DB, &mut dst, MAIN_DB)?;
|
||||
backup.step(-1)?;
|
||||
}
|
||||
|
||||
assert_eq!(42, dst.one_column::<i64, _>("SELECT x FROM foo", [])?);
|
||||
|
||||
src.execute_batch("INSERT INTO foo VALUES(43)")?;
|
||||
|
||||
{
|
||||
let backup = Backup::new_with_names(&src, TEMP_DB, &mut dst, MAIN_DB)?;
|
||||
backup.run_to_completion(5, Duration::from_millis(250), None)?;
|
||||
}
|
||||
|
||||
let the_answer: i64 = dst.one_column("SELECT SUM(x) FROM foo", [])?;
|
||||
assert_eq!(42 + 43, the_answer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backup_attached() -> Result<()> {
|
||||
let src = Connection::open_in_memory()?;
|
||||
let sql = "ATTACH DATABASE ':memory:' AS my_attached;
|
||||
BEGIN;
|
||||
CREATE TABLE my_attached.foo(x INTEGER);
|
||||
INSERT INTO my_attached.foo VALUES(42);
|
||||
END;";
|
||||
src.execute_batch(sql)?;
|
||||
|
||||
let mut dst = Connection::open_in_memory()?;
|
||||
|
||||
{
|
||||
let backup = Backup::new_with_names(&src, c"my_attached", &mut dst, MAIN_DB)?;
|
||||
backup.step(-1)?;
|
||||
}
|
||||
|
||||
assert_eq!(42, dst.one_column::<i64, _>("SELECT x FROM foo", [])?);
|
||||
|
||||
src.execute_batch("INSERT INTO foo VALUES(43)")?;
|
||||
|
||||
{
|
||||
let backup = Backup::new_with_names(&src, c"my_attached", &mut dst, MAIN_DB)?;
|
||||
backup.run_to_completion(5, Duration::from_millis(250), None)?;
|
||||
}
|
||||
|
||||
let the_answer: i64 = dst.one_column("SELECT SUM(x) FROM foo", [])?;
|
||||
assert_eq!(42 + 43, the_answer);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user