1mod nested_loop;
10
11use std::collections::{BTreeMap, HashMap};
12use std::fs::{File, OpenOptions};
13use std::hash::BuildHasher;
14use std::io::{ErrorKind, Read, Seek, SeekFrom, Write};
15use std::path::Path;
16
17use smallvec::SmallVec;
18use tempfile::{Builder as TempBuilder, NamedTempFile, TempDir};
19use uqa_core::Value;
20use uqa_sql::ast::JoinKind;
21use uqa_sql::expr::truthy;
22use uqa_sql::ResultRow;
23
24use crate::distinct::{encode_non_null_key, hash_canonical_row, EncodedKey};
25use crate::{
26 Batch, ExecError, ExecResult, IndexedSpill, PhysicalOperator, PhysicalRow, ProjectedPredicate,
27 RowSchema, ScalarExpr, SharedExpressionEvaluator, SpillBuffer,
28};
29
30pub use nested_loop::NestedLoopJoin;
31
32const DEFAULT_JOIN_WORK_MEM_BYTES: usize = 64 * 1024 * 1024;
33const HASH_BUCKETS: u64 = 64;
34
35fn output_schema(
36 left: &RowSchema,
37 right: &RowSchema,
38 left_nulls: &ResultRow,
39 right_nulls: &ResultRow,
40) -> RowSchema {
41 RowSchema::join(
42 left,
43 right,
44 left_nulls.keys().chain(right_nulls.keys()).cloned(),
45 )
46}
47
48fn push_output_row(
49 output: &mut SpillBuffer,
50 pending: &mut Vec<PhysicalRow>,
51 schema: &RowSchema,
52 row: PhysicalRow,
53) -> ExecResult<()> {
54 pending.push(row);
55 if pending.len() == crate::batch::DEFAULT_BATCH_SIZE {
56 output.push(Batch::from_physical_rows(
57 schema.clone(),
58 std::mem::take(pending),
59 ))?;
60 pending.reserve(crate::batch::DEFAULT_BATCH_SIZE);
61 }
62 Ok(())
63}
64
65fn join_io_error(operation: &str, error: impl std::fmt::Display) -> ExecError {
66 ExecError::Other(format!("join spill {operation}: {error}"))
67}
68
69struct HybridRowStore {
73 schema: RowSchema,
74 memory: Vec<PhysicalRow>,
75 rows: u64,
76 memory_bytes: usize,
77 budget_bytes: usize,
78 disk: Option<IndexedSpill>,
79}
80
81impl HybridRowStore {
82 fn new(schema: RowSchema, budget_bytes: usize) -> Self {
83 Self {
84 schema,
85 memory: Vec::new(),
86 rows: 0,
87 memory_bytes: 0,
88 budget_bytes,
89 disk: None,
90 }
91 }
92
93 fn len(&self) -> u64 {
94 self.disk.as_ref().map_or(self.rows, IndexedSpill::len)
95 }
96
97 fn has_spilled(&self) -> bool {
98 self.disk.is_some()
99 }
100
101 fn memory_row(&self, index: u64) -> Option<&PhysicalRow> {
102 if self.disk.is_some() {
103 return None;
104 }
105 usize::try_from(index)
106 .ok()
107 .and_then(|index| self.memory.get(index))
108 }
109
110 fn push(&mut self, row: PhysicalRow) -> ExecResult<()> {
111 if let Some(disk) = self.disk.as_mut() {
112 return disk.push(&row);
113 }
114
115 let row_bytes = IndexedSpill::encoded_row_size(&self.schema, &row)?;
116 let next_rows = self
117 .rows
118 .checked_add(1)
119 .ok_or_else(|| ExecError::Other("join build row count overflow".into()))?;
120 let fits = self
121 .memory_bytes
122 .checked_add(row_bytes)
123 .is_some_and(|bytes| bytes <= self.budget_bytes);
124 if fits {
125 self.memory.push(row);
126 self.rows = next_rows;
127 self.memory_bytes += row_bytes;
128 return Ok(());
129 }
130
131 let mut disk = IndexedSpill::new(self.schema.clone())?;
135 for existing in &self.memory {
136 disk.push(existing)?;
137 }
138 disk.push(&row)?;
139 self.memory.clear();
140 self.rows = disk.len();
141 self.memory_bytes = 0;
142 self.disk = Some(disk);
143 Ok(())
144 }
145
146 fn with_row<T>(
147 &mut self,
148 index: u64,
149 visitor: impl FnOnce(&PhysicalRow) -> ExecResult<T>,
150 ) -> ExecResult<T> {
151 if let Some(disk) = self.disk.as_mut() {
152 let row = disk.get(index)?;
153 return visitor(&row);
154 }
155
156 let index = usize::try_from(index)
157 .map_err(|_| ExecError::Other(format!("join row index {index} exceeds usize")))?;
158 let row = self.memory.get(index).ok_or_else(|| {
159 ExecError::Other(format!(
160 "join row {index} is outside 0..{}",
161 self.memory.len()
162 ))
163 })?;
164 visitor(row)
165 }
166}
167
168struct DirectHashIndex {
176 buckets: HashMap<u64, SmallVec<[u64; 1]>, ahash::RandomState>,
177 memory_bytes: usize,
178 budget_bytes: usize,
179 overflowed: bool,
180}
181
182impl DirectHashIndex {
183 fn new(budget_bytes: usize) -> Self {
184 Self {
185 buckets: HashMap::with_hasher(ahash::RandomState::new()),
186 memory_bytes: 0,
187 budget_bytes,
188 overflowed: false,
189 }
190 }
191
192 fn hasher(&self) -> &ahash::RandomState {
193 self.buckets.hasher()
194 }
195
196 fn insert(&mut self, hash: u64, row_index: u64) -> ExecResult<()> {
197 if self.overflowed {
198 return Ok(());
199 }
200
201 let record_bytes = if self.buckets.contains_key(&hash) {
204 8
205 } else {
206 64
207 };
208 let fits = self
209 .memory_bytes
210 .checked_add(record_bytes)
211 .is_some_and(|bytes| bytes <= self.budget_bytes);
212 if !fits {
213 self.buckets.clear();
214 self.memory_bytes = 0;
215 self.overflowed = true;
216 return Ok(());
217 }
218
219 self.buckets.entry(hash).or_default().push(row_index);
220 self.memory_bytes += record_bytes;
221 Ok(())
222 }
223
224 fn is_available(&self) -> bool {
225 !self.overflowed
226 }
227
228 fn candidates(&self, hash: u64) -> &[u64] {
229 self.buckets.get(&hash).map_or(&[], SmallVec::as_slice)
230 }
231
232 fn keys_are_unique(
233 &self,
234 rows: &HybridRowStore,
235 schema: &RowSchema,
236 positions: &[usize],
237 ) -> bool {
238 self.is_available()
239 && self.buckets.values().all(|bucket| {
240 bucket.iter().enumerate().all(|(offset, left_index)| {
241 bucket[offset + 1..].iter().all(|right_index| {
242 let Some(left) = rows.memory_row(*left_index) else {
243 return false;
244 };
245 let Some(right) = rows.memory_row(*right_index) else {
246 return false;
247 };
248 !positional_keys_equal(schema, left, positions, schema, right, positions)
249 })
250 })
251 })
252 }
253}
254
255fn positional_key_hash<S: BuildHasher>(
256 build_hasher: &S,
257 schema: &RowSchema,
258 row: &PhysicalRow,
259 positions: &[usize],
260) -> ExecResult<Option<u64>> {
261 let view = schema.view(row);
262 if positions.iter().any(|position| {
263 view.value_at(*position)
264 .is_none_or(|value| matches!(value, Value::Null))
265 }) {
266 return Ok(None);
267 }
268 hash_canonical_row(
269 build_hasher,
270 positions.iter().map(|position| view.value_at(*position)),
271 )
272 .map(Some)
273}
274
275fn positional_keys_equal(
276 left_schema: &RowSchema,
277 left_row: &PhysicalRow,
278 left_positions: &[usize],
279 right_schema: &RowSchema,
280 right_row: &PhysicalRow,
281 right_positions: &[usize],
282) -> bool {
283 if left_positions.len() != right_positions.len() {
284 return false;
285 }
286 let left = left_schema.view(left_row);
287 let right = right_schema.view(right_row);
288 left_positions
289 .iter()
290 .zip(right_positions)
291 .all(|(left_position, right_position)| {
292 let Some(left) = left.value_at(*left_position) else {
293 return false;
294 };
295 let Some(right) = right.value_at(*right_position) else {
296 return false;
297 };
298 !matches!(left, Value::Null) && !matches!(right, Value::Null) && left == right
299 })
300}
301
302fn direct_unique_match(
303 index: &DirectHashIndex,
304 build_rows: &HybridRowStore,
305 build_positions: &[usize],
306 probe_schema: &RowSchema,
307 probe_row: &PhysicalRow,
308 probe_positions: &[usize],
309) -> ExecResult<Option<u64>> {
310 let Some(hash) = positional_key_hash(index.hasher(), probe_schema, probe_row, probe_positions)?
311 else {
312 return Ok(None);
313 };
314 Ok(index.candidates(hash).iter().copied().find(|row_index| {
315 build_rows.memory_row(*row_index).is_some_and(|build_row| {
316 positional_keys_equal(
317 &build_rows.schema,
318 build_row,
319 build_positions,
320 probe_schema,
321 probe_row,
322 probe_positions,
323 )
324 })
325 }))
326}
327
328struct MatchFlags {
332 file: NamedTempFile,
333 rows: u64,
334}
335
336impl MatchFlags {
337 fn new(rows: u64) -> ExecResult<Self> {
338 let file =
339 NamedTempFile::new().map_err(|error| join_io_error("create match flags", error))?;
340 file.as_file()
341 .set_len(rows)
342 .map_err(|error| join_io_error("size match flags", error))?;
343 Ok(Self { file, rows })
344 }
345
346 fn mark(&mut self, index: u64) -> ExecResult<()> {
347 self.check_index(index)?;
348 self.file
349 .as_file_mut()
350 .seek(SeekFrom::Start(index))
351 .map_err(|error| join_io_error("seek match flags", error))?;
352 self.file
353 .as_file_mut()
354 .write_all(&[1])
355 .map_err(|error| join_io_error("write match flag", error))
356 }
357
358 fn is_marked(&mut self, index: u64) -> ExecResult<bool> {
359 self.check_index(index)?;
360 self.file
361 .as_file_mut()
362 .seek(SeekFrom::Start(index))
363 .map_err(|error| join_io_error("seek match flags", error))?;
364 let mut flag = [0_u8; 1];
365 self.file
366 .as_file_mut()
367 .read_exact(&mut flag)
368 .map_err(|error| join_io_error("read match flag", error))?;
369 Ok(flag[0] != 0)
370 }
371
372 fn check_index(&self, index: u64) -> ExecResult<()> {
373 if index >= self.rows {
374 return Err(ExecError::Other(format!(
375 "join match flag {index} is outside 0..{}",
376 self.rows
377 )));
378 }
379 Ok(())
380 }
381}
382
383struct HybridHashIndex {
388 memory: HashMap<EncodedKey, Vec<u64>, ahash::RandomState>,
389 memory_bytes: usize,
390 budget_bytes: usize,
391 disk: Option<DiskHashIndex>,
392}
393
394#[derive(Clone, Copy)]
395enum MemoryMatchSummary {
396 Absent,
397 Single(u64),
398 Multiple,
399}
400
401impl HybridHashIndex {
402 fn new(budget_bytes: usize) -> Self {
403 Self {
404 memory: HashMap::with_hasher(ahash::RandomState::new()),
408 memory_bytes: 0,
409 budget_bytes,
410 disk: None,
411 }
412 }
413
414 fn insert(&mut self, key: EncodedKey, row_index: u64) -> ExecResult<()> {
415 if let Some(disk) = self.disk.as_mut() {
416 return disk.insert(&key, row_index);
417 }
418
419 let record_bytes = key
420 .len()
421 .checked_add(16)
422 .ok_or_else(|| ExecError::Other("join hash-index size overflow".into()))?;
423 let fits = self
424 .memory_bytes
425 .checked_add(record_bytes)
426 .is_some_and(|bytes| bytes <= self.budget_bytes);
427 if fits {
428 self.memory.entry(key).or_default().push(row_index);
429 self.memory_bytes += record_bytes;
430 return Ok(());
431 }
432
433 let mut disk = DiskHashIndex::new(None)?;
434 for (existing_key, indices) in &self.memory {
435 for index in indices {
436 disk.insert(existing_key, *index)?;
437 }
438 }
439 disk.insert(&key, row_index)?;
440 self.memory.clear();
441 self.memory_bytes = 0;
442 self.disk = Some(disk);
443 Ok(())
444 }
445
446 fn for_each_match(
447 &mut self,
448 key: &[u8],
449 visitor: &mut dyn FnMut(u64) -> ExecResult<()>,
450 ) -> ExecResult<bool> {
451 if let Some(disk) = self.disk.as_mut() {
452 return disk.for_each_match(key, visitor);
453 }
454 let Some(indices) = self.memory.get(key) else {
455 return Ok(false);
456 };
457 for index in indices {
458 visitor(*index)?;
459 }
460 Ok(!indices.is_empty())
461 }
462
463 fn memory_match_summary(&self, key: &[u8]) -> Option<MemoryMatchSummary> {
466 if self.disk.is_some() {
467 return None;
468 }
469 Some(match self.memory.get(key).map(Vec::as_slice) {
470 None | Some([]) => MemoryMatchSummary::Absent,
471 Some([index]) => MemoryMatchSummary::Single(*index),
472 Some(_) => MemoryMatchSummary::Multiple,
473 })
474 }
475
476 fn has_spilled(&self) -> bool {
477 self.disk.is_some()
478 }
479
480 fn is_memory_unique(&self) -> bool {
481 self.disk.is_none() && self.memory.values().all(|indices| indices.len() == 1)
482 }
483}
484
485struct DiskHashIndex {
487 directory: TempDir,
488 buckets: BTreeMap<u8, File>,
489}
490
491impl DiskHashIndex {
492 fn new(parent: Option<&Path>) -> ExecResult<Self> {
493 let mut builder = TempBuilder::new();
494 builder.prefix("uqa-hash-join-");
495 let directory = parent
496 .map_or_else(|| builder.tempdir(), |parent| builder.tempdir_in(parent))
497 .map_err(|error| join_io_error("create hash directory", error))?;
498 Ok(Self {
499 directory,
500 buckets: BTreeMap::new(),
501 })
502 }
503
504 fn insert(&mut self, key: &[u8], row_index: u64) -> ExecResult<()> {
505 let bucket = u8::try_from(stable_hash(key) % HASH_BUCKETS)
506 .map_err(|_| ExecError::Other("join spill bucket exceeds u8".into()))?;
507 if !self.buckets.contains_key(&bucket) {
508 let path = self.directory.path().join(format!("bucket-{bucket:02x}"));
509 let file = OpenOptions::new()
510 .create_new(true)
511 .read(true)
512 .write(true)
513 .open(&path)
514 .map_err(|error| join_io_error("create hash bucket", error))?;
515 self.buckets.insert(bucket, file);
516 }
517 let file = self
518 .buckets
519 .get_mut(&bucket)
520 .ok_or_else(|| ExecError::Other("join hash bucket registration failed".into()))?;
521 let original_len = file
522 .seek(SeekFrom::End(0))
523 .map_err(|error| join_io_error("seek hash bucket", error))?;
524 let key_len = u64::try_from(key.len())
525 .map_err(|_| ExecError::Other("join hash key is too large".into()))?;
526 let write_result = (|| -> std::io::Result<()> {
527 file.write_all(&key_len.to_le_bytes())?;
528 file.write_all(key)?;
529 file.write_all(&row_index.to_le_bytes())?;
530 file.flush()
531 })();
532 if let Err(error) = write_result {
533 if let Err(rollback) = file.set_len(original_len) {
534 return Err(ExecError::Other(format!(
535 "join spill append hash bucket: {error}; rollback failed: {rollback}"
536 )));
537 }
538 return Err(join_io_error("append hash bucket", error));
539 }
540 Ok(())
541 }
542
543 fn for_each_match(
544 &mut self,
545 key: &[u8],
546 visitor: &mut dyn FnMut(u64) -> ExecResult<()>,
547 ) -> ExecResult<bool> {
548 let bucket = u8::try_from(stable_hash(key) % HASH_BUCKETS)
549 .map_err(|_| ExecError::Other("join spill bucket exceeds u8".into()))?;
550 let Some(file) = self.buckets.get_mut(&bucket) else {
551 return Ok(false);
552 };
553 file.seek(SeekFrom::Start(0))
554 .map_err(|error| join_io_error("rewind hash bucket", error))?;
555 let file_len = file
556 .metadata()
557 .map_err(|error| join_io_error("inspect hash bucket", error))?
558 .len();
559 let mut matched = false;
560 while let Some(key_len) = read_u64(file, "read hash key length")? {
561 let key_start = file
562 .stream_position()
563 .map_err(|error| join_io_error("locate hash key", error))?;
564 let key_end = key_start
565 .checked_add(key_len)
566 .ok_or_else(|| ExecError::Other("join hash key offset overflow".into()))?;
567 let record_end = key_end
568 .checked_add(8)
569 .ok_or_else(|| ExecError::Other("join hash record offset overflow".into()))?;
570 if record_end > file_len {
571 return Err(ExecError::Other(format!(
572 "join hash key length {key_len} exceeds remaining bucket record bytes"
573 )));
574 }
575 let key_matches = compare_hash_key(file, key_start, key_end, key_len, key)?;
576 let row_index = read_u64(file, "read hash row index")?
577 .ok_or_else(|| ExecError::Other("truncated join hash row index".into()))?;
578 if key_matches {
579 visitor(row_index)?;
580 matched = true;
581 }
582 }
583 Ok(matched)
584 }
585}
586
587fn compare_hash_key(
588 file: &mut File,
589 key_start: u64,
590 key_end: u64,
591 stored_len: u64,
592 expected: &[u8],
593) -> ExecResult<bool> {
594 let expected_len = u64::try_from(expected.len())
595 .map_err(|_| ExecError::Other("join probe key length is invalid".into()))?;
596 if stored_len != expected_len {
597 file.seek(SeekFrom::Start(key_end))
598 .map_err(|error| join_io_error("skip non-matching hash key", error))?;
599 return Ok(false);
600 }
601
602 file.seek(SeekFrom::Start(key_start))
603 .map_err(|error| join_io_error("seek hash key", error))?;
604 let mut buffer = [0_u8; 8 * 1024];
605 let mut compared = 0_usize;
606 let mut matches = true;
607 while compared < expected.len() {
608 let take = (expected.len() - compared).min(buffer.len());
609 file.read_exact(&mut buffer[..take])
610 .map_err(|error| join_io_error("read hash key", error))?;
611 matches &= buffer[..take] == expected[compared..compared + take];
612 compared += take;
613 }
614 Ok(matches)
615}
616
617fn read_u64(file: &mut File, operation: &str) -> ExecResult<Option<u64>> {
618 let mut encoded = [0_u8; 8];
619 match file.read(&mut encoded[..1]) {
620 Ok(0) => return Ok(None),
621 Ok(1) => {}
622 Ok(count) => {
623 return Err(ExecError::Other(format!(
624 "invalid join spill read count: requested 1 byte, received {count}"
625 )));
626 }
627 Err(error) => return Err(join_io_error(operation, error)),
628 }
629 file.read_exact(&mut encoded[1..]).map_err(|error| {
630 if error.kind() == ErrorKind::UnexpectedEof {
631 ExecError::Other(format!(
632 "truncated join spill record while attempting to {operation}"
633 ))
634 } else {
635 join_io_error(operation, error)
636 }
637 })?;
638 Ok(Some(u64::from_le_bytes(encoded)))
639}
640
641fn stable_hash(bytes: &[u8]) -> u64 {
642 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
643 for byte in bytes {
644 hash ^= u64::from(*byte);
645 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
646 }
647 hash
648}
649
650fn simple_key_positions(schema: &RowSchema, expressions: &[ScalarExpr]) -> Option<Vec<usize>> {
655 expressions
656 .iter()
657 .map(|expression| match expression {
658 ScalarExpr::Column(column) => schema.position(column),
659 ScalarExpr::QualifiedColumn { qualifier, column } => {
660 schema.qualified_position(qualifier, column)
661 }
662 _ => None,
663 })
664 .collect()
665}
666
667pub struct HashJoin<'a> {
668 left: Box<dyn PhysicalOperator + 'a>,
669 right: Box<dyn PhysicalOperator + 'a>,
670 kind: JoinKind,
671 left_keys: Vec<ScalarExpr>,
672 right_keys: Vec<ScalarExpr>,
673 left_key_positions: Option<Vec<usize>>,
674 right_key_positions: Option<Vec<usize>>,
675 predicate: Option<ScalarExpr>,
676 prepared_predicate: Option<ProjectedPredicate>,
677 evaluator: SharedExpressionEvaluator<'a>,
678 left_nulls: PhysicalRow,
679 right_nulls: PhysicalRow,
680 schema: RowSchema,
681 estimated_cardinality: Option<u64>,
682 build_left: bool,
683 work_mem_bytes: usize,
684 output: Option<crate::spill::SpillDrain>,
685 streaming_unique: Option<UniqueHashJoinState>,
686 output_spilled: SpillState,
687 right_input_spilled: SpillState,
688 hash_index_spilled: SpillState,
689}
690
691#[derive(Clone, Copy, Default, Eq, PartialEq)]
692enum SpillState {
693 #[default]
694 InMemory,
695 Spilled,
696}
697
698impl SpillState {
699 fn is_spilled(self) -> bool {
700 matches!(self, Self::Spilled)
701 }
702}
703
704impl From<bool> for SpillState {
705 fn from(spilled: bool) -> Self {
706 if spilled {
707 Self::Spilled
708 } else {
709 Self::InMemory
710 }
711 }
712}
713
714struct UniqueHashJoinState {
718 build_rows: HybridRowStore,
719 hash_index: UniqueHashIndex,
720 build_left: bool,
721}
722
723enum UniqueHashIndex {
724 Direct(DirectHashIndex),
727 Encoded(HybridHashIndex),
729}
730
731impl<'a> HashJoin<'a> {
732 #[allow(clippy::too_many_arguments)]
733 pub fn new(
734 left: Box<dyn PhysicalOperator + 'a>,
735 right: Box<dyn PhysicalOperator + 'a>,
736 kind: JoinKind,
737 left_keys: Vec<ScalarExpr>,
738 right_keys: Vec<ScalarExpr>,
739 evaluator: SharedExpressionEvaluator<'a>,
740 left_nulls: ResultRow,
741 right_nulls: ResultRow,
742 ) -> Self {
743 Self::new_with_work_mem(
744 left,
745 right,
746 kind,
747 left_keys,
748 right_keys,
749 evaluator,
750 left_nulls,
751 right_nulls,
752 DEFAULT_JOIN_WORK_MEM_BYTES,
753 )
754 }
755
756 #[allow(clippy::too_many_arguments)]
757 pub fn new_with_work_mem(
758 left: Box<dyn PhysicalOperator + 'a>,
759 right: Box<dyn PhysicalOperator + 'a>,
760 kind: JoinKind,
761 left_keys: Vec<ScalarExpr>,
762 right_keys: Vec<ScalarExpr>,
763 evaluator: SharedExpressionEvaluator<'a>,
764 left_nulls: ResultRow,
765 right_nulls: ResultRow,
766 work_mem_bytes: usize,
767 ) -> Self {
768 Self::new_with_work_mem_and_predicate(
769 left,
770 right,
771 kind,
772 left_keys,
773 right_keys,
774 None,
775 evaluator,
776 left_nulls,
777 right_nulls,
778 work_mem_bytes,
779 )
780 }
781
782 #[allow(clippy::too_many_arguments)]
783 pub fn new_with_work_mem_and_predicate(
784 left: Box<dyn PhysicalOperator + 'a>,
785 right: Box<dyn PhysicalOperator + 'a>,
786 kind: JoinKind,
787 left_keys: Vec<ScalarExpr>,
788 right_keys: Vec<ScalarExpr>,
789 predicate: Option<ScalarExpr>,
790 evaluator: SharedExpressionEvaluator<'a>,
791 left_nulls: ResultRow,
792 right_nulls: ResultRow,
793 work_mem_bytes: usize,
794 ) -> Self {
795 let left_key_positions = simple_key_positions(left.row_schema(), &left_keys);
796 let right_key_positions = simple_key_positions(right.row_schema(), &right_keys);
797 let schema = output_schema(
798 left.row_schema(),
799 right.row_schema(),
800 &left_nulls,
801 &right_nulls,
802 );
803 let left_nulls = PhysicalRow::nulls(left.row_schema().physical_width());
804 let right_nulls = PhysicalRow::nulls(right.row_schema().physical_width());
805 let left_cardinality = left.estimated_cardinality();
806 let right_cardinality = right.estimated_cardinality();
807 let build_left = matches!(kind, JoinKind::Inner)
808 && left_cardinality
809 .zip(right_cardinality)
810 .is_some_and(|(left, right)| left < right);
811 let estimated_cardinality = left_cardinality
812 .zip(right_cardinality)
813 .map(|(left, right)| match kind {
814 JoinKind::Inner => left.max(right),
815 JoinKind::Left => left,
816 JoinKind::Right => right,
817 JoinKind::Full => left.saturating_add(right),
818 JoinKind::Cross => left.saturating_mul(right),
819 });
820 let prepared_predicate = predicate.as_ref().and_then(|predicate| {
821 ProjectedPredicate::compile_with_schema(predicate, &schema, &[])
822 .ok()
823 .flatten()
824 });
825 Self {
826 left,
827 right,
828 kind,
829 left_keys,
830 right_keys,
831 left_key_positions,
832 right_key_positions,
833 predicate,
834 prepared_predicate,
835 evaluator,
836 left_nulls,
837 right_nulls,
838 schema,
839 estimated_cardinality,
840 build_left,
841 work_mem_bytes,
842 output: None,
843 streaming_unique: None,
844 output_spilled: SpillState::InMemory,
845 right_input_spilled: SpillState::InMemory,
846 hash_index_spilled: SpillState::InMemory,
847 }
848 }
849
850 #[allow(clippy::too_many_arguments)]
854 pub fn try_new_with_work_mem_and_predicate(
855 left: Box<dyn PhysicalOperator + 'a>,
856 right: Box<dyn PhysicalOperator + 'a>,
857 kind: JoinKind,
858 left_keys: Vec<ScalarExpr>,
859 right_keys: Vec<ScalarExpr>,
860 predicate: Option<ScalarExpr>,
861 evaluator: SharedExpressionEvaluator<'a>,
862 left_nulls: ResultRow,
863 right_nulls: ResultRow,
864 work_mem_bytes: usize,
865 params: &[uqa_sql::SQLParam],
866 ) -> ExecResult<Self> {
867 let mut join = Self::new_with_work_mem_and_predicate(
868 left,
869 right,
870 kind,
871 left_keys,
872 right_keys,
873 predicate,
874 evaluator,
875 left_nulls,
876 right_nulls,
877 work_mem_bytes,
878 );
879 join.prepared_predicate = join
880 .predicate
881 .as_ref()
882 .map(|predicate| {
883 ProjectedPredicate::compile_with_schema(predicate, &join.schema, params)
884 })
885 .transpose()?
886 .flatten();
887 Ok(join)
888 }
889
890 pub fn output_has_spilled(&self) -> bool {
891 self.output_spilled.is_spilled()
892 }
893
894 pub fn right_input_has_spilled(&self) -> bool {
895 self.right_input_spilled.is_spilled()
896 }
897
898 pub fn hash_index_has_spilled(&self) -> bool {
899 self.hash_index_spilled.is_spilled()
900 }
901
902 pub fn builds_left_input(&self) -> bool {
903 self.build_left
904 }
905
906 fn rebuild_encoded_index(
907 &self,
908 rows: &mut HybridRowStore,
909 expressions: &[ScalarExpr],
910 positions: &[usize],
911 budget_bytes: usize,
912 ) -> ExecResult<HybridHashIndex> {
913 let schema = rows.schema.clone();
914 let mut index = HybridHashIndex::new(budget_bytes);
915 for row_index in 0..rows.len() {
916 let key = rows.with_row(row_index, |row| {
917 self.key(expressions, Some(positions), row, &schema)
918 })?;
919 if let Some(key) = key {
920 index.insert(key, row_index)?;
921 }
922 }
923 Ok(index)
924 }
925
926 fn open_build_left(&mut self, state_budget: usize, output_budget: usize) -> ExecResult<()> {
927 debug_assert!(matches!(self.kind, JoinKind::Inner));
928 let left_budget = state_budget / 2;
929 let hash_budget = state_budget.saturating_sub(left_budget);
930 let left_schema = self.left.row_schema().clone();
931 let mut left = HybridRowStore::new(left_schema, left_budget);
932 let direct_positions = self
933 .predicate
934 .is_none()
935 .then_some(())
936 .and(self.left_key_positions.as_deref())
937 .zip(self.right_key_positions.as_deref());
938 let mut direct_index = direct_positions.map(|_| DirectHashIndex::new(hash_budget));
939 let mut encoded_index = direct_index
940 .is_none()
941 .then(|| HybridHashIndex::new(hash_budget));
942 self.left.open()?;
943 while let Some(batch) = self.left.next()? {
944 for row in batch.rows {
945 let index = left.len();
946 if let (Some(direct), Some((positions, _))) =
947 (direct_index.as_mut(), direct_positions)
948 {
949 if let Some(hash) =
950 positional_key_hash(direct.hasher(), &batch.schema, &row, positions)?
951 {
952 direct.insert(hash, index)?;
953 }
954 } else if let Some(key) = self.key(
955 &self.left_keys,
956 self.left_key_positions.as_deref(),
957 &row,
958 &batch.schema,
959 )? {
960 encoded_index
961 .as_mut()
962 .ok_or_else(|| ExecError::Other("join hash index is missing".into()))?
963 .insert(key, index)?;
964 }
965 left.push(row)?;
966 }
967 }
968 self.right_input_spilled = SpillState::InMemory;
969
970 let mut output = SpillBuffer::new(output_budget);
971 if left.len() == 0 {
972 self.output = Some(output.drain()?);
973 return Ok(());
974 }
975
976 let direct_is_unique = direct_index.as_ref().is_some_and(|direct| {
977 direct_positions.is_some_and(|(positions, _)| {
978 !left.has_spilled() && direct.keys_are_unique(&left, &left.schema, positions)
979 })
980 });
981 if direct_is_unique {
982 self.right.open()?;
983 self.streaming_unique = Some(UniqueHashJoinState {
984 build_rows: left,
985 hash_index: UniqueHashIndex::Direct(
986 direct_index
987 .take()
988 .ok_or_else(|| ExecError::Other("direct join index is missing".into()))?,
989 ),
990 build_left: true,
991 });
992 return Ok(());
993 }
994
995 let mut left_by_key = match encoded_index {
996 Some(index) => index,
997 None => {
998 let (positions, _) = direct_positions
999 .ok_or_else(|| ExecError::Other("direct join positions are missing".into()))?;
1000 self.rebuild_encoded_index(&mut left, &self.left_keys, positions, hash_budget)?
1001 }
1002 };
1003 self.hash_index_spilled = left_by_key.has_spilled().into();
1004 if self.predicate.is_none() && !left.has_spilled() && left_by_key.is_memory_unique() {
1005 self.right.open()?;
1006 self.streaming_unique = Some(UniqueHashJoinState {
1007 build_rows: left,
1008 hash_index: UniqueHashIndex::Encoded(left_by_key),
1009 build_left: true,
1010 });
1011 return Ok(());
1012 }
1013 let mut pending = Vec::with_capacity(crate::batch::DEFAULT_BATCH_SIZE);
1014
1015 self.right.open()?;
1016 while let Some(batch) = self.right.next()? {
1017 for right_row in batch.rows {
1018 let Some(key) = self.key(
1019 &self.right_keys,
1020 self.right_key_positions.as_deref(),
1021 &right_row,
1022 &batch.schema,
1023 )?
1024 else {
1025 continue;
1026 };
1027 if self.predicate.is_none() {
1028 match left_by_key.memory_match_summary(&key) {
1029 Some(MemoryMatchSummary::Absent) => continue,
1030 Some(MemoryMatchSummary::Single(index)) => {
1031 let merged = left.with_row(index, |left_row| {
1032 Ok(PhysicalRow::concat_right_owned(left_row, right_row))
1033 })?;
1034 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
1035 continue;
1036 }
1037 Some(MemoryMatchSummary::Multiple) | None => {}
1038 }
1039 }
1040 left_by_key.for_each_match(&key, &mut |index| {
1041 left.with_row(index, |left_row| {
1042 let merged = PhysicalRow::concat(left_row, &right_row);
1043 if self.matches(&merged)? {
1044 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
1045 }
1046 Ok(())
1047 })
1048 })?;
1049 }
1050 }
1051 if !pending.is_empty() {
1052 output.push(Batch::from_physical_rows(self.schema.clone(), pending))?;
1053 }
1054 self.output_spilled = output.has_spilled().into();
1055 self.output = Some(output.drain()?);
1056 Ok(())
1057 }
1058
1059 fn key(
1060 &self,
1061 expressions: &[ScalarExpr],
1062 positions: Option<&[usize]>,
1063 row: &PhysicalRow,
1064 schema: &RowSchema,
1065 ) -> ExecResult<Option<EncodedKey>> {
1066 if let Some(positions) = positions {
1067 let view = schema.view(row);
1068 return encode_non_null_key(positions.iter().map(|position| view.value_at(*position)));
1069 }
1070 let mut values = SmallVec::<[Value; 4]>::with_capacity(expressions.len());
1071 for expression in expressions {
1072 let value = self.evaluator.evaluate_physical(expression, schema, row)?;
1073 if matches!(value, Value::Null) {
1074 return Ok(None);
1075 }
1076 values.push(value);
1077 }
1078 encode_non_null_key(values.iter().map(Some))
1079 }
1080
1081 fn matches(&self, row: &PhysicalRow) -> ExecResult<bool> {
1082 if let Some(predicate) = self.prepared_predicate.as_ref() {
1083 return Ok(predicate.keep_row(&self.schema.view(row))?);
1084 }
1085 self.predicate.as_ref().map_or(Ok(true), |predicate| {
1086 Ok(truthy(&self.evaluator.evaluate_physical(
1087 predicate,
1088 &self.schema,
1089 row,
1090 )?))
1091 })
1092 }
1093
1094 fn next_streaming_unique(
1095 &mut self,
1096 state: &mut UniqueHashJoinState,
1097 ) -> ExecResult<Option<Batch>> {
1098 loop {
1099 let next = if state.build_left {
1100 self.right.next()?
1101 } else {
1102 self.left.next()?
1103 };
1104 let Some(batch) = next else {
1105 return Ok(None);
1106 };
1107 let mut output = Vec::with_capacity(batch.rows.len());
1108 for probe_row in batch.rows {
1109 let index = match &state.hash_index {
1110 UniqueHashIndex::Direct(index) => {
1111 let (build_positions, probe_positions) = if state.build_left {
1112 (
1113 self.left_key_positions.as_deref(),
1114 self.right_key_positions.as_deref(),
1115 )
1116 } else {
1117 (
1118 self.right_key_positions.as_deref(),
1119 self.left_key_positions.as_deref(),
1120 )
1121 };
1122 let (Some(build_positions), Some(probe_positions)) =
1123 (build_positions, probe_positions)
1124 else {
1125 return Err(ExecError::Other(
1126 "direct join key positions are missing".into(),
1127 ));
1128 };
1129 direct_unique_match(
1130 index,
1131 &state.build_rows,
1132 build_positions,
1133 &batch.schema,
1134 &probe_row,
1135 probe_positions,
1136 )?
1137 }
1138 UniqueHashIndex::Encoded(index) => {
1139 let expressions = if state.build_left {
1140 &self.right_keys
1141 } else {
1142 &self.left_keys
1143 };
1144 let positions = if state.build_left {
1145 self.right_key_positions.as_deref()
1146 } else {
1147 self.left_key_positions.as_deref()
1148 };
1149 let Some(key) =
1150 self.key(expressions, positions, &probe_row, &batch.schema)?
1151 else {
1152 continue;
1153 };
1154 match index.memory_match_summary(&key) {
1155 Some(MemoryMatchSummary::Single(index)) => Some(index),
1156 _ => None,
1157 }
1158 }
1159 };
1160 let Some(index) = index else { continue };
1161 let merged = if state.build_left {
1162 state.build_rows.with_row(index, |build_row| {
1163 Ok(PhysicalRow::concat_right_owned(build_row, probe_row))
1164 })?
1165 } else {
1166 state.build_rows.with_row(index, |build_row| {
1167 Ok(PhysicalRow::concat_left_owned(probe_row, build_row))
1168 })?
1169 };
1170 output.push(merged);
1171 }
1172 if !output.is_empty() {
1173 return Ok(Some(Batch::from_physical_rows(self.schema.clone(), output)));
1174 }
1175 }
1176 }
1177}
1178
1179impl PhysicalOperator for HashJoin<'_> {
1180 fn row_schema(&self) -> &RowSchema {
1181 &self.schema
1182 }
1183
1184 fn estimated_cardinality(&self) -> Option<u64> {
1185 self.estimated_cardinality
1186 }
1187
1188 fn open(&mut self) -> ExecResult<()> {
1189 self.output = None;
1190 self.streaming_unique = None;
1191 self.output_spilled = SpillState::InMemory;
1192 self.right_input_spilled = SpillState::InMemory;
1193 self.hash_index_spilled = SpillState::InMemory;
1194
1195 let state_budget = self.work_mem_bytes / 2;
1196 let output_budget = self.work_mem_bytes.saturating_sub(state_budget);
1197 if self.build_left {
1198 return self.open_build_left(state_budget, output_budget);
1199 }
1200 let right_budget = state_budget / 2;
1201 let hash_budget = state_budget.saturating_sub(right_budget);
1202 let right_schema = self.right.row_schema().clone();
1203 let mut right = HybridRowStore::new(right_schema, right_budget);
1204 let direct_positions = (matches!(self.kind, JoinKind::Inner) && self.predicate.is_none())
1205 .then_some(())
1206 .and(self.right_key_positions.as_deref())
1207 .zip(self.left_key_positions.as_deref());
1208 let mut direct_index = direct_positions.map(|_| DirectHashIndex::new(hash_budget));
1209 let mut encoded_index = direct_index
1210 .is_none()
1211 .then(|| HybridHashIndex::new(hash_budget));
1212 self.right.open()?;
1213 while let Some(batch) = self.right.next()? {
1214 for row in batch.rows {
1215 let index = right.len();
1216 if let (Some(direct), Some((positions, _))) =
1217 (direct_index.as_mut(), direct_positions)
1218 {
1219 if let Some(hash) =
1220 positional_key_hash(direct.hasher(), &batch.schema, &row, positions)?
1221 {
1222 direct.insert(hash, index)?;
1223 }
1224 } else if let Some(key) = self.key(
1225 &self.right_keys,
1226 self.right_key_positions.as_deref(),
1227 &row,
1228 &batch.schema,
1229 )? {
1230 encoded_index
1231 .as_mut()
1232 .ok_or_else(|| ExecError::Other("join hash index is missing".into()))?
1233 .insert(key, index)?;
1234 }
1235 right.push(row)?;
1236 }
1237 }
1238 self.right_input_spilled = right.has_spilled().into();
1239
1240 if right.len() == 0 && matches!(self.kind, JoinKind::Inner) {
1241 let mut output = SpillBuffer::new(output_budget);
1242 self.output = Some(output.drain()?);
1243 return Ok(());
1244 }
1245
1246 let direct_is_unique = direct_index.as_ref().is_some_and(|direct| {
1247 direct_positions.is_some_and(|(positions, _)| {
1248 !right.has_spilled() && direct.keys_are_unique(&right, &right.schema, positions)
1249 })
1250 });
1251 if direct_is_unique {
1252 self.left.open()?;
1253 self.streaming_unique = Some(UniqueHashJoinState {
1254 build_rows: right,
1255 hash_index: UniqueHashIndex::Direct(
1256 direct_index
1257 .take()
1258 .ok_or_else(|| ExecError::Other("direct join index is missing".into()))?,
1259 ),
1260 build_left: false,
1261 });
1262 return Ok(());
1263 }
1264
1265 let mut right_by_key = match encoded_index {
1266 Some(index) => index,
1267 None => {
1268 let (positions, _) = direct_positions
1269 .ok_or_else(|| ExecError::Other("direct join positions are missing".into()))?;
1270 self.rebuild_encoded_index(&mut right, &self.right_keys, positions, hash_budget)?
1271 }
1272 };
1273 self.hash_index_spilled = right_by_key.has_spilled().into();
1274 if matches!(self.kind, JoinKind::Inner)
1275 && self.predicate.is_none()
1276 && !right.has_spilled()
1277 && right_by_key.is_memory_unique()
1278 {
1279 self.left.open()?;
1280 self.streaming_unique = Some(UniqueHashJoinState {
1281 build_rows: right,
1282 hash_index: UniqueHashIndex::Encoded(right_by_key),
1283 build_left: false,
1284 });
1285 return Ok(());
1286 }
1287
1288 let mut matched_right = matches!(self.kind, JoinKind::Right | JoinKind::Full)
1289 .then(|| MatchFlags::new(right.len()))
1290 .transpose()?;
1291 let mut output = SpillBuffer::new(output_budget);
1292 let mut pending = Vec::with_capacity(crate::batch::DEFAULT_BATCH_SIZE);
1293
1294 self.left.open()?;
1295 while let Some(batch) = self.left.next()? {
1296 for left_row in batch.rows {
1297 let mut matched_left = false;
1298 if let Some(key) = self.key(
1299 &self.left_keys,
1300 self.left_key_positions.as_deref(),
1301 &left_row,
1302 &batch.schema,
1303 )? {
1304 if self.predicate.is_none() {
1305 match right_by_key.memory_match_summary(&key) {
1306 Some(MemoryMatchSummary::Absent) => {
1307 if matches!(self.kind, JoinKind::Left | JoinKind::Full) {
1308 push_output_row(
1309 &mut output,
1310 &mut pending,
1311 &self.schema,
1312 PhysicalRow::concat_left_owned(left_row, &self.right_nulls),
1313 )?;
1314 }
1315 continue;
1316 }
1317 Some(MemoryMatchSummary::Single(index)) => {
1318 let merged = right.with_row(index, |right_row| {
1319 Ok(PhysicalRow::concat_left_owned(left_row, right_row))
1320 })?;
1321 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
1322 if let Some(flags) = matched_right.as_mut() {
1323 flags.mark(index)?;
1324 }
1325 continue;
1326 }
1327 Some(MemoryMatchSummary::Multiple) | None => {}
1328 }
1329 }
1330 right_by_key.for_each_match(&key, &mut |index| {
1331 right.with_row(index, |right_row| {
1332 let merged = PhysicalRow::concat(&left_row, right_row);
1333 if self.matches(&merged)? {
1334 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
1335 if let Some(flags) = matched_right.as_mut() {
1336 flags.mark(index)?;
1337 }
1338 matched_left = true;
1339 }
1340 Ok(())
1341 })
1342 })?;
1343 }
1344 if !matched_left && matches!(self.kind, JoinKind::Left | JoinKind::Full) {
1345 push_output_row(
1346 &mut output,
1347 &mut pending,
1348 &self.schema,
1349 PhysicalRow::concat_left_owned(left_row, &self.right_nulls),
1350 )?;
1351 }
1352 }
1353 }
1354
1355 if matches!(self.kind, JoinKind::Right | JoinKind::Full) {
1356 let matched_right = matched_right.as_mut().ok_or_else(|| {
1357 ExecError::Other("right/full hash join has no match flags".into())
1358 })?;
1359 for index in 0..right.len() {
1360 if !matched_right.is_marked(index)? {
1361 right.with_row(index, |right_row| {
1362 push_output_row(
1363 &mut output,
1364 &mut pending,
1365 &self.schema,
1366 PhysicalRow::concat(&self.left_nulls, right_row),
1367 )
1368 })?;
1369 }
1370 }
1371 }
1372 if !pending.is_empty() {
1373 output.push(Batch::from_physical_rows(self.schema.clone(), pending))?;
1374 }
1375 self.output_spilled = output.has_spilled().into();
1376 self.output = Some(output.drain()?);
1377 Ok(())
1378 }
1379
1380 fn next(&mut self) -> ExecResult<Option<Batch>> {
1381 if let Some(mut state) = self.streaming_unique.take() {
1382 let result = self.next_streaming_unique(&mut state);
1383 self.streaming_unique = Some(state);
1384 return result;
1385 }
1386 self.output
1387 .as_mut()
1388 .map_or(Ok(None), |output| output.next().transpose())
1389 }
1390
1391 fn close(&mut self) -> ExecResult<()> {
1392 self.output = None;
1393 self.streaming_unique = None;
1394 let left = self.left.close();
1395 let right = self.right.close();
1396 crate::physical::with_cleanup(left, right, "close right hash-join input")
1397 }
1398}
1399
1400#[cfg(test)]
1401mod tests;