lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

sqlite_native_backup.rs (6615B)


      1 //! Sealed online-backup calls over SQLx-owned locked SQLite handles.
      2 
      3 use core::{fmt, marker::PhantomData, ptr::NonNull};
      4 use std::error::Error;
      5 
      6 use libsqlite3_sys as ffi;
      7 use sqlx::sqlite::LockedSqliteHandle;
      8 
      9 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     10 pub(crate) enum NativeBackupStep {
     11     Done,
     12     More,
     13     Busy,
     14     Locked,
     15 }
     16 
     17 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     18 enum NativeBackupFailureKind {
     19     Initialize,
     20     Step,
     21     Finish,
     22 }
     23 
     24 #[derive(PartialEq, Eq)]
     25 pub(crate) struct NativeBackupError {
     26     kind: NativeBackupFailureKind,
     27     code: i32,
     28 }
     29 
     30 impl fmt::Debug for NativeBackupError {
     31     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     32         formatter
     33             .debug_struct("NativeBackupError")
     34             .field("kind", &self.kind)
     35             .field("code", &self.code)
     36             .finish()
     37     }
     38 }
     39 
     40 impl fmt::Display for NativeBackupError {
     41     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     42         formatter.write_str(match self.kind {
     43             NativeBackupFailureKind::Initialize => "native SQLite backup initialization failed",
     44             NativeBackupFailureKind::Step => "native SQLite backup step failed",
     45             NativeBackupFailureKind::Finish => "native SQLite backup finalization failed",
     46         })
     47     }
     48 }
     49 
     50 impl Error for NativeBackupError {}
     51 
     52 pub(crate) struct NativeBackup<'destination, 'source> {
     53     handle: Option<NonNull<ffi::sqlite3_backup>>,
     54     _locked_handles: PhantomData<(&'destination mut (), &'source mut ())>,
     55 }
     56 
     57 impl<'destination, 'source> NativeBackup<'destination, 'source> {
     58     pub(crate) fn start(
     59         destination: &'destination mut LockedSqliteHandle<'_>,
     60         source: &'source mut LockedSqliteHandle<'_>,
     61     ) -> Result<Self, NativeBackupError> {
     62         let destination = destination.as_raw_handle();
     63         let source = source.as_raw_handle();
     64         // SAFETY: both handles are borrowed from live SQLx lock guards, the
     65         // fixed schema names are valid NUL-terminated strings, and the backup
     66         // handle cannot outlive this function's returned owner.
     67         let handle = unsafe {
     68             ffi::sqlite3_backup_init(
     69                 destination.as_ptr(),
     70                 c"main".as_ptr(),
     71                 source.as_ptr(),
     72                 c"main".as_ptr(),
     73             )
     74         };
     75         let handle = NonNull::new(handle).ok_or_else(|| {
     76             // SAFETY: the destination SQLx guard keeps this handle live and
     77             // exclusively locked for the duration of the call.
     78             let code = unsafe { ffi::sqlite3_errcode(destination.as_ptr()) };
     79             NativeBackupError {
     80                 kind: NativeBackupFailureKind::Initialize,
     81                 code,
     82             }
     83         })?;
     84         Ok(Self {
     85             handle: Some(handle),
     86             _locked_handles: PhantomData,
     87         })
     88     }
     89 
     90     pub(crate) fn step(&mut self, pages: i32) -> Result<NativeBackupStep, NativeBackupError> {
     91         if pages <= 0 {
     92             return Err(NativeBackupError {
     93                 kind: NativeBackupFailureKind::Step,
     94                 code: ffi::SQLITE_MISUSE,
     95             });
     96         }
     97         let Some(handle) = self.handle else {
     98             return Err(NativeBackupError {
     99                 kind: NativeBackupFailureKind::Step,
    100                 code: ffi::SQLITE_MISUSE,
    101             });
    102         };
    103         // SAFETY: `handle` remains owned by this adapter and `pages` is a
    104         // positive bounded batch supplied by the capture driver.
    105         classify_step(unsafe { ffi::sqlite3_backup_step(handle.as_ptr(), pages) })
    106     }
    107 
    108     pub(crate) fn finish(mut self) -> Result<(), NativeBackupError> {
    109         let code = self.finish_once();
    110         if code == ffi::SQLITE_OK {
    111             Ok(())
    112         } else {
    113             Err(NativeBackupError {
    114                 kind: NativeBackupFailureKind::Finish,
    115                 code,
    116             })
    117         }
    118     }
    119 
    120     fn finish_once(&mut self) -> i32 {
    121         self.handle.map_or(ffi::SQLITE_OK, |handle| {
    122             self.handle = None;
    123             // SAFETY: taking the handle ensures exactly one finalization call.
    124             unsafe { ffi::sqlite3_backup_finish(handle.as_ptr()) }
    125         })
    126     }
    127 }
    128 
    129 impl Drop for NativeBackup<'_, '_> {
    130     fn drop(&mut self) {
    131         let _ = self.finish_once();
    132     }
    133 }
    134 
    135 fn classify_step(code: i32) -> Result<NativeBackupStep, NativeBackupError> {
    136     match code {
    137         ffi::SQLITE_DONE => Ok(NativeBackupStep::Done),
    138         ffi::SQLITE_OK => Ok(NativeBackupStep::More),
    139         ffi::SQLITE_BUSY => Ok(NativeBackupStep::Busy),
    140         ffi::SQLITE_LOCKED => Ok(NativeBackupStep::Locked),
    141         code => Err(NativeBackupError {
    142             kind: NativeBackupFailureKind::Step,
    143             code,
    144         }),
    145     }
    146 }
    147 
    148 #[cfg(test)]
    149 mod tests {
    150     use super::*;
    151 
    152     #[test]
    153     fn step_codes_are_closed_and_stable() {
    154         assert_eq!(classify_step(ffi::SQLITE_DONE), Ok(NativeBackupStep::Done));
    155         assert_eq!(classify_step(ffi::SQLITE_OK), Ok(NativeBackupStep::More));
    156         assert_eq!(classify_step(ffi::SQLITE_BUSY), Ok(NativeBackupStep::Busy));
    157         assert_eq!(
    158             classify_step(ffi::SQLITE_LOCKED),
    159             Ok(NativeBackupStep::Locked)
    160         );
    161         assert_eq!(
    162             classify_step(ffi::SQLITE_CORRUPT)
    163                 .expect_err("unexpected native code must fail")
    164                 .kind,
    165             NativeBackupFailureKind::Step
    166         );
    167     }
    168 
    169     #[test]
    170     fn empty_and_misused_native_handles_fail_or_finish_deterministically() {
    171         let mut backup = NativeBackup {
    172             handle: None,
    173             _locked_handles: PhantomData,
    174         };
    175         for pages in [i32::MIN, -1, 0, 1] {
    176             let error = backup.step(pages).expect_err("invalid native handle");
    177             assert_eq!(error.kind, NativeBackupFailureKind::Step);
    178             assert_eq!(error.code, ffi::SQLITE_MISUSE);
    179         }
    180         backup.finish().expect("empty handle is already finalized");
    181 
    182         for (kind, expected) in [
    183             (
    184                 NativeBackupFailureKind::Initialize,
    185                 "native SQLite backup initialization failed",
    186             ),
    187             (
    188                 NativeBackupFailureKind::Step,
    189                 "native SQLite backup step failed",
    190             ),
    191             (
    192                 NativeBackupFailureKind::Finish,
    193                 "native SQLite backup finalization failed",
    194             ),
    195         ] {
    196             let error = NativeBackupError { kind, code: 1 };
    197             assert_eq!(error.to_string(), expected);
    198             assert!(format!("{error:?}").contains("code: 1"));
    199         }
    200     }
    201 }