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