1use crate::algebra::{Binding, Solution, Term, Variable};
7use anyhow::{anyhow, Result};
8use oxirs_core::model::NamedNode;
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, VecDeque};
11use std::fs::{remove_file, File};
12use std::io::{BufReader, BufWriter, Read, Write};
13use std::path::PathBuf;
14use std::sync::{Arc, Mutex};
15use tempfile::NamedTempFile;
16
17#[derive(Debug, Clone)]
19pub struct StreamingConfig {
20 pub memory_limit: usize,
22 pub temp_dir: Option<PathBuf>,
24 pub buffer_size: usize,
26 pub compress_spills: bool,
28 pub spill_strategy: SpillStrategy,
30 pub adaptive_buffering: bool,
32 pub parallel_spilling: bool,
34 pub compression_algorithm: CompressionAlgorithm,
36}
37
38#[derive(Debug, Clone)]
40pub enum SpillStrategy {
41 Fifo,
43 LargestFirst,
45 LeastRecentlyUsed,
47 Adaptive,
49}
50
51#[derive(Debug, Clone)]
53pub enum CompressionAlgorithm {
54 None,
56 Lz4,
58 Gzip,
60 Zstd,
62}
63
64impl Default for StreamingConfig {
65 fn default() -> Self {
66 Self {
67 memory_limit: 1024 * 1024 * 1024, temp_dir: None,
69 buffer_size: 10000,
70 compress_spills: true,
71 spill_strategy: SpillStrategy::Adaptive,
72 adaptive_buffering: true,
73 parallel_spilling: true,
74 compression_algorithm: CompressionAlgorithm::Zstd,
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct MemoryTracker {
82 current_usage: Arc<Mutex<usize>>,
83 peak_usage: Arc<Mutex<usize>>,
84 limit: usize,
85 allocation_history: Arc<Mutex<Vec<AllocationEvent>>>,
86 pressure_threshold: f64,
87 prediction_window: usize,
88}
89
90#[derive(Debug, Clone)]
92struct AllocationEvent {
93 timestamp: std::time::Instant,
94 size: usize,
95 operation: AllocationType,
96}
97
98#[derive(Debug, Clone)]
99enum AllocationType {
100 Allocate,
101 Deallocate,
102}
103
104impl MemoryTracker {
105 pub fn new(limit: usize) -> Self {
106 Self {
107 current_usage: Arc::new(Mutex::new(0)),
108 peak_usage: Arc::new(Mutex::new(0)),
109 limit,
110 allocation_history: Arc::new(Mutex::new(Vec::new())),
111 pressure_threshold: 0.8, prediction_window: 100, }
114 }
115
116 pub fn with_pressure_threshold(limit: usize, threshold: f64) -> Self {
118 Self {
119 current_usage: Arc::new(Mutex::new(0)),
120 peak_usage: Arc::new(Mutex::new(0)),
121 limit,
122 allocation_history: Arc::new(Mutex::new(Vec::new())),
123 pressure_threshold: threshold,
124 prediction_window: 100,
125 }
126 }
127
128 pub fn allocate(&self, size: usize) -> Result<bool> {
129 let mut current = self.current_usage.lock().expect("lock poisoned");
130 let new_usage = *current + size;
131
132 self.record_allocation_event(size, AllocationType::Allocate);
134
135 if new_usage > self.limit {
136 return Ok(false); }
138
139 *current = new_usage;
140
141 let mut peak = self.peak_usage.lock().expect("lock poisoned");
142 if new_usage > *peak {
143 *peak = new_usage;
144 }
145
146 Ok(true)
147 }
148
149 pub fn deallocate(&self, size: usize) {
150 let mut current = self.current_usage.lock().expect("lock poisoned");
151 *current = current.saturating_sub(size);
152
153 self.record_allocation_event(size, AllocationType::Deallocate);
155 }
156
157 fn record_allocation_event(&self, size: usize, operation: AllocationType) {
159 let mut history = self.allocation_history.lock().expect("lock poisoned");
160
161 history.push(AllocationEvent {
162 timestamp: std::time::Instant::now(),
163 size,
164 operation,
165 });
166
167 let history_len = history.len();
169 if history_len > self.prediction_window {
170 history.drain(0..history_len - self.prediction_window);
171 }
172 }
173
174 pub fn current_usage(&self) -> usize {
175 *self.current_usage.lock().expect("lock poisoned")
176 }
177
178 pub fn peak_usage(&self) -> usize {
179 *self.peak_usage.lock().expect("lock poisoned")
180 }
181
182 pub fn should_spill(&self) -> bool {
183 let usage_ratio = self.current_usage() as f64 / self.limit as f64;
184 usage_ratio > self.pressure_threshold
185 }
186
187 pub fn should_spill_adaptive(&self) -> bool {
189 let current_ratio = self.current_usage() as f64 / self.limit as f64;
190
191 if current_ratio > self.pressure_threshold {
193 return true;
194 }
195
196 let allocation_velocity = self.calculate_allocation_velocity();
198 let predicted_usage = self.predict_memory_usage(allocation_velocity);
199
200 if predicted_usage > self.limit as f64 * 0.9 {
202 return true;
203 }
204
205 false
206 }
207
208 fn calculate_allocation_velocity(&self) -> f64 {
210 let history = self.allocation_history.lock().expect("lock poisoned");
211
212 if history.len() < 2 {
213 return 0.0;
214 }
215
216 let now = std::time::Instant::now();
217 let window_duration = std::time::Duration::from_secs(5); let mut net_allocation = 0i64;
220 let mut oldest_timestamp = now;
221
222 for event in history.iter().rev() {
223 if now.duration_since(event.timestamp) > window_duration {
224 break;
225 }
226
227 match event.operation {
228 AllocationType::Allocate => net_allocation += event.size as i64,
229 AllocationType::Deallocate => net_allocation -= event.size as i64,
230 }
231
232 oldest_timestamp = event.timestamp;
233 }
234
235 let elapsed = now.duration_since(oldest_timestamp).as_secs_f64();
236 if elapsed > 0.0 {
237 net_allocation as f64 / elapsed
238 } else {
239 0.0
240 }
241 }
242
243 fn predict_memory_usage(&self, velocity: f64) -> f64 {
245 let current = self.current_usage() as f64;
246 let prediction_horizon = 2.0; current + (velocity * prediction_horizon).max(0.0)
249 }
250
251 pub fn get_detailed_stats(&self) -> MemoryStats {
253 let history = self.allocation_history.lock().expect("lock poisoned");
254
255 let total_allocations = history
256 .iter()
257 .filter(|e| matches!(e.operation, AllocationType::Allocate))
258 .count();
259
260 let total_deallocations = history
261 .iter()
262 .filter(|e| matches!(e.operation, AllocationType::Deallocate))
263 .count();
264
265 let avg_allocation_size = history
266 .iter()
267 .filter(|e| matches!(e.operation, AllocationType::Allocate))
268 .map(|e| e.size)
269 .sum::<usize>()
270 .checked_div(total_allocations)
271 .unwrap_or(0);
272
273 MemoryStats {
274 current_usage: self.current_usage(),
275 peak_usage: self.peak_usage(),
276 total_allocations,
277 total_deallocations,
278 avg_allocation_size,
279 allocation_velocity: self.calculate_allocation_velocity(),
280 pressure_ratio: self.current_usage() as f64 / self.limit as f64,
281 }
282 }
283
284 pub fn adjust_pressure_threshold(&mut self, workload_intensity: f64) {
286 self.pressure_threshold = (0.6 + (0.3 * (1.0 - workload_intensity))).clamp(0.5, 0.9);
288 }
289}
290
291#[derive(Debug, Clone)]
293pub struct MemoryStats {
294 pub current_usage: usize,
295 pub peak_usage: usize,
296 pub total_allocations: usize,
297 pub total_deallocations: usize,
298 pub avg_allocation_size: usize,
299 pub allocation_velocity: f64,
300 pub pressure_ratio: f64,
301}
302
303pub struct StreamingSolution {
305 solutions: VecDeque<Solution>,
306 spill_files: Vec<SpillFile>,
307 current_spill_idx: usize,
308 memory_tracker: MemoryTracker,
309 config: StreamingConfig,
310 finished: bool,
311}
312
313#[derive(Debug)]
315struct SpillFile {
316 path: PathBuf,
317 size: usize,
318 compressed: bool,
319}
320
321impl StreamingSolution {
322 pub fn new(config: StreamingConfig) -> Self {
323 let memory_tracker = MemoryTracker::new(config.memory_limit);
324
325 Self {
326 solutions: VecDeque::new(),
327 spill_files: Vec::new(),
328 current_spill_idx: 0,
329 memory_tracker,
330 config,
331 finished: false,
332 }
333 }
334
335 pub fn add_solution(&mut self, solution: Solution) -> Result<()> {
337 let solution_size = self.estimate_solution_size(&solution);
338
339 if !self.memory_tracker.allocate(solution_size)? {
340 self.spill_to_disk()?;
342 if !self.memory_tracker.allocate(solution_size)? {
344 return Err(anyhow!("Cannot allocate memory even after spilling"));
345 }
346 }
347
348 self.solutions.push_back(solution);
349
350 let should_spill = if self.config.adaptive_buffering {
352 self.memory_tracker.should_spill_adaptive()
353 } else {
354 self.memory_tracker.should_spill()
355 };
356
357 if self.solutions.len() >= self.config.buffer_size || should_spill {
358 self.spill_to_disk()?;
359 }
360
361 Ok(())
362 }
363
364 fn estimate_solution_size(&self, solution: &Solution) -> usize {
366 let mut size = std::mem::size_of::<Solution>();
367 for binding in solution {
368 size += binding
369 .iter()
370 .map(|(var, term)| var.as_str().len() + self.estimate_term_size(term))
371 .sum::<usize>();
372 }
373 size
374 }
375
376 #[allow(clippy::only_used_in_recursion)]
378 fn estimate_term_size(&self, term: &Term) -> usize {
379 match term {
380 Term::Iri(iri) => iri.as_str().len(),
381 Term::Literal(lit) => lit.value.len() + lit.language.as_ref().map_or(0, |l| l.len()),
382 Term::BlankNode(bn) => bn.len(),
383 Term::Variable(var) => var.as_str().len(),
384 Term::QuotedTriple(triple) => {
385 self.estimate_term_size(&triple.subject)
387 + self.estimate_term_size(&triple.predicate)
388 + self.estimate_term_size(&triple.object)
389 + 6 }
391 Term::PropertyPath(path) => {
392 match path.complexity() {
394 c if c < 10 => 20,
395 c if c < 100 => 50,
396 _ => 100,
397 }
398 }
399 }
400 }
401
402 fn spill_to_disk(&mut self) -> Result<()> {
404 if self.solutions.is_empty() {
405 return Ok(());
406 }
407
408 let temp_file = if let Some(ref temp_dir) = self.config.temp_dir {
409 NamedTempFile::new_in(temp_dir)?
410 } else {
411 NamedTempFile::new()?
412 };
413
414 let serialized_solutions: Vec<SerializableSolution> = self
416 .solutions
417 .iter()
418 .map(SerializableSolution::from_solution)
419 .collect();
420
421 let data = if self.config.compress_spills {
422 self.compress_data(&serialized_solutions)?
423 } else {
424 oxicode::serde::encode_to_vec(&serialized_solutions, oxicode::config::standard())?
425 };
426
427 {
428 let mut writer = BufWriter::new(&temp_file);
429 writer.write_all(&data)?;
430 writer.flush()?;
431 }
432
433 let original_path = temp_file.path().to_path_buf();
435 let new_path = original_path.with_extension("spill");
436 temp_file.persist(&new_path)?;
437 let path = new_path;
438
439 let spill_file = SpillFile {
441 path,
442 size: data.len(),
443 compressed: self.config.compress_spills,
444 };
445 self.spill_files.push(spill_file);
446
447 let total_size: usize = self
449 .solutions
450 .iter()
451 .map(|sol| self.estimate_solution_size(sol))
452 .sum();
453 self.memory_tracker.deallocate(total_size);
454 self.solutions.clear();
455
456 Ok(())
457 }
458
459 fn compress_data(&self, data: &[SerializableSolution]) -> Result<Vec<u8>> {
461 let serialized = oxicode::serde::encode_to_vec(&data, oxicode::config::standard())?;
462
463 match self.config.compression_algorithm {
464 CompressionAlgorithm::None => Ok(serialized),
465 CompressionAlgorithm::Lz4 => {
466 oxiarc_lz4::compress(&serialized)
468 .map_err(|e| anyhow!("LZ4 compression failed: {}", e))
469 }
470 CompressionAlgorithm::Gzip => {
471 Ok(oxiarc_deflate::gzip_compress(&serialized, 6)?)
474 }
475 CompressionAlgorithm::Zstd => {
476 oxiarc_zstd::encode_all(&serialized, 3)
478 .map_err(|e| anyhow!("Zstd compression failed: {}", e))
479 }
480 }
481 }
482
483 fn decompress_data(&self, compressed: &[u8]) -> Result<Vec<SerializableSolution>> {
485 let decompressed = match self.config.compression_algorithm {
486 CompressionAlgorithm::None => {
487 compressed.to_vec()
489 }
490 CompressionAlgorithm::Lz4 => {
491 oxiarc_lz4::decompress(compressed, 100 * 1024 * 1024)
493 .map_err(|e| anyhow!("LZ4 decompression failed: {}", e))?
494 }
495 CompressionAlgorithm::Gzip => {
496 oxiarc_deflate::gzip_decompress(compressed)?
498 }
499 CompressionAlgorithm::Zstd => {
500 oxiarc_zstd::decode_all(compressed)
502 .map_err(|e| anyhow!("Zstd decompression failed: {}", e))?
503 }
504 };
505
506 Ok(
507 oxicode::serde::decode_from_slice(&decompressed, oxicode::config::standard())
508 .map(|(v, _)| v)?,
509 )
510 }
511
512 fn load_from_spill(&mut self) -> Result<bool> {
514 if self.current_spill_idx >= self.spill_files.len() {
515 return Ok(false);
516 }
517
518 let spill_file = &self.spill_files[self.current_spill_idx];
519 let file = File::open(&spill_file.path)?;
520 let mut reader = BufReader::new(file);
521
522 let mut data = Vec::new();
523 reader.read_to_end(&mut data)?;
524
525 let serialized_solutions = if spill_file.compressed {
526 self.decompress_data(&data)?
527 } else {
528 oxicode::serde::decode_from_slice(&data, oxicode::config::standard()).map(|(v, _)| v)?
529 };
530
531 for serialized in serialized_solutions {
533 let solution = serialized.to_solution();
534 self.solutions.push_back(solution);
535 }
536
537 self.current_spill_idx += 1;
538 Ok(true)
539 }
540
541 pub fn finish(&mut self) {
543 self.finished = true;
544 }
545
546 pub fn get_stats(&self) -> StreamingStats {
548 StreamingStats {
549 current_memory: self.memory_tracker.current_usage(),
550 peak_memory: self.memory_tracker.peak_usage(),
551 spill_files: self.spill_files.len(),
552 total_spill_size: self.spill_files.iter().map(|f| f.size).sum(),
553 in_memory_solutions: self.solutions.len(),
554 }
555 }
556}
557
558impl Iterator for StreamingSolution {
559 type Item = Result<Solution>;
560
561 fn next(&mut self) -> Option<Self::Item> {
562 if let Some(solution) = self.solutions.pop_front() {
564 let size = self.estimate_solution_size(&solution);
566 self.memory_tracker.deallocate(size);
567 return Some(Ok(solution));
568 }
569
570 if self.current_spill_idx < self.spill_files.len() {
572 match self.load_from_spill() {
573 Ok(true) => {
574 if let Some(solution) = self.solutions.pop_front() {
576 let size = self.estimate_solution_size(&solution);
577 self.memory_tracker.deallocate(size);
578 return Some(Ok(solution));
579 }
580 }
581 Ok(false) => {
582 }
584 Err(e) => {
585 return Some(Err(e));
586 }
587 }
588 }
589
590 None
591 }
592}
593
594impl Drop for StreamingSolution {
595 fn drop(&mut self) {
596 for spill_file in &self.spill_files {
598 let _ = remove_file(&spill_file.path);
599 }
600 }
601}
602
603#[derive(Serialize, Deserialize)]
605struct SerializableSolution {
606 bindings: Vec<SerializableBinding>,
607}
608
609#[derive(Serialize, Deserialize)]
610struct SerializableBinding {
611 variable: String,
612 term: SerializableTerm,
613}
614
615#[derive(Serialize, Deserialize)]
616enum SerializableTerm {
617 Iri(String),
618 Literal {
619 value: String,
620 language: Option<String>,
621 datatype: Option<String>,
622 },
623 BlankNode(String),
624 Variable(String),
625}
626
627impl SerializableSolution {
628 fn from_solution(solution: &Solution) -> Self {
629 let mut bindings = Vec::new();
630 for binding in solution {
631 for (var, term) in binding {
632 bindings.push(SerializableBinding {
633 variable: var.as_str().to_string(),
634 term: SerializableTerm::from_term(term),
635 });
636 }
637 }
638 Self { bindings }
639 }
640
641 fn to_solution(&self) -> Solution {
642 let mut solution = Solution::new();
643 let mut current_binding = Binding::new();
644
645 for binding in &self.bindings {
646 current_binding.insert(
647 Variable::new(&binding.variable).expect("variable name should be valid"),
648 binding.term.to_term(),
649 );
650 }
651
652 if !current_binding.is_empty() {
653 solution.push(current_binding);
654 }
655
656 solution
657 }
658}
659
660impl SerializableTerm {
661 fn from_term(term: &Term) -> Self {
662 match term {
663 Term::Iri(iri) => Self::Iri(iri.as_str().to_string()),
664 Term::Literal(lit) => Self::Literal {
665 value: lit.value.clone(),
666 language: lit.language.clone(),
667 datatype: lit.datatype.as_ref().map(|dt| dt.as_str().to_string()),
668 },
669 Term::BlankNode(bn) => Self::BlankNode(bn.clone()),
670 Term::Variable(var) => Self::Variable(var.as_str().to_string()),
671 Term::QuotedTriple(triple) => {
672 Self::Literal {
674 value: format!(
675 "<<{} {} {}>>",
676 triple.subject, triple.predicate, triple.object
677 ),
678 language: None,
679 datatype: Some("http://example.org/quoted-triple".to_string()),
680 }
681 }
682 Term::PropertyPath(path) => {
683 Self::Literal {
685 value: path.to_string(),
686 language: None,
687 datatype: Some("http://example.org/property-path".to_string()),
688 }
689 }
690 }
691 }
692
693 fn to_term(&self) -> Term {
694 match self {
695 Self::Iri(iri) => Term::Iri(NamedNode::new(iri).expect("IRI should be valid")),
696 Self::Literal {
697 value,
698 language,
699 datatype,
700 } => Term::Literal(crate::algebra::Literal {
701 value: value.clone(),
702 language: language.clone(),
703 datatype: datatype
704 .as_ref()
705 .map(|dt| NamedNode::new(dt).expect("datatype IRI should be valid")),
706 }),
707 Self::BlankNode(bn) => Term::BlankNode(bn.clone()),
708 Self::Variable(var) => {
709 Term::Variable(Variable::new(var).expect("variable name should be valid"))
710 }
711 }
712 }
713}
714
715pub struct SpillableHashJoin {
723 config: StreamingConfig,
724 memory_tracker: MemoryTracker,
725 hash_buckets: Vec<HashMap<String, Vec<Solution>>>,
726 spill_buckets: Vec<Vec<SpillFile>>,
727 right_buffers: Vec<Vec<Solution>>,
729 right_spill_buckets: Vec<Vec<SpillFile>>,
731 num_buckets: usize,
732}
733
734impl SpillableHashJoin {
735 pub fn new(config: StreamingConfig) -> Self {
736 let num_buckets = 16; let memory_tracker = MemoryTracker::new(config.memory_limit);
738
739 Self {
740 config,
741 memory_tracker,
742 hash_buckets: (0..num_buckets).map(|_| HashMap::new()).collect(),
743 spill_buckets: (0..num_buckets).map(|_| Vec::new()).collect(),
744 right_buffers: (0..num_buckets).map(|_| Vec::new()).collect(),
745 right_spill_buckets: (0..num_buckets).map(|_| Vec::new()).collect(),
746 num_buckets,
747 }
748 }
749
750 pub fn execute(
752 &mut self,
753 left: Vec<Solution>,
754 right: Vec<Solution>,
755 join_vars: &[Variable],
756 ) -> Result<Vec<Solution>> {
757 self.build_phase(left, join_vars)?;
759
760 let mut results = Vec::new();
762 self.probe_phase(right, join_vars, &mut results)?;
763
764 self.handle_spilled_buckets(join_vars, &mut results)?;
766
767 Ok(results)
768 }
769
770 fn build_phase(&mut self, left: Vec<Solution>, join_vars: &[Variable]) -> Result<()> {
772 for solution in left {
773 let hash_key = self.create_hash_key(&solution, join_vars);
774 let bucket_idx = self.hash_to_bucket(&hash_key);
775
776 let solution_size = self.estimate_solution_size(&solution);
777
778 if !self.memory_tracker.allocate(solution_size)? {
779 self.spill_bucket(bucket_idx)?;
781 if !self.memory_tracker.allocate(solution_size)? {
783 return Err(anyhow!("Cannot allocate memory even after spilling bucket"));
784 }
785 }
786
787 self.hash_buckets[bucket_idx]
788 .entry(hash_key)
789 .or_default()
790 .push(solution);
791 }
792
793 Ok(())
794 }
795
796 fn probe_phase(
806 &mut self,
807 right: Vec<Solution>,
808 join_vars: &[Variable],
809 results: &mut Vec<Solution>,
810 ) -> Result<()> {
811 for right_solution in right {
812 let hash_key = self.create_hash_key(&right_solution, join_vars);
813 let bucket_idx = self.hash_to_bucket(&hash_key);
814
815 if self.spill_buckets[bucket_idx].is_empty() {
816 if let Some(left_solutions) = self.hash_buckets[bucket_idx].get(&hash_key) {
818 for left_solution in left_solutions {
819 if let Some(joined) =
820 self.join_solutions(left_solution, &right_solution, join_vars)
821 {
822 results.push(joined);
823 }
824 }
825 }
826 } else {
827 self.right_buffers[bucket_idx].push(right_solution);
829 if self.right_buffers[bucket_idx].len() >= self.config.buffer_size.max(1) {
830 self.spill_right_buffer(bucket_idx)?;
831 }
832 }
833 }
834
835 Ok(())
836 }
837
838 fn handle_spilled_buckets(
846 &mut self,
847 join_vars: &[Variable],
848 results: &mut Vec<Solution>,
849 ) -> Result<()> {
850 for bucket_idx in 0..self.num_buckets {
851 if self.spill_buckets[bucket_idx].is_empty() {
852 continue;
854 }
855
856 let mut left_solutions = Vec::new();
858 for spill_file in &self.spill_buckets[bucket_idx] {
859 left_solutions.extend(self.load_spilled_solutions(spill_file)?);
860 }
861 for (_key, sols) in self.hash_buckets[bucket_idx].drain() {
862 left_solutions.extend(sols);
863 }
864
865 let mut right_solutions = std::mem::take(&mut self.right_buffers[bucket_idx]);
867 for spill_file in &self.right_spill_buckets[bucket_idx] {
868 right_solutions.extend(self.load_spilled_solutions(spill_file)?);
869 }
870
871 self.join_partition(&left_solutions, &right_solutions, join_vars, results);
872
873 for spill_file in self.spill_buckets[bucket_idx].drain(..) {
875 let _ = remove_file(&spill_file.path);
876 }
877 for spill_file in self.right_spill_buckets[bucket_idx].drain(..) {
878 let _ = remove_file(&spill_file.path);
879 }
880 }
881
882 Ok(())
883 }
884
885 fn join_partition(
889 &self,
890 left: &[Solution],
891 right: &[Solution],
892 join_vars: &[Variable],
893 results: &mut Vec<Solution>,
894 ) {
895 let mut table: HashMap<String, Vec<&Solution>> = HashMap::new();
896 for left_solution in left {
897 let key = self.create_hash_key(left_solution, join_vars);
898 table.entry(key).or_default().push(left_solution);
899 }
900
901 for right_solution in right {
902 let key = self.create_hash_key(right_solution, join_vars);
903 if let Some(left_matches) = table.get(&key) {
904 for &left_solution in left_matches {
905 if let Some(joined) =
906 self.join_solutions(left_solution, right_solution, join_vars)
907 {
908 results.push(joined);
909 }
910 }
911 }
912 }
913 }
914
915 fn create_hash_key(&self, solution: &Solution, join_vars: &[Variable]) -> String {
917 let mut key_parts = Vec::new();
918
919 for binding in solution {
920 for join_var in join_vars {
921 if let Some(term) = binding.get(join_var) {
922 key_parts.push(format!("{join_var}:{term:?}"));
923 }
924 }
925 }
926
927 key_parts.join("|")
928 }
929
930 fn hash_to_bucket(&self, key: &str) -> usize {
932 use std::collections::hash_map::DefaultHasher;
933 use std::hash::{Hash, Hasher};
934
935 let mut hasher = DefaultHasher::new();
936 key.hash(&mut hasher);
937 (hasher.finish() as usize) % self.num_buckets
938 }
939
940 fn write_solutions_to_spill(&self, solutions: &[Solution]) -> Result<SpillFile> {
947 let temp_file = if let Some(ref temp_dir) = self.config.temp_dir {
948 NamedTempFile::new_in(temp_dir)?
949 } else {
950 NamedTempFile::new()?
951 };
952
953 let serialized_solutions: Vec<SerializableSolution> = solutions
954 .iter()
955 .map(SerializableSolution::from_solution)
956 .collect();
957 let data =
958 oxicode::serde::encode_to_vec(&serialized_solutions, oxicode::config::standard())?;
959
960 let (file, path) = temp_file
961 .keep()
962 .map_err(|e| anyhow!("failed to persist spill file: {e}"))?;
963 let mut writer = BufWriter::new(file);
964 writer.write_all(&data)?;
965 writer.flush()?;
966
967 Ok(SpillFile {
968 path,
969 size: data.len(),
970 compressed: false,
971 })
972 }
973
974 fn spill_bucket(&mut self, bucket_idx: usize) -> Result<()> {
976 if self.hash_buckets[bucket_idx].is_empty() {
977 return Ok(());
978 }
979
980 let all_solutions: Vec<Solution> = self.hash_buckets[bucket_idx]
982 .values()
983 .flat_map(|solutions| solutions.iter().cloned())
984 .collect();
985
986 let total_size: usize = all_solutions
987 .iter()
988 .map(|sol| self.estimate_solution_size(sol))
989 .sum();
990
991 let spill_file = self.write_solutions_to_spill(&all_solutions)?;
992 self.spill_buckets[bucket_idx].push(spill_file);
993
994 self.memory_tracker.deallocate(total_size);
995 self.hash_buckets[bucket_idx].clear();
996
997 Ok(())
998 }
999
1000 fn spill_right_buffer(&mut self, bucket_idx: usize) -> Result<()> {
1002 if self.right_buffers[bucket_idx].is_empty() {
1003 return Ok(());
1004 }
1005 let solutions = std::mem::take(&mut self.right_buffers[bucket_idx]);
1006 let spill_file = self.write_solutions_to_spill(&solutions)?;
1007 self.right_spill_buckets[bucket_idx].push(spill_file);
1008 Ok(())
1009 }
1010
1011 fn load_spilled_solutions(&self, spill_file: &SpillFile) -> Result<Vec<Solution>> {
1013 let file = File::open(&spill_file.path)?;
1014 let mut reader = BufReader::new(file);
1015
1016 let mut data = Vec::new();
1017 reader.read_to_end(&mut data)?;
1018
1019 let serialized_solutions: Vec<SerializableSolution> =
1020 oxicode::serde::decode_from_slice(&data, oxicode::config::standard())
1021 .map(|(v, _)| v)?;
1022
1023 Ok(serialized_solutions
1024 .into_iter()
1025 .map(|s| s.to_solution())
1026 .collect())
1027 }
1028
1029 fn join_solutions(
1031 &self,
1032 left: &Solution,
1033 right: &Solution,
1034 join_vars: &[Variable],
1035 ) -> Option<Solution> {
1036 for left_binding in left {
1038 for right_binding in right {
1039 let mut compatible = true;
1040 for join_var in join_vars {
1041 let left_val = left_binding.get(join_var);
1042 let right_val = right_binding.get(join_var);
1043
1044 match (left_val, right_val) {
1045 (Some(l), Some(r)) if l != r => {
1046 compatible = false;
1047 break;
1048 }
1049 _ => {}
1050 }
1051 }
1052
1053 if compatible {
1054 let mut joined = Solution::new();
1056 let mut new_binding = Binding::new();
1057
1058 for (var, term) in left_binding {
1060 new_binding.insert(var.clone(), term.clone());
1061 }
1062
1063 for (var, term) in right_binding {
1065 if !new_binding.contains_key(var) {
1066 new_binding.insert(var.clone(), term.clone());
1067 }
1068 }
1069
1070 joined.push(new_binding);
1071 return Some(joined);
1072 }
1073 }
1074 }
1075
1076 None
1077 }
1078
1079 fn estimate_solution_size(&self, solution: &Solution) -> usize {
1081 let mut size = std::mem::size_of::<Solution>();
1082 for binding in solution {
1083 size += binding.len() * (std::mem::size_of::<Variable>() + std::mem::size_of::<Term>());
1084 size += binding
1085 .iter()
1086 .map(|(var, term)| var.as_str().len() + self.estimate_term_size(term))
1087 .sum::<usize>();
1088 }
1089 size
1090 }
1091
1092 fn estimate_term_size(&self, term: &Term) -> usize {
1094 match term {
1095 Term::Iri(iri) => iri.as_str().len(),
1096 Term::Literal(lit) => lit.value.len() + lit.language.as_ref().map_or(0, |l| l.len()),
1097 Term::BlankNode(bn) => bn.len(),
1098 Term::Variable(var) => var.as_str().len(),
1099 Term::QuotedTriple(_) => 100, Term::PropertyPath(_) => 50, }
1102 }
1103}
1104
1105#[derive(Debug, Clone)]
1107pub struct StreamingStats {
1108 pub current_memory: usize,
1109 pub peak_memory: usize,
1110 pub spill_files: usize,
1111 pub total_spill_size: usize,
1112 pub in_memory_solutions: usize,
1113}
1114
1115impl Default for SpillableHashJoin {
1116 fn default() -> Self {
1117 Self::new(StreamingConfig::default())
1118 }
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123 use super::*;
1124
1125 #[test]
1126 fn test_streaming_solution_basic() {
1127 let config = StreamingConfig {
1128 memory_limit: 1024, buffer_size: 2,
1130 ..Default::default()
1131 };
1132
1133 let mut stream = StreamingSolution::new(config);
1134
1135 let mut solution1 = Solution::new();
1137 let mut binding1 = Binding::new();
1138 binding1.insert(
1139 Variable::new("x").unwrap(),
1140 Term::Iri(NamedNode::new("http://example.org/1").unwrap()),
1141 );
1142 solution1.push(binding1);
1143
1144 let mut solution2 = Solution::new();
1145 let mut binding2 = Binding::new();
1146 binding2.insert(
1147 Variable::new("y").unwrap(),
1148 Term::Iri(NamedNode::new("http://example.org/2").unwrap()),
1149 );
1150 solution2.push(binding2);
1151
1152 stream.add_solution(solution1).unwrap();
1153 stream.add_solution(solution2).unwrap();
1154 stream.finish();
1155
1156 let mut count = 0;
1158 for result in &mut stream {
1159 assert!(result.is_ok());
1160 count += 1;
1161 }
1162
1163 assert_eq!(count, 2);
1164 }
1165
1166 #[test]
1167 fn test_memory_tracker() {
1168 let tracker = MemoryTracker::new(1000);
1169
1170 assert!(tracker.allocate(500).unwrap());
1171 assert_eq!(tracker.current_usage(), 500);
1172
1173 assert!(tracker.allocate(400).unwrap());
1174 assert_eq!(tracker.current_usage(), 900);
1175
1176 assert!(!tracker.allocate(200).unwrap()); tracker.deallocate(400);
1179 assert_eq!(tracker.current_usage(), 500);
1180
1181 assert!(tracker.allocate(200).unwrap());
1182 }
1183
1184 #[test]
1185 fn test_spillable_hash_join() {
1186 let config = StreamingConfig {
1187 memory_limit: 2048,
1188 ..Default::default()
1189 };
1190
1191 let mut join = SpillableHashJoin::new(config);
1192
1193 let mut left_solution = Solution::new();
1195 let mut left_binding = Binding::new();
1196 left_binding.insert(
1197 Variable::new("x").unwrap(),
1198 Term::Iri(NamedNode::new("http://example.org/1").unwrap()),
1199 );
1200 left_solution.push(left_binding);
1201
1202 let mut right_solution = Solution::new();
1203 let mut right_binding = Binding::new();
1204 right_binding.insert(
1205 Variable::new("x").unwrap(),
1206 Term::Iri(NamedNode::new("http://example.org/1").unwrap()),
1207 );
1208 right_solution.push(right_binding);
1209
1210 let left = vec![left_solution];
1211 let right = vec![right_solution];
1212 let join_vars = vec![Variable::new("x").unwrap()];
1213 let results = join.execute(left, right, &join_vars).unwrap();
1214
1215 assert!(!results.is_empty());
1216 }
1217
1218 #[test]
1219 fn grace_hash_join_spilled_buckets_produce_correct_results() {
1220 let mut sample = Solution::new();
1223 let mut sample_binding = Binding::new();
1224 sample_binding.insert(
1225 Variable::new_unchecked("x"),
1226 Term::Iri(NamedNode::new_unchecked("http://example.org/x/1")),
1227 );
1228 sample_binding.insert(
1229 Variable::new_unchecked("y"),
1230 Term::Iri(NamedNode::new_unchecked("http://example.org/y/000")),
1231 );
1232 sample.push(sample_binding);
1233
1234 let sizer = SpillableHashJoin::new(StreamingConfig::default());
1235 let one = sizer.estimate_solution_size(&sample).max(1);
1236
1237 let config = StreamingConfig {
1238 memory_limit: one * 2, buffer_size: 2,
1240 ..Default::default()
1241 };
1242 let mut join = SpillableHashJoin::new(config);
1243
1244 let mut left = Vec::new();
1246 for i in 0..6 {
1247 let mut sol = Solution::new();
1248 let mut binding = Binding::new();
1249 binding.insert(
1250 Variable::new_unchecked("x"),
1251 Term::Iri(NamedNode::new_unchecked("http://example.org/x/1")),
1252 );
1253 binding.insert(
1254 Variable::new_unchecked("y"),
1255 Term::Iri(NamedNode::new_unchecked(format!(
1256 "http://example.org/y/{i:03}"
1257 ))),
1258 );
1259 sol.push(binding);
1260 left.push(sol);
1261 }
1262 let mut right = Vec::new();
1264 for j in 0..3 {
1265 let mut sol = Solution::new();
1266 let mut binding = Binding::new();
1267 binding.insert(
1268 Variable::new_unchecked("x"),
1269 Term::Iri(NamedNode::new_unchecked("http://example.org/x/1")),
1270 );
1271 binding.insert(
1272 Variable::new_unchecked("z"),
1273 Term::Iri(NamedNode::new_unchecked(format!(
1274 "http://example.org/z/{j:03}"
1275 ))),
1276 );
1277 sol.push(binding);
1278 right.push(sol);
1279 }
1280
1281 let join_vars = vec![Variable::new_unchecked("x")];
1282 let results = join
1283 .execute(left, right, &join_vars)
1284 .expect("grace hash join should succeed");
1285
1286 assert_eq!(
1290 results.len(),
1291 18,
1292 "grace hash join must join spilled left and right partitions"
1293 );
1294 for sol in &results {
1295 for binding in sol {
1296 assert!(binding.contains_key(&Variable::new_unchecked("y")));
1297 assert!(binding.contains_key(&Variable::new_unchecked("z")));
1298 }
1299 }
1300 }
1301}