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")]
14#[non_exhaustive]
15pub enum DatabaseMode {
16 Lpg,
18 Rdf,
20}
21
22impl std::fmt::Display for DatabaseMode {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 DatabaseMode::Lpg => write!(f, "lpg"),
26 DatabaseMode::Rdf => write!(f, "rdf"),
27 }
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct DatabaseInfo {
34 pub mode: DatabaseMode,
36 pub node_count: usize,
38 pub edge_count: usize,
40 pub is_persistent: bool,
42 pub path: Option<PathBuf>,
44 pub wal_enabled: bool,
46 pub version: String,
48 pub features: Vec<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct DatabaseStats {
55 pub node_count: usize,
57 pub edge_count: usize,
59 pub label_count: usize,
61 pub edge_type_count: usize,
63 pub property_key_count: usize,
65 pub index_count: usize,
67 pub memory_bytes: usize,
69 pub disk_bytes: Option<usize>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct LpgSchemaInfo {
76 pub labels: Vec<LabelInfo>,
78 pub edge_types: Vec<EdgeTypeInfo>,
80 pub property_keys: Vec<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct LabelInfo {
87 pub name: String,
89 pub count: usize,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct EdgeTypeInfo {
96 pub name: String,
98 pub count: usize,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct RdfSchemaInfo {
105 pub predicates: Vec<PredicateInfo>,
107 pub named_graphs: Vec<String>,
109 pub subject_count: usize,
111 pub object_count: usize,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PredicateInfo {
118 pub iri: String,
120 pub count: usize,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(tag = "mode")]
127#[non_exhaustive]
128pub enum SchemaInfo {
129 #[serde(rename = "lpg")]
131 Lpg(LpgSchemaInfo),
132 #[serde(rename = "rdf")]
134 Rdf(RdfSchemaInfo),
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct IndexInfo {
140 pub name: String,
142 pub index_type: String,
144 pub target: String,
146 pub unique: bool,
148 pub cardinality: Option<usize>,
150 pub size_bytes: Option<usize>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct WalStatus {
157 pub enabled: bool,
159 pub path: Option<PathBuf>,
161 pub size_bytes: usize,
163 pub record_count: usize,
165 pub last_checkpoint: Option<u64>,
167 pub current_epoch: u64,
169}
170
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
173pub struct ValidationResult {
174 pub errors: Vec<ValidationError>,
176 pub warnings: Vec<ValidationWarning>,
178}
179
180impl ValidationResult {
181 #[must_use]
183 pub fn is_valid(&self) -> bool {
184 self.errors.is_empty()
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ValidationError {
191 pub code: String,
193 pub message: String,
195 pub context: Option<String>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ValidationWarning {
202 pub code: String,
204 pub message: String,
206 pub context: Option<String>,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(rename_all = "lowercase")]
213#[non_exhaustive]
214pub enum DumpFormat {
215 Parquet,
217 Turtle,
219 Json,
221 Arrow,
223 Gexf,
225 GraphMl,
227}
228
229impl Default for DumpFormat {
230 fn default() -> Self {
231 DumpFormat::Parquet
232 }
233}
234
235impl std::fmt::Display for DumpFormat {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 match self {
238 DumpFormat::Parquet => write!(f, "parquet"),
239 DumpFormat::Turtle => write!(f, "turtle"),
240 DumpFormat::Json => write!(f, "json"),
241 DumpFormat::Arrow => write!(f, "arrow"),
242 DumpFormat::Gexf => write!(f, "gexf"),
243 DumpFormat::GraphMl => write!(f, "graphml"),
244 }
245 }
246}
247
248impl std::str::FromStr for DumpFormat {
249 type Err = String;
250
251 fn from_str(s: &str) -> Result<Self, Self::Err> {
252 match s.to_lowercase().as_str() {
253 "parquet" => Ok(DumpFormat::Parquet),
254 "turtle" | "ttl" => Ok(DumpFormat::Turtle),
255 "json" | "jsonl" => Ok(DumpFormat::Json),
256 "arrow" | "arrow-ipc" | "ipc" => Ok(DumpFormat::Arrow),
257 "gexf" => Ok(DumpFormat::Gexf),
258 "graphml" => Ok(DumpFormat::GraphMl),
259 _ => Err(format!("Unknown dump format: {}", s)),
260 }
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct CompactionStats {
267 pub bytes_reclaimed: usize,
269 pub nodes_compacted: usize,
271 pub edges_compacted: usize,
273 pub duration_ms: u64,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct DumpMetadata {
280 pub version: String,
282 pub mode: DatabaseMode,
284 pub format: DumpFormat,
286 pub node_count: usize,
288 pub edge_count: usize,
290 pub created_at: String,
292 #[serde(default)]
294 pub extra: HashMap<String, String>,
295}
296
297pub trait AdminService {
305 fn info(&self) -> DatabaseInfo;
307
308 fn detailed_stats(&self) -> DatabaseStats;
310
311 fn schema(&self) -> SchemaInfo;
313
314 fn validate(&self) -> ValidationResult;
316
317 fn wal_status(&self) -> WalStatus;
319
320 fn wal_checkpoint(&self) -> grafeo_common::utils::error::Result<()>;
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
335 fn test_database_mode_display() {
336 assert_eq!(DatabaseMode::Lpg.to_string(), "lpg");
337 assert_eq!(DatabaseMode::Rdf.to_string(), "rdf");
338 }
339
340 #[test]
341 fn test_database_mode_serde_roundtrip() {
342 let json = serde_json::to_string(&DatabaseMode::Lpg).unwrap();
343 let mode: DatabaseMode = serde_json::from_str(&json).unwrap();
344 assert_eq!(mode, DatabaseMode::Lpg);
345
346 let json = serde_json::to_string(&DatabaseMode::Rdf).unwrap();
347 let mode: DatabaseMode = serde_json::from_str(&json).unwrap();
348 assert_eq!(mode, DatabaseMode::Rdf);
349 }
350
351 #[test]
352 fn test_database_mode_equality() {
353 assert_eq!(DatabaseMode::Lpg, DatabaseMode::Lpg);
354 assert_ne!(DatabaseMode::Lpg, DatabaseMode::Rdf);
355 }
356
357 #[test]
360 fn test_dump_format_default() {
361 assert_eq!(DumpFormat::default(), DumpFormat::Parquet);
362 }
363
364 #[test]
365 fn test_dump_format_display() {
366 assert_eq!(DumpFormat::Parquet.to_string(), "parquet");
367 assert_eq!(DumpFormat::Turtle.to_string(), "turtle");
368 assert_eq!(DumpFormat::Json.to_string(), "json");
369 assert_eq!(DumpFormat::Arrow.to_string(), "arrow");
370 assert_eq!(DumpFormat::Gexf.to_string(), "gexf");
371 assert_eq!(DumpFormat::GraphMl.to_string(), "graphml");
372 }
373
374 #[test]
375 fn test_dump_format_from_str() {
376 assert_eq!(
377 "parquet".parse::<DumpFormat>().unwrap(),
378 DumpFormat::Parquet
379 );
380 assert_eq!("turtle".parse::<DumpFormat>().unwrap(), DumpFormat::Turtle);
381 assert_eq!("ttl".parse::<DumpFormat>().unwrap(), DumpFormat::Turtle);
382 assert_eq!("json".parse::<DumpFormat>().unwrap(), DumpFormat::Json);
383 assert_eq!("jsonl".parse::<DumpFormat>().unwrap(), DumpFormat::Json);
384 assert_eq!("arrow".parse::<DumpFormat>().unwrap(), DumpFormat::Arrow);
385 assert_eq!(
386 "arrow-ipc".parse::<DumpFormat>().unwrap(),
387 DumpFormat::Arrow
388 );
389 assert_eq!("ipc".parse::<DumpFormat>().unwrap(), DumpFormat::Arrow);
390 assert_eq!("gexf".parse::<DumpFormat>().unwrap(), DumpFormat::Gexf);
391 assert_eq!(
392 "graphml".parse::<DumpFormat>().unwrap(),
393 DumpFormat::GraphMl
394 );
395 assert_eq!(
396 "PARQUET".parse::<DumpFormat>().unwrap(),
397 DumpFormat::Parquet
398 );
399 }
400
401 #[test]
402 fn test_dump_format_from_str_invalid() {
403 let result = "xml".parse::<DumpFormat>();
404 assert!(result.is_err());
405 assert!(result.unwrap_err().contains("Unknown dump format"));
406 }
407
408 #[test]
409 fn test_dump_format_serde_roundtrip() {
410 for format in [
411 DumpFormat::Parquet,
412 DumpFormat::Turtle,
413 DumpFormat::Json,
414 DumpFormat::Arrow,
415 DumpFormat::Gexf,
416 DumpFormat::GraphMl,
417 ] {
418 let json = serde_json::to_string(&format).unwrap();
419 let parsed: DumpFormat = serde_json::from_str(&json).unwrap();
420 assert_eq!(parsed, format);
421 }
422 }
423
424 #[test]
427 fn test_validation_result_default_is_valid() {
428 let result = ValidationResult::default();
429 assert!(result.is_valid());
430 assert!(result.errors.is_empty());
431 assert!(result.warnings.is_empty());
432 }
433
434 #[test]
435 fn test_validation_result_with_errors() {
436 let result = ValidationResult {
437 errors: vec![ValidationError {
438 code: "E001".to_string(),
439 message: "Orphaned edge".to_string(),
440 context: Some("edge_42".to_string()),
441 }],
442 warnings: Vec::new(),
443 };
444 assert!(!result.is_valid());
445 }
446
447 #[test]
448 fn test_validation_result_with_warnings_still_valid() {
449 let result = ValidationResult {
450 errors: Vec::new(),
451 warnings: vec![ValidationWarning {
452 code: "W001".to_string(),
453 message: "Unused index".to_string(),
454 context: None,
455 }],
456 };
457 assert!(result.is_valid());
458 }
459
460 #[test]
463 fn test_database_info_serde() {
464 let info = DatabaseInfo {
465 mode: DatabaseMode::Lpg,
466 node_count: 100,
467 edge_count: 200,
468 is_persistent: true,
469 path: Some(PathBuf::from("/tmp/db")),
470 wal_enabled: true,
471 version: "0.4.1".to_string(),
472 features: vec!["gql".into(), "cypher".into()],
473 };
474 let json = serde_json::to_string(&info).unwrap();
475 let parsed: DatabaseInfo = serde_json::from_str(&json).unwrap();
476 assert_eq!(parsed.node_count, 100);
477 assert_eq!(parsed.edge_count, 200);
478 assert!(parsed.is_persistent);
479 }
480
481 #[test]
482 fn test_database_stats_serde() {
483 let stats = DatabaseStats {
484 node_count: 50,
485 edge_count: 75,
486 label_count: 3,
487 edge_type_count: 2,
488 property_key_count: 10,
489 index_count: 4,
490 memory_bytes: 1024,
491 disk_bytes: Some(2048),
492 };
493 let json = serde_json::to_string(&stats).unwrap();
494 let parsed: DatabaseStats = serde_json::from_str(&json).unwrap();
495 assert_eq!(parsed.node_count, 50);
496 assert_eq!(parsed.disk_bytes, Some(2048));
497 }
498
499 #[test]
500 fn test_schema_info_lpg_serde() {
501 let schema = SchemaInfo::Lpg(LpgSchemaInfo {
502 labels: vec![LabelInfo {
503 name: "Person".to_string(),
504 count: 10,
505 }],
506 edge_types: vec![EdgeTypeInfo {
507 name: "KNOWS".to_string(),
508 count: 20,
509 }],
510 property_keys: vec!["name".to_string(), "age".to_string()],
511 });
512 let json = serde_json::to_string(&schema).unwrap();
513 let parsed: SchemaInfo = serde_json::from_str(&json).unwrap();
514 match parsed {
515 SchemaInfo::Lpg(lpg) => {
516 assert_eq!(lpg.labels.len(), 1);
517 assert_eq!(lpg.labels[0].name, "Person");
518 assert_eq!(lpg.edge_types[0].count, 20);
519 }
520 SchemaInfo::Rdf(_) => panic!("Expected LPG schema"),
521 }
522 }
523
524 #[test]
525 fn test_schema_info_rdf_serde() {
526 let schema = SchemaInfo::Rdf(RdfSchemaInfo {
527 predicates: vec![PredicateInfo {
528 iri: "http://xmlns.com/foaf/0.1/knows".to_string(),
529 count: 5,
530 }],
531 named_graphs: vec!["default".to_string()],
532 subject_count: 10,
533 object_count: 15,
534 });
535 let json = serde_json::to_string(&schema).unwrap();
536 let parsed: SchemaInfo = serde_json::from_str(&json).unwrap();
537 match parsed {
538 SchemaInfo::Rdf(rdf) => {
539 assert_eq!(rdf.predicates.len(), 1);
540 assert_eq!(rdf.subject_count, 10);
541 }
542 SchemaInfo::Lpg(_) => panic!("Expected RDF schema"),
543 }
544 }
545
546 #[test]
547 fn test_index_info_serde() {
548 let info = IndexInfo {
549 name: "idx_person_name".to_string(),
550 index_type: "btree".to_string(),
551 target: "Person:name".to_string(),
552 unique: true,
553 cardinality: Some(1000),
554 size_bytes: Some(4096),
555 };
556 let json = serde_json::to_string(&info).unwrap();
557 let parsed: IndexInfo = serde_json::from_str(&json).unwrap();
558 assert_eq!(parsed.name, "idx_person_name");
559 assert!(parsed.unique);
560 }
561
562 #[test]
563 fn test_wal_status_serde() {
564 let status = WalStatus {
565 enabled: true,
566 path: Some(PathBuf::from("/tmp/wal")),
567 size_bytes: 8192,
568 record_count: 42,
569 last_checkpoint: Some(1700000000),
570 current_epoch: 100,
571 };
572 let json = serde_json::to_string(&status).unwrap();
573 let parsed: WalStatus = serde_json::from_str(&json).unwrap();
574 assert_eq!(parsed.record_count, 42);
575 assert_eq!(parsed.current_epoch, 100);
576 }
577
578 #[test]
579 fn test_compaction_stats_serde() {
580 let stats = CompactionStats {
581 bytes_reclaimed: 1024,
582 nodes_compacted: 10,
583 edges_compacted: 20,
584 duration_ms: 150,
585 };
586 let json = serde_json::to_string(&stats).unwrap();
587 let parsed: CompactionStats = serde_json::from_str(&json).unwrap();
588 assert_eq!(parsed.bytes_reclaimed, 1024);
589 assert_eq!(parsed.duration_ms, 150);
590 }
591
592 #[test]
593 fn test_dump_metadata_serde() {
594 let metadata = DumpMetadata {
595 version: "0.4.1".to_string(),
596 mode: DatabaseMode::Lpg,
597 format: DumpFormat::Parquet,
598 node_count: 1000,
599 edge_count: 5000,
600 created_at: "2025-01-15T12:00:00Z".to_string(),
601 extra: HashMap::new(),
602 };
603 let json = serde_json::to_string(&metadata).unwrap();
604 let parsed: DumpMetadata = serde_json::from_str(&json).unwrap();
605 assert_eq!(parsed.node_count, 1000);
606 assert_eq!(parsed.format, DumpFormat::Parquet);
607 }
608
609 #[test]
610 fn test_dump_metadata_with_extra() {
611 let mut extra = HashMap::new();
612 extra.insert("compression".to_string(), "zstd".to_string());
613 let metadata = DumpMetadata {
614 version: "0.4.1".to_string(),
615 mode: DatabaseMode::Rdf,
616 format: DumpFormat::Turtle,
617 node_count: 0,
618 edge_count: 0,
619 created_at: "2025-01-15T12:00:00Z".to_string(),
620 extra,
621 };
622 let json = serde_json::to_string(&metadata).unwrap();
623 let parsed: DumpMetadata = serde_json::from_str(&json).unwrap();
624 assert_eq!(parsed.extra.get("compression").unwrap(), "zstd");
625 }
626
627 #[test]
628 fn test_validation_error_serde() {
629 let error = ValidationError {
630 code: "E001".to_string(),
631 message: "Broken reference".to_string(),
632 context: Some("node_id=42".to_string()),
633 };
634 let json = serde_json::to_string(&error).unwrap();
635 let parsed: ValidationError = serde_json::from_str(&json).unwrap();
636 assert_eq!(parsed.code, "E001");
637 assert_eq!(parsed.context, Some("node_id=42".to_string()));
638 }
639
640 #[test]
641 fn test_validation_warning_serde() {
642 let warning = ValidationWarning {
643 code: "W001".to_string(),
644 message: "High memory usage".to_string(),
645 context: None,
646 };
647 let json = serde_json::to_string(&warning).unwrap();
648 let parsed: ValidationWarning = serde_json::from_str(&json).unwrap();
649 assert_eq!(parsed.code, "W001");
650 assert!(parsed.context.is_none());
651 }
652}