1use anyhow::Result;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
33pub enum SuggestionSeverity {
34 Info,
36
37 Warning,
39
40 Critical,
42}
43
44impl std::fmt::Display for SuggestionSeverity {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 SuggestionSeverity::Info => write!(f, "INFO"),
48 SuggestionSeverity::Warning => write!(f, "WARNING"),
49 SuggestionSeverity::Critical => write!(f, "CRITICAL"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub enum SuggestionCategory {
57 PatternOrdering,
59
60 IndexUsage,
62
63 FilterPlacement,
65
66 JoinStrategy,
68
69 ResultLimitation,
71
72 QueryStructure,
74
75 BestPractices,
77
78 ResourceUsage,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct OptimizationSuggestion {
85 pub id: String,
87
88 pub message: String,
90
91 pub severity: SuggestionSeverity,
93
94 pub category: SuggestionCategory,
96
97 pub suggested_rewrite: Option<String>,
99
100 pub estimated_speedup: Option<f64>,
102
103 pub explanation: String,
105
106 pub location: Option<QueryLocation>,
108
109 pub documentation_url: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct QueryLocation {
116 pub line: usize,
117 pub column: usize,
118 pub length: usize,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct AdvisorConfig {
124 pub analyze_pattern_ordering: bool,
126
127 pub analyze_index_usage: bool,
129
130 pub analyze_filter_placement: bool,
132
133 pub analyze_join_strategy: bool,
135
136 pub analyze_result_limits: bool,
138
139 pub analyze_best_practices: bool,
141
142 pub filter_selectivity_threshold: f64,
144
145 pub max_optional_depth: usize,
147
148 pub max_union_branches: usize,
150
151 pub require_limit_clause: bool,
153
154 pub recommended_max_limit: usize,
156}
157
158impl Default for AdvisorConfig {
159 fn default() -> Self {
160 Self {
161 analyze_pattern_ordering: true,
162 analyze_index_usage: true,
163 analyze_filter_placement: true,
164 analyze_join_strategy: true,
165 analyze_result_limits: true,
166 analyze_best_practices: true,
167 filter_selectivity_threshold: 0.01,
168 max_optional_depth: 3,
169 max_union_branches: 5,
170 require_limit_clause: false,
171 recommended_max_limit: 10_000,
172 }
173 }
174}
175
176pub struct OptimizationAdvisor {
178 config: AdvisorConfig,
179 #[allow(dead_code)]
180 pattern_statistics: HashMap<String, PatternStatistics>,
181}
182
183#[allow(dead_code)]
185#[derive(Debug, Clone)]
186struct PatternStatistics {
187 selectivity: f64,
188 estimated_results: usize,
189 has_index: bool,
190}
191
192impl OptimizationAdvisor {
193 pub fn new(config: AdvisorConfig) -> Self {
195 Self {
196 config,
197 pattern_statistics: HashMap::new(),
198 }
199 }
200
201 pub fn analyze_query(&self, query: &str) -> Result<Vec<OptimizationSuggestion>> {
203 let mut suggestions = Vec::new();
204
205 let analysis = self.analyze_query_structure(query)?;
207
208 if self.config.analyze_pattern_ordering {
210 suggestions.extend(self.analyze_pattern_ordering(&analysis)?);
211 }
212
213 if self.config.analyze_filter_placement {
214 suggestions.extend(self.analyze_filter_placement(&analysis)?);
215 }
216
217 if self.config.analyze_join_strategy {
218 suggestions.extend(self.analyze_join_strategy(&analysis)?);
219 }
220
221 if self.config.analyze_result_limits {
222 suggestions.extend(self.analyze_result_limits(&analysis)?);
223 }
224
225 if self.config.analyze_best_practices {
226 suggestions.extend(self.analyze_best_practices(&analysis)?);
227 }
228
229 if self.config.analyze_index_usage {
230 suggestions.extend(self.analyze_index_usage(&analysis)?);
231 }
232
233 suggestions.sort_by_key(|b| std::cmp::Reverse(b.severity));
235
236 Ok(suggestions)
237 }
238
239 fn analyze_query_structure(&self, query: &str) -> Result<QueryAnalysis> {
241 let mut analysis = QueryAnalysis::default();
242
243 if query.contains("SELECT") {
245 analysis.query_form = QueryForm::Select;
246 } else if query.contains("ASK") {
247 analysis.query_form = QueryForm::Ask;
248 } else if query.contains("CONSTRUCT") {
249 analysis.query_form = QueryForm::Construct;
250 } else if query.contains("DESCRIBE") {
251 analysis.query_form = QueryForm::Describe;
252 }
253
254 analysis.has_distinct = query.contains("DISTINCT");
256
257 if let Some(pos) = query.find("LIMIT") {
259 if let Some(limit_str) = query[pos..].split_whitespace().nth(1) {
260 analysis.limit = limit_str.parse().ok();
261 }
262 }
263
264 analysis.optional_count = query.matches("OPTIONAL").count();
266
267 analysis.union_count = query.matches("UNION").count();
269
270 analysis.filter_count = query.matches("FILTER").count();
272
273 let dot_space = query.matches(". ").count();
276 let space_dot = query.matches(" .").count();
277 let dot_brace = query.matches(".}").count();
278
279 let pattern_count = std::cmp::max(dot_space, std::cmp::max(space_dot, dot_brace));
281 analysis.triple_pattern_count = if pattern_count > 0 { pattern_count } else { 1 };
282
283 analysis.has_select_star = query.contains("SELECT *") || query.contains("SELECT*");
285
286 analysis.bind_count = query.matches("BIND").count();
288
289 analysis.has_order_by = query.contains("ORDER BY");
291
292 analysis.has_group_by = query.contains("GROUP BY");
294
295 analysis.has_aggregates = query.contains("COUNT(")
297 || query.contains("SUM(")
298 || query.contains("AVG(")
299 || query.contains("MIN(")
300 || query.contains("MAX(");
301
302 Ok(analysis)
303 }
304
305 fn analyze_pattern_ordering(
307 &self,
308 analysis: &QueryAnalysis,
309 ) -> Result<Vec<OptimizationSuggestion>> {
310 let mut suggestions = Vec::new();
311
312 if analysis.triple_pattern_count >= 5 {
314 suggestions.push(OptimizationSuggestion {
315 id: "PATTERN_ORDER_001".to_string(),
316 message: format!(
317 "Query has {} triple patterns. Consider pattern ordering to start with most selective patterns.",
318 analysis.triple_pattern_count
319 ),
320 severity: SuggestionSeverity::Warning,
321 category: SuggestionCategory::PatternOrdering,
322 suggested_rewrite: None,
323 estimated_speedup: Some(2.0),
324 explanation: "Placing selective patterns first reduces intermediate results. \
325 Patterns with bound subjects/objects are typically more selective than those with all variables."
326 .to_string(),
327 location: None,
328 documentation_url: Some("https://www.w3.org/TR/sparql11-query/#sparqlBGPExtend".to_string()),
329 });
330 }
331
332 Ok(suggestions)
333 }
334
335 fn analyze_filter_placement(
337 &self,
338 analysis: &QueryAnalysis,
339 ) -> Result<Vec<OptimizationSuggestion>> {
340 let mut suggestions = Vec::new();
341
342 if analysis.filter_count > 0 && analysis.triple_pattern_count >= 3 {
343 suggestions.push(OptimizationSuggestion {
344 id: "FILTER_PLACEMENT_001".to_string(),
345 message: format!(
346 "Query has {} FILTERs. Ensure they are placed close to the patterns that bind their variables.",
347 analysis.filter_count
348 ),
349 severity: SuggestionSeverity::Info,
350 category: SuggestionCategory::FilterPlacement,
351 suggested_rewrite: None,
352 estimated_speedup: Some(1.5),
353 explanation: "Filters should be evaluated as soon as their variables are bound. \
354 Late filter evaluation can cause unnecessary computation on large intermediate results."
355 .to_string(),
356 location: None,
357 documentation_url: None,
358 });
359 }
360
361 Ok(suggestions)
362 }
363
364 fn analyze_join_strategy(
366 &self,
367 analysis: &QueryAnalysis,
368 ) -> Result<Vec<OptimizationSuggestion>> {
369 let mut suggestions = Vec::new();
370
371 if analysis.triple_pattern_count > 10 {
372 suggestions.push(OptimizationSuggestion {
373 id: "JOIN_STRATEGY_001".to_string(),
374 message: "Large query detected. Consider using query hints to guide join algorithm selection.".to_string(),
375 severity: SuggestionSeverity::Info,
376 category: SuggestionCategory::JoinStrategy,
377 suggested_rewrite: Some("Add /*+ HASH_JOIN */ or /*+ MERGE_JOIN */ hint".to_string()),
378 estimated_speedup: Some(1.3),
379 explanation: "For queries with many patterns, hash joins often outperform nested loop joins. \
380 Use query hints to override default join selection."
381 .to_string(),
382 location: None,
383 documentation_url: None,
384 });
385 }
386
387 Ok(suggestions)
388 }
389
390 fn analyze_result_limits(
392 &self,
393 analysis: &QueryAnalysis,
394 ) -> Result<Vec<OptimizationSuggestion>> {
395 let mut suggestions = Vec::new();
396
397 if self.config.require_limit_clause
399 && analysis.limit.is_none()
400 && analysis.query_form == QueryForm::Select
401 {
402 suggestions.push(OptimizationSuggestion {
403 id: "RESULT_LIMIT_001".to_string(),
404 message: "Query has no LIMIT clause. Consider adding one to prevent excessive results.".to_string(),
405 severity: SuggestionSeverity::Warning,
406 category: SuggestionCategory::ResultLimitation,
407 suggested_rewrite: Some(format!("Add LIMIT {}", self.config.recommended_max_limit)),
408 estimated_speedup: Some(10.0),
409 explanation: "Queries without LIMIT can return millions of results, consuming memory and network bandwidth. \
410 Add LIMIT to improve responsiveness."
411 .to_string(),
412 location: None,
413 documentation_url: None,
414 });
415 }
416
417 if let Some(limit) = analysis.limit {
419 if limit > self.config.recommended_max_limit {
420 suggestions.push(OptimizationSuggestion {
421 id: "RESULT_LIMIT_002".to_string(),
422 message: format!(
423 "LIMIT {} exceeds recommended maximum {}. Consider pagination instead.",
424 limit, self.config.recommended_max_limit
425 ),
426 severity: SuggestionSeverity::Warning,
427 category: SuggestionCategory::ResultLimitation,
428 suggested_rewrite: Some(
429 "Use cursor-based pagination for large result sets".to_string(),
430 ),
431 estimated_speedup: Some(2.0),
432 explanation:
433 "Large result sets should be paginated to maintain responsiveness. \
434 Use the query_pagination module for efficient pagination."
435 .to_string(),
436 location: None,
437 documentation_url: None,
438 });
439 }
440 }
441
442 Ok(suggestions)
443 }
444
445 fn analyze_best_practices(
447 &self,
448 analysis: &QueryAnalysis,
449 ) -> Result<Vec<OptimizationSuggestion>> {
450 let mut suggestions = Vec::new();
451
452 if analysis.has_select_star {
454 suggestions.push(OptimizationSuggestion {
455 id: "BEST_PRACTICE_001".to_string(),
456 message: "Avoid SELECT *. Specify only the variables you need.".to_string(),
457 severity: SuggestionSeverity::Warning,
458 category: SuggestionCategory::BestPractices,
459 suggested_rewrite: Some("SELECT ?var1 ?var2 ... WHERE { ... }".to_string()),
460 estimated_speedup: Some(1.2),
461 explanation: "SELECT * returns all variables, which may include unnecessary data. \
462 Selecting specific variables reduces result size and improves serialization performance."
463 .to_string(),
464 location: None,
465 documentation_url: None,
466 });
467 }
468
469 if analysis.optional_count > self.config.max_optional_depth {
471 suggestions.push(OptimizationSuggestion {
472 id: "BEST_PRACTICE_002".to_string(),
473 message: format!(
474 "Query has {} OPTIONAL blocks (max recommended: {}). This can cause performance issues.",
475 analysis.optional_count, self.config.max_optional_depth
476 ),
477 severity: SuggestionSeverity::Warning,
478 category: SuggestionCategory::BestPractices,
479 suggested_rewrite: None,
480 estimated_speedup: None,
481 explanation: "Excessive OPTIONAL clauses create combinatorial complexity. \
482 Consider restructuring the query or using UNION instead."
483 .to_string(),
484 location: None,
485 documentation_url: None,
486 });
487 }
488
489 if analysis.union_count > self.config.max_union_branches {
491 suggestions.push(OptimizationSuggestion {
492 id: "BEST_PRACTICE_003".to_string(),
493 message: format!(
494 "Query has {} UNION blocks (max recommended: {}). Consider query simplification.",
495 analysis.union_count, self.config.max_union_branches
496 ),
497 severity: SuggestionSeverity::Info,
498 category: SuggestionCategory::BestPractices,
499 suggested_rewrite: None,
500 estimated_speedup: None,
501 explanation: "Many UNION branches may indicate overly complex queries. \
502 Consider splitting into separate queries or using property paths."
503 .to_string(),
504 location: None,
505 documentation_url: None,
506 });
507 }
508
509 if analysis.has_distinct && !analysis.has_aggregates {
511 suggestions.push(OptimizationSuggestion {
512 id: "BEST_PRACTICE_004".to_string(),
513 message: "DISTINCT requires result deduplication. Ensure it's necessary."
514 .to_string(),
515 severity: SuggestionSeverity::Info,
516 category: SuggestionCategory::BestPractices,
517 suggested_rewrite: None,
518 estimated_speedup: Some(1.5),
519 explanation:
520 "DISTINCT adds overhead for deduplication. If your data model guarantees \
521 unique results, DISTINCT is unnecessary."
522 .to_string(),
523 location: None,
524 documentation_url: None,
525 });
526 }
527
528 Ok(suggestions)
529 }
530
531 fn analyze_index_usage(&self, analysis: &QueryAnalysis) -> Result<Vec<OptimizationSuggestion>> {
533 let mut suggestions = Vec::new();
534
535 if analysis.triple_pattern_count > 5 && analysis.filter_count > 0 {
536 suggestions.push(OptimizationSuggestion {
537 id: "INDEX_USAGE_001".to_string(),
538 message: "Consider creating indexes on frequently filtered predicates.".to_string(),
539 severity: SuggestionSeverity::Info,
540 category: SuggestionCategory::IndexUsage,
541 suggested_rewrite: None,
542 estimated_speedup: Some(5.0),
543 explanation:
544 "Indexes on filtered predicates can dramatically improve query performance. \
545 Use the adaptive_index_advisor module for specific recommendations."
546 .to_string(),
547 location: None,
548 documentation_url: None,
549 });
550 }
551
552 Ok(suggestions)
553 }
554
555 pub fn generate_report(&self, suggestions: &[OptimizationSuggestion]) -> String {
557 let mut report = String::new();
558 report.push_str("# Query Optimization Report\n\n");
559
560 if suggestions.is_empty() {
561 report.push_str("✓ No optimization suggestions. Query looks good!\n");
562 return report;
563 }
564
565 let critical: Vec<_> = suggestions
567 .iter()
568 .filter(|s| s.severity == SuggestionSeverity::Critical)
569 .collect();
570 let warnings: Vec<_> = suggestions
571 .iter()
572 .filter(|s| s.severity == SuggestionSeverity::Warning)
573 .collect();
574 let info: Vec<_> = suggestions
575 .iter()
576 .filter(|s| s.severity == SuggestionSeverity::Info)
577 .collect();
578
579 if !critical.is_empty() {
580 report.push_str(&format!("## Critical Issues ({})\n\n", critical.len()));
581 for (i, s) in critical.iter().enumerate() {
582 report.push_str(&self.format_suggestion(i + 1, s));
583 }
584 }
585
586 if !warnings.is_empty() {
587 report.push_str(&format!("## Warnings ({})\n\n", warnings.len()));
588 for (i, s) in warnings.iter().enumerate() {
589 report.push_str(&self.format_suggestion(i + 1, s));
590 }
591 }
592
593 if !info.is_empty() {
594 report.push_str(&format!("## Informational ({})\n\n", info.len()));
595 for (i, s) in info.iter().enumerate() {
596 report.push_str(&self.format_suggestion(i + 1, s));
597 }
598 }
599
600 let total_speedup: f64 = suggestions
602 .iter()
603 .filter_map(|s| s.estimated_speedup)
604 .product();
605
606 if total_speedup > 1.0 {
607 report.push_str(&format!(
608 "\n## Estimated Performance Impact\n\n\
609 Applying all suggestions could improve query performance by up to {:.1}x\n",
610 total_speedup
611 ));
612 }
613
614 report
615 }
616
617 fn format_suggestion(&self, index: usize, suggestion: &OptimizationSuggestion) -> String {
618 let mut output = format!(
619 "### {}. {} [{}]\n\n",
620 index, suggestion.message, suggestion.id
621 );
622 output.push_str(&format!("**Severity**: {}\n", suggestion.severity));
623 output.push_str(&format!("**Category**: {:?}\n\n", suggestion.category));
624 output.push_str(&format!("{}\n\n", suggestion.explanation));
625
626 if let Some(ref rewrite) = suggestion.suggested_rewrite {
627 output.push_str(&format!("**Suggested Fix**: {}\n\n", rewrite));
628 }
629
630 if let Some(speedup) = suggestion.estimated_speedup {
631 output.push_str(&format!("**Estimated Speedup**: {:.1}x\n\n", speedup));
632 }
633
634 output.push_str("---\n\n");
635 output
636 }
637}
638
639#[derive(Debug, Clone, Default)]
641struct QueryAnalysis {
642 query_form: QueryForm,
643 has_distinct: bool,
644 limit: Option<usize>,
645 optional_count: usize,
646 union_count: usize,
647 filter_count: usize,
648 triple_pattern_count: usize,
649 has_select_star: bool,
650 bind_count: usize,
651 has_order_by: bool,
652 has_group_by: bool,
653 has_aggregates: bool,
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
657enum QueryForm {
658 #[default]
659 Select,
660 Ask,
661 Construct,
662 Describe,
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 #[test]
670 fn test_advisor_config_defaults() {
671 let config = AdvisorConfig::default();
672 assert!(config.analyze_pattern_ordering);
673 assert!(config.analyze_best_practices);
674 assert_eq!(config.max_optional_depth, 3);
675 }
676
677 #[test]
678 fn test_simple_query_analysis() {
679 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
680 let query = "SELECT * WHERE { ?s ?p ?o }";
681
682 let suggestions = advisor.analyze_query(query).unwrap();
683 assert!(suggestions.iter().any(|s| s.id == "BEST_PRACTICE_001"));
685 }
686
687 #[test]
688 fn test_missing_limit_warning() {
689 let config = AdvisorConfig {
690 require_limit_clause: true,
691 ..Default::default()
692 };
693
694 let advisor = OptimizationAdvisor::new(config);
695 let query = "SELECT ?s WHERE { ?s ?p ?o }";
696
697 let suggestions = advisor.analyze_query(query).unwrap();
698 assert!(suggestions.iter().any(|s| s.id == "RESULT_LIMIT_001"));
699 }
700
701 #[test]
702 fn test_excessive_limit_warning() {
703 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
704 let query = "SELECT ?s WHERE { ?s ?p ?o } LIMIT 50000";
705
706 let suggestions = advisor.analyze_query(query).unwrap();
707 assert!(suggestions.iter().any(|s| s.id == "RESULT_LIMIT_002"));
708 }
709
710 #[test]
711 fn test_many_patterns_warning() {
712 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
713 let query = "SELECT ?s WHERE { ?s ?p ?o . ?s ?p2 ?o2 . ?s ?p3 ?o3 . ?s ?p4 ?o4 . ?s ?p5 ?o5 . ?s ?p6 ?o6 }";
714
715 let suggestions = advisor.analyze_query(query).unwrap();
716 assert!(suggestions.iter().any(|s| s.id == "PATTERN_ORDER_001"));
717 }
718
719 #[test]
720 fn test_excessive_optional_warning() {
721 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
722 let query = "SELECT ?s WHERE { ?s ?p ?o OPTIONAL { ?s ?p1 ?o1 } OPTIONAL { ?s ?p2 ?o2 } \
723 OPTIONAL { ?s ?p3 ?o3 } OPTIONAL { ?s ?p4 ?o4 } }";
724
725 let suggestions = advisor.analyze_query(query).unwrap();
726 assert!(suggestions.iter().any(|s| s.id == "BEST_PRACTICE_002"));
727 }
728
729 #[test]
730 fn test_report_generation() {
731 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
732 let query = "SELECT * WHERE { ?s ?p ?o }";
733
734 let suggestions = advisor.analyze_query(query).unwrap();
735 let report = advisor.generate_report(&suggestions);
736
737 assert!(report.contains("Query Optimization Report"));
738 assert!(report.contains("SELECT *"));
739 }
740
741 #[test]
742 fn test_severity_ordering() {
743 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
744 let query = "SELECT * WHERE { ?s ?p ?o OPTIONAL { ?s ?p1 ?o1 } OPTIONAL { ?s ?p2 ?o2 } \
745 OPTIONAL { ?s ?p3 ?o3 } OPTIONAL { ?s ?p4 ?o4 } }";
746
747 let suggestions = advisor.analyze_query(query).unwrap();
748
749 for i in 0..suggestions.len().saturating_sub(1) {
751 assert!(suggestions[i].severity >= suggestions[i + 1].severity);
752 }
753 }
754
755 #[test]
756 fn test_filter_placement_suggestion() {
757 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
758 let query =
759 "SELECT ?s WHERE { ?s ?p ?o . ?s ?p2 ?o2 . ?s ?p3 ?o3 . ?s ?p4 ?o4 FILTER(?o > 100) }";
760
761 let suggestions = advisor.analyze_query(query).unwrap();
762 assert!(suggestions.iter().any(|s| s.id == "FILTER_PLACEMENT_001"));
763 }
764
765 #[test]
766 fn test_good_query_no_suggestions() {
767 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
768 let query = "SELECT ?name WHERE { ?person foaf:name ?name } LIMIT 10";
769
770 let suggestions = advisor.analyze_query(query).unwrap();
771
772 let critical_count = suggestions
774 .iter()
775 .filter(|s| s.severity == SuggestionSeverity::Critical)
776 .count();
777
778 assert_eq!(critical_count, 0);
779 }
780
781 #[test]
782 fn test_distinct_suggestion() {
783 let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
784 let query = "SELECT DISTINCT ?s WHERE { ?s ?p ?o }";
785
786 let suggestions = advisor.analyze_query(query).unwrap();
787 assert!(suggestions.iter().any(|s| s.id == "BEST_PRACTICE_004"));
788 }
789}