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