Skip to main content

oxirs_gql/
dynamic_query_planner.rs

1//! Dynamic Query Plan Adaptation
2//!
3//! This module provides intelligent, runtime-adaptive query planning that automatically
4//! adjusts execution strategies based on real-time performance metrics, resource availability,
5//! and historical query patterns.
6//!
7//! ## Features
8//!
9//! - **Adaptive Strategy Selection**: Dynamically chooses optimal execution strategy
10//! - **Performance Monitoring**: Real-time tracking of query execution metrics
11//! - **Resource-Aware Planning**: Adapts to CPU, memory, and network conditions
12//! - **Cost-Based Optimization**: Uses historical cost data for better plans
13//! - **Fallback Strategies**: Automatic degradation under high load
14//! - **Learning-Based Adaptation**: Improves over time with ML-based predictions
15
16use anyhow::Result;
17use scirs2_core::metrics::{Counter, Gauge};
18use serde::{Deserialize, Serialize};
19use std::collections::{HashMap, VecDeque};
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22use sysinfo::System;
23use tokio::sync::RwLock;
24
25use crate::historical_cost_estimator::HistoricalCostEstimator;
26use crate::ml_optimizer::{MLOptimizerConfig, MLQueryOptimizer, QueryFeatures, TrainingSample};
27use crate::performance::PerformanceTracker;
28
29/// Configuration for dynamic query planning
30#[derive(Debug, Clone)]
31pub struct DynamicPlannerConfig {
32    /// Enable dynamic query plan adaptation
33    pub enabled: bool,
34    /// Minimum execution time to trigger adaptation (ms)
35    pub min_adaptation_threshold_ms: f64,
36    /// CPU usage threshold for degradation (0.0-1.0)
37    pub cpu_threshold: f64,
38    /// Memory usage threshold for degradation (0.0-1.0)
39    pub memory_threshold: f64,
40    /// Enable ML-based strategy prediction
41    pub enable_ml_prediction: bool,
42    /// Number of recent executions to track
43    pub history_size: usize,
44    /// Adaptation interval (how often to reassess strategy)
45    pub adaptation_interval: Duration,
46    /// Enable aggressive optimization under high load
47    pub aggressive_mode_enabled: bool,
48}
49
50impl Default for DynamicPlannerConfig {
51    fn default() -> Self {
52        Self {
53            enabled: true,
54            min_adaptation_threshold_ms: 100.0,
55            cpu_threshold: 0.80,    // 80% CPU usage triggers degradation
56            memory_threshold: 0.85, // 85% memory usage triggers degradation
57            enable_ml_prediction: true,
58            history_size: 100,
59            adaptation_interval: Duration::from_secs(10),
60            aggressive_mode_enabled: true,
61        }
62    }
63}
64
65/// Query execution strategy
66#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
67pub enum ExecutionStrategy {
68    /// Standard sequential execution
69    Sequential,
70    /// Parallel field resolution
71    Parallel,
72    /// Batched execution with DataLoader
73    Batched,
74    /// Streaming results as they become available
75    Streaming,
76    /// Cached result (no execution needed)
77    Cached,
78    /// Optimized with query rewriting
79    Optimized,
80    /// Degraded mode for high load
81    Degraded,
82}
83
84impl ExecutionStrategy {
85    /// Get the estimated overhead of this strategy
86    pub fn overhead_multiplier(&self) -> f64 {
87        match self {
88            Self::Sequential => 1.0,
89            Self::Parallel => 1.2,  // Slight overhead for parallelization
90            Self::Batched => 0.8,   // Reduces N+1 queries
91            Self::Streaming => 1.1, // Small overhead for streaming
92            Self::Cached => 0.01,   // Minimal overhead
93            Self::Optimized => 0.7, // Best performance with optimization
94            Self::Degraded => 1.5,  // Higher overhead, but safer under load
95        }
96    }
97
98    /// Check if this strategy is suitable for high load conditions
99    pub fn is_high_load_safe(&self) -> bool {
100        matches!(self, Self::Degraded | Self::Cached | Self::Sequential)
101    }
102}
103
104/// System resource snapshot
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ResourceSnapshot {
107    #[serde(skip, default = "Instant::now")]
108    pub timestamp: Instant,
109    pub cpu_usage: f64,
110    pub memory_usage: f64,
111    pub active_queries: usize,
112    pub avg_query_time_ms: f64,
113}
114
115impl ResourceSnapshot {
116    /// Check if system is under high load
117    pub fn is_high_load(&self, config: &DynamicPlannerConfig) -> bool {
118        self.cpu_usage > config.cpu_threshold || self.memory_usage > config.memory_threshold
119    }
120
121    /// Get load severity (0.0 = no load, 1.0 = critical)
122    pub fn load_severity(&self, config: &DynamicPlannerConfig) -> f64 {
123        let cpu_severity = (self.cpu_usage / config.cpu_threshold).min(1.0);
124        let memory_severity = (self.memory_usage / config.memory_threshold).min(1.0);
125
126        cpu_severity.max(memory_severity)
127    }
128}
129
130/// Query plan with adaptive strategy
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct AdaptivePlan {
133    pub query_fingerprint: String,
134    pub strategy: ExecutionStrategy,
135    pub estimated_cost: f64,
136    pub estimated_time_ms: f64,
137    pub confidence: f64,
138    pub fallback_strategy: Option<ExecutionStrategy>,
139    #[serde(skip, default = "Instant::now")]
140    pub created_at: Instant,
141    pub metadata: HashMap<String, String>,
142}
143
144impl AdaptivePlan {
145    pub fn new(
146        query_fingerprint: String,
147        strategy: ExecutionStrategy,
148        estimated_cost: f64,
149    ) -> Self {
150        Self {
151            query_fingerprint,
152            strategy,
153            estimated_cost,
154            estimated_time_ms: estimated_cost * 10.0, // Rough heuristic
155            confidence: 0.5,
156            fallback_strategy: None,
157            created_at: Instant::now(),
158            metadata: HashMap::new(),
159        }
160    }
161
162    pub fn with_fallback(mut self, fallback: ExecutionStrategy) -> Self {
163        self.fallback_strategy = Some(fallback);
164        self
165    }
166
167    pub fn with_confidence(mut self, confidence: f64) -> Self {
168        self.confidence = confidence.clamp(0.0, 1.0);
169        self
170    }
171
172    pub fn with_estimated_time(mut self, time_ms: f64) -> Self {
173        self.estimated_time_ms = time_ms;
174        self
175    }
176}
177
178/// Execution result for feedback
179#[derive(Debug, Clone)]
180pub struct ExecutionResult {
181    pub query_fingerprint: String,
182    pub strategy_used: ExecutionStrategy,
183    pub actual_time_ms: f64,
184    pub success: bool,
185    pub error_message: Option<String>,
186    pub resource_snapshot: ResourceSnapshot,
187}
188
189/// Dynamic query planner with adaptive strategy selection
190pub struct DynamicQueryPlanner {
191    config: DynamicPlannerConfig,
192    cost_estimator: Arc<RwLock<HistoricalCostEstimator>>,
193    ml_optimizer: Option<Arc<RwLock<MLQueryOptimizer>>>,
194    execution_history: Arc<RwLock<VecDeque<ExecutionResult>>>,
195    strategy_performance: Arc<RwLock<HashMap<ExecutionStrategy, StrategyStats>>>,
196    system: Arc<RwLock<System>>,
197
198    // Metrics
199    adaptations: Arc<Counter>,
200    strategy_switches: Arc<Counter>,
201    high_load_events: Arc<Counter>,
202    avg_adaptation_quality: Arc<Gauge>,
203}
204
205/// Performance statistics for each strategy
206#[derive(Debug, Clone)]
207pub struct StrategyStats {
208    pub total_executions: u64,
209    pub successful_executions: u64,
210    pub total_time_ms: f64,
211    pub avg_time_ms: f64,
212    #[allow(dead_code)]
213    pub p95_time_ms: f64,
214    pub error_rate: f64,
215    #[allow(dead_code)]
216    last_used: Instant,
217}
218
219impl Default for StrategyStats {
220    fn default() -> Self {
221        Self {
222            total_executions: 0,
223            successful_executions: 0,
224            total_time_ms: 0.0,
225            avg_time_ms: 0.0,
226            p95_time_ms: 0.0,
227            error_rate: 0.0,
228            last_used: Instant::now(),
229        }
230    }
231}
232
233impl DynamicQueryPlanner {
234    pub fn new(config: DynamicPlannerConfig) -> Self {
235        let ml_optimizer = if config.enable_ml_prediction {
236            let tracker = Arc::new(PerformanceTracker::new());
237            let optimizer = MLQueryOptimizer::new(MLOptimizerConfig::default(), tracker);
238            Some(Arc::new(RwLock::new(optimizer)))
239        } else {
240            None
241        };
242
243        Self {
244            cost_estimator: Arc::new(RwLock::new(HistoricalCostEstimator::new())),
245            ml_optimizer,
246            execution_history: Arc::new(RwLock::new(VecDeque::with_capacity(config.history_size))),
247            strategy_performance: Arc::new(RwLock::new(HashMap::new())),
248            system: Arc::new(RwLock::new(System::new_all())),
249            adaptations: Arc::new(Counter::new("dynamic_planner_adaptations".to_string())),
250            strategy_switches: Arc::new(Counter::new(
251                "dynamic_planner_strategy_switches".to_string(),
252            )),
253            high_load_events: Arc::new(Counter::new(
254                "dynamic_planner_high_load_events".to_string(),
255            )),
256            avg_adaptation_quality: Arc::new(Gauge::new(
257                "dynamic_planner_adaptation_quality".to_string(),
258            )),
259            config,
260        }
261    }
262
263    /// Create an adaptive plan for a query
264    pub async fn create_plan(&self, query: &str, query_complexity: f64) -> Result<AdaptivePlan> {
265        if !self.config.enabled {
266            // Return basic sequential plan
267            return Ok(AdaptivePlan::new(
268                Self::fingerprint_query(query),
269                ExecutionStrategy::Sequential,
270                query_complexity,
271            ));
272        }
273
274        let query_fingerprint = Self::fingerprint_query(query);
275
276        // Get current resource snapshot
277        let snapshot = self.get_resource_snapshot().await?;
278
279        // Check for high load condition
280        let is_high_load = snapshot.is_high_load(&self.config);
281        if is_high_load {
282            self.high_load_events.inc();
283        }
284
285        // Get historical cost estimate
286        let cost_estimate = {
287            let estimator = self.cost_estimator.read().await;
288            estimator.estimate_cost(&query_fingerprint).ok()
289        };
290
291        // Determine optimal strategy
292        let strategy = self
293            .select_optimal_strategy(
294                &query_fingerprint,
295                query_complexity,
296                &snapshot,
297                cost_estimate.as_ref(),
298            )
299            .await?;
300
301        // Select fallback strategy
302        let fallback = self.select_fallback_strategy(strategy, &snapshot);
303
304        // Build adaptive plan
305        let mut plan = AdaptivePlan::new(query_fingerprint.clone(), strategy, query_complexity)
306            .with_fallback(fallback);
307
308        // Add estimated time from historical data
309        if let Some(estimate) = cost_estimate {
310            plan = plan
311                .with_estimated_time(estimate.estimated_time_ms)
312                .with_confidence(estimate.confidence);
313        }
314
315        self.adaptations.inc();
316
317        Ok(plan)
318    }
319
320    /// Select the optimal execution strategy based on current conditions
321    async fn select_optimal_strategy(
322        &self,
323        query_fingerprint: &str,
324        complexity: f64,
325        snapshot: &ResourceSnapshot,
326        cost_estimate: Option<&crate::historical_cost_estimator::CostEstimate>,
327    ) -> Result<ExecutionStrategy> {
328        // High load: use safe strategies
329        if snapshot.is_high_load(&self.config) {
330            return Ok(self.select_high_load_strategy(snapshot).await);
331        }
332
333        // Check if ML prediction is available
334        if let Some(ml_opt) = &self.ml_optimizer {
335            let ml = ml_opt.read().await;
336            if let Ok(strategy) = self
337                .ml_predict_strategy(&ml, query_fingerprint, complexity, snapshot)
338                .await
339            {
340                return Ok(strategy);
341            }
342        }
343
344        // Use historical performance to select strategy
345        if let Some(estimate) = cost_estimate {
346            return Ok(self
347                .select_strategy_from_history(estimate, complexity, snapshot)
348                .await);
349        }
350
351        // Default selection based on complexity
352        Ok(self.select_default_strategy(complexity, snapshot))
353    }
354
355    /// Select strategy using ML prediction
356    async fn ml_predict_strategy(
357        &self,
358        _ml: &MLQueryOptimizer,
359        _query_fingerprint: &str,
360        complexity: f64,
361        snapshot: &ResourceSnapshot,
362    ) -> Result<ExecutionStrategy> {
363        // Create feature vector for ML prediction (currently unused)
364        let _features = [
365            complexity,
366            snapshot.cpu_usage,
367            snapshot.memory_usage,
368            snapshot.active_queries as f64,
369            snapshot.avg_query_time_ms,
370        ];
371
372        // Get strategy performance stats
373        let stats = self.strategy_performance.read().await;
374
375        // Score each strategy
376        let mut best_strategy = ExecutionStrategy::Sequential;
377        let mut best_score = f64::MIN;
378
379        for strategy in &[
380            ExecutionStrategy::Sequential,
381            ExecutionStrategy::Parallel,
382            ExecutionStrategy::Batched,
383            ExecutionStrategy::Streaming,
384            ExecutionStrategy::Optimized,
385        ] {
386            let score = if let Some(stat) = stats.get(strategy) {
387                // Factor in success rate and average time
388                let success_rate =
389                    stat.successful_executions as f64 / stat.total_executions.max(1) as f64;
390                let time_penalty = stat.avg_time_ms / 1000.0; // Normalize
391                (success_rate * 10.0) - time_penalty
392            } else {
393                // No history - use default scoring
394                match strategy {
395                    ExecutionStrategy::Optimized => 8.0,
396                    ExecutionStrategy::Parallel => 7.0,
397                    ExecutionStrategy::Batched => 6.0,
398                    ExecutionStrategy::Streaming => 5.0,
399                    ExecutionStrategy::Sequential => 4.0,
400                    ExecutionStrategy::Cached => 10.0,
401                    ExecutionStrategy::Degraded => 2.0,
402                }
403            };
404
405            if score > best_score {
406                best_score = score;
407                best_strategy = *strategy;
408            }
409        }
410
411        Ok(best_strategy)
412    }
413
414    /// Select strategy based on historical performance
415    async fn select_strategy_from_history(
416        &self,
417        estimate: &crate::historical_cost_estimator::CostEstimate,
418        complexity: f64,
419        snapshot: &ResourceSnapshot,
420    ) -> ExecutionStrategy {
421        // If query is fast, use sequential
422        if estimate.estimated_time_ms < self.config.min_adaptation_threshold_ms {
423            return ExecutionStrategy::Sequential;
424        }
425
426        // If query is complex and system has capacity, use parallel
427        if complexity > 50.0 && !snapshot.is_high_load(&self.config) {
428            return ExecutionStrategy::Parallel;
429        }
430
431        // If confidence is low, use optimized strategy to learn
432        if estimate.confidence < 0.5 {
433            return ExecutionStrategy::Optimized;
434        }
435
436        // Default to optimized for medium-high complexity
437        if complexity > 20.0 {
438            ExecutionStrategy::Optimized
439        } else {
440            ExecutionStrategy::Sequential
441        }
442    }
443
444    /// Default strategy selection based on complexity
445    fn select_default_strategy(
446        &self,
447        complexity: f64,
448        snapshot: &ResourceSnapshot,
449    ) -> ExecutionStrategy {
450        if snapshot.is_high_load(&self.config) {
451            return ExecutionStrategy::Degraded;
452        }
453
454        match complexity {
455            c if c < 10.0 => ExecutionStrategy::Sequential,
456            c if c < 30.0 => ExecutionStrategy::Batched,
457            c if c < 60.0 => ExecutionStrategy::Optimized,
458            _ => ExecutionStrategy::Parallel,
459        }
460    }
461
462    /// Select strategy for high load conditions
463    async fn select_high_load_strategy(&self, snapshot: &ResourceSnapshot) -> ExecutionStrategy {
464        let severity = snapshot.load_severity(&self.config);
465
466        if severity > 0.95 {
467            // Critical load - use degraded mode
468            ExecutionStrategy::Degraded
469        } else if severity > 0.85 {
470            // High load - use sequential
471            ExecutionStrategy::Sequential
472        } else {
473            // Moderate load - use batched
474            ExecutionStrategy::Batched
475        }
476    }
477
478    /// Select fallback strategy
479    fn select_fallback_strategy(
480        &self,
481        primary: ExecutionStrategy,
482        snapshot: &ResourceSnapshot,
483    ) -> ExecutionStrategy {
484        if snapshot.is_high_load(&self.config) {
485            return ExecutionStrategy::Degraded;
486        }
487
488        match primary {
489            ExecutionStrategy::Parallel => ExecutionStrategy::Sequential,
490            ExecutionStrategy::Optimized => ExecutionStrategy::Batched,
491            ExecutionStrategy::Batched => ExecutionStrategy::Sequential,
492            ExecutionStrategy::Streaming => ExecutionStrategy::Sequential,
493            _ => ExecutionStrategy::Sequential,
494        }
495    }
496
497    /// Record execution result for adaptation
498    pub async fn record_execution(&self, result: ExecutionResult) -> Result<()> {
499        let query_fingerprint = result.query_fingerprint.clone();
500        let strategy = result.strategy_used;
501        let execution_time = result.actual_time_ms;
502        let success = result.success;
503        let resource_snapshot = result.resource_snapshot.clone();
504
505        // Update historical cost estimator
506        {
507            let mut estimator = self.cost_estimator.write().await;
508            let _ = estimator.record_execution(
509                &query_fingerprint,
510                execution_time,
511                1,    // complexity placeholder
512                1024, // memory placeholder (1KB)
513            );
514        }
515
516        // Update strategy performance stats
517        {
518            let mut stats = self.strategy_performance.write().await;
519            let strategy_stat = stats.entry(strategy).or_default();
520
521            strategy_stat.total_executions += 1;
522            if success {
523                strategy_stat.successful_executions += 1;
524            }
525            strategy_stat.total_time_ms += execution_time;
526            strategy_stat.avg_time_ms =
527                strategy_stat.total_time_ms / strategy_stat.total_executions as f64;
528            strategy_stat.error_rate = 1.0
529                - (strategy_stat.successful_executions as f64
530                    / strategy_stat.total_executions as f64);
531            strategy_stat.last_used = Instant::now();
532        }
533
534        // Add to execution history
535        {
536            let mut history = self.execution_history.write().await;
537            if history.len() >= self.config.history_size {
538                history.pop_front();
539            }
540            history.push_back(result);
541        }
542
543        // Train ML optimizer with synthesized features from execution result
544        if let Some(ml_opt) = &self.ml_optimizer {
545            let features = QueryFeatures {
546                field_count: resource_snapshot.active_queries as f64,
547                max_depth: 1.0,
548                complexity_score: execution_time,
549                selection_count: resource_snapshot.active_queries as f64,
550                has_fragments: 0.0,
551                has_variables: 0.0,
552                operation_type: 0.0,
553                unique_field_types: 1.0,
554                nested_list_count: 0.0,
555                argument_count: 0.0,
556                directive_count: 0.0,
557                estimated_result_size: resource_snapshot.avg_query_time_ms.log10().max(1.0),
558            };
559            let sample = TrainingSample {
560                features,
561                execution_time_ms: execution_time,
562                memory_usage_mb: resource_snapshot.memory_usage * 100.0,
563                cache_hit: false,
564                error_occurred: !success,
565                timestamp: std::time::SystemTime::now(),
566            };
567            let ml = ml_opt.read().await;
568            // best-effort: ignore training errors
569            let _ = ml.add_training_sample(sample).await;
570        }
571
572        Ok(())
573    }
574
575    /// Get current resource snapshot
576    async fn get_resource_snapshot(&self) -> Result<ResourceSnapshot> {
577        let mut sys = self.system.write().await;
578        sys.refresh_cpu_all();
579        sys.refresh_memory();
580
581        // Average CPU usage across all cores
582        let cpu_usage = sys
583            .cpus()
584            .iter()
585            .map(|cpu| cpu.cpu_usage() as f64)
586            .sum::<f64>()
587            / sys.cpus().len().max(1) as f64
588            / 100.0;
589
590        let memory_usage = {
591            let total_mem = sys.total_memory();
592            let used_mem = sys.used_memory();
593            if total_mem > 0 {
594                used_mem as f64 / total_mem as f64
595            } else {
596                0.0
597            }
598        };
599
600        // Get active queries and avg time from history
601        let (active_queries, avg_query_time) = {
602            let history = self.execution_history.read().await;
603            let active = history.len();
604
605            let avg_time = if !history.is_empty() {
606                history.iter().map(|r| r.actual_time_ms).sum::<f64>() / history.len() as f64
607            } else {
608                0.0
609            };
610
611            (active, avg_time)
612        };
613
614        Ok(ResourceSnapshot {
615            timestamp: Instant::now(),
616            cpu_usage,
617            memory_usage,
618            active_queries,
619            avg_query_time_ms: avg_query_time,
620        })
621    }
622
623    /// Generate query fingerprint for tracking
624    fn fingerprint_query(query: &str) -> String {
625        use std::collections::hash_map::DefaultHasher;
626        use std::hash::{Hash, Hasher};
627
628        let normalized = query
629            .to_lowercase()
630            .split_whitespace()
631            .collect::<Vec<_>>()
632            .join(" ");
633
634        let mut hasher = DefaultHasher::new();
635        normalized.hash(&mut hasher);
636
637        format!("query_{:x}", hasher.finish())
638    }
639
640    /// Get current strategy performance statistics
641    pub async fn get_strategy_stats(&self) -> HashMap<ExecutionStrategy, StrategyStats> {
642        self.strategy_performance.read().await.clone()
643    }
644
645    /// Get planner metrics
646    pub fn get_metrics(&self) -> DynamicPlannerMetrics {
647        DynamicPlannerMetrics {
648            total_adaptations: self.adaptations.get() as usize,
649            strategy_switches: self.strategy_switches.get() as usize,
650            high_load_events: self.high_load_events.get() as usize,
651            avg_adaptation_quality: self.avg_adaptation_quality.get(),
652        }
653    }
654}
655
656/// Metrics for dynamic query planner
657#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct DynamicPlannerMetrics {
659    pub total_adaptations: usize,
660    pub strategy_switches: usize,
661    pub high_load_events: usize,
662    pub avg_adaptation_quality: f64,
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668
669    #[test]
670    fn test_execution_strategy_overhead() {
671        assert_eq!(ExecutionStrategy::Sequential.overhead_multiplier(), 1.0);
672        assert!(ExecutionStrategy::Parallel.overhead_multiplier() > 1.0);
673        assert!(ExecutionStrategy::Batched.overhead_multiplier() < 1.0);
674        assert!(ExecutionStrategy::Cached.overhead_multiplier() < 0.1);
675    }
676
677    #[test]
678    fn test_execution_strategy_high_load_safe() {
679        assert!(ExecutionStrategy::Degraded.is_high_load_safe());
680        assert!(ExecutionStrategy::Cached.is_high_load_safe());
681        assert!(ExecutionStrategy::Sequential.is_high_load_safe());
682        assert!(!ExecutionStrategy::Parallel.is_high_load_safe());
683    }
684
685    #[test]
686    fn test_resource_snapshot_high_load() {
687        let config = DynamicPlannerConfig::default();
688
689        let low_load = ResourceSnapshot {
690            timestamp: Instant::now(),
691            cpu_usage: 0.5,
692            memory_usage: 0.6,
693            active_queries: 10,
694            avg_query_time_ms: 50.0,
695        };
696        assert!(!low_load.is_high_load(&config));
697
698        let high_load = ResourceSnapshot {
699            timestamp: Instant::now(),
700            cpu_usage: 0.9,
701            memory_usage: 0.9,
702            active_queries: 100,
703            avg_query_time_ms: 500.0,
704        };
705        assert!(high_load.is_high_load(&config));
706    }
707
708    #[test]
709    fn test_resource_snapshot_load_severity() {
710        let config = DynamicPlannerConfig::default();
711
712        let snapshot = ResourceSnapshot {
713            timestamp: Instant::now(),
714            cpu_usage: 0.9,    // Above threshold (0.8)
715            memory_usage: 0.7, // Below threshold (0.85)
716            active_queries: 50,
717            avg_query_time_ms: 100.0,
718        };
719
720        let severity = snapshot.load_severity(&config);
721        assert!(severity > 0.8); // Should be high due to CPU
722    }
723
724    #[test]
725    fn test_adaptive_plan_creation() {
726        let plan = AdaptivePlan::new("query_123".to_string(), ExecutionStrategy::Parallel, 50.0);
727
728        assert_eq!(plan.query_fingerprint, "query_123");
729        assert_eq!(plan.strategy, ExecutionStrategy::Parallel);
730        assert_eq!(plan.estimated_cost, 50.0);
731        assert_eq!(plan.confidence, 0.5);
732    }
733
734    #[test]
735    fn test_adaptive_plan_with_fallback() {
736        let plan = AdaptivePlan::new("query_123".to_string(), ExecutionStrategy::Parallel, 50.0)
737            .with_fallback(ExecutionStrategy::Sequential);
738
739        assert_eq!(plan.fallback_strategy, Some(ExecutionStrategy::Sequential));
740    }
741
742    #[test]
743    fn test_adaptive_plan_with_confidence() {
744        let plan = AdaptivePlan::new("query_123".to_string(), ExecutionStrategy::Parallel, 50.0)
745            .with_confidence(0.85);
746
747        assert_eq!(plan.confidence, 0.85);
748
749        // Test clamping
750        let clamped =
751            AdaptivePlan::new("query_456".to_string(), ExecutionStrategy::Sequential, 10.0)
752                .with_confidence(1.5);
753
754        assert_eq!(clamped.confidence, 1.0); // Should be clamped to 1.0
755    }
756
757    #[tokio::test]
758    async fn test_dynamic_planner_creation() {
759        let config = DynamicPlannerConfig::default();
760        let planner = DynamicQueryPlanner::new(config);
761
762        let metrics = planner.get_metrics();
763        assert_eq!(metrics.total_adaptations, 0);
764        assert_eq!(metrics.strategy_switches, 0);
765    }
766
767    #[tokio::test]
768    async fn test_create_plan_disabled() {
769        let config = DynamicPlannerConfig {
770            enabled: false,
771            ..Default::default()
772        };
773        let planner = DynamicQueryPlanner::new(config);
774
775        let plan = planner
776            .create_plan("SELECT * WHERE { ?s ?p ?o }", 10.0)
777            .await
778            .expect("should succeed");
779
780        assert_eq!(plan.strategy, ExecutionStrategy::Sequential);
781    }
782
783    #[tokio::test]
784    async fn test_create_plan_simple_query() {
785        // Use max thresholds to prevent real system load from triggering degraded mode
786        let config = DynamicPlannerConfig {
787            cpu_threshold: 1.0,    // Never trigger high load from CPU
788            memory_threshold: 1.0, // Never trigger high load from memory
789            ..Default::default()
790        };
791        let planner = DynamicQueryPlanner::new(config);
792
793        let plan = planner
794            .create_plan("SELECT ?s WHERE { ?s ?p ?o } LIMIT 10", 5.0)
795            .await
796            .expect("should succeed");
797
798        // Simple query should NOT use parallel or degraded
799        assert!(
800            !matches!(
801                plan.strategy,
802                ExecutionStrategy::Parallel | ExecutionStrategy::Degraded
803            ),
804            "Simple query used {:?} strategy",
805            plan.strategy
806        );
807
808        // Verify fallback strategy is set
809        assert!(plan.fallback_strategy.is_some());
810    }
811
812    #[tokio::test]
813    async fn test_create_plan_complex_query() {
814        // Use max thresholds to prevent real system load from triggering degraded mode
815        let config = DynamicPlannerConfig {
816            cpu_threshold: 1.0,    // Never trigger high load from CPU
817            memory_threshold: 1.0, // Never trigger high load from memory
818            ..Default::default()
819        };
820        let planner = DynamicQueryPlanner::new(config);
821
822        let complex_query =
823            "SELECT ?s ?p ?o WHERE { ?s ?p ?o . ?s ?p2 ?o2 . ?s ?p3 ?o3 } LIMIT 1000";
824        let plan = planner
825            .create_plan(complex_query, 75.0)
826            .await
827            .expect("should succeed");
828
829        // Complex query should use optimized or parallel
830        assert!(matches!(
831            plan.strategy,
832            ExecutionStrategy::Optimized | ExecutionStrategy::Parallel
833        ));
834        assert!(plan.fallback_strategy.is_some());
835    }
836
837    #[tokio::test]
838    async fn test_record_execution() {
839        let config = DynamicPlannerConfig::default();
840        let planner = DynamicQueryPlanner::new(config);
841
842        let result = ExecutionResult {
843            query_fingerprint: "query_123".to_string(),
844            strategy_used: ExecutionStrategy::Parallel,
845            actual_time_ms: 150.0,
846            success: true,
847            error_message: None,
848            resource_snapshot: ResourceSnapshot {
849                timestamp: Instant::now(),
850                cpu_usage: 0.6,
851                memory_usage: 0.7,
852                active_queries: 5,
853                avg_query_time_ms: 100.0,
854            },
855        };
856
857        planner
858            .record_execution(result)
859            .await
860            .expect("should succeed");
861
862        // Check strategy stats were updated
863        let stats = planner.get_strategy_stats().await;
864        let parallel_stats = stats
865            .get(&ExecutionStrategy::Parallel)
866            .expect("should succeed");
867
868        assert_eq!(parallel_stats.total_executions, 1);
869        assert_eq!(parallel_stats.successful_executions, 1);
870        assert_eq!(parallel_stats.avg_time_ms, 150.0);
871    }
872
873    #[tokio::test]
874    async fn test_query_fingerprinting() {
875        let query1 = "SELECT ?s WHERE { ?s ?p ?o }";
876        let query2 = "select ?s where { ?s ?p ?o }"; // Same query, different case
877        let query3 = "SELECT ?x WHERE { ?x ?p ?o }"; // Different variables
878
879        let fp1 = DynamicQueryPlanner::fingerprint_query(query1);
880        let fp2 = DynamicQueryPlanner::fingerprint_query(query2);
881        let fp3 = DynamicQueryPlanner::fingerprint_query(query3);
882
883        // Same query (case-insensitive) should have same fingerprint
884        assert_eq!(fp1, fp2);
885
886        // Different query should have different fingerprint
887        assert_ne!(fp1, fp3);
888    }
889
890    #[tokio::test]
891    async fn test_default_strategy_selection() {
892        let config = DynamicPlannerConfig::default();
893        let planner = DynamicQueryPlanner::new(config.clone());
894
895        let low_load = ResourceSnapshot {
896            timestamp: Instant::now(),
897            cpu_usage: 0.3,
898            memory_usage: 0.4,
899            active_queries: 2,
900            avg_query_time_ms: 50.0,
901        };
902
903        // Low complexity
904        let strategy = planner.select_default_strategy(5.0, &low_load);
905        assert_eq!(strategy, ExecutionStrategy::Sequential);
906
907        // Medium complexity
908        let strategy = planner.select_default_strategy(25.0, &low_load);
909        assert_eq!(strategy, ExecutionStrategy::Batched);
910
911        // High complexity
912        let strategy = planner.select_default_strategy(70.0, &low_load);
913        assert_eq!(strategy, ExecutionStrategy::Parallel);
914
915        // High load - should always use degraded
916        let high_load = ResourceSnapshot {
917            timestamp: Instant::now(),
918            cpu_usage: 0.95,
919            memory_usage: 0.90,
920            active_queries: 100,
921            avg_query_time_ms: 500.0,
922        };
923
924        let strategy = planner.select_default_strategy(70.0, &high_load);
925        assert_eq!(strategy, ExecutionStrategy::Degraded);
926    }
927
928    #[test]
929    fn test_strategy_stats_default() {
930        let stats = StrategyStats::default();
931        assert_eq!(stats.total_executions, 0);
932        assert_eq!(stats.successful_executions, 0);
933        assert_eq!(stats.error_rate, 0.0);
934    }
935
936    #[test]
937    fn test_config_defaults() {
938        let config = DynamicPlannerConfig::default();
939        assert!(config.enabled);
940        assert!(config.enable_ml_prediction);
941        assert_eq!(config.cpu_threshold, 0.80);
942        assert_eq!(config.memory_threshold, 0.85);
943    }
944
945    #[tokio::test]
946    async fn test_ml_optimizer_wired() {
947        let config = DynamicPlannerConfig {
948            enable_ml_prediction: true,
949            cpu_threshold: 1.0,
950            memory_threshold: 1.0,
951            ..Default::default()
952        };
953        let planner = DynamicQueryPlanner::new(config);
954        // With ML enabled, create_plan should succeed
955        let plan = planner
956            .create_plan("SELECT ?s WHERE { ?s ?p ?o }", 10.0)
957            .await
958            .expect("should succeed");
959        assert!(plan.fallback_strategy.is_some());
960    }
961
962    #[tokio::test]
963    async fn test_ml_training_updates() {
964        let config = DynamicPlannerConfig {
965            enable_ml_prediction: true,
966            cpu_threshold: 1.0,
967            memory_threshold: 1.0,
968            ..Default::default()
969        };
970        let planner = DynamicQueryPlanner::new(config);
971
972        let snapshot = ResourceSnapshot {
973            timestamp: Instant::now(),
974            cpu_usage: 0.3,
975            memory_usage: 0.4,
976            active_queries: 5,
977            avg_query_time_ms: 50.0,
978        };
979
980        for i in 0..5u32 {
981            let exec_result = ExecutionResult {
982                query_fingerprint: format!("query_{i}"),
983                strategy_used: ExecutionStrategy::Sequential,
984                actual_time_ms: (i as f64 + 1.0) * 20.0,
985                success: true,
986                error_message: None,
987                resource_snapshot: snapshot.clone(),
988            };
989            planner
990                .record_execution(exec_result)
991                .await
992                .expect("should succeed");
993        }
994
995        let stats = planner.get_strategy_stats().await;
996        let seq_stats = stats
997            .get(&ExecutionStrategy::Sequential)
998            .expect("should have Sequential stats");
999        assert_eq!(seq_stats.total_executions, 5);
1000        assert!(seq_stats.avg_time_ms > 0.0);
1001    }
1002}