Skip to main content

grafeo_engine/
admin.rs

1//! Admin API types for database inspection, backup, and maintenance.
2//!
3//! These types support both LPG (Labeled Property Graph) and RDF (Resource Description Framework)
4//! data models.
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11/// Database mode - either LPG (Labeled Property Graph) or RDF (Triple Store).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum DatabaseMode {
15    /// Labeled Property Graph mode (nodes with labels and properties, typed edges).
16    Lpg,
17    /// RDF mode (subject-predicate-object triples).
18    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/// High-level database information returned by `db.info()`.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DatabaseInfo {
33    /// Database mode (LPG or RDF).
34    pub mode: DatabaseMode,
35    /// Number of nodes (LPG) or subjects (RDF).
36    pub node_count: usize,
37    /// Number of edges (LPG) or triples (RDF).
38    pub edge_count: usize,
39    /// Whether the database is backed by a file.
40    pub is_persistent: bool,
41    /// Database file path, if persistent.
42    pub path: Option<PathBuf>,
43    /// Whether WAL is enabled.
44    pub wal_enabled: bool,
45    /// Database version.
46    pub version: String,
47}
48
49/// Detailed database statistics returned by `db.stats()`.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct DatabaseStats {
52    /// Number of nodes (LPG) or subjects (RDF).
53    pub node_count: usize,
54    /// Number of edges (LPG) or triples (RDF).
55    pub edge_count: usize,
56    /// Number of distinct labels (LPG) or classes (RDF).
57    pub label_count: usize,
58    /// Number of distinct edge types (LPG) or predicates (RDF).
59    pub edge_type_count: usize,
60    /// Number of distinct property keys.
61    pub property_key_count: usize,
62    /// Number of indexes.
63    pub index_count: usize,
64    /// Memory usage in bytes (approximate).
65    pub memory_bytes: usize,
66    /// Disk usage in bytes (if persistent).
67    pub disk_bytes: Option<usize>,
68}
69
70/// Schema information for LPG databases.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct LpgSchemaInfo {
73    /// All labels used in the database.
74    pub labels: Vec<LabelInfo>,
75    /// All edge types used in the database.
76    pub edge_types: Vec<EdgeTypeInfo>,
77    /// All property keys used in the database.
78    pub property_keys: Vec<String>,
79}
80
81/// Information about a label.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct LabelInfo {
84    /// The label name.
85    pub name: String,
86    /// Number of nodes with this label.
87    pub count: usize,
88}
89
90/// Information about an edge type.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct EdgeTypeInfo {
93    /// The edge type name.
94    pub name: String,
95    /// Number of edges with this type.
96    pub count: usize,
97}
98
99/// Schema information for RDF databases.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RdfSchemaInfo {
102    /// All predicates used in the database.
103    pub predicates: Vec<PredicateInfo>,
104    /// All named graphs.
105    pub named_graphs: Vec<String>,
106    /// Number of distinct subjects.
107    pub subject_count: usize,
108    /// Number of distinct objects.
109    pub object_count: usize,
110}
111
112/// Information about an RDF predicate.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct PredicateInfo {
115    /// The predicate IRI.
116    pub iri: String,
117    /// Number of triples using this predicate.
118    pub count: usize,
119}
120
121/// Combined schema information supporting both LPG and RDF.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(tag = "mode")]
124pub enum SchemaInfo {
125    /// LPG schema information.
126    #[serde(rename = "lpg")]
127    Lpg(LpgSchemaInfo),
128    /// RDF schema information.
129    #[serde(rename = "rdf")]
130    Rdf(RdfSchemaInfo),
131}
132
133/// Index information.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct IndexInfo {
136    /// Index name.
137    pub name: String,
138    /// Index type (hash, btree, fulltext, etc.).
139    pub index_type: String,
140    /// Target (label:property for LPG, predicate for RDF).
141    pub target: String,
142    /// Whether the index is unique.
143    pub unique: bool,
144    /// Estimated cardinality.
145    pub cardinality: Option<usize>,
146    /// Size in bytes.
147    pub size_bytes: Option<usize>,
148}
149
150/// WAL (Write-Ahead Log) status.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct WalStatus {
153    /// Whether WAL is enabled.
154    pub enabled: bool,
155    /// WAL file path.
156    pub path: Option<PathBuf>,
157    /// WAL size in bytes.
158    pub size_bytes: usize,
159    /// Number of WAL records.
160    pub record_count: usize,
161    /// Last checkpoint timestamp (Unix epoch seconds).
162    pub last_checkpoint: Option<u64>,
163    /// Current epoch/LSN.
164    pub current_epoch: u64,
165}
166
167/// Validation result.
168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct ValidationResult {
170    /// List of validation errors (empty = valid).
171    pub errors: Vec<ValidationError>,
172    /// List of validation warnings.
173    pub warnings: Vec<ValidationWarning>,
174}
175
176impl ValidationResult {
177    /// Returns true if validation passed (no errors).
178    #[must_use]
179    pub fn is_valid(&self) -> bool {
180        self.errors.is_empty()
181    }
182}
183
184/// A validation error.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ValidationError {
187    /// Error code.
188    pub code: String,
189    /// Human-readable error message.
190    pub message: String,
191    /// Optional context (e.g., affected entity ID).
192    pub context: Option<String>,
193}
194
195/// A validation warning.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ValidationWarning {
198    /// Warning code.
199    pub code: String,
200    /// Human-readable warning message.
201    pub message: String,
202    /// Optional context.
203    pub context: Option<String>,
204}
205
206/// Dump format for export operations.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "lowercase")]
209pub enum DumpFormat {
210    /// Apache Parquet format (default for LPG).
211    Parquet,
212    /// RDF Turtle format (default for RDF).
213    Turtle,
214    /// JSON Lines format.
215    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/// Compaction statistics returned after a compact operation.
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct CompactionStats {
250    /// Bytes reclaimed.
251    pub bytes_reclaimed: usize,
252    /// Number of nodes compacted.
253    pub nodes_compacted: usize,
254    /// Number of edges compacted.
255    pub edges_compacted: usize,
256    /// Duration in milliseconds.
257    pub duration_ms: u64,
258}
259
260/// Metadata for dump files.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct DumpMetadata {
263    /// Grafeo version that created the dump.
264    pub version: String,
265    /// Database mode.
266    pub mode: DatabaseMode,
267    /// Dump format.
268    pub format: DumpFormat,
269    /// Number of nodes.
270    pub node_count: usize,
271    /// Number of edges.
272    pub edge_count: usize,
273    /// Timestamp (ISO 8601).
274    pub created_at: String,
275    /// Additional metadata.
276    #[serde(default)]
277    pub extra: HashMap<String, String>,
278}
279
280/// Trait for administrative database operations.
281///
282/// Provides a uniform interface for introspection, validation, and
283/// maintenance operations. Used by the CLI, REST API, and bindings
284/// to inspect and manage a Grafeo database.
285///
286/// Implemented by [`GrafeoDB`](crate::GrafeoDB).
287pub trait AdminService {
288    /// Returns high-level database information (counts, mode, persistence).
289    fn info(&self) -> DatabaseInfo;
290
291    /// Returns detailed database statistics (memory, disk, indexes).
292    fn detailed_stats(&self) -> DatabaseStats;
293
294    /// Returns schema information (labels, edge types, property keys).
295    fn schema(&self) -> SchemaInfo;
296
297    /// Validates database integrity, returning errors and warnings.
298    fn validate(&self) -> ValidationResult;
299
300    /// Returns WAL (Write-Ahead Log) status.
301    fn wal_status(&self) -> WalStatus;
302
303    /// Forces a WAL checkpoint, flushing pending records to storage.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if the checkpoint fails.
308    fn wal_checkpoint(&self) -> grafeo_common::utils::error::Result<()>;
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    // ---- DatabaseMode ----
316
317    #[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    // ---- DumpFormat ----
341
342    #[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    // ---- ValidationResult ----
387
388    #[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    // ---- Serde roundtrips for complex types ----
423
424    #[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}