oxirs_tdb/
database_ops.rs

1//! Database operations and management utilities
2//!
3//! Provides comprehensive database management operations inspired by Apache Jena's DatabaseOps.
4//! Includes database lifecycle management, maintenance operations, and administrative tasks.
5
6use crate::error::{Result, TdbError};
7use crate::store::{StoreParams, TdbStore};
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10use std::time::{Duration, SystemTime};
11
12/// Database metadata
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct DatabaseMetadata {
15    /// Database name
16    pub name: String,
17    /// Database location
18    pub location: PathBuf,
19    /// Creation timestamp
20    pub created_at: SystemTime,
21    /// Last modified timestamp
22    pub modified_at: SystemTime,
23    /// Database version
24    pub version: String,
25    /// Database size in bytes
26    pub size_bytes: u64,
27    /// Number of triples
28    pub triple_count: u64,
29    /// Database status
30    pub status: DatabaseStatus,
31}
32
33/// Database status
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum DatabaseStatus {
36    /// Database is active and available
37    Active,
38    /// Database is being created
39    Creating,
40    /// Database is being compacted
41    Compacting,
42    /// Database is being backed up
43    BackingUp,
44    /// Database is being repaired
45    Repairing,
46    /// Database is offline
47    Offline,
48    /// Database has errors
49    Error,
50}
51
52/// Database operations manager
53pub struct DatabaseOps {
54    /// Base directory for all databases
55    base_dir: PathBuf,
56}
57
58impl DatabaseOps {
59    /// Create new database operations manager
60    pub fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
61        let base_dir = base_dir.as_ref().to_path_buf();
62
63        // Create base directory if it doesn't exist
64        if !base_dir.exists() {
65            std::fs::create_dir_all(&base_dir)?;
66        }
67
68        Ok(Self { base_dir })
69    }
70
71    /// Create a new database with the given parameters
72    pub fn create_database(&self, name: &str, params: StoreParams) -> Result<DatabaseMetadata> {
73        let db_path = self.base_dir.join(name);
74
75        // Check if database already exists
76        if db_path.exists() {
77            return Err(TdbError::InvalidInput(format!(
78                "Database '{}' already exists",
79                name
80            )));
81        }
82
83        // Validate database name
84        self.validate_database_name(name)?;
85
86        // Create database directory
87        std::fs::create_dir_all(&db_path)?;
88
89        // Save store parameters
90        let params_file = db_path.join("store_params.json");
91        params.save_to_file(&params_file)?;
92
93        // Initialize database
94        let _store = TdbStore::open(&db_path)?;
95
96        // Create metadata
97        let metadata = DatabaseMetadata {
98            name: name.to_string(),
99            location: db_path.clone(),
100            created_at: SystemTime::now(),
101            modified_at: SystemTime::now(),
102            version: crate::VERSION.to_string(),
103            size_bytes: self.calculate_database_size(&db_path)?,
104            triple_count: 0,
105            status: DatabaseStatus::Active,
106        };
107
108        // Save metadata
109        self.save_metadata(&metadata)?;
110
111        log::info!("Created database '{}' at {:?}", name, db_path);
112
113        Ok(metadata)
114    }
115
116    /// Delete a database
117    pub fn delete_database(&self, name: &str) -> Result<()> {
118        let db_path = self.base_dir.join(name);
119
120        if !db_path.exists() {
121            return Err(TdbError::InvalidInput(format!(
122                "Database '{}' does not exist",
123                name
124            )));
125        }
126
127        // Delete database directory and all contents
128        std::fs::remove_dir_all(&db_path)?;
129
130        log::info!("Deleted database '{}'", name);
131
132        Ok(())
133    }
134
135    /// List all databases
136    pub fn list_databases(&self) -> Result<Vec<DatabaseMetadata>> {
137        let mut databases = Vec::new();
138
139        if !self.base_dir.exists() {
140            return Ok(databases);
141        }
142
143        for entry in std::fs::read_dir(&self.base_dir)? {
144            let entry = entry?;
145            let path = entry.path();
146
147            if path.is_dir() {
148                // Try to load metadata
149                if let Ok(metadata) = self.load_metadata(&path) {
150                    databases.push(metadata);
151                }
152            }
153        }
154
155        Ok(databases)
156    }
157
158    /// Get metadata for a specific database
159    pub fn get_metadata(&self, name: &str) -> Result<DatabaseMetadata> {
160        let db_path = self.base_dir.join(name);
161
162        if !db_path.exists() {
163            return Err(TdbError::InvalidInput(format!(
164                "Database '{}' does not exist",
165                name
166            )));
167        }
168
169        self.load_metadata(&db_path)
170    }
171
172    /// Compact a database
173    pub fn compact_database(&self, name: &str) -> Result<CompactionStats> {
174        let db_path = self.base_dir.join(name);
175
176        if !db_path.exists() {
177            return Err(TdbError::InvalidInput(format!(
178                "Database '{}' does not exist",
179                name
180            )));
181        }
182
183        // Update status
184        let mut metadata = self.load_metadata(&db_path)?;
185        metadata.status = DatabaseStatus::Compacting;
186        self.save_metadata(&metadata)?;
187
188        let start_time = SystemTime::now();
189        let size_before = self.calculate_database_size(&db_path)?;
190
191        // Open database and compact
192        let mut store = TdbStore::open(&db_path)?;
193        store.compact()?;
194
195        let size_after = self.calculate_database_size(&db_path)?;
196        let duration = start_time.elapsed().unwrap_or(Duration::from_secs(0));
197
198        // Update metadata
199        metadata.status = DatabaseStatus::Active;
200        metadata.modified_at = SystemTime::now();
201        metadata.size_bytes = size_after;
202        self.save_metadata(&metadata)?;
203
204        let stats = CompactionStats {
205            size_before,
206            size_after,
207            space_saved: size_before.saturating_sub(size_after),
208            duration_secs: duration.as_secs_f64(),
209            compression_ratio: if size_before > 0 {
210                size_after as f64 / size_before as f64
211            } else {
212                1.0
213            },
214        };
215
216        log::info!(
217            "Compacted database '{}': saved {} bytes ({:.1}% reduction)",
218            name,
219            stats.space_saved,
220            (1.0 - stats.compression_ratio) * 100.0
221        );
222
223        Ok(stats)
224    }
225
226    /// Repair a database (check and fix corruption)
227    pub fn repair_database(&self, name: &str) -> Result<RepairReport> {
228        let db_path = self.base_dir.join(name);
229
230        if !db_path.exists() {
231            return Err(TdbError::InvalidInput(format!(
232                "Database '{}' does not exist",
233                name
234            )));
235        }
236
237        // Update status
238        let mut metadata = self.load_metadata(&db_path)?;
239        metadata.status = DatabaseStatus::Repairing;
240        self.save_metadata(&metadata)?;
241
242        let start_time = SystemTime::now();
243
244        // Open database and run diagnostics
245        let store = TdbStore::open(&db_path)?;
246        let diagnostic_report = store.run_diagnostics(crate::diagnostics::DiagnosticLevel::Deep);
247
248        // Count issues from diagnostic report
249        let issues_found =
250            diagnostic_report.summary.error_count + diagnostic_report.summary.critical_count;
251        let issues_fixed = 0; // Currently no automatic repair implemented
252
253        let duration = start_time.elapsed().unwrap_or(Duration::from_secs(0));
254
255        // Update metadata
256        metadata.status = if issues_found == issues_fixed {
257            DatabaseStatus::Active
258        } else {
259            DatabaseStatus::Error
260        };
261        metadata.modified_at = SystemTime::now();
262        self.save_metadata(&metadata)?;
263
264        let report = RepairReport {
265            issues_found,
266            issues_fixed,
267            duration_secs: duration.as_secs_f64(),
268            success: issues_found == issues_fixed,
269        };
270
271        log::info!(
272            "Repaired database '{}': {} issues found, {} fixed",
273            name,
274            report.issues_found,
275            report.issues_fixed
276        );
277
278        Ok(report)
279    }
280
281    /// Copy a database
282    pub fn copy_database(&self, source: &str, destination: &str) -> Result<()> {
283        let source_path = self.base_dir.join(source);
284        let dest_path = self.base_dir.join(destination);
285
286        if !source_path.exists() {
287            return Err(TdbError::InvalidInput(format!(
288                "Source database '{}' does not exist",
289                source
290            )));
291        }
292
293        if dest_path.exists() {
294            return Err(TdbError::InvalidInput(format!(
295                "Destination database '{}' already exists",
296                destination
297            )));
298        }
299
300        // Validate destination name
301        self.validate_database_name(destination)?;
302
303        // Copy directory recursively
304        self.copy_dir_recursive(&source_path, &dest_path)?;
305
306        // Update metadata for destination
307        if let Ok(mut metadata) = self.load_metadata(&dest_path) {
308            metadata.name = destination.to_string();
309            metadata.location = dest_path.clone();
310            metadata.created_at = SystemTime::now();
311            self.save_metadata(&metadata)?;
312        }
313
314        log::info!("Copied database '{}' to '{}'", source, destination);
315
316        Ok(())
317    }
318
319    /// Get database size in bytes
320    pub fn get_database_size(&self, name: &str) -> Result<u64> {
321        let db_path = self.base_dir.join(name);
322
323        if !db_path.exists() {
324            return Err(TdbError::InvalidInput(format!(
325                "Database '{}' does not exist",
326                name
327            )));
328        }
329
330        self.calculate_database_size(&db_path)
331    }
332
333    /// Validate database name
334    fn validate_database_name(&self, name: &str) -> Result<()> {
335        if name.is_empty() {
336            return Err(TdbError::InvalidInput(
337                "Database name cannot be empty".to_string(),
338            ));
339        }
340
341        // Check for invalid characters
342        if name.contains(['/', '\\', ':', '*', '?', '"', '<', '>', '|']) {
343            return Err(TdbError::InvalidInput(format!(
344                "Database name '{}' contains invalid characters",
345                name
346            )));
347        }
348
349        Ok(())
350    }
351
352    /// Calculate total size of database directory
353    #[allow(clippy::only_used_in_recursion)]
354    fn calculate_database_size(&self, path: &Path) -> Result<u64> {
355        let mut total_size = 0u64;
356
357        if path.is_dir() {
358            for entry in std::fs::read_dir(path)? {
359                let entry = entry?;
360                let entry_path = entry.path();
361
362                if entry_path.is_dir() {
363                    total_size += self.calculate_database_size(&entry_path)?;
364                } else if entry_path.is_file() {
365                    total_size += entry.metadata()?.len();
366                }
367            }
368        }
369
370        Ok(total_size)
371    }
372
373    /// Save database metadata
374    fn save_metadata(&self, metadata: &DatabaseMetadata) -> Result<()> {
375        let metadata_file = metadata.location.join("metadata.json");
376        let json = serde_json::to_string_pretty(metadata)
377            .map_err(|e| TdbError::Serialization(format!("Failed to serialize metadata: {}", e)))?;
378        std::fs::write(metadata_file, json)?;
379        Ok(())
380    }
381
382    /// Load database metadata
383    fn load_metadata(&self, db_path: &Path) -> Result<DatabaseMetadata> {
384        let metadata_file = db_path.join("metadata.json");
385
386        if !metadata_file.exists() {
387            // Create default metadata if file doesn't exist
388            let metadata = DatabaseMetadata {
389                name: db_path
390                    .file_name()
391                    .and_then(|n| n.to_str())
392                    .unwrap_or("unknown")
393                    .to_string(),
394                location: db_path.to_path_buf(),
395                created_at: SystemTime::now(),
396                modified_at: SystemTime::now(),
397                version: crate::VERSION.to_string(),
398                size_bytes: self.calculate_database_size(db_path)?,
399                triple_count: 0,
400                status: DatabaseStatus::Active,
401            };
402            self.save_metadata(&metadata)?;
403            return Ok(metadata);
404        }
405
406        let json = std::fs::read_to_string(metadata_file)?;
407        let metadata: DatabaseMetadata = serde_json::from_str(&json)
408            .map_err(|e| TdbError::Deserialization(format!("Failed to parse metadata: {}", e)))?;
409        Ok(metadata)
410    }
411
412    /// Copy directory recursively
413    #[allow(clippy::only_used_in_recursion)]
414    fn copy_dir_recursive(&self, src: &Path, dst: &Path) -> Result<()> {
415        std::fs::create_dir_all(dst)?;
416
417        for entry in std::fs::read_dir(src)? {
418            let entry = entry?;
419            let src_path = entry.path();
420            let dst_path = dst.join(entry.file_name());
421
422            if src_path.is_dir() {
423                self.copy_dir_recursive(&src_path, &dst_path)?;
424            } else {
425                std::fs::copy(&src_path, &dst_path)?;
426            }
427        }
428
429        Ok(())
430    }
431}
432
433/// Statistics from database compaction
434#[derive(Debug, Clone)]
435pub struct CompactionStats {
436    /// Database size before compaction
437    pub size_before: u64,
438    /// Database size after compaction
439    pub size_after: u64,
440    /// Space saved in bytes
441    pub space_saved: u64,
442    /// Duration in seconds
443    pub duration_secs: f64,
444    /// Compression ratio (after/before)
445    pub compression_ratio: f64,
446}
447
448impl CompactionStats {
449    /// Get space savings percentage
450    pub fn savings_percentage(&self) -> f64 {
451        if self.size_before > 0 {
452            (self.space_saved as f64 / self.size_before as f64) * 100.0
453        } else {
454            0.0
455        }
456    }
457}
458
459/// Report from database repair operation
460#[derive(Debug, Clone)]
461pub struct RepairReport {
462    /// Number of issues found
463    pub issues_found: usize,
464    /// Number of issues fixed
465    pub issues_fixed: usize,
466    /// Duration in seconds
467    pub duration_secs: f64,
468    /// Whether repair was successful
469    pub success: bool,
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use crate::store::StorePresets;
476    use std::env;
477
478    fn create_test_base_dir() -> PathBuf {
479        env::temp_dir().join(format!("oxirs_dbops_test_{}", uuid::Uuid::new_v4()))
480    }
481
482    #[test]
483    fn test_create_database() {
484        let base_dir = create_test_base_dir();
485        let ops = DatabaseOps::new(&base_dir).unwrap();
486
487        let params = StorePresets::minimal(base_dir.join("test_db"))
488            .build()
489            .unwrap();
490
491        let metadata = ops.create_database("test_db", params).unwrap();
492
493        assert_eq!(metadata.name, "test_db");
494        assert_eq!(metadata.status, DatabaseStatus::Active);
495    }
496
497    #[test]
498    fn test_list_databases() {
499        let base_dir = create_test_base_dir();
500        let ops = DatabaseOps::new(&base_dir).unwrap();
501
502        let params1 = StorePresets::minimal(base_dir.join("db1")).build().unwrap();
503        let params2 = StorePresets::minimal(base_dir.join("db2")).build().unwrap();
504
505        ops.create_database("db1", params1).unwrap();
506        ops.create_database("db2", params2).unwrap();
507
508        let databases = ops.list_databases().unwrap();
509        assert_eq!(databases.len(), 2);
510    }
511
512    #[test]
513    fn test_delete_database() {
514        let base_dir = create_test_base_dir();
515        let ops = DatabaseOps::new(&base_dir).unwrap();
516
517        let params = StorePresets::minimal(base_dir.join("test_db"))
518            .build()
519            .unwrap();
520
521        ops.create_database("test_db", params).unwrap();
522        assert!(ops.get_metadata("test_db").is_ok());
523
524        ops.delete_database("test_db").unwrap();
525        assert!(ops.get_metadata("test_db").is_err());
526    }
527
528    #[test]
529    fn test_get_database_size() {
530        let base_dir = create_test_base_dir();
531        let ops = DatabaseOps::new(&base_dir).unwrap();
532
533        let params = StorePresets::minimal(base_dir.join("test_db"))
534            .build()
535            .unwrap();
536
537        ops.create_database("test_db", params).unwrap();
538
539        let size = ops.get_database_size("test_db").unwrap();
540        assert!(size > 0);
541    }
542
543    #[test]
544    fn test_copy_database() {
545        let base_dir = create_test_base_dir();
546        let ops = DatabaseOps::new(&base_dir).unwrap();
547
548        let params = StorePresets::minimal(base_dir.join("source_db"))
549            .build()
550            .unwrap();
551
552        ops.create_database("source_db", params).unwrap();
553        ops.copy_database("source_db", "dest_db").unwrap();
554
555        assert!(ops.get_metadata("source_db").is_ok());
556        assert!(ops.get_metadata("dest_db").is_ok());
557    }
558
559    #[test]
560    fn test_validate_database_name() {
561        let base_dir = create_test_base_dir();
562        let ops = DatabaseOps::new(&base_dir).unwrap();
563
564        assert!(ops.validate_database_name("valid_name").is_ok());
565        assert!(ops.validate_database_name("").is_err());
566        assert!(ops.validate_database_name("invalid/name").is_err());
567        assert!(ops.validate_database_name("invalid:name").is_err());
568    }
569
570    #[test]
571    fn test_compaction_stats() {
572        let stats = CompactionStats {
573            size_before: 1000,
574            size_after: 600,
575            space_saved: 400,
576            duration_secs: 1.5,
577            compression_ratio: 0.6,
578        };
579
580        assert_eq!(stats.savings_percentage(), 40.0);
581    }
582
583    #[test]
584    fn test_repair_report() {
585        let report = RepairReport {
586            issues_found: 5,
587            issues_fixed: 5,
588            duration_secs: 2.0,
589            success: true,
590        };
591
592        assert!(report.success);
593        assert_eq!(report.issues_found, 5);
594    }
595}