oxify_authz/
query_optimizer.rs

1//! Query optimization utilities for authorization checks
2//!
3//! This module provides tools to analyze and optimize database queries,
4//! helping identify slow queries and suggesting improvements.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use oxify_authz::query_optimizer::*;
10//!
11//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! let optimizer = QueryOptimizer::new();
13//!
14//! // Analyze a query
15//! let analysis = QueryAnalysis {
16//!     query_type: QueryType::Check,
17//!     execution_time_ms: 150.0,
18//!     rows_scanned: 10000,
19//!     rows_returned: 1,
20//!     uses_index: false,
21//!     cache_hit: false,
22//! };
23//!
24//! let suggestions = optimizer.analyze(&analysis);
25//! for suggestion in suggestions {
26//!     println!("Optimization: {:?}", suggestion);
27//! }
28//! # Ok(())
29//! # }
30//! ```
31
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35/// Query optimizer for authorization operations
36#[derive(Clone)]
37pub struct QueryOptimizer {
38    /// Performance thresholds for different query types
39    thresholds: HashMap<QueryType, PerformanceThreshold>,
40}
41
42/// Type of authorization query
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum QueryType {
45    /// Direct permission check
46    Check,
47    /// Permission expansion (find all subjects)
48    Expand,
49    /// Write tuple operation
50    Write,
51    /// Delete tuple operation
52    Delete,
53    /// Batch check operation
54    BatchCheck,
55    /// List tuples query
56    List,
57    /// Transitive check (multi-hop)
58    TransitiveCheck,
59}
60
61/// Performance thresholds for a query type
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PerformanceThreshold {
64    /// Maximum acceptable execution time in milliseconds
65    pub max_execution_time_ms: f64,
66    /// Maximum acceptable rows scanned
67    pub max_rows_scanned: u64,
68    /// Target cache hit rate (0.0 - 1.0)
69    pub target_cache_hit_rate: f64,
70}
71
72/// Analysis of a query execution
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct QueryAnalysis {
75    /// Type of query
76    pub query_type: QueryType,
77    /// Execution time in milliseconds
78    pub execution_time_ms: f64,
79    /// Number of rows scanned
80    pub rows_scanned: u64,
81    /// Number of rows returned
82    pub rows_returned: u64,
83    /// Whether an index was used
84    pub uses_index: bool,
85    /// Whether the result came from cache
86    pub cache_hit: bool,
87}
88
89/// Optimization suggestion
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct OptimizationSuggestion {
92    /// Severity of the issue
93    pub severity: Severity,
94    /// Category of the suggestion
95    pub category: Category,
96    /// Description of the suggestion
97    pub description: String,
98    /// Potential impact (e.g., "50% reduction in query time")
99    pub potential_impact: String,
100}
101
102/// Severity level of an optimization suggestion
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
104pub enum Severity {
105    /// Minor optimization opportunity
106    Low,
107    /// Moderate optimization opportunity
108    Medium,
109    /// Important optimization opportunity
110    High,
111    /// Critical issue affecting performance
112    Critical,
113}
114
115/// Category of optimization
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
117pub enum Category {
118    /// Index-related optimization
119    Indexing,
120    /// Caching optimization
121    Caching,
122    /// Query structure optimization
123    QueryStructure,
124    /// Data volume optimization
125    DataVolume,
126    /// General performance
127    Performance,
128}
129
130impl QueryOptimizer {
131    /// Create a new query optimizer with default thresholds
132    pub fn new() -> Self {
133        let mut thresholds = HashMap::new();
134
135        // Set default thresholds for each query type
136        thresholds.insert(
137            QueryType::Check,
138            PerformanceThreshold {
139                max_execution_time_ms: 3.0, // 3ms for cached checks
140                max_rows_scanned: 100,
141                target_cache_hit_rate: 0.95,
142            },
143        );
144
145        thresholds.insert(
146            QueryType::Expand,
147            PerformanceThreshold {
148                max_execution_time_ms: 10.0,
149                max_rows_scanned: 1000,
150                target_cache_hit_rate: 0.80,
151            },
152        );
153
154        thresholds.insert(
155            QueryType::Write,
156            PerformanceThreshold {
157                max_execution_time_ms: 5.0,
158                max_rows_scanned: 10,
159                target_cache_hit_rate: 0.0, // Writes don't benefit from read cache
160            },
161        );
162
163        thresholds.insert(
164            QueryType::Delete,
165            PerformanceThreshold {
166                max_execution_time_ms: 5.0,
167                max_rows_scanned: 10,
168                target_cache_hit_rate: 0.0,
169            },
170        );
171
172        thresholds.insert(
173            QueryType::BatchCheck,
174            PerformanceThreshold {
175                max_execution_time_ms: 50.0, // 50ms for 100 checks
176                max_rows_scanned: 10000,
177                target_cache_hit_rate: 0.90,
178            },
179        );
180
181        thresholds.insert(
182            QueryType::List,
183            PerformanceThreshold {
184                max_execution_time_ms: 20.0,
185                max_rows_scanned: 10000,
186                target_cache_hit_rate: 0.50,
187            },
188        );
189
190        thresholds.insert(
191            QueryType::TransitiveCheck,
192            PerformanceThreshold {
193                max_execution_time_ms: 10.0,
194                max_rows_scanned: 500,
195                target_cache_hit_rate: 0.85,
196            },
197        );
198
199        Self { thresholds }
200    }
201
202    /// Analyze a query and provide optimization suggestions
203    pub fn analyze(&self, analysis: &QueryAnalysis) -> Vec<OptimizationSuggestion> {
204        let mut suggestions = Vec::new();
205
206        let threshold = self
207            .thresholds
208            .get(&analysis.query_type)
209            .cloned()
210            .unwrap_or(PerformanceThreshold {
211                max_execution_time_ms: 10.0,
212                max_rows_scanned: 1000,
213                target_cache_hit_rate: 0.90,
214            });
215
216        // Check execution time
217        if analysis.execution_time_ms > threshold.max_execution_time_ms {
218            let severity = if analysis.execution_time_ms > threshold.max_execution_time_ms * 3.0 {
219                Severity::Critical
220            } else if analysis.execution_time_ms > threshold.max_execution_time_ms * 2.0 {
221                Severity::High
222            } else {
223                Severity::Medium
224            };
225
226            suggestions.push(OptimizationSuggestion {
227                severity,
228                category: Category::Performance,
229                description: format!(
230                    "Query execution time ({:.2}ms) exceeds threshold ({:.2}ms)",
231                    analysis.execution_time_ms, threshold.max_execution_time_ms
232                ),
233                potential_impact: "Reduce latency by optimizing query or adding caching"
234                    .to_string(),
235            });
236        }
237
238        // Check index usage
239        if !analysis.uses_index && analysis.rows_scanned > 100 {
240            suggestions.push(OptimizationSuggestion {
241                severity: Severity::High,
242                category: Category::Indexing,
243                description: format!(
244                    "Query scanned {} rows without using an index",
245                    analysis.rows_scanned
246                ),
247                potential_impact: "Adding an index could reduce query time by 90%+".to_string(),
248            });
249        }
250
251        // Check rows scanned vs returned ratio
252        if analysis.rows_returned > 0 {
253            let scan_ratio = analysis.rows_scanned as f64 / analysis.rows_returned as f64;
254            if scan_ratio > 100.0 && analysis.rows_scanned > 1000 {
255                suggestions.push(OptimizationSuggestion {
256                    severity: Severity::Medium,
257                    category: Category::QueryStructure,
258                    description: format!(
259                        "Query scanned {} rows but returned only {} (ratio: {:.1}:1)",
260                        analysis.rows_scanned, analysis.rows_returned, scan_ratio
261                    ),
262                    potential_impact: "Optimize query filters or add covering indexes".to_string(),
263                });
264            }
265        }
266
267        // Check cache hit
268        if !analysis.cache_hit
269            && matches!(
270                analysis.query_type,
271                QueryType::Check | QueryType::TransitiveCheck | QueryType::Expand
272            )
273        {
274            suggestions.push(OptimizationSuggestion {
275                severity: Severity::Medium,
276                category: Category::Caching,
277                description: "Cache miss for a cacheable query type".to_string(),
278                potential_impact: "Improve cache hit rate to reduce database load".to_string(),
279            });
280        }
281
282        // Check data volume
283        if analysis.rows_scanned > threshold.max_rows_scanned {
284            suggestions.push(OptimizationSuggestion {
285                severity: Severity::Medium,
286                category: Category::DataVolume,
287                description: format!(
288                    "Query scanned {} rows, exceeding threshold of {}",
289                    analysis.rows_scanned, threshold.max_rows_scanned
290                ),
291                potential_impact: "Consider data archival or partitioning strategies".to_string(),
292            });
293        }
294
295        suggestions
296    }
297
298    /// Set custom threshold for a query type
299    pub fn set_threshold(&mut self, query_type: QueryType, threshold: PerformanceThreshold) {
300        self.thresholds.insert(query_type, threshold);
301    }
302
303    /// Get threshold for a query type
304    pub fn get_threshold(&self, query_type: QueryType) -> Option<&PerformanceThreshold> {
305        self.thresholds.get(&query_type)
306    }
307
308    /// Generate optimization report from multiple analyses
309    pub fn generate_report(&self, analyses: &[QueryAnalysis]) -> OptimizationReport {
310        let mut suggestions_by_severity = HashMap::new();
311        let mut total_suggestions = 0;
312
313        for analysis in analyses {
314            let suggestions = self.analyze(analysis);
315            total_suggestions += suggestions.len();
316
317            for suggestion in suggestions {
318                *suggestions_by_severity
319                    .entry(suggestion.severity)
320                    .or_insert(0) += 1;
321            }
322        }
323
324        let critical_count = *suggestions_by_severity
325            .get(&Severity::Critical)
326            .unwrap_or(&0);
327        let high_count = *suggestions_by_severity.get(&Severity::High).unwrap_or(&0);
328        let medium_count = *suggestions_by_severity.get(&Severity::Medium).unwrap_or(&0);
329        let low_count = *suggestions_by_severity.get(&Severity::Low).unwrap_or(&0);
330
331        OptimizationReport {
332            total_queries: analyses.len(),
333            total_suggestions,
334            critical_issues: critical_count,
335            high_priority: high_count,
336            medium_priority: medium_count,
337            low_priority: low_count,
338        }
339    }
340}
341
342impl Default for QueryOptimizer {
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348/// Summary report of optimization analysis
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct OptimizationReport {
351    /// Total number of queries analyzed
352    pub total_queries: usize,
353    /// Total optimization suggestions
354    pub total_suggestions: usize,
355    /// Number of critical issues
356    pub critical_issues: usize,
357    /// Number of high-priority suggestions
358    pub high_priority: usize,
359    /// Number of medium-priority suggestions
360    pub medium_priority: usize,
361    /// Number of low-priority suggestions
362    pub low_priority: usize,
363}
364
365impl OptimizationReport {
366    /// Check if there are any critical issues
367    pub fn has_critical_issues(&self) -> bool {
368        self.critical_issues > 0
369    }
370
371    /// Get overall health score (0-100)
372    pub fn health_score(&self) -> u8 {
373        if self.total_queries == 0 {
374            return 100;
375        }
376
377        let penalty = (self.critical_issues * 20)
378            + (self.high_priority * 10)
379            + (self.medium_priority * 5)
380            + (self.low_priority * 2);
381
382        let score = 100_i32 - penalty as i32;
383        score.clamp(0, 100) as u8
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_optimizer_basic() {
393        let optimizer = QueryOptimizer::new();
394
395        let analysis = QueryAnalysis {
396            query_type: QueryType::Check,
397            execution_time_ms: 5.0, // Exceeds 3ms threshold
398            rows_scanned: 100,
399            rows_returned: 1,
400            uses_index: true,
401            cache_hit: false,
402        };
403
404        let suggestions = optimizer.analyze(&analysis);
405        assert!(!suggestions.is_empty());
406
407        // Should suggest caching improvement
408        assert!(suggestions
409            .iter()
410            .any(|s| s.category == Category::Performance));
411    }
412
413    #[test]
414    fn test_missing_index_detection() {
415        let optimizer = QueryOptimizer::new();
416
417        let analysis = QueryAnalysis {
418            query_type: QueryType::Check,
419            execution_time_ms: 2.0,
420            rows_scanned: 10000, // Many rows scanned
421            rows_returned: 1,
422            uses_index: false, // No index used
423            cache_hit: false,
424        };
425
426        let suggestions = optimizer.analyze(&analysis);
427
428        // Should suggest adding an index
429        assert!(suggestions.iter().any(|s| s.category == Category::Indexing));
430    }
431
432    #[test]
433    fn test_scan_ratio_detection() {
434        let optimizer = QueryOptimizer::new();
435
436        let analysis = QueryAnalysis {
437            query_type: QueryType::List,
438            execution_time_ms: 15.0,
439            rows_scanned: 10000,
440            rows_returned: 10, // Poor scan ratio (1000:1)
441            uses_index: true,
442            cache_hit: false,
443        };
444
445        let suggestions = optimizer.analyze(&analysis);
446
447        // Should suggest query structure improvement
448        assert!(suggestions
449            .iter()
450            .any(|s| s.category == Category::QueryStructure));
451    }
452
453    #[test]
454    fn test_cache_miss_detection() {
455        let optimizer = QueryOptimizer::new();
456
457        let analysis = QueryAnalysis {
458            query_type: QueryType::Check,
459            execution_time_ms: 2.0,
460            rows_scanned: 10,
461            rows_returned: 1,
462            uses_index: true,
463            cache_hit: false, // Cache miss for cacheable query
464        };
465
466        let suggestions = optimizer.analyze(&analysis);
467
468        // Should suggest caching improvement
469        assert!(suggestions.iter().any(|s| s.category == Category::Caching));
470    }
471
472    #[test]
473    fn test_custom_threshold() {
474        let mut optimizer = QueryOptimizer::new();
475
476        let custom_threshold = PerformanceThreshold {
477            max_execution_time_ms: 1.0,
478            max_rows_scanned: 50,
479            target_cache_hit_rate: 0.99,
480        };
481
482        optimizer.set_threshold(QueryType::Check, custom_threshold);
483
484        let analysis = QueryAnalysis {
485            query_type: QueryType::Check,
486            execution_time_ms: 1.5, // Exceeds custom threshold
487            rows_scanned: 10,
488            rows_returned: 1,
489            uses_index: true,
490            cache_hit: true,
491        };
492
493        let suggestions = optimizer.analyze(&analysis);
494        assert!(!suggestions.is_empty());
495    }
496
497    #[test]
498    fn test_optimization_report() {
499        let optimizer = QueryOptimizer::new();
500
501        let analyses = vec![
502            QueryAnalysis {
503                query_type: QueryType::Check,
504                execution_time_ms: 50.0, // Critical
505                rows_scanned: 10000,
506                rows_returned: 1,
507                uses_index: false,
508                cache_hit: false,
509            },
510            QueryAnalysis {
511                query_type: QueryType::Check,
512                execution_time_ms: 2.0, // Good
513                rows_scanned: 10,
514                rows_returned: 1,
515                uses_index: true,
516                cache_hit: true,
517            },
518        ];
519
520        let report = optimizer.generate_report(&analyses);
521        assert_eq!(report.total_queries, 2);
522        assert!(report.total_suggestions > 0);
523    }
524
525    #[test]
526    fn test_health_score() {
527        let report = OptimizationReport {
528            total_queries: 100,
529            total_suggestions: 5,
530            critical_issues: 1,
531            high_priority: 2,
532            medium_priority: 1,
533            low_priority: 1,
534        };
535
536        let score = report.health_score();
537        // 100 - (1*20 + 2*10 + 1*5 + 1*2) = 100 - 47 = 53
538        assert_eq!(score, 53);
539    }
540
541    #[test]
542    fn test_perfect_health_score() {
543        let report = OptimizationReport {
544            total_queries: 100,
545            total_suggestions: 0,
546            critical_issues: 0,
547            high_priority: 0,
548            medium_priority: 0,
549            low_priority: 0,
550        };
551
552        assert_eq!(report.health_score(), 100);
553        assert!(!report.has_critical_issues());
554    }
555
556    #[test]
557    fn test_severity_ordering() {
558        assert!(Severity::Critical > Severity::High);
559        assert!(Severity::High > Severity::Medium);
560        assert!(Severity::Medium > Severity::Low);
561    }
562
563    #[test]
564    fn test_data_volume_detection() {
565        let optimizer = QueryOptimizer::new();
566
567        let analysis = QueryAnalysis {
568            query_type: QueryType::Check,
569            execution_time_ms: 2.0,
570            rows_scanned: 1000, // Exceeds threshold
571            rows_returned: 1,
572            uses_index: true,
573            cache_hit: true,
574        };
575
576        let suggestions = optimizer.analyze(&analysis);
577
578        // Should suggest data volume optimization
579        assert!(suggestions
580            .iter()
581            .any(|s| s.category == Category::DataVolume));
582    }
583}