Skip to main content

oxirs_arq/
query_optimization_advisor.rs

1//! Automatic Query Optimization Advisor
2//!
3//! Analyzes SPARQL queries and provides actionable optimization suggestions.
4//!
5//! ## Features
6//!
7//! - **Pattern analysis**: Identifies inefficient triple patterns
8//! - **Index recommendations**: Suggests beneficial indexes
9//! - **Query rewriting**: Proposes equivalent but faster formulations
10//! - **Cost estimation**: Predicts performance impact of suggestions
11//! - **Best practices**: Enforces SPARQL optimization guidelines
12//! - **SciRS2 integration**: Statistical analysis of query patterns
13//!
14//! ## Example
15//!
16//! ```rust,ignore
17//! use oxirs_arq::query_optimization_advisor::{OptimizationAdvisor, AdvisorConfig};
18//!
19//! let advisor = OptimizationAdvisor::new(AdvisorConfig::default());
20//! let suggestions = advisor.analyze_query("SELECT * WHERE { ?s ?p ?o }")?;
21//!
22//! for suggestion in suggestions {
23//!     println!("{}: {}", suggestion.severity, suggestion.message);
24//! }
25//! ```
26
27use anyhow::Result;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31/// Optimization suggestion severity
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
33pub enum SuggestionSeverity {
34    /// Informational - minor optimization opportunity
35    Info,
36
37    /// Warning - notable performance issue
38    Warning,
39
40    /// Critical - major performance bottleneck
41    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/// Category of optimization suggestion
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub enum SuggestionCategory {
57    /// Pattern ordering suggestions
58    PatternOrdering,
59
60    /// Index usage recommendations
61    IndexUsage,
62
63    /// Filter placement optimization
64    FilterPlacement,
65
66    /// Join algorithm selection
67    JoinStrategy,
68
69    /// Result limiting
70    ResultLimitation,
71
72    /// Query structure
73    QueryStructure,
74
75    /// Best practices
76    BestPractices,
77
78    /// Resource usage
79    ResourceUsage,
80}
81
82/// Optimization suggestion with metadata
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct OptimizationSuggestion {
85    /// Unique identifier for this suggestion type
86    pub id: String,
87
88    /// Human-readable message
89    pub message: String,
90
91    /// Severity level
92    pub severity: SuggestionSeverity,
93
94    /// Category
95    pub category: SuggestionCategory,
96
97    /// Suggested rewrite (if applicable)
98    pub suggested_rewrite: Option<String>,
99
100    /// Estimated performance impact (multiplier: >1.0 = faster)
101    pub estimated_speedup: Option<f64>,
102
103    /// Detailed explanation
104    pub explanation: String,
105
106    /// Location in query (line/column if available)
107    pub location: Option<QueryLocation>,
108
109    /// Related documentation
110    pub documentation_url: Option<String>,
111}
112
113/// Location in a SPARQL query
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct QueryLocation {
116    pub line: usize,
117    pub column: usize,
118    pub length: usize,
119}
120
121/// Configuration for optimization advisor
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct AdvisorConfig {
124    /// Enable pattern ordering suggestions
125    pub analyze_pattern_ordering: bool,
126
127    /// Enable index recommendations
128    pub analyze_index_usage: bool,
129
130    /// Enable filter placement analysis
131    pub analyze_filter_placement: bool,
132
133    /// Enable join strategy recommendations
134    pub analyze_join_strategy: bool,
135
136    /// Enable result limitation checks
137    pub analyze_result_limits: bool,
138
139    /// Enable best practices checks
140    pub analyze_best_practices: bool,
141
142    /// Minimum selectivity for filter warnings
143    pub filter_selectivity_threshold: f64,
144
145    /// Maximum recommended OPTIONAL depth
146    pub max_optional_depth: usize,
147
148    /// Maximum recommended UNION branches
149    pub max_union_branches: usize,
150
151    /// Warn about queries without LIMIT
152    pub require_limit_clause: bool,
153
154    /// Maximum suggested LIMIT value
155    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
176/// Query optimization advisor
177pub struct OptimizationAdvisor {
178    config: AdvisorConfig,
179    #[allow(dead_code)]
180    pattern_statistics: HashMap<String, PatternStatistics>,
181}
182
183/// Statistics for a triple pattern
184#[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    /// Create a new advisor with configuration
194    pub fn new(config: AdvisorConfig) -> Self {
195        Self {
196            config,
197            pattern_statistics: HashMap::new(),
198        }
199    }
200
201    /// Analyze a SPARQL query and return optimization suggestions
202    pub fn analyze_query(&self, query: &str) -> Result<Vec<OptimizationSuggestion>> {
203        let mut suggestions = Vec::new();
204
205        // Parse query structure (simplified analysis)
206        let analysis = self.analyze_query_structure(query)?;
207
208        // Run various analyses
209        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        // Sort by severity (critical first)
234        suggestions.sort_by_key(|b| std::cmp::Reverse(b.severity));
235
236        Ok(suggestions)
237    }
238
239    /// Analyze query structure (simplified pattern extraction)
240    fn analyze_query_structure(&self, query: &str) -> Result<QueryAnalysis> {
241        let mut analysis = QueryAnalysis::default();
242
243        // Count SELECT/ASK/CONSTRUCT/DESCRIBE
244        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        // Check for DISTINCT
255        analysis.has_distinct = query.contains("DISTINCT");
256
257        // Check for LIMIT
258        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        // Count OPTIONAL blocks
265        analysis.optional_count = query.matches("OPTIONAL").count();
266
267        // Count UNION blocks
268        analysis.union_count = query.matches("UNION").count();
269
270        // Count FILTER clauses
271        analysis.filter_count = query.matches("FILTER").count();
272
273        // Count triple patterns (approximate - count pattern terminators)
274        // Each triple pattern ends with either ". " or " ." (possibly followed by })
275        let dot_space = query.matches(". ").count();
276        let space_dot = query.matches(" .").count();
277        let dot_brace = query.matches(".}").count();
278
279        // Take the maximum as a rough estimate
280        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        // Check for SELECT *
284        analysis.has_select_star = query.contains("SELECT *") || query.contains("SELECT*");
285
286        // Check for BIND
287        analysis.bind_count = query.matches("BIND").count();
288
289        // Check for ORDER BY
290        analysis.has_order_by = query.contains("ORDER BY");
291
292        // Check for GROUP BY
293        analysis.has_group_by = query.contains("GROUP BY");
294
295        // Check for aggregates
296        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    /// Analyze triple pattern ordering
306    fn analyze_pattern_ordering(
307        &self,
308        analysis: &QueryAnalysis,
309    ) -> Result<Vec<OptimizationSuggestion>> {
310        let mut suggestions = Vec::new();
311
312        // Suggest reordering if many patterns without statistics
313        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    /// Analyze filter placement
336    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    /// Analyze join strategy
365    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    /// Analyze result limitations
391    fn analyze_result_limits(
392        &self,
393        analysis: &QueryAnalysis,
394    ) -> Result<Vec<OptimizationSuggestion>> {
395        let mut suggestions = Vec::new();
396
397        // Check for missing LIMIT
398        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        // Check for excessive LIMIT
418        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    /// Analyze best practices
446    fn analyze_best_practices(
447        &self,
448        analysis: &QueryAnalysis,
449    ) -> Result<Vec<OptimizationSuggestion>> {
450        let mut suggestions = Vec::new();
451
452        // Warn about SELECT *
453        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        // Warn about excessive OPTIONAL
470        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        // Warn about excessive UNION
490        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        // Recommend DISTINCT carefully
510        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    /// Analyze index usage
532    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    /// Generate optimization report
556    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        // Group by severity
566        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        // Calculate potential speedup
601        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/// Simplified query analysis structure
640#[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        // Should suggest avoiding SELECT *
684        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        // Verify suggestions are sorted by severity (critical first)
750        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        // This query follows best practices - should have minimal/no critical suggestions
773        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}