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