Skip to main content

oxirs_gql/
optimizer.rs

1//! GraphQL Query Optimization and Caching
2//!
3//! This module provides query optimization, caching strategies, and performance
4//! enhancements for GraphQL execution over RDF data.
5
6use crate::ast::{Document, Field, Selection, SelectionSet, Value};
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::sync::{Arc, RwLock};
11use std::time::{Duration, Instant};
12use tokio::sync::RwLock as AsyncRwLock;
13
14/// Query optimization and caching configuration
15#[derive(Debug, Clone)]
16pub struct OptimizationConfig {
17    pub enable_query_caching: bool,
18    pub enable_result_caching: bool,
19    pub enable_query_planning: bool,
20    pub enable_field_batching: bool,
21    pub cache_ttl: Duration,
22    pub max_cache_size: usize,
23    pub max_query_depth: usize,
24    pub max_query_complexity: usize,
25}
26
27impl Default for OptimizationConfig {
28    fn default() -> Self {
29        Self {
30            enable_query_caching: true,
31            enable_result_caching: true,
32            enable_query_planning: true,
33            enable_field_batching: true,
34            cache_ttl: Duration::from_secs(300), // 5 minutes
35            max_cache_size: 1000,
36            max_query_depth: 15,
37            max_query_complexity: 1000,
38        }
39    }
40}
41
42/// Query complexity analysis result
43#[derive(Debug, Clone)]
44pub struct QueryComplexity {
45    pub depth: usize,
46    pub field_count: usize,
47    pub complexity_score: usize,
48    pub estimated_cost: f64,
49}
50
51impl QueryComplexity {
52    pub fn is_valid(&self, config: &OptimizationConfig) -> bool {
53        self.depth <= config.max_query_depth && self.complexity_score <= config.max_query_complexity
54    }
55}
56
57/// Cached query plan
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct QueryPlan {
60    pub query_hash: u64,
61    pub sparql_query: String,
62    pub field_order: Vec<String>,
63    pub required_joins: Vec<String>,
64    pub estimated_cost: f64,
65    pub created_at: std::time::SystemTime,
66}
67
68/// Cached result entry
69#[derive(Debug, Clone)]
70pub struct CachedResult {
71    pub result: serde_json::Value,
72    pub created_at: Instant,
73    pub access_count: u32,
74    pub ttl: Duration,
75}
76
77impl CachedResult {
78    pub fn is_expired(&self) -> bool {
79        self.created_at.elapsed() > self.ttl
80    }
81}
82
83/// Query optimization and caching engine
84pub struct QueryOptimizer {
85    config: OptimizationConfig,
86    query_plan_cache: Arc<RwLock<HashMap<u64, QueryPlan>>>,
87    result_cache: Arc<AsyncRwLock<HashMap<String, CachedResult>>>,
88    complexity_analyzer: ComplexityAnalyzer,
89    query_planner: QueryPlanner,
90}
91
92impl QueryOptimizer {
93    pub fn new(config: OptimizationConfig) -> Self {
94        Self {
95            config: config.clone(),
96            query_plan_cache: Arc::new(RwLock::new(HashMap::new())),
97            result_cache: Arc::new(AsyncRwLock::new(HashMap::new())),
98            complexity_analyzer: ComplexityAnalyzer::new(config.clone()),
99            query_planner: QueryPlanner::new(config),
100        }
101    }
102
103    /// Analyze query complexity and validate against limits
104    pub fn analyze_complexity(&self, document: &Document) -> Result<QueryComplexity> {
105        self.complexity_analyzer.analyze(document)
106    }
107
108    /// Get or create optimized query plan
109    pub async fn get_query_plan(&self, document: &Document) -> Result<QueryPlan> {
110        if !self.config.enable_query_planning {
111            return self.query_planner.create_basic_plan(document).await;
112        }
113
114        let query_hash = self.calculate_query_hash(document);
115
116        // Check cache first
117        if let Some(cached_plan) = self.get_cached_plan(query_hash) {
118            return Ok(cached_plan);
119        }
120
121        // Create new plan
122        let plan = self.query_planner.create_optimized_plan(document).await?;
123
124        // Cache the plan
125        self.cache_query_plan(query_hash, plan.clone()).await;
126
127        Ok(plan)
128    }
129
130    /// Get cached result if available and not expired
131    pub async fn get_cached_result(&self, cache_key: &str) -> Option<serde_json::Value> {
132        if !self.config.enable_result_caching {
133            return None;
134        }
135
136        let cache = self.result_cache.read().await;
137        if let Some(entry) = cache.get(cache_key) {
138            if !entry.is_expired() {
139                return Some(entry.result.clone());
140            }
141        }
142        None
143    }
144
145    /// Cache query result
146    pub async fn cache_result(&self, cache_key: String, result: serde_json::Value) {
147        if !self.config.enable_result_caching {
148            return;
149        }
150
151        let mut cache = self.result_cache.write().await;
152
153        // Evict expired entries and enforce size limit
154        self.evict_expired_results(&mut cache);
155
156        if cache.len() >= self.config.max_cache_size {
157            self.evict_lru_result(&mut cache);
158        }
159
160        cache.insert(
161            cache_key,
162            CachedResult {
163                result,
164                created_at: Instant::now(),
165                access_count: 1,
166                ttl: self.config.cache_ttl,
167            },
168        );
169    }
170
171    /// Generate cache key for a query with variables
172    pub fn generate_cache_key(
173        &self,
174        document: &Document,
175        variables: &HashMap<String, Value>,
176    ) -> String {
177        use std::collections::hash_map::DefaultHasher;
178        use std::hash::{Hash, Hasher};
179
180        let mut hasher = DefaultHasher::new();
181        format!("{document:?}").hash(&mut hasher);
182        format!("{variables:?}").hash(&mut hasher);
183        let finish = hasher.finish();
184        format!("query_{finish}")
185    }
186
187    /// Optimize field selections for batching
188    pub fn optimize_field_batching(&self, selections: &[Selection]) -> Vec<Vec<Selection>> {
189        if !self.config.enable_field_batching {
190            return vec![selections.to_vec()];
191        }
192
193        // Group fields by their expected cost/complexity
194        let mut scalar_fields = Vec::new();
195        let mut object_fields = Vec::new();
196        let mut complex_fields = Vec::new();
197
198        for selection in selections {
199            match selection {
200                Selection::Field(field) => {
201                    if field.selection_set.is_some() {
202                        if self.is_complex_field(field) {
203                            complex_fields.push(selection.clone());
204                        } else {
205                            object_fields.push(selection.clone());
206                        }
207                    } else {
208                        scalar_fields.push(selection.clone());
209                    }
210                }
211                _ => complex_fields.push(selection.clone()),
212            }
213        }
214
215        // Return batches: scalars first, then objects, then complex fields
216        let mut batches = Vec::new();
217        if !scalar_fields.is_empty() {
218            batches.push(scalar_fields);
219        }
220        if !object_fields.is_empty() {
221            batches.push(object_fields);
222        }
223        if !complex_fields.is_empty() {
224            batches.push(complex_fields);
225        }
226
227        if batches.is_empty() {
228            vec![selections.to_vec()]
229        } else {
230            batches
231        }
232    }
233
234    fn calculate_query_hash(&self, document: &Document) -> u64 {
235        use std::collections::hash_map::DefaultHasher;
236        use std::hash::{Hash, Hasher};
237
238        let mut hasher = DefaultHasher::new();
239        format!("{document:?}").hash(&mut hasher);
240        hasher.finish()
241    }
242
243    fn get_cached_plan(&self, query_hash: u64) -> Option<QueryPlan> {
244        let cache = self.query_plan_cache.read().ok()?;
245        cache.get(&query_hash).cloned()
246    }
247
248    async fn cache_query_plan(&self, query_hash: u64, plan: QueryPlan) {
249        if let Ok(mut cache) = self.query_plan_cache.write() {
250            if cache.len() >= self.config.max_cache_size {
251                // Simple LRU: remove oldest entry
252                if let Some(oldest_key) = cache.keys().next().copied() {
253                    cache.remove(&oldest_key);
254                }
255            }
256            cache.insert(query_hash, plan);
257        }
258    }
259
260    fn evict_expired_results(&self, cache: &mut HashMap<String, CachedResult>) {
261        let expired_keys: Vec<String> = cache
262            .iter()
263            .filter(|(_, entry)| entry.is_expired())
264            .map(|(key, _)| key.clone())
265            .collect();
266
267        for key in expired_keys {
268            cache.remove(&key);
269        }
270    }
271
272    fn evict_lru_result(&self, cache: &mut HashMap<String, CachedResult>) {
273        // Find entry with lowest access count
274        let lru_key = cache
275            .iter()
276            .min_by_key(|(_, entry)| entry.access_count)
277            .map(|(key, _)| key.clone());
278
279        if let Some(key) = lru_key {
280            cache.remove(&key);
281        }
282    }
283
284    fn is_complex_field(&self, field: &Field) -> bool {
285        // Determine if a field is complex based on arguments and nesting
286        field.arguments.len() > 2
287            || field
288                .selection_set
289                .as_ref()
290                .map(|ss| ss.selections.len() > 5)
291                .unwrap_or(false)
292    }
293}
294
295/// Query complexity analyzer
296pub struct ComplexityAnalyzer {
297    #[allow(dead_code)]
298    config: OptimizationConfig,
299}
300
301impl ComplexityAnalyzer {
302    pub fn new(config: OptimizationConfig) -> Self {
303        Self { config }
304    }
305
306    pub fn analyze(&self, document: &Document) -> Result<QueryComplexity> {
307        let mut depth = 0;
308        let mut field_count = 0;
309        let mut complexity_score = 0;
310
311        for definition in &document.definitions {
312            if let crate::ast::Definition::Operation(operation) = definition {
313                let (op_depth, op_fields, op_complexity) =
314                    self.analyze_selection_set(&operation.selection_set, 1)?;
315
316                depth = depth.max(op_depth);
317                field_count += op_fields;
318                complexity_score += op_complexity;
319            }
320        }
321
322        let estimated_cost = self.calculate_estimated_cost(depth, field_count, complexity_score);
323
324        Ok(QueryComplexity {
325            depth,
326            field_count,
327            complexity_score,
328            estimated_cost,
329        })
330    }
331
332    fn analyze_selection_set(
333        &self,
334        selection_set: &SelectionSet,
335        current_depth: usize,
336    ) -> Result<(usize, usize, usize)> {
337        let mut max_depth = current_depth;
338        let mut total_fields = 0;
339        let mut total_complexity = 0;
340
341        for selection in &selection_set.selections {
342            match selection {
343                Selection::Field(field) => {
344                    total_fields += 1;
345                    total_complexity += self.calculate_field_complexity(field);
346
347                    if let Some(nested_selection) = &field.selection_set {
348                        let (nested_depth, nested_fields, nested_complexity) =
349                            self.analyze_selection_set(nested_selection, current_depth + 1)?;
350
351                        max_depth = max_depth.max(nested_depth);
352                        total_fields += nested_fields;
353                        total_complexity += nested_complexity;
354                    }
355                }
356                Selection::InlineFragment(fragment) => {
357                    let (frag_depth, frag_fields, frag_complexity) =
358                        self.analyze_selection_set(&fragment.selection_set, current_depth)?;
359
360                    max_depth = max_depth.max(frag_depth);
361                    total_fields += frag_fields;
362                    total_complexity += frag_complexity;
363                }
364                Selection::FragmentSpread(_) => {
365                    // Fragment spreads add complexity
366                    total_complexity += 5;
367                }
368            }
369        }
370
371        Ok((max_depth, total_fields, total_complexity))
372    }
373
374    fn calculate_field_complexity(&self, field: &Field) -> usize {
375        let mut complexity = 1; // Base complexity
376
377        // Arguments increase complexity
378        complexity += field.arguments.len() * 2;
379
380        // Directives increase complexity
381        complexity += field.directives.len() * 3;
382
383        // Certain field names are known to be expensive
384        match field.name.as_str() {
385            "sparql" => complexity += 10,
386            "search" | "filter" => complexity += 5,
387            _ if field.name.contains("aggregate") => complexity += 8,
388            _ => {}
389        }
390
391        complexity
392    }
393
394    fn calculate_estimated_cost(
395        &self,
396        depth: usize,
397        field_count: usize,
398        complexity_score: usize,
399    ) -> f64 {
400        // Simple cost estimation formula
401        let depth_cost = depth as f64 * 1.5;
402        let field_cost = field_count as f64 * 0.5;
403        let complexity_cost = complexity_score as f64 * 0.1;
404
405        depth_cost + field_cost + complexity_cost
406    }
407}
408
409/// Query planner for optimizing SPARQL generation
410pub struct QueryPlanner {
411    #[allow(dead_code)]
412    config: OptimizationConfig,
413}
414
415impl QueryPlanner {
416    pub fn new(config: OptimizationConfig) -> Self {
417        Self { config }
418    }
419
420    pub async fn create_basic_plan(&self, document: &Document) -> Result<QueryPlan> {
421        let query_hash = self.calculate_document_hash(document);
422
423        Ok(QueryPlan {
424            query_hash,
425            sparql_query: "SELECT * WHERE { ?s ?p ?o } LIMIT 10".to_string(),
426            field_order: vec!["s".to_string(), "p".to_string(), "o".to_string()],
427            required_joins: vec![],
428            estimated_cost: 1.0,
429            created_at: std::time::SystemTime::now(),
430        })
431    }
432
433    pub async fn create_optimized_plan(&self, document: &Document) -> Result<QueryPlan> {
434        let query_hash = self.calculate_document_hash(document);
435
436        // Analyze the query structure
437        let field_analysis = self.analyze_query_fields(document)?;
438        let join_analysis = self.analyze_required_joins(&field_analysis);
439        let optimization_hints = self.generate_optimization_hints(&field_analysis);
440
441        // Build optimized SPARQL query
442        let sparql_query =
443            self.build_optimized_sparql(&field_analysis, &join_analysis, &optimization_hints);
444
445        // Calculate estimated cost
446        let estimated_cost = self.estimate_query_cost(&field_analysis, &join_analysis);
447
448        Ok(QueryPlan {
449            query_hash,
450            sparql_query,
451            field_order: field_analysis.field_names,
452            required_joins: join_analysis,
453            estimated_cost,
454            created_at: std::time::SystemTime::now(),
455        })
456    }
457
458    fn calculate_document_hash(&self, document: &Document) -> u64 {
459        use std::collections::hash_map::DefaultHasher;
460        use std::hash::{Hash, Hasher};
461
462        let mut hasher = DefaultHasher::new();
463        format!("{document:?}").hash(&mut hasher);
464        hasher.finish()
465    }
466
467    fn analyze_query_fields(&self, document: &Document) -> Result<FieldAnalysis> {
468        let mut field_names = Vec::new();
469        let mut scalar_fields = HashSet::new();
470        let mut object_fields = HashSet::new();
471        let mut filter_conditions = Vec::new();
472
473        for definition in &document.definitions {
474            if let crate::ast::Definition::Operation(operation) = definition {
475                self.collect_field_info(
476                    &operation.selection_set,
477                    &mut field_names,
478                    &mut scalar_fields,
479                    &mut object_fields,
480                    &mut filter_conditions,
481                );
482            }
483        }
484
485        Ok(FieldAnalysis {
486            field_names,
487            scalar_fields,
488            object_fields,
489            filter_conditions,
490        })
491    }
492
493    #[allow(clippy::only_used_in_recursion)]
494    fn collect_field_info(
495        &self,
496        selection_set: &SelectionSet,
497        field_names: &mut Vec<String>,
498        scalar_fields: &mut HashSet<String>,
499        object_fields: &mut HashSet<String>,
500        filter_conditions: &mut Vec<String>,
501    ) {
502        for selection in &selection_set.selections {
503            if let Selection::Field(field) = selection {
504                field_names.push(field.name.clone());
505
506                if field.selection_set.is_some() {
507                    object_fields.insert(field.name.clone());
508                    if let Some(nested) = &field.selection_set {
509                        self.collect_field_info(
510                            nested,
511                            field_names,
512                            scalar_fields,
513                            object_fields,
514                            filter_conditions,
515                        );
516                    }
517                } else {
518                    scalar_fields.insert(field.name.clone());
519                }
520
521                // Extract filter conditions from arguments
522                for arg in &field.arguments {
523                    if arg.name == "where" || arg.name == "filter" {
524                        if let Value::StringValue(condition) = &arg.value {
525                            filter_conditions.push(condition.clone());
526                        }
527                    }
528                }
529            }
530        }
531    }
532
533    fn analyze_required_joins(&self, field_analysis: &FieldAnalysis) -> Vec<String> {
534        let mut joins = Vec::new();
535
536        // Analyze object fields that require joins
537        for object_field in &field_analysis.object_fields {
538            match object_field.as_str() {
539                "knows" => joins.push("PERSON_KNOWS_JOIN".to_string()),
540                "worksFor" => joins.push("PERSON_ORG_JOIN".to_string()),
541                "memberOf" => joins.push("PERSON_GROUP_JOIN".to_string()),
542                _ => {}
543            }
544        }
545
546        joins
547    }
548
549    fn generate_optimization_hints(&self, field_analysis: &FieldAnalysis) -> Vec<String> {
550        let mut hints = Vec::new();
551
552        // Generate hints based on field patterns
553        if field_analysis.scalar_fields.contains("id") {
554            hints.push("USE_ID_INDEX".to_string());
555        }
556
557        if field_analysis.field_names.len() > 10 {
558            hints.push("LIMIT_RESULT_SIZE".to_string());
559        }
560
561        if !field_analysis.filter_conditions.is_empty() {
562            hints.push("FILTER_EARLY".to_string());
563        }
564
565        hints
566    }
567
568    fn build_optimized_sparql(
569        &self,
570        field_analysis: &FieldAnalysis,
571        joins: &[String],
572        hints: &[String],
573    ) -> String {
574        let mut sparql_parts = Vec::new();
575
576        // Add prefixes
577        sparql_parts.push("PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>".to_string());
578        sparql_parts.push("PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>".to_string());
579        sparql_parts.push("PREFIX foaf: <http://xmlns.com/foaf/0.1/>".to_string());
580
581        // Build SELECT clause
582        let select_vars: Vec<String> = field_analysis
583            .field_names
584            .iter()
585            .map(|name| format!("?{name}"))
586            .collect();
587
588        if select_vars.is_empty() {
589            sparql_parts.push("SELECT ?s ?p ?o".to_string());
590        } else {
591            sparql_parts.push(format!("SELECT {}", select_vars.join(" ")));
592        }
593
594        // Build WHERE clause
595        let mut where_patterns = Vec::new();
596
597        // Basic triple pattern
598        where_patterns.push("?s ?p ?o".to_string());
599
600        // Add specific patterns based on fields
601        for field in &field_analysis.scalar_fields {
602            match field.as_str() {
603                "name" => where_patterns.push("?s foaf:name ?name".to_string()),
604                "email" => where_patterns.push("?s foaf:mbox ?email".to_string()),
605                "age" => where_patterns.push("?s foaf:age ?age".to_string()),
606                _ => {}
607            }
608        }
609
610        // Add join patterns
611        for join in joins {
612            match join.as_str() {
613                "PERSON_KNOWS_JOIN" => where_patterns.push("?s foaf:knows ?knows".to_string()),
614                "PERSON_ORG_JOIN" => {
615                    where_patterns.push("?s foaf:workplaceHomepage ?org".to_string())
616                }
617                _ => {}
618            }
619        }
620
621        // Add filter conditions
622        for condition in &field_analysis.filter_conditions {
623            where_patterns.push(format!("FILTER({condition})"));
624        }
625
626        sparql_parts.push(format!("WHERE {{ {} }}", where_patterns.join(" . ")));
627
628        // Add optimization hints as comments
629        for hint in hints {
630            sparql_parts.push(format!("# HINT: {hint}"));
631        }
632
633        // Add LIMIT if suggested
634        if hints.contains(&"LIMIT_RESULT_SIZE".to_string()) {
635            sparql_parts.push("LIMIT 100".to_string());
636        } else {
637            sparql_parts.push("LIMIT 1000".to_string());
638        }
639
640        sparql_parts.join("\n")
641    }
642
643    fn estimate_query_cost(&self, field_analysis: &FieldAnalysis, joins: &[String]) -> f64 {
644        let base_cost = 1.0;
645        let field_cost = field_analysis.field_names.len() as f64 * 0.1;
646        let join_cost = joins.len() as f64 * 2.0;
647        let filter_cost = field_analysis.filter_conditions.len() as f64 * 0.5;
648
649        base_cost + field_cost + join_cost + filter_cost
650    }
651}
652
653/// Analysis result for query fields
654#[derive(Debug)]
655struct FieldAnalysis {
656    field_names: Vec<String>,
657    scalar_fields: HashSet<String>,
658    object_fields: HashSet<String>,
659    filter_conditions: Vec<String>,
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::parser::parse_document;
666
667    #[test]
668    fn test_complexity_analyzer() {
669        let query = "{ hello world }";
670        let document = parse_document(query).expect("should succeed");
671
672        let analyzer = ComplexityAnalyzer::new(OptimizationConfig::default());
673        let complexity = analyzer.analyze(&document).expect("should succeed");
674
675        assert!(complexity.depth >= 1);
676        assert!(complexity.field_count >= 2);
677    }
678
679    #[tokio::test]
680    async fn test_query_optimizer() {
681        let config = OptimizationConfig::default();
682        let optimizer = QueryOptimizer::new(config);
683
684        let query = "{ hello version }";
685        let document = parse_document(query).expect("should succeed");
686
687        let complexity = optimizer
688            .analyze_complexity(&document)
689            .expect("should succeed");
690        assert!(complexity.is_valid(&optimizer.config));
691
692        let plan = optimizer
693            .get_query_plan(&document)
694            .await
695            .expect("should succeed");
696        assert!(!plan.sparql_query.is_empty());
697    }
698
699    #[tokio::test]
700    async fn test_result_caching() {
701        let config = OptimizationConfig::default();
702        let optimizer = QueryOptimizer::new(config);
703
704        let cache_key = "test_key".to_string();
705        let result = serde_json::json!({"hello": "world"});
706
707        optimizer
708            .cache_result(cache_key.clone(), result.clone())
709            .await;
710
711        let cached = optimizer.get_cached_result(&cache_key).await;
712        assert_eq!(cached, Some(result));
713    }
714}