1use uqa_sql::ast::JoinKind;
10use uqa_sql::expr::truthy;
11use uqa_sql::ResultRow;
12
13use super::{
14 output_schema, push_output_row, HybridRowStore, MatchFlags, DEFAULT_JOIN_WORK_MEM_BYTES,
15};
16use crate::{
17 Batch, ExecError, ExecResult, PhysicalOperator, PhysicalRow, RowSchema, ScalarExpr,
18 SharedExpressionEvaluator, SpillBuffer,
19};
20
21pub struct NestedLoopJoin<'a> {
26 left: Box<dyn PhysicalOperator + 'a>,
27 right: Box<dyn PhysicalOperator + 'a>,
28 kind: JoinKind,
29 predicate: Option<ScalarExpr>,
30 evaluator: SharedExpressionEvaluator<'a>,
31 left_nulls: PhysicalRow,
32 right_nulls: PhysicalRow,
33 schema: RowSchema,
34 work_mem_bytes: usize,
35 output: Option<crate::spill::SpillDrain>,
36 output_spilled: bool,
37 right_input_spilled: bool,
38}
39
40impl<'a> NestedLoopJoin<'a> {
41 #[allow(clippy::too_many_arguments)]
42 pub fn new(
43 left: Box<dyn PhysicalOperator + 'a>,
44 right: Box<dyn PhysicalOperator + 'a>,
45 kind: JoinKind,
46 predicate: Option<ScalarExpr>,
47 evaluator: SharedExpressionEvaluator<'a>,
48 left_nulls: ResultRow,
49 right_nulls: ResultRow,
50 ) -> Self {
51 Self::new_with_work_mem(
52 left,
53 right,
54 kind,
55 predicate,
56 evaluator,
57 left_nulls,
58 right_nulls,
59 DEFAULT_JOIN_WORK_MEM_BYTES,
60 )
61 }
62
63 #[allow(clippy::too_many_arguments)]
64 pub fn new_with_work_mem(
65 left: Box<dyn PhysicalOperator + 'a>,
66 right: Box<dyn PhysicalOperator + 'a>,
67 kind: JoinKind,
68 predicate: Option<ScalarExpr>,
69 evaluator: SharedExpressionEvaluator<'a>,
70 left_nulls: ResultRow,
71 right_nulls: ResultRow,
72 work_mem_bytes: usize,
73 ) -> Self {
74 let schema = output_schema(
75 left.row_schema(),
76 right.row_schema(),
77 &left_nulls,
78 &right_nulls,
79 );
80 let left_nulls = PhysicalRow::nulls(left.row_schema().physical_width());
81 let right_nulls = PhysicalRow::nulls(right.row_schema().physical_width());
82 Self {
83 left,
84 right,
85 kind,
86 predicate,
87 evaluator,
88 left_nulls,
89 right_nulls,
90 schema,
91 work_mem_bytes,
92 output: None,
93 output_spilled: false,
94 right_input_spilled: false,
95 }
96 }
97
98 pub fn output_has_spilled(&self) -> bool {
99 self.output_spilled
100 }
101
102 pub fn right_input_has_spilled(&self) -> bool {
105 self.right_input_spilled
106 }
107
108 fn matches(&self, row: &PhysicalRow) -> ExecResult<bool> {
109 match self.predicate.as_ref() {
110 None => Ok(true),
111 Some(predicate) => Ok(truthy(&self.evaluator.evaluate_physical(
112 predicate,
113 &self.schema,
114 row,
115 )?)),
116 }
117 }
118}
119
120impl PhysicalOperator for NestedLoopJoin<'_> {
121 fn row_schema(&self) -> &RowSchema {
122 &self.schema
123 }
124
125 fn open(&mut self) -> ExecResult<()> {
126 self.output = None;
127 self.output_spilled = false;
128 self.right_input_spilled = false;
129
130 let right_budget = self.work_mem_bytes / 2;
131 let output_budget = self.work_mem_bytes.saturating_sub(right_budget);
132 let right_schema = self.right.row_schema().clone();
133 let mut right = HybridRowStore::new(right_schema, right_budget);
134 self.right.open()?;
135 while let Some(batch) = self.right.next()? {
136 for row in batch.rows {
137 right.push(row)?;
138 }
139 }
140 self.right_input_spilled = right.has_spilled();
141 let mut matched_right = matches!(self.kind, JoinKind::Right | JoinKind::Full)
142 .then(|| MatchFlags::new(right.len()))
143 .transpose()?;
144 let mut output = SpillBuffer::new(output_budget);
145 let mut pending = Vec::with_capacity(crate::batch::DEFAULT_BATCH_SIZE);
146
147 self.left.open()?;
148 while let Some(batch) = self.left.next()? {
149 for left_row in batch.rows {
150 let mut matched_left = false;
151 for right_index in 0..right.len() {
152 right.with_row(right_index, |right_row| {
153 let merged = PhysicalRow::concat(&left_row, right_row);
154 if self.matches(&merged)? {
155 push_output_row(&mut output, &mut pending, &self.schema, merged)?;
156 matched_left = true;
157 if let Some(flags) = matched_right.as_mut() {
158 flags.mark(right_index)?;
159 }
160 }
161 Ok(())
162 })?;
163 }
164 if !matched_left && matches!(self.kind, JoinKind::Left | JoinKind::Full) {
165 push_output_row(
166 &mut output,
167 &mut pending,
168 &self.schema,
169 PhysicalRow::concat_left_owned(left_row, &self.right_nulls),
170 )?;
171 }
172 }
173 }
174
175 if matches!(self.kind, JoinKind::Right | JoinKind::Full) {
176 let matched_right = matched_right.as_mut().ok_or_else(|| {
177 ExecError::Other("right/full nested-loop join has no match flags".into())
178 })?;
179 for right_index in 0..right.len() {
180 if !matched_right.is_marked(right_index)? {
181 right.with_row(right_index, |right_row| {
182 push_output_row(
183 &mut output,
184 &mut pending,
185 &self.schema,
186 PhysicalRow::concat(&self.left_nulls, right_row),
187 )
188 })?;
189 }
190 }
191 }
192 if !pending.is_empty() {
193 output.push(Batch::from_physical_rows(self.schema.clone(), pending))?;
194 }
195 self.output_spilled = output.has_spilled();
196 self.output = Some(output.drain()?);
197 Ok(())
198 }
199
200 fn next(&mut self) -> ExecResult<Option<Batch>> {
201 self.output
202 .as_mut()
203 .map_or(Ok(None), |output| output.next().transpose())
204 }
205
206 fn close(&mut self) -> ExecResult<()> {
207 self.output = None;
208 let left = self.left.close();
209 let right = self.right.close();
210 crate::physical::with_cleanup(left, right, "close right nested-loop join input")
211 }
212}