use std::{collections::BTreeSet, path::Path};
use anyhow::{Context, Result};
use rusqlite::{Connection, OpenFlags, OptionalExtension, named_params};
use serde::Serialize;
pub struct PimdirDb {
conn: Connection,
}
#[derive(Clone, Copy, Debug, Default, Serialize)]
pub struct PimdirObjectStats {
pub count: u64,
pub bytes: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PimdirRefcountDrift {
pub hash: String,
pub stored: i64,
pub expected: i64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PimdirDangling {
pub kind: &'static str,
pub row: String,
pub target: String,
}
impl PimdirDb {
pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
let dir = dir.as_ref();
let flags = OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX;
let conn = Connection::open_with_flags(dir.join("pimdir.db"), flags)
.with_context(|| format!("cannot read the index at {}", dir.display()))?;
conn.execute_batch("PRAGMA busy_timeout = 30000;")?;
Ok(Self { conn })
}
pub fn version(&self) -> Result<i64> {
Ok(self
.conn
.pragma_query_value(None, "user_version", |r| r.get(0))?)
}
pub fn object_stats(&self) -> Result<PimdirObjectStats> {
let (count, bytes) = self.conn.query_row(
"SELECT count(*), coalesce(sum(size), 0) FROM objects",
[],
|r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
)?;
Ok(PimdirObjectStats {
count: count.max(0) as u64,
bytes: bytes.max(0) as u64,
})
}
pub fn live_bytes(&self) -> Result<u64> {
let bytes: i64 = self.conn.query_row(
"SELECT coalesce(sum(size), 0) FROM objects WHERE hash IN \
(SELECT object_hash FROM items \
WHERE object_hash IS NOT NULL AND retained_at IS NULL)",
[],
|r| r.get(0),
)?;
Ok(bytes.max(0) as u64)
}
pub fn object_size(&self, hash: &str) -> Result<Option<u64>> {
let size: Option<i64> = self
.conn
.query_row(
"SELECT size FROM objects WHERE hash = :hash",
named_params! { ":hash": hash },
|r| r.get(0),
)
.optional()?;
Ok(size.map(|size| size.max(0) as u64))
}
pub fn retained_before(&self, cutoff: &str) -> Result<(u64, u64)> {
let (count, bytes) = self.conn.query_row(
"SELECT count(*), coalesce(sum(o.size), 0) FROM items i \
LEFT JOIN objects o ON o.hash = i.object_hash \
WHERE i.retained_at IS NOT NULL AND i.retained_at < :cutoff",
named_params! { ":cutoff": cutoff },
|r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
)?;
Ok((count.max(0) as u64, bytes.max(0) as u64))
}
pub fn hashes(&self) -> Result<BTreeSet<String>> {
let mut stmt = self.conn.prepare("SELECT hash FROM objects")?;
let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
let mut hashes = BTreeSet::new();
for row in rows {
hashes.insert(row?);
}
Ok(hashes)
}
pub fn refcount_drift(&self) -> Result<Vec<PimdirRefcountDrift>> {
let mut stmt = self.conn.prepare(
"WITH refs(hash) AS ( \
SELECT object_hash FROM items WHERE object_hash IS NOT NULL \
UNION ALL SELECT conflict_object FROM items WHERE conflict_object IS NOT NULL \
UNION ALL SELECT base_object FROM bindings WHERE base_object IS NOT NULL \
UNION ALL SELECT object_hash FROM queue WHERE object_hash IS NOT NULL \
), counted(hash, n) AS (SELECT hash, count(*) FROM refs GROUP BY hash) \
SELECT o.hash, o.refcount, coalesce(c.n, 0) FROM objects o \
LEFT JOIN counted c ON c.hash = o.hash \
WHERE o.refcount != coalesce(c.n, 0) ORDER BY o.hash",
)?;
let rows = stmt.query_map([], |r| {
Ok(PimdirRefcountDrift {
hash: r.get(0)?,
stored: r.get(1)?,
expected: r.get(2)?,
})
})?;
let mut drifts = Vec::new();
for row in rows {
drifts.push(row?);
}
Ok(drifts)
}
pub fn dangling(&self) -> Result<Vec<PimdirDangling>> {
let mut dangling = Vec::new();
let mut stmt = self.conn.prepare(
"SELECT b.collection, b.link_id, b.source FROM bindings b \
WHERE NOT EXISTS (SELECT 1 FROM items i \
WHERE i.collection = b.collection AND i.link_id = b.link_id) \
ORDER BY b.collection, b.link_id, b.source",
)?;
let rows = stmt.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?;
for row in rows {
let (collection, link, source) = row?;
dangling.push(PimdirDangling {
kind: "binding",
row: format!("{collection}/{link} @{source}"),
target: format!("item {collection}/{link}"),
});
}
let mut stmt = self.conn.prepare(
"SELECT collection, link_id, object_hash FROM items \
WHERE object_hash IS NOT NULL \
AND object_hash NOT IN (SELECT hash FROM objects) \
ORDER BY collection, link_id",
)?;
let rows = stmt.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?;
for row in rows {
let (collection, link, hash) = row?;
dangling.push(PimdirDangling {
kind: "item-object",
row: format!("{collection}/{link}"),
target: format!("object {hash}"),
});
}
let mut stmt = self.conn.prepare(
"SELECT id, collection, object_hash FROM queue \
WHERE object_hash IS NOT NULL \
AND object_hash NOT IN (SELECT hash FROM objects) ORDER BY id",
)?;
let rows = stmt.query_map([], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?;
for row in rows {
let (id, collection, hash) = row?;
dangling.push(PimdirDangling {
kind: "queue-object",
row: format!("queue {id} ({collection})"),
target: format!("object {hash}"),
});
}
Ok(dangling)
}
}