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