pub mod format;
pub mod manifest;
pub mod snapshot;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub use format::{
FormatVersion, GraphHeader, MAGIC_BYTES, MAGIC_BYTES_V7, MAGIC_BYTES_V8, MAGIC_BYTES_V9,
MAGIC_BYTES_V10, VERSION,
};
pub use manifest::{
BuildProvenance, ConfigProvenance, ConfigProvenanceBuilder, MANIFEST_SCHEMA_VERSION, Manifest,
ManifestCheck, OverrideEntry, OverrideSource, PluginSelectionManifest, SNAPSHOT_FORMAT_VERSION,
compute_config_checksum, default_provenance, try_load_manifest,
};
pub use snapshot::{
PersistenceError, check_config_drift, load_from_bytes, load_from_path, load_header_from_path,
save_to_path, save_to_path_with_provenance, validate_snapshot, verify_snapshot_bytes,
};
const GRAPH_DIR_NAME: &str = ".sqry/graph";
const ANALYSIS_DIR_NAME: &str = ".sqry/analysis";
const MANIFEST_FILE_NAME: &str = "manifest.json";
const SNAPSHOT_FILE_NAME: &str = "snapshot.sqry";
#[derive(Debug, Clone)]
pub struct GraphStorage {
graph_dir: PathBuf,
analysis_dir: PathBuf,
manifest_path: PathBuf,
snapshot_path: PathBuf,
}
impl GraphStorage {
#[must_use]
pub fn new(root_path: &Path) -> Self {
let graph_dir = root_path.join(GRAPH_DIR_NAME);
let analysis_dir = root_path.join(ANALYSIS_DIR_NAME);
Self {
manifest_path: graph_dir.join(MANIFEST_FILE_NAME),
snapshot_path: graph_dir.join(SNAPSHOT_FILE_NAME),
graph_dir,
analysis_dir,
}
}
#[must_use]
pub fn graph_dir(&self) -> &Path {
&self.graph_dir
}
#[must_use]
pub fn analysis_dir(&self) -> &Path {
&self.analysis_dir
}
#[must_use]
pub fn analysis_scc_path(&self, edge_kind: &str) -> PathBuf {
self.analysis_dir.join(format!("scc_{edge_kind}.scc"))
}
#[must_use]
pub fn analysis_cond_path(&self, edge_kind: &str) -> PathBuf {
self.analysis_dir.join(format!("cond_{edge_kind}.dag"))
}
#[must_use]
pub fn analysis_csr_path(&self) -> PathBuf {
self.analysis_dir.join("adjacency.csr")
}
#[must_use]
pub fn manifest_path(&self) -> &Path {
&self.manifest_path
}
#[must_use]
pub fn snapshot_path(&self) -> &Path {
&self.snapshot_path
}
#[must_use]
pub fn exists(&self) -> bool {
self.manifest_path.exists()
}
#[must_use]
pub fn snapshot_exists(&self) -> bool {
self.snapshot_path.exists()
}
pub fn load_manifest(&self) -> std::io::Result<Manifest> {
Manifest::load(&self.manifest_path)
}
#[must_use]
pub fn try_load_manifest(&self) -> ManifestCheck {
manifest::try_load_manifest(&self.manifest_path)
}
pub fn snapshot_age(&self, manifest: &Manifest) -> std::io::Result<Duration> {
let built_at = chrono::DateTime::parse_from_rfc3339(&manifest.built_at)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let now = chrono::Utc::now();
let duration = now.signed_duration_since(built_at.with_timezone(&chrono::Utc));
let seconds = duration.num_seconds().max(0);
let seconds = u64::try_from(seconds).unwrap_or(0);
Ok(Duration::from_secs(seconds))
}
#[must_use]
pub fn config_dir(&self) -> PathBuf {
self.graph_dir.join("config")
}
#[must_use]
pub fn config_path(&self) -> PathBuf {
self.config_dir().join("config.json")
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_graph_storage_paths() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
assert_eq!(storage.graph_dir(), tmp.path().join(".sqry/graph"));
assert_eq!(
storage.manifest_path(),
tmp.path().join(".sqry/graph/manifest.json")
);
assert_eq!(
storage.snapshot_path(),
tmp.path().join(".sqry/graph/snapshot.sqry")
);
assert!(!storage.exists());
assert!(!storage.snapshot_exists());
}
#[test]
fn test_graph_storage_exists() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
assert!(!storage.exists());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
std::fs::write(storage.manifest_path(), "{}").unwrap();
assert!(storage.exists());
}
#[test]
fn test_manifest_roundtrip() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
let provenance = BuildProvenance::new("0.15.0", "sqry index");
let manifest = Manifest::new("/test/path", 100, 200, "abc123", provenance);
manifest.save(storage.manifest_path()).unwrap();
let loaded = storage.load_manifest().unwrap();
assert_eq!(loaded.node_count, 100);
assert_eq!(loaded.edge_count, 200);
assert_eq!(loaded.snapshot_sha256, "abc123");
assert_eq!(loaded.build_provenance.sqry_version, "0.15.0");
}
#[test]
fn test_snapshot_age() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
let provenance = BuildProvenance::new("0.15.0", "sqry index");
let manifest = Manifest::new("/test/path", 100, 200, "abc123", provenance);
let age = storage.snapshot_age(&manifest).unwrap();
assert!(age.as_secs() < 2, "Age should be less than 2 seconds");
}
#[test]
fn test_reader_readiness_snapshot_without_manifest() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
std::fs::write(storage.snapshot_path(), b"fake snapshot data").unwrap();
assert!(storage.snapshot_exists(), "Snapshot file should exist");
assert!(
!storage.exists(),
"Index should NOT be ready without manifest (manifest-last ordering)"
);
}
#[test]
fn test_reader_readiness_manifest_without_snapshot() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
let provenance = BuildProvenance::new("3.6.0", "test");
let manifest = Manifest::new(
tmp.path().display().to_string(),
100,
200,
"sha256",
provenance,
);
manifest.save(storage.manifest_path()).unwrap();
assert!(
storage.exists(),
"Index should report exists (manifest present)"
);
assert!(!storage.snapshot_exists(), "Snapshot should not exist");
let result = load_from_path(storage.snapshot_path(), None);
assert!(
result.is_err(),
"Loading from missing snapshot should return error, not panic"
);
}
#[test]
fn test_try_load_manifest_missing_returns_missing() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
let result = storage.try_load_manifest();
assert!(
result.is_missing(),
"Missing manifest file should return ManifestCheck::Missing, not Err"
);
assert!(!result.is_present());
assert!(!result.is_corrupt());
}
#[test]
fn test_try_load_manifest_removed_after_creation() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
let provenance = BuildProvenance::new("0.15.0", "sqry index");
let manifest = Manifest::new("/test/path", 10, 20, "sha_initial", provenance);
manifest.save(storage.manifest_path()).unwrap();
assert!(storage.try_load_manifest().is_present());
std::fs::remove_file(storage.manifest_path()).unwrap();
let result = storage.try_load_manifest();
assert!(
result.is_missing(),
"Freshness check on removed manifest must return Missing, not Err/Corrupt"
);
}
#[test]
fn test_try_load_manifest_corrupt_returns_corrupt() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
std::fs::write(storage.manifest_path(), b"not valid json {{{{").unwrap();
let result = storage.try_load_manifest();
assert!(
result.is_corrupt(),
"Invalid JSON in manifest should return ManifestCheck::Corrupt"
);
assert!(!result.is_present());
assert!(!result.is_missing());
}
#[test]
fn test_try_load_manifest_valid_returns_present() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
let provenance = BuildProvenance::new("9.0.0", "sqry index");
let original = Manifest::new("/workspace/root", 42, 99, "sha256_test", provenance);
original.save(storage.manifest_path()).unwrap();
match storage.try_load_manifest() {
ManifestCheck::Present(m) => {
assert_eq!(m.node_count, 42);
assert_eq!(m.edge_count, 99);
assert_eq!(m.snapshot_sha256, "sha256_test");
assert_eq!(m.root_path, "/workspace/root");
}
ManifestCheck::Missing => panic!("Expected Present, got Missing"),
ManifestCheck::Corrupt(e) => panic!("Expected Present, got Corrupt: {e}"),
}
}
#[test]
fn test_manifest_check_into_manifest() {
let tmp = TempDir::new().unwrap();
let storage = GraphStorage::new(tmp.path());
let missing = storage.try_load_manifest();
assert!(missing.into_manifest().is_none());
std::fs::create_dir_all(storage.graph_dir()).unwrap();
std::fs::write(storage.manifest_path(), b"bad json").unwrap();
let corrupt = storage.try_load_manifest();
assert!(corrupt.into_manifest().is_none());
let provenance = BuildProvenance::new("9.0.0", "sqry index");
let manifest = Manifest::new("/path", 1, 2, "sha", provenance);
manifest.save(storage.manifest_path()).unwrap();
let present = storage.try_load_manifest();
assert!(present.into_manifest().is_some());
}
}