1use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35#[derive(Clone)]
37pub struct QueryOptimizer {
38 thresholds: HashMap<QueryType, PerformanceThreshold>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum QueryType {
45 Check,
47 Expand,
49 Write,
51 Delete,
53 BatchCheck,
55 List,
57 TransitiveCheck,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PerformanceThreshold {
64 pub max_execution_time_ms: f64,
66 pub max_rows_scanned: u64,
68 pub target_cache_hit_rate: f64,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct QueryAnalysis {
75 pub query_type: QueryType,
77 pub execution_time_ms: f64,
79 pub rows_scanned: u64,
81 pub rows_returned: u64,
83 pub uses_index: bool,
85 pub cache_hit: bool,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct OptimizationSuggestion {
92 pub severity: Severity,
94 pub category: Category,
96 pub description: String,
98 pub potential_impact: String,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
104pub enum Severity {
105 Low,
107 Medium,
109 High,
111 Critical,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
117pub enum Category {
118 Indexing,
120 Caching,
122 QueryStructure,
124 DataVolume,
126 Performance,
128}
129
130impl QueryOptimizer {
131 pub fn new() -> Self {
133 let mut thresholds = HashMap::new();
134
135 thresholds.insert(
137 QueryType::Check,
138 PerformanceThreshold {
139 max_execution_time_ms: 3.0, 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, },
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, 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 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 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 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 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 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 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 pub fn set_threshold(&mut self, query_type: QueryType, threshold: PerformanceThreshold) {
300 self.thresholds.insert(query_type, threshold);
301 }
302
303 pub fn get_threshold(&self, query_type: QueryType) -> Option<&PerformanceThreshold> {
305 self.thresholds.get(&query_type)
306 }
307
308 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#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct OptimizationReport {
351 pub total_queries: usize,
353 pub total_suggestions: usize,
355 pub critical_issues: usize,
357 pub high_priority: usize,
359 pub medium_priority: usize,
361 pub low_priority: usize,
363}
364
365impl OptimizationReport {
366 pub fn has_critical_issues(&self) -> bool {
368 self.critical_issues > 0
369 }
370
371 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, 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 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, rows_returned: 1,
422 uses_index: false, cache_hit: false,
424 };
425
426 let suggestions = optimizer.analyze(&analysis);
427
428 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, uses_index: true,
442 cache_hit: false,
443 };
444
445 let suggestions = optimizer.analyze(&analysis);
446
447 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, };
465
466 let suggestions = optimizer.analyze(&analysis);
467
468 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, 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, 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, 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 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, rows_returned: 1,
572 uses_index: true,
573 cache_hit: true,
574 };
575
576 let suggestions = optimizer.analyze(&analysis);
577
578 assert!(suggestions
580 .iter()
581 .any(|s| s.category == Category::DataVolume));
582 }
583}