Merge pull request #93 from jgallagher/online-backup

Initial implementation of the online backup API.
This commit is contained in:
John Gallagher 2015-12-10 16:36:15 -05:00
commit 26a7930d73
5 changed files with 451 additions and 1 deletions

View File

@ -8,7 +8,11 @@ env:
script: script:
- cargo build - cargo build
- cargo test - cargo test
- cargo doc --no-deps - cargo test --features backup
- cargo test --features load_extension
- cargo test --features trace
- cargo test --features "backup load_extension trace"
- cargo doc --no-deps --features "backup load_extension trace"
after_success: | after_success: |
[ $TRAVIS_BRANCH = master ] && [ $TRAVIS_BRANCH = master ] &&

View File

@ -14,6 +14,7 @@ name = "rusqlite"
[features] [features]
load_extension = ["libsqlite3-sys/load_extension"] load_extension = ["libsqlite3-sys/load_extension"]
backup = []
trace = [] trace = []
[dependencies] [dependencies]

View File

@ -1,3 +1,7 @@
# Version UPCOMING (TBD)
* Adds `backup` feature that exposes SQLite's online backup API.
# Version 0.5.0 (2015-12-08) # Version 0.5.0 (2015-12-08)
* Adds `trace` feature that allows the use of SQLite's logging, tracing, and profiling hooks. * Adds `trace` feature that allows the use of SQLite's logging, tracing, and profiling hooks.

414
src/backup.rs Normal file
View File

@ -0,0 +1,414 @@
//! Online SQLite backup API.
//!
//! To create a `Backup`, you must have two distinct `SqliteConnection`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` will attempt to back up a specified number of pages, `progress` gets
//! the current progress of the backup as of the last call to `step`, and
//! `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, SqliteConnection, SqliteResult};
//! # use std::path::Path;
//! # use std::time;
//!
//! fn backupDb<P: AsRef<Path>>(src: &SqliteConnection, dst: P, progress: fn(backup::Progress))
//! -> SqliteResult<()> {
//! let mut dst = try!(SqliteConnection::open(dst));
//! let backup = try!(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 libc::c_int;
use std::thread;
use std::time::Duration;
use ffi;
use {DatabaseName, SqliteConnection, SqliteError, SqliteResult};
impl SqliteConnection {
/// 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<P: AsRef<Path>>(&self,
name: DatabaseName,
dst_path: P,
progress: Option<fn(Progress)>)
-> SqliteResult<()> {
use self::StepResult::{More, Done, Busy, Locked};
let mut dst = try!(SqliteConnection::open(dst_path));
let backup = try!(Backup::new_with_names(self, name, &mut dst, DatabaseName::Main));
let mut r = More;
while r == More {
r = try!(backup.step(100));
if let Some(f) = progress {
f(backup.progress());
}
}
match r {
Done => Ok(()),
Busy => Err(SqliteError::from_handle(ptr::null_mut(), ffi::SQLITE_BUSY)),
Locked => Err(SqliteError::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<P: AsRef<Path>>(&mut self,
name: DatabaseName,
src_path: P,
progress: Option<fn(Progress)>)
-> SqliteResult<()> {
use self::StepResult::{More, Done, Busy, Locked};
let src = try!(SqliteConnection::open(src_path));
let restore = try!(Backup::new_with_names(&src, DatabaseName::Main, self, name));
let mut r = More;
let mut busy_count = 0i32;
'restore_loop: while r == More || r == Busy {
r = try!(restore.step(100));
if let Some(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(SqliteError::from_handle(ptr::null_mut(), ffi::SQLITE_BUSY)),
Locked => Err(SqliteError::from_handle(ptr::null_mut(), ffi::SQLITE_LOCKED)),
More => unreachable!(),
}
}
}
/// Possible successful results of calling `Backup::step`.
#[derive(Copy,Clone,Debug,PartialEq,Eq)]
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 aquired. 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` - if the source database is
/// modified after a call to `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 ()>,
phantom_to: PhantomData<&'b ()>,
b: *mut ffi::sqlite3_backup,
}
impl<'a, 'b> Backup<'a, 'b> {
/// 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`.
pub fn new(from: &'a SqliteConnection,
to: &'b mut SqliteConnection)
-> SqliteResult<Backup<'a, 'b>> {
Backup::new_with_names(from, DatabaseName::Main, to, DatabaseName::Main)
}
/// 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(from: &'a SqliteConnection,
from_name: DatabaseName,
to: &'b mut SqliteConnection,
to_name: DatabaseName)
-> SqliteResult<Backup<'a, 'b>> {
let to_name = try!(to_name.to_cstring());
let from_name = try!(from_name.to_cstring());
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(SqliteError::from_handle(to_db, ffi::sqlite3_errcode(to_db)));
}
b
};
Ok(Backup {
phantom_from: PhantomData,
phantom_to: PhantomData,
b: b,
})
}
/// Gets the progress of the backup as of the last call to `step`.
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` 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.
pub fn step(&self, num_pages: c_int) -> SqliteResult<StepResult> {
use self::StepResult::{Done, More, Busy, Locked};
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),
rc => {
Err(SqliteError {
code: rc,
message: ffi::code_to_str(rc).into(),
})
}
}
}
/// Attempts to run the entire backup. Will call `step(pages_per_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` return `Err`.
pub fn run_to_completion(&self,
pages_per_step: c_int,
pause_between_pages: Duration,
progress: Option<fn(Progress)>)
-> SqliteResult<()> {
use self::StepResult::{Done, More, Busy, Locked};
assert!(pages_per_step > 0, "pages_per_step must be positive");
loop {
let r = try!(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<'a, 'b> Drop for Backup<'a, 'b> {
fn drop(&mut self) {
unsafe { ffi::sqlite3_backup_finish(self.b) };
}
}
#[cfg(test)]
mod test {
use {SqliteConnection, DatabaseName};
use std::time::Duration;
use super::Backup;
#[test]
#[cfg_attr(rustfmt, rustfmt_skip)]
fn test_backup() {
let src = SqliteConnection::open_in_memory().unwrap();
let sql = "BEGIN;
CREATE TABLE foo(x INTEGER);
INSERT INTO foo VALUES(42);
END;";
src.execute_batch(sql).unwrap();
let mut dst = SqliteConnection::open_in_memory().unwrap();
{
let backup = Backup::new(&src, &mut dst).unwrap();
backup.step(-1).unwrap();
}
let the_answer = dst.query_row("SELECT x FROM foo", &[], |r| r.get::<i64>(0)).unwrap();
assert_eq!(42, the_answer);
src.execute_batch("INSERT INTO foo VALUES(43)").unwrap();
{
let backup = Backup::new(&src, &mut dst).unwrap();
backup.run_to_completion(5, Duration::from_millis(250), None).unwrap();
}
let the_answer = dst.query_row("SELECT SUM(x) FROM foo", &[], |r| r.get::<i64>(0)).unwrap();
assert_eq!(42 + 43, the_answer);
}
#[test]
#[cfg_attr(rustfmt, rustfmt_skip)]
fn test_backup_temp() {
let src = SqliteConnection::open_in_memory().unwrap();
let sql = "BEGIN;
CREATE TEMPORARY TABLE foo(x INTEGER);
INSERT INTO foo VALUES(42);
END;";
src.execute_batch(sql).unwrap();
let mut dst = SqliteConnection::open_in_memory().unwrap();
{
let backup = Backup::new_with_names(&src,
DatabaseName::Temp,
&mut dst,
DatabaseName::Main)
.unwrap();
backup.step(-1).unwrap();
}
let the_answer = dst.query_row("SELECT x FROM foo", &[], |r| r.get::<i64>(0)).unwrap();
assert_eq!(42, the_answer);
src.execute_batch("INSERT INTO foo VALUES(43)").unwrap();
{
let backup = Backup::new_with_names(&src,
DatabaseName::Temp,
&mut dst,
DatabaseName::Main)
.unwrap();
backup.run_to_completion(5, Duration::from_millis(250), None).unwrap();
}
let the_answer = dst.query_row("SELECT SUM(x) FROM foo", &[], |r| r.get::<i64>(0)).unwrap();
assert_eq!(42 + 43, the_answer);
}
#[test]
#[cfg_attr(rustfmt, rustfmt_skip)]
fn test_backup_attached() {
let src = SqliteConnection::open_in_memory().unwrap();
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).unwrap();
let mut dst = SqliteConnection::open_in_memory().unwrap();
{
let backup = Backup::new_with_names(&src,
DatabaseName::Attached("my_attached"),
&mut dst,
DatabaseName::Main)
.unwrap();
backup.step(-1).unwrap();
}
let the_answer = dst.query_row("SELECT x FROM foo", &[], |r| r.get::<i64>(0)).unwrap();
assert_eq!(42, the_answer);
src.execute_batch("INSERT INTO foo VALUES(43)").unwrap();
{
let backup = Backup::new_with_names(&src,
DatabaseName::Attached("my_attached"),
&mut dst,
DatabaseName::Main)
.unwrap();
backup.run_to_completion(5, Duration::from_millis(250), None).unwrap();
}
let the_answer = dst.query_row("SELECT SUM(x) FROM foo", &[], |r| r.get::<i64>(0)).unwrap();
assert_eq!(42 + 43, the_answer);
}
}

View File

@ -82,6 +82,7 @@ pub mod types;
mod transaction; mod transaction;
#[cfg(feature = "load_extension")] mod load_extension_guard; #[cfg(feature = "load_extension")] mod load_extension_guard;
#[cfg(feature = "trace")] pub mod trace; #[cfg(feature = "trace")] pub mod trace;
#[cfg(feature = "backup")] pub mod backup;
/// A typedef of the result returned by many methods. /// A typedef of the result returned by many methods.
pub type SqliteResult<T> = Result<T, SqliteError>; pub type SqliteResult<T> = Result<T, SqliteError>;
@ -142,6 +143,32 @@ fn path_to_cstring(p: &Path) -> SqliteResult<CString> {
str_to_cstring(s) str_to_cstring(s)
} }
/// Name for a database within a SQLite connection.
pub enum DatabaseName<'a> {
/// The main database.
Main,
/// The temporary database (e.g., any "CREATE TEMPORARY TABLE" tables).
Temp,
/// A database that has been attached via "ATTACH DATABASE ...".
Attached(&'a str),
}
// Currently DatabaseName is only used by the backup mod, so hide this (private)
// impl to avoid dead code warnings.
#[cfg(feature = "backup")]
impl<'a> DatabaseName<'a> {
fn to_cstring(self) -> SqliteResult<CString> {
use self::DatabaseName::{Main, Temp, Attached};
match self {
Main => str_to_cstring("main"),
Temp => str_to_cstring("temp"),
Attached(s) => str_to_cstring(s),
}
}
}
/// A connection to a SQLite database. /// A connection to a SQLite database.
pub struct SqliteConnection { pub struct SqliteConnection {
db: RefCell<InnerSqliteConnection>, db: RefCell<InnerSqliteConnection>,