1use std::collections::HashMap;
46use std::hash::Hash;
47use std::sync::RwLock;
48
49#[derive(Debug, Clone)]
55pub struct GraphEdge {
56 pub parent_field: String,
58 pub relation: String,
60 pub sub_graph: Option<Box<EntityGraph>>,
62}
63
64#[derive(Debug, Clone, Default)]
78pub struct EntityGraph {
79 edges: Vec<GraphEdge>,
81}
82
83impl EntityGraph {
84 pub fn new() -> Self {
86 Self { edges: Vec::new() }
87 }
88
89 pub fn add_edge(
91 &mut self,
92 parent_field: impl Into<String>,
93 relation: impl Into<String>,
94 ) -> &mut Self {
95 self.edges.push(GraphEdge {
96 parent_field: parent_field.into(),
97 relation: relation.into(),
98 sub_graph: None,
99 });
100 self
101 }
102
103 pub fn add_edge_with_graph(
105 &mut self,
106 parent_field: impl Into<String>,
107 relation: impl Into<String>,
108 sub_graph: EntityGraph,
109 ) -> &mut Self {
110 self.edges.push(GraphEdge {
111 parent_field: parent_field.into(),
112 relation: relation.into(),
113 sub_graph: Some(Box::new(sub_graph)),
114 });
115 self
116 }
117
118 pub fn edges(&self) -> &[GraphEdge] {
120 &self.edges
121 }
122
123 pub fn edge_count(&self) -> usize {
125 self.edges.len()
126 }
127
128 pub fn relations_of(&self, parent_field: &str) -> Vec<&GraphEdge> {
130 self.edges
131 .iter()
132 .filter(|e| e.parent_field == parent_field)
133 .collect()
134 }
135
136 pub fn all_relations(&self) -> Vec<String> {
138 let mut rels: Vec<String> = self.edges.iter().map(|e| e.relation.clone()).collect();
139 rels.sort();
140 rels.dedup();
141 rels
142 }
143
144 pub fn all_parent_fields(&self) -> Vec<String> {
146 let mut fields: Vec<String> = self.edges.iter().map(|e| e.parent_field.clone()).collect();
147 fields.sort();
148 fields.dedup();
149 fields
150 }
151
152 pub fn is_empty(&self) -> bool {
154 self.edges.is_empty()
155 }
156
157 pub fn all_relations_recursive(&self) -> Vec<String> {
159 let mut result = Vec::new();
160 for edge in &self.edges {
161 result.push(edge.relation.clone());
162 if let Some(sub) = &edge.sub_graph {
163 result.extend(sub.all_relations_recursive());
164 }
165 }
166 result.sort();
167 result.dedup();
168 result
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
178pub enum BatchStrategy {
179 #[default]
183 In,
184 Join,
188 Subquery,
192}
193
194impl BatchStrategy {
195 pub fn name(&self) -> &'static str {
197 match self {
198 BatchStrategy::In => "in",
199 BatchStrategy::Join => "join",
200 BatchStrategy::Subquery => "subquery",
201 }
202 }
203
204 pub fn render_in_clause(column: &str, placeholders: usize) -> String {
208 if placeholders == 0 {
209 return format!("{} IN ()", column);
210 }
211 let marks: Vec<&str> = vec!["?"; placeholders];
212 format!("{} IN ({})", column, marks.join(", "))
213 }
214}
215
216#[derive(Debug, Clone, Copy)]
224pub struct BatchSizeConfig {
225 pub size: usize,
227 pub strategy: BatchStrategy,
229}
230
231impl Default for BatchSizeConfig {
232 fn default() -> Self {
233 Self {
234 size: 100,
235 strategy: BatchStrategy::In,
236 }
237 }
238}
239
240impl BatchSizeConfig {
241 pub fn new(size: usize, strategy: BatchStrategy) -> Self {
243 Self { size, strategy }
244 }
245
246 pub fn with_size(size: usize) -> Self {
248 Self {
249 size,
250 strategy: BatchStrategy::In,
251 }
252 }
253
254 pub fn batch_count(&self, total: usize) -> usize {
269 if total == 0 {
270 0
271 } else {
272 total.div_ceil(self.size)
273 }
274 }
275
276 pub fn batch_range(&self, batch_index: usize, total: usize) -> std::ops::Range<usize> {
289 let start = batch_index * self.size;
290 let end = (start + self.size).min(total);
291 start..end
292 }
293}
294
295pub type BatchLoaderFn<K, V> = Box<dyn Fn(&[K]) -> HashMap<K, V> + Send + Sync>;
301
302pub struct BatchLoader<K, V>
326where
327 K: Hash + Eq + Clone + Send + Sync,
328 V: Clone + Send + Sync,
329{
330 batch_size: usize,
332 loader: BatchLoaderFn<K, V>,
334 cache: RwLock<HashMap<K, V>>,
336}
337
338impl<K, V> BatchLoader<K, V>
339where
340 K: Hash + Eq + Clone + Send + Sync,
341 V: Clone + Send + Sync,
342{
343 pub fn new(batch_size: usize, loader: BatchLoaderFn<K, V>) -> Self {
349 Self {
350 batch_size,
351 loader,
352 cache: RwLock::new(HashMap::new()),
353 }
354 }
355
356 pub fn load_many(&self, keys: &[K]) -> HashMap<K, V> {
362 let mut result: HashMap<K, V> = HashMap::new();
363
364 let cached = self.cache.read().unwrap();
366 let mut to_load: Vec<K> = Vec::new();
367 for k in keys {
368 if let Some(v) = cached.get(k) {
369 result.insert(k.clone(), v.clone());
370 } else {
371 to_load.push(k.clone());
372 }
373 }
374 drop(cached);
375
376 if to_load.is_empty() {
377 return result;
378 }
379
380 let batch_size = self.batch_size.max(1);
382 let mut all_loaded: HashMap<K, V> = HashMap::new();
383 for chunk in to_load.chunks(batch_size) {
384 let loaded = (self.loader)(chunk);
385 all_loaded.extend(loaded);
386 }
387
388 let mut cache = self.cache.write().unwrap();
390 for (k, v) in &all_loaded {
391 cache.insert(k.clone(), v.clone());
392 }
393 drop(cache);
394
395 result.extend(all_loaded);
397 result
398 }
399
400 pub fn load_one(&self, key: &K) -> Option<V> {
402 let result = self.load_many(std::slice::from_ref(key));
403 result.get(key).cloned()
404 }
405
406 pub fn clear_cache(&self) {
408 self.cache.write().unwrap().clear();
409 }
410
411 pub fn cache_size(&self) -> usize {
413 self.cache.read().unwrap().len()
414 }
415
416 pub fn batch_size(&self) -> usize {
418 self.batch_size
419 }
420}
421
422#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
433 fn test_new_graph_is_empty() {
434 let g = EntityGraph::new();
435 assert!(g.is_empty());
436 assert_eq!(g.edge_count(), 0);
437 }
438
439 #[test]
440 fn test_add_edge() {
441 let mut g = EntityGraph::new();
442 g.add_edge("user", "posts");
443 assert_eq!(g.edge_count(), 1);
444 assert!(!g.is_empty());
445 }
446
447 #[test]
448 fn test_add_multiple_edges() {
449 let mut g = EntityGraph::new();
450 g.add_edge("user", "posts")
451 .add_edge("user", "profile")
452 .add_edge("user", "comments");
453 assert_eq!(g.edge_count(), 3);
454 }
455
456 #[test]
457 fn test_add_edge_with_sub_graph() {
458 let mut sub = EntityGraph::new();
459 sub.add_edge("comments", "author");
460
461 let mut g = EntityGraph::new();
462 g.add_edge_with_graph("user", "posts", sub);
463
464 assert_eq!(g.edge_count(), 1);
465 assert!(g.edges()[0].sub_graph.is_some());
466 assert_eq!(g.edges()[0].sub_graph.as_ref().unwrap().edge_count(), 1);
467 }
468
469 #[test]
470 fn test_relations_of() {
471 let mut g = EntityGraph::new();
472 g.add_edge("user", "posts")
473 .add_edge("user", "profile")
474 .add_edge("post", "comments");
475
476 let user_relations = g.relations_of("user");
477 assert_eq!(user_relations.len(), 2);
478 assert_eq!(user_relations[0].relation, "posts");
479 assert_eq!(user_relations[1].relation, "profile");
480
481 let post_relations = g.relations_of("post");
482 assert_eq!(post_relations.len(), 1);
483
484 let none = g.relations_of("nonexistent");
485 assert!(none.is_empty());
486 }
487
488 #[test]
489 fn test_all_relations() {
490 let mut g = EntityGraph::new();
491 g.add_edge("user", "posts")
492 .add_edge("user", "profile")
493 .add_edge("post", "comments");
494
495 let rels = g.all_relations();
496 assert_eq!(rels, vec!["comments", "posts", "profile"]);
497 }
498
499 #[test]
500 fn test_all_parent_fields() {
501 let mut g = EntityGraph::new();
502 g.add_edge("user", "posts")
503 .add_edge("user", "profile")
504 .add_edge("post", "comments");
505
506 let fields = g.all_parent_fields();
507 assert_eq!(fields, vec!["post", "user"]);
508 }
509
510 #[test]
511 fn test_all_relations_recursive() {
512 let mut sub = EntityGraph::new();
513 sub.add_edge("comments", "author")
514 .add_edge("comments", "likes");
515
516 let mut g = EntityGraph::new();
517 g.add_edge("user", "posts")
518 .add_edge_with_graph("user", "comments", sub);
519
520 let all = g.all_relations_recursive();
521 assert!(all.contains(&"posts".to_string()));
522 assert!(all.contains(&"comments".to_string()));
523 assert!(all.contains(&"author".to_string()));
524 assert!(all.contains(&"likes".to_string()));
525 assert_eq!(all.len(), 4);
526 }
527
528 #[test]
529 fn test_default_graph_is_empty() {
530 let g = EntityGraph::default();
531 assert!(g.is_empty());
532 }
533
534 #[test]
537 fn test_strategy_name() {
538 assert_eq!(BatchStrategy::In.name(), "in");
539 assert_eq!(BatchStrategy::Join.name(), "join");
540 assert_eq!(BatchStrategy::Subquery.name(), "subquery");
541 }
542
543 #[test]
544 fn test_strategy_default_is_in() {
545 assert_eq!(BatchStrategy::default(), BatchStrategy::In);
546 }
547
548 #[test]
549 fn test_render_in_clause_empty() {
550 let sql = BatchStrategy::render_in_clause("id", 0);
551 assert_eq!(sql, "id IN ()");
552 }
553
554 #[test]
555 fn test_render_in_clause_single() {
556 let sql = BatchStrategy::render_in_clause("id", 1);
557 assert_eq!(sql, "id IN (?)");
558 }
559
560 #[test]
561 fn test_render_in_clause_multiple() {
562 let sql = BatchStrategy::render_in_clause("user_id", 3);
563 assert_eq!(sql, "user_id IN (?, ?, ?)");
564 }
565
566 #[test]
569 fn test_default_config() {
570 let config = BatchSizeConfig::default();
571 assert_eq!(config.size, 100);
572 assert_eq!(config.strategy, BatchStrategy::In);
573 }
574
575 #[test]
576 fn test_with_size() {
577 let config = BatchSizeConfig::with_size(50);
578 assert_eq!(config.size, 50);
579 assert_eq!(config.strategy, BatchStrategy::In);
580 }
581
582 #[test]
583 fn test_new_with_strategy() {
584 let config = BatchSizeConfig::new(200, BatchStrategy::Join);
585 assert_eq!(config.size, 200);
586 assert_eq!(config.strategy, BatchStrategy::Join);
587 }
588
589 #[test]
590 fn test_batch_count_zero() {
591 let config = BatchSizeConfig::with_size(100);
592 assert_eq!(config.batch_count(0), 0);
593 }
594
595 #[test]
596 fn test_batch_count_exact_multiple() {
597 let config = BatchSizeConfig::with_size(100);
598 assert_eq!(config.batch_count(100), 1);
599 assert_eq!(config.batch_count(200), 2);
600 assert_eq!(config.batch_count(500), 5);
601 }
602
603 #[test]
604 fn test_batch_count_with_remainder() {
605 let config = BatchSizeConfig::with_size(100);
606 assert_eq!(config.batch_count(1), 1);
607 assert_eq!(config.batch_count(99), 1);
608 assert_eq!(config.batch_count(101), 2);
609 assert_eq!(config.batch_count(150), 2);
610 assert_eq!(config.batch_count(201), 3);
611 }
612
613 #[test]
614 fn test_batch_range() {
615 let config = BatchSizeConfig::with_size(100);
616
617 assert_eq!(config.batch_range(0, 250), 0..100);
618 assert_eq!(config.batch_range(1, 250), 100..200);
619 assert_eq!(config.batch_range(2, 250), 200..250);
620 }
621
622 #[test]
623 fn test_batch_range_exact() {
624 let config = BatchSizeConfig::with_size(100);
625
626 assert_eq!(config.batch_range(0, 100), 0..100);
627 assert_eq!(config.batch_range(1, 100), 100..100); }
629
630 #[test]
631 fn test_batch_range_small_batch() {
632 let config = BatchSizeConfig::with_size(10);
633
634 assert_eq!(config.batch_range(0, 25), 0..10);
635 assert_eq!(config.batch_range(1, 25), 10..20);
636 assert_eq!(config.batch_range(2, 25), 20..25);
637 }
638
639 fn make_loader() -> BatchLoader<i64, String> {
642 let loader = Box::new(|ids: &[i64]| -> HashMap<i64, String> {
643 ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
644 });
645 BatchLoader::new(2, loader)
646 }
647
648 #[test]
649 fn test_batch_loader_load_many_single_batch() {
650 let loader = make_loader();
651 let result = loader.load_many(&[1, 2]);
652 assert_eq!(result.len(), 2);
653 assert_eq!(result.get(&1), Some(&"user_1".to_string()));
654 assert_eq!(result.get(&2), Some(&"user_2".to_string()));
655 }
656
657 #[test]
658 fn test_batch_loader_load_many_multiple_batches() {
659 let loader = make_loader();
660 let result = loader.load_many(&[1, 2, 3, 4, 5]);
662 assert_eq!(result.len(), 5);
663 for id in 1..=5 {
664 assert_eq!(
665 result.get(&id),
666 Some(&format!("user_{}", id)),
667 "missing user {}",
668 id
669 );
670 }
671 }
672
673 #[test]
674 fn test_batch_loader_load_one() {
675 let loader = make_loader();
676 let result = loader.load_one(&42);
677 assert_eq!(result, Some("user_42".to_string()));
678 }
679
680 #[test]
681 fn test_batch_loader_load_one_missing() {
682 let loader: BatchLoader<i64, String> =
684 BatchLoader::new(10, Box::new(|_ids: &[i64]| HashMap::new()));
685 let result = loader.load_one(&100);
686 assert_eq!(result, None);
687 }
688
689 #[test]
690 fn test_batch_loader_caches_results() {
691 let call_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
692 let call_count_clone = call_count.clone();
693
694 let loader = Box::new(move |ids: &[i64]| -> HashMap<i64, String> {
695 *call_count_clone.lock().unwrap() += 1;
696 ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
697 });
698
699 let batch_loader = BatchLoader::new(100, loader);
700
701 batch_loader.load_many(&[1, 2, 3]);
703 assert_eq!(*call_count.lock().unwrap(), 1);
704
705 batch_loader.load_many(&[1, 2, 3]);
707 assert_eq!(*call_count.lock().unwrap(), 1); batch_loader.load_many(&[4, 5]);
711 assert_eq!(*call_count.lock().unwrap(), 2);
712 }
713
714 #[test]
715 fn test_batch_loader_partial_cache_hit() {
716 let call_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
717 let call_count_clone = call_count.clone();
718
719 let loader = Box::new(move |ids: &[i64]| -> HashMap<i64, String> {
720 *call_count_clone.lock().unwrap() += 1;
721 ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
722 });
723
724 let batch_loader = BatchLoader::new(100, loader);
725
726 batch_loader.load_many(&[1, 2, 3]);
728 assert_eq!(*call_count.lock().unwrap(), 1);
729
730 let result = batch_loader.load_many(&[1, 2, 3, 4, 5]);
732 assert_eq!(result.len(), 5);
733 assert_eq!(*call_count.lock().unwrap(), 2); assert_eq!(batch_loader.cache_size(), 5);
737 }
738
739 #[test]
740 fn test_batch_loader_clear_cache() {
741 let loader = make_loader();
742 loader.load_many(&[1, 2]);
743 assert_eq!(loader.cache_size(), 2);
744
745 loader.clear_cache();
746 assert_eq!(loader.cache_size(), 0);
747 }
748
749 #[test]
750 fn test_batch_loader_empty_input() {
751 let loader = make_loader();
752 let result = loader.load_many(&[]);
753 assert!(result.is_empty());
754 }
755
756 #[test]
757 fn test_batch_loader_batch_size_attribute() {
758 let loader = make_loader();
759 assert_eq!(loader.batch_size(), 2);
760 }
761
762 #[test]
763 fn test_batch_loader_with_size_1() {
764 let loader = BatchLoader::new(
765 1,
766 Box::new(|ids: &[i64]| ids.iter().map(|id| (*id, *id * 10)).collect()),
767 );
768 let result = loader.load_many(&[1, 2, 3]);
769 assert_eq!(result.len(), 3);
770 assert_eq!(result.get(&1), Some(&10));
771 assert_eq!(result.get(&2), Some(&20));
772 assert_eq!(result.get(&3), Some(&30));
773 }
774
775 #[test]
778 fn test_workflow_graph_and_batch_loader() {
779 let mut graph = EntityGraph::new();
781 graph.add_edge_with_graph("user", "posts", {
782 let mut sub = EntityGraph::new();
783 sub.add_edge("posts", "comments");
784 sub
785 });
786 assert_eq!(graph.all_relations_recursive().len(), 2);
787
788 let user_loader = BatchLoader::new(
790 50,
791 Box::new(|ids: &[i64]| ids.iter().map(|id| (*id, format!("User#{}", id))).collect()),
792 );
793
794 let user_ids: Vec<i64> = (1..=123).collect();
796 let users = user_loader.load_many(&user_ids);
797 assert_eq!(users.len(), 123);
798 assert_eq!(user_loader.cache_size(), 123);
799 }
800
801 #[test]
802 fn test_n_plus_1_problem_solved() {
803 let query_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
808 let query_count_clone = query_count.clone();
809
810 let post_loader = BatchLoader::new(
811 100,
812 Box::new(move |user_ids: &[i64]| {
813 *query_count_clone.lock().unwrap() += 1;
814 user_ids
816 .iter()
817 .map(|uid| (*uid, format!("posts_for_user_{}", uid)))
818 .collect()
819 }),
820 );
821
822 let user_ids: Vec<i64> = (1..=250).collect();
824 let _posts = post_loader.load_many(&user_ids);
825
826 assert_eq!(*query_count.lock().unwrap(), 3);
828 }
829}