Skip to main content

linkrs_query/executor/streaming/operators/
shuffle_join_operator.rs

1use 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    /// Index of the current left row being probed.
29    current_left_index: usize,
30    /// For hash-key join: offset within the current left row's matching_indices
31    /// that have already been emitted.  None means start from the beginning.
32    current_match_offset: Option<usize>,
33    /// For cross join (no keys): the next right row index to process for the
34    /// current left row.
35    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    /// Inject the runtime and execution config (called once by the executor
104    /// before this operator produces any data).
105    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                // Cross join path: iterate right rows starting from saved cursor.
267                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                // All right rows processed for this left row.
290                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                    // All match indices processed for this left row.
335                    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}