mangle_db/
file_backend.rs1use std::path::PathBuf;
22
23use anyhow::Result;
24
25use crate::backend::{CacheMeta, IdbBackend, IdbSnapshot};
26use crate::simplerow;
27
28pub struct FileIdbBackend {
30 dir: PathBuf,
31}
32
33impl FileIdbBackend {
34 pub fn new(dir: impl Into<PathBuf>) -> Self {
35 Self { dir: dir.into() }
36 }
37
38 fn meta_path(&self, db_name: &str) -> PathBuf {
39 self.dir.join(format!("{db_name}.meta.json"))
40 }
41
42 fn idb_path(&self, db_name: &str) -> PathBuf {
43 self.dir.join(format!("{db_name}.idb.mgr"))
44 }
45}
46
47impl IdbBackend for FileIdbBackend {
48 fn load(&self, db_name: &str) -> Result<Option<(CacheMeta, IdbSnapshot)>> {
49 let meta_path = self.meta_path(db_name);
50 let idb_path = self.idb_path(db_name);
51
52 if !meta_path.exists() || !idb_path.exists() {
53 return Ok(None);
54 }
55
56 let meta_json = std::fs::read_to_string(&meta_path)?;
57 let meta: CacheMeta = serde_json::from_str(&meta_json)?;
58
59 let idb_data = std::fs::read(&idb_path)?;
60 let sr_data = simplerow::read_from_bytes(&idb_data)?;
61
62 let relations: Vec<_> = sr_data.tables.into_iter().collect();
63 let snapshot = IdbSnapshot { relations };
64
65 Ok(Some((meta, snapshot)))
66 }
67
68 fn save(&self, db_name: &str, meta: &CacheMeta, snapshot: &IdbSnapshot) -> Result<()> {
69 std::fs::create_dir_all(&self.dir)?;
70
71 let meta_json = serde_json::to_string_pretty(meta)?;
72 std::fs::write(self.meta_path(db_name), meta_json)?;
73
74 let mut file = std::fs::File::create(self.idb_path(db_name))?;
75 let tables: Vec<_> = snapshot
76 .relations
77 .iter()
78 .map(|(name, facts)| (name.clone(), facts.clone()))
79 .collect();
80 simplerow::write_simple_row(&mut file, &tables)?;
81
82 Ok(())
83 }
84
85 fn invalidate(&self, db_name: &str) -> Result<()> {
86 let meta_path = self.meta_path(db_name);
87 let idb_path = self.idb_path(db_name);
88
89 if meta_path.exists() {
90 std::fs::remove_file(&meta_path)?;
91 }
92 if idb_path.exists() {
93 std::fs::remove_file(&idb_path)?;
94 }
95
96 Ok(())
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use mangle_common::Value;
104
105 #[test]
106 fn test_file_idb_backend_round_trip() -> Result<()> {
107 let dir = tempfile::tempdir()?;
108 let backend = FileIdbBackend::new(dir.path());
109
110 assert!(backend.load("test")?.is_none());
112
113 let meta = CacheMeta {
114 program_hash: [0xAB; 32],
115 edb_fingerprint: vec![0xCD; 32],
116 created_at: 1234567890,
117 };
118
119 let snapshot = IdbSnapshot {
120 relations: vec![(
121 "derived".to_string(),
122 vec![
123 vec![Value::Number(1), Value::Number(2)],
124 vec![Value::Number(3), Value::Number(4)],
125 ],
126 )],
127 };
128
129 backend.save("test", &meta, &snapshot)?;
130
131 let (loaded_meta, loaded_snapshot) = backend.load("test")?.expect("should exist");
132 assert_eq!(loaded_meta.program_hash, meta.program_hash);
133 assert_eq!(loaded_meta.edb_fingerprint, meta.edb_fingerprint);
134 assert_eq!(loaded_meta.created_at, meta.created_at);
135 assert_eq!(loaded_snapshot.relations.len(), 1);
136 assert_eq!(loaded_snapshot.relations[0].0, "derived");
137 assert_eq!(loaded_snapshot.relations[0].1.len(), 2);
138
139 backend.invalidate("test")?;
141 assert!(backend.load("test")?.is_none());
142
143 Ok(())
144 }
145}