use rusqlite::Connection;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BlobRefcountDrift {
pub unowned: u64,
pub unowned_bytes: u64,
pub too_high: u64,
pub too_high_bytes: u64,
pub too_low: u64,
pub too_low_bytes: u64,
pub unindexed: u64,
}
impl BlobRefcountDrift {
pub fn mismatched(&self) -> u64 {
self.unowned
.saturating_add(self.too_high)
.saturating_add(self.too_low)
.saturating_add(self.unindexed)
}
pub fn is_clean(&self) -> bool {
self.mismatched() == 0
}
}
const BLOB_SIDE: &str = "
SELECT
COALESCE(SUM(m.refs IS NULL), 0),
COALESCE(SUM(CASE WHEN m.refs IS NULL THEN b.size END), 0),
COALESCE(SUM(b.refcount > m.refs), 0),
COALESCE(SUM(CASE WHEN b.refcount > m.refs THEN b.size END), 0),
COALESCE(SUM(b.refcount < m.refs), 0),
COALESCE(SUM(CASE WHEN b.refcount < m.refs THEN b.size END), 0)
FROM blobs b
LEFT JOIN (SELECT hash, SUM(refs) AS refs FROM entry_blobs GROUP BY hash) m
ON m.hash = b.hash";
const MAPPING_SIDE: &str = "
SELECT COUNT(DISTINCT hash) FROM entry_blobs
WHERE hash NOT IN (SELECT hash FROM blobs)";
pub fn blob_refcount_drift(db: &Connection) -> rusqlite::Result<BlobRefcountDrift> {
let count = |row: &rusqlite::Row<'_>, column: usize| -> rusqlite::Result<u64> {
row.get::<_, i64>(column).map(|value| value.max(0) as u64)
};
let mut drift = db.query_row(BLOB_SIDE, [], |row| {
Ok(BlobRefcountDrift {
unowned: count(row, 0)?,
unowned_bytes: count(row, 1)?,
too_high: count(row, 2)?,
too_high_bytes: count(row, 3)?,
too_low: count(row, 4)?,
too_low_bytes: count(row, 5)?,
unindexed: 0,
})
})?;
drift.unindexed = db.query_row(MAPPING_SIDE, [], |row| count(row, 0))?;
Ok(drift)
}
#[cfg(test)]
mod tests {
use super::*;
fn index() -> Connection {
let db = Connection::open_in_memory().unwrap();
db.execute_batch(
"CREATE TABLE blobs (hash TEXT PRIMARY KEY, size INTEGER NOT NULL, refcount INTEGER NOT NULL);
CREATE TABLE entry_blobs (cache_key TEXT NOT NULL, hash TEXT NOT NULL, refs INTEGER NOT NULL,
PRIMARY KEY (cache_key, hash));
INSERT INTO blobs VALUES ('exact', 10, 3), ('single', 20, 1);
INSERT INTO entry_blobs VALUES ('k1', 'exact', 2), ('k2', 'exact', 1), ('k1', 'single', 1);",
)
.unwrap();
db
}
#[test]
fn empty_and_consistent_tables_report_no_drift() {
let db = index();
let drift = blob_refcount_drift(&db).unwrap();
assert_eq!(drift, BlobRefcountDrift::default());
assert!(drift.is_clean());
assert_eq!(drift.mismatched(), 0);
db.execute_batch("DELETE FROM blobs; DELETE FROM entry_blobs;")
.unwrap();
assert_eq!(
blob_refcount_drift(&db).unwrap(),
BlobRefcountDrift::default()
);
}
#[test]
fn each_drift_class_is_counted_with_its_bytes() {
let db = index();
db.execute_batch(
"INSERT INTO blobs VALUES ('unowned_a', 100, 1), ('unowned_b', 1000, 0), ('unowned_c', 10000, 7);
INSERT INTO blobs VALUES ('high_a', 3, 2), ('high_b', 30, 9);
INSERT INTO entry_blobs VALUES ('k1', 'high_a', 1), ('k1', 'high_b', 4), ('k2', 'high_b', 4);
INSERT INTO blobs VALUES ('low', 7, 1);
INSERT INTO entry_blobs VALUES ('k1', 'low', 1), ('k2', 'low', 1);
INSERT INTO entry_blobs VALUES ('k1', 'gone', 1), ('k2', 'gone', 1), ('k3', 'gone_too', 1);",
)
.unwrap();
let drift = blob_refcount_drift(&db).unwrap();
assert_eq!(
drift,
BlobRefcountDrift {
unowned: 3,
unowned_bytes: 11_100,
too_high: 2,
too_high_bytes: 33,
too_low: 1,
too_low_bytes: 7,
unindexed: 2,
}
);
assert_eq!(drift.mismatched(), 8);
assert!(!drift.is_clean());
}
#[test]
fn every_class_alone_makes_the_probe_dirty() {
let one = |field: fn(&mut BlobRefcountDrift)| {
let mut drift = BlobRefcountDrift::default();
field(&mut drift);
drift
};
for drift in [
one(|d| d.unowned = 1),
one(|d| d.too_high = 1),
one(|d| d.too_low = 1),
one(|d| d.unindexed = 1),
] {
assert_eq!(drift.mismatched(), 1, "{drift:?}");
assert!(!drift.is_clean(), "{drift:?}");
}
assert!(one(|d| d.unowned_bytes = 5).is_clean());
let all = BlobRefcountDrift {
unowned: 1,
too_high: 2,
too_low: 4,
unindexed: 8,
..Default::default()
};
assert_eq!(all.mismatched(), 15);
}
#[test]
fn negative_sums_clamp_to_zero() {
let db = index();
db.execute_batch("INSERT INTO blobs VALUES ('negative', -5, 1);")
.unwrap();
let drift = blob_refcount_drift(&db).unwrap();
assert_eq!(drift.unowned, 1);
assert_eq!(drift.unowned_bytes, 0);
}
}