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