1mod canonical_index;
10mod direct_index;
11mod nested_loop;
12mod row_store;
13
14use smallvec::SmallVec;
15use uqa_core::Value;
16use uqa_sql::ast::JoinKind;
17use uqa_sql::expr::truthy;
18use uqa_sql::ResultRow;
19
20use crate::distinct::{encode_non_null_key, EncodedKey};
21use crate::{
22 Batch, ExecError, ExecResult, PhysicalOperator, PhysicalRow, ProjectedPredicate, RowSchema,
23 ScalarExpr, SharedExpressionEvaluator, SpillBuffer,
24};
25
26use canonical_index::{HybridHashIndex, MatchFlags, MemoryMatchSummary};
27use direct_index::{direct_unique_match, positional_key_hash, DirectHashIndex};
28use row_store::HybridRowStore;
29
30pub use nested_loop::NestedLoopJoin;
31
32#[cfg(test)]
33use canonical_index::{stable_hash, DiskHashIndex, HASH_BUCKETS};
34
35const DEFAULT_JOIN_WORK_MEM_BYTES: usize = 64 * 1024 * 1024;
36
37fn output_schema(
38 left: &RowSchema,
39 right: &RowSchema,
40 left_nulls: &ResultRow,
41 right_nulls: &ResultRow,
42) -> RowSchema {
43 RowSchema::join(
44 left,
45 right,
46 left_nulls.keys().chain(right_nulls.keys()).cloned(),
47 )
48}
49
50fn push_output_row(
51 output: &mut SpillBuffer,
52 pending: &mut Vec<PhysicalRow>,
53 schema: &RowSchema,
54 row: PhysicalRow,
55) -> ExecResult<()> {
56 pending.push(row);
57 if pending.len() == crate::batch::DEFAULT_BATCH_SIZE {
58 output.push(Batch::from_physical_rows(
59 schema.clone(),
60 std::mem::take(pending),
61 ))?;
62 pending.reserve(crate::batch::DEFAULT_BATCH_SIZE);
63 }
64 Ok(())
65}
66
67fn join_io_error(operation: &str, error: impl std::fmt::Display) -> ExecError {
68 ExecError::Other(format!("join spill {operation}: {error}"))
69}
70
71fn simple_key_positions(schema: &RowSchema, expressions: &[ScalarExpr]) -> Option<Vec<usize>> {
72 expressions
73 .iter()
74 .map(|expression| match expression {
75 ScalarExpr::Column(column) => schema.position(column),
76 ScalarExpr::QualifiedColumn { qualifier, column } => {
77 schema.qualified_position(qualifier, column)
78 }
79 _ => None,
80 })
81 .collect()
82}
83
84pub struct HashJoin<'a> {
89 left: Box<dyn PhysicalOperator + 'a>,
90 right: Box<dyn PhysicalOperator + 'a>,
91 kind: JoinKind,
92 left_keys: Vec<ScalarExpr>,
93 right_keys: Vec<ScalarExpr>,
94 left_key_positions: Option<Vec<usize>>,
95 right_key_positions: Option<Vec<usize>>,
96 predicate: Option<ScalarExpr>,
97 prepared_predicate: Option<ProjectedPredicate>,
98 evaluator: SharedExpressionEvaluator<'a>,
99 left_nulls: PhysicalRow,
100 right_nulls: PhysicalRow,
101 schema: RowSchema,
102 estimated_cardinality: Option<u64>,
103 build_left: bool,
104 work_mem_bytes: usize,
105 output: Option<crate::spill::SpillDrain>,
106 streaming_unique: Option<UniqueHashJoinState>,
107 output_spilled: SpillState,
108 right_input_spilled: SpillState,
109 hash_index_spilled: SpillState,
110}
111
112#[derive(Clone, Copy, Default, Eq, PartialEq)]
113enum SpillState {
114 #[default]
115 InMemory,
116 Spilled,
117}
118
119impl SpillState {
120 fn is_spilled(self) -> bool {
121 matches!(self, Self::Spilled)
122 }
123}
124
125impl From<bool> for SpillState {
126 fn from(spilled: bool) -> Self {
127 if spilled {
128 Self::Spilled
129 } else {
130 Self::InMemory
131 }
132 }
133}
134
135struct UniqueHashJoinState {
139 build_rows: HybridRowStore,
140 hash_index: UniqueHashIndex,
141 build_left: bool,
142}
143
144enum UniqueHashIndex {
145 Direct(DirectHashIndex),
148 Encoded(HybridHashIndex),
150}
151
152impl<'a> HashJoin<'a> {
153 #[expect(
154 clippy::too_many_arguments,
155 reason = "keeps join keys and schema aligned"
156 )]
157 pub fn new(
158 left: Box<dyn PhysicalOperator + 'a>,
159 right: Box<dyn PhysicalOperator + 'a>,
160 kind: JoinKind,
161 left_keys: Vec<ScalarExpr>,
162 right_keys: Vec<ScalarExpr>,
163 evaluator: SharedExpressionEvaluator<'a>,
164 left_nulls: ResultRow,
165 right_nulls: ResultRow,
166 ) -> Self {
167 Self::new_with_work_mem(
168 left,
169 right,
170 kind,
171 left_keys,
172 right_keys,
173 evaluator,
174 left_nulls,
175 right_nulls,
176 DEFAULT_JOIN_WORK_MEM_BYTES,
177 )
178 }
179
180 #[expect(
181 clippy::too_many_arguments,
182 reason = "keeps join keys and schema aligned"
183 )]
184 pub fn new_with_work_mem(
185 left: Box<dyn PhysicalOperator + 'a>,
186 right: Box<dyn PhysicalOperator + 'a>,
187 kind: JoinKind,
188 left_keys: Vec<ScalarExpr>,
189 right_keys: Vec<ScalarExpr>,
190 evaluator: SharedExpressionEvaluator<'a>,
191 left_nulls: ResultRow,
192 right_nulls: ResultRow,
193 work_mem_bytes: usize,
194 ) -> Self {
195 Self::new_with_work_mem_and_predicate(
196 left,
197 right,
198 kind,
199 left_keys,
200 right_keys,
201 None,
202 evaluator,
203 left_nulls,
204 right_nulls,
205 work_mem_bytes,
206 )
207 }
208
209 #[expect(
210 clippy::too_many_arguments,
211 reason = "keeps join keys and schema aligned"
212 )]
213 pub fn new_with_work_mem_and_predicate(
214 left: Box<dyn PhysicalOperator + 'a>,
215 right: Box<dyn PhysicalOperator + 'a>,
216 kind: JoinKind,
217 left_keys: Vec<ScalarExpr>,
218 right_keys: Vec<ScalarExpr>,
219 predicate: Option<ScalarExpr>,
220 evaluator: SharedExpressionEvaluator<'a>,
221 left_nulls: ResultRow,
222 right_nulls: ResultRow,
223 work_mem_bytes: usize,
224 ) -> Self {
225 let left_key_positions = simple_key_positions(left.row_schema(), &left_keys);
226 let right_key_positions = simple_key_positions(right.row_schema(), &right_keys);
227 let schema = output_schema(
228 left.row_schema(),
229 right.row_schema(),
230 &left_nulls,
231 &right_nulls,
232 );
233 let left_nulls = PhysicalRow::nulls(left.row_schema().physical_width());
234 let right_nulls = PhysicalRow::nulls(right.row_schema().physical_width());
235 let left_cardinality = left.estimated_cardinality();
236 let right_cardinality = right.estimated_cardinality();
237 let build_left = matches!(kind, JoinKind::Inner)
238 && left_cardinality
239 .zip(right_cardinality)
240 .is_some_and(|(left, right)| left < right);
241 let estimated_cardinality = left_cardinality
242 .zip(right_cardinality)
243 .map(|(left, right)| match kind {
244 JoinKind::Inner => left.max(right),
245 JoinKind::Left => left,
246 JoinKind::Right => right,
247 JoinKind::Full => left.saturating_add(right),
248 JoinKind::Cross => left.saturating_mul(right),
249 });
250 let prepared_predicate = predicate.as_ref().and_then(|predicate| {
251 ProjectedPredicate::compile_with_schema(predicate, &schema, &[])
252 .ok()
253 .flatten()
254 });
255 Self {
256 left,
257 right,
258 kind,
259 left_keys,
260 right_keys,
261 left_key_positions,
262 right_key_positions,
263 predicate,
264 prepared_predicate,
265 evaluator,
266 left_nulls,
267 right_nulls,
268 schema,
269 estimated_cardinality,
270 build_left,
271 work_mem_bytes,
272 output: None,
273 streaming_unique: None,
274 output_spilled: SpillState::InMemory,
275 right_input_spilled: SpillState::InMemory,
276 hash_index_spilled: SpillState::InMemory,
277 }
278 }
279
280 #[expect(
284 clippy::too_many_arguments,
285 reason = "keeps join keys and schema aligned"
286 )]
287 pub fn try_new_with_work_mem_and_predicate(
288 left: Box<dyn PhysicalOperator + 'a>,
289 right: Box<dyn PhysicalOperator + 'a>,
290 kind: JoinKind,
291 left_keys: Vec<ScalarExpr>,
292 right_keys: Vec<ScalarExpr>,
293 predicate: Option<ScalarExpr>,
294 evaluator: SharedExpressionEvaluator<'a>,
295 left_nulls: ResultRow,
296 right_nulls: ResultRow,
297 work_mem_bytes: usize,
298 params: &[uqa_sql::SQLParam],
299 ) -> ExecResult<Self> {
300 let mut join = Self::new_with_work_mem_and_predicate(
301 left,
302 right,
303 kind,
304 left_keys,
305 right_keys,
306 predicate,
307 evaluator,
308 left_nulls,
309 right_nulls,
310 work_mem_bytes,
311 );
312 join.prepared_predicate = join
313 .predicate
314 .as_ref()
315 .map(|predicate| {
316 ProjectedPredicate::compile_with_schema(predicate, &join.schema, params)
317 })
318 .transpose()?
319 .flatten();
320 Ok(join)
321 }
322
323 pub fn output_has_spilled(&self) -> bool {
324 self.output_spilled.is_spilled()
325 }
326
327 pub fn right_input_has_spilled(&self) -> bool {
328 self.right_input_spilled.is_spilled()
329 }
330
331 pub fn hash_index_has_spilled(&self) -> bool {
332 self.hash_index_spilled.is_spilled()
333 }
334
335 pub fn builds_left_input(&self) -> bool {
336 self.build_left
337 }
338
339 fn rebuild_encoded_index(
340 &self,
341 rows: &mut HybridRowStore,
342 expressions: &[ScalarExpr],
343 positions: &[usize],
344 budget_bytes: usize,
345 ) -> ExecResult<HybridHashIndex> {
346 let schema = rows.schema.clone();
347 let mut index = HybridHashIndex::new(budget_bytes);
348 for row_index in 0..rows.len() {
349 let key = rows.with_row(row_index, |row| {
350 self.key(expressions, Some(positions), row, &schema)
351 })?;
352 if let Some(key) = key {
353 index.insert(key, row_index)?;
354 }
355 }
356 Ok(index)
357 }
358
359 #[expect(
360 clippy::too_many_lines,
361 reason = "join driver keeps key layout, NULL policy, and output order aligned"
362 )]
363 fn open_build_left(&mut self, state_budget: usize, output_budget: usize) -> ExecResult<()> {
364 debug_assert!(matches!(self.kind, JoinKind::Inner));
365 let left_budget = state_budget / 2;
366 let hash_budget = state_budget.saturating_sub(left_budget);
367 let left_schema = self.left.row_schema().clone();
368 let mut left = HybridRowStore::new(left_schema, left_budget);
369 let direct_positions = self
370 .predicate
371 .is_none()
372 .then_some(())
373 .and(self.left_key_positions.as_deref())
374 .zip(self.right_key_positions.as_deref());
375 let mut direct_index = direct_positions.map(|_| DirectHashIndex::new(hash_budget));
376 let mut encoded_index = direct_index
377 .is_none()
378 .then(|| HybridHashIndex::new(hash_budget));
379 self.left.open()?;
380 while let Some(batch) = self.left.next()? {
381 for row in batch.rows {
382 let index = left.len();
383 if let (Some(direct), Some((positions, _))) =
384 (direct_index.as_mut(), direct_positions)
385 {
386 if let Some(hash) =
387 positional_key_hash(direct.hasher(), &batch.schema, &row, positions)?
388 {
389 direct.insert(hash, index)?;
390 }
391 } else if let Some(key) = self.key(
392 &self.left_keys,
393 self.left_key_positions.as_deref(),
394 &row,
395 &batch.schema,
396 )? {
397 encoded_index
398 .as_mut()
399 .ok_or_else(|| ExecError::Other("join hash index is missing".into()))?
400 .insert(key, index)?;
401 }
402 left.push(row)?;
403 }
404 }
405 self.right_input_spilled = SpillState::InMemory;
406
407 let mut output = SpillBuffer::new(output_budget);
408 if left.len() == 0 {
409 self.output = Some(output.drain()?);
410 return Ok(());
411 }
412
413 let direct_is_unique = direct_index.as_ref().is_some_and(|direct| {
414 direct_positions.is_some_and(|(positions, _)| {
415 !left.has_spilled() && direct.keys_are_unique(&left, &left.schema, positions)
416 })
417 });
418 if direct_is_unique {
419 self.right.open()?;
420 self.streaming_unique = Some(UniqueHashJoinState {
421 build_rows: left,
422 hash_index: UniqueHashIndex::Direct(
423 direct_index
424 .take()
425 .ok_or_else(|| ExecError::Other("direct join index is missing".into()))?,
426 ),
427 build_left: true,
428 });
429 return Ok(());
430 }
431
432 let mut left_by_key = match encoded_index {
433 Some(index) => index,
434 None => {
435 let (positions, _) = direct_positions
436 .ok_or_else(|| ExecError::Other("direct join positions are missing".into()))?;
437 self.rebuild_encoded_index(&mut left, &self.left_keys, positions, hash_budget)?
438 }
439 };
440 self.hash_index_spilled = left_by_key.has_spilled().into();
441 if self.predicate.is_none() && !left.has_spilled() && left_by_key.is_memory_unique() {
442 self.right.open()?;
443 self.streaming_unique = Some(UniqueHashJoinState {
444 build_rows: left,
445 hash_index: UniqueHashIndex::Encoded(left_by_key),
446 build_left: true,
447 });
448 return Ok(());
449 }
450 let mut pending = Vec::with_capacity(crate::batch::DEFAULT_BATCH_SIZE);
451
452 self.right.open()?;
453 while let Some(batch) = self.right.next()? {
454 for right_row in batch.rows {
455 let Some(key) = self.key(
456 &self.right_keys,
457 self.right_key_positions.as_deref(),
458 &right_row,
459 &batch.schema,
460 )?
461 else {
462 continue;
463 };
464 if self.predicate.is_none() {
465 match left_by_key.memory_match_summary(&key) {
466 Some(MemoryMatchSummary::Absent) => continue,
467 Some(MemoryMatchSummary::Single(index)) => {
468 let merged = left.with_row(index, |left_row| {
469 Ok(PhysicalRow::concat_right_owned(left_row, right_row))
470 })?;
471 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
472 continue;
473 }
474 Some(MemoryMatchSummary::Multiple) | None => {}
475 }
476 }
477 left_by_key.for_each_match(&key, &mut |index| {
478 left.with_row(index, |left_row| {
479 let merged = PhysicalRow::concat(left_row, &right_row);
480 if self.matches(&merged)? {
481 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
482 }
483 Ok(())
484 })
485 })?;
486 }
487 }
488 if !pending.is_empty() {
489 output.push(Batch::from_physical_rows(self.schema.clone(), pending))?;
490 }
491 self.output_spilled = output.has_spilled().into();
492 self.output = Some(output.drain()?);
493 Ok(())
494 }
495
496 fn key(
497 &self,
498 expressions: &[ScalarExpr],
499 positions: Option<&[usize]>,
500 row: &PhysicalRow,
501 schema: &RowSchema,
502 ) -> ExecResult<Option<EncodedKey>> {
503 if let Some(positions) = positions {
504 let view = schema.view(row);
505 return encode_non_null_key(positions.iter().map(|position| view.value_at(*position)));
506 }
507 let mut values = SmallVec::<[Value; 4]>::with_capacity(expressions.len());
508 for expression in expressions {
509 let value = self.evaluator.evaluate_physical(expression, schema, row)?;
510 if matches!(value, Value::Null) {
511 return Ok(None);
512 }
513 values.push(value);
514 }
515 encode_non_null_key(values.iter().map(Some))
516 }
517
518 fn matches(&self, row: &PhysicalRow) -> ExecResult<bool> {
519 if let Some(predicate) = self.prepared_predicate.as_ref() {
520 return Ok(predicate.keep_row(&self.schema.view(row))?);
521 }
522 self.predicate.as_ref().map_or(Ok(true), |predicate| {
523 Ok(truthy(&self.evaluator.evaluate_physical(
524 predicate,
525 &self.schema,
526 row,
527 )?))
528 })
529 }
530
531 fn next_streaming_unique(
532 &mut self,
533 state: &mut UniqueHashJoinState,
534 ) -> ExecResult<Option<Batch>> {
535 loop {
536 let next = if state.build_left {
537 self.right.next()?
538 } else {
539 self.left.next()?
540 };
541 let Some(batch) = next else {
542 return Ok(None);
543 };
544 let mut output = Vec::with_capacity(batch.rows.len());
545 for probe_row in batch.rows {
546 let index = match &state.hash_index {
547 UniqueHashIndex::Direct(index) => {
548 let (build_positions, probe_positions) = if state.build_left {
549 (
550 self.left_key_positions.as_deref(),
551 self.right_key_positions.as_deref(),
552 )
553 } else {
554 (
555 self.right_key_positions.as_deref(),
556 self.left_key_positions.as_deref(),
557 )
558 };
559 let (Some(build_positions), Some(probe_positions)) =
560 (build_positions, probe_positions)
561 else {
562 return Err(ExecError::Other(
563 "direct join key positions are missing".into(),
564 ));
565 };
566 direct_unique_match(
567 index,
568 &state.build_rows,
569 build_positions,
570 &batch.schema,
571 &probe_row,
572 probe_positions,
573 )?
574 }
575 UniqueHashIndex::Encoded(index) => {
576 let expressions = if state.build_left {
577 &self.right_keys
578 } else {
579 &self.left_keys
580 };
581 let positions = if state.build_left {
582 self.right_key_positions.as_deref()
583 } else {
584 self.left_key_positions.as_deref()
585 };
586 let Some(key) =
587 self.key(expressions, positions, &probe_row, &batch.schema)?
588 else {
589 continue;
590 };
591 match index.memory_match_summary(&key) {
592 Some(MemoryMatchSummary::Single(index)) => Some(index),
593 _ => None,
594 }
595 }
596 };
597 let Some(index) = index else { continue };
598 let merged = if state.build_left {
599 state.build_rows.with_row(index, |build_row| {
600 Ok(PhysicalRow::concat_right_owned(build_row, probe_row))
601 })?
602 } else {
603 state.build_rows.with_row(index, |build_row| {
604 Ok(PhysicalRow::concat_left_owned(probe_row, build_row))
605 })?
606 };
607 output.push(merged);
608 }
609 if !output.is_empty() {
610 return Ok(Some(Batch::from_physical_rows(self.schema.clone(), output)));
611 }
612 }
613 }
614}
615
616impl PhysicalOperator for HashJoin<'_> {
617 fn row_schema(&self) -> &RowSchema {
618 &self.schema
619 }
620
621 fn estimated_cardinality(&self) -> Option<u64> {
622 self.estimated_cardinality
623 }
624
625 #[expect(
626 clippy::too_many_lines,
627 reason = "join driver keeps key layout, NULL policy, and output order aligned"
628 )]
629 fn open(&mut self) -> ExecResult<()> {
630 self.output = None;
631 self.streaming_unique = None;
632 self.output_spilled = SpillState::InMemory;
633 self.right_input_spilled = SpillState::InMemory;
634 self.hash_index_spilled = SpillState::InMemory;
635
636 let state_budget = self.work_mem_bytes / 2;
637 let output_budget = self.work_mem_bytes.saturating_sub(state_budget);
638 if self.build_left {
639 return self.open_build_left(state_budget, output_budget);
640 }
641 let right_budget = state_budget / 2;
642 let hash_budget = state_budget.saturating_sub(right_budget);
643 let right_schema = self.right.row_schema().clone();
644 let mut right = HybridRowStore::new(right_schema, right_budget);
645 let direct_positions = (matches!(self.kind, JoinKind::Inner) && self.predicate.is_none())
646 .then_some(())
647 .and(self.right_key_positions.as_deref())
648 .zip(self.left_key_positions.as_deref());
649 let mut direct_index = direct_positions.map(|_| DirectHashIndex::new(hash_budget));
650 let mut encoded_index = direct_index
651 .is_none()
652 .then(|| HybridHashIndex::new(hash_budget));
653 self.right.open()?;
654 while let Some(batch) = self.right.next()? {
655 for row in batch.rows {
656 let index = right.len();
657 if let (Some(direct), Some((positions, _))) =
658 (direct_index.as_mut(), direct_positions)
659 {
660 if let Some(hash) =
661 positional_key_hash(direct.hasher(), &batch.schema, &row, positions)?
662 {
663 direct.insert(hash, index)?;
664 }
665 } else if let Some(key) = self.key(
666 &self.right_keys,
667 self.right_key_positions.as_deref(),
668 &row,
669 &batch.schema,
670 )? {
671 encoded_index
672 .as_mut()
673 .ok_or_else(|| ExecError::Other("join hash index is missing".into()))?
674 .insert(key, index)?;
675 }
676 right.push(row)?;
677 }
678 }
679 self.right_input_spilled = right.has_spilled().into();
680
681 if right.len() == 0 && matches!(self.kind, JoinKind::Inner) {
682 let mut output = SpillBuffer::new(output_budget);
683 self.output = Some(output.drain()?);
684 return Ok(());
685 }
686
687 let direct_is_unique = direct_index.as_ref().is_some_and(|direct| {
688 direct_positions.is_some_and(|(positions, _)| {
689 !right.has_spilled() && direct.keys_are_unique(&right, &right.schema, positions)
690 })
691 });
692 if direct_is_unique {
693 self.left.open()?;
694 self.streaming_unique = Some(UniqueHashJoinState {
695 build_rows: right,
696 hash_index: UniqueHashIndex::Direct(
697 direct_index
698 .take()
699 .ok_or_else(|| ExecError::Other("direct join index is missing".into()))?,
700 ),
701 build_left: false,
702 });
703 return Ok(());
704 }
705
706 let mut right_by_key = match encoded_index {
707 Some(index) => index,
708 None => {
709 let (positions, _) = direct_positions
710 .ok_or_else(|| ExecError::Other("direct join positions are missing".into()))?;
711 self.rebuild_encoded_index(&mut right, &self.right_keys, positions, hash_budget)?
712 }
713 };
714 self.hash_index_spilled = right_by_key.has_spilled().into();
715 if matches!(self.kind, JoinKind::Inner)
716 && self.predicate.is_none()
717 && !right.has_spilled()
718 && right_by_key.is_memory_unique()
719 {
720 self.left.open()?;
721 self.streaming_unique = Some(UniqueHashJoinState {
722 build_rows: right,
723 hash_index: UniqueHashIndex::Encoded(right_by_key),
724 build_left: false,
725 });
726 return Ok(());
727 }
728
729 let mut matched_right = matches!(self.kind, JoinKind::Right | JoinKind::Full)
730 .then(|| MatchFlags::new(right.len()))
731 .transpose()?;
732 let mut output = SpillBuffer::new(output_budget);
733 let mut pending = Vec::with_capacity(crate::batch::DEFAULT_BATCH_SIZE);
734
735 self.left.open()?;
736 while let Some(batch) = self.left.next()? {
737 for left_row in batch.rows {
738 let mut matched_left = false;
739 if let Some(key) = self.key(
740 &self.left_keys,
741 self.left_key_positions.as_deref(),
742 &left_row,
743 &batch.schema,
744 )? {
745 if self.predicate.is_none() {
746 match right_by_key.memory_match_summary(&key) {
747 Some(MemoryMatchSummary::Absent) => {
748 if matches!(self.kind, JoinKind::Left | JoinKind::Full) {
749 push_output_row(
750 &mut output,
751 &mut pending,
752 &self.schema,
753 PhysicalRow::concat_left_owned(left_row, &self.right_nulls),
754 )?;
755 }
756 continue;
757 }
758 Some(MemoryMatchSummary::Single(index)) => {
759 let merged = right.with_row(index, |right_row| {
760 Ok(PhysicalRow::concat_left_owned(left_row, right_row))
761 })?;
762 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
763 if let Some(flags) = matched_right.as_mut() {
764 flags.mark(index)?;
765 }
766 continue;
767 }
768 Some(MemoryMatchSummary::Multiple) | None => {}
769 }
770 }
771 right_by_key.for_each_match(&key, &mut |index| {
772 right.with_row(index, |right_row| {
773 let merged = PhysicalRow::concat(&left_row, right_row);
774 if self.matches(&merged)? {
775 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
776 if let Some(flags) = matched_right.as_mut() {
777 flags.mark(index)?;
778 }
779 matched_left = true;
780 }
781 Ok(())
782 })
783 })?;
784 }
785 if !matched_left && matches!(self.kind, JoinKind::Left | JoinKind::Full) {
786 push_output_row(
787 &mut output,
788 &mut pending,
789 &self.schema,
790 PhysicalRow::concat_left_owned(left_row, &self.right_nulls),
791 )?;
792 }
793 }
794 }
795
796 if matches!(self.kind, JoinKind::Right | JoinKind::Full) {
797 let matched_right = matched_right.as_mut().ok_or_else(|| {
798 ExecError::Other("right/full hash join has no match flags".into())
799 })?;
800 for index in 0..right.len() {
801 if !matched_right.is_marked(index)? {
802 right.with_row(index, |right_row| {
803 push_output_row(
804 &mut output,
805 &mut pending,
806 &self.schema,
807 PhysicalRow::concat(&self.left_nulls, right_row),
808 )
809 })?;
810 }
811 }
812 }
813 if !pending.is_empty() {
814 output.push(Batch::from_physical_rows(self.schema.clone(), pending))?;
815 }
816 self.output_spilled = output.has_spilled().into();
817 self.output = Some(output.drain()?);
818 Ok(())
819 }
820
821 fn next(&mut self) -> ExecResult<Option<Batch>> {
822 if let Some(mut state) = self.streaming_unique.take() {
823 let result = self.next_streaming_unique(&mut state);
824 self.streaming_unique = Some(state);
825 return result;
826 }
827 self.output
828 .as_mut()
829 .map_or(Ok(None), |output| output.next().transpose())
830 }
831
832 fn close(&mut self) -> ExecResult<()> {
833 self.output = None;
834 self.streaming_unique = None;
835 let left = self.left.close();
836 let right = self.right.close();
837 crate::physical::with_cleanup(left, right, "close right hash-join input")
838 }
839}
840
841#[cfg(test)]
842mod tests;