use std::path::Path;
use serde::Serialize;
use crate::{Database, Error, Result};
pub(crate) const SNAPSHOT_KEY: &str = "snapshot_created_at";
pub(crate) const LEVELS_KEY: &str = "fingerprint_levels";
#[derive(Debug, Clone, Default)]
pub struct SnapshotOptions {
pub ecosystems: Vec<String>,
pub latest_only: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct SnapshotSummary {
pub packages: usize,
pub versions: usize,
pub functions: usize,
pub bytes: u64,
pub ecosystems: Vec<String>,
pub meta: std::collections::BTreeMap<String, String>,
}
impl Database {
pub fn snapshot(
&self,
out: impl AsRef<Path>,
options: &SnapshotOptions,
) -> Result<SnapshotSummary> {
let out = out.as_ref();
let levels = self.fingerprint_levels()?;
self.conn
.execute("VACUUM INTO ?1", [out.to_string_lossy().as_ref()])?;
let mut copy = Database::open_unchecked(out)?;
copy.select(options)?;
copy.clean()?;
copy.rebuild_hash_stats()?;
copy.conn.execute_batch(
"DELETE FROM minhash;
DELETE FROM stats_pending_exact;
DELETE FROM stats_pending_fingerprint;",
)?;
copy.conn.execute(
"INSERT INTO meta (key, value) VALUES (?1, ?2), (?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
ON CONFLICT (key) DO UPDATE SET value = excluded.value",
[LEVELS_KEY, levels.as_str(), SNAPSHOT_KEY],
)?;
let stats = copy.stats()?;
let ecosystems = copy.ecosystems()?;
let meta = copy.meta()?;
copy.vacuum()?;
copy.conn.pragma_update(None, "journal_mode", "DELETE")?;
drop(copy);
Ok(SnapshotSummary {
packages: stats.packages,
versions: stats.versions,
functions: stats.functions,
bytes: std::fs::metadata(out).map(|m| m.len()).unwrap_or(0),
ecosystems,
meta,
})
}
pub fn is_snapshot(&self) -> Result<bool> {
Ok(self.meta()?.contains_key(SNAPSHOT_KEY))
}
pub(crate) fn refuse_snapshot(&self) -> Result<()> {
if self.is_snapshot()? {
return Err(Error::Snapshot);
}
Ok(())
}
fn fingerprint_levels(&self) -> Result<String> {
if let Some(levels) = self.meta()?.remove(LEVELS_KEY) {
return Ok(levels);
}
let mut stmt = self
.conn
.prepare("SELECT DISTINCT level FROM minhash ORDER BY level")?;
let levels: Vec<String> = stmt
.query_map([], |r| r.get::<_, i64>(0))?
.map(|l| l.map(|l| l.to_string()))
.collect::<rusqlite::Result<_>>()?;
Ok(levels.join(","))
}
fn select(&mut self, options: &SnapshotOptions) -> Result<()> {
let tx = self.conn.transaction()?;
tx.execute_batch(
"DROP TABLE IF EXISTS temp.keep_version;
CREATE TEMP TABLE keep_version (id INTEGER PRIMARY KEY);
DROP TABLE IF EXISTS temp.keep_ecosystem;
CREATE TEMP TABLE keep_ecosystem (name TEXT PRIMARY KEY);",
)?;
for ecosystem in &options.ecosystems {
tx.execute(
"INSERT OR IGNORE INTO temp.keep_ecosystem (name) VALUES (?1)",
[ecosystem],
)?;
}
let latest = if options.latest_only {
"AND pv.id = (SELECT v.id FROM package_version v
WHERE v.package_id = pv.package_id AND v.status = 'indexed'
ORDER BY v.published_at IS NULL, v.published_at DESC,
v.crawled_at DESC, v.id DESC
LIMIT 1)"
} else {
""
};
let sql = format!(
"INSERT INTO temp.keep_version (id)
SELECT pv.id FROM package_version pv
JOIN package p ON p.id = pv.package_id
JOIN ecosystem e ON e.id = p.ecosystem_id
WHERE pv.status = 'indexed'
AND (NOT EXISTS (SELECT 1 FROM temp.keep_ecosystem)
OR e.name IN (SELECT name FROM temp.keep_ecosystem))
{latest}"
);
tx.execute(&sql, [])?;
tx.execute_batch(
"DELETE FROM version_file
WHERE package_version_id NOT IN (SELECT id FROM temp.keep_version);
DELETE FROM package_version WHERE id NOT IN (SELECT id FROM temp.keep_version);
DELETE FROM package WHERE id NOT IN (SELECT package_id FROM package_version);
DELETE FROM ecosystem WHERE id NOT IN (SELECT ecosystem_id FROM package);
DROP TABLE temp.keep_version;
DROP TABLE temp.keep_ecosystem;",
)?;
tx.commit()?;
Ok(())
}
}