linkrs_query/executor/streaming/operators/
shuffle_join_operator.rs1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::executor::base::MemoryTracker;
5use crate::executor::streaming::chunk::DataChunk;
6use crate::executor::streaming::executor::StreamingExecutor;
7use crate::executor::streaming::join_helpers::{evaluate_join_key, evaluate_residual_condition};
8use crate::executor::streaming::operators::source_operator::OperatorConfig;
9use crate::executor::streaming::runtime::ExecutionRuntime;
10use crate::executor::streaming::slot::SlotLayout;
11use linkrs_core::error::QueryError;
12use linkrs_core::types::expr::Expression;
13use linkrs_core::Value;
14
15const CHUNK_SIZE: usize = 2048;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum HashJoinKind {
19 Inner,
20 Left,
21}
22
23#[derive(Debug)]
24struct Bucket {
25 left_rows: Vec<Vec<Value>>,
26 right_rows: Vec<Vec<Value>>,
27 right_hash: Option<HashMap<Vec<Value>, Vec<usize>>>,
28 current_left_index: usize,
30 current_match_offset: Option<usize>,
33 current_right_index: usize,
36}
37
38#[derive(Debug)]
39enum ShufflePhase {
40 Building,
41 Processing(usize),
42 Exhausted,
43}
44
45#[derive(Debug)]
46pub struct HashShuffleJoinOperator {
47 pub join_kind: HashJoinKind,
48 pub left_key_expressions: Vec<Expression>,
49 pub right_key_expressions: Vec<Expression>,
50 pub join_condition: Option<Expression>,
51 pub bucket_count: usize,
52 pub left_schema: Vec<String>,
53 pub right_schema: Vec<String>,
54 pub memory_tracker: MemoryTracker,
55 pub runtime: Option<Arc<ExecutionRuntime>>,
56 pub output_layout: Arc<SlotLayout>,
57 pub config: OperatorConfig,
58 buckets: Vec<Bucket>,
59 phase: ShufflePhase,
60}
61
62impl HashShuffleJoinOperator {
63 #[allow(clippy::too_many_arguments)]
64 pub fn new(
65 join_kind: HashJoinKind,
66 left_key_expressions: Vec<Expression>,
67 right_key_expressions: Vec<Expression>,
68 join_condition: Option<Expression>,
69 bucket_count: usize,
70 left_schema: Vec<String>,
71 right_schema: Vec<String>,
72 memory_tracker: MemoryTracker,
73 output_layout: Arc<SlotLayout>,
74 ) -> Self {
75 let mut buckets = Vec::with_capacity(bucket_count);
76 for _ in 0..bucket_count {
77 buckets.push(Bucket {
78 left_rows: Vec::new(),
79 right_rows: Vec::new(),
80 right_hash: None,
81 current_left_index: 0,
82 current_match_offset: None,
83 current_right_index: 0,
84 });
85 }
86 Self {
87 join_kind,
88 left_key_expressions,
89 right_key_expressions,
90 join_condition,
91 bucket_count,
92 left_schema,
93 right_schema,
94 memory_tracker,
95 runtime: None,
96 output_layout,
97 config: OperatorConfig::default(),
98 buckets,
99 phase: ShufflePhase::Building,
100 }
101 }
102
103 pub fn inject_context(
106 &mut self,
107 runtime: Option<&Arc<ExecutionRuntime>>,
108 config: OperatorConfig,
109 ) {
110 if let Some(rt) = runtime {
111 self.runtime = Some(rt.clone());
112 }
113 self.config = config;
114 }
115
116 pub fn open(
117 &mut self,
118 left: &mut [StreamingExecutor],
119 right: &mut [StreamingExecutor],
120 ) -> Result<(), QueryError> {
121 for tree in left.iter_mut() {
122 tree.open()?;
123 }
124 for tree in right.iter_mut() {
125 tree.open()?;
126 }
127 Ok(())
128 }
129
130 fn hash_row_to_bucket(
131 row: &[Value],
132 col_names: &[String],
133 key_expressions: &[Expression],
134 bucket_count: usize,
135 ) -> Result<usize, QueryError> {
136 if key_expressions.is_empty() || bucket_count == 0 {
137 return Ok(0);
138 }
139 let layout = Arc::new(SlotLayout::from_names(col_names));
140 let key = evaluate_join_key(row, layout, key_expressions)?;
141 let hash = Self::hash_values(&key);
142 Ok((hash % bucket_count as u64) as usize)
143 }
144
145 fn hash_values(values: &[Value]) -> u64 {
146 use std::hash::{Hash, Hasher};
147 let mut hasher = std::collections::hash_map::DefaultHasher::new();
148 values.hash(&mut hasher);
149 hasher.finish()
150 }
151
152 fn read_all_trees(
153 &mut self,
154 left_trees: &mut [StreamingExecutor],
155 right_trees: &mut [StreamingExecutor],
156 ) -> Result<(), QueryError> {
157 for tree in left_trees.iter_mut() {
158 while let Some(mut chunk) = tree.advance()? {
159 chunk.normalize_for_opaque("ShuffleJoin");
160 if let Some(rt) = &self.runtime {
161 rt.ensure_not_cancelled()?;
162 }
163 let col_names = chunk.col_names();
164 for row in &chunk.rows {
165 self.memory_tracker.try_reserve_row(row)?;
166 let bucket_idx = Self::hash_row_to_bucket(
167 row,
168 &col_names,
169 &self.left_key_expressions,
170 self.bucket_count,
171 )?;
172 self.buckets[bucket_idx].left_rows.push(row.clone());
173 }
174 }
175 }
176 for tree in right_trees.iter_mut() {
177 while let Some(mut chunk) = tree.advance()? {
178 chunk.normalize_for_opaque("ShuffleJoin");
179 if let Some(rt) = &self.runtime {
180 rt.ensure_not_cancelled()?;
181 }
182 let col_names = chunk.col_names();
183 for row in &chunk.rows {
184 self.memory_tracker.try_reserve_row(row)?;
185 let bucket_idx = Self::hash_row_to_bucket(
186 row,
187 &col_names,
188 &self.right_key_expressions,
189 self.bucket_count,
190 )?;
191 self.buckets[bucket_idx].right_rows.push(row.clone());
192 }
193 }
194 }
195 Ok(())
196 }
197
198 pub fn next(
199 &mut self,
200 left_trees: &mut [StreamingExecutor],
201 right_trees: &mut [StreamingExecutor],
202 ) -> Result<Option<DataChunk>, QueryError> {
203 loop {
204 match self.phase {
205 ShufflePhase::Building => {
206 self.read_all_trees(left_trees, right_trees)?;
207 self.phase = ShufflePhase::Processing(0);
208 }
209 ShufflePhase::Processing(current) => {
210 if current >= self.bucket_count {
211 self.phase = ShufflePhase::Exhausted;
212 return Ok(None);
213 }
214 if let Some(chunk) = self.process_bucket_chunked(current)? {
215 return Ok(Some(chunk));
216 }
217 self.release_bucket(current);
218 self.phase = ShufflePhase::Processing(current + 1);
219 }
220 ShufflePhase::Exhausted => return Ok(None),
221 }
222 }
223 }
224
225 fn release_bucket(&mut self, idx: usize) {
226 let bucket = &mut self.buckets[idx];
227 bucket.left_rows.clear();
228 bucket.right_rows.clear();
229 bucket.right_hash = None;
230 bucket.current_left_index = 0;
231 bucket.current_match_offset = None;
232 bucket.current_right_index = 0;
233 }
234
235 fn process_bucket_chunked(
236 &mut self,
237 bucket_idx: usize,
238 ) -> Result<Option<DataChunk>, QueryError> {
239 let output_layout = &self.output_layout;
240 let bucket = &mut self.buckets[bucket_idx];
241
242 if bucket.right_hash.is_none() && !bucket.right_rows.is_empty() {
243 let mut hash: HashMap<Vec<Value>, Vec<usize>> = HashMap::new();
244 for (idx, row) in bucket.right_rows.iter().enumerate() {
245 let right_layout = Arc::new(SlotLayout::from_names(&self.right_schema));
246 let key = evaluate_join_key(row, right_layout, &self.right_key_expressions)?;
247 hash.entry(key).or_default().push(idx);
248 }
249 bucket.right_hash = Some(hash);
250 }
251
252 let right_width = output_layout
253 .len()
254 .checked_sub(self.left_schema.len())
255 .ok_or_else(|| {
256 QueryError::execution(
257 "HashShuffleJoin output layout is narrower than its left input".to_string(),
258 )
259 })?;
260 let mut chunk_rows = Vec::with_capacity(CHUNK_SIZE);
261
262 while bucket.current_left_index < bucket.left_rows.len() {
263 let left_row = &bucket.left_rows[bucket.current_left_index];
264
265 if self.left_key_expressions.is_empty() {
266 while bucket.current_right_index < bucket.right_rows.len() {
268 let right_row = &bucket.right_rows[bucket.current_right_index];
269 if Self::check_condition(
270 &self.join_condition,
271 left_row,
272 right_row,
273 &self.left_schema,
274 &self.right_schema,
275 )? {
276 let mut joined = left_row.clone();
277 joined.extend(right_row.clone());
278 chunk_rows.push(joined);
279 if chunk_rows.len() >= CHUNK_SIZE {
280 bucket.current_right_index += 1;
281 return Ok(Some(DataChunk::new_with_layout(
282 chunk_rows,
283 Arc::clone(output_layout),
284 )));
285 }
286 }
287 bucket.current_right_index += 1;
288 }
289 if self.join_kind == HashJoinKind::Left && bucket.right_rows.is_empty() {
291 let mut joined = left_row.clone();
292 joined.extend(vec![
293 Value::Null(linkrs_core::value::NullType::Null);
294 right_width
295 ]);
296 chunk_rows.push(joined);
297 if chunk_rows.len() >= CHUNK_SIZE {
298 bucket.current_left_index += 1;
299 bucket.current_right_index = 0;
300 return Ok(Some(DataChunk::new_with_layout(
301 chunk_rows,
302 Arc::clone(output_layout),
303 )));
304 }
305 }
306 bucket.current_right_index = 0;
307 } else if let Some(hash) = bucket.right_hash.as_ref() {
308 let left_layout = Arc::new(SlotLayout::from_names(&self.left_schema));
309 let probe_key =
310 evaluate_join_key(left_row, left_layout, &self.left_key_expressions)?;
311 if let Some(matching_indices) = hash.get(&probe_key) {
312 let start_offset = bucket.current_match_offset.unwrap_or(0);
313 for (i, &right_idx) in matching_indices.iter().enumerate().skip(start_offset) {
314 let right_row = &bucket.right_rows[right_idx];
315 if Self::check_condition(
316 &self.join_condition,
317 left_row,
318 right_row,
319 &self.left_schema,
320 &self.right_schema,
321 )? {
322 let mut joined = left_row.clone();
323 joined.extend(right_row.clone());
324 chunk_rows.push(joined);
325 if chunk_rows.len() >= CHUNK_SIZE {
326 bucket.current_match_offset = Some(i + 1);
327 return Ok(Some(DataChunk::new_with_layout(
328 chunk_rows,
329 Arc::clone(output_layout),
330 )));
331 }
332 }
333 }
334 bucket.current_match_offset = None;
336 } else if self.join_kind == HashJoinKind::Left {
337 let mut joined = left_row.clone();
338 joined.extend(vec![
339 Value::Null(linkrs_core::value::NullType::Null);
340 right_width
341 ]);
342 chunk_rows.push(joined);
343 if chunk_rows.len() >= CHUNK_SIZE {
344 bucket.current_left_index += 1;
345 return Ok(Some(DataChunk::new_with_layout(
346 chunk_rows,
347 Arc::clone(output_layout),
348 )));
349 }
350 }
351 } else if self.join_kind == HashJoinKind::Left {
352 let mut joined = left_row.clone();
353 joined.extend(vec![
354 Value::Null(linkrs_core::value::NullType::Null);
355 right_width
356 ]);
357 chunk_rows.push(joined);
358 if chunk_rows.len() >= CHUNK_SIZE {
359 bucket.current_left_index += 1;
360 return Ok(Some(DataChunk::new_with_layout(
361 chunk_rows,
362 Arc::clone(output_layout),
363 )));
364 }
365 }
366
367 bucket.current_left_index += 1;
368 }
369
370 if chunk_rows.is_empty() {
371 Ok(None)
372 } else {
373 Ok(Some(DataChunk::new_with_layout(
374 chunk_rows,
375 Arc::clone(output_layout),
376 )))
377 }
378 }
379
380 fn check_condition(
381 join_condition: &Option<Expression>,
382 left_row: &[Value],
383 right_row: &[Value],
384 left_schema: &[String],
385 right_schema: &[String],
386 ) -> Result<bool, QueryError> {
387 match join_condition {
388 None => Ok(true),
389 Some(condition) => evaluate_residual_condition(
390 condition,
391 left_row,
392 right_row,
393 left_schema,
394 right_schema,
395 ),
396 }
397 }
398
399 pub fn stop(&mut self) -> Result<(), QueryError> {
400 Ok(())
401 }
402
403 pub fn close(&mut self) -> Result<(), QueryError> {
404 self.memory_tracker.reset();
405 self.buckets.clear();
406 Ok(())
407 }
408
409 pub fn memory_tracker(&self) -> &MemoryTracker {
410 &self.memory_tracker
411 }
412}