summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock28
-rw-r--r--Cargo.toml10
-rw-r--r--src/db/backup.rs134
3 files changed, 65 insertions, 107 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6d5dc03..85aa54d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -555,6 +555,18 @@ dependencies = [
]
[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
name = "fastrand"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -795,10 +807,10 @@ dependencies = [
"headers",
"hex-literal",
"itertools",
- "libsqlite3-sys",
"password-hash",
"rand",
"rand_core",
+ "rusqlite",
"serde",
"serde_json",
"sqlx",
@@ -1369,6 +1381,20 @@ dependencies = [
]
[[package]]
+name = "rusqlite"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
+[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index b584631..faa858f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,16 +14,16 @@ futures = "0.3.30"
headers = "0.4.0"
hex-literal = "0.4.1"
itertools = "0.13.0"
-# Pinned to keep sqlx and libsqlite3 in lockstep. See also:
-# <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html>
-libsqlite3-sys = { version = "=0.30.1", features = ["bundled"] }
password-hash = { version = "0.5.0", features = ["std"] }
rand = "0.8.5"
rand_core = { version = "0.6.4", features = ["getrandom"] }
+# Pinned to maintain libsqlite3 version match between this and sqlx. See also:
+# <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html>
+rusqlite = { version = "=0.32.1", features = ["backup"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
-# Pinned to keep sqlx and libsqlite3 in lockstep. See also:
-# <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html>
+# Pinned to maintain libsqlite3 version match between this and rusqlite. See
+# also: <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html>
sqlx = { version = "=0.8.2", features = ["chrono", "runtime-tokio", "sqlite"] }
thiserror = "1.0.64"
tokio = { version = "1.40.0", features = ["rt", "macros", "rt-multi-thread"] }
diff --git a/src/db/backup.rs b/src/db/backup.rs
index b09bb3d..bb36aea 100644
--- a/src/db/backup.rs
+++ b/src/db/backup.rs
@@ -1,14 +1,8 @@
-use std::{
- ffi::{c_int, CStr, CString},
- ptr::NonNull,
- str::from_utf8,
+use rusqlite::{
+ backup::{self},
+ Connection,
};
-
-use libsqlite3_sys::{
- sqlite3, sqlite3_backup, sqlite3_backup_finish, sqlite3_backup_init, sqlite3_backup_step,
- sqlite3_errmsg, sqlite3_errstr, sqlite3_extended_errcode, SQLITE_DONE, SQLITE_OK,
-};
-use sqlx::sqlite::SqlitePool;
+use sqlx::sqlite::{LockedSqliteHandle, SqlitePool};
pub struct Builder<'p> {
from: &'p SqlitePool,
@@ -37,63 +31,39 @@ impl<'p> Backup<'p> {
impl<'p> Backup<'p> {
pub async fn backup(&mut self) -> Result<(), Error> {
let mut to = self.to.acquire().await?;
- let mut to = to.lock_handle().await?;
+ let mut to = Self::connection(&mut to.lock_handle().await?)?;
let mut from = self.from.acquire().await?;
- let mut from = from.lock_handle().await?;
-
- let handle = Self::start(to.as_raw_handle(), from.as_raw_handle())?;
- let step_result = loop {
- match Self::step(handle, -1) {
- Err(error) => break Err(error),
- Ok(SQLITE_DONE) => break Ok(()),
- Ok(SQLITE_OK) => (), // keep pumping the backup step function
- Ok(other) => panic!("unexpected step result: {other}"),
+ let from = Self::connection(&mut from.lock_handle().await?)?;
+
+ let backup = backup::Backup::new(&from, &mut to)?;
+ loop {
+ use rusqlite::backup::StepResult::{Busy, Done, Locked, More};
+ match backup.step(-1)? {
+ Done => break Ok(()),
+ // In the system as actually written, yielding does nothing: this is the
+ // only task actually in flight at the time. However, that may change; if
+ // it does, we _definitely_ want to give other tasks an opportunity to
+ // make progress before we retry.
+ More => tokio::task::consume_budget().await,
+ // If another task is actually using the DB, then it _definitely_ needs
+ // an opportunity to make progress before we retry.
+ Busy | Locked => tokio::task::yield_now().await,
+ // As of this writing, this arm is formally unreachable: all of the enum
+ // constants that actually exist in rusqlite are matched above. However,
+ // they've reserved the prerogative to add additional arms without a
+ // breaking change by making the type as non-exhaustive; if we get one,
+ // stop immediately as there's no way to guess what the correct handling
+ // will be, a priori.
+ other => panic!("unexpected backup step result: {other:?}"),
}
- };
- Self::finish(handle)?;
-
- step_result
- }
-
- fn start(
- to: NonNull<sqlite3>,
- from: NonNull<sqlite3>,
- ) -> Result<NonNull<sqlite3_backup>, Error> {
- let name = CString::new("main").expect("static constant is a valid C string");
- // Invariants:
- //
- // * `to` and `from` must be valid `sqlite3` pointers (guaranteed by sqlx)
- // * `zDestName` and `zSourceName` must be valid C strings (see above)
- //
- // Never evaluates to null (even though `sqlite3_backup_init` can).
- let handle = unsafe {
- sqlite3_backup_init(to.as_ptr(), name.as_ptr(), from.as_ptr(), name.as_ptr())
- };
- if handle.is_null() {
- Err(Error::from_handle(to))?;
}
- // Having proven that `handle` is not null, we could use new_unchecked here.
- // Choosing not to so that any mistakes are caught, rather than causing
- // undefined behaviour later on.
- Ok(NonNull::new(handle).expect("backup handle is non-null"))
}
- fn step(handle: NonNull<sqlite3_backup>, pages: c_int) -> Result<c_int, Error> {
- let step = unsafe { sqlite3_backup_step(handle.as_ptr(), pages) };
- if [SQLITE_DONE, SQLITE_OK].contains(&step) {
- Ok(step)
- } else {
- Err(Error::from_code(step))
- }
- }
+ fn connection(handle: &mut LockedSqliteHandle) -> Result<Connection, rusqlite::Error> {
+ let handle = handle.as_raw_handle();
+ let connection = unsafe { Connection::from_handle(handle.as_ptr()) }?;
- fn finish(handle: NonNull<sqlite3_backup>) -> Result<(), Error> {
- let finished = unsafe { sqlite3_backup_finish(handle.as_ptr()) };
- if SQLITE_OK == finished {
- Ok(())
- } else {
- Err(Error::from_code(finished))
- }
+ Ok(connection)
}
}
@@ -101,44 +71,6 @@ impl<'p> Backup<'p> {
pub enum Error {
#[error(transparent)]
Sqlx(#[from] sqlx::Error),
- #[error("backup failed: {message} (code={code})")]
- Backup { code: c_int, message: String },
-}
-
-impl Error {
- pub fn from_handle(handle: NonNull<sqlite3>) -> Self {
- Self::Backup {
- code: Self::code_for(handle),
- message: Self::message_for(handle),
- }
- }
-
- pub fn from_code(code: c_int) -> Self {
- Self::Backup {
- code,
- message: Self::message_from_code(code),
- }
- }
-
- fn code_for(handle: NonNull<sqlite3>) -> c_int {
- unsafe { sqlite3_extended_errcode(handle.as_ptr()) }
- }
-
- fn message_for(handle: NonNull<sqlite3>) -> String {
- Self::message_from(|| unsafe { sqlite3_errmsg(handle.as_ptr()) })
- }
-
- fn message_from_code(code: c_int) -> String {
- Self::message_from(|| unsafe { sqlite3_errstr(code) })
- }
-
- fn message_from(f: impl FnOnce() -> *const i8) -> String {
- let msg = f();
- debug_assert!(!msg.is_null());
- from_utf8(unsafe { CStr::from_ptr(msg) }.to_bytes())
- // This is actually promised in the Sqlite3 docs, but we check anyways to catch
- // mistakes. See <https://www.sqlite.org/c3ref/errcode.html>.
- .expect("error messages from sqlite are always utf-8")
- .to_owned()
- }
+ #[error(transparent)]
+ Rusqlite(#[from] rusqlite::Error),
}