Skip to main content

linkrs_query/executor/streaming/operators/
set_operator.rs

1use std::collections::HashSet;
2use std::sync::Arc;
3
4use crate::executor::base::{MemoryBudget, MemoryTracker};
5use crate::executor::streaming::chunk::DataChunk;
6use crate::executor::streaming::executor::StreamingExecutor;
7use crate::executor::streaming::operators::source_operator::OperatorConfig;
8use crate::executor::streaming::runtime::ExecutionRuntime;
9use crate::executor::streaming::slot::SlotLayout;
10use crate::executor::streaming::spill::COLLECTOR_RUN_ROWS_MAX;
11use linkrs_core::error::QueryError;
12use linkrs_core::Value;
13
14#[derive(Debug)]
15pub enum SetOperatorKind {
16    Union {
17        seen_rows: HashSet<String>,
18        left_consumed: bool,
19        memory_tracker: MemoryTracker,
20    },
21    UnionAll {
22        left_consumed: bool,
23    },
24    Intersect {
25        left_rows: Vec<Vec<Value>>,
26        right_rows: HashSet<String>,
27        left_buffered: bool,
28        right_buffered: bool,
29        emitted: bool,
30        memory_tracker: MemoryTracker,
31    },
32    Except {
33        exclude_rows: HashSet<String>,
34        right_buffered: bool,
35        memory_tracker: MemoryTracker,
36    },
37    Minus {
38        exclude_rows: HashSet<String>,
39        right_buffered: bool,
40        memory_tracker: MemoryTracker,
41    },
42}
43
44/// Set operator.
45///
46/// Wraps [`SetOperatorKind`] with the runtime context injected at `open()`.
47/// Lifecycle state is owned exclusively by the executor; operators never
48/// write it.
49#[derive(Debug)]
50pub struct SetOperator {
51    pub kind: SetOperatorKind,
52    pub runtime: Option<Arc<ExecutionRuntime>>,
53    pub output_layout: Arc<SlotLayout>,
54    pub config: OperatorConfig,
55    spilled_bytes: u64,
56    spilled_rows: u64,
57    spill_count: u64,
58}
59
60impl SetOperator {
61    pub fn from_spec(
62        spec: &super::spec::SetSpec,
63        budget: &MemoryBudget,
64        output_layout: Arc<SlotLayout>,
65    ) -> Self {
66        let kind = match spec {
67            super::spec::SetSpec::Union => SetOperatorKind::Union {
68                seen_rows: std::collections::HashSet::new(),
69                left_consumed: false,
70                memory_tracker: MemoryTracker::new(budget.clone()),
71            },
72            super::spec::SetSpec::UnionAll => SetOperatorKind::UnionAll {
73                left_consumed: false,
74            },
75            super::spec::SetSpec::Intersect => SetOperatorKind::Intersect {
76                left_rows: Vec::new(),
77                right_rows: std::collections::HashSet::new(),
78                left_buffered: false,
79                right_buffered: false,
80                emitted: false,
81                memory_tracker: MemoryTracker::new(budget.clone()),
82            },
83            super::spec::SetSpec::Except => SetOperatorKind::Except {
84                exclude_rows: std::collections::HashSet::new(),
85                right_buffered: false,
86                memory_tracker: MemoryTracker::new(budget.clone()),
87            },
88            super::spec::SetSpec::Minus => SetOperatorKind::Minus {
89                exclude_rows: std::collections::HashSet::new(),
90                right_buffered: false,
91                memory_tracker: MemoryTracker::new(budget.clone()),
92            },
93        };
94        Self::new(kind, output_layout)
95    }
96
97    pub fn new(kind: SetOperatorKind, output_layout: Arc<SlotLayout>) -> Self {
98        Self {
99            kind,
100            runtime: None,
101            output_layout,
102            config: OperatorConfig::default(),
103            spilled_bytes: 0,
104            spilled_rows: 0,
105            spill_count: 0,
106        }
107    }
108
109    /// Inject the runtime and execution config (called once by the executor
110    /// before this operator produces any data).
111    pub fn inject_context(
112        &mut self,
113        runtime: Option<&Arc<ExecutionRuntime>>,
114        config: OperatorConfig,
115    ) {
116        if let Some(rt) = runtime {
117            self.runtime = Some(rt.clone());
118        }
119        self.config = config;
120    }
121
122    pub fn memory_tracker(&self) -> &MemoryTracker {
123        match &self.kind {
124            SetOperatorKind::Union { memory_tracker, .. }
125            | SetOperatorKind::Intersect { memory_tracker, .. }
126            | SetOperatorKind::Except { memory_tracker, .. }
127            | SetOperatorKind::Minus { memory_tracker, .. } => memory_tracker,
128            SetOperatorKind::UnionAll { .. } => {
129                panic!("memory_tracker called on variant without memory tracking")
130            }
131        }
132    }
133
134    pub fn open(
135        &mut self,
136        left: &mut StreamingExecutor,
137        right: &mut StreamingExecutor,
138    ) -> Result<(), QueryError> {
139        match &mut self.kind {
140            SetOperatorKind::Union { .. }
141            | SetOperatorKind::UnionAll { .. }
142            | SetOperatorKind::Intersect { .. }
143            | SetOperatorKind::Except { .. }
144            | SetOperatorKind::Minus { .. } => {
145                left.open()?;
146                right.open()?;
147                Ok(())
148            }
149        }
150    }
151
152    pub fn next(
153        &mut self,
154        left: &mut StreamingExecutor,
155        right: &mut StreamingExecutor,
156    ) -> Result<Option<DataChunk>, QueryError> {
157        match &mut self.kind {
158            SetOperatorKind::Union {
159                seen_rows,
160                left_consumed,
161                memory_tracker,
162                ..
163            } => loop {
164                if let Some(rt) = self.runtime.as_ref() {
165                    rt.ensure_not_cancelled()?;
166                }
167                if !*left_consumed {
168                    if let Some(mut chunk) = left.advance()? {
169                        chunk.normalize_for_opaque("Set");
170                        let mut result_rows = Vec::new();
171                        for row in chunk.rows {
172                            let row_str = format!("{:?}", row);
173                            if !seen_rows.contains(&row_str) {
174                                memory_tracker.try_reserve(row_str.len())?;
175                                seen_rows.insert(row_str);
176                                result_rows.push(row);
177                            }
178                        }
179                        if !result_rows.is_empty() {
180                            return Ok(Some(DataChunk::new_with_layout(
181                                result_rows,
182                                Arc::clone(&self.output_layout),
183                            )));
184                        }
185                        continue;
186                    } else {
187                        *left_consumed = true;
188                    }
189                }
190
191                if let Some(mut chunk) = right.advance()? {
192                    chunk.normalize_for_opaque("Set");
193                    let mut result_rows = Vec::new();
194                    for row in chunk.rows {
195                        let row_str = format!("{:?}", row);
196                        if !seen_rows.contains(&row_str) {
197                            memory_tracker.try_reserve(row_str.len())?;
198                            seen_rows.insert(row_str);
199                            result_rows.push(row);
200                        }
201                    }
202                    if !result_rows.is_empty() {
203                        return Ok(Some(DataChunk::new_with_layout(
204                            result_rows,
205                            Arc::clone(&self.output_layout),
206                        )));
207                    }
208                    continue;
209                }
210                return Ok(None);
211            },
212
213            SetOperatorKind::UnionAll { left_consumed, .. } => loop {
214                if !*left_consumed {
215                    if let Some(mut chunk) = left.advance()? {
216                        chunk.normalize_for_opaque("Set");
217                        if !chunk.is_empty() {
218                            return Ok(Some(DataChunk::new_with_layout(
219                                chunk.rows,
220                                Arc::clone(&self.output_layout),
221                            )));
222                        }
223                    } else {
224                        *left_consumed = true;
225                    }
226                }
227
228                if let Some(mut chunk) = right.advance()? {
229                    chunk.normalize_for_opaque("Set");
230                    if !chunk.is_empty() {
231                        return Ok(Some(DataChunk::new_with_layout(
232                            chunk.rows,
233                            Arc::clone(&self.output_layout),
234                        )));
235                    }
236                } else {
237                    return Ok(None);
238                }
239            },
240
241            SetOperatorKind::Intersect {
242                left_rows,
243                right_rows,
244                left_buffered,
245                right_buffered,
246                emitted,
247                memory_tracker,
248                ..
249            } => {
250                if *emitted {
251                    return Ok(None);
252                }
253                if !*left_buffered {
254                    while let Some(mut chunk) = left.advance()? {
255                        chunk.normalize_for_opaque("Set");
256                        if let Some(rt) = self.runtime.as_ref() {
257                            rt.ensure_not_cancelled()?;
258                        }
259                        for row in &chunk.rows {
260                            memory_tracker.try_reserve_row(row)?;
261                        }
262                        left_rows.extend(chunk.rows);
263                    }
264                    *left_buffered = true;
265                }
266
267                if !*right_buffered {
268                    while let Some(mut chunk) = right.advance()? {
269                        chunk.normalize_for_opaque("Set");
270                        if let Some(rt) = self.runtime.as_ref() {
271                            rt.ensure_not_cancelled()?;
272                        }
273                        for row in chunk.rows {
274                            let row_str = format!("{:?}", row);
275                            memory_tracker.try_reserve(row_str.len())?;
276                            right_rows.insert(row_str);
277                        }
278                    }
279                    *right_buffered = true;
280                }
281
282                let result_rows: Vec<Vec<Value>> = left_rows
283                    .iter()
284                    .filter(|row| right_rows.contains(&format!("{:?}", row)))
285                    .cloned()
286                    .collect();
287
288                if result_rows.is_empty() {
289                    Ok(None)
290                } else {
291                    *emitted = true;
292                    Ok(Some(DataChunk::new_with_layout(
293                        result_rows,
294                        Arc::clone(&self.output_layout),
295                    )))
296                }
297            }
298
299            SetOperatorKind::Except {
300                exclude_rows,
301                right_buffered,
302                memory_tracker,
303                ..
304            } => loop {
305                if let Some(rt) = self.runtime.as_ref() {
306                    rt.ensure_not_cancelled()?;
307                }
308                if !*right_buffered {
309                    while let Some(mut chunk) = right.advance()? {
310                        chunk.normalize_for_opaque("Set");
311                        for row in chunk.rows {
312                            let row_str = format!("{:?}", row);
313                            memory_tracker.try_reserve(row_str.len())?;
314                            exclude_rows.insert(row_str);
315                        }
316                    }
317                    *right_buffered = true;
318                }
319
320                if let Some(mut chunk) = left.advance()? {
321                    chunk.normalize_for_opaque("Set");
322                    let result_rows: Vec<Vec<Value>> = chunk
323                        .rows
324                        .into_iter()
325                        .filter(|row| !exclude_rows.contains(&format!("{:?}", row)))
326                        .collect();
327
328                    if result_rows.is_empty() {
329                        continue;
330                    }
331                    return Ok(Some(DataChunk::new_with_layout(
332                        result_rows,
333                        Arc::clone(&self.output_layout),
334                    )));
335                }
336                return Ok(None);
337            },
338
339            SetOperatorKind::Minus {
340                exclude_rows,
341                right_buffered,
342                memory_tracker,
343                ..
344            } => {
345                if !*right_buffered {
346                    while let Some(mut chunk) = right.advance()? {
347                        chunk.normalize_for_opaque("Set");
348                        if let Some(rt) = self.runtime.as_ref() {
349                            rt.ensure_not_cancelled()?;
350                        }
351                        for row in chunk.rows {
352                            let row_str = format!("{:?}", row);
353                            memory_tracker.try_reserve(row_str.len())?;
354                            exclude_rows.insert(row_str);
355                        }
356                    }
357                    *right_buffered = true;
358                }
359
360                loop {
361                    if let Some(rt) = self.runtime.as_ref() {
362                        rt.ensure_not_cancelled()?;
363                    }
364                    if let Some(mut chunk) = left.advance()? {
365                        chunk.normalize_for_opaque("Set");
366                        let mut result_rows = Vec::new();
367                        for row in chunk.rows {
368                            let row_str = format!("{:?}", row);
369                            if !exclude_rows.contains(&row_str) {
370                                result_rows.push(row);
371                            }
372                        }
373                        if !result_rows.is_empty() {
374                            return Ok(Some(DataChunk::new_with_layout(
375                                result_rows,
376                                Arc::clone(&self.output_layout),
377                            )));
378                        }
379                        continue;
380                    }
381                    return Ok(None);
382                }
383            }
384        }
385    }
386
387    pub fn stop(&mut self) -> Result<(), QueryError> {
388        Ok(())
389    }
390
391    /// Reset per-run set state (seen/exclude sets, buffered sides, phase
392    /// flags) and rewind both inputs so the set operation re-runs cleanly.
393    pub fn reset(
394        &mut self,
395        left: &mut StreamingExecutor,
396        right: &mut StreamingExecutor,
397    ) -> Result<bool, QueryError> {
398        match &mut self.kind {
399            SetOperatorKind::Union {
400                seen_rows,
401                left_consumed,
402                ..
403            } => {
404                seen_rows.clear();
405                *left_consumed = false;
406            }
407            SetOperatorKind::UnionAll { left_consumed } => *left_consumed = false,
408            SetOperatorKind::Intersect {
409                left_rows,
410                right_rows,
411                left_buffered,
412                right_buffered,
413                emitted,
414                ..
415            } => {
416                left_rows.clear();
417                right_rows.clear();
418                *left_buffered = false;
419                *right_buffered = false;
420                *emitted = false;
421            }
422            SetOperatorKind::Except {
423                exclude_rows,
424                right_buffered,
425                ..
426            }
427            | SetOperatorKind::Minus {
428                exclude_rows,
429                right_buffered,
430                ..
431            } => {
432                exclude_rows.clear();
433                *right_buffered = false;
434            }
435        }
436        left.reset()?;
437        right.reset()?;
438        Ok(false)
439    }
440
441    pub fn close(&mut self) -> Result<(), QueryError> {
442        match &mut self.kind {
443            SetOperatorKind::Union {
444                seen_rows,
445                memory_tracker,
446                ..
447            } => {
448                memory_tracker.reset();
449                seen_rows.clear();
450            }
451            SetOperatorKind::UnionAll { .. } => {}
452            SetOperatorKind::Intersect {
453                left_rows,
454                right_rows,
455                memory_tracker,
456                ..
457            } => {
458                memory_tracker.reset();
459                left_rows.clear();
460                right_rows.clear();
461            }
462            SetOperatorKind::Except {
463                exclude_rows,
464                memory_tracker,
465                ..
466            }
467            | SetOperatorKind::Minus {
468                exclude_rows,
469                memory_tracker,
470                ..
471            } => {
472                memory_tracker.reset();
473                exclude_rows.clear();
474            }
475        }
476        Ok(())
477    }
478
479    pub fn spill_with_manager(
480        &mut self,
481        sm: &crate::executor::streaming::spill::SpillManager,
482    ) -> Result<(), linkrs_core::error::QueryError> {
483        match &mut self.kind {
484            SetOperatorKind::Union {
485                seen_rows,
486                memory_tracker,
487                ..
488            } => {
489                if !seen_rows.is_empty() {
490                    let rows: Vec<Vec<linkrs_core::Value>> = seen_rows
491                        .iter()
492                        .map(|s| vec![linkrs_core::Value::string(s.clone())])
493                        .collect();
494                    // Single spill format: versioned run file (fire-and-forget,
495                    // same lifecycle as the legacy writer before it). Writers
496                    // rotate every COLLECTOR_RUN_ROWS_MAX rows so neither the
497                    // writer-side body buffer nor the reader-side full-run
498                    // load grows without bound.
499                    let fp = crate::executor::streaming::spill::schema_fingerprint(
500                        &self.output_layout.names(),
501                    );
502                    for chunk in rows.chunks(COLLECTOR_RUN_ROWS_MAX as usize) {
503                        let mut writer = sm.create_run_writer(fp)?;
504                        writer.write_rows(chunk)?;
505                        let run = sm.finalize_run(writer)?;
506                        self.spilled_rows += run.row_count;
507                        self.spilled_bytes += run.byte_size;
508                        self.spill_count += 1;
509                        if let Some(rt) = self.runtime.as_ref() {
510                            rt.columnar_stats()
511                                .record_spill(run.row_count, run.byte_size);
512                        }
513                    }
514                    seen_rows.clear();
515                    memory_tracker.reset();
516                }
517                Ok(())
518            }
519            SetOperatorKind::Intersect {
520                left_rows,
521                right_rows,
522                memory_tracker,
523                ..
524            } => {
525                if !left_rows.is_empty() {
526                    let fp = crate::executor::streaming::spill::schema_fingerprint(
527                        &self.output_layout.names(),
528                    );
529                    while !left_rows.is_empty() {
530                        let take = (COLLECTOR_RUN_ROWS_MAX as usize).min(left_rows.len());
531                        let chunk: Vec<Vec<linkrs_core::Value>> = left_rows.drain(..take).collect();
532                        let mut writer = sm.create_run_writer(fp)?;
533                        writer.write_rows(&chunk)?;
534                        let run = sm.finalize_run(writer)?;
535                        self.spilled_rows += run.row_count;
536                        self.spilled_bytes += run.byte_size;
537                        self.spill_count += 1;
538                        if let Some(rt) = self.runtime.as_ref() {
539                            rt.columnar_stats()
540                                .record_spill(run.row_count, run.byte_size);
541                        }
542                    }
543                    memory_tracker.reset();
544                }
545                if !right_rows.is_empty() {
546                    right_rows.clear();
547                }
548                Ok(())
549            }
550            SetOperatorKind::Except {
551                exclude_rows,
552                memory_tracker,
553                ..
554            }
555            | SetOperatorKind::Minus {
556                exclude_rows,
557                memory_tracker,
558                ..
559            } => {
560                if !exclude_rows.is_empty() {
561                    exclude_rows.clear();
562                    memory_tracker.reset();
563                }
564                Ok(())
565            }
566            SetOperatorKind::UnionAll { .. } => Ok(()),
567        }
568    }
569
570    pub fn spilled_bytes(&self) -> u64 {
571        self.spilled_bytes
572    }
573
574    pub fn spilled_rows(&self) -> u64 {
575        self.spilled_rows
576    }
577
578    pub fn spill_count(&self) -> u64 {
579        self.spill_count
580    }
581}