Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
use crate::skv::{
connection::{ConnectionIncident, ConnectionIncidents, ConnectionMaintenanceTask},
maintenance::{Maintenance, MaintenanceError},
};
/// Checks an SQLite database for corruption.
#[derive(Debug)]
pub struct Checker {
checks: Checks,
}
impl ConnectionMaintenanceTask for Checker {
type Error = MaintenanceError;
fn run(self, conn: &mut rusqlite::Connection) -> Result<(), Self::Error> {
let maintenance = Maintenance::new(conn);
if self.checks.reindex {
maintenance.reindex()?;
}
match self.checks.consistency {
Some(ConsistencyCheck::Quick) => maintenance.quick_check()?,
Some(ConsistencyCheck::Full) => maintenance.integrity_check()?,
None => (),
}
if self.checks.foreign_keys {
maintenance.foreign_key_check()?;
}
Ok(())
}
}
/// Determines whether to check an SQLite database for corruption,
/// and which checks to run.
pub trait IntoChecker<C> {
fn into_checker(self) -> CheckerAction<C>;
}
impl IntoChecker<Checker> for ConnectionIncidents<'_> {
fn into_checker(self) -> CheckerAction<Checker> {
self.map(|incidents| {
let (penalty, checks) = incidents.iter().fold(
(Penalty::default(), Checks::default()),
|(penalty, checks), incident| (penalty.adding(incident), checks.adding(incident)),
);
if penalty < Penalty::MIN {
// No incidents, or maybe some transient errors.
// Keep any existing incidents, and skip checks for now.
CheckerAction::Skip
} else if penalty < Penalty::MAX {
// Check the database for potential corruption.
incidents.resolve();
CheckerAction::Check(Checker { checks })
} else {
// Too many incidents; replace the database.
incidents.resolve();
CheckerAction::Replace
}
})
}
}
/// Whether to skip checking, check, or replace a corrupt SQLite database.
#[derive(Debug)]
pub enum CheckerAction<C> {
Skip,
Check(C),
Replace,
}
/// The penalty for one or more [`ConnectionIncidents`].
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct Penalty(usize);
impl Penalty {
/// The minimum penalty needed to check a database for errors.
const MIN: Self = Penalty(5);
/// The maximum penalty before replacing a database.
const MAX: Self = Penalty(20);
fn adding(self, incident: ConnectionIncident) -> Self {
// Each incident contributes a (completely arbitrary) amount to
// the penalty, depending on the severity and frequency.
Self(
self.0
+ match incident {
ConnectionIncident::CorruptFile => 3,
ConnectionIncident::CorruptIndex => 2,
ConnectionIncident::CorruptForeignKey => 1,
},
)
}
}
const _: () = {
assert!(
Penalty::MIN.0 < Penalty::MAX.0,
"`Penalty::MIN` should be less than `Penalty::MAX`"
);
};
/// Checks to run on a potentially corrupt SQLite database.
#[derive(Clone, Copy, Debug, Default)]
struct Checks {
reindex: bool,
consistency: Option<ConsistencyCheck>,
foreign_keys: bool,
}
impl Checks {
fn adding(self, incident: ConnectionIncident) -> Self {
match incident {
ConnectionIncident::CorruptFile => Self {
consistency: Some(
// If we haven't seen index corruption, a quick check will
// likely find other problems faster than a full check.
self.consistency
.unwrap_or(ConsistencyCheck::Quick)
.and(ConsistencyCheck::Quick),
),
..self
},
ConnectionIncident::CorruptIndex => Self {
// Try to rebuild the indexes.
reindex: true,
consistency: Some(
// If we have seen index corruption, we need to run a
// full check.
self.consistency
.unwrap_or(ConsistencyCheck::Full)
.and(ConsistencyCheck::Full),
),
..self
},
ConnectionIncident::CorruptForeignKey => Self {
// Neither quick nor full checks look for foreign key errors.
foreign_keys: true,
..self
},
}
}
}
#[derive(Clone, Copy, Debug)]
enum ConsistencyCheck {
Quick,
Full,
}
impl ConsistencyCheck {
fn and(self, other: Self) -> Self {
match (self, other) {
(Self::Quick, Self::Quick) => Self::Quick,
// A full consistency check subsumes a quick check.
_ => Self::Full,
}
}
}