1use 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#[derive(Debug, Clone, Default)]
62pub struct BatchExecutionOutcome {
63 pub data: Option<serde_json::Value>,
65 pub errors: Vec<String>,
67}
68
69#[async_trait]
75pub trait BatchQueryExecutor: Send + Sync {
76 async fn execute_batched(&self, query: &BatchedQuery) -> BatchExecutionOutcome;
78}
79
80pub struct GraphQLBatchExecutor {
83 executor: Arc<crate::execution::QueryExecutor>,
84}
85
86impl GraphQLBatchExecutor {
87 pub fn new(executor: Arc<crate::execution::QueryExecutor>) -> Self {
89 Self { executor }
90 }
91
92 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160pub enum ExecutionStrategy {
161 Sequential,
163 Parallel,
165 Adaptive,
167 PriorityBased,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
173pub enum QueryPriority {
174 Low = 0,
176 #[default]
178 Normal = 1,
179 High = 2,
181 Critical = 3,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct BatchConfig {
188 pub max_batch_size: usize,
190 pub max_concurrency: usize,
192 pub batch_timeout: Duration,
194 pub query_timeout: Duration,
196 pub strategy: ExecutionStrategy,
198 pub enable_deduplication: bool,
200 pub enable_caching: bool,
202}
203
204impl BatchConfig {
205 pub fn new() -> Self {
207 Self::default()
208 }
209
210 pub fn with_max_batch_size(mut self, size: usize) -> Self {
212 self.max_batch_size = size;
213 self
214 }
215
216 pub fn with_max_concurrency(mut self, concurrency: usize) -> Self {
218 self.max_concurrency = concurrency;
219 self
220 }
221
222 pub fn with_batch_timeout(mut self, timeout: Duration) -> Self {
224 self.batch_timeout = timeout;
225 self
226 }
227
228 pub fn with_strategy(mut self, strategy: ExecutionStrategy) -> Self {
230 self.strategy = strategy;
231 self
232 }
233
234 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#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct BatchedQuery {
258 pub id: String,
260 pub query: String,
262 pub variables: Option<serde_json::Value>,
264 pub operation_name: Option<String>,
266 pub priority: QueryPriority,
268 pub fingerprint: String,
270}
271
272impl BatchedQuery {
273 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 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 pub fn with_operation_name(mut self, name: String) -> Self {
295 self.operation_name = Some(name);
296 self
297 }
298
299 pub fn with_priority(mut self, priority: QueryPriority) -> Self {
301 self.priority = priority;
302 self
303 }
304
305 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]) }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct QueryResult {
323 pub query_id: String,
325 pub data: Option<serde_json::Value>,
327 pub errors: Vec<String>,
329 pub duration: Duration,
331 pub cached: bool,
333 pub deduplicated: bool,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct BatchResult {
340 pub batch_id: String,
342 pub results: Vec<QueryResult>,
344 pub total_duration: Duration,
346 pub queries_executed: usize,
348 pub queries_deduplicated: usize,
350 pub cache_hits: usize,
352 pub statistics: BatchStatistics,
354}
355
356#[derive(Debug, Clone, Default, Serialize, Deserialize)]
358pub struct BatchStatistics {
359 pub total_queries: usize,
361 pub executed_queries: usize,
363 pub deduplicated_queries: usize,
365 pub cached_queries: usize,
367 pub failed_queries: usize,
369 pub avg_query_duration: Duration,
371 pub max_query_duration: Duration,
373 pub min_query_duration: Duration,
375}
376
377#[derive(Debug)]
379struct QueryBatch {
380 id: String,
382 queries: Vec<BatchedQuery>,
384 #[allow(dead_code)]
386 created_at: Instant,
387 started: bool,
389 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
413pub struct QueryBatcher {
415 config: BatchConfig,
417 batches: Arc<RwLock<HashMap<String, QueryBatch>>>,
419 cache: Arc<RwLock<HashMap<String, QueryResult>>>,
421 statistics: Arc<RwLock<HashMap<String, BatchStatistics>>>,
423 executor: Option<Arc<dyn BatchQueryExecutor>>,
427}
428
429impl QueryBatcher {
430 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 pub fn with_executor(mut self, executor: Arc<dyn BatchQueryExecutor>) -> Self {
447 self.executor = Some(executor);
448 self
449 }
450
451 pub fn set_executor(&mut self, executor: Arc<dyn BatchQueryExecutor>) {
453 self.executor = Some(executor);
454 }
455
456 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 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 pub async fn execute_batch(&self, batch_id: &str) -> Result<BatchResult, String> {
488 let start_time = Instant::now();
489
490 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 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 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 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 let statistics = self.calculate_statistics(&results, dedup_map.len());
535
536 {
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 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 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 dedup_map.insert(query.id.clone(), canonical_id.clone());
574 } else {
575 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 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 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 pub fn analyze_batch_dependencies(queries: &[BatchedQuery]) -> HashMap<String, Vec<String>> {
615 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 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 let mut query_map: HashMap<String, BatchedQuery> =
647 queries.into_iter().map(|q| (q.id.clone(), q)).collect();
648
649 let mut completed: std::collections::HashSet<String> = std::collections::HashSet::new();
651 let mut all_results: Vec<QueryResult> = Vec::new();
652
653 let mut queue: VecDeque<String> = VecDeque::new();
655
656 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 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 let wave_results = self.execute_parallel(wave.clone()).await?;
678
679 for q in &wave {
681 completed.insert(q.id.clone());
682 }
683 all_results.extend(wave_results);
684
685 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 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 async fn execute_priority_based(
708 &self,
709 mut queries: Vec<BatchedQuery>,
710 ) -> Result<Vec<QueryResult>, String> {
711 queries.sort_by_key(|q| Reverse(q.priority));
713 self.execute_parallel(queries).await
714 }
715
716 async fn execute_single_query(&self, query: BatchedQuery) -> Result<QueryResult, String> {
718 let start_time = Instant::now();
719
720 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 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 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 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 pub async fn get_statistics(&self, batch_id: &str) -> Option<BatchStatistics> {
797 self.statistics.read().await.get(batch_id).cloned()
798 }
799
800 pub async fn clear_cache(&self) {
802 self.cache.write().await.clear();
803 }
804
805 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 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 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 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 assert!(data.get("placeholder").is_none());
865 assert_eq!(data["echo"], serde_json::json!("{ real }"));
866 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()); 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); assert_eq!(result.queries_deduplicated, 1);
1015 assert_eq!(result.results.len(), 2); }
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 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 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}