Skip to main content

linkrs_query/executor/streaming/operators/
exchange_operator.rs

1use std::cmp::Ordering;
2use std::collections::hash_map::DefaultHasher;
3use std::hash::{Hash, Hasher};
4use std::sync::Arc;
5use std::time::Instant;
6
7use crate::executor::expression::evaluator::ExpressionEvaluator;
8use crate::executor::streaming::chunk::DataChunk;
9use crate::executor::streaming::executor::{SortDirection, StreamingExecutor, ValueRowContext};
10use crate::executor::streaming::helpers::compare_values;
11use crate::executor::streaming::operators::source_operator::OperatorConfig;
12use crate::executor::streaming::operators::spec::ExchangeSpec;
13use crate::executor::streaming::operators::state::{ExchangeState, MergeInputState};
14use crate::executor::streaming::pool::{PartitionBatch, PartitionHandle};
15use crate::executor::streaming::runtime::ExecutionRuntime;
16use crate::executor::streaming::slot::SlotLayout;
17use linkrs_core::error::QueryError;
18use linkrs_core::types::expr::Expression;
19use linkrs_core::Value;
20
21const CHUNK_SIZE: usize = 2048;
22
23/// Exchange operator.
24///
25/// Wraps [`ExchangeState`] with the runtime context injected at `open()`.
26/// Lifecycle state is owned exclusively by the executor; operators never
27/// write it.
28#[derive(Debug)]
29pub struct ExchangeOperator {
30    pub state: ExchangeState,
31    pub(crate) handle: Option<PartitionHandle>,
32    pub runtime: Option<Arc<ExecutionRuntime>>,
33    pub output_layout: Arc<SlotLayout>,
34    pub config: OperatorConfig,
35}
36
37impl ExchangeOperator {
38    pub fn from_spec(spec: &ExchangeSpec, output_layout: Arc<SlotLayout>) -> Self {
39        let state = match spec {
40            ExchangeSpec::Concatenate { .. } => ExchangeState::Concatenate {
41                current_index: 0,
42                col_names: None,
43            },
44            ExchangeSpec::MergeSort {
45                sort_expressions,
46                sort_directions,
47                limit,
48            } => ExchangeState::MergeSort {
49                sort_expressions: sort_expressions.clone(),
50                sort_directions: sort_directions.clone(),
51                inputs: Vec::new(),
52                col_names: None,
53                limit: *limit,
54                emitted: 0,
55            },
56            ExchangeSpec::RepartitionHash {
57                num_partitions,
58                hash_expressions,
59                ..
60            } => ExchangeState::RepartitionHash {
61                num_partitions: *num_partitions,
62                buckets: (0..*num_partitions).map(|_| Vec::new()).collect(),
63                current_bucket: 0,
64                current_row: 0,
65                hash_expressions: hash_expressions.clone(),
66                col_names: None,
67            },
68            ExchangeSpec::Broadcast { num_consumers } => ExchangeState::Broadcast {
69                num_consumers: *num_consumers,
70                buffered_chunks: Vec::new(),
71                current_consumer: 0,
72                chunk_index: 0,
73                row_index: 0,
74            },
75            ExchangeSpec::Barrier => ExchangeState::Barrier { passed: false },
76            ExchangeSpec::Materialize { child_count: _ } => ExchangeState::Materialize {
77                rows: Vec::new(),
78                position: 0,
79                col_names: None,
80            },
81        };
82        Self::new(state, output_layout)
83    }
84
85    pub fn new(kind: ExchangeState, output_layout: Arc<SlotLayout>) -> Self {
86        Self {
87            state: kind,
88            handle: None,
89            runtime: None,
90            output_layout,
91            config: OperatorConfig::default(),
92        }
93    }
94
95    /// Inject the runtime and execution config (called once by the executor
96    /// before this operator produces any data).
97    pub fn inject_context(
98        &mut self,
99        runtime: Option<&Arc<ExecutionRuntime>>,
100        config: OperatorConfig,
101    ) {
102        if let Some(rt) = runtime {
103            self.runtime = Some(rt.clone());
104        }
105        self.config = config;
106    }
107
108    pub fn open(&mut self, children: &mut Vec<StreamingExecutor>) -> Result<(), QueryError> {
109        match &mut self.state {
110            ExchangeState::Concatenate { .. } => {}
111            ExchangeState::MergeSort {
112                inputs, emitted, ..
113            } => {
114                *inputs = (0..children.len())
115                    .map(|_| MergeInputState::Pending)
116                    .collect();
117                *emitted = 0;
118            }
119            ExchangeState::RepartitionHash {
120                buckets,
121                current_bucket,
122                current_row,
123                ..
124            } => {
125                for bucket in buckets.iter_mut() {
126                    bucket.clear();
127                }
128                *current_bucket = 0;
129                *current_row = 0;
130            }
131            ExchangeState::Broadcast {
132                buffered_chunks,
133                current_consumer,
134                chunk_index,
135                row_index,
136                ..
137            } => {
138                buffered_chunks.clear();
139                *current_consumer = 0;
140                *chunk_index = 0;
141                *row_index = 0;
142            }
143            ExchangeState::Barrier { passed } => {
144                *passed = false;
145            }
146            ExchangeState::Materialize {
147                rows,
148                position,
149                col_names,
150            } => {
151                rows.clear();
152                *position = 0;
153                *col_names = None;
154            }
155        }
156
157        let runtime = self.runtime.clone();
158        if let Some(rt) = &runtime {
159            let pool = rt.worker_pool.lock().clone();
160            if let Some(pool) = pool {
161                if children.len() > 1 && pool.max_workers() > 1 {
162                    let max_buffered = self.config.chunk_size.clamp(1, 10);
163                    let (batch, receivers, error_rx) =
164                        PartitionBatch::new(std::mem::take(children), rt.clone(), max_buffered);
165                    let batch = Arc::new(batch);
166                    let handle = PartitionHandle::from_batch(
167                        &batch,
168                        receivers,
169                        error_rx,
170                        rt.clone(),
171                        Instant::now(),
172                        pool.max_workers(),
173                    );
174                    pool.submit(batch);
175                    self.handle = Some(handle);
176                    return Ok(());
177                }
178            }
179        }
180
181        for child in children.iter_mut() {
182            child.open()?;
183        }
184        Ok(())
185    }
186
187    pub fn next(
188        &mut self,
189        children: &mut [StreamingExecutor],
190    ) -> Result<Option<DataChunk>, QueryError> {
191        if let Some(rt) = self.runtime.as_ref() {
192            rt.ensure_not_cancelled()?;
193        }
194        match &mut self.state {
195            ExchangeState::Concatenate {
196                current_index,
197                col_names,
198            } => {
199                while *current_index < input_count(children, &self.handle) {
200                    if let Some(rt) = self.runtime.as_ref() {
201                        rt.ensure_not_cancelled()?;
202                    }
203                    if let Some(chunk) = advance_input(children, &mut self.handle, *current_index)?
204                    {
205                        if chunk.is_empty() {
206                            continue;
207                        }
208                        validate_schema(*current_index, &chunk, col_names)?;
209                        return Ok(Some(DataChunk::new_with_layout(
210                            chunk.rows,
211                            Arc::clone(&self.output_layout),
212                        )));
213                    }
214                    *current_index += 1;
215                }
216                Ok(None)
217            }
218            ExchangeState::MergeSort {
219                sort_expressions,
220                sort_directions,
221                inputs,
222                col_names,
223                limit,
224                emitted,
225            } => {
226                if limit.is_some_and(|value| *emitted >= value) {
227                    return Ok(None);
228                }
229
230                let mut result_rows = Vec::with_capacity(CHUNK_SIZE);
231                while result_rows.len() < CHUNK_SIZE {
232                    if let Some(rt) = self.runtime.as_ref() {
233                        rt.ensure_not_cancelled()?;
234                    }
235                    if limit.is_some_and(|value| *emitted >= value) {
236                        break;
237                    }
238                    match next_merge_row(
239                        &self.runtime,
240                        children,
241                        &mut self.handle,
242                        sort_expressions,
243                        sort_directions,
244                        inputs,
245                        col_names,
246                    )? {
247                        Some(row) => {
248                            result_rows.push(row);
249                            *emitted += 1;
250                        }
251                        None => break,
252                    }
253                }
254
255                if result_rows.is_empty() {
256                    Ok(None)
257                } else {
258                    Ok(Some(DataChunk::new_with_layout(
259                        result_rows,
260                        Arc::clone(&self.output_layout),
261                    )))
262                }
263            }
264            ExchangeState::RepartitionHash {
265                num_partitions,
266                buckets,
267                current_bucket,
268                current_row,
269                hash_expressions,
270                col_names,
271            } => {
272                // Drain all children and partition into buckets.
273                if *current_bucket == 0 && *current_row == 0 && buckets.iter().all(|b| b.is_empty())
274                {
275                    drain_and_partition(
276                        children,
277                        &mut self.handle,
278                        *num_partitions,
279                        buckets,
280                        hash_expressions,
281                        col_names,
282                        &self.runtime,
283                    )?;
284                }
285
286                // Emit rows bucket by bucket.
287                while *current_bucket < *num_partitions {
288                    if let Some(rt) = self.runtime.as_ref() {
289                        rt.ensure_not_cancelled()?;
290                    }
291                    if *current_row < buckets[*current_bucket].len() {
292                        let mut result_rows = Vec::with_capacity(CHUNK_SIZE);
293                        while *current_row < buckets[*current_bucket].len()
294                            && result_rows.len() < CHUNK_SIZE
295                        {
296                            result_rows.push(buckets[*current_bucket][*current_row].clone());
297                            *current_row += 1;
298                        }
299                        return Ok(Some(DataChunk::new_with_layout(
300                            result_rows,
301                            Arc::clone(&self.output_layout),
302                        )));
303                    }
304                    *current_bucket += 1;
305                    *current_row = 0;
306                }
307                Ok(None)
308            }
309            ExchangeState::Broadcast {
310                num_consumers,
311                buffered_chunks,
312                current_consumer,
313                chunk_index,
314                row_index,
315            } => {
316                // Drain all children into chunks if not yet drained.
317                if buffered_chunks.is_empty() && *chunk_index == 0 && *row_index == 0 {
318                    let count = input_count(children, &self.handle);
319                    for i in 0..count {
320                        loop {
321                            if let Some(rt) = self.runtime.as_ref() {
322                                rt.ensure_not_cancelled()?;
323                            }
324                            match advance_input(children, &mut self.handle, i)? {
325                                Some(chunk) if !chunk.is_empty() => buffered_chunks.push(chunk),
326                                Some(_) => continue,
327                                None => break,
328                            }
329                        }
330                    }
331                }
332
333                if buffered_chunks.is_empty() {
334                    return Ok(None);
335                }
336
337                // Emit rows for the current consumer.
338                let mut result_rows = Vec::with_capacity(CHUNK_SIZE);
339                while *chunk_index < buffered_chunks.len() && result_rows.len() < CHUNK_SIZE {
340                    if let Some(rt) = self.runtime.as_ref() {
341                        rt.ensure_not_cancelled()?;
342                    }
343                    let chunk = &buffered_chunks[*chunk_index];
344                    while *row_index < chunk.rows.len() && result_rows.len() < CHUNK_SIZE {
345                        result_rows.push(chunk.rows[*row_index].clone());
346                        *row_index += 1;
347                    }
348                    if *row_index >= chunk.rows.len() {
349                        *chunk_index += 1;
350                        *row_index = 0;
351                    }
352                }
353
354                if result_rows.is_empty() {
355                    return Ok(None);
356                }
357                let result =
358                    DataChunk::new_with_layout(result_rows, Arc::clone(&self.output_layout));
359
360                // Advance consumer. When all consumers have been served, reset
361                // chunk tracking so the next consumer starts from the beginning.
362                *current_consumer += 1;
363                if *current_consumer >= *num_consumers {
364                    *current_consumer = 0;
365                    *chunk_index = 0;
366                    *row_index = 0;
367                }
368
369                Ok(Some(result))
370            }
371            ExchangeState::Barrier { passed } => {
372                // Barrier: consume all children, then pass through one EOF marker.
373                if !*passed {
374                    // Drain all input children.
375                    let mut all_rows = Vec::new();
376                    let mut col_names: Option<Vec<String>> = None;
377                    loop {
378                        if let Some(rt) = self.runtime.as_ref() {
379                            rt.ensure_not_cancelled()?;
380                        }
381                        match advance_input(children, &mut self.handle, 0)? {
382                            Some(mut chunk) => {
383                                chunk.normalize_for_opaque("Exchange");
384                                if col_names.is_none() {
385                                    col_names = Some(chunk.col_names());
386                                }
387                                all_rows.extend(chunk.rows);
388                            }
389                            None => break,
390                        }
391                    }
392                    *passed = true;
393                    if all_rows.is_empty() {
394                        return Ok(None);
395                    }
396                    return Ok(Some(DataChunk::new_with_layout(
397                        all_rows,
398                        Arc::clone(&self.output_layout),
399                    )));
400                }
401                Ok(None)
402            }
403            ExchangeState::Materialize {
404                rows,
405                position,
406                col_names,
407            } => {
408                // Drain all children if not yet drained.
409                if rows.is_empty() && *position == 0 {
410                    let count = input_count(children, &self.handle);
411                    for i in 0..count {
412                        loop {
413                            if let Some(rt) = self.runtime.as_ref() {
414                                rt.ensure_not_cancelled()?;
415                            }
416                            match advance_input(children, &mut self.handle, i)? {
417                                Some(mut chunk) => {
418                                    chunk.normalize_for_opaque("Exchange");
419                                    if col_names.is_none() {
420                                        *col_names = Some(chunk.col_names());
421                                    }
422                                    rows.extend(chunk.rows);
423                                }
424                                None => break,
425                            }
426                        }
427                    }
428                }
429
430                if *position >= rows.len() {
431                    return Ok(None);
432                }
433
434                let mut result_rows = Vec::with_capacity(CHUNK_SIZE);
435                while *position < rows.len() && result_rows.len() < CHUNK_SIZE {
436                    result_rows.push(rows[*position].clone());
437                    *position += 1;
438                }
439
440                Ok(Some(DataChunk::new_with_layout(
441                    result_rows,
442                    Arc::clone(&self.output_layout),
443                )))
444            }
445        }
446    }
447
448    pub fn stop(&mut self) -> Result<(), QueryError> {
449        if let Some(handle) = self.handle.as_mut() {
450            return handle.stop_and_join();
451        }
452        Ok(())
453    }
454
455    pub fn close(&mut self) -> Result<(), QueryError> {
456        if let Some(handle) = self.handle.as_mut() {
457            let _ = handle.stop_and_join();
458            self.handle = None;
459        }
460        Ok(())
461    }
462}
463
464// ── Drain helpers ──────────────────────────────────────────────────────────
465
466/// Drain children and partition rows into hash buckets.
467#[allow(clippy::too_many_arguments)]
468fn drain_and_partition(
469    children: &mut [StreamingExecutor],
470    handle: &mut Option<PartitionHandle>,
471    num_partitions: usize,
472    buckets: &mut [Vec<Vec<Value>>],
473    hash_expressions: &[Expression],
474    col_names: &mut Option<Vec<String>>,
475    runtime: &Option<Arc<ExecutionRuntime>>,
476) -> Result<(), QueryError> {
477    let count = input_count(children, handle);
478    for i in 0..count {
479        loop {
480            if let Some(rt) = runtime.as_ref() {
481                rt.ensure_not_cancelled()?;
482            }
483            match advance_input(children, handle, i)? {
484                Some(mut chunk) => {
485                    chunk.normalize_for_opaque("Exchange");
486                    if col_names.is_none() {
487                        *col_names = Some(chunk.col_names());
488                    }
489                    let layout =
490                        Arc::new(SlotLayout::from_names(col_names.as_deref().unwrap_or(&[])));
491                    for row in &chunk.rows {
492                        let hash = compute_hash(row, hash_expressions, &layout)?;
493                        let bucket = (hash as usize) % num_partitions;
494                        buckets[bucket].push(row.clone());
495                    }
496                }
497                None => break,
498            }
499        }
500    }
501    Ok(())
502}
503
504/// Compute a hash value for a row based on the given expressions.
505fn compute_hash(
506    row: &[Value],
507    hash_expressions: &[Expression],
508    layout: &SlotLayout,
509) -> Result<u64, QueryError> {
510    let mut hasher = DefaultHasher::new();
511    let arc_layout = Arc::new(layout.clone());
512    for expr in hash_expressions {
513        let mut ctx = ValueRowContext::new(row.to_vec(), arc_layout.clone());
514        let value = ExpressionEvaluator::evaluate(expr, &mut ctx).map_err(|e| {
515            QueryError::execution(format!("RepartitionHash failed to evaluate key: {e}"))
516        })?;
517        value.hash(&mut hasher);
518    }
519    Ok(hasher.finish())
520}
521
522// ── Existing helpers (unchanged) ───────────────────────────────────────────
523
524fn input_count(serial: &[StreamingExecutor], handle: &Option<PartitionHandle>) -> usize {
525    handle
526        .as_ref()
527        .map(|h| h.partition_count)
528        .unwrap_or(serial.len())
529}
530
531fn advance_input(
532    children: &mut [StreamingExecutor],
533    handle: &mut Option<PartitionHandle>,
534    index: usize,
535) -> Result<Option<DataChunk>, QueryError> {
536    if let Some(handle) = handle {
537        handle.next_for_partition(index)
538    } else if index < children.len() {
539        children[index].advance()
540    } else {
541        Ok(None)
542    }
543}
544
545fn next_merge_row(
546    runtime: &Option<Arc<ExecutionRuntime>>,
547    children: &mut [StreamingExecutor],
548    handle: &mut Option<PartitionHandle>,
549    sort_expressions: &[Expression],
550    sort_directions: &[SortDirection],
551    inputs: &mut [MergeInputState],
552    col_names: &mut Option<Vec<String>>,
553) -> Result<Option<Vec<Value>>, QueryError> {
554    let mut best_child = None;
555
556    for index in 0..input_count(children, handle) {
557        if let Some(rt) = runtime.as_ref() {
558            rt.ensure_not_cancelled()?;
559        }
560        fill_input(index, children, handle, inputs, col_names)?;
561        let MergeInputState::Buffered { chunk, row_index } = &inputs[index] else {
562            continue;
563        };
564        let row = chunk.rows.get(*row_index).ok_or_else(|| {
565            QueryError::execution("Exchange merge input has an invalid row index".to_string())
566        })?;
567
568        let is_better = match best_child {
569            None => true,
570            Some(best_index) => {
571                let MergeInputState::Buffered {
572                    chunk: best_chunk,
573                    row_index: best_row_index,
574                } = &inputs[best_index]
575                else {
576                    return Err(QueryError::execution(
577                        "Exchange merge state changed while selecting a row".to_string(),
578                    ));
579                };
580                let best_row = best_chunk.rows.get(*best_row_index).ok_or_else(|| {
581                    QueryError::execution(
582                        "Exchange merge input has an invalid best-row index".to_string(),
583                    )
584                })?;
585                compare_rows(
586                    row,
587                    best_row,
588                    sort_expressions,
589                    sort_directions,
590                    col_names.as_deref().unwrap_or_default(),
591                )? == Ordering::Less
592            }
593        };
594        if is_better {
595            best_child = Some(index);
596        }
597    }
598
599    let Some(index) = best_child else {
600        return Ok(None);
601    };
602    let MergeInputState::Buffered { chunk, row_index } = &mut inputs[index] else {
603        return Err(QueryError::execution(
604            "Exchange selected a non-buffered input".to_string(),
605        ));
606    };
607    let row = chunk.rows.get(*row_index).cloned().ok_or_else(|| {
608        QueryError::execution("Exchange merge input has an invalid selected row index".to_string())
609    })?;
610    *row_index += 1;
611    if *row_index >= chunk.rows.len() {
612        inputs[index] = MergeInputState::Pending;
613    }
614    Ok(Some(row))
615}
616
617fn fill_input(
618    index: usize,
619    children: &mut [StreamingExecutor],
620    handle: &mut Option<PartitionHandle>,
621    inputs: &mut [MergeInputState],
622    col_names: &mut Option<Vec<String>>,
623) -> Result<(), QueryError> {
624    if !matches!(inputs[index], MergeInputState::Pending) {
625        return Ok(());
626    }
627
628    loop {
629        match advance_input(children, handle, index)? {
630            Some(chunk) if chunk.is_empty() => continue,
631            Some(mut chunk) => {
632                chunk.normalize_for_opaque("Exchange");
633                validate_schema(index, &chunk, col_names)?;
634                inputs[index] = MergeInputState::Buffered {
635                    chunk,
636                    row_index: 0,
637                };
638                return Ok(());
639            }
640            None => {
641                inputs[index] = MergeInputState::Exhausted;
642                return Ok(());
643            }
644        }
645    }
646}
647
648fn compare_rows(
649    a: &[Value],
650    b: &[Value],
651    sort_expressions: &[Expression],
652    sort_directions: &[SortDirection],
653    col_names: &[String],
654) -> Result<Ordering, QueryError> {
655    let layout = Arc::new(SlotLayout::from_names(col_names));
656    for (index, expression) in sort_expressions.iter().enumerate() {
657        let direction = sort_directions
658            .get(index)
659            .copied()
660            .unwrap_or(SortDirection::Ascending);
661        let mut left_context = ValueRowContext::new(a.to_vec(), layout.clone());
662        let mut right_context = ValueRowContext::new(b.to_vec(), layout.clone());
663        let left =
664            ExpressionEvaluator::evaluate(expression, &mut left_context).map_err(|error| {
665                QueryError::execution(format!(
666                    "Exchange failed to evaluate left sort key: {error}"
667                ))
668            })?;
669        let right =
670            ExpressionEvaluator::evaluate(expression, &mut right_context).map_err(|error| {
671                QueryError::execution(format!(
672                    "Exchange failed to evaluate right sort key: {error}"
673                ))
674            })?;
675        let comparison = match direction {
676            SortDirection::Ascending => compare_values(&left, &right),
677            SortDirection::Descending => compare_values(&left, &right).reverse(),
678        };
679        if comparison != Ordering::Equal {
680            return Ok(comparison);
681        }
682    }
683    Ok(Ordering::Equal)
684}
685
686fn validate_schema(
687    partition_id: usize,
688    chunk: &DataChunk,
689    col_names: &mut Option<Vec<String>>,
690) -> Result<(), QueryError> {
691    let child_columns = chunk.col_names();
692    if let Some(expected) = col_names.as_ref() {
693        if expected != &child_columns {
694            return Err(QueryError::execution(format!(
695                "Exchange schema mismatch in partition {}: expected {:?}, got {:?}",
696                partition_id, expected, child_columns
697            )));
698        }
699    } else {
700        *col_names = Some(child_columns);
701    }
702    Ok(())
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use crate::executor::streaming::operators::base::OperatorBase;
709    use crate::executor::streaming::operators::source_operator::SourceOperator;
710    use crate::executor::streaming::operators::source_operator::SourceOperatorKind;
711
712    fn source(values: &[i64], column: &str) -> StreamingExecutor {
713        let layout = Arc::new(SlotLayout::from_names(&[column.to_string()]));
714        StreamingExecutor::Source(
715            OperatorBase::new(1).with_output_layout(layout.clone()),
716            SourceOperator::new(
717                SourceOperatorKind::ScanVertices {
718                    buffer: values.iter().map(|v| vec![Value::BigInt(*v)]).collect(),
719                    current_index: 0,
720                    col_names: vec![column.to_string()],
721                },
722                layout,
723            ),
724        )
725    }
726
727    #[test]
728    fn test_repartition_hash_partitions_rows_by_hash() {
729        let _op = ExchangeOperator::from_spec(
730            &ExchangeSpec::RepartitionHash {
731                num_partitions: 3,
732                hash_expressions: vec![Expression::Variable("value".to_string())],
733                input_layout: None,
734                output_layout: None,
735            },
736            Arc::new(SlotLayout::new(Vec::new())),
737        );
738
739        // Manually test the partition logic
740        let col_names = vec!["value".to_string()];
741        let layout = Arc::new(SlotLayout::from_names(&col_names));
742        let rows = vec![
743            vec![Value::BigInt(1)],
744            vec![Value::BigInt(2)],
745            vec![Value::BigInt(3)],
746            vec![Value::BigInt(4)],
747            vec![Value::BigInt(5)],
748        ];
749
750        let mut buckets: Vec<Vec<Vec<Value>>> = (0..3).map(|_| Vec::new()).collect();
751        let hash_expr = vec![Expression::Variable("value".to_string())];
752        for row in &rows {
753            let hash = compute_hash(row, &hash_expr, &layout).unwrap();
754            let bucket = (hash as usize) % 3;
755            buckets[bucket].push(row.clone());
756        }
757
758        let total: usize = buckets.iter().map(|b| b.len()).sum();
759        assert_eq!(total, 5);
760        // Each bucket must have at least one row for 5 items across 3 buckets
761        assert!(buckets.iter().any(|b| b.len() >= 2));
762    }
763
764    #[test]
765    fn test_broadcast_state_from_spec() {
766        let op = ExchangeOperator::from_spec(
767            &ExchangeSpec::Broadcast { num_consumers: 4 },
768            Arc::new(SlotLayout::new(Vec::new())),
769        );
770        match op.state {
771            ExchangeState::Broadcast {
772                num_consumers,
773                buffered_chunks,
774                current_consumer,
775                ..
776            } => {
777                assert_eq!(num_consumers, 4);
778                assert!(buffered_chunks.is_empty());
779                assert_eq!(current_consumer, 0);
780            }
781            _ => panic!("Expected Broadcast state"),
782        }
783    }
784
785    #[test]
786    fn test_barrier_state_from_spec() {
787        let op = ExchangeOperator::from_spec(
788            &ExchangeSpec::Barrier,
789            Arc::new(SlotLayout::new(Vec::new())),
790        );
791        match op.state {
792            ExchangeState::Barrier { passed } => {
793                assert!(!passed);
794            }
795            _ => panic!("Expected Barrier state"),
796        }
797    }
798
799    #[test]
800    fn test_materialize_state_from_spec() {
801        let op = ExchangeOperator::from_spec(
802            &ExchangeSpec::Materialize { child_count: 2 },
803            Arc::new(SlotLayout::new(Vec::new())),
804        );
805        match op.state {
806            ExchangeState::Materialize { rows, position, .. } => {
807                assert!(rows.is_empty());
808                assert_eq!(position, 0);
809            }
810            _ => panic!("Expected Materialize state"),
811        }
812    }
813
814    #[test]
815    fn test_merge_sort_honors_limit() {
816        let mut children = vec![source(&[1, 3], "value"), source(&[2, 4], "value")];
817
818        let layout = Arc::new(SlotLayout::new(Vec::new()));
819        let mut op = ExchangeOperator::from_spec(
820            &ExchangeSpec::MergeSort {
821                sort_expressions: vec![Expression::Variable("value".to_string())],
822                sort_directions: vec![SortDirection::Ascending],
823                limit: Some(3),
824            },
825            layout,
826        );
827
828        // Open exchange operator (initializes merge state) and children.
829        op.open(&mut children).unwrap();
830
831        let mut all_values = Vec::new();
832        while let Some(chunk) = op.next(&mut children).unwrap() {
833            for row in chunk.rows {
834                if let Some(Value::BigInt(v)) = row.first() {
835                    all_values.push(*v);
836                }
837            }
838        }
839
840        op.close().unwrap();
841        assert_eq!(all_values, vec![1, 2, 3]);
842    }
843}