pub mod format;
pub mod manifest;
pub mod snapshot;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub use format::{GraphHeader, MAGIC_BYTES, VERSION};
pub use manifest::{
BuildProvenance, ConfigProvenance, ConfigProvenanceBuilder, MANIFEST_SCHEMA_VERSION, Manifest,
OverrideEntry, OverrideSource, SNAPSHOT_FORMAT_VERSION, compute_config_checksum,
default_provenance,
};
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)
}
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"
);
}
}