1use crate::model::{Triple, TriplePattern};
7use crate::storage::StorageEngine;
8use crate::OxirsError;
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct VirtualStorageConfig {
20 pub path: PathBuf,
22 pub backends: Vec<BackendConfig>,
24 pub routing: RoutingPolicy,
26 pub migration: MigrationPolicy,
28 pub caching: bool,
30 pub cache_size_mb: usize,
32}
33
34impl Default for VirtualStorageConfig {
35 fn default() -> Self {
36 VirtualStorageConfig {
37 path: PathBuf::from("/var/oxirs/virtual"),
38 backends: vec![BackendConfig {
39 name: "primary".to_string(),
40 backend_type: BackendType::Tiered,
41 config: serde_json::json!({
42 "enable_tiering": true,
43 "enable_columnar": false,
44 "enable_temporal": false,
45 "compression": {
46 "Zstd": { "level": 3 }
47 },
48 "tiers": {
49 "hot_tier": {
50 "max_size_mb": 1024,
51 "eviction_policy": "Lru",
52 "ttl_seconds": 3600
53 },
54 "warm_tier": {
55 "path": "/tmp/oxirs_virtual_warm",
56 "max_size_gb": 10,
57 "promotion_threshold": 10,
58 "demotion_threshold_days": 7
59 },
60 "cold_tier": {
61 "path": "/tmp/oxirs_virtual_cold",
62 "max_size_tb": 1,
63 "compression_level": 9,
64 "archive_threshold_days": 90
65 },
66 "archive_tier": {
67 "backend": { "Local": "/tmp/oxirs_virtual_archive" },
68 "retention_years": 7,
69 "immutable": true
70 }
71 },
72 "cache_size_mb": 512
73 }),
74 weight: 1.0,
75 read_only: false,
76 }],
77 routing: RoutingPolicy::default(),
78 migration: MigrationPolicy::default(),
79 caching: true,
80 cache_size_mb: 1024,
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct BackendConfig {
88 pub name: String,
90 pub backend_type: BackendType,
92 pub config: serde_json::Value,
94 pub weight: f64,
96 pub read_only: bool,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum BackendType {
103 Tiered,
104 Columnar,
105 Immutable,
106 Temporal,
107 Remote { endpoint: String },
108 Cloud { provider: CloudProvider },
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum CloudProvider {
114 AWS { bucket: String, region: String },
115 GCP { bucket: String, project: String },
116 Azure { container: String, account: String },
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct RoutingPolicy {
122 pub read_strategy: ReadStrategy,
124 pub write_strategy: WriteStrategy,
126 pub query_hints: HashMap<String, String>,
128 pub predicate_rules: HashMap<String, String>,
130}
131
132impl Default for RoutingPolicy {
133 fn default() -> Self {
134 RoutingPolicy {
135 read_strategy: ReadStrategy::FirstAvailable,
136 write_strategy: WriteStrategy::All,
137 query_hints: HashMap::new(),
138 predicate_rules: HashMap::new(),
139 }
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub enum ReadStrategy {
146 FirstAvailable,
148 RoundRobin,
150 WeightedRandom,
152 Broadcast,
154 PatternBased,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub enum WriteStrategy {
161 All,
163 PrimaryOnly,
165 Quorum { n: usize },
167 Selective,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct MigrationPolicy {
174 pub auto_migrate: bool,
176 pub trigger: MigrationTrigger,
178 pub batch_size: usize,
180 pub rate_limit: Option<usize>,
182 pub rules: Vec<MigrationRule>,
184}
185
186impl Default for MigrationPolicy {
187 fn default() -> Self {
188 MigrationPolicy {
189 auto_migrate: false,
190 trigger: MigrationTrigger::Manual,
191 batch_size: 10000,
192 rate_limit: Some(100000),
193 rules: Vec::new(),
194 }
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub enum MigrationTrigger {
201 Manual,
203 StorageThreshold(f64),
205 Periodic(u32),
207 CostOptimization,
209 Performance,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct MigrationRule {
216 pub name: String,
218 pub source: String,
220 pub target: String,
222 pub criteria: SelectionCriteria,
224 pub priority: u32,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub enum SelectionCriteria {
231 Age(u32),
233 AccessFrequency { threshold: u32 },
235 Size(usize),
237 PredicatePattern(String),
239 Custom(String),
241}
242
243pub struct VirtualStorage {
245 config: VirtualStorageConfig,
246 backends: Arc<RwLock<HashMap<String, Arc<dyn StorageEngine>>>>,
248 routing_state: Arc<RwLock<RoutingState>>,
250 migration_state: Arc<RwLock<MigrationState>>,
252 cache: Arc<RwLock<StorageCache>>,
254 stats: Arc<RwLock<VirtualStorageStats>>,
256}
257
258struct RoutingState {
260 round_robin_counter: usize,
262 backend_health: HashMap<String, BackendHealth>,
264 #[allow(dead_code)]
266 active_migrations: Vec<String>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271struct BackendHealth {
272 healthy: bool,
274 last_check: u64,
276 failure_count: u32,
278 avg_response_time_ms: f64,
280}
281
282struct MigrationState {
284 active_migrations: HashMap<String, MigrationJob>,
286 history: Vec<MigrationRecord>,
288 #[allow(dead_code)]
290 coordinator: Option<MigrationCoordinator>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct MigrationJob {
296 id: String,
298 source: String,
300 target: String,
302 progress: MigrationProgress,
304 start_time: u64,
306 status: MigrationStatus,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312struct MigrationProgress {
313 total_triples: u64,
315 migrated_triples: u64,
317 failed_triples: u64,
319 current_batch: u64,
321 eta: Option<u64>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327enum MigrationStatus {
328 Pending,
329 Running,
330 Paused,
331 Completed,
332 Failed(String),
333 Cancelled,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338struct MigrationRecord {
339 job_id: String,
341 source: String,
343 target: String,
345 start_time: chrono::DateTime<chrono::Utc>,
347 end_time: chrono::DateTime<chrono::Utc>,
349 triples_migrated: u64,
351 status: MigrationStatus,
353}
354
355struct MigrationCoordinator {
357 #[allow(dead_code)]
359 workers: Vec<tokio::task::JoinHandle<()>>,
360 #[allow(dead_code)]
362 control_tx: tokio::sync::mpsc::Sender<MigrationControl>,
363}
364
365#[derive(Debug)]
367enum MigrationControl {
368 #[allow(dead_code)]
369 Pause,
370 #[allow(dead_code)]
371 Resume,
372 #[allow(dead_code)]
373 Cancel,
374 #[allow(dead_code)]
375 UpdateRateLimit(usize),
376}
377
378struct StorageCache {
380 triple_cache: lru::LruCache<u64, Triple>,
382 query_cache: lru::LruCache<String, Vec<Triple>>,
384 stats: CacheStats,
386}
387
388#[derive(Debug, Default)]
390struct CacheStats {
391 hits: u64,
392 misses: u64,
393 #[allow(dead_code)]
394 evictions: u64,
395}
396
397#[derive(Debug, Default)]
399struct VirtualStorageStats {
400 total_operations: u64,
402 #[allow(dead_code)]
404 backend_operations: HashMap<String, u64>,
405 #[allow(dead_code)]
407 migration_stats: MigrationStats,
408 performance: PerformanceMetrics,
410}
411
412#[derive(Debug, Default)]
414struct MigrationStats {
415 #[allow(dead_code)]
417 total_migrations: u64,
418 #[allow(dead_code)]
420 successful_migrations: u64,
421 #[allow(dead_code)]
423 failed_migrations: u64,
424 #[allow(dead_code)]
426 total_triples_migrated: u64,
427 #[allow(dead_code)]
429 total_migration_time_sec: u64,
430}
431
432#[derive(Debug, Default)]
434struct PerformanceMetrics {
435 avg_read_latency_ms: f64,
437 avg_write_latency_ms: f64,
439 query_throughput_qps: f64,
441 #[allow(dead_code)]
443 write_throughput_tps: f64,
444}
445
446impl VirtualStorage {
447 pub async fn new(config: VirtualStorageConfig) -> Result<Self, OxirsError> {
449 std::fs::create_dir_all(&config.path)?;
450
451 let cache_size = (config.cache_size_mb * 1024 * 1024) / 1000; Ok(VirtualStorage {
454 config: config.clone(),
455 backends: Arc::new(RwLock::new(HashMap::new())),
456 routing_state: Arc::new(RwLock::new(RoutingState {
457 round_robin_counter: 0,
458 backend_health: HashMap::new(),
459 active_migrations: Vec::new(),
460 })),
461 migration_state: Arc::new(RwLock::new(MigrationState {
462 active_migrations: HashMap::new(),
463 history: Vec::new(),
464 coordinator: None,
465 })),
466 cache: Arc::new(RwLock::new(StorageCache {
467 triple_cache: lru::LruCache::new(
468 std::num::NonZeroUsize::new(cache_size).unwrap_or(
469 std::num::NonZeroUsize::new(10000).expect("constant is non-zero"),
470 ),
471 ),
472 query_cache: lru::LruCache::new(
473 std::num::NonZeroUsize::new(1000).expect("constant is non-zero"),
474 ),
475 stats: CacheStats::default(),
476 })),
477 stats: Arc::new(RwLock::new(VirtualStorageStats::default())),
478 })
479 }
480
481 pub async fn initialize_backends(&self) -> Result<(), OxirsError> {
483 for backend_config in &self.config.backends {
484 let backend = self.create_backend(backend_config).await?;
485 let mut backends = self.backends.write().await;
486 backends.insert(backend_config.name.clone(), backend);
487
488 let mut routing = self.routing_state.write().await;
490 routing.backend_health.insert(
491 backend_config.name.clone(),
492 BackendHealth {
493 healthy: true,
494 last_check: std::time::SystemTime::now()
495 .duration_since(std::time::UNIX_EPOCH)
496 .expect("SystemTime should be after UNIX_EPOCH")
497 .as_secs(),
498 failure_count: 0,
499 avg_response_time_ms: 0.0,
500 },
501 );
502 }
503
504 self.start_health_monitoring();
506
507 Ok(())
508 }
509
510 pub async fn start_migration(
512 &self,
513 source: &str,
514 target: &str,
515 criteria: SelectionCriteria,
516 ) -> Result<String, OxirsError> {
517 let job_id = uuid::Uuid::new_v4().to_string();
518
519 let job = MigrationJob {
520 id: job_id.clone(),
521 source: source.to_string(),
522 target: target.to_string(),
523 progress: MigrationProgress {
524 total_triples: 0,
525 migrated_triples: 0,
526 failed_triples: 0,
527 current_batch: 0,
528 eta: None,
529 },
530 start_time: std::time::SystemTime::now()
531 .duration_since(std::time::UNIX_EPOCH)
532 .expect("SystemTime should be after UNIX_EPOCH")
533 .as_secs(),
534 status: MigrationStatus::Pending,
535 };
536
537 let mut migration_state = self.migration_state.write().await;
538 migration_state
539 .active_migrations
540 .insert(job_id.clone(), job);
541
542 self.spawn_migration_worker(
544 job_id.clone(),
545 source.to_string(),
546 target.to_string(),
547 criteria,
548 )
549 .await?;
550
551 Ok(job_id)
552 }
553
554 pub async fn get_migration_status(&self, job_id: &str) -> Result<MigrationJob, OxirsError> {
556 let migration_state = self.migration_state.read().await;
557 migration_state
558 .active_migrations
559 .get(job_id)
560 .cloned()
561 .ok_or_else(|| OxirsError::Store(format!("Migration job not found: {job_id}")))
562 }
563
564 async fn create_backend(
566 &self,
567 config: &BackendConfig,
568 ) -> Result<Arc<dyn StorageEngine>, OxirsError> {
569 match &config.backend_type {
570 BackendType::Tiered => {
571 Err(OxirsError::Store(
573 "Tiered backend persistent storage is not available in this build".to_string(),
574 ))
575 }
576 BackendType::Columnar => {
577 Err(OxirsError::Store(
579 "Columnar backend not yet integrated".to_string(),
580 ))
581 }
582 BackendType::Immutable => {
583 Err(OxirsError::Store(
585 "Immutable backend not yet integrated".to_string(),
586 ))
587 }
588 BackendType::Temporal => {
589 Err(OxirsError::Store(
591 "Temporal backend not yet integrated".to_string(),
592 ))
593 }
594 BackendType::Remote { endpoint } => {
595 Err(OxirsError::Store(format!(
597 "Remote backend not implemented: {endpoint}"
598 )))
599 }
600 BackendType::Cloud { provider: _ } => {
601 Err(OxirsError::Store(
603 "Cloud backend not implemented".to_string(),
604 ))
605 }
606 }
607 }
608
609 async fn route_read(&self) -> Result<Vec<String>, OxirsError> {
611 let routing_state = self.routing_state.read().await;
612 let backends = self.backends.read().await;
613
614 match self.config.routing.read_strategy {
615 ReadStrategy::FirstAvailable => {
616 for (name, health) in &routing_state.backend_health {
618 if health.healthy && backends.contains_key(name) {
619 return Ok(vec![name.clone()]);
620 }
621 }
622 Err(OxirsError::Store(
623 "No healthy backends available".to_string(),
624 ))
625 }
626 ReadStrategy::RoundRobin => {
627 let healthy_backends: Vec<_> = routing_state
629 .backend_health
630 .iter()
631 .filter(|(name, health)| health.healthy && backends.contains_key(*name))
632 .map(|(name, _)| name.clone())
633 .collect();
634
635 if healthy_backends.is_empty() {
636 return Err(OxirsError::Store(
637 "No healthy backends available".to_string(),
638 ));
639 }
640
641 let index = routing_state.round_robin_counter % healthy_backends.len();
642 Ok(vec![healthy_backends[index].clone()])
643 }
644 ReadStrategy::Broadcast => {
645 Ok(routing_state
647 .backend_health
648 .iter()
649 .filter(|(name, health)| health.healthy && backends.contains_key(*name))
650 .map(|(name, _)| name.clone())
651 .collect())
652 }
653 _ => Ok(vec!["primary".to_string()]), }
655 }
656
657 async fn route_write(&self) -> Result<Vec<String>, OxirsError> {
659 let routing_state = self.routing_state.read().await;
660 let backends = self.backends.read().await;
661
662 match self.config.routing.write_strategy {
663 WriteStrategy::All => {
664 Ok(self
666 .config
667 .backends
668 .iter()
669 .filter(|b| !b.read_only)
670 .filter(|b| {
671 routing_state
672 .backend_health
673 .get(&b.name)
674 .map(|h| h.healthy)
675 .unwrap_or(false)
676 })
677 .filter(|b| backends.contains_key(&b.name))
678 .map(|b| b.name.clone())
679 .collect())
680 }
681 WriteStrategy::PrimaryOnly => Ok(vec!["primary".to_string()]),
682 WriteStrategy::Quorum { n } => {
683 let writable: Vec<_> = self
685 .config
686 .backends
687 .iter()
688 .filter(|b| !b.read_only)
689 .filter(|b| {
690 routing_state
691 .backend_health
692 .get(&b.name)
693 .map(|h| h.healthy)
694 .unwrap_or(false)
695 })
696 .filter(|b| backends.contains_key(&b.name))
697 .take(n)
698 .map(|b| b.name.clone())
699 .collect();
700
701 if writable.len() < n {
702 return Err(OxirsError::Store(format!(
703 "Not enough healthy backends for quorum: need {}, have {}",
704 n,
705 writable.len()
706 )));
707 }
708
709 Ok(writable)
710 }
711 WriteStrategy::Selective => {
712 Ok(vec!["primary".to_string()]) }
715 }
716 }
717
718 fn start_health_monitoring(&self) {
720 let backends = self.backends.clone();
721 let routing_state = self.routing_state.clone();
722
723 tokio::spawn(async move {
724 let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
725
726 loop {
727 interval.tick().await;
728
729 let backend_list = backends.read().await;
731 for (name, backend) in backend_list.iter() {
732 let start = std::time::Instant::now();
733 let health_check = backend.stats().await;
734 let elapsed = start.elapsed();
735
736 let mut routing = routing_state.write().await;
737 if let Some(health) = routing.backend_health.get_mut(name) {
738 health.last_check = std::time::SystemTime::now()
739 .duration_since(std::time::UNIX_EPOCH)
740 .expect("SystemTime should be after UNIX_EPOCH")
741 .as_secs();
742 health.avg_response_time_ms = elapsed.as_millis() as f64;
743
744 if health_check.is_ok() {
745 health.healthy = true;
746 health.failure_count = 0;
747 } else {
748 health.failure_count += 1;
749 if health.failure_count >= 3 {
750 health.healthy = false;
751 }
752 }
753 }
754 }
755 }
756 });
757 }
758
759 async fn spawn_migration_worker(
761 &self,
762 job_id: String,
763 _source: String,
764 _target: String,
765 _criteria: SelectionCriteria,
766 ) -> Result<(), OxirsError> {
767 let _backends = self.backends.clone();
768 let migration_state = self.migration_state.clone();
769 let _config = self.config.clone();
770
771 tokio::spawn(async move {
772 let mut state = migration_state.write().await;
775 if let Some(job) = state.active_migrations.get_mut(&job_id) {
776 job.status = MigrationStatus::Running;
777 }
778
779 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
781
782 let mut state = migration_state.write().await;
784 if let Some(job) = state.active_migrations.get_mut(&job_id) {
785 job.status = MigrationStatus::Completed;
786 job.progress.migrated_triples = 1000; }
788 });
789
790 Ok(())
791 }
792}
793
794#[async_trait]
795impl StorageEngine for VirtualStorage {
796 async fn init(&mut self, _config: super::StorageConfig) -> Result<(), OxirsError> {
797 self.initialize_backends().await
798 }
799
800 async fn store_triple(&self, triple: &Triple) -> Result<(), OxirsError> {
801 let start = std::time::Instant::now();
802
803 let target_backends = self.route_write().await?;
805 let backends = self.backends.read().await;
806
807 let mut errors = Vec::new();
808 for backend_name in &target_backends {
809 if let Some(backend) = backends.get(backend_name) {
810 if let Err(e) = backend.store_triple(triple).await {
811 errors.push((backend_name.clone(), e));
812 }
813 }
814 }
815
816 if self.config.caching {
818 let mut cache = self.cache.write().await;
819 let hash = self.hash_triple(triple);
820 cache.triple_cache.put(hash, triple.clone());
821 }
822
823 let elapsed = start.elapsed();
825 let mut stats = self.stats.write().await;
826 stats.total_operations += 1;
827 stats.performance.avg_write_latency_ms = (stats.performance.avg_write_latency_ms
828 * (stats.total_operations - 1) as f64
829 + elapsed.as_millis() as f64)
830 / stats.total_operations as f64;
831
832 match self.config.routing.write_strategy {
834 WriteStrategy::All if !errors.is_empty() => {
835 return Err(OxirsError::Store(format!(
836 "Failed to write to backends: {errors:?}"
837 )));
838 }
839 WriteStrategy::Quorum { n } if target_backends.len() - errors.len() < n => {
840 return Err(OxirsError::Store(format!(
841 "Quorum write failed: {} successes, needed {}",
842 target_backends.len() - errors.len(),
843 n
844 )));
845 }
846 _ => {}
847 }
848
849 Ok(())
850 }
851
852 async fn store_triples(&self, triples: &[Triple]) -> Result<(), OxirsError> {
853 for triple in triples {
855 self.store_triple(triple).await?;
856 }
857 Ok(())
858 }
859
860 async fn query_triples(&self, pattern: &TriplePattern) -> Result<Vec<Triple>, OxirsError> {
861 let start = std::time::Instant::now();
862
863 if self.config.caching {
865 let pattern_key = format!("{pattern:?}");
866 let mut cache = self.cache.write().await;
867 if let Some(cached) = cache.query_cache.get(&pattern_key).cloned() {
868 cache.stats.hits += 1;
869 return Ok(cached);
870 }
871 cache.stats.misses += 1;
872 }
873
874 let target_backends = self.route_read().await?;
876 let backends = self.backends.read().await;
877
878 let mut all_results = Vec::new();
879 let mut seen = std::collections::HashSet::new();
880
881 for backend_name in &target_backends {
882 if let Some(backend) = backends.get(backend_name) {
883 match backend.query_triples(pattern).await {
884 Ok(results) => {
885 for triple in results {
886 let hash = self.hash_triple(&triple);
887 if seen.insert(hash) {
888 all_results.push(triple);
889 }
890 }
891 }
892 Err(e) => {
893 tracing::warn!("Query failed on backend {}: {}", backend_name, e);
894 }
895 }
896 }
897 }
898
899 if self.config.caching && !all_results.is_empty() {
901 let pattern_key = format!("{pattern:?}");
902 let mut cache = self.cache.write().await;
903 cache.query_cache.put(pattern_key, all_results.clone());
904 }
905
906 let elapsed = start.elapsed();
908 let mut stats = self.stats.write().await;
909 stats.total_operations += 1;
910 stats.performance.avg_read_latency_ms = (stats.performance.avg_read_latency_ms
911 * (stats.total_operations - 1) as f64
912 + elapsed.as_millis() as f64)
913 / stats.total_operations as f64;
914
915 Ok(all_results)
916 }
917
918 async fn delete_triples(&self, pattern: &TriplePattern) -> Result<usize, OxirsError> {
919 let target_backends = self.route_write().await?;
921 let backends = self.backends.read().await;
922
923 let mut total_deleted = 0;
924 for backend_name in &target_backends {
925 if let Some(backend) = backends.get(backend_name) {
926 match backend.delete_triples(pattern).await {
927 Ok(count) => total_deleted = total_deleted.max(count),
928 Err(e) => {
929 tracing::warn!("Delete failed on backend {}: {}", backend_name, e);
930 }
931 }
932 }
933 }
934
935 if self.config.caching {
937 let mut cache = self.cache.write().await;
938 cache.query_cache.clear();
939 }
941
942 Ok(total_deleted)
943 }
944
945 async fn stats(&self) -> Result<super::StorageStats, OxirsError> {
946 let backends = self.backends.read().await;
947 let stats = self.stats.read().await;
948
949 let mut total_triples = 0u64;
951 let mut total_size = 0u64;
952
953 for (_, backend) in backends.iter() {
954 if let Ok(backend_stats) = backend.stats().await {
955 total_triples += backend_stats.total_triples;
956 total_size += backend_stats.total_size_bytes;
957 }
958 }
959
960 Ok(super::StorageStats {
961 total_triples,
962 total_size_bytes: total_size,
963 tier_stats: super::TierStats {
964 hot: super::TierStat {
965 triple_count: 0,
966 size_bytes: 0,
967 hit_rate: 0.0,
968 avg_access_time_us: 0,
969 },
970 warm: super::TierStat {
971 triple_count: 0,
972 size_bytes: 0,
973 hit_rate: 0.0,
974 avg_access_time_us: 0,
975 },
976 cold: super::TierStat {
977 triple_count: 0,
978 size_bytes: 0,
979 hit_rate: 0.0,
980 avg_access_time_us: 0,
981 },
982 archive: super::TierStat {
983 triple_count: 0,
984 size_bytes: 0,
985 hit_rate: 0.0,
986 avg_access_time_us: 0,
987 },
988 },
989 compression_ratio: 1.0,
990 query_metrics: super::QueryMetrics {
991 avg_query_time_ms: stats.performance.avg_read_latency_ms,
992 p99_query_time_ms: stats.performance.avg_read_latency_ms * 2.0, qps: stats.performance.query_throughput_qps,
994 cache_hit_rate: if self.config.caching {
995 let cache = self.cache.read().await;
996 let total = cache.stats.hits + cache.stats.misses;
997 if total > 0 {
998 (cache.stats.hits as f64 / total as f64) * 100.0
999 } else {
1000 0.0
1001 }
1002 } else {
1003 0.0
1004 },
1005 },
1006 })
1007 }
1008
1009 async fn optimize(&self) -> Result<(), OxirsError> {
1010 let backends = self.backends.read().await;
1011
1012 for (name, backend) in backends.iter() {
1014 if let Err(e) = backend.optimize().await {
1015 tracing::warn!("Optimization failed on backend {}: {}", name, e);
1016 }
1017 }
1018
1019 if self.config.caching {
1021 let mut cache = self.cache.write().await;
1022 cache.query_cache.clear();
1023 }
1024
1025 Ok(())
1026 }
1027
1028 async fn backup(&self, path: &Path) -> Result<(), OxirsError> {
1029 std::fs::create_dir_all(path)?;
1030
1031 let backends = self.backends.read().await;
1032
1033 for (name, backend) in backends.iter() {
1035 let backend_path = path.join(name);
1036 backend.backup(&backend_path).await?;
1037 }
1038
1039 let metadata = VirtualStorageMetadata {
1041 config: self.config.clone(),
1042 migration_history: {
1043 let state = self.migration_state.read().await;
1044 state.history.clone()
1045 },
1046 };
1047
1048 let metadata_path = path.join("virtual_metadata.json");
1049 let metadata_json = serde_json::to_string_pretty(&metadata)
1050 .map_err(|e| OxirsError::Serialize(e.to_string()))?;
1051 std::fs::write(metadata_path, metadata_json)?;
1052
1053 Ok(())
1054 }
1055
1056 async fn restore(&self, path: &Path) -> Result<(), OxirsError> {
1057 let metadata_path = path.join("virtual_metadata.json");
1059 let metadata_json = std::fs::read_to_string(metadata_path)?;
1060 let metadata: VirtualStorageMetadata =
1061 serde_json::from_str(&metadata_json).map_err(|e| OxirsError::Parse(e.to_string()))?;
1062
1063 let backends = self.backends.read().await;
1064
1065 for (name, backend) in backends.iter() {
1067 let backend_path = path.join(name);
1068 if backend_path.exists() {
1069 backend.restore(&backend_path).await?;
1070 }
1071 }
1072
1073 let mut state = self.migration_state.write().await;
1075 state.history = metadata.migration_history;
1076
1077 Ok(())
1078 }
1079}
1080
1081#[derive(Debug, Serialize, Deserialize)]
1083struct VirtualStorageMetadata {
1084 config: VirtualStorageConfig,
1085 migration_history: Vec<MigrationRecord>,
1086}
1087
1088impl VirtualStorage {
1089 fn hash_triple(&self, triple: &Triple) -> u64 {
1091 use std::hash::{Hash, Hasher};
1092 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1093 triple.hash(&mut hasher);
1094 hasher.finish()
1095 }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use super::*;
1101 use crate::model::{Literal, NamedNode};
1102
1103 #[tokio::test]
1104 #[ignore = "Virtual storage backends not yet implemented - all backend types are disabled"]
1105 async fn test_virtual_storage() {
1106 let dir = tempfile::tempdir().expect("tempdir");
1107 let test_dir = dir.path().join("oxirs_virtual_test").display().to_string();
1108
1109 let mut config = VirtualStorageConfig {
1110 path: PathBuf::from(&test_dir),
1111 ..Default::default()
1112 };
1113
1114 if let Some(backend) = config.backends.get_mut(0) {
1116 backend.config = serde_json::json!({
1117 "enable_tiering": true,
1118 "enable_columnar": false,
1119 "enable_temporal": false,
1120 "compression": {
1121 "Zstd": { "level": 3 }
1122 },
1123 "tiers": {
1124 "hot_tier": {
1125 "max_size_mb": 1024,
1126 "eviction_policy": "Lru",
1127 "ttl_seconds": 3600
1128 },
1129 "warm_tier": {
1130 "path": format!("{test_dir}/warm"),
1131 "max_size_gb": 10,
1132 "promotion_threshold": 10,
1133 "demotion_threshold_days": 7
1134 },
1135 "cold_tier": {
1136 "path": format!("{test_dir}/cold"),
1137 "max_size_tb": 1,
1138 "compression_level": 9,
1139 "archive_threshold_days": 90
1140 },
1141 "archive_tier": {
1142 "backend": { "Local": format!("{test_dir}/archive") },
1143 "retention_years": 7,
1144 "immutable": true
1145 }
1146 },
1147 "cache_size_mb": 512
1148 });
1149 }
1150
1151 let storage = VirtualStorage::new(config)
1152 .await
1153 .expect("async operation should succeed");
1154 storage
1155 .initialize_backends()
1156 .await
1157 .expect("async operation should succeed");
1158
1159 let triple = Triple::new(
1161 NamedNode::new("http://example.org/s").expect("valid IRI"),
1162 NamedNode::new("http://example.org/p").expect("valid IRI"),
1163 crate::model::Object::Literal(Literal::new("test")),
1164 );
1165
1166 storage
1168 .store_triple(&triple)
1169 .await
1170 .expect("async operation should succeed");
1171
1172 let pattern = TriplePattern::new(None, None, None);
1174 let results = storage
1175 .query_triples(&pattern)
1176 .await
1177 .expect("async operation should succeed");
1178 assert_eq!(results.len(), 1);
1179 assert_eq!(results[0], triple);
1180 }
1181}