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 }