Skip to main content

oxirs_gql/
query_batching.rs

1//! GraphQL Query Batching
2//!
3//! This module implements efficient query batching to reduce round-trip overhead
4//! and improve overall system performance:
5//! - **Request Batching**: Execute multiple queries in a single HTTP request
6//! - **Automatic Deduplication**: Detect and eliminate duplicate queries
7//! - **Intelligent Scheduling**: Optimize execution order based on dependencies
8//! - **Parallel Execution**: Execute independent queries concurrently
9//! - **Result Aggregation**: Combine results from batched queries
10//! - **Timeout Management**: Per-query and batch-level timeouts
11//!
12//! ## Features
13//!
14//! ### Batch Processing
15//! - Combine multiple GraphQL queries into a single request
16//! - Automatic query deduplication within a batch
17//! - Configurable batch size limits
18//! - Batch timeout management
19//!
20//! ### Execution Strategies
21//! - **Sequential**: Execute queries one by one (for dependencies)
22//! - **Parallel**: Execute all queries concurrently (default)
23//! - **Adaptive**: Analyze dependencies and execute optimally
24//! - **Priority-based**: Execute high-priority queries first
25//!
26//! ### Performance Optimization
27//! - Query result sharing for duplicates
28//! - Connection pooling across batch
29//! - Memory-efficient result aggregation
30//! - Adaptive concurrency based on system load
31//!
32//! ## Usage
33//!
34//! ```rust,ignore
35//! use oxirs_gql::query_batching::{QueryBatcher, BatchConfig, ExecutionStrategy};
36//!
37//! // Create a query batcher
38//! let config = BatchConfig::default()
39//!     .with_max_batch_size(10)
40//!     .with_strategy(ExecutionStrategy::Parallel);
41//!
42//! let batcher = QueryBatcher::new(config);
43//!
44//! // Add queries to batch
45//! let batch_id = batcher.create_batch().await;
46//! batcher.add_query(&batch_id, batched_query).await?;
47//!
48//! // Execute batch
49//! let results = batcher.execute_batch(&batch_id).await?;
50//! ```
51
52use async_trait::async_trait;
53use serde::{Deserialize, Serialize};
54use std::cmp::Reverse;
55use std::collections::{HashMap, VecDeque};
56use std::sync::Arc;
57use std::time::{Duration, Instant};
58use tokio::sync::RwLock;
59
60/// Outcome of executing a single batched query against a backend executor.
61#[derive(Debug, Clone, Default)]
62pub struct BatchExecutionOutcome {
63    /// Result data (JSON), if the query produced any.
64    pub data: Option<serde_json::Value>,
65    /// GraphQL errors encountered while executing the query.
66    pub errors: Vec<String>,
67}
68
69/// Backend that actually executes a batched GraphQL query.
70///
71/// [`QueryBatcher`] is transport/scheduling only; the real work of parsing and
72/// resolving a query is delegated to an implementation of this trait so that a
73/// concrete `QueryExecutor` (or any other engine) can be plugged in.
74#[async_trait]
75pub trait BatchQueryExecutor: Send + Sync {
76    /// Execute one query and return its data / errors.
77    async fn execute_batched(&self, query: &BatchedQuery) -> BatchExecutionOutcome;
78}
79
80/// Adapter that runs batched queries through the crate's real
81/// [`crate::execution::QueryExecutor`].
82pub struct GraphQLBatchExecutor {
83    executor: Arc<crate::execution::QueryExecutor>,
84}
85
86impl GraphQLBatchExecutor {
87    /// Wrap a shared [`crate::execution::QueryExecutor`].
88    pub fn new(executor: Arc<crate::execution::QueryExecutor>) -> Self {
89        Self { executor }
90    }
91
92    /// Convert a JSON variable value into a GraphQL AST value.
93    fn json_to_ast_value(value: &serde_json::Value) -> crate::ast::Value {
94        use crate::ast::Value as AstValue;
95        match value {
96            serde_json::Value::Null => AstValue::NullValue,
97            serde_json::Value::Bool(b) => AstValue::BooleanValue(*b),
98            serde_json::Value::Number(n) => {
99                if let Some(i) = n.as_i64() {
100                    AstValue::IntValue(i)
101                } else {
102                    AstValue::FloatValue(n.as_f64().unwrap_or(0.0))
103                }
104            }
105            serde_json::Value::String(s) => AstValue::StringValue(s.clone()),
106            serde_json::Value::Array(arr) => {
107                AstValue::ListValue(arr.iter().map(Self::json_to_ast_value).collect())
108            }
109            serde_json::Value::Object(obj) => {
110                let map = obj
111                    .iter()
112                    .map(|(k, v)| (k.clone(), Self::json_to_ast_value(v)))
113                    .collect();
114                AstValue::ObjectValue(map)
115            }
116        }
117    }
118}
119
120#[async_trait]
121impl BatchQueryExecutor for GraphQLBatchExecutor {
122    async fn execute_batched(&self, query: &BatchedQuery) -> BatchExecutionOutcome {
123        let document = match crate::parser::parse_document(&query.query) {
124            Ok(doc) => doc,
125            Err(e) => {
126                return BatchExecutionOutcome {
127                    data: None,
128                    errors: vec![format!("Query parse error: {e}")],
129                };
130            }
131        };
132
133        let mut context = crate::execution::ExecutionContext::new();
134        if let Some(name) = &query.operation_name {
135            context.operation_name = Some(name.clone());
136        }
137        if let Some(serde_json::Value::Object(vars)) = &query.variables {
138            for (k, v) in vars {
139                context
140                    .variables
141                    .insert(k.clone(), Self::json_to_ast_value(v));
142            }
143        }
144
145        match self.executor.execute(&document, &context).await {
146            Ok(result) => BatchExecutionOutcome {
147                data: result.data,
148                errors: result.errors.into_iter().map(|e| e.message).collect(),
149            },
150            Err(e) => BatchExecutionOutcome {
151                data: None,
152                errors: vec![e.to_string()],
153            },
154        }
155    }
156}
157
158/// Execution strategy for batch processing
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160pub enum ExecutionStrategy {
161    /// Execute queries sequentially
162    Sequential,
163    /// Execute all queries in parallel
164    Parallel,
165    /// Analyze dependencies and optimize execution
166    Adaptive,
167    /// Execute by priority order
168    PriorityBased,
169}
170
171/// Query priority level
172#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
173pub enum QueryPriority {
174    /// Low priority (background tasks)
175    Low = 0,
176    /// Normal priority (default)
177    #[default]
178    Normal = 1,
179    /// High priority (user-facing queries)
180    High = 2,
181    /// Critical priority (real-time requirements)
182    Critical = 3,
183}
184
185/// Batch configuration
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct BatchConfig {
188    /// Maximum number of queries per batch
189    pub max_batch_size: usize,
190    /// Maximum concurrent query execution
191    pub max_concurrency: usize,
192    /// Batch execution timeout
193    pub batch_timeout: Duration,
194    /// Per-query timeout
195    pub query_timeout: Duration,
196    /// Execution strategy
197    pub strategy: ExecutionStrategy,
198    /// Enable query deduplication
199    pub enable_deduplication: bool,
200    /// Enable result caching
201    pub enable_caching: bool,
202}
203
204impl BatchConfig {
205    /// Create a new batch configuration
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    /// Set maximum batch size
211    pub fn with_max_batch_size(mut self, size: usize) -> Self {
212        self.max_batch_size = size;
213        self
214    }
215
216    /// Set maximum concurrency
217    pub fn with_max_concurrency(mut self, concurrency: usize) -> Self {
218        self.max_concurrency = concurrency;
219        self
220    }
221
222    /// Set batch timeout
223    pub fn with_batch_timeout(mut self, timeout: Duration) -> Self {
224        self.batch_timeout = timeout;
225        self
226    }
227
228    /// Set execution strategy
229    pub fn with_strategy(mut self, strategy: ExecutionStrategy) -> Self {
230        self.strategy = strategy;
231        self
232    }
233
234    /// Enable query deduplication
235    pub fn with_deduplication(mut self, enable: bool) -> Self {
236        self.enable_deduplication = enable;
237        self
238    }
239}
240
241impl Default for BatchConfig {
242    fn default() -> Self {
243        Self {
244            max_batch_size: 100,
245            max_concurrency: 10,
246            batch_timeout: Duration::from_secs(30),
247            query_timeout: Duration::from_secs(10),
248            strategy: ExecutionStrategy::Parallel,
249            enable_deduplication: true,
250            enable_caching: true,
251        }
252    }
253}
254
255/// Batched query
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct BatchedQuery {
258    /// Query ID
259    pub id: String,
260    /// GraphQL query string
261    pub query: String,
262    /// Query variables
263    pub variables: Option<serde_json::Value>,
264    /// Operation name
265    pub operation_name: Option<String>,
266    /// Query priority
267    pub priority: QueryPriority,
268    /// Query fingerprint for deduplication
269    pub fingerprint: String,
270}
271
272impl BatchedQuery {
273    /// Create a new batched query
274    pub fn new(query: String) -> Self {
275        let fingerprint = Self::calculate_fingerprint(&query, &None);
276        Self {
277            id: uuid::Uuid::new_v4().to_string(),
278            query,
279            variables: None,
280            operation_name: None,
281            priority: QueryPriority::default(),
282            fingerprint,
283        }
284    }
285
286    /// Create with variables
287    pub fn with_variables(mut self, variables: serde_json::Value) -> Self {
288        self.fingerprint = Self::calculate_fingerprint(&self.query, &Some(variables.clone()));
289        self.variables = Some(variables);
290        self
291    }
292
293    /// Set operation name
294    pub fn with_operation_name(mut self, name: String) -> Self {
295        self.operation_name = Some(name);
296        self
297    }
298
299    /// Set priority
300    pub fn with_priority(mut self, priority: QueryPriority) -> Self {
301        self.priority = priority;
302        self
303    }
304
305    /// Calculate query fingerprint for deduplication
306    fn calculate_fingerprint(query: &str, variables: &Option<serde_json::Value>) -> String {
307        use sha2::{Digest, Sha256};
308        let mut hasher = Sha256::new();
309        hasher.update(query.as_bytes());
310        if let Some(vars) = variables {
311            if let Ok(vars_str) = serde_json::to_string(vars) {
312                hasher.update(vars_str.as_bytes());
313            }
314        }
315        let result = hasher.finalize();
316        hex::encode(&result[..16]) // Use first 16 bytes
317    }
318}
319
320/// Query execution result
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct QueryResult {
323    /// Query ID
324    pub query_id: String,
325    /// Execution result (JSON)
326    pub data: Option<serde_json::Value>,
327    /// Errors if any
328    pub errors: Vec<String>,
329    /// Execution duration
330    pub duration: Duration,
331    /// Whether result was cached
332    pub cached: bool,
333    /// Whether result was deduplicated
334    pub deduplicated: bool,
335}
336
337/// Batch execution result
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct BatchResult {
340    /// Batch ID
341    pub batch_id: String,
342    /// Query results
343    pub results: Vec<QueryResult>,
344    /// Total batch execution time
345    pub total_duration: Duration,
346    /// Number of queries executed
347    pub queries_executed: usize,
348    /// Number of queries deduplicated
349    pub queries_deduplicated: usize,
350    /// Number of cache hits
351    pub cache_hits: usize,
352    /// Batch statistics
353    pub statistics: BatchStatistics,
354}
355
356/// Batch execution statistics
357#[derive(Debug, Clone, Default, Serialize, Deserialize)]
358pub struct BatchStatistics {
359    /// Total queries submitted
360    pub total_queries: usize,
361    /// Queries executed (after dedup)
362    pub executed_queries: usize,
363    /// Deduplicated queries
364    pub deduplicated_queries: usize,
365    /// Cached queries
366    pub cached_queries: usize,
367    /// Failed queries
368    pub failed_queries: usize,
369    /// Average query duration
370    pub avg_query_duration: Duration,
371    /// Max query duration
372    pub max_query_duration: Duration,
373    /// Min query duration
374    pub min_query_duration: Duration,
375}
376
377/// Query batch
378#[derive(Debug)]
379struct QueryBatch {
380    /// Batch ID
381    id: String,
382    /// Queries in batch
383    queries: Vec<BatchedQuery>,
384    /// Creation timestamp
385    #[allow(dead_code)]
386    created_at: Instant,
387    /// Execution started
388    started: bool,
389    /// Execution completed
390    completed: bool,
391}
392
393impl QueryBatch {
394    fn new() -> Self {
395        Self {
396            id: uuid::Uuid::new_v4().to_string(),
397            queries: Vec::new(),
398            created_at: Instant::now(),
399            started: false,
400            completed: false,
401        }
402    }
403
404    fn add_query(&mut self, query: BatchedQuery) {
405        self.queries.push(query);
406    }
407
408    fn query_count(&self) -> usize {
409        self.queries.len()
410    }
411}
412
413/// Query batcher
414pub struct QueryBatcher {
415    /// Batch configuration
416    config: BatchConfig,
417    /// Active batches
418    batches: Arc<RwLock<HashMap<String, QueryBatch>>>,
419    /// Query result cache
420    cache: Arc<RwLock<HashMap<String, QueryResult>>>,
421    /// Batch statistics
422    statistics: Arc<RwLock<HashMap<String, BatchStatistics>>>,
423    /// Backend that actually executes each GraphQL query. When absent, the
424    /// batcher fails loud (returns an error) rather than fabricating a
425    /// placeholder success payload.
426    executor: Option<Arc<dyn BatchQueryExecutor>>,
427}
428
429impl QueryBatcher {
430    /// Create a new query batcher.
431    ///
432    /// Note: without an executor wired in via [`QueryBatcher::with_executor`],
433    /// every query execution fails loud. This prevents the batcher from
434    /// silently returning (and caching) fabricated placeholder results.
435    pub fn new(config: BatchConfig) -> Self {
436        Self {
437            config,
438            batches: Arc::new(RwLock::new(HashMap::new())),
439            cache: Arc::new(RwLock::new(HashMap::new())),
440            statistics: Arc::new(RwLock::new(HashMap::new())),
441            executor: None,
442        }
443    }
444
445    /// Attach the backend GraphQL executor used to run each batched query.
446    pub fn with_executor(mut self, executor: Arc<dyn BatchQueryExecutor>) -> Self {
447        self.executor = Some(executor);
448        self
449    }
450
451    /// Set the backend GraphQL executor after construction.
452    pub fn set_executor(&mut self, executor: Arc<dyn BatchQueryExecutor>) {
453        self.executor = Some(executor);
454    }
455
456    /// Create a new batch
457    pub async fn create_batch(&self) -> String {
458        let batch = QueryBatch::new();
459        let batch_id = batch.id.clone();
460        self.batches.write().await.insert(batch_id.clone(), batch);
461        batch_id
462    }
463
464    /// Add query to batch
465    pub async fn add_query(&self, batch_id: &str, query: BatchedQuery) -> Result<(), String> {
466        let mut batches = self.batches.write().await;
467        let batch = batches
468            .get_mut(batch_id)
469            .ok_or_else(|| "Batch not found".to_string())?;
470
471        if batch.started {
472            return Err("Batch already started execution".to_string());
473        }
474
475        if batch.query_count() >= self.config.max_batch_size {
476            return Err(format!(
477                "Batch size limit reached ({})",
478                self.config.max_batch_size
479            ));
480        }
481
482        batch.add_query(query);
483        Ok(())
484    }
485
486    /// Execute batch
487    pub async fn execute_batch(&self, batch_id: &str) -> Result<BatchResult, String> {
488        let start_time = Instant::now();
489
490        // Get and mark batch as started
491        let queries = {
492            let mut batches = self.batches.write().await;
493            let batch = batches
494                .get_mut(batch_id)
495                .ok_or_else(|| "Batch not found".to_string())?;
496
497            if batch.started {
498                return Err("Batch already started".to_string());
499            }
500
501            batch.started = true;
502            batch.queries.clone()
503        };
504
505        // Deduplicate queries
506        let (unique_queries, dedup_map) = if self.config.enable_deduplication {
507            self.deduplicate_queries(queries)
508        } else {
509            let map: HashMap<String, String> = HashMap::new();
510            (queries, map)
511        };
512
513        // Execute queries based on strategy
514        let mut results = match self.config.strategy {
515            ExecutionStrategy::Sequential => self.execute_sequential(unique_queries.clone()).await,
516            ExecutionStrategy::Parallel => self.execute_parallel(unique_queries.clone()).await,
517            ExecutionStrategy::Adaptive => self.execute_adaptive(unique_queries.clone()).await,
518            ExecutionStrategy::PriorityBased => {
519                self.execute_priority_based(unique_queries.clone()).await
520            }
521        }?;
522
523        // Apply deduplication results
524        for (original_id, canonical_id) in dedup_map.iter() {
525            if let Some(canonical_result) = results.iter().find(|r| &r.query_id == canonical_id) {
526                let mut dedup_result = canonical_result.clone();
527                dedup_result.query_id = original_id.clone();
528                dedup_result.deduplicated = true;
529                results.push(dedup_result);
530            }
531        }
532
533        // Calculate statistics
534        let statistics = self.calculate_statistics(&results, dedup_map.len());
535
536        // Mark batch as completed
537        {
538            let mut batches = self.batches.write().await;
539            if let Some(batch) = batches.get_mut(batch_id) {
540                batch.completed = true;
541            }
542        }
543
544        // Store statistics
545        self.statistics
546            .write()
547            .await
548            .insert(batch_id.to_string(), statistics.clone());
549
550        Ok(BatchResult {
551            batch_id: batch_id.to_string(),
552            results,
553            total_duration: start_time.elapsed(),
554            queries_executed: unique_queries.len(),
555            queries_deduplicated: dedup_map.len(),
556            cache_hits: statistics.cached_queries,
557            statistics,
558        })
559    }
560
561    /// Deduplicate queries
562    fn deduplicate_queries(
563        &self,
564        queries: Vec<BatchedQuery>,
565    ) -> (Vec<BatchedQuery>, HashMap<String, String>) {
566        let mut unique_queries = Vec::new();
567        let mut dedup_map = HashMap::new();
568        let mut fingerprint_map: HashMap<String, String> = HashMap::new();
569
570        for query in queries {
571            if let Some(canonical_id) = fingerprint_map.get(&query.fingerprint) {
572                // Duplicate found
573                dedup_map.insert(query.id.clone(), canonical_id.clone());
574            } else {
575                // First occurrence
576                fingerprint_map.insert(query.fingerprint.clone(), query.id.clone());
577                unique_queries.push(query);
578            }
579        }
580
581        (unique_queries, dedup_map)
582    }
583
584    /// Execute queries sequentially
585    async fn execute_sequential(
586        &self,
587        queries: Vec<BatchedQuery>,
588    ) -> Result<Vec<QueryResult>, String> {
589        let mut results = Vec::new();
590        for query in queries {
591            let result = self.execute_single_query(query).await?;
592            results.push(result);
593        }
594        Ok(results)
595    }
596
597    /// Execute queries in parallel
598    async fn execute_parallel(
599        &self,
600        queries: Vec<BatchedQuery>,
601    ) -> Result<Vec<QueryResult>, String> {
602        use futures::stream::{self, StreamExt};
603
604        let results = stream::iter(queries)
605            .map(|query| async move { self.execute_single_query(query).await })
606            .buffer_unordered(self.config.max_concurrency)
607            .collect::<Vec<_>>()
608            .await;
609
610        results.into_iter().collect()
611    }
612
613    /// Analyze batch dependencies: returns query_id -> list of query_ids it depends on
614    pub fn analyze_batch_dependencies(queries: &[BatchedQuery]) -> HashMap<String, Vec<String>> {
615        // Build a map from operation_name -> query_id for lookup
616        let name_to_id: HashMap<String, String> = queries
617            .iter()
618            .filter_map(|q| q.operation_name.as_ref().map(|n| (n.clone(), q.id.clone())))
619            .collect();
620
621        let mut dep_map: HashMap<String, Vec<String>> = HashMap::new();
622
623        for query in queries {
624            let mut deps = Vec::new();
625            if let Some(ref vars) = query.variables {
626                if let Some(dep_name) = vars.get("$depends_on").and_then(|v| v.as_str()) {
627                    if let Some(dep_id) = name_to_id.get(dep_name) {
628                        deps.push(dep_id.clone());
629                    }
630                }
631            }
632            dep_map.insert(query.id.clone(), deps);
633        }
634
635        dep_map
636    }
637
638    /// Execute queries adaptively with real dependency analysis and topological sort
639    async fn execute_adaptive(
640        &self,
641        queries: Vec<BatchedQuery>,
642    ) -> Result<Vec<QueryResult>, String> {
643        let dep_map = Self::analyze_batch_dependencies(&queries);
644
645        // Build a map from query_id -> BatchedQuery for lookup
646        let mut query_map: HashMap<String, BatchedQuery> =
647            queries.into_iter().map(|q| (q.id.clone(), q)).collect();
648
649        // Track which query_ids have completed
650        let mut completed: std::collections::HashSet<String> = std::collections::HashSet::new();
651        let mut all_results: Vec<QueryResult> = Vec::new();
652
653        // Topological BFS: process waves of queries whose dependencies are satisfied
654        let mut queue: VecDeque<String> = VecDeque::new();
655
656        // Seed queue with queries that have no dependencies
657        for (id, deps) in &dep_map {
658            if deps.is_empty() {
659                queue.push_back(id.clone());
660            }
661        }
662
663        while !queue.is_empty() {
664            // Drain the current wave
665            let mut wave: Vec<BatchedQuery> = Vec::new();
666            while let Some(id) = queue.pop_front() {
667                if let Some(q) = query_map.remove(&id) {
668                    wave.push(q);
669                }
670            }
671
672            if wave.is_empty() {
673                break;
674            }
675
676            // Execute this wave in parallel
677            let wave_results = self.execute_parallel(wave.clone()).await?;
678
679            // Mark all executed queries as completed
680            for q in &wave {
681                completed.insert(q.id.clone());
682            }
683            all_results.extend(wave_results);
684
685            // Find queries whose dependencies are now all satisfied
686            for (id, deps) in &dep_map {
687                if !completed.contains(id)
688                    && query_map.contains_key(id)
689                    && deps.iter().all(|d| completed.contains(d))
690                {
691                    queue.push_back(id.clone());
692                }
693            }
694        }
695
696        // Any remaining queries with unresolvable deps: execute them anyway
697        if !query_map.is_empty() {
698            let remaining: Vec<BatchedQuery> = query_map.into_values().collect();
699            let remaining_results = self.execute_parallel(remaining).await?;
700            all_results.extend(remaining_results);
701        }
702
703        Ok(all_results)
704    }
705
706    /// Execute queries by priority
707    async fn execute_priority_based(
708        &self,
709        mut queries: Vec<BatchedQuery>,
710    ) -> Result<Vec<QueryResult>, String> {
711        // Sort by priority (highest first)
712        queries.sort_by_key(|q| Reverse(q.priority));
713        self.execute_parallel(queries).await
714    }
715
716    /// Execute a single query
717    async fn execute_single_query(&self, query: BatchedQuery) -> Result<QueryResult, String> {
718        let start_time = Instant::now();
719
720        // Check cache
721        if self.config.enable_caching {
722            let cache = self.cache.read().await;
723            if let Some(cached_result) = cache.get(&query.fingerprint) {
724                let mut result = cached_result.clone();
725                result.query_id = query.id;
726                result.cached = true;
727                return Ok(result);
728            }
729        }
730
731        // Execute the query against the wired-in backend. Fail loud if none is
732        // configured — never fabricate a placeholder success payload.
733        let executor = self.executor.as_ref().ok_or_else(|| {
734            "No GraphQL executor configured on QueryBatcher; \
735             call QueryBatcher::with_executor before executing batches"
736                .to_string()
737        })?;
738
739        let outcome = executor.execute_batched(&query).await;
740
741        let result = QueryResult {
742            query_id: query.id.clone(),
743            data: outcome.data,
744            errors: outcome.errors,
745            duration: start_time.elapsed(),
746            cached: false,
747            deduplicated: false,
748        };
749
750        // Only cache successful (error-free) results so a transient failure is
751        // never memoised as if it were a real answer.
752        if self.config.enable_caching && result.errors.is_empty() {
753            self.cache
754                .write()
755                .await
756                .insert(query.fingerprint, result.clone());
757        }
758
759        Ok(result)
760    }
761
762    /// Calculate batch statistics
763    fn calculate_statistics(
764        &self,
765        results: &[QueryResult],
766        deduplicated_count: usize,
767    ) -> BatchStatistics {
768        let total_queries = results.len();
769        let cached_queries = results.iter().filter(|r| r.cached).count();
770        let failed_queries = results.iter().filter(|r| !r.errors.is_empty()).count();
771        let executed_queries = total_queries - deduplicated_count;
772
773        let durations: Vec<Duration> = results.iter().map(|r| r.duration).collect();
774        let avg_duration = if !durations.is_empty() {
775            durations.iter().sum::<Duration>() / durations.len() as u32
776        } else {
777            Duration::from_secs(0)
778        };
779
780        let max_duration = durations.iter().max().copied().unwrap_or_default();
781        let min_duration = durations.iter().min().copied().unwrap_or_default();
782
783        BatchStatistics {
784            total_queries,
785            executed_queries,
786            deduplicated_queries: deduplicated_count,
787            cached_queries,
788            failed_queries,
789            avg_query_duration: avg_duration,
790            max_query_duration: max_duration,
791            min_query_duration: min_duration,
792        }
793    }
794
795    /// Get batch statistics
796    pub async fn get_statistics(&self, batch_id: &str) -> Option<BatchStatistics> {
797        self.statistics.read().await.get(batch_id).cloned()
798    }
799
800    /// Clear cache
801    pub async fn clear_cache(&self) {
802        self.cache.write().await.clear();
803    }
804
805    /// Get cache size
806    pub async fn cache_size(&self) -> usize {
807        self.cache.read().await.len()
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814
815    /// Deterministic in-test executor: echoes the query text back as real data
816    /// so tests exercise the batching machinery without a live GraphQL schema.
817    struct MockBatchExecutor;
818
819    #[async_trait]
820    impl BatchQueryExecutor for MockBatchExecutor {
821        async fn execute_batched(&self, query: &BatchedQuery) -> BatchExecutionOutcome {
822            BatchExecutionOutcome {
823                data: Some(serde_json::json!({ "echo": query.query })),
824                errors: Vec::new(),
825            }
826        }
827    }
828
829    /// Build a batcher wired to the mock executor.
830    fn test_batcher(config: BatchConfig) -> QueryBatcher {
831        QueryBatcher::new(config).with_executor(Arc::new(MockBatchExecutor))
832    }
833
834    #[tokio::test]
835    async fn regression_execute_without_executor_fails_loud() {
836        // No executor configured => must error, never a placeholder success.
837        let batcher = QueryBatcher::new(BatchConfig::default());
838        let batch_id = batcher.create_batch().await;
839        batcher
840            .add_query(
841                &batch_id,
842                BatchedQuery::new("{ user { name } }".to_string()),
843            )
844            .await
845            .expect("add ok");
846        let result = batcher.execute_batch(&batch_id).await;
847        assert!(
848            result.is_err(),
849            "executing with no backend executor must fail loud"
850        );
851    }
852
853    #[tokio::test]
854    async fn regression_no_placeholder_payload_and_only_real_results_cached() {
855        let batcher = test_batcher(BatchConfig::default());
856        let batch_id = batcher.create_batch().await;
857        batcher
858            .add_query(&batch_id, BatchedQuery::new("{ real }".to_string()))
859            .await
860            .expect("add ok");
861        let result = batcher.execute_batch(&batch_id).await.expect("exec ok");
862        let data = result.results[0].data.as_ref().expect("has data");
863        // The old fabricated payload must be gone.
864        assert!(data.get("placeholder").is_none());
865        assert_eq!(data["echo"], serde_json::json!("{ real }"));
866        // Real result was cached.
867        assert!(batcher.cache_size().await > 0);
868    }
869
870    #[tokio::test]
871    async fn test_batch_config_default() {
872        let config = BatchConfig::default();
873        assert_eq!(config.max_batch_size, 100);
874        assert_eq!(config.max_concurrency, 10);
875        assert_eq!(config.strategy, ExecutionStrategy::Parallel);
876        assert!(config.enable_deduplication);
877    }
878
879    #[tokio::test]
880    async fn test_batch_config_builder() {
881        let config = BatchConfig::new()
882            .with_max_batch_size(50)
883            .with_max_concurrency(5)
884            .with_strategy(ExecutionStrategy::Sequential)
885            .with_deduplication(false);
886
887        assert_eq!(config.max_batch_size, 50);
888        assert_eq!(config.max_concurrency, 5);
889        assert_eq!(config.strategy, ExecutionStrategy::Sequential);
890        assert!(!config.enable_deduplication);
891    }
892
893    #[tokio::test]
894    async fn test_batched_query_creation() {
895        let query = BatchedQuery::new("{ user { name } }".to_string());
896        assert!(!query.id.is_empty());
897        assert_eq!(query.query, "{ user { name } }");
898        assert!(query.variables.is_none());
899        assert_eq!(query.priority, QueryPriority::Normal);
900    }
901
902    #[tokio::test]
903    async fn test_batched_query_with_variables() {
904        let vars = serde_json::json!({"id": 123});
905        let query =
906            BatchedQuery::new("{ user(id: $id) { name } }".to_string()).with_variables(vars);
907
908        assert!(query.variables.is_some());
909    }
910
911    #[tokio::test]
912    async fn test_query_priority() {
913        assert!(QueryPriority::Critical > QueryPriority::High);
914        assert!(QueryPriority::High > QueryPriority::Normal);
915        assert!(QueryPriority::Normal > QueryPriority::Low);
916    }
917
918    #[tokio::test]
919    async fn test_batcher_creation() {
920        let config = BatchConfig::default();
921        let batcher = QueryBatcher::new(config);
922        assert_eq!(batcher.cache_size().await, 0);
923    }
924
925    #[tokio::test]
926    async fn test_create_batch() {
927        let batcher = QueryBatcher::new(BatchConfig::default());
928        let batch_id = batcher.create_batch().await;
929        assert!(!batch_id.is_empty());
930    }
931
932    #[tokio::test]
933    async fn test_add_query_to_batch() {
934        let batcher = QueryBatcher::new(BatchConfig::default());
935        let batch_id = batcher.create_batch().await;
936        let query = BatchedQuery::new("{ user { name } }".to_string());
937
938        let result = batcher.add_query(&batch_id, query).await;
939        assert!(result.is_ok());
940    }
941
942    #[tokio::test]
943    async fn test_add_query_to_nonexistent_batch() {
944        let batcher = QueryBatcher::new(BatchConfig::default());
945        let query = BatchedQuery::new("{ user { name } }".to_string());
946
947        let result = batcher.add_query("nonexistent", query).await;
948        assert!(result.is_err());
949    }
950
951    #[tokio::test]
952    async fn test_batch_size_limit() {
953        let config = BatchConfig::default().with_max_batch_size(2);
954        let batcher = QueryBatcher::new(config);
955        let batch_id = batcher.create_batch().await;
956
957        let q1 = BatchedQuery::new("query1".to_string());
958        let q2 = BatchedQuery::new("query2".to_string());
959        let q3 = BatchedQuery::new("query3".to_string());
960
961        assert!(batcher.add_query(&batch_id, q1).await.is_ok());
962        assert!(batcher.add_query(&batch_id, q2).await.is_ok());
963        assert!(batcher.add_query(&batch_id, q3).await.is_err());
964    }
965
966    #[tokio::test]
967    async fn test_execute_batch() {
968        let batcher = test_batcher(BatchConfig::default());
969        let batch_id = batcher.create_batch().await;
970
971        let q1 = BatchedQuery::new("{ user { name } }".to_string());
972        let q2 = BatchedQuery::new("{ posts { title } }".to_string());
973
974        batcher
975            .add_query(&batch_id, q1)
976            .await
977            .expect("should succeed");
978        batcher
979            .add_query(&batch_id, q2)
980            .await
981            .expect("should succeed");
982
983        let result = batcher.execute_batch(&batch_id).await;
984        assert!(result.is_ok());
985
986        let batch_result = result.expect("should succeed");
987        assert_eq!(batch_result.results.len(), 2);
988        assert_eq!(batch_result.queries_executed, 2);
989    }
990
991    #[tokio::test]
992    async fn test_query_deduplication() {
993        let config = BatchConfig::default().with_deduplication(true);
994        let batcher = test_batcher(config);
995        let batch_id = batcher.create_batch().await;
996
997        let q1 = BatchedQuery::new("{ user { name } }".to_string());
998        let q2 = BatchedQuery::new("{ user { name } }".to_string()); // Duplicate
999
1000        batcher
1001            .add_query(&batch_id, q1)
1002            .await
1003            .expect("should succeed");
1004        batcher
1005            .add_query(&batch_id, q2)
1006            .await
1007            .expect("should succeed");
1008
1009        let result = batcher
1010            .execute_batch(&batch_id)
1011            .await
1012            .expect("should succeed");
1013        assert_eq!(result.queries_executed, 1); // Only one unique query
1014        assert_eq!(result.queries_deduplicated, 1);
1015        assert_eq!(result.results.len(), 2); // But two results
1016    }
1017
1018    #[tokio::test]
1019    async fn test_priority_based_execution() {
1020        let config = BatchConfig::default().with_strategy(ExecutionStrategy::PriorityBased);
1021        let batcher = test_batcher(config);
1022        let batch_id = batcher.create_batch().await;
1023
1024        let q1 = BatchedQuery::new("low".to_string()).with_priority(QueryPriority::Low);
1025        let q2 = BatchedQuery::new("high".to_string()).with_priority(QueryPriority::High);
1026
1027        batcher
1028            .add_query(&batch_id, q1)
1029            .await
1030            .expect("should succeed");
1031        batcher
1032            .add_query(&batch_id, q2)
1033            .await
1034            .expect("should succeed");
1035
1036        let result = batcher.execute_batch(&batch_id).await;
1037        assert!(result.is_ok());
1038    }
1039
1040    #[tokio::test]
1041    async fn test_cache_functionality() {
1042        let config = BatchConfig::default().with_deduplication(false);
1043        let batcher = test_batcher(config);
1044
1045        let batch1 = batcher.create_batch().await;
1046        let q1 = BatchedQuery::new("{ user { name } }".to_string());
1047        batcher
1048            .add_query(&batch1, q1)
1049            .await
1050            .expect("should succeed");
1051        batcher
1052            .execute_batch(&batch1)
1053            .await
1054            .expect("should succeed");
1055
1056        // Second batch with same query should hit cache
1057        let batch2 = batcher.create_batch().await;
1058        let q2 = BatchedQuery::new("{ user { name } }".to_string());
1059        batcher
1060            .add_query(&batch2, q2)
1061            .await
1062            .expect("should succeed");
1063        let result = batcher
1064            .execute_batch(&batch2)
1065            .await
1066            .expect("should succeed");
1067
1068        assert_eq!(result.cache_hits, 1);
1069    }
1070
1071    #[tokio::test]
1072    async fn test_clear_cache() {
1073        let batcher = test_batcher(BatchConfig::default());
1074        let batch_id = batcher.create_batch().await;
1075        let q1 = BatchedQuery::new("{ user { name } }".to_string());
1076        batcher
1077            .add_query(&batch_id, q1)
1078            .await
1079            .expect("should succeed");
1080        batcher
1081            .execute_batch(&batch_id)
1082            .await
1083            .expect("should succeed");
1084
1085        assert!(batcher.cache_size().await > 0);
1086        batcher.clear_cache().await;
1087        assert_eq!(batcher.cache_size().await, 0);
1088    }
1089
1090    #[tokio::test]
1091    async fn test_statistics() {
1092        let batcher = test_batcher(BatchConfig::default());
1093        let batch_id = batcher.create_batch().await;
1094
1095        let q1 = BatchedQuery::new("query1".to_string());
1096        let q2 = BatchedQuery::new("query2".to_string());
1097
1098        batcher
1099            .add_query(&batch_id, q1)
1100            .await
1101            .expect("should succeed");
1102        batcher
1103            .add_query(&batch_id, q2)
1104            .await
1105            .expect("should succeed");
1106
1107        batcher
1108            .execute_batch(&batch_id)
1109            .await
1110            .expect("should succeed");
1111
1112        let stats = batcher.get_statistics(&batch_id).await;
1113        assert!(stats.is_some());
1114
1115        let stats = stats.expect("should succeed");
1116        assert_eq!(stats.total_queries, 2);
1117        assert_eq!(stats.executed_queries, 2);
1118    }
1119
1120    #[tokio::test]
1121    async fn test_adaptive_dependency_analysis() {
1122        let config = BatchConfig::default().with_strategy(ExecutionStrategy::Adaptive);
1123        let batcher = test_batcher(config);
1124        let batch_id = batcher.create_batch().await;
1125
1126        let q_a = BatchedQuery::new("{ a }".to_string()).with_operation_name("A".to_string());
1127        let q_b = BatchedQuery::new("{ b }".to_string())
1128            .with_operation_name("B".to_string())
1129            .with_variables(serde_json::json!({"$depends_on": "A"}));
1130        let q_c = BatchedQuery::new("{ c }".to_string())
1131            .with_operation_name("C".to_string())
1132            .with_variables(serde_json::json!({"$depends_on": "B"}));
1133
1134        let queries = vec![q_a.clone(), q_b.clone(), q_c.clone()];
1135        let dep_map = QueryBatcher::analyze_batch_dependencies(&queries);
1136
1137        // A has no deps, B depends on A, C depends on B
1138        assert!(dep_map[&q_a.id].is_empty(), "A should have no deps");
1139        assert!(!dep_map[&q_b.id].is_empty(), "B should depend on A");
1140        assert!(!dep_map[&q_c.id].is_empty(), "C should depend on B");
1141
1142        batcher
1143            .add_query(&batch_id, q_a)
1144            .await
1145            .expect("should succeed");
1146        batcher
1147            .add_query(&batch_id, q_b)
1148            .await
1149            .expect("should succeed");
1150        batcher
1151            .add_query(&batch_id, q_c)
1152            .await
1153            .expect("should succeed");
1154
1155        let result = batcher
1156            .execute_batch(&batch_id)
1157            .await
1158            .expect("should succeed");
1159        assert_eq!(result.results.len(), 3, "all 3 results should be returned");
1160    }
1161}