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, DatabaseName, Result};
impl Connection {
pub fn backup<P: AsRef<Path>>(
&self,
name: DatabaseName<'_>,
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, DatabaseName::Main)?;
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!(),
}
}
pub fn restore<P: AsRef<Path>, F: Fn(Progress)>(
&mut self,
name: DatabaseName<'_>,
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, DatabaseName::Main, 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!(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum StepResult {
Done,
More,
Busy,
Locked,
}
#[derive(Copy, Clone, Debug)]
pub struct Progress {
pub remaining: c_int,
pub pagecount: c_int,
}
pub struct Backup<'a, 'b> {
phantom_from: PhantomData<&'a Connection>,
to: &'b Connection,
b: *mut ffi::sqlite3_backup,
}
impl Backup<'_, '_> {
#[inline]
pub fn new<'a, 'b>(from: &'a Connection, to: &'b mut Connection) -> Result<Backup<'a, 'b>> {
Backup::new_with_names(from, DatabaseName::Main, to, DatabaseName::Main)
}
pub fn new_with_names<'a, 'b>(
from: &'a Connection,
from_name: DatabaseName<'_>,
to: &'b mut Connection,
to_name: DatabaseName<'_>,
) -> 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,
})
}
#[inline]
#[must_use]
pub fn progress(&self) -> Progress {
unsafe {
Progress {
remaining: ffi::sqlite3_backup_remaining(self.b),
pagecount: ffi::sqlite3_backup_pagecount(self.b),
}
}
}
#[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),
}
}
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 {
use super::{Backup, Progress};
use crate::{Connection, DatabaseName, Result};
use std::time::Duration;
#[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(DatabaseName::Main, path.as_path(), Some(progress))?;
let mut dst = Connection::open_in_memory()?;
dst.restore(DatabaseName::Main, 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)?;
}
let the_answer: i64 = dst.one_column("SELECT x FROM foo")?;
assert_eq!(42, the_answer);
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, DatabaseName::Temp, &mut dst, DatabaseName::Main)?;
backup.step(-1)?;
}
let the_answer: i64 = dst.one_column("SELECT x FROM foo")?;
assert_eq!(42, the_answer);
src.execute_batch("INSERT INTO foo VALUES(43)")?;
{
let backup =
Backup::new_with_names(&src, DatabaseName::Temp, &mut dst, DatabaseName::Main)?;
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,
DatabaseName::Attached("my_attached"),
&mut dst,
DatabaseName::Main,
)?;
backup.step(-1)?;
}
let the_answer: i64 = dst.one_column("SELECT x FROM foo")?;
assert_eq!(42, the_answer);
src.execute_batch("INSERT INTO foo VALUES(43)")?;
{
let backup = Backup::new_with_names(
&src,
DatabaseName::Attached("my_attached"),
&mut dst,
DatabaseName::Main,
)?;
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(())
}
}