1use 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#[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), max_cache_size: 1000,
36 max_query_depth: 15,
37 max_query_complexity: 1000,
38 }
39 }
40}
41
42#[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#[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#[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
83pub 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 pub fn analyze_complexity(&self, document: &Document) -> Result<QueryComplexity> {
105 self.complexity_analyzer.analyze(document)
106 }
107
108 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 if let Some(cached_plan) = self.get_cached_plan(query_hash) {
118 return Ok(cached_plan);
119 }
120
121 let plan = self.query_planner.create_optimized_plan(document).await?;
123
124 self.cache_query_plan(query_hash, plan.clone()).await;
126
127 Ok(plan)
128 }
129
130 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 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 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 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 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 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 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 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 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 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
295pub 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 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; complexity += field.arguments.len() * 2;
379
380 complexity += field.directives.len() * 3;
382
383 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 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
409pub 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 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 let sparql_query =
443 self.build_optimized_sparql(&field_analysis, &join_analysis, &optimization_hints);
444
445 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 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 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 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 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 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 let mut where_patterns = Vec::new();
596
597 where_patterns.push("?s ?p ?o".to_string());
599
600 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 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 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 for hint in hints {
630 sparql_parts.push(format!("# HINT: {hint}"));
631 }
632
633 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#[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}