Skip to main content

linkrs_query/executor/streaming/operators/
gather_operator.rs

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/// Internal merge cursor state. It is public only because `GatherOperatorKind`
20/// is a public enum; callers should use [`GatherOperator::merge_sort`] instead
21/// of constructing this state directly.
22#[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/// Gather operator.
49///
50/// Wraps [`GatherOperatorKind`] with the runtime context injected at `open()`.
51/// Lifecycle state is owned exclusively by the executor; operators never
52/// write it.
53#[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    /// Inject the runtime and execution config (called once by the executor
72    /// before this operator produces any data).
73    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        // Try parallel path via the runtime's morsel worker pool.
143        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        // Serial fallback: open children normally.
172        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
332/// Advance one input using the blocking drain fast path (used by
333/// [`GatherOperatorKind::Concatenate`], which reads one partition at a time).
334fn 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}