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#[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 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 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 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 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 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 *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 if !*passed {
374 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 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#[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
504fn 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
522fn 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 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 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 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}