1use crate::recover::{
17 allowed, is_unreadable, quarantine_database_journaled, unfinished_quarantine, RecoveryPolicy,
18 QUARANTINE_JOURNAL_SUFFIX,
19};
20use crate::{DbError, DbOptions, Result};
21use rusqlite::{Connection, ErrorCode, OptionalExtension};
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24use std::time::{SystemTime, UNIX_EPOCH};
25
26pub enum Step {
27 Sql(&'static str),
28 Code(fn(&Connection) -> rusqlite::Result<()>),
30}
31
32pub struct Migration {
33 pub version: i64,
34 pub name: &'static str,
35 pub up: Step,
36 pub down: Option<&'static str>,
38}
39
40impl Migration {
41 pub const fn sql(version: i64, name: &'static str, up: &'static str) -> Self {
42 Self {
43 version,
44 name,
45 up: Step::Sql(up),
46 down: None,
47 }
48 }
49
50 pub const fn with_down(mut self, down: &'static str) -> Self {
51 self.down = Some(down);
52 self
53 }
54
55 pub const fn code(
56 version: i64,
57 name: &'static str,
58 up: fn(&Connection) -> rusqlite::Result<()>,
59 ) -> Self {
60 Self {
61 version,
62 name,
63 up: Step::Code(up),
64 down: None,
65 }
66 }
67}
68
69#[derive(Clone, Debug, Default, PartialEq, Eq)]
72pub enum VersionStorage {
73 #[default]
74 UserVersion,
75 Table { table: String, key: String },
78 VersionTable {
84 table: String,
85 version_column: String,
86 applied_at_column: String,
87 },
88}
89
90impl VersionStorage {
91 pub fn version_table(table: impl Into<String>) -> Self {
93 Self::VersionTable {
94 table: table.into(),
95 version_column: "version".into(),
96 applied_at_column: "applied_at".into(),
97 }
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct LegacyVersionTable {
105 pub table: String,
106 pub version_column: String,
107}
108
109pub type VerifyBackup = Arc<dyn Fn(&Path) -> std::result::Result<(), String> + Send + Sync>;
112pub type BackupName = Arc<dyn Fn(i64, i64) -> String + Send + Sync>;
115#[derive(Clone)]
116pub struct MigrateOptions {
117 pub version_storage: VersionStorage,
119 pub backup: bool,
121 pub backup_options: Option<DbOptions>,
123 pub forward_compatible: Option<fn(&Connection) -> bool>,
126 pub optimize_after: bool,
128 pub legacy_versions: Option<LegacyVersionTable>,
130 pub backup_name: Option<BackupName>,
135 pub verify_backup: Option<VerifyBackup>,
138 pub backup_retention: Option<usize>,
143}
144
145impl Default for MigrateOptions {
146 fn default() -> Self {
147 Self {
148 version_storage: VersionStorage::default(),
149 backup: true,
150 backup_options: None,
151 forward_compatible: None,
152 optimize_after: true,
153 legacy_versions: None,
154 backup_name: None,
155 verify_backup: None,
156 backup_retention: None,
157 }
158 }
159}
160
161impl MigrateOptions {
162 pub fn import_legacy_versions(mut self, legacy: LegacyVersionTable) -> Self {
172 self.legacy_versions = Some(legacy);
173 self
174 }
175}
176
177#[derive(Clone, Debug, Default, PartialEq, Eq)]
178pub struct MigrationReport {
179 pub from: i64,
180 pub to: i64,
181 pub applied: Vec<i64>,
182 pub backup: Option<PathBuf>,
183 pub forward_compatible: bool,
185 pub interrupted_previous: Option<PendingMarker>,
187}
188
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub struct PendingMarker {
191 pub from: i64,
192 pub to: i64,
193 pub pid: u32,
194}
195
196pub fn user_version(conn: &Connection) -> Result<i64> {
197 Ok(conn.query_row("PRAGMA user_version", [], |row| row.get(0))?)
198}
199
200fn set_user_version(conn: &Connection, version: i64) -> rusqlite::Result<()> {
201 conn.execute_batch(&format!("PRAGMA user_version = {version};"))
203}
204
205pub fn migration_version(conn: &Connection, storage: &VersionStorage) -> Result<i64> {
208 match storage {
209 VersionStorage::UserVersion => user_version(conn),
210 VersionStorage::Table { table, key } => key_value_version(conn, table, key),
211 VersionStorage::VersionTable {
212 table,
213 version_column,
214 ..
215 } => version_row_max(conn, table, version_column),
216 }
217}
218
219fn key_value_version(conn: &Connection, table: &str, key: &str) -> Result<i64> {
220 check_ident(table)?;
221 let exists: bool = conn.query_row(
222 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1 COLLATE NOCASE)",
223 [table],
224 |row| row.get(0),
225 )?;
226 if !exists {
227 return Ok(0);
228 }
229 let value: Option<String> = conn
230 .query_row(
231 &format!("SELECT CAST(\"value\" AS TEXT) FROM \"{table}\" WHERE \"key\"=?1"),
232 [key],
233 |row| row.get(0),
234 )
235 .optional()?;
236 match value {
237 None => Ok(0),
238 Some(value) => value
239 .parse::<i64>()
240 .ok()
241 .filter(|version| *version >= 0)
242 .ok_or_else(|| DbError::InvalidMigrations(format!("invalid stored version {value:?}"))),
243 }
244}
245
246fn set_key_value_version(
248 conn: &Connection,
249 storage: &VersionStorage,
250 version: i64,
251) -> rusqlite::Result<()> {
252 match storage {
253 VersionStorage::UserVersion => set_user_version(conn, version),
254 VersionStorage::Table { table, key } => {
255 conn.execute_batch(&format!(
256 "CREATE TABLE IF NOT EXISTS \"{table}\" (\"key\" TEXT PRIMARY KEY, \"value\" TEXT NOT NULL);"
257 ))?;
258 conn.execute(
259 &format!(
260 "INSERT INTO \"{table}\"(\"key\",\"value\") VALUES(?1,?2) \
261 ON CONFLICT(\"key\") DO UPDATE SET \"value\"=excluded.\"value\""
262 ),
263 rusqlite::params![key, version.to_string()],
264 )?;
265 Ok(())
266 }
267 VersionStorage::VersionTable { .. } => Err(rusqlite::Error::InvalidQuery),
269 }
270}
271
272fn advance_version(
274 conn: &Connection,
275 storage: &VersionStorage,
276 version: i64,
277) -> rusqlite::Result<()> {
278 match storage {
279 VersionStorage::VersionTable {
280 table,
281 version_column,
282 applied_at_column,
283 } => {
284 conn.execute_batch(&format!(
285 "CREATE TABLE IF NOT EXISTS \"{table}\" (\"{version_column}\" INTEGER PRIMARY KEY, \"{applied_at_column}\" INTEGER NOT NULL);"
286 ))?;
287 conn.execute(
288 &format!("DELETE FROM \"{table}\" WHERE \"{version_column}\" = ?1"),
289 [version],
290 )?;
291 conn.execute(
292 &format!(
293 "INSERT INTO \"{table}\" (\"{version_column}\", \"{applied_at_column}\") VALUES (?1, ?2)"
294 ),
295 rusqlite::params![version, now_secs()],
296 )?;
297 Ok(())
298 }
299 _ => set_key_value_version(conn, storage, version),
300 }
301}
302
303fn retreat_version(
305 conn: &Connection,
306 storage: &VersionStorage,
307 version: i64,
308) -> rusqlite::Result<()> {
309 match storage {
310 VersionStorage::VersionTable {
311 table,
312 version_column,
313 ..
314 } => {
315 conn.execute(
316 &format!("DELETE FROM \"{table}\" WHERE \"{version_column}\" = ?1"),
317 [version],
318 )?;
319 Ok(())
320 }
321 _ => set_key_value_version(conn, storage, version - 1),
322 }
323}
324
325fn adopt_legacy_version(conn: &Connection, storage: &VersionStorage, through: i64) -> Result<()> {
328 conn.execute_batch("BEGIN IMMEDIATE;")?;
329 let seeded = match storage {
330 VersionStorage::VersionTable { .. } => {
331 (1..=through).try_for_each(|version| advance_version(conn, storage, version))
332 }
333 _ => advance_version(conn, storage, through),
334 };
335 match seeded {
336 Ok(()) => {
337 conn.execute_batch("COMMIT;")?;
338 Ok(())
339 }
340 Err(error) => {
341 let _ = conn.execute_batch("ROLLBACK;");
342 Err(DbError::Sqlite(error))
343 }
344 }
345}
346
347fn version_row_max(conn: &Connection, table: &str, version_column: &str) -> Result<i64> {
349 check_ident(table)?;
350 check_ident(version_column)?;
351 let exists: bool = conn.query_row(
352 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1 COLLATE NOCASE)",
353 [table],
354 |row| row.get(0),
355 )?;
356 if !exists {
357 return Ok(0);
358 }
359 let highest: Option<i64> = conn.query_row(
360 &format!("SELECT MAX(CAST(\"{version_column}\" AS INTEGER)) FROM \"{table}\""),
361 [],
362 |row| row.get(0),
363 )?;
364 let version = highest.unwrap_or(0);
365 if version < 0 {
366 return Err(DbError::InvalidMigrations(format!(
367 "invalid stored version {version}"
368 )));
369 }
370 Ok(version)
371}
372
373fn check_storage(storage: &VersionStorage) -> Result<()> {
375 match storage {
376 VersionStorage::UserVersion => Ok(()),
377 VersionStorage::Table { table, .. } => check_ident(table),
378 VersionStorage::VersionTable {
379 table,
380 version_column,
381 applied_at_column,
382 } => {
383 check_ident(table)?;
384 check_ident(version_column)?;
385 check_ident(applied_at_column)
386 }
387 }
388}
389
390fn backup_path(db: &Path, options: &MigrateOptions, current: i64, target: i64) -> Result<PathBuf> {
393 let name = db
394 .file_name()
395 .ok_or(DbError::InvalidDatabasePath)?
396 .to_string_lossy()
397 .into_owned();
398 let file = match &options.backup_name {
399 Some(namer) => namer(current, target),
400 None => format!("{name}.pre-v{target}.bak"),
401 };
402 if file.is_empty() || file.contains(['/', '\\', '\0']) || file == "." || file == ".." {
403 return Err(DbError::Backup(format!(
404 "backup name {file:?} must be a plain file name other than the database's"
405 )));
406 }
407 let lowered = file.to_lowercase();
411 let reserved = [
412 "",
413 "-wal",
414 "-shm",
415 "-journal",
416 ".migrating",
417 ".restore-journal",
418 ".store-lock",
419 ".transition-journal",
420 QUARANTINE_JOURNAL_SUFFIX,
421 ]
422 .iter()
423 .any(|suffix| lowered == format!("{name}{suffix}").to_lowercase());
424 if reserved {
425 return Err(DbError::Backup(format!(
426 "backup name {file:?} is the database, one of its sidecars or a recovery file; choose another name"
427 )));
428 }
429 Ok(db.with_file_name(file))
430}
431
432fn prune_backups(db: &Path, newest: &Path, keep: usize) {
436 let Some(name) = db.file_name().map(|n| n.to_string_lossy().into_owned()) else {
437 return;
438 };
439 let dir = db
440 .parent()
441 .filter(|p| !p.as_os_str().is_empty())
442 .unwrap_or(Path::new("."));
443 let prefix = format!("{name}.pre-");
444 let Ok(entries) = std::fs::read_dir(dir) else {
445 return;
446 };
447 let mut older: Vec<(SystemTime, PathBuf)> = entries
448 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
449 .filter(|path| path.as_path() != newest)
450 .filter(|path| {
451 path.file_name().is_some_and(|n| {
452 let n = n.to_string_lossy();
453 n.starts_with(&prefix) && n.ends_with(".bak")
454 })
455 })
456 .filter_map(|path| {
457 let modified = std::fs::metadata(&path).and_then(|m| m.modified()).ok()?;
458 Some((modified, path))
459 })
460 .collect();
461 older.sort_by_key(|(modified, _)| std::cmp::Reverse(*modified));
463 for (_, path) in older.into_iter().skip(keep.saturating_sub(1)) {
464 let _ = std::fs::remove_file(path);
465 }
466}
467
468fn now_secs() -> i64 {
469 SystemTime::now()
470 .duration_since(UNIX_EPOCH)
471 .map(|d| d.as_secs() as i64)
472 .unwrap_or(0)
473}
474
475pub fn validate(migrations: &[Migration]) -> Result<()> {
476 for (index, migration) in migrations.iter().enumerate() {
477 let expected = index as i64 + 1;
478 if migration.version != expected {
479 return Err(DbError::InvalidMigrations(format!(
480 "expected version {expected} at position {index}, found {} ({})",
481 migration.version, migration.name
482 )));
483 }
484 }
485 Ok(())
486}
487
488fn marker_path(db: &Path) -> PathBuf {
489 let mut name = db.as_os_str().to_os_string();
490 name.push(".migrating");
491 PathBuf::from(name)
492}
493
494pub fn pending_marker(db: &Path) -> Option<PendingMarker> {
495 let text = std::fs::read_to_string(marker_path(db)).ok()?;
496 let mut marker = PendingMarker {
497 from: 0,
498 to: 0,
499 pid: 0,
500 };
501 for line in text.lines() {
502 match line.split_once('=')? {
503 ("from", v) => marker.from = v.parse().ok()?,
504 ("to", v) => marker.to = v.parse().ok()?,
505 ("pid", v) => marker.pid = v.parse().ok()?,
506 _ => {}
507 }
508 }
509 Some(marker)
510}
511
512pub fn pending_marker_started_at(db: &Path) -> Option<u64> {
516 pending_marker(db)?;
517 let text = std::fs::read_to_string(marker_path(db)).ok()?;
518 Some(
519 text.lines()
520 .find_map(|line| line.strip_prefix("started="))
521 .and_then(|v| v.parse().ok())
522 .unwrap_or(0),
523 )
524}
525
526pub fn write_pending_marker(db: &Path, from: i64, to: i64) -> Result<()> {
531 let body = format!(
532 "from={from}\nto={to}\npid={}\nstarted={}\n",
533 std::process::id(),
534 now_secs().max(0)
535 );
536 rightkit_fs::atomic_write(&marker_path(db), body.as_bytes())?;
537 Ok(())
538}
539
540pub fn stored_version_is_newer(
544 conn: &Connection,
545 storage: &VersionStorage,
546 migrations: &[Migration],
547) -> Result<bool> {
548 validate(migrations)?;
549 check_storage(storage)?;
550 let max = migrations.last().map_or(0, |m| m.version);
551 Ok(migration_version(conn, storage)? > max)
552}
553
554pub fn migrate(
555 conn: &Connection,
556 db_path: Option<&Path>,
557 migrations: &[Migration],
558 options: &MigrateOptions,
559) -> Result<MigrationReport> {
560 validate(migrations)?;
561 check_storage(&options.version_storage)?;
562 let mut current = migration_version(conn, &options.version_storage)?;
563 let max = migrations.last().map(|m| m.version).unwrap_or(0);
564 let mut report = MigrationReport {
565 from: current,
566 to: current,
567 ..Default::default()
568 };
569 report.interrupted_previous = db_path.and_then(pending_marker);
570
571 if let Some(legacy) = options.legacy_versions.as_ref().filter(|_| current == 0) {
573 let legacy_version = version_row_max(conn, &legacy.table, &legacy.version_column)?;
574 if legacy_version > 0 {
575 if legacy_version <= max {
576 adopt_legacy_version(conn, &options.version_storage, legacy_version)?;
577 }
578 current = legacy_version;
579 report.from = current;
580 report.to = current;
581 }
582 }
583
584 if current > max {
585 if options.forward_compatible.is_some_and(|probe| probe(conn)) {
586 report.forward_compatible = true;
587 return Ok(report);
588 }
589 return Err(DbError::NewerDatabase { current, max });
590 }
591 let pending: Vec<&Migration> = migrations.iter().filter(|m| m.version > current).collect();
592 if pending.is_empty() {
593 return Ok(report);
594 }
595
596 if current > 0 && options.backup {
597 if let Some(path) = db_path {
598 let backup = backup_path(path, options, current, max)?;
599 let backup_options = options
600 .backup_options
601 .clone()
602 .unwrap_or_else(|| DbOptions::new(path));
603 crate::backup::online_backup_sized(&backup_options, conn, &backup, None)?;
604 if let Some(verify) = &options.verify_backup {
605 verify(&backup).map_err(|reason| {
606 DbError::Backup(format!("pre-migration backup failed its check: {reason}"))
607 })?;
608 }
609 if let Some(keep) = options.backup_retention {
610 prune_backups(path, &backup, keep);
611 }
612 report.backup = Some(backup);
613 }
614 }
615
616 if let Some(path) = db_path {
617 write_pending_marker(path, current, max)?;
618 }
619
620 for migration in pending {
621 conn.execute_batch("BEGIN IMMEDIATE;")?;
622 let applied = match &migration.up {
623 Step::Sql(sql) => conn.execute_batch(sql),
624 Step::Code(run) => run(conn),
625 }
626 .and_then(|()| advance_version(conn, &options.version_storage, migration.version));
627 match applied {
628 Ok(()) => {
629 conn.execute_batch("COMMIT;")?;
630 report.applied.push(migration.version);
631 report.to = migration.version;
632 }
633 Err(source) => {
634 let _ = conn.execute_batch("ROLLBACK;");
635 return Err(DbError::MigrationFailed {
636 version: migration.version,
637 name: migration.name,
638 source,
639 });
640 }
641 }
642 }
643
644 if let Some(path) = db_path {
645 let _ = std::fs::remove_file(marker_path(path));
646 }
647 if options.optimize_after {
648 crate::optimize_boundary(conn)?;
649 }
650 Ok(report)
651}
652
653pub fn rollback_to(conn: &Connection, migrations: &[Migration], target: i64) -> Result<Vec<i64>> {
656 rollback_to_with_storage(conn, migrations, target, &VersionStorage::UserVersion)
657}
658
659pub fn rollback_to_with_storage(
661 conn: &Connection,
662 migrations: &[Migration],
663 target: i64,
664 storage: &VersionStorage,
665) -> Result<Vec<i64>> {
666 validate(migrations)?;
667 check_storage(storage)?;
668 if target < 0 {
669 return Err(DbError::Rollback(
670 "target version must be nonnegative".into(),
671 ));
672 }
673 let current = migration_version(conn, storage)?;
674 if current <= target {
675 return Err(DbError::Rollback(format!(
676 "already at v{current}, at or below target v{target}"
677 )));
678 }
679 let mut plan = Vec::new();
680 for version in ((target + 1)..=current).rev() {
681 let migration = migrations
682 .iter()
683 .find(|m| m.version == version)
684 .ok_or_else(|| DbError::Rollback(format!("v{version} is not a known migration")))?;
685 let down = migration.down.ok_or_else(|| {
686 DbError::Rollback(format!(
687 "v{version} ({}) has no down migration",
688 migration.name
689 ))
690 })?;
691 plan.push((version, down));
692 }
693 let mut reverted = Vec::new();
694 for (version, down) in plan {
695 conn.execute_batch("BEGIN IMMEDIATE;")?;
696 let result = conn
697 .execute_batch(down)
698 .and_then(|()| retreat_version(conn, storage, version));
699 match result {
700 Ok(()) => {
701 conn.execute_batch("COMMIT;")?;
702 reverted.push(version);
703 }
704 Err(error) => {
705 let _ = conn.execute_batch("ROLLBACK;");
706 return Err(DbError::Rollback(format!(
707 "down v{version} failed and was rolled back: {error}"
708 )));
709 }
710 }
711 }
712 Ok(reverted)
713}
714
715pub fn column_exists(conn: &Connection, table: &str, column: &str) -> Result<bool> {
718 check_ident(table)?;
719 let mut statement = conn.prepare(&format!("PRAGMA table_xinfo({table})"))?;
720 let names = statement.query_map([], |row| row.get::<_, String>(1))?;
721 for name in names {
722 if name? == column {
723 return Ok(true);
724 }
725 }
726 Ok(false)
727}
728
729pub fn add_column_if_missing(
732 conn: &Connection,
733 table: &str,
734 column: &str,
735 decl: &str,
736) -> Result<()> {
737 check_ident(table)?;
738 check_ident(column)?;
739 if !column_exists(conn, table, column)? {
740 conn.execute_batch(&format!("ALTER TABLE {table} ADD COLUMN {column} {decl};"))?;
741 }
742 Ok(())
743}
744
745fn check_ident(ident: &str) -> Result<()> {
746 if !ident.is_empty()
747 && ident
748 .bytes()
749 .all(|b| b.is_ascii_alphanumeric() || b == b'_')
750 {
751 Ok(())
752 } else {
753 Err(DbError::InvalidMigrations(format!(
754 "unsafe SQL identifier {ident:?}"
755 )))
756 }
757}
758
759#[derive(Debug)]
761pub struct OpenOrDegrade {
762 pub conn: Connection,
764 pub report: MigrationReport,
766 pub degraded: Option<Degraded>,
769}
770
771#[derive(Clone, Debug, PartialEq, Eq)]
773pub struct Degraded {
774 pub reason: String,
776 pub quarantined: Option<PathBuf>,
780 pub quarantine_incomplete: bool,
786 pub in_memory: bool,
788}
789
790impl DbOptions {
791 pub fn open_writer_migrated(
794 &self,
795 migrations: &[Migration],
796 options: &MigrateOptions,
797 ) -> Result<(Connection, MigrationReport)> {
798 let conn = self.open_writer()?;
799 let mut options = options.clone();
800 options.backup_options.get_or_insert_with(|| self.clone());
801 let report = match migrate(&conn, Some(&self.path), migrations, &options) {
802 Ok(report) => report,
803 Err(err) => {
804 let _ = conn.set_db_config(
808 rusqlite::config::DbConfig::SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE,
809 true,
810 );
811 return Err(err);
812 }
813 };
814 Ok((conn, report))
815 }
816
817 pub fn open_writer_migrated_or_degrade(
833 &self,
834 migrations: &[Migration],
835 options: &MigrateOptions,
836 policy: RecoveryPolicy,
837 ) -> Result<OpenOrDegrade> {
838 if let Some(journal) = unfinished_quarantine(&self.path) {
841 let reason = format!(
842 "an earlier quarantine of {} stopped part way and was not undone; see {}",
843 self.path.display(),
844 journal.display()
845 );
846 let refuse = DbError::Fs(rightkit_fs::FsError::io(
847 &journal,
848 std::io::Error::other(reason.clone()),
849 ));
850 return self.degrade_in_memory(migrations, options, policy, reason, None, true, refuse);
851 }
852 let error = match self.open_writer_migrated(migrations, options) {
853 Ok((conn, report)) => {
854 return Ok(OpenOrDegrade {
855 conn,
856 report,
857 degraded: None,
858 })
859 }
860 Err(error) if is_recoverable_damage(&error) => error,
861 Err(error) => return Err(error),
862 };
863 let reason = error.to_string();
864 if !allowed(self, policy) {
865 return self.degrade_in_memory(migrations, options, policy, reason, None, false, error);
866 }
867 let quarantined = match quarantine_database_journaled(
868 &self.path,
869 policy.quarantine_stamp,
870 policy.sidecars,
871 ) {
872 Ok(moved) => moved,
873 Err(failure) => {
874 let reason = format!("{reason}; quarantine failed: {}", failure.error);
877 let incomplete = failure.stranded_at.is_some();
878 return self.degrade_in_memory(
879 migrations,
880 options,
881 policy,
882 reason,
883 failure.stranded_at,
884 incomplete,
885 failure.error,
886 );
887 }
888 };
889 match self.open_writer_migrated(migrations, options) {
890 Ok((conn, report)) => Ok(OpenOrDegrade {
891 conn,
892 report,
893 degraded: Some(Degraded {
894 reason,
895 quarantined: Some(quarantined),
896 quarantine_incomplete: false,
897 in_memory: false,
898 }),
899 }),
900 Err(second) => {
901 let reason = format!("{reason}; reopen after quarantine failed: {second}");
902 self.degrade_in_memory(
903 migrations,
904 options,
905 policy,
906 reason,
907 Some(quarantined),
908 false,
909 second,
910 )
911 }
912 }
913 }
914
915 #[allow(clippy::too_many_arguments)]
916 fn degrade_in_memory(
917 &self,
918 migrations: &[Migration],
919 options: &MigrateOptions,
920 policy: RecoveryPolicy,
921 reason: String,
922 quarantined: Option<PathBuf>,
923 quarantine_incomplete: bool,
924 refuse: DbError,
925 ) -> Result<OpenOrDegrade> {
926 if !policy.allow_in_memory_fallback {
929 return Err(refuse);
930 }
931 let conn = self.open_in_memory()?;
932 let report = migrate(&conn, None, migrations, options)?;
933 Ok(OpenOrDegrade {
934 conn,
935 report,
936 degraded: Some(Degraded {
937 reason,
938 quarantined,
939 quarantine_incomplete,
940 in_memory: true,
941 }),
942 })
943 }
944}
945
946fn is_recoverable_damage(error: &DbError) -> bool {
949 match error {
950 DbError::MigrationFailed {
951 source: rusqlite::Error::SqliteFailure(failure, _),
952 ..
953 } => matches!(
954 failure.code,
955 ErrorCode::NotADatabase | ErrorCode::DatabaseCorrupt
956 ),
957 other => is_unreadable(other),
958 }
959}