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