1use std::collections::HashMap;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum DatabaseMode {
15 Lpg,
17 Rdf,
19}
20
21impl std::fmt::Display for DatabaseMode {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 DatabaseMode::Lpg => write!(f, "lpg"),
25 DatabaseMode::Rdf => write!(f, "rdf"),
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DatabaseInfo {
33 pub mode: DatabaseMode,
35 pub node_count: usize,
37 pub edge_count: usize,
39 pub is_persistent: bool,
41 pub path: Option<PathBuf>,
43 pub wal_enabled: bool,
45 pub version: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct DatabaseStats {
52 pub node_count: usize,
54 pub edge_count: usize,
56 pub label_count: usize,
58 pub edge_type_count: usize,
60 pub property_key_count: usize,
62 pub index_count: usize,
64 pub memory_bytes: usize,
66 pub disk_bytes: Option<usize>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct LpgSchemaInfo {
73 pub labels: Vec<LabelInfo>,
75 pub edge_types: Vec<EdgeTypeInfo>,
77 pub property_keys: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct LabelInfo {
84 pub name: String,
86 pub count: usize,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct EdgeTypeInfo {
93 pub name: String,
95 pub count: usize,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RdfSchemaInfo {
102 pub predicates: Vec<PredicateInfo>,
104 pub named_graphs: Vec<String>,
106 pub subject_count: usize,
108 pub object_count: usize,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct PredicateInfo {
115 pub iri: String,
117 pub count: usize,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(tag = "mode")]
124pub enum SchemaInfo {
125 #[serde(rename = "lpg")]
127 Lpg(LpgSchemaInfo),
128 #[serde(rename = "rdf")]
130 Rdf(RdfSchemaInfo),
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct IndexInfo {
136 pub name: String,
138 pub index_type: String,
140 pub target: String,
142 pub unique: bool,
144 pub cardinality: Option<usize>,
146 pub size_bytes: Option<usize>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct WalStatus {
153 pub enabled: bool,
155 pub path: Option<PathBuf>,
157 pub size_bytes: usize,
159 pub record_count: usize,
161 pub last_checkpoint: Option<u64>,
163 pub current_epoch: u64,
165}
166
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct ValidationResult {
170 pub errors: Vec<ValidationError>,
172 pub warnings: Vec<ValidationWarning>,
174}
175
176impl ValidationResult {
177 #[must_use]
179 pub fn is_valid(&self) -> bool {
180 self.errors.is_empty()
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ValidationError {
187 pub code: String,
189 pub message: String,
191 pub context: Option<String>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ValidationWarning {
198 pub code: String,
200 pub message: String,
202 pub context: Option<String>,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "lowercase")]
209pub enum DumpFormat {
210 Parquet,
212 Turtle,
214 Json,
216}
217
218impl Default for DumpFormat {
219 fn default() -> Self {
220 DumpFormat::Parquet
221 }
222}
223
224impl std::fmt::Display for DumpFormat {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 DumpFormat::Parquet => write!(f, "parquet"),
228 DumpFormat::Turtle => write!(f, "turtle"),
229 DumpFormat::Json => write!(f, "json"),
230 }
231 }
232}
233
234impl std::str::FromStr for DumpFormat {
235 type Err = String;
236
237 fn from_str(s: &str) -> Result<Self, Self::Err> {
238 match s.to_lowercase().as_str() {
239 "parquet" => Ok(DumpFormat::Parquet),
240 "turtle" | "ttl" => Ok(DumpFormat::Turtle),
241 "json" | "jsonl" => Ok(DumpFormat::Json),
242 _ => Err(format!("Unknown dump format: {}", s)),
243 }
244 }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct CompactionStats {
250 pub bytes_reclaimed: usize,
252 pub nodes_compacted: usize,
254 pub edges_compacted: usize,
256 pub duration_ms: u64,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct DumpMetadata {
263 pub version: String,
265 pub mode: DatabaseMode,
267 pub format: DumpFormat,
269 pub node_count: usize,
271 pub edge_count: usize,
273 pub created_at: String,
275 #[serde(default)]
277 pub extra: HashMap<String, String>,
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
287 fn test_database_mode_display() {
288 assert_eq!(DatabaseMode::Lpg.to_string(), "lpg");
289 assert_eq!(DatabaseMode::Rdf.to_string(), "rdf");
290 }
291
292 #[test]
293 fn test_database_mode_serde_roundtrip() {
294 let json = serde_json::to_string(&DatabaseMode::Lpg).unwrap();
295 let mode: DatabaseMode = serde_json::from_str(&json).unwrap();
296 assert_eq!(mode, DatabaseMode::Lpg);
297
298 let json = serde_json::to_string(&DatabaseMode::Rdf).unwrap();
299 let mode: DatabaseMode = serde_json::from_str(&json).unwrap();
300 assert_eq!(mode, DatabaseMode::Rdf);
301 }
302
303 #[test]
304 fn test_database_mode_equality() {
305 assert_eq!(DatabaseMode::Lpg, DatabaseMode::Lpg);
306 assert_ne!(DatabaseMode::Lpg, DatabaseMode::Rdf);
307 }
308
309 #[test]
312 fn test_dump_format_default() {
313 assert_eq!(DumpFormat::default(), DumpFormat::Parquet);
314 }
315
316 #[test]
317 fn test_dump_format_display() {
318 assert_eq!(DumpFormat::Parquet.to_string(), "parquet");
319 assert_eq!(DumpFormat::Turtle.to_string(), "turtle");
320 assert_eq!(DumpFormat::Json.to_string(), "json");
321 }
322
323 #[test]
324 fn test_dump_format_from_str() {
325 assert_eq!(
326 "parquet".parse::<DumpFormat>().unwrap(),
327 DumpFormat::Parquet
328 );
329 assert_eq!("turtle".parse::<DumpFormat>().unwrap(), DumpFormat::Turtle);
330 assert_eq!("ttl".parse::<DumpFormat>().unwrap(), DumpFormat::Turtle);
331 assert_eq!("json".parse::<DumpFormat>().unwrap(), DumpFormat::Json);
332 assert_eq!("jsonl".parse::<DumpFormat>().unwrap(), DumpFormat::Json);
333 assert_eq!(
334 "PARQUET".parse::<DumpFormat>().unwrap(),
335 DumpFormat::Parquet
336 );
337 }
338
339 #[test]
340 fn test_dump_format_from_str_invalid() {
341 let result = "xml".parse::<DumpFormat>();
342 assert!(result.is_err());
343 assert!(result.unwrap_err().contains("Unknown dump format"));
344 }
345
346 #[test]
347 fn test_dump_format_serde_roundtrip() {
348 for format in [DumpFormat::Parquet, DumpFormat::Turtle, DumpFormat::Json] {
349 let json = serde_json::to_string(&format).unwrap();
350 let parsed: DumpFormat = serde_json::from_str(&json).unwrap();
351 assert_eq!(parsed, format);
352 }
353 }
354
355 #[test]
358 fn test_validation_result_default_is_valid() {
359 let result = ValidationResult::default();
360 assert!(result.is_valid());
361 assert!(result.errors.is_empty());
362 assert!(result.warnings.is_empty());
363 }
364
365 #[test]
366 fn test_validation_result_with_errors() {
367 let result = ValidationResult {
368 errors: vec![ValidationError {
369 code: "E001".to_string(),
370 message: "Orphaned edge".to_string(),
371 context: Some("edge_42".to_string()),
372 }],
373 warnings: Vec::new(),
374 };
375 assert!(!result.is_valid());
376 }
377
378 #[test]
379 fn test_validation_result_with_warnings_still_valid() {
380 let result = ValidationResult {
381 errors: Vec::new(),
382 warnings: vec![ValidationWarning {
383 code: "W001".to_string(),
384 message: "Unused index".to_string(),
385 context: None,
386 }],
387 };
388 assert!(result.is_valid());
389 }
390
391 #[test]
394 fn test_database_info_serde() {
395 let info = DatabaseInfo {
396 mode: DatabaseMode::Lpg,
397 node_count: 100,
398 edge_count: 200,
399 is_persistent: true,
400 path: Some(PathBuf::from("/tmp/db")),
401 wal_enabled: true,
402 version: "0.4.1".to_string(),
403 };
404 let json = serde_json::to_string(&info).unwrap();
405 let parsed: DatabaseInfo = serde_json::from_str(&json).unwrap();
406 assert_eq!(parsed.node_count, 100);
407 assert_eq!(parsed.edge_count, 200);
408 assert!(parsed.is_persistent);
409 }
410
411 #[test]
412 fn test_database_stats_serde() {
413 let stats = DatabaseStats {
414 node_count: 50,
415 edge_count: 75,
416 label_count: 3,
417 edge_type_count: 2,
418 property_key_count: 10,
419 index_count: 4,
420 memory_bytes: 1024,
421 disk_bytes: Some(2048),
422 };
423 let json = serde_json::to_string(&stats).unwrap();
424 let parsed: DatabaseStats = serde_json::from_str(&json).unwrap();
425 assert_eq!(parsed.node_count, 50);
426 assert_eq!(parsed.disk_bytes, Some(2048));
427 }
428
429 #[test]
430 fn test_schema_info_lpg_serde() {
431 let schema = SchemaInfo::Lpg(LpgSchemaInfo {
432 labels: vec![LabelInfo {
433 name: "Person".to_string(),
434 count: 10,
435 }],
436 edge_types: vec![EdgeTypeInfo {
437 name: "KNOWS".to_string(),
438 count: 20,
439 }],
440 property_keys: vec!["name".to_string(), "age".to_string()],
441 });
442 let json = serde_json::to_string(&schema).unwrap();
443 let parsed: SchemaInfo = serde_json::from_str(&json).unwrap();
444 match parsed {
445 SchemaInfo::Lpg(lpg) => {
446 assert_eq!(lpg.labels.len(), 1);
447 assert_eq!(lpg.labels[0].name, "Person");
448 assert_eq!(lpg.edge_types[0].count, 20);
449 }
450 SchemaInfo::Rdf(_) => panic!("Expected LPG schema"),
451 }
452 }
453
454 #[test]
455 fn test_schema_info_rdf_serde() {
456 let schema = SchemaInfo::Rdf(RdfSchemaInfo {
457 predicates: vec![PredicateInfo {
458 iri: "http://xmlns.com/foaf/0.1/knows".to_string(),
459 count: 5,
460 }],
461 named_graphs: vec!["default".to_string()],
462 subject_count: 10,
463 object_count: 15,
464 });
465 let json = serde_json::to_string(&schema).unwrap();
466 let parsed: SchemaInfo = serde_json::from_str(&json).unwrap();
467 match parsed {
468 SchemaInfo::Rdf(rdf) => {
469 assert_eq!(rdf.predicates.len(), 1);
470 assert_eq!(rdf.subject_count, 10);
471 }
472 SchemaInfo::Lpg(_) => panic!("Expected RDF schema"),
473 }
474 }
475
476 #[test]
477 fn test_index_info_serde() {
478 let info = IndexInfo {
479 name: "idx_person_name".to_string(),
480 index_type: "btree".to_string(),
481 target: "Person:name".to_string(),
482 unique: true,
483 cardinality: Some(1000),
484 size_bytes: Some(4096),
485 };
486 let json = serde_json::to_string(&info).unwrap();
487 let parsed: IndexInfo = serde_json::from_str(&json).unwrap();
488 assert_eq!(parsed.name, "idx_person_name");
489 assert!(parsed.unique);
490 }
491
492 #[test]
493 fn test_wal_status_serde() {
494 let status = WalStatus {
495 enabled: true,
496 path: Some(PathBuf::from("/tmp/wal")),
497 size_bytes: 8192,
498 record_count: 42,
499 last_checkpoint: Some(1700000000),
500 current_epoch: 100,
501 };
502 let json = serde_json::to_string(&status).unwrap();
503 let parsed: WalStatus = serde_json::from_str(&json).unwrap();
504 assert_eq!(parsed.record_count, 42);
505 assert_eq!(parsed.current_epoch, 100);
506 }
507
508 #[test]
509 fn test_compaction_stats_serde() {
510 let stats = CompactionStats {
511 bytes_reclaimed: 1024,
512 nodes_compacted: 10,
513 edges_compacted: 20,
514 duration_ms: 150,
515 };
516 let json = serde_json::to_string(&stats).unwrap();
517 let parsed: CompactionStats = serde_json::from_str(&json).unwrap();
518 assert_eq!(parsed.bytes_reclaimed, 1024);
519 assert_eq!(parsed.duration_ms, 150);
520 }
521
522 #[test]
523 fn test_dump_metadata_serde() {
524 let metadata = DumpMetadata {
525 version: "0.4.1".to_string(),
526 mode: DatabaseMode::Lpg,
527 format: DumpFormat::Parquet,
528 node_count: 1000,
529 edge_count: 5000,
530 created_at: "2025-01-15T12:00:00Z".to_string(),
531 extra: HashMap::new(),
532 };
533 let json = serde_json::to_string(&metadata).unwrap();
534 let parsed: DumpMetadata = serde_json::from_str(&json).unwrap();
535 assert_eq!(parsed.node_count, 1000);
536 assert_eq!(parsed.format, DumpFormat::Parquet);
537 }
538
539 #[test]
540 fn test_dump_metadata_with_extra() {
541 let mut extra = HashMap::new();
542 extra.insert("compression".to_string(), "zstd".to_string());
543 let metadata = DumpMetadata {
544 version: "0.4.1".to_string(),
545 mode: DatabaseMode::Rdf,
546 format: DumpFormat::Turtle,
547 node_count: 0,
548 edge_count: 0,
549 created_at: "2025-01-15T12:00:00Z".to_string(),
550 extra,
551 };
552 let json = serde_json::to_string(&metadata).unwrap();
553 let parsed: DumpMetadata = serde_json::from_str(&json).unwrap();
554 assert_eq!(parsed.extra.get("compression").unwrap(), "zstd");
555 }
556
557 #[test]
558 fn test_validation_error_serde() {
559 let error = ValidationError {
560 code: "E001".to_string(),
561 message: "Broken reference".to_string(),
562 context: Some("node_id=42".to_string()),
563 };
564 let json = serde_json::to_string(&error).unwrap();
565 let parsed: ValidationError = serde_json::from_str(&json).unwrap();
566 assert_eq!(parsed.code, "E001");
567 assert_eq!(parsed.context, Some("node_id=42".to_string()));
568 }
569
570 #[test]
571 fn test_validation_warning_serde() {
572 let warning = ValidationWarning {
573 code: "W001".to_string(),
574 message: "High memory usage".to_string(),
575 context: None,
576 };
577 let json = serde_json::to_string(&warning).unwrap();
578 let parsed: ValidationWarning = serde_json::from_str(&json).unwrap();
579 assert_eq!(parsed.code, "W001");
580 assert!(parsed.context.is_none());
581 }
582}