sqry_core/graph/unified/persistence/
format.rs1use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8
9use super::manifest::ConfigProvenance;
10
11pub const MAGIC_BYTES: &[u8; 13] = b"SQRY_GRAPH_V7";
22
23pub const VERSION: u32 = 7;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct GraphHeader {
32 pub version: u32,
34
35 pub node_count: usize,
37
38 pub edge_count: usize,
40
41 pub string_count: usize,
43
44 pub file_count: usize,
46
47 pub timestamp: u64,
49
50 #[serde(default)]
52 pub config_provenance: Option<ConfigProvenance>,
53
54 #[serde(default)]
59 pub plugin_versions: HashMap<String, String>,
60}
61
62impl GraphHeader {
63 #[must_use]
65 pub fn new(
66 node_count: usize,
67 edge_count: usize,
68 string_count: usize,
69 file_count: usize,
70 ) -> Self {
71 Self {
72 version: VERSION,
73 node_count,
74 edge_count,
75 string_count,
76 file_count,
77 timestamp: std::time::SystemTime::now()
78 .duration_since(std::time::UNIX_EPOCH)
79 .unwrap_or_default()
80 .as_secs(),
81 config_provenance: None,
82 plugin_versions: HashMap::new(),
83 }
84 }
85
86 #[must_use]
88 pub fn with_provenance(
89 node_count: usize,
90 edge_count: usize,
91 string_count: usize,
92 file_count: usize,
93 provenance: ConfigProvenance,
94 ) -> Self {
95 Self {
96 version: VERSION,
97 node_count,
98 edge_count,
99 string_count,
100 file_count,
101 timestamp: std::time::SystemTime::now()
102 .duration_since(std::time::UNIX_EPOCH)
103 .unwrap_or_default()
104 .as_secs(),
105 config_provenance: Some(provenance),
106 plugin_versions: HashMap::new(),
107 }
108 }
109
110 #[must_use]
112 pub fn with_provenance_and_plugins(
113 node_count: usize,
114 edge_count: usize,
115 string_count: usize,
116 file_count: usize,
117 provenance: ConfigProvenance,
118 plugin_versions: HashMap<String, String>,
119 ) -> Self {
120 Self {
121 version: VERSION,
122 node_count,
123 edge_count,
124 string_count,
125 file_count,
126 timestamp: std::time::SystemTime::now()
127 .duration_since(std::time::UNIX_EPOCH)
128 .unwrap_or_default()
129 .as_secs(),
130 config_provenance: Some(provenance),
131 plugin_versions,
132 }
133 }
134
135 #[must_use]
137 pub fn provenance(&self) -> Option<&ConfigProvenance> {
138 self.config_provenance.as_ref()
139 }
140
141 #[must_use]
143 pub fn has_provenance(&self) -> bool {
144 self.config_provenance.is_some()
145 }
146
147 #[must_use]
149 pub fn plugin_versions(&self) -> &HashMap<String, String> {
150 &self.plugin_versions
151 }
152
153 pub fn set_plugin_versions(&mut self, versions: HashMap<String, String>) {
155 self.plugin_versions = versions;
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::collections::HashMap;
163 use std::path::PathBuf;
164
165 fn make_test_provenance() -> ConfigProvenance {
166 ConfigProvenance {
167 config_file: PathBuf::from(".sqry/graph/config/config.json"),
168 config_checksum: "abc123def456".to_string(),
169 schema_version: 1,
170 overrides: HashMap::new(),
171 build_timestamp: std::time::SystemTime::now()
172 .duration_since(std::time::UNIX_EPOCH)
173 .unwrap_or_default()
174 .as_secs(),
175 build_host: Some("test-host".to_string()),
176 }
177 }
178
179 #[test]
180 fn test_magic_bytes() {
181 assert_eq!(MAGIC_BYTES, b"SQRY_GRAPH_V7");
182 assert_eq!(MAGIC_BYTES.len(), 13);
183 }
184
185 #[test]
186 fn test_version() {
187 assert_eq!(VERSION, 7);
188 }
189
190 #[test]
191 fn test_graph_header_new() {
192 let header = GraphHeader::new(100, 50, 200, 10);
193
194 assert_eq!(header.version, VERSION);
195 assert_eq!(header.node_count, 100);
196 assert_eq!(header.edge_count, 50);
197 assert_eq!(header.string_count, 200);
198 assert_eq!(header.file_count, 10);
199 assert!(header.timestamp > 0);
200 assert!(header.config_provenance.is_none());
201 }
202
203 #[test]
204 fn test_graph_header_with_provenance() {
205 let provenance = make_test_provenance();
206 let header = GraphHeader::with_provenance(100, 50, 200, 10, provenance);
207
208 assert_eq!(header.version, VERSION);
209 assert_eq!(header.node_count, 100);
210 assert_eq!(header.edge_count, 50);
211 assert!(header.config_provenance.is_some());
212 assert_eq!(
213 header.config_provenance.as_ref().unwrap().config_checksum,
214 "abc123def456"
215 );
216 }
217
218 #[test]
219 fn test_graph_header_provenance_method() {
220 let header = GraphHeader::new(10, 5, 20, 2);
221 assert!(header.provenance().is_none());
222
223 let provenance = make_test_provenance();
224 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
225 assert!(header_with.provenance().is_some());
226 assert_eq!(
227 header_with.provenance().unwrap().config_checksum,
228 "abc123def456"
229 );
230 }
231
232 #[test]
233 fn test_graph_header_has_provenance() {
234 let header = GraphHeader::new(10, 5, 20, 2);
235 assert!(!header.has_provenance());
236
237 let provenance = make_test_provenance();
238 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
239 assert!(header_with.has_provenance());
240 }
241
242 #[test]
243 fn test_graph_header_clone() {
244 let header = GraphHeader::new(100, 50, 200, 10);
245 let cloned = header.clone();
246
247 assert_eq!(header.version, cloned.version);
248 assert_eq!(header.node_count, cloned.node_count);
249 assert_eq!(header.edge_count, cloned.edge_count);
250 assert_eq!(header.string_count, cloned.string_count);
251 assert_eq!(header.file_count, cloned.file_count);
252 }
253
254 #[test]
255 fn test_graph_header_debug() {
256 let header = GraphHeader::new(100, 50, 200, 10);
257 let debug_str = format!("{:?}", header);
258
259 assert!(debug_str.contains("GraphHeader"));
260 assert!(debug_str.contains("version"));
261 assert!(debug_str.contains("node_count"));
262 }
263
264 #[test]
265 fn test_graph_header_timestamp_is_recent() {
266 let header = GraphHeader::new(10, 5, 20, 2);
267 let now = std::time::SystemTime::now()
268 .duration_since(std::time::UNIX_EPOCH)
269 .unwrap()
270 .as_secs();
271
272 assert!(header.timestamp <= now);
274 assert!(header.timestamp >= now - 1);
275 }
276
277 #[test]
278 fn test_graph_header_zero_counts() {
279 let header = GraphHeader::new(0, 0, 0, 0);
280
281 assert_eq!(header.node_count, 0);
282 assert_eq!(header.edge_count, 0);
283 assert_eq!(header.string_count, 0);
284 assert_eq!(header.file_count, 0);
285 }
286
287 #[test]
288 fn test_graph_header_large_counts() {
289 let header = GraphHeader::new(1_000_000, 5_000_000, 10_000_000, 100_000);
290
291 assert_eq!(header.node_count, 1_000_000);
292 assert_eq!(header.edge_count, 5_000_000);
293 assert_eq!(header.string_count, 10_000_000);
294 assert_eq!(header.file_count, 100_000);
295 }
296
297 #[test]
298 fn test_graph_header_plugin_versions_empty_by_default() {
299 let header = GraphHeader::new(10, 5, 20, 2);
300 assert!(header.plugin_versions().is_empty());
301 }
302
303 #[test]
304 fn test_graph_header_set_plugin_versions() {
305 let mut header = GraphHeader::new(10, 5, 20, 2);
306
307 let mut versions = HashMap::new();
308 versions.insert("rust".to_string(), "3.3.0".to_string());
309 versions.insert("javascript".to_string(), "3.3.0".to_string());
310
311 header.set_plugin_versions(versions.clone());
312
313 assert_eq!(header.plugin_versions().len(), 2);
314 assert_eq!(
315 header.plugin_versions().get("rust"),
316 Some(&"3.3.0".to_string())
317 );
318 assert_eq!(
319 header.plugin_versions().get("javascript"),
320 Some(&"3.3.0".to_string())
321 );
322 }
323
324 #[test]
325 fn test_graph_header_with_provenance_and_plugins() {
326 let provenance = make_test_provenance();
327
328 let mut plugin_versions = HashMap::new();
329 plugin_versions.insert("rust".to_string(), "3.3.0".to_string());
330 plugin_versions.insert("python".to_string(), "3.3.0".to_string());
331
332 let header = GraphHeader::with_provenance_and_plugins(
333 100,
334 50,
335 200,
336 10,
337 provenance,
338 plugin_versions.clone(),
339 );
340
341 assert_eq!(header.version, VERSION);
342 assert_eq!(header.node_count, 100);
343 assert!(header.config_provenance.is_some());
344 assert_eq!(header.plugin_versions().len(), 2);
345 assert_eq!(
346 header.plugin_versions().get("rust"),
347 Some(&"3.3.0".to_string())
348 );
349 }
350}