1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct DatabaseMetadata {
15 pub name: String,
17 pub location: PathBuf,
19 pub created_at: SystemTime,
21 pub modified_at: SystemTime,
23 pub version: String,
25 pub size_bytes: u64,
27 pub triple_count: u64,
29 pub status: DatabaseStatus,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum DatabaseStatus {
36 Active,
38 Creating,
40 Compacting,
42 BackingUp,
44 Repairing,
46 Offline,
48 Error,
50}
51
52pub struct DatabaseOps {
54 base_dir: PathBuf,
56}
57
58impl DatabaseOps {
59 pub fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
61 let base_dir = base_dir.as_ref().to_path_buf();
62
63 if !base_dir.exists() {
65 std::fs::create_dir_all(&base_dir)?;
66 }
67
68 Ok(Self { base_dir })
69 }
70
71 pub fn create_database(&self, name: &str, params: StoreParams) -> Result<DatabaseMetadata> {
73 let db_path = self.base_dir.join(name);
74
75 if db_path.exists() {
77 return Err(TdbError::InvalidInput(format!(
78 "Database '{}' already exists",
79 name
80 )));
81 }
82
83 self.validate_database_name(name)?;
85
86 params.validate()?;
88
89 std::fs::create_dir_all(&db_path)?;
91
92 let params_file = db_path.join("store_params.json");
95 params.save_to_file(¶ms_file)?;
96
97 let _store = TdbStore::open_with_params(&db_path, params)?;
100
101 let metadata = DatabaseMetadata {
103 name: name.to_string(),
104 location: db_path.clone(),
105 created_at: SystemTime::now(),
106 modified_at: SystemTime::now(),
107 version: crate::VERSION.to_string(),
108 size_bytes: self.calculate_database_size(&db_path)?,
109 triple_count: 0,
110 status: DatabaseStatus::Active,
111 };
112
113 self.save_metadata(&metadata)?;
115
116 log::info!("Created database '{}' at {:?}", name, db_path);
117
118 Ok(metadata)
119 }
120
121 fn open_store(&self, db_path: &Path) -> Result<TdbStore> {
130 let params_file = db_path.join("store_params.json");
131 if params_file.exists() {
132 let params = StoreParams::load_from_file(¶ms_file)?;
133 TdbStore::open_with_params(db_path, params)
134 } else {
135 TdbStore::open(db_path)
136 }
137 }
138
139 pub fn delete_database(&self, name: &str) -> Result<()> {
141 let db_path = self.base_dir.join(name);
142
143 if !db_path.exists() {
144 return Err(TdbError::InvalidInput(format!(
145 "Database '{}' does not exist",
146 name
147 )));
148 }
149
150 std::fs::remove_dir_all(&db_path)?;
152
153 log::info!("Deleted database '{}'", name);
154
155 Ok(())
156 }
157
158 pub fn list_databases(&self) -> Result<Vec<DatabaseMetadata>> {
160 let mut databases = Vec::new();
161
162 if !self.base_dir.exists() {
163 return Ok(databases);
164 }
165
166 for entry in std::fs::read_dir(&self.base_dir)? {
167 let entry = entry?;
168 let path = entry.path();
169
170 if path.is_dir() {
171 if let Ok(metadata) = self.load_metadata(&path) {
173 databases.push(metadata);
174 }
175 }
176 }
177
178 Ok(databases)
179 }
180
181 pub fn get_metadata(&self, name: &str) -> Result<DatabaseMetadata> {
183 let db_path = self.base_dir.join(name);
184
185 if !db_path.exists() {
186 return Err(TdbError::InvalidInput(format!(
187 "Database '{}' does not exist",
188 name
189 )));
190 }
191
192 self.load_metadata(&db_path)
193 }
194
195 pub fn compact_database(&self, name: &str) -> Result<CompactionStats> {
197 let db_path = self.base_dir.join(name);
198
199 if !db_path.exists() {
200 return Err(TdbError::InvalidInput(format!(
201 "Database '{}' does not exist",
202 name
203 )));
204 }
205
206 let mut metadata = self.load_metadata(&db_path)?;
208 metadata.status = DatabaseStatus::Compacting;
209 self.save_metadata(&metadata)?;
210
211 let start_time = SystemTime::now();
212 let size_before = self.calculate_database_size(&db_path)?;
213
214 let mut store = self.open_store(&db_path)?;
216 store.compact()?;
217
218 let size_after = self.calculate_database_size(&db_path)?;
219 let duration = start_time.elapsed().unwrap_or(Duration::from_secs(0));
220
221 metadata.status = DatabaseStatus::Active;
223 metadata.modified_at = SystemTime::now();
224 metadata.size_bytes = size_after;
225 self.save_metadata(&metadata)?;
226
227 let stats = CompactionStats {
228 size_before,
229 size_after,
230 space_saved: size_before.saturating_sub(size_after),
231 duration_secs: duration.as_secs_f64(),
232 compression_ratio: if size_before > 0 {
233 size_after as f64 / size_before as f64
234 } else {
235 1.0
236 },
237 };
238
239 log::info!(
240 "Compacted database '{}': saved {} bytes ({:.1}% reduction)",
241 name,
242 stats.space_saved,
243 (1.0 - stats.compression_ratio) * 100.0
244 );
245
246 Ok(stats)
247 }
248
249 pub fn repair_database(&self, name: &str) -> Result<RepairReport> {
251 let db_path = self.base_dir.join(name);
252
253 if !db_path.exists() {
254 return Err(TdbError::InvalidInput(format!(
255 "Database '{}' does not exist",
256 name
257 )));
258 }
259
260 let mut metadata = self.load_metadata(&db_path)?;
262 metadata.status = DatabaseStatus::Repairing;
263 self.save_metadata(&metadata)?;
264
265 let start_time = SystemTime::now();
266
267 let store = self.open_store(&db_path)?;
269 let diagnostic_report = store.run_diagnostics(crate::diagnostics::DiagnosticLevel::Deep);
270
271 let issues_found =
273 diagnostic_report.summary.error_count + diagnostic_report.summary.critical_count;
274 let issues_fixed = 0; let duration = start_time.elapsed().unwrap_or(Duration::from_secs(0));
277
278 metadata.status = if issues_found == issues_fixed {
280 DatabaseStatus::Active
281 } else {
282 DatabaseStatus::Error
283 };
284 metadata.modified_at = SystemTime::now();
285 self.save_metadata(&metadata)?;
286
287 let report = RepairReport {
288 issues_found,
289 issues_fixed,
290 duration_secs: duration.as_secs_f64(),
291 success: issues_found == issues_fixed,
292 };
293
294 log::info!(
295 "Repaired database '{}': {} issues found, {} fixed",
296 name,
297 report.issues_found,
298 report.issues_fixed
299 );
300
301 Ok(report)
302 }
303
304 pub fn copy_database(&self, source: &str, destination: &str) -> Result<()> {
306 let source_path = self.base_dir.join(source);
307 let dest_path = self.base_dir.join(destination);
308
309 if !source_path.exists() {
310 return Err(TdbError::InvalidInput(format!(
311 "Source database '{}' does not exist",
312 source
313 )));
314 }
315
316 if dest_path.exists() {
317 return Err(TdbError::InvalidInput(format!(
318 "Destination database '{}' already exists",
319 destination
320 )));
321 }
322
323 self.validate_database_name(destination)?;
325
326 self.copy_dir_recursive(&source_path, &dest_path)?;
328
329 if let Ok(mut metadata) = self.load_metadata(&dest_path) {
331 metadata.name = destination.to_string();
332 metadata.location = dest_path.clone();
333 metadata.created_at = SystemTime::now();
334 self.save_metadata(&metadata)?;
335 }
336
337 log::info!("Copied database '{}' to '{}'", source, destination);
338
339 Ok(())
340 }
341
342 pub fn get_database_size(&self, name: &str) -> Result<u64> {
344 let db_path = self.base_dir.join(name);
345
346 if !db_path.exists() {
347 return Err(TdbError::InvalidInput(format!(
348 "Database '{}' does not exist",
349 name
350 )));
351 }
352
353 self.calculate_database_size(&db_path)
354 }
355
356 fn validate_database_name(&self, name: &str) -> Result<()> {
358 if name.is_empty() {
359 return Err(TdbError::InvalidInput(
360 "Database name cannot be empty".to_string(),
361 ));
362 }
363
364 if name.contains(['/', '\\', ':', '*', '?', '"', '<', '>', '|']) {
366 return Err(TdbError::InvalidInput(format!(
367 "Database name '{}' contains invalid characters",
368 name
369 )));
370 }
371
372 Ok(())
373 }
374
375 #[allow(clippy::only_used_in_recursion)]
377 fn calculate_database_size(&self, path: &Path) -> Result<u64> {
378 let mut total_size = 0u64;
379
380 if path.is_dir() {
381 for entry in std::fs::read_dir(path)? {
382 let entry = entry?;
383 let entry_path = entry.path();
384
385 if entry_path.is_dir() {
386 total_size += self.calculate_database_size(&entry_path)?;
387 } else if entry_path.is_file() {
388 total_size += entry.metadata()?.len();
389 }
390 }
391 }
392
393 Ok(total_size)
394 }
395
396 fn save_metadata(&self, metadata: &DatabaseMetadata) -> Result<()> {
398 let metadata_file = metadata.location.join("metadata.json");
399 let json = serde_json::to_string_pretty(metadata)
400 .map_err(|e| TdbError::Serialization(format!("Failed to serialize metadata: {}", e)))?;
401 std::fs::write(metadata_file, json)?;
402 Ok(())
403 }
404
405 fn load_metadata(&self, db_path: &Path) -> Result<DatabaseMetadata> {
407 let metadata_file = db_path.join("metadata.json");
408
409 if !metadata_file.exists() {
410 let metadata = DatabaseMetadata {
412 name: db_path
413 .file_name()
414 .and_then(|n| n.to_str())
415 .unwrap_or("unknown")
416 .to_string(),
417 location: db_path.to_path_buf(),
418 created_at: SystemTime::now(),
419 modified_at: SystemTime::now(),
420 version: crate::VERSION.to_string(),
421 size_bytes: self.calculate_database_size(db_path)?,
422 triple_count: 0,
423 status: DatabaseStatus::Active,
424 };
425 self.save_metadata(&metadata)?;
426 return Ok(metadata);
427 }
428
429 let json = std::fs::read_to_string(metadata_file)?;
430 let metadata: DatabaseMetadata = serde_json::from_str(&json)
431 .map_err(|e| TdbError::Deserialization(format!("Failed to parse metadata: {}", e)))?;
432 Ok(metadata)
433 }
434
435 #[allow(clippy::only_used_in_recursion)]
437 fn copy_dir_recursive(&self, src: &Path, dst: &Path) -> Result<()> {
438 std::fs::create_dir_all(dst)?;
439
440 for entry in std::fs::read_dir(src)? {
441 let entry = entry?;
442 let src_path = entry.path();
443 let dst_path = dst.join(entry.file_name());
444
445 if src_path.is_dir() {
446 self.copy_dir_recursive(&src_path, &dst_path)?;
447 } else {
448 std::fs::copy(&src_path, &dst_path)?;
449 }
450 }
451
452 Ok(())
453 }
454}
455
456#[derive(Debug, Clone)]
458pub struct CompactionStats {
459 pub size_before: u64,
461 pub size_after: u64,
463 pub space_saved: u64,
465 pub duration_secs: f64,
467 pub compression_ratio: f64,
469}
470
471impl CompactionStats {
472 pub fn savings_percentage(&self) -> f64 {
474 if self.size_before > 0 {
475 (self.space_saved as f64 / self.size_before as f64) * 100.0
476 } else {
477 0.0
478 }
479 }
480}
481
482#[derive(Debug, Clone)]
484pub struct RepairReport {
485 pub issues_found: usize,
487 pub issues_fixed: usize,
489 pub duration_secs: f64,
491 pub success: bool,
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::store::{StoreParamsBuilder, StorePresets};
499 use std::env;
500
501 fn create_test_base_dir() -> PathBuf {
502 env::temp_dir().join(format!("oxirs_dbops_test_{}", uuid::Uuid::new_v4()))
503 }
504
505 #[test]
506 fn test_create_database() {
507 let base_dir = create_test_base_dir();
508 let ops = DatabaseOps::new(&base_dir).unwrap();
509
510 let params = StorePresets::minimal(base_dir.join("test_db"))
511 .build()
512 .unwrap();
513
514 let metadata = ops.create_database("test_db", params).unwrap();
515
516 assert_eq!(metadata.name, "test_db");
517 assert_eq!(metadata.status, DatabaseStatus::Active);
518 }
519
520 #[test]
521 fn test_list_databases() {
522 let base_dir = create_test_base_dir();
523 let ops = DatabaseOps::new(&base_dir).unwrap();
524
525 let params1 = StorePresets::minimal(base_dir.join("db1")).build().unwrap();
526 let params2 = StorePresets::minimal(base_dir.join("db2")).build().unwrap();
527
528 ops.create_database("db1", params1).unwrap();
529 ops.create_database("db2", params2).unwrap();
530
531 let databases = ops.list_databases().unwrap();
532 assert_eq!(databases.len(), 2);
533 }
534
535 #[test]
536 fn test_delete_database() {
537 let base_dir = create_test_base_dir();
538 let ops = DatabaseOps::new(&base_dir).unwrap();
539
540 let params = StorePresets::minimal(base_dir.join("test_db"))
541 .build()
542 .unwrap();
543
544 ops.create_database("test_db", params).unwrap();
545 assert!(ops.get_metadata("test_db").is_ok());
546
547 ops.delete_database("test_db").unwrap();
548 assert!(ops.get_metadata("test_db").is_err());
549 }
550
551 #[test]
552 fn test_get_database_size() {
553 let base_dir = create_test_base_dir();
554 let ops = DatabaseOps::new(&base_dir).unwrap();
555
556 let params = StorePresets::minimal(base_dir.join("test_db"))
557 .build()
558 .unwrap();
559
560 ops.create_database("test_db", params).unwrap();
561
562 let size = ops.get_database_size("test_db").unwrap();
563 assert!(size > 0);
564 }
565
566 #[test]
567 fn test_copy_database() {
568 let base_dir = create_test_base_dir();
569 let ops = DatabaseOps::new(&base_dir).unwrap();
570
571 let params = StorePresets::minimal(base_dir.join("source_db"))
572 .build()
573 .unwrap();
574
575 ops.create_database("source_db", params).unwrap();
576 ops.copy_database("source_db", "dest_db").unwrap();
577
578 assert!(ops.get_metadata("source_db").is_ok());
579 assert!(ops.get_metadata("dest_db").is_ok());
580 }
581
582 #[test]
583 fn test_validate_database_name() {
584 let base_dir = create_test_base_dir();
585 let ops = DatabaseOps::new(&base_dir).unwrap();
586
587 assert!(ops.validate_database_name("valid_name").is_ok());
588 assert!(ops.validate_database_name("").is_err());
589 assert!(ops.validate_database_name("invalid/name").is_err());
590 assert!(ops.validate_database_name("invalid:name").is_err());
591 }
592
593 #[test]
594 fn test_compaction_stats() {
595 let stats = CompactionStats {
596 size_before: 1000,
597 size_after: 600,
598 space_saved: 400,
599 duration_secs: 1.5,
600 compression_ratio: 0.6,
601 };
602
603 assert_eq!(stats.savings_percentage(), 40.0);
604 }
605
606 #[test]
607 fn test_reopen_preserves_store_params() {
608 let base_dir = create_test_base_dir();
609 let ops = DatabaseOps::new(&base_dir).unwrap();
610
611 let params = StoreParamsBuilder::new(base_dir.join("pdb"))
613 .buffer_pool_size(3333)
614 .build()
615 .unwrap();
616 ops.create_database("pdb", params).unwrap();
617
618 let db_path = base_dir.join("pdb");
621 let store = ops.open_store(&db_path).unwrap();
622 assert_eq!(store.buffer_pool.pool_size(), 3333);
623
624 drop(store);
625 std::fs::remove_dir_all(&base_dir).ok();
626 }
627
628 #[test]
629 fn test_repair_report() {
630 let report = RepairReport {
631 issues_found: 5,
632 issues_fixed: 5,
633 duration_secs: 2.0,
634 success: true,
635 };
636
637 assert!(report.success);
638 assert_eq!(report.issues_found, 5);
639 }
640}