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}
222
223impl Default for DumpFormat {
224 fn default() -> Self {
225 DumpFormat::Parquet
226 }
227}
228
229impl std::fmt::Display for DumpFormat {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 match self {
232 DumpFormat::Parquet => write!(f, "parquet"),
233 DumpFormat::Turtle => write!(f, "turtle"),
234 DumpFormat::Json => write!(f, "json"),
235 }
236 }
237}
238
239impl std::str::FromStr for DumpFormat {
240 type Err = String;
241
242 fn from_str(s: &str) -> Result<Self, Self::Err> {
243 match s.to_lowercase().as_str() {
244 "parquet" => Ok(DumpFormat::Parquet),
245 "turtle" | "ttl" => Ok(DumpFormat::Turtle),
246 "json" | "jsonl" => Ok(DumpFormat::Json),
247 _ => Err(format!("Unknown dump format: {}", s)),
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct CompactionStats {
255 pub bytes_reclaimed: usize,
257 pub nodes_compacted: usize,
259 pub edges_compacted: usize,
261 pub duration_ms: u64,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct DumpMetadata {
268 pub version: String,
270 pub mode: DatabaseMode,
272 pub format: DumpFormat,
274 pub node_count: usize,
276 pub edge_count: usize,
278 pub created_at: String,
280 #[serde(default)]
282 pub extra: HashMap<String, String>,
283}
284
285pub trait AdminService {
293 fn info(&self) -> DatabaseInfo;
295
296 fn detailed_stats(&self) -> DatabaseStats;
298
299 fn schema(&self) -> SchemaInfo;
301
302 fn validate(&self) -> ValidationResult;
304
305 fn wal_status(&self) -> WalStatus;
307
308 fn wal_checkpoint(&self) -> grafeo_common::utils::error::Result<()>;
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
323 fn test_database_mode_display() {
324 assert_eq!(DatabaseMode::Lpg.to_string(), "lpg");
325 assert_eq!(DatabaseMode::Rdf.to_string(), "rdf");
326 }
327
328 #[test]
329 fn test_database_mode_serde_roundtrip() {
330 let json = serde_json::to_string(&DatabaseMode::Lpg).unwrap();
331 let mode: DatabaseMode = serde_json::from_str(&json).unwrap();
332 assert_eq!(mode, DatabaseMode::Lpg);
333
334 let json = serde_json::to_string(&DatabaseMode::Rdf).unwrap();
335 let mode: DatabaseMode = serde_json::from_str(&json).unwrap();
336 assert_eq!(mode, DatabaseMode::Rdf);
337 }
338
339 #[test]
340 fn test_database_mode_equality() {
341 assert_eq!(DatabaseMode::Lpg, DatabaseMode::Lpg);
342 assert_ne!(DatabaseMode::Lpg, DatabaseMode::Rdf);
343 }
344
345 #[test]
348 fn test_dump_format_default() {
349 assert_eq!(DumpFormat::default(), DumpFormat::Parquet);
350 }
351
352 #[test]
353 fn test_dump_format_display() {
354 assert_eq!(DumpFormat::Parquet.to_string(), "parquet");
355 assert_eq!(DumpFormat::Turtle.to_string(), "turtle");
356 assert_eq!(DumpFormat::Json.to_string(), "json");
357 }
358
359 #[test]
360 fn test_dump_format_from_str() {
361 assert_eq!(
362 "parquet".parse::<DumpFormat>().unwrap(),
363 DumpFormat::Parquet
364 );
365 assert_eq!("turtle".parse::<DumpFormat>().unwrap(), DumpFormat::Turtle);
366 assert_eq!("ttl".parse::<DumpFormat>().unwrap(), DumpFormat::Turtle);
367 assert_eq!("json".parse::<DumpFormat>().unwrap(), DumpFormat::Json);
368 assert_eq!("jsonl".parse::<DumpFormat>().unwrap(), DumpFormat::Json);
369 assert_eq!(
370 "PARQUET".parse::<DumpFormat>().unwrap(),
371 DumpFormat::Parquet
372 );
373 }
374
375 #[test]
376 fn test_dump_format_from_str_invalid() {
377 let result = "xml".parse::<DumpFormat>();
378 assert!(result.is_err());
379 assert!(result.unwrap_err().contains("Unknown dump format"));
380 }
381
382 #[test]
383 fn test_dump_format_serde_roundtrip() {
384 for format in [DumpFormat::Parquet, DumpFormat::Turtle, DumpFormat::Json] {
385 let json = serde_json::to_string(&format).unwrap();
386 let parsed: DumpFormat = serde_json::from_str(&json).unwrap();
387 assert_eq!(parsed, format);
388 }
389 }
390
391 #[test]
394 fn test_validation_result_default_is_valid() {
395 let result = ValidationResult::default();
396 assert!(result.is_valid());
397 assert!(result.errors.is_empty());
398 assert!(result.warnings.is_empty());
399 }
400
401 #[test]
402 fn test_validation_result_with_errors() {
403 let result = ValidationResult {
404 errors: vec![ValidationError {
405 code: "E001".to_string(),
406 message: "Orphaned edge".to_string(),
407 context: Some("edge_42".to_string()),
408 }],
409 warnings: Vec::new(),
410 };
411 assert!(!result.is_valid());
412 }
413
414 #[test]
415 fn test_validation_result_with_warnings_still_valid() {
416 let result = ValidationResult {
417 errors: Vec::new(),
418 warnings: vec![ValidationWarning {
419 code: "W001".to_string(),
420 message: "Unused index".to_string(),
421 context: None,
422 }],
423 };
424 assert!(result.is_valid());
425 }
426
427 #[test]
430 fn test_database_info_serde() {
431 let info = DatabaseInfo {
432 mode: DatabaseMode::Lpg,
433 node_count: 100,
434 edge_count: 200,
435 is_persistent: true,
436 path: Some(PathBuf::from("/tmp/db")),
437 wal_enabled: true,
438 version: "0.4.1".to_string(),
439 features: vec!["gql".into(), "cypher".into()],
440 };
441 let json = serde_json::to_string(&info).unwrap();
442 let parsed: DatabaseInfo = serde_json::from_str(&json).unwrap();
443 assert_eq!(parsed.node_count, 100);
444 assert_eq!(parsed.edge_count, 200);
445 assert!(parsed.is_persistent);
446 }
447
448 #[test]
449 fn test_database_stats_serde() {
450 let stats = DatabaseStats {
451 node_count: 50,
452 edge_count: 75,
453 label_count: 3,
454 edge_type_count: 2,
455 property_key_count: 10,
456 index_count: 4,
457 memory_bytes: 1024,
458 disk_bytes: Some(2048),
459 };
460 let json = serde_json::to_string(&stats).unwrap();
461 let parsed: DatabaseStats = serde_json::from_str(&json).unwrap();
462 assert_eq!(parsed.node_count, 50);
463 assert_eq!(parsed.disk_bytes, Some(2048));
464 }
465
466 #[test]
467 fn test_schema_info_lpg_serde() {
468 let schema = SchemaInfo::Lpg(LpgSchemaInfo {
469 labels: vec![LabelInfo {
470 name: "Person".to_string(),
471 count: 10,
472 }],
473 edge_types: vec![EdgeTypeInfo {
474 name: "KNOWS".to_string(),
475 count: 20,
476 }],
477 property_keys: vec!["name".to_string(), "age".to_string()],
478 });
479 let json = serde_json::to_string(&schema).unwrap();
480 let parsed: SchemaInfo = serde_json::from_str(&json).unwrap();
481 match parsed {
482 SchemaInfo::Lpg(lpg) => {
483 assert_eq!(lpg.labels.len(), 1);
484 assert_eq!(lpg.labels[0].name, "Person");
485 assert_eq!(lpg.edge_types[0].count, 20);
486 }
487 SchemaInfo::Rdf(_) => panic!("Expected LPG schema"),
488 }
489 }
490
491 #[test]
492 fn test_schema_info_rdf_serde() {
493 let schema = SchemaInfo::Rdf(RdfSchemaInfo {
494 predicates: vec![PredicateInfo {
495 iri: "http://xmlns.com/foaf/0.1/knows".to_string(),
496 count: 5,
497 }],
498 named_graphs: vec!["default".to_string()],
499 subject_count: 10,
500 object_count: 15,
501 });
502 let json = serde_json::to_string(&schema).unwrap();
503 let parsed: SchemaInfo = serde_json::from_str(&json).unwrap();
504 match parsed {
505 SchemaInfo::Rdf(rdf) => {
506 assert_eq!(rdf.predicates.len(), 1);
507 assert_eq!(rdf.subject_count, 10);
508 }
509 SchemaInfo::Lpg(_) => panic!("Expected RDF schema"),
510 }
511 }
512
513 #[test]
514 fn test_index_info_serde() {
515 let info = IndexInfo {
516 name: "idx_person_name".to_string(),
517 index_type: "btree".to_string(),
518 target: "Person:name".to_string(),
519 unique: true,
520 cardinality: Some(1000),
521 size_bytes: Some(4096),
522 };
523 let json = serde_json::to_string(&info).unwrap();
524 let parsed: IndexInfo = serde_json::from_str(&json).unwrap();
525 assert_eq!(parsed.name, "idx_person_name");
526 assert!(parsed.unique);
527 }
528
529 #[test]
530 fn test_wal_status_serde() {
531 let status = WalStatus {
532 enabled: true,
533 path: Some(PathBuf::from("/tmp/wal")),
534 size_bytes: 8192,
535 record_count: 42,
536 last_checkpoint: Some(1700000000),
537 current_epoch: 100,
538 };
539 let json = serde_json::to_string(&status).unwrap();
540 let parsed: WalStatus = serde_json::from_str(&json).unwrap();
541 assert_eq!(parsed.record_count, 42);
542 assert_eq!(parsed.current_epoch, 100);
543 }
544
545 #[test]
546 fn test_compaction_stats_serde() {
547 let stats = CompactionStats {
548 bytes_reclaimed: 1024,
549 nodes_compacted: 10,
550 edges_compacted: 20,
551 duration_ms: 150,
552 };
553 let json = serde_json::to_string(&stats).unwrap();
554 let parsed: CompactionStats = serde_json::from_str(&json).unwrap();
555 assert_eq!(parsed.bytes_reclaimed, 1024);
556 assert_eq!(parsed.duration_ms, 150);
557 }
558
559 #[test]
560 fn test_dump_metadata_serde() {
561 let metadata = DumpMetadata {
562 version: "0.4.1".to_string(),
563 mode: DatabaseMode::Lpg,
564 format: DumpFormat::Parquet,
565 node_count: 1000,
566 edge_count: 5000,
567 created_at: "2025-01-15T12:00:00Z".to_string(),
568 extra: HashMap::new(),
569 };
570 let json = serde_json::to_string(&metadata).unwrap();
571 let parsed: DumpMetadata = serde_json::from_str(&json).unwrap();
572 assert_eq!(parsed.node_count, 1000);
573 assert_eq!(parsed.format, DumpFormat::Parquet);
574 }
575
576 #[test]
577 fn test_dump_metadata_with_extra() {
578 let mut extra = HashMap::new();
579 extra.insert("compression".to_string(), "zstd".to_string());
580 let metadata = DumpMetadata {
581 version: "0.4.1".to_string(),
582 mode: DatabaseMode::Rdf,
583 format: DumpFormat::Turtle,
584 node_count: 0,
585 edge_count: 0,
586 created_at: "2025-01-15T12:00:00Z".to_string(),
587 extra,
588 };
589 let json = serde_json::to_string(&metadata).unwrap();
590 let parsed: DumpMetadata = serde_json::from_str(&json).unwrap();
591 assert_eq!(parsed.extra.get("compression").unwrap(), "zstd");
592 }
593
594 #[test]
595 fn test_validation_error_serde() {
596 let error = ValidationError {
597 code: "E001".to_string(),
598 message: "Broken reference".to_string(),
599 context: Some("node_id=42".to_string()),
600 };
601 let json = serde_json::to_string(&error).unwrap();
602 let parsed: ValidationError = serde_json::from_str(&json).unwrap();
603 assert_eq!(parsed.code, "E001");
604 assert_eq!(parsed.context, Some("node_id=42".to_string()));
605 }
606
607 #[test]
608 fn test_validation_warning_serde() {
609 let warning = ValidationWarning {
610 code: "W001".to_string(),
611 message: "High memory usage".to_string(),
612 context: None,
613 };
614 let json = serde_json::to_string(&warning).unwrap();
615 let parsed: ValidationWarning = serde_json::from_str(&json).unwrap();
616 assert_eq!(parsed.code, "W001");
617 assert!(parsed.context.is_none());
618 }
619}