1use std::cmp::Ordering;
2use std::sync::Arc;
3use std::time::Instant;
4
5use crate::executor::expression::evaluator::ExpressionEvaluator;
6use crate::executor::streaming::chunk::DataChunk;
7use crate::executor::streaming::executor::{SortDirection, StreamingExecutor, ValueRowContext};
8use crate::executor::streaming::helpers::compare_values;
9use crate::executor::streaming::operators::source_operator::OperatorConfig;
10use crate::executor::streaming::pool::{PartitionBatch, PartitionHandle};
11use crate::executor::streaming::runtime::ExecutionRuntime;
12use crate::executor::streaming::slot::SlotLayout;
13use linkrs_core::error::QueryError;
14use linkrs_core::types::expr::Expression;
15use linkrs_core::Value;
16
17const CHUNK_SIZE: usize = 2048;
18
19#[doc(hidden)]
23#[derive(Debug)]
24pub enum MergeInputState {
25 Pending,
26 Buffered { chunk: DataChunk, row_index: usize },
27 Exhausted,
28}
29
30#[derive(Debug)]
31pub enum GatherOperatorKind {
32 Concatenate {
33 current_index: usize,
34 col_names: Option<Vec<String>>,
35 handle: Option<PartitionHandle>,
36 },
37 MergeSort {
38 sort_expressions: Vec<Expression>,
39 sort_directions: Vec<SortDirection>,
40 inputs: Vec<MergeInputState>,
41 col_names: Option<Vec<String>>,
42 limit: Option<usize>,
43 emitted: usize,
44 handle: Option<PartitionHandle>,
45 },
46}
47
48#[derive(Debug)]
54pub struct GatherOperator {
55 pub kind: GatherOperatorKind,
56 pub runtime: Option<Arc<ExecutionRuntime>>,
57 pub output_layout: Arc<SlotLayout>,
58 pub config: OperatorConfig,
59}
60
61impl GatherOperator {
62 pub fn new(kind: GatherOperatorKind, output_layout: Arc<SlotLayout>) -> Self {
63 Self {
64 kind,
65 runtime: None,
66 output_layout,
67 config: OperatorConfig::default(),
68 }
69 }
70
71 pub fn inject_context(
74 &mut self,
75 runtime: Option<&Arc<ExecutionRuntime>>,
76 config: OperatorConfig,
77 ) {
78 if let Some(rt) = runtime {
79 self.runtime = Some(rt.clone());
80 }
81 self.config = config;
82 }
83
84 pub fn concatenate(output_layout: Arc<SlotLayout>) -> Self {
85 Self::new(
86 GatherOperatorKind::Concatenate {
87 current_index: 0,
88 col_names: None,
89 handle: None,
90 },
91 output_layout,
92 )
93 }
94
95 pub fn merge_sort(
96 sort_expressions: Vec<Expression>,
97 sort_directions: Vec<SortDirection>,
98 limit: Option<usize>,
99 output_layout: Arc<SlotLayout>,
100 ) -> Self {
101 Self::new(
102 GatherOperatorKind::MergeSort {
103 sort_expressions,
104 sort_directions,
105 inputs: Vec::new(),
106 col_names: None,
107 limit,
108 emitted: 0,
109 handle: None,
110 },
111 output_layout,
112 )
113 }
114
115 pub fn open(&mut self, children: &mut Vec<StreamingExecutor>) -> Result<(), QueryError> {
116 let handle = match &mut self.kind {
117 GatherOperatorKind::Concatenate {
118 current_index,
119 col_names,
120 handle,
121 } => {
122 *current_index = 0;
123 *col_names = None;
124 std::mem::take(handle)
125 }
126 GatherOperatorKind::MergeSort {
127 inputs,
128 col_names,
129 emitted,
130 handle,
131 ..
132 } => {
133 *inputs = (0..children.len())
134 .map(|_| MergeInputState::Pending)
135 .collect();
136 *col_names = None;
137 *emitted = 0;
138 std::mem::take(handle)
139 }
140 };
141
142 if handle.is_none() {
144 if let Some(rt) = self.runtime.clone() {
145 let pool = rt.worker_pool.lock().clone();
146 if let Some(pool) = pool {
147 if children.len() > 1 && pool.max_workers() > 1 {
148 let max_buffered = rt
149 .max_buffered_chunks
150 .load(std::sync::atomic::Ordering::Relaxed)
151 .max(1);
152 let (batch, receivers, error_rx) =
153 PartitionBatch::new(std::mem::take(children), rt.clone(), max_buffered);
154 let batch = Arc::new(batch);
155 let h = PartitionHandle::from_batch(
156 &batch,
157 receivers,
158 error_rx,
159 rt.clone(),
160 Instant::now(),
161 pool.max_workers(),
162 );
163 pool.submit(batch);
164 Self::set_handle(self, Some(h));
165 return Ok(());
166 }
167 }
168 }
169 }
170
171 for (opened_children, child) in children.iter_mut().enumerate() {
173 if let Err(error) = child.open() {
174 let close_error = close_children(&mut children[..opened_children]);
175 return Err(close_error.unwrap_or(error));
176 }
177 }
178 Ok(())
179 }
180
181 pub fn next(
182 &mut self,
183 children: &mut [StreamingExecutor],
184 ) -> Result<Option<DataChunk>, QueryError> {
185 if let Some(rt) = self.runtime.as_ref() {
186 rt.ensure_not_cancelled()?;
187 }
188 let Self {
189 kind,
190 runtime,
191 output_layout,
192 ..
193 } = self;
194 match kind {
195 GatherOperatorKind::Concatenate {
196 current_index,
197 col_names,
198 handle,
199 } => {
200 while *current_index < input_count(children, handle) {
201 if let Some(rt) = runtime.as_ref() {
202 rt.ensure_not_cancelled()?;
203 }
204 if let Some(chunk) = advance_input_blocking(children, handle, *current_index)? {
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(output_layout),
212 )));
213 }
214 *current_index += 1;
215 }
216 Ok(None)
217 }
218 GatherOperatorKind::MergeSort {
219 sort_expressions,
220 sort_directions,
221 inputs,
222 col_names,
223 limit,
224 emitted,
225 handle,
226 } => {
227 if limit.is_some_and(|value| *emitted >= value) {
228 return Ok(None);
229 }
230
231 let mut result_rows = Vec::with_capacity(CHUNK_SIZE);
232 while result_rows.len() < CHUNK_SIZE {
233 if let Some(rt) = runtime.as_ref() {
234 rt.ensure_not_cancelled()?;
235 }
236 if limit.is_some_and(|value| *emitted >= value) {
237 break;
238 }
239 match next_merge_row(
240 runtime,
241 children,
242 handle,
243 sort_expressions,
244 sort_directions,
245 inputs,
246 col_names,
247 )? {
248 Some(row) => {
249 result_rows.push(row);
250 *emitted += 1;
251 }
252 None => break,
253 }
254 }
255
256 if result_rows.is_empty() {
257 Ok(None)
258 } else {
259 Ok(Some(DataChunk::new_with_layout(
260 result_rows,
261 Arc::clone(output_layout),
262 )))
263 }
264 }
265 }
266 }
267
268 pub fn stop(&mut self) -> Result<(), QueryError> {
269 if let Some(mut handle) = Self::take_handle(self) {
270 return handle.stop_and_join();
271 }
272 Ok(())
273 }
274
275 pub fn close(&mut self) -> Result<(), QueryError> {
276 if let GatherOperatorKind::MergeSort {
277 inputs,
278 col_names,
279 emitted,
280 ..
281 } = &mut self.kind
282 {
283 inputs.clear();
284 *col_names = None;
285 *emitted = 0;
286 }
287 if let GatherOperatorKind::Concatenate { col_names, .. } = &mut self.kind {
288 *col_names = None;
289 }
290 if let Some(mut handle) = Self::take_handle(self) {
291 return handle.stop_and_join();
292 }
293 Ok(())
294 }
295
296 fn set_handle(op: &mut Self, h: Option<PartitionHandle>) {
297 match &mut op.kind {
298 GatherOperatorKind::Concatenate { handle, .. }
299 | GatherOperatorKind::MergeSort { handle, .. } => *handle = h,
300 }
301 }
302
303 fn take_handle(op: &mut Self) -> Option<PartitionHandle> {
304 match &mut op.kind {
305 GatherOperatorKind::Concatenate { handle, .. }
306 | GatherOperatorKind::MergeSort { handle, .. } => handle.take(),
307 }
308 }
309}
310
311fn input_count(serial: &[StreamingExecutor], handle: &Option<PartitionHandle>) -> usize {
312 handle
313 .as_ref()
314 .map(|h| h.partition_count)
315 .unwrap_or(serial.len())
316}
317
318fn advance_input(
319 children: &mut [StreamingExecutor],
320 handle: &mut Option<PartitionHandle>,
321 index: usize,
322) -> Result<Option<DataChunk>, QueryError> {
323 if let Some(handle) = handle {
324 handle.next_for_partition(index)
325 } else if index < children.len() {
326 children[index].advance()
327 } else {
328 Ok(None)
329 }
330}
331
332fn advance_input_blocking(
335 children: &mut [StreamingExecutor],
336 handle: &mut Option<PartitionHandle>,
337 index: usize,
338) -> Result<Option<DataChunk>, QueryError> {
339 if let Some(handle) = handle {
340 handle.next_for_partition_blocking(index)
341 } else if index < children.len() {
342 children[index].advance()
343 } else {
344 Ok(None)
345 }
346}
347
348fn next_merge_row(
349 runtime: &Option<Arc<ExecutionRuntime>>,
350 children: &mut [StreamingExecutor],
351 handle: &mut Option<PartitionHandle>,
352 sort_expressions: &[Expression],
353 sort_directions: &[SortDirection],
354 inputs: &mut [MergeInputState],
355 col_names: &mut Option<Vec<String>>,
356) -> Result<Option<Vec<Value>>, QueryError> {
357 let mut best_child = None;
358
359 for index in 0..input_count(children, handle) {
360 if let Some(rt) = runtime.as_ref() {
361 rt.ensure_not_cancelled()?;
362 }
363 fill_input(index, children, handle, inputs, col_names)?;
364 let MergeInputState::Buffered { chunk, row_index } = &inputs[index] else {
365 continue;
366 };
367 let row = chunk.rows.get(*row_index).ok_or_else(|| {
368 QueryError::execution("Gather merge input has an invalid row index".to_string())
369 })?;
370 let is_better = match best_child {
371 None => true,
372 Some(best_index) => {
373 let MergeInputState::Buffered {
374 chunk: best_chunk,
375 row_index: best_row_index,
376 } = &inputs[best_index]
377 else {
378 return Err(QueryError::execution(
379 "Gather merge state changed while selecting a row".to_string(),
380 ));
381 };
382 let best_row = best_chunk.rows.get(*best_row_index).ok_or_else(|| {
383 QueryError::execution(
384 "Gather merge input has an invalid best-row index".to_string(),
385 )
386 })?;
387 compare_rows(
388 row,
389 best_row,
390 sort_expressions,
391 sort_directions,
392 col_names.as_deref().unwrap_or_default(),
393 )? == Ordering::Less
394 }
395 };
396 if is_better {
397 best_child = Some(index);
398 }
399 }
400
401 let Some(index) = best_child else {
402 return Ok(None);
403 };
404 let MergeInputState::Buffered { chunk, row_index } = &mut inputs[index] else {
405 return Err(QueryError::execution(
406 "Gather selected a non-buffered input".to_string(),
407 ));
408 };
409 let row = chunk.rows.get(*row_index).cloned().ok_or_else(|| {
410 QueryError::execution("Gather merge input has an invalid selected row index".to_string())
411 })?;
412 *row_index += 1;
413 if *row_index >= chunk.rows.len() {
414 inputs[index] = MergeInputState::Pending;
415 }
416 Ok(Some(row))
417}
418
419fn fill_input(
420 index: usize,
421 children: &mut [StreamingExecutor],
422 handle: &mut Option<PartitionHandle>,
423 inputs: &mut [MergeInputState],
424 col_names: &mut Option<Vec<String>>,
425) -> Result<(), QueryError> {
426 if !matches!(inputs[index], MergeInputState::Pending) {
427 return Ok(());
428 }
429
430 loop {
431 match advance_input(children, handle, index)? {
432 Some(chunk) if chunk.is_empty() => continue,
433 Some(mut chunk) => {
434 chunk.normalize_for_opaque("Gather");
435 validate_schema(index, &chunk, col_names)?;
436 inputs[index] = MergeInputState::Buffered {
437 chunk,
438 row_index: 0,
439 };
440 return Ok(());
441 }
442 None => {
443 inputs[index] = MergeInputState::Exhausted;
444 return Ok(());
445 }
446 }
447 }
448}
449
450fn compare_rows(
451 a: &[Value],
452 b: &[Value],
453 sort_expressions: &[Expression],
454 sort_directions: &[SortDirection],
455 col_names: &[String],
456) -> Result<Ordering, QueryError> {
457 let layout = Arc::new(SlotLayout::from_names(col_names));
458 for (index, expression) in sort_expressions.iter().enumerate() {
459 let direction = sort_directions
460 .get(index)
461 .copied()
462 .unwrap_or(SortDirection::Ascending);
463 let mut left_context = ValueRowContext::new(a.to_vec(), layout.clone());
464 let mut right_context = ValueRowContext::new(b.to_vec(), layout.clone());
465 let left =
466 ExpressionEvaluator::evaluate(expression, &mut left_context).map_err(|error| {
467 QueryError::execution(format!("Gather failed to evaluate left sort key: {error}"))
468 })?;
469 let right =
470 ExpressionEvaluator::evaluate(expression, &mut right_context).map_err(|error| {
471 QueryError::execution(format!("Gather failed to evaluate right sort key: {error}"))
472 })?;
473 let comparison = match direction {
474 SortDirection::Ascending => compare_values(&left, &right),
475 SortDirection::Descending => compare_values(&left, &right).reverse(),
476 };
477 if comparison != Ordering::Equal {
478 return Ok(comparison);
479 }
480 }
481 Ok(Ordering::Equal)
482}
483
484fn validate_schema(
485 partition_id: usize,
486 chunk: &DataChunk,
487 col_names: &mut Option<Vec<String>>,
488) -> Result<(), QueryError> {
489 let child_columns = chunk.col_names();
490 if let Some(expected) = col_names.as_ref() {
491 if expected != &child_columns {
492 return Err(QueryError::execution(format!(
493 "Gather schema mismatch in partition {}: expected {:?}, got {:?}",
494 partition_id, expected, child_columns
495 )));
496 }
497 } else {
498 *col_names = Some(child_columns);
499 }
500 Ok(())
501}
502
503fn close_children(children: &mut [StreamingExecutor]) -> Option<QueryError> {
504 let mut first_error = None;
505 for child in children.iter_mut() {
506 if let Err(error) = child.close() {
507 if first_error.is_none() {
508 first_error = Some(error);
509 }
510 }
511 }
512 first_error
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::executor::streaming::operators::base::OperatorBase;
519 use crate::executor::streaming::operators::source_operator::SourceOperator;
520 use crate::executor::streaming::operators::source_operator::SourceOperatorKind;
521
522 fn source(values: &[i64], column: &str) -> StreamingExecutor {
523 let layout = Arc::new(SlotLayout::from_names(&[column.to_string()]));
524 StreamingExecutor::Source(
525 OperatorBase::new(1).with_output_layout(layout.clone()),
526 SourceOperator::new(
527 SourceOperatorKind::ScanVertices {
528 buffer: values
529 .iter()
530 .map(|value| vec![Value::BigInt(*value)])
531 .collect(),
532 current_index: 0,
533 col_names: vec![column.to_string()],
534 },
535 layout,
536 ),
537 )
538 }
539
540 fn merge_executor(children: Vec<StreamingExecutor>, limit: Option<usize>) -> StreamingExecutor {
541 let layout = Arc::new(SlotLayout::from_names(&["value".to_string()]));
542 StreamingExecutor::Gather(
543 OperatorBase::new(i64::MIN)
544 .with_global(true)
545 .with_output_layout(layout.clone()),
546 children,
547 GatherOperator::merge_sort(
548 vec![Expression::Variable("value".to_string())],
549 vec![SortDirection::Ascending],
550 limit,
551 layout,
552 ),
553 )
554 }
555
556 fn collect_values(
557 executor: &mut StreamingExecutor,
558 ) -> Result<(Vec<i64>, Vec<String>), QueryError> {
559 executor.open()?;
560 let mut values = Vec::new();
561 let mut columns = Vec::new();
562 while let Some(mut chunk) = executor.advance()? {
563 chunk.normalize_for_opaque("Gather");
564 if columns.is_empty() {
565 columns = chunk.col_names();
566 }
567 for row in chunk.rows {
568 if let Some(Value::BigInt(value)) = row.first() {
569 values.push(*value);
570 }
571 }
572 }
573 executor.close()?;
574 Ok((values, columns))
575 }
576
577 #[test]
578 fn merge_sort_merges_sorted_partition_streams_and_preserves_schema() {
579 let mut executor = merge_executor(
580 vec![source(&[1, 3], "value"), source(&[2, 4], "value")],
581 None,
582 );
583
584 let (values, columns) = collect_values(&mut executor).expect("merge should succeed");
585
586 assert_eq!(values, vec![1, 2, 3, 4]);
587 assert_eq!(columns, vec!["value"]);
588 }
589
590 #[test]
591 fn merge_sort_honors_global_limit() {
592 let mut executor = merge_executor(
593 vec![source(&[1, 3], "value"), source(&[2, 4], "value")],
594 Some(3),
595 );
596
597 let (values, _) = collect_values(&mut executor).expect("merge should succeed");
598
599 assert_eq!(values, vec![1, 2, 3]);
600 }
601
602 #[test]
603 fn merge_sort_rejects_incompatible_partition_schema() {
604 let mut executor = merge_executor(vec![source(&[1], "value"), source(&[2], "other")], None);
605
606 executor.open().expect("open should succeed");
607 let error = executor.advance().expect_err("schema mismatch must fail");
608 executor
609 .close()
610 .expect("close should succeed after failure");
611
612 assert!(error.to_string().contains("schema mismatch"));
613 }
614
615 #[test]
616 fn concatenate_rejects_incompatible_partition_schema() {
617 let layout = Arc::new(SlotLayout::from_names(&["id".to_string()]));
618 let mut executor = StreamingExecutor::Gather(
619 OperatorBase::new(10).with_output_layout(layout.clone()),
620 vec![source(&[1], "id"), source(&[2], "other_id")],
621 GatherOperator::concatenate(layout),
622 );
623
624 executor.open().expect("open gather");
625 assert!(executor.advance().expect("first partition").is_some());
626 let error = executor.advance().expect_err("schema mismatch must fail");
627 executor
628 .close()
629 .expect("close should succeed after failure");
630
631 assert!(error.to_string().contains("schema mismatch"));
632 }
633}