1#![deny(unsafe_code)]
3
4#[cfg(all(feature = "sqlcipher", feature = "sqlcipher-system"))]
5compile_error!("rightkit-db features `sqlcipher` and `sqlcipher-system` are mutually exclusive; enable exactly one");
6
7use rusqlite::{Connection, OpenFlags, Transaction, TransactionBehavior};
8use std::ffi::OsString;
9use std::ops::Deref;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak};
12use std::time::{Duration, Instant};
13pub use zeroize::Zeroizing;
16
17mod backup;
18mod checkpoint_job;
19#[allow(unsafe_code)]
20mod host;
21pub use host::per_host_path;
22pub use rusqlite;
26mod migrate;
27mod recover;
28mod restore;
29mod unreadable;
30
31#[cfg(all(feature = "encrypted-lifecycle", not(feature = "cipher")))]
32compile_error!("rightkit-db feature `encrypted-lifecycle` needs a SQLCipher backend; also enable `sqlcipher` or `sqlcipher-system`");
33
34#[cfg(all(feature = "encrypted-lifecycle", feature = "cipher"))]
37pub mod encrypted_lifecycle;
38
39pub use backup::{
40 online_backup_to, remove_database_files, replace_database, replace_database_locked,
41 restore_from, store_lock_path, BackupProgress, StoreLock,
42};
43pub use checkpoint_job::{
44 WalCheckpointJob, WalCheckpointMode, WalCheckpointPolicy, WalCheckpointRun,
45};
46pub use migrate::{
47 add_column_if_missing, column_exists, migrate, migration_version, pending_marker,
48 pending_marker_started_at, rollback_to, rollback_to_with_storage, stored_version_is_newer,
49 user_version, write_pending_marker, Degraded, LegacyVersionTable, MigrateOptions, Migration,
50 MigrationReport, OpenOrDegrade, PendingMarker, Step, VersionStorage,
51};
52#[cfg(feature = "cipher")]
53pub use recover::rekey;
54pub use recover::{
55 quarantine_database, quarantine_database_opts, quarantine_database_with, MoveMode, OpenOutcome,
56 QuarantineOptions, QuarantineStamp, RecoveryPolicy, SidecarPolicy,
57};
58
59pub use restore::{
60 recover_interrupted_restore, recover_interrupted_restore_locked, restore_database,
61 restore_database_locked, InterruptedRestore, RestoreOptions, RestoreReceipt, RestoreVerify,
62};
63
64pub use unreadable::{
65 classify_open_failure, unreadable_counter_path, RecoveryAction, StorageProblem,
66 UnreadableCause, UnreadablePolicy, UnreadableRecovery,
67};
68
69const BUSY_TIMEOUT_MS: u64 = 5_000;
70const JOURNAL_SIZE_LIMIT: i64 = 67_108_864;
71
72pub type Result<T> = std::result::Result<T, DbError>;
73
74#[derive(Debug, thiserror::Error)]
75#[non_exhaustive]
76pub enum DbError {
77 #[error("SQLite error: {0}")]
78 Sqlite(#[from] rusqlite::Error),
79 #[error("a SQLCipher key requires the rightkit-db `sqlcipher` or `sqlcipher-system` feature")]
80 SqlcipherFeatureRequired,
81 #[error("SQLCipher key contains characters that are unsafe for PRAGMA interpolation")]
82 InvalidSqlcipherKey,
83 #[error("database integrity check failed: {0}")]
86 Integrity(String),
87 #[error("quarantine suffix {0:?} must be non-empty and contain no path separator or NUL")]
88 InvalidQuarantineSuffix(String),
89 #[error("read-pool capacity must be greater than zero")]
90 InvalidPoolCapacity,
91 #[error("reader cache size must be between 1 and 2147483647 KiB, got {0}")]
92 InvalidReaderCacheSize(u32),
93 #[error("read pool is closed")]
95 ReadPoolClosed,
96 #[error("read pool close timed out after {timeout:?} with {outstanding} reader(s) still out")]
99 ReadPoolCloseTimeout {
100 outstanding: usize,
101 timeout: Duration,
102 },
103 #[error("reader connection is not read-only: {0}")]
106 ReadOnlyNotEffective(String),
107 #[error("database path has no file name")]
108 InvalidDatabasePath,
109 #[error("database is at schema v{current}, newer than this build understands (v{max}) and not forward compatible")]
110 NewerDatabase { current: i64, max: i64 },
111 #[error("invalid migration list: {0}")]
112 InvalidMigrations(String),
113 #[error("migration v{version} ({name}) failed and was rolled back: {source}")]
114 MigrationFailed {
115 version: i64,
116 name: &'static str,
117 #[source]
118 source: rusqlite::Error,
119 },
120 #[error("cannot roll back: {0}")]
121 Rollback(String),
122 #[error("database backup failed: {0}")]
123 Backup(String),
124 #[error("database restore failed: {message}")]
130 RestoreFailed {
131 message: String,
132 previous: Option<PathBuf>,
133 failed_copy: Option<PathBuf>,
134 rolled_back: bool,
135 },
136 #[error("no candidate key opened the database")]
137 NoKeyOpened,
138 #[error(transparent)]
139 Fs(#[from] rightkit_fs::FsError),
140 #[error(
145 "database {} cannot be read: {cause} ({consecutive_failures} consecutive failed open(s)): {source}",
146 .path.display()
147 )]
148 Unreadable {
149 path: PathBuf,
150 cause: UnreadableCause,
151 consecutive_failures: u32,
154 quarantine_after: Option<u32>,
156 #[source]
157 source: Box<DbError>,
158 },
159}
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum CacheProfile {
163 Default,
164 LargeWorkingSet,
165}
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum DurabilityProfile {
169 Full,
170 Normal,
171}
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177#[non_exhaustive]
178pub enum JournalSelect {
179 WalWithFallback,
182 WalStrict,
184 Delete,
186 Truncate,
188}
189
190impl JournalSelect {
191 fn is_rollback(self) -> bool {
192 matches!(self, JournalSelect::Delete | JournalSelect::Truncate)
193 }
194}
195
196pub const JOURNAL_ENV: &str = "RIGHTKIT_SQLITE_JOURNAL_MODE";
199
200#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
203pub enum IntegrityCheck {
204 #[default]
206 Off,
207 Quick,
210 Full,
212}
213
214pub type ConnInit = Arc<dyn Fn(&Connection) -> rusqlite::Result<()> + Send + Sync>;
215
216#[derive(Clone)]
217pub struct DbOptions {
218 pub path: PathBuf,
219 pub busy_timeout: Duration,
222 pub key: Option<Zeroizing<String>>,
224 pub raw_key: Option<Zeroizing<[u8; 32]>>,
226 pub profile: CacheProfile,
227 pub durability: DurabilityProfile,
228 pub foreign_keys: bool,
229 pub integrity_check_on_open: bool,
231 pub integrity: IntegrityCheck,
234 pub journal: JournalSelect,
235 pub owner_only: bool,
239 pub init: Option<ConnInit>,
240 pub journal_env: Option<String>,
243 pub per_host_fallback: bool,
248 pub secure_delete: Option<bool>,
251 pub optimize_on_open: bool,
255}
256
257impl DbOptions {
258 pub fn new(path: impl Into<PathBuf>) -> Self {
259 Self {
260 path: path.into(),
261 busy_timeout: Duration::from_millis(BUSY_TIMEOUT_MS),
262 key: None,
263 raw_key: None,
264 profile: CacheProfile::Default,
265 durability: DurabilityProfile::Normal,
266 foreign_keys: false,
267 integrity_check_on_open: false,
268 integrity: IntegrityCheck::Off,
269 journal: JournalSelect::WalWithFallback,
270 owner_only: true,
271 init: None,
272 journal_env: None,
273 per_host_fallback: false,
274 secure_delete: None,
275 optimize_on_open: true,
276 }
277 }
278
279 pub fn with_journal_env(mut self, name: impl Into<String>) -> Self {
280 self.journal_env = Some(name.into());
281 self
282 }
283
284 pub fn with_per_host_fallback(mut self, on: bool) -> Self {
285 self.per_host_fallback = on;
286 self
287 }
288
289 pub fn without_optimize_on_open(mut self) -> Self {
291 self.optimize_on_open = false;
292 self
293 }
294
295 pub fn effective_path(&self) -> PathBuf {
299 if !self.per_host_fallback {
300 return self.path.clone();
301 }
302 let sibling = per_host_path(&self.path);
303 if effective_journal(self).is_rollback() || (sibling.exists() && !self.path.exists()) {
304 sibling
305 } else {
306 self.path.clone()
307 }
308 }
309
310 pub fn with_passphrase(mut self, key: impl Into<String>) -> Self {
311 self.key = Some(Zeroizing::new(key.into()));
312 self.raw_key = None;
313 self
314 }
315
316 pub fn with_raw_key(mut self, key: [u8; 32]) -> Self {
317 self.raw_key = Some(Zeroizing::new(key));
318 self.key = None;
319 self
320 }
321
322 pub fn is_keyed(&self) -> bool {
323 self.key.is_some() || self.raw_key.is_some()
324 }
325
326 pub fn open_writer(&self) -> Result<Connection> {
327 self.open(ConnectionKind::Writer)
328 }
329
330 pub fn open_reader(&self) -> Result<Connection> {
331 self.open(ConnectionKind::Reader { cache_kib: None })
332 }
333
334 pub fn open_read_only(&self) -> Result<Connection> {
338 self.open_existing_at(&self.path, OpenFlags::SQLITE_OPEN_READ_ONLY)
339 }
340
341 pub fn open_existing_writer(&self) -> Result<Connection> {
344 self.open_existing_at(&self.path, OpenFlags::SQLITE_OPEN_READ_WRITE)
345 }
346
347 pub fn open_in_memory(&self) -> Result<Connection> {
350 let key = self.key_pragma_value()?;
351 let conn = Connection::open_in_memory()?;
352 self.initialize(&conn, key.as_ref().map(|k| k.as_str()))?;
353 configure_connection(&conn, self, ConnectionKind::Memory, None)?;
354 self.check_on_open(&conn)?;
355 Ok(conn)
356 }
357
358 fn open_existing_at(&self, path: &Path, access: OpenFlags) -> Result<Connection> {
359 let key = self.key_pragma_value()?;
360 let conn = Connection::open_with_flags(
361 path,
362 access | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX,
363 )?;
364 conn.busy_timeout(self.busy_timeout)?;
365 apply_key_first(&conn, key.as_ref().map(|k| k.as_str()))?;
366 verify_key(&conn)?;
367 self.check_on_open(&conn)?;
368 Ok(conn)
369 }
370
371 fn initialize(&self, conn: &Connection, key: Option<&str>) -> Result<()> {
372 conn.busy_timeout(self.busy_timeout)?;
373 apply_key_first(conn, key)?;
374 verify_key(conn)?;
375 if let Some(init) = &self.init {
376 init(conn)?;
377 }
378 Ok(())
379 }
380
381 pub fn open_read_pool(&self, cap: usize) -> Result<ReadPool> {
384 ReadPool::open(self.clone(), cap, ReadPoolOptions::new())
385 }
386
387 pub fn open_read_pool_with(&self, cap: usize, pool: ReadPoolOptions) -> Result<ReadPool> {
389 ReadPool::open(self.clone(), cap, pool)
390 }
391
392 fn open_pool_reader(&self, pool: ReadPoolOptions) -> Result<Connection> {
393 if pool.read_only {
394 self.open_pool_read_only(pool.cache_kib)
395 } else {
396 self.open(ConnectionKind::Reader {
397 cache_kib: pool.cache_kib,
398 })
399 }
400 }
401
402 fn open_pool_read_only(&self, cache_kib: Option<u32>) -> Result<Connection> {
406 let path = self.effective_path();
407 let key = self.key_pragma_value()?;
408 let conn = Connection::open_with_flags(
409 &path,
410 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
411 )?;
412 conn.busy_timeout(self.busy_timeout)?;
413 apply_key_first(&conn, key.as_ref().map(|k| k.as_str()))?;
414 verify_key(&conn)?;
415 conn.pragma_update(None, "temp_store", "MEMORY")?;
416 if self.foreign_keys {
417 conn.pragma_update(None, "foreign_keys", true)?;
418 }
419 apply_cache_settings(&conn, self, ConnectionKind::Reader { cache_kib })?;
420 if let Some(init) = &self.init {
423 init(&conn)?;
424 }
425 enforce_query_only(&conn)?;
426 if !conn.is_readonly("main")? {
427 return Err(DbError::ReadOnlyNotEffective(
428 "SQLITE_OPEN_READ_ONLY handle reports a writable main database".into(),
429 ));
430 }
431 self.check_on_open(&conn)?;
432 Ok(conn)
433 }
434
435 fn open(&self, kind: ConnectionKind) -> Result<Connection> {
436 let path = self.effective_path();
437 let forced = (path != self.path).then(|| match effective_journal(self) {
439 mode if mode.is_rollback() => mode,
440 _ => fallback_mode(self),
441 });
442 let (conn, journal) = self.open_at(&path, kind, forced)?;
443 if journal == Opened::FellBack && self.per_host_fallback && path == self.path {
451 drop(conn);
452 let sibling = per_host_path(&self.path);
453 return Ok(self
454 .open_at(&sibling, kind, Some(JournalSelect::Truncate))?
455 .0);
456 }
457 Ok(conn)
458 }
459
460 fn open_at(
461 &self,
462 path: &Path,
463 kind: ConnectionKind,
464 forced: Option<JournalSelect>,
465 ) -> Result<(Connection, Opened)> {
466 let key = self.key_pragma_value()?;
467 let flags = OpenFlags::SQLITE_OPEN_READ_WRITE
468 | OpenFlags::SQLITE_OPEN_CREATE
469 | OpenFlags::SQLITE_OPEN_NO_MUTEX;
470 let conn = Connection::open_with_flags(path, flags)?;
471 if self.owner_only {
473 rightkit_fs::restrict_owner_only(path)?;
474 }
475 self.initialize(&conn, key.as_ref().map(|k| k.as_str()))?;
476 if let Some(on) = self.secure_delete {
477 conn.pragma_update(None, "secure_delete", if on { "ON" } else { "OFF" })?;
478 }
479 let journal = configure_connection(&conn, self, kind, forced)?;
480 self.check_on_open(&conn)?;
481 if self.owner_only {
482 harden_sidecars(path)?;
483 }
484 Ok((conn, journal))
485 }
486
487 fn check_on_open(&self, conn: &Connection) -> Result<()> {
489 let mode = match self.integrity {
490 IntegrityCheck::Off if self.integrity_check_on_open => IntegrityCheck::Full,
491 mode => mode,
492 };
493 match (mode, self.is_keyed()) {
494 (IntegrityCheck::Off, _) => Ok(()),
495 (IntegrityCheck::Quick, _) => run_integrity_pragma(conn, "PRAGMA quick_check"),
496 (IntegrityCheck::Full, true) => {
497 run_integrity_pragma(conn, "PRAGMA cipher_integrity_check")
498 }
499 (IntegrityCheck::Full, false) => run_integrity_pragma(conn, "PRAGMA integrity_check"),
500 }
501 }
502
503 fn key_pragma_value(&self) -> Result<Option<Zeroizing<String>>> {
505 if let Some(raw) = &self.raw_key {
506 #[cfg(not(feature = "cipher"))]
507 {
508 let _ = raw;
509 return Err(DbError::SqlcipherFeatureRequired);
510 }
511 #[cfg(feature = "cipher")]
512 {
513 let hex: String = raw.iter().map(|b| format!("{b:02x}")).collect();
514 return Ok(Some(Zeroizing::new(format!("\"x'{hex}'\""))));
515 }
516 }
517 let key = self.key.as_ref().map(|key| key.as_str());
518 validate_key(key)?;
519 Ok(key.map(|k| Zeroizing::new(format!("'{k}'"))))
520 }
521}
522
523pub fn open_read_only(path: impl AsRef<Path>) -> Result<Connection> {
525 DbOptions::new(path.as_ref()).open_read_only()
526}
527
528pub fn open_existing_writer(path: impl AsRef<Path>) -> Result<Connection> {
530 DbOptions::new(path.as_ref()).open_existing_writer()
531}
532
533pub fn open_in_memory() -> Result<Connection> {
535 DbOptions::new(":memory:").open_in_memory()
536}
537
538fn harden_sidecars(db: &Path) -> Result<()> {
539 for suffix in ["-wal", "-shm"] {
540 let mut name = OsString::from(db.file_name().ok_or(DbError::InvalidDatabasePath)?);
541 name.push(suffix);
542 rightkit_fs::restrict_owner_only(&db.with_file_name(name))?;
543 }
544 Ok(())
545}
546
547#[derive(Clone, Copy)]
548enum ConnectionKind {
549 Writer,
550 Reader {
552 cache_kib: Option<u32>,
553 },
554 Memory,
555}
556
557impl ConnectionKind {
558 fn reader_cache_kib(self) -> Option<u32> {
559 match self {
560 ConnectionKind::Reader { cache_kib } => cache_kib,
561 ConnectionKind::Writer | ConnectionKind::Memory => None,
562 }
563 }
564}
565
566#[derive(Clone, Copy, Debug, Eq, PartialEq)]
568enum Opened {
569 AsRequested,
570 FellBack,
572}
573
574fn configure_connection(
575 conn: &Connection,
576 options: &DbOptions,
577 kind: ConnectionKind,
578 forced: Option<JournalSelect>,
579) -> Result<Opened> {
580 let opened = if matches!(kind, ConnectionKind::Memory) {
581 Opened::AsRequested
582 } else {
583 configure_journal(conn, options, kind, forced)?
584 };
585 let synchronous = match options.durability {
586 DurabilityProfile::Full => "FULL",
587 DurabilityProfile::Normal => "NORMAL",
588 };
589 conn.pragma_update(None, "synchronous", synchronous)?;
590 conn.pragma_update(None, "journal_size_limit", JOURNAL_SIZE_LIMIT)?;
591 conn.pragma_update(None, "temp_store", "MEMORY")?;
592 if options.foreign_keys {
593 conn.pragma_update(None, "foreign_keys", true)?;
594 }
595 apply_cache_settings(conn, options, kind)?;
596 if matches!(kind, ConnectionKind::Reader { .. }) {
597 enforce_query_only(conn)?;
600 } else if options.optimize_on_open {
601 optimize_on_open(conn)?;
602 }
603 Ok(opened)
604}
605
606fn apply_cache_settings(
609 conn: &Connection,
610 options: &DbOptions,
611 kind: ConnectionKind,
612) -> Result<()> {
613 if options.profile == CacheProfile::LargeWorkingSet {
614 conn.pragma_update(None, "cache_size", -65_536_i64)?;
615 if !matches!(kind, ConnectionKind::Memory) {
616 conn.pragma_update(None, "mmap_size", 268_435_456_i64)?;
617 }
618 }
619 if let Some(kib) = kind.reader_cache_kib() {
620 conn.pragma_update(None, "cache_size", -i64::from(kib))?;
621 }
622 Ok(())
623}
624
625fn enforce_query_only(conn: &Connection) -> Result<()> {
628 conn.pragma_update(None, "query_only", true)?;
629 let on: i64 = conn.query_row("PRAGMA query_only", [], |row| row.get(0))?;
630 if on != 1 {
631 return Err(DbError::ReadOnlyNotEffective(
632 "PRAGMA query_only did not stay ON".into(),
633 ));
634 }
635 Ok(())
636}
637
638fn set_rollback(conn: &Connection, mode: JournalSelect) -> Result<()> {
639 let pragma = if mode == JournalSelect::Truncate {
640 "PRAGMA journal_mode=TRUNCATE"
641 } else {
642 "PRAGMA journal_mode=DELETE"
643 };
644 conn.query_row(pragma, [], |row| row.get::<_, String>(0))?;
645 Ok(())
646}
647
648fn configure_journal(
649 conn: &Connection,
650 options: &DbOptions,
651 kind: ConnectionKind,
652 forced: Option<JournalSelect>,
653) -> Result<Opened> {
654 let wanted = forced.unwrap_or_else(|| effective_journal(options));
655 if wanted.is_rollback() {
656 set_rollback(conn, wanted)?;
659 return Ok(Opened::AsRequested);
660 }
661 let honored = conn
662 .query_row("PRAGMA journal_mode=WAL", [], |row| row.get::<_, String>(0))
663 .map(|mode| mode.eq_ignore_ascii_case("wal"))
664 .unwrap_or(false)
665 && (matches!(kind, ConnectionKind::Reader { .. })
668 || conn.execute_batch("BEGIN IMMEDIATE; ROLLBACK;").is_ok());
669 if honored {
670 return Ok(Opened::AsRequested);
671 }
672 if wanted == JournalSelect::WalStrict {
673 return Err(DbError::Sqlite(rusqlite::Error::SqliteFailure(
674 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
675 Some("WAL journal mode was not honored by this filesystem".into()),
676 )));
677 }
678 let _ = conn.execute_batch("ROLLBACK;");
679 set_rollback(conn, fallback_mode(options))?;
680 Ok(Opened::FellBack)
681}
682
683fn fallback_mode(options: &DbOptions) -> JournalSelect {
686 if options.per_host_fallback {
687 JournalSelect::Truncate
688 } else {
689 JournalSelect::Delete
690 }
691}
692
693fn env_journal(options: &DbOptions) -> Option<JournalSelect> {
696 options
697 .journal_env
698 .iter()
699 .map(String::as_str)
700 .chain([JOURNAL_ENV])
701 .find_map(|name| {
702 match std::env::var(name)
703 .ok()?
704 .trim()
705 .to_ascii_lowercase()
706 .as_str()
707 {
708 "wal" => Some(JournalSelect::WalStrict),
709 "delete" => Some(JournalSelect::Delete),
710 "truncate" => Some(JournalSelect::Truncate),
711 _ => None,
712 }
713 })
714}
715
716fn effective_journal(options: &DbOptions) -> JournalSelect {
717 if let Some(mode) = env_journal(options) {
718 return mode;
719 }
720 if options.journal == JournalSelect::WalWithFallback
721 && rightkit_fs::is_network_filesystem(&options.path)
722 {
723 return fallback_mode(options);
724 }
725 options.journal
726}
727
728pub fn journal_mode(conn: &Connection) -> Result<String> {
730 Ok(conn.query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0))?)
731}
732
733pub fn write_txn(conn: &mut Connection) -> Result<Transaction<'_>> {
736 conn.transaction_with_behavior(TransactionBehavior::Immediate)
737 .map_err(Into::into)
738}
739
740pub fn write_txn_unchecked(conn: &Connection) -> Result<Transaction<'_>> {
745 Transaction::new_unchecked(conn, TransactionBehavior::Immediate).map_err(Into::into)
746}
747
748#[cfg(feature = "cipher")]
749fn apply_key_first(conn: &Connection, key: Option<&str>) -> Result<()> {
750 if let Some(key) = key {
751 let statement = Zeroizing::new(format!("PRAGMA key = {key};"));
752 conn.execute_batch(statement.as_str())?;
753 }
754 Ok(())
755}
756
757#[cfg(not(feature = "cipher"))]
758fn apply_key_first(_conn: &Connection, key: Option<&str>) -> Result<()> {
759 if key.is_some() {
760 return Err(DbError::SqlcipherFeatureRequired);
761 }
762 Ok(())
763}
764
765fn validate_key(key: Option<&str>) -> Result<()> {
766 #[cfg(not(feature = "cipher"))]
767 if key.is_some() {
768 return Err(DbError::SqlcipherFeatureRequired);
769 }
770 if let Some(key) = key {
771 let safe = !key.is_empty()
772 && key.len() <= 4_096
773 && key.bytes().all(|byte| {
774 byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=' | b'-' | b'_')
775 });
776 if !safe {
777 return Err(DbError::InvalidSqlcipherKey);
778 }
779 }
780 Ok(())
781}
782
783fn verify_key(conn: &Connection) -> Result<()> {
784 conn.query_row("SELECT count(*) FROM sqlite_master", [], |row| {
785 row.get::<_, i64>(0)
786 })?;
787 Ok(())
788}
789
790fn run_integrity_pragma(conn: &Connection, pragma: &str) -> Result<()> {
791 let mut statement = conn.prepare(pragma)?;
792 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
793 let messages = rows.collect::<std::result::Result<Vec<_>, _>>()?;
794 if messages.is_empty()
795 || messages
796 .iter()
797 .all(|message| message.eq_ignore_ascii_case("ok"))
798 {
799 return Ok(());
800 }
801 Err(DbError::Integrity(messages.join("; ")))
802}
803
804#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
808pub struct ReadPoolOptions {
809 read_only: bool,
810 cache_kib: Option<u32>,
811}
812
813impl ReadPoolOptions {
814 pub fn new() -> Self {
815 Self::default()
816 }
817
818 pub fn with_read_only(mut self, on: bool) -> Self {
824 self.read_only = on;
825 self
826 }
827
828 pub fn with_reader_cache_kib(mut self, kib: u32) -> Self {
834 self.cache_kib = Some(kib);
835 self
836 }
837}
838
839#[derive(Clone)]
841pub struct ReadPool {
842 inner: Arc<ReadPoolInner>,
843}
844
845type ReaderOpener = Box<dyn Fn() -> Result<Connection> + Send + Sync>;
848
849struct ReadPoolInner {
850 open: ReaderOpener,
851 cap: usize,
852 state: Mutex<ReadPoolState>,
853 available: Condvar,
855 drained: Condvar,
857}
858
859#[derive(Default)]
860struct ReadPoolState {
861 idle: Vec<Connection>,
862 total: usize,
864 closed: bool,
865}
866
867impl ReadPoolInner {
868 fn open_reader(&self) -> Result<Connection> {
869 (self.open)()
870 }
871}
872
873pub struct PooledReadConnection {
874 conn: Option<Connection>,
875 pool: Weak<ReadPoolInner>,
876 _lifecycle_lease: Option<Arc<dyn Send + Sync>>,
878}
879
880impl PooledReadConnection {
881 #[cfg(feature = "encrypted-lifecycle")]
882 pub(crate) fn with_lifecycle_lease(mut self, lease: Arc<dyn Send + Sync>) -> Self {
883 self._lifecycle_lease = Some(lease);
884 self
885 }
886}
887
888impl Deref for PooledReadConnection {
889 type Target = Connection;
890
891 fn deref(&self) -> &Self::Target {
892 self.conn.as_ref().expect("pooled connection is present")
893 }
894}
895
896impl Drop for PooledReadConnection {
897 fn drop(&mut self) {
898 let Some(conn) = self.conn.take() else {
899 return;
900 };
901 let Some(pool) = self.pool.upgrade() else {
902 return;
903 };
904 let mut state = lock_recover(&pool.state);
905 if state.closed {
906 drop(conn);
910 state.total -= 1;
911 pool.drained.notify_all();
912 } else {
913 state.idle.push(conn);
914 pool.available.notify_one();
915 }
916 }
917}
918
919impl ReadPool {
920 fn open(options: DbOptions, cap: usize, pool_options: ReadPoolOptions) -> Result<Self> {
921 if cap == 0 {
922 return Err(DbError::InvalidPoolCapacity);
923 }
924 if let Some(kib) = pool_options.cache_kib {
925 if kib == 0 || kib > i32::MAX.unsigned_abs() {
926 return Err(DbError::InvalidReaderCacheSize(kib));
927 }
928 }
929 Self::with_opener(
930 cap,
931 Box::new(move || options.open_pool_reader(pool_options)),
932 )
933 }
934
935 pub fn from_opener(
940 cap: usize,
941 open: impl Fn() -> Result<Connection> + Send + Sync + 'static,
942 ) -> Result<Self> {
943 Self::with_opener(cap, Box::new(open))
944 }
945
946 fn with_opener(cap: usize, open: ReaderOpener) -> Result<Self> {
947 if cap == 0 {
948 return Err(DbError::InvalidPoolCapacity);
949 }
950 let pool = Self {
951 inner: Arc::new(ReadPoolInner {
952 open,
953 cap,
954 state: Mutex::new(ReadPoolState::default()),
955 available: Condvar::new(),
956 drained: Condvar::new(),
957 }),
958 };
959 let conn = pool.inner.open_reader()?;
960 let mut state = lock_recover(&pool.inner.state);
961 state.total = 1;
962 state.idle.push(conn);
963 drop(state);
964 Ok(pool)
965 }
966
967 pub fn get(&self) -> Result<PooledReadConnection> {
971 match self.checkout(None)? {
972 Some(reader) => Ok(reader),
973 None => Err(DbError::ReadPoolClosed),
975 }
976 }
977
978 pub fn get_timeout(&self, timeout: Duration) -> Result<Option<PooledReadConnection>> {
985 self.checkout(Some(timeout))
986 }
987
988 fn checkout(&self, wait: Option<Duration>) -> Result<Option<PooledReadConnection>> {
990 let started = Instant::now();
991 let deadline = wait.and_then(|wait| started.checked_add(wait));
992 let mut state = lock_recover(&self.inner.state);
993 loop {
994 if state.closed {
995 return Err(DbError::ReadPoolClosed);
996 }
997 if let Some(conn) = state.idle.pop() {
998 return Ok(Some(PooledReadConnection {
999 conn: Some(conn),
1000 pool: Arc::downgrade(&self.inner),
1001 _lifecycle_lease: None,
1002 }));
1003 }
1004 if state.total < self.inner.cap {
1005 state.total += 1;
1006 drop(state);
1007 return self.open_checked_out().map(Some);
1008 }
1009 state = match (wait, deadline) {
1010 (None, _) => match self.inner.available.wait(state) {
1011 Ok(state) => state,
1012 Err(poisoned) => poisoned.into_inner(),
1013 },
1014 (Some(_), Some(at)) => {
1015 let remaining = at.saturating_duration_since(Instant::now());
1016 if remaining.is_zero() {
1017 return Ok(None);
1018 }
1019 match self.inner.available.wait_timeout(state, remaining) {
1020 Ok((state, _)) => state,
1021 Err(poisoned) => poisoned.into_inner().0,
1022 }
1023 }
1024 (Some(_), None) => {
1025 match self
1026 .inner
1027 .available
1028 .wait_timeout(state, Duration::from_secs(60))
1029 {
1030 Ok((state, _)) => state,
1031 Err(poisoned) => poisoned.into_inner().0,
1032 }
1033 }
1034 };
1035 }
1036 }
1037
1038 fn open_checked_out(&self) -> Result<PooledReadConnection> {
1041 match self.inner.open_reader() {
1042 Ok(conn) => {
1043 let mut state = lock_recover(&self.inner.state);
1044 if state.closed {
1045 drop(conn);
1046 state.total -= 1;
1047 self.inner.drained.notify_all();
1048 return Err(DbError::ReadPoolClosed);
1049 }
1050 Ok(PooledReadConnection {
1051 conn: Some(conn),
1052 pool: Arc::downgrade(&self.inner),
1053 _lifecycle_lease: None,
1054 })
1055 }
1056 Err(error) => {
1057 let mut state = lock_recover(&self.inner.state);
1058 state.total -= 1;
1059 self.inner.available.notify_one();
1060 self.inner.drained.notify_all();
1061 Err(error)
1062 }
1063 }
1064 }
1065
1066 pub fn close(&self) {
1069 let idle = {
1070 let mut state = lock_recover(&self.inner.state);
1071 if state.closed {
1072 return;
1073 }
1074 state.closed = true;
1075 self.inner.available.notify_all();
1076 std::mem::take(&mut state.idle)
1077 };
1078 let closed = idle.len();
1079 drop(idle);
1081 let mut state = lock_recover(&self.inner.state);
1082 state.total -= closed;
1083 self.inner.drained.notify_all();
1084 }
1085
1086 pub fn close_and_wait(&self, timeout: Duration) -> Result<()> {
1092 self.close();
1093 let deadline = Instant::now().checked_add(timeout);
1094 let mut state = lock_recover(&self.inner.state);
1095 loop {
1096 if state.total == 0 {
1097 return Ok(());
1098 }
1099 let remaining = match deadline {
1100 Some(at) => at.saturating_duration_since(Instant::now()),
1101 None => Duration::from_secs(60),
1102 };
1103 if remaining.is_zero() {
1104 return Err(DbError::ReadPoolCloseTimeout {
1105 outstanding: state.total,
1106 timeout,
1107 });
1108 }
1109 let (next, _) = match self.inner.drained.wait_timeout(state, remaining) {
1110 Ok(pair) => pair,
1111 Err(poisoned) => poisoned.into_inner(),
1112 };
1113 state = next;
1114 }
1115 }
1116
1117 pub fn capacity(&self) -> usize {
1118 self.inner.cap
1119 }
1120
1121 pub fn idle_len(&self) -> usize {
1122 lock_recover(&self.inner.state).idle.len()
1123 }
1124
1125 pub fn total_len(&self) -> usize {
1126 lock_recover(&self.inner.state).total
1127 }
1128}
1129
1130fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
1131 mutex
1132 .lock()
1133 .unwrap_or_else(|poisoned| poisoned.into_inner())
1134}
1135
1136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1137pub struct CheckpointStatus {
1138 pub busy: i64,
1139 pub log_frames: i64,
1140 pub checkpointed_frames: i64,
1141}
1142
1143pub fn checkpoint_truncate(conn: &Connection) -> Result<CheckpointStatus> {
1144 checkpoint(conn, "PRAGMA wal_checkpoint(TRUNCATE)")
1145}
1146
1147pub fn checkpoint_restart(conn: &Connection) -> Result<CheckpointStatus> {
1152 checkpoint(conn, "PRAGMA wal_checkpoint(RESTART)")
1153}
1154
1155pub fn checkpoint_if_wal_larger_than(
1156 conn: &Connection,
1157 db: &Path,
1158 bytes: u64,
1159) -> Result<Option<CheckpointStatus>> {
1160 let wal = wal_path(db)?;
1161 let size = match std::fs::metadata(wal) {
1162 Ok(metadata) => metadata.len(),
1163 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1164 Err(_) => return Ok(None),
1165 };
1166 if size <= bytes {
1167 return Ok(None);
1168 }
1169 checkpoint(conn, "PRAGMA wal_checkpoint(PASSIVE)").map(Some)
1170}
1171
1172fn checkpoint(conn: &Connection, pragma: &str) -> Result<CheckpointStatus> {
1173 conn.query_row(pragma, [], |row| {
1174 Ok(CheckpointStatus {
1175 busy: row.get(0)?,
1176 log_frames: row.get(1)?,
1177 checkpointed_frames: row.get(2)?,
1178 })
1179 })
1180 .map_err(Into::into)
1181}
1182
1183fn wal_path(db: &Path) -> Result<PathBuf> {
1184 let file_name = db.file_name().ok_or(DbError::InvalidDatabasePath)?;
1185 let mut wal_name = OsString::from(file_name);
1186 wal_name.push("-wal");
1187 Ok(db.with_file_name(wal_name))
1188}
1189
1190pub fn optimize_on_open(conn: &Connection) -> Result<()> {
1191 conn.execute_batch("PRAGMA optimize=0x10002;")?;
1192 Ok(())
1193}
1194
1195pub fn optimize_boundary(conn: &Connection) -> Result<()> {
1196 conn.execute_batch("PRAGMA optimize;")?;
1197 Ok(())
1198}