Skip to main content

rudb_exec/
build.rs

1//! Turning a bound plan into the pipelines that run it.
2//!
3//! One match, one arm per logical operator, and nothing else. There is no physical plan and no cost
4//! based choice between two ways of running the same node, which is the honest description of tier
5//! 0: there is one implementation of each operator so there is nothing to choose between. The
6//! physical planner that section 9.6 describes goes here, and the reason this is a separate module
7//! from the operators is so that it can grow into one without any of them moving.
8//!
9//! The pipelines borrow the plan and the catalog for as long as they exist. A scan reads its rows
10//! out of the catalog's table rather than copying them, and an expression reads its constants, its
11//! function names and its types out of the plan's arena, so a plan that outlives the query it built
12//! is the whole of the lifetime story here.
13//!
14//! # How a tree becomes a list
15//!
16//! The walk is the same one it always was, down from the root, and what changed is what it carries
17//! back up. A node returns a [`Segment`], which is a source with the streaming operators stacked on
18//! it so far, and a node that is a pipeline breaker closes the segment under it into a finished
19//! [`Pipeline`] and starts a new segment over the buffer that breaker finalises into. So a plan with
20//! two breakers in it comes back as three pipelines, and they are pushed onto the list in the order
21//! they have to run, because a breaker's own pipeline is closed before the walk returns to whatever
22//! is above it.
23//!
24//! A node with two inputs closes the side that has to finish first and then walks the side that uses
25//! it, which is the same order the ids are handed out in and the same order the work happens in.
26//!
27//! # Where the measurement comes from
28//!
29//! Every operator this module makes is wrapped in [`Watched`] before it goes into a pipeline, and
30//! the counters it reports into are registered with the [`Report`] the caller passed in. That is the
31//! only place the wrapping happens, which is what makes it impossible for an operator to be left
32//! out: an arm that forgets to wrap is an arm that does not compile, because the id it was handed
33//! has to go somewhere.
34//!
35//! A breaker's counters go around two objects rather than one. The sink is the operator, and the
36//! buffer the next pipeline sources from is where its rows come back out, so both are wrapped in the
37//! same counters and a sort's row count is the rows it produced rather than zero.
38//!
39//! Neither the ids nor the pipeline numbers are worked out here. They come from [`Shape`], which is
40//! one walk over the plan in `rudb-plan`, because `EXPLAIN` prints the same numbering and the same
41//! decomposition without building anything, and two versions of that rule would be right on the day
42//! they were written and disagree some time after. What this module does is ask which operator a
43//! node is and wrap it.
44
45use std::sync::Arc;
46
47use rudb_catalog::{Catalog, QualifiedName};
48use rudb_common::{Cancel, Memory, Result};
49use rudb_functions::TableFunction;
50use rudb_metrics::{Counters, Driver, Report};
51use rudb_parquet::{Bound, Op};
52use rudb_pipeline::{
53    BufferId, DynSink, DynStream, Pipeline, PipelineId, Source, Watched, root, root_in_order,
54};
55use rudb_plan::{
56    CompareOp, ConjunctionOp, Expr, ExprRef, Node, NodeRef, PipelineRef, Plan, ROOT, Shape, Slice,
57    seams_of,
58};
59use rudb_seam::Settings;
60
61use crate::fetch::Fetch;
62use crate::gather::{Gather, Keep};
63use crate::group::{Aggregate, Distinct};
64use crate::join::{CrossProduct, Gathered, Join};
65use crate::query::Query;
66use crate::register::registries;
67use crate::schema::Schema;
68use crate::setop::SetOp;
69use crate::sort::Sort;
70use crate::source::{Dummy, FileScan, Scan, Series, Values};
71use crate::strategies::Strategies;
72use crate::stream::{Filter, Limit, Project};
73use crate::topn::TopN;
74
75/// Builds the pipelines for a plan's root, for a query nothing will stop.
76///
77/// Every seam is left at its default, which is what a caller with no session behind it wants and is
78/// what the tests in this crate are written against.
79///
80/// # Errors
81///
82/// If the plan names a table or a column the catalog does not have, if an expression is malformed
83/// in a way [`Plan::validate`] would have caught, or anything an operator's construction reports.
84pub fn build<'a>(plan: &'a Plan, catalog: &'a Catalog) -> Result<Query<'a>> {
85    build_with(plan, catalog, &Cancel::new(), &Memory::unlimited(), &Settings::new())
86}
87
88/// Builds the pipelines for a plan's root, stoppable through this token and held to this budget.
89///
90/// The token is checked once per chunk by the driver, so the query stops at the first chunk boundary
91/// after the token says to. It is one check in one place rather than a decision per operator,
92/// because a decision per operator is a decision somebody gets wrong when they add the twentieth
93/// one. What the driver cannot see is work an operator does inside one call, and the join is the one
94/// that can: its nested loop runs to the end inside a single push, and a hundred thousand left rows
95/// against thirty thousand right ones is a minute with nothing looking at the token, so that loop
96/// holds the token as well and checks it once per left row.
97///
98/// The budget is not uniform, and that is the difference between the two. A streaming operator holds
99/// one chunk and gives it away again, so charging every operator would count the same megabyte once
100/// per level. Only the operators that buffer without bound take a reservation, and
101/// [`rudb_common::Memory`] lists which ones those are.
102///
103/// The measurement still happens. It goes into a report nobody reads, because the alternative is two
104/// builders that drift apart, and a pair of clock readings per chunk is not a cost worth avoiding by
105/// having a second one.
106///
107/// The seam settings are the session's with the statement's hints on top, and they are read here
108/// rather than looked up later, because a choice made while the query is built is a choice `EXPLAIN`
109/// can print before the query runs. An operator that sits on a seam chooses once, in its
110/// constructor, and holds what it chose.
111///
112/// # Errors
113///
114/// The same as [`build`].
115pub fn build_with<'a>(
116    plan: &'a Plan,
117    catalog: &'a Catalog,
118    cancel: &Cancel,
119    memory: &Memory,
120    seams: &Settings,
121) -> Result<Query<'a>> {
122    build_measured(plan, catalog, cancel, memory, seams, &Report::new())
123}
124
125/// Builds the pipelines, reporting what every operator in them did into `report`.
126///
127/// The report is what the caller keeps. Once the query has been run, [`Report::fill`] turns it into
128/// the operator and pipeline rows of a metrics document, and that document is the same one
129/// `EXPLAIN ANALYZE` prints and `--metrics` writes.
130///
131/// # Errors
132///
133/// The same as [`build`].
134pub fn build_measured<'a>(
135    plan: &'a Plan,
136    catalog: &'a Catalog,
137    cancel: &Cancel,
138    memory: &Memory,
139    seams: &Settings,
140    report: &Report,
141) -> Result<Query<'a>> {
142    let shape = Shape::of(plan);
143    for pipeline in shape.all() {
144        report.pipeline(pipeline);
145        for waits_for in shape.waits_for(pipeline) {
146            report.depends(pipeline, *waits_for);
147        }
148    }
149    let mut building = Building {
150        plan,
151        catalog,
152        cancel,
153        memory,
154        seams,
155        report,
156        shape,
157        done: Vec::new(),
158        drivers: Vec::new(),
159        pruning: Vec::new(),
160    };
161    let segment = building.node(plan.root())?;
162    let schema = segment.schema.clone();
163    // A query whose rows come out of a sort or a top n is already in the order somebody asked for,
164    // and holding chunks back to restore the source order would only add latency to an order nobody
165    // is going to look at. Everything else gets the root that puts them back, because the moment
166    // several threads read the same file a plain `SELECT` would otherwise come back in a different
167    // order on every run. It costs nothing to decide here and it means the scheduler never has to.
168    let (sink, reader) = if ordered(plan, plan.root()) {
169        root(BufferId(0), None)
170    } else {
171        root_in_order(BufferId(0), None)
172    };
173    building.close(segment, ROOT, Arc::new(sink));
174    let Building { done, drivers, .. } = building;
175    Query::new(done, drivers, reader, schema)
176}
177
178/// Whether the rows reaching the root are already in an order the plan chose.
179///
180/// A sort and a top n both decide one. Everything between them and the root either keeps the order
181/// it was given or is not a node that can sit there, and the walk stops at the first node that is
182/// neither.
183fn ordered(plan: &Plan, node: NodeRef) -> bool {
184    match *plan.node(node) {
185        Node::Sort { .. } | Node::TopN { .. } => true,
186        Node::Project { input, .. }
187        | Node::Filter { input, .. }
188        | Node::Limit { input, .. }
189        | Node::Fetch { input, .. } => ordered(plan, input),
190        _ => false,
191    }
192}
193
194/// What a filter over a Parquet scan can tell that scan before it opens anything.
195///
196/// A row group carries the smallest and largest value of each of its columns in the footer, so a
197/// conjunct comparing one of those columns against a constant can rule a whole group out without
198/// reading a page of it. This pulls out the conjuncts of that shape and drops everything else,
199/// which is the conservative direction: a test that is not here costs time, a test that is here
200/// wrongly costs rows.
201///
202/// Only an `AND` is walked into. Under an `OR` a conjunct being false says nothing about the row,
203/// and a `NOT` is already gone by the time the plan is bound. Only `read_parquet` is worth doing
204/// this for, because a CSV has no footer to read, and only a comparison against this scan's own
205/// columns counts, since a binding into some other operator's output is not in this file at all.
206fn bounds(plan: &Plan, input: NodeRef, predicate: ExprRef) -> Vec<(usize, Op, Bound)> {
207    let Node::TableFunction { index, function, .. } = *plan.node(input) else { return Vec::new() };
208    if TableFunction::lookup(plan.string(function)) != Some(TableFunction::ReadParquet) {
209        return Vec::new();
210    }
211    let mut tests = Vec::new();
212    conjuncts(plan, predicate, index, &mut tests);
213    tests
214}
215
216/// Every conjunct of `predicate` that reads as a bounds test, appended to `out`.
217fn conjuncts(plan: &Plan, predicate: ExprRef, index: u32, out: &mut Vec<(usize, Op, Bound)>) {
218    match *plan.expr(predicate) {
219        Expr::Conjunction { op: ConjunctionOp::And, children } => {
220            for child in plan.expr_list(children) {
221                conjuncts(plan, *child, index, out);
222            }
223        }
224        Expr::Compare { op, left, right } => {
225            if let Some(test) = comparison(plan, op, left, right, index) {
226                out.push(test);
227            }
228        }
229        _ => {}
230    }
231}
232
233/// One comparison read as a test on a column of the scan numbered `index`, if it is one.
234///
235/// Written either way round, because `5 < a` and `a > 5` say the same thing and the optimizer does
236/// not normalise which side the constant sits on. The comparisons that survive a null are the four
237/// orderings and equality: `<>` rules out a row group only when the group holds one distinct value,
238/// which the footer does not say, and the two distinctness operators are about nulls rather than
239/// about bounds.
240fn comparison(
241    plan: &Plan,
242    op: CompareOp,
243    left: ExprRef,
244    right: ExprRef,
245    index: u32,
246) -> Option<(usize, Op, Bound)> {
247    let op = match op {
248        CompareOp::Equal => Op::Equal,
249        CompareOp::Less => Op::Less,
250        CompareOp::LessOrEqual => Op::LessOrEqual,
251        CompareOp::Greater => Op::Greater,
252        CompareOp::GreaterOrEqual => Op::GreaterOrEqual,
253        CompareOp::NotEqual | CompareOp::DistinctFrom | CompareOp::NotDistinctFrom => return None,
254    };
255    let (op, binding, value) = match (plan.expr(left), plan.expr(right)) {
256        (Expr::Column(binding), Expr::Constant(value)) => (op, *binding, *value),
257        (Expr::Constant(value), Expr::Column(binding)) => (op.flipped(), *binding, *value),
258        _ => return None,
259    };
260    if binding.table != index {
261        return None;
262    }
263    Some((binding.column as usize, op, Bound::of_value(plan.value(value))?))
264}
265
266/// A pipeline being built from the bottom up.
267///
268/// It is not a [`Pipeline`] yet because it has no sink. What ends it is whichever node above it
269/// turns out to be a pipeline breaker, or the root of the plan, and neither is known until the walk
270/// gets back there.
271struct Segment<'a> {
272    source: Arc<dyn Source + 'a>,
273    /// In the order they run, nearest the source first.
274    streams: Vec<Arc<dyn DynStream + 'a>>,
275    /// What the segment produces as it stands, which changes as streams are added.
276    schema: Schema,
277    /// The pipelines this one cannot start before.
278    after: Vec<PipelineRef>,
279}
280
281impl<'a> Segment<'a> {
282    /// A segment that is just its source.
283    fn new(source: Arc<dyn Source + 'a>, schema: Schema) -> Self {
284        Self { source, streams: Vec::new(), schema, after: Vec::new() }
285    }
286
287    /// A segment reading what a pipeline breaker finalised into.
288    fn reading(source: Arc<dyn Source + 'a>, schema: Schema, after: PipelineRef) -> Self {
289        Self { source, streams: Vec::new(), schema, after: vec![after] }
290    }
291
292    /// Puts a streaming operator on the end, which becomes what the segment produces.
293    fn then(mut self, stream: Arc<dyn DynStream + 'a>, schema: Schema) -> Self {
294        self.streams.push(stream);
295        self.schema = schema;
296        self
297    }
298}
299
300/// What the walk down the plan carries with it.
301struct Building<'a, 'b> {
302    plan: &'a Plan,
303    catalog: &'a Catalog,
304    cancel: &'b Cancel,
305    memory: &'b Memory,
306    seams: &'b Settings,
307    report: &'b Report,
308    shape: Shape,
309    /// The pipelines closed so far, in the order they have to run.
310    done: Vec<Pipeline<'a>>,
311    /// One per entry of `done`, in the same order.
312    drivers: Vec<Arc<Driver>>,
313    /// The bounds tests the filter arm worked out for the scan it is about to walk into.
314    ///
315    /// A scan is built before the filter above it, because the filter needs the schema the scan
316    /// produces, so by the time there is a filter to read there is already a scan that cannot be
317    /// told anything. This carries the tests the other way, down the one step from a filter to its
318    /// own input, and the scan arm takes them. It is empty every other time it is read, and empty
319    /// means hand out every row group, which is what every scan did before pruning existed.
320    pruning: Vec<(usize, Op, Bound)>,
321}
322
323impl<'a> Building<'a, '_> {
324    /// The id of the operator holding the side of this node that has to finish first.
325    ///
326    /// # Panics
327    ///
328    /// If the node has one input, which is a node whose arm below should not have called this.
329    fn gathered(&self, node: NodeRef) -> u32 {
330        self.shape.gathered(node).expect("a node with two inputs has a second operator")
331    }
332
333    /// Ends a segment with a sink and puts the finished pipeline on the list.
334    fn close(&mut self, segment: Segment<'a>, id: PipelineRef, sink: Arc<dyn DynSink + 'a>) {
335        let mut pipeline = Pipeline::new(PipelineId(id), segment.source, sink);
336        for stream in segment.streams {
337            pipeline = pipeline.then(stream);
338        }
339        for after in segment.after {
340            pipeline = pipeline.after(PipelineId(after));
341        }
342        self.done.push(pipeline);
343        self.drivers.push(self.report.driving(id));
344    }
345
346    /// The counters for one operator, registered with the report.
347    ///
348    /// The row records what this operator picked at each seam it sits on, which is `seams_of` on
349    /// its plan node crossed with what is registered and what the statement pinned. That is the
350    /// same three things `EXPLAIN` puts its reference marker from, and it is read here rather than
351    /// asserted here for a reason worth writing down: this used to mark every operator as a
352    /// reference implementation unconditionally, so every ClickBench run said 41 of 41 operators
353    /// ran the slow path no matter what had actually run, and the fold that reported it was read as
354    /// if it meant something.
355    ///
356    /// An operator that sits on no registered seam records nothing and stays marked as a reference,
357    /// because there is one implementation of it and that one is the obvious correct one. The
358    /// marker comes off by itself on the day a seam under it has something else registered and
359    /// chosen, with nothing to remember to change here.
360    fn watch(
361        &self,
362        node: NodeRef,
363        id: u32,
364        pipeline: u32,
365        kind: &str,
366        detail: Option<&str>,
367    ) -> Arc<Counters> {
368        let mut counters = Counters::new(id, pipeline, kind);
369        if let Some(detail) = detail {
370            counters = counters.detailed(detail);
371        }
372        for seam in seams_of(self.plan.node(node)) {
373            if let Some(running) = registries().running(*seam, self.seams) {
374                counters = counters.chose(seam.name(), &running.name, running.is_reference);
375            }
376        }
377        self.report.watch(counters)
378    }
379
380    fn aggregate(
381        &mut self,
382        reference: NodeRef,
383        input: NodeRef,
384        index: u32,
385        groups: Slice,
386        aggregates: Slice,
387        max_groups: Option<usize>,
388    ) -> Result<Segment<'a>> {
389        let below = self.node(input)?;
390        let (aggregate, out) =
391            Aggregate::new(self.plan, &below.schema, index, groups, aggregates, self.memory)?;
392        let aggregate = match max_groups {
393            Some(limit) => aggregate.limit_groups(limit),
394            None => aggregate,
395        };
396        let schema = aggregate.schema().clone();
397        let id = self.shape.operator(reference);
398        let pipeline = self.shape.pipeline(reference);
399        let counters = self.watch(reference, id, pipeline, "Aggregate", None);
400        let reading = Arc::clone(&counters);
401        self.close(below, pipeline, Arc::new(Watched::new(aggregate, counters)));
402        Ok(Segment::reading(Arc::new(Watched::new(out, reading)), schema, pipeline))
403    }
404
405    /// The segment a node produces, closing any pipeline that ends underneath it.
406    fn node(&mut self, reference: NodeRef) -> Result<Segment<'a>> {
407        let plan = self.plan;
408        let memory = self.memory;
409        let id = self.shape.operator(reference);
410        let pipeline = self.shape.pipeline(reference);
411        let segment = match *plan.node(reference) {
412            Node::Get { catalog: database, schema, table, index, columns, .. } => {
413                let name = QualifiedName::new(
414                    plan.string(database),
415                    plan.string(schema),
416                    plan.string(table),
417                );
418                let scan = Scan::new(plan, self.catalog.table(&name)?, index, columns)?;
419                let schema = scan.schema().clone();
420                let counters =
421                    self.watch(reference, id, pipeline, "Scan", Some(plan.string(table)));
422                Segment::new(Arc::new(Watched::new(scan, counters)), schema)
423            }
424            Node::Dummy => {
425                let dummy = Dummy::new();
426                let schema = dummy.schema().clone();
427                let counters = self.watch(reference, id, pipeline, "Dummy", None);
428                Segment::new(Arc::new(Watched::new(dummy, counters)), schema)
429            }
430            Node::Values { index, columns, rows } => {
431                let values = Values::new(plan, index, columns, rows)?;
432                let schema = values.schema().clone();
433                let counters = self.watch(reference, id, pipeline, "Values", None);
434                Segment::new(Arc::new(Watched::new(values, counters)), schema)
435            }
436            Node::TableFunction { index, function, args, options, settings, columns } => {
437                let name = plan.string(function);
438                match TableFunction::lookup(name) {
439                    Some(function @ (TableFunction::ReadParquet | TableFunction::ReadCsv)) => {
440                        let counters = self.watch(reference, id, pipeline, "FileScan", Some(name));
441                        let tests = std::mem::take(&mut self.pruning);
442                        let scan = FileScan::new(
443                            plan, index, function, args, options, settings, columns, tests,
444                        )?
445                        .watched(counters.clone());
446                        let schema = scan.schema().clone();
447                        Segment::new(Arc::new(Watched::new(scan, counters)), schema)
448                    }
449                    Some(TableFunction::RudbStrategies) => {
450                        let table = Strategies::new(plan, index, columns)?;
451                        let schema = table.schema().clone();
452                        let counters = self.watch(reference, id, pipeline, "Strategies", None);
453                        Segment::new(Arc::new(Watched::new(table, counters)), schema)
454                    }
455                    _ => {
456                        let series = Series::new(plan, index, name, args)?;
457                        let schema = series.schema().clone();
458                        let counters = self.watch(reference, id, pipeline, "Series", Some(name));
459                        Segment::new(Arc::new(Watched::new(series, counters)), schema)
460                    }
461                }
462            }
463            Node::Fetch { input, index, args, columns, row } => {
464                let below = self.node(input)?;
465                let counters = self.watch(reference, id, pipeline, "Fetch", None);
466                let fetch = Fetch::new(plan, &below.schema, index, args, columns, row)?
467                    .watched(counters.clone());
468                let schema = fetch.schema().clone();
469                below.then(Arc::new(Watched::new(fetch, counters)), schema)
470            }
471            Node::Filter { input, predicate } => {
472                self.pruning = bounds(plan, input, predicate);
473                let below = self.node(input)?;
474                // Cleared whether or not the scan arm took them, because a filter over anything
475                // else leaves them sitting there for whatever scan the walk reaches next.
476                self.pruning = Vec::new();
477                let schema = below.schema.clone();
478                let filter = Filter::new(plan, reference, predicate, &schema, self.seams)?;
479                let counters = self.watch(reference, id, pipeline, "Filter", None);
480                below.then(Arc::new(Watched::new(filter, counters)), schema)
481            }
482            Node::Project { input, index, exprs, names } => {
483                let below = self.node(input)?;
484                let project = Project::new(plan, &below.schema, index, exprs, names)?;
485                let schema = project.schema().clone();
486                let counters = self.watch(reference, id, pipeline, "Project", None);
487                below.then(Arc::new(Watched::new(project, counters)), schema)
488            }
489            Node::Aggregate { input, index, groups, aggregates } => {
490                self.aggregate(reference, input, index, groups, aggregates, None)?
491            }
492            Node::Sort { input, keys } => {
493                let below = self.node(input)?;
494                let schema = below.schema.clone();
495                let (sort, out) = Sort::new(plan, &schema, keys, memory)?;
496                let counters = self.watch(reference, id, pipeline, "Sort", None);
497                let reading = Arc::clone(&counters);
498                self.close(below, pipeline, Arc::new(Watched::new(sort, counters)));
499                Segment::reading(Arc::new(Watched::new(out, reading)), schema, pipeline)
500            }
501            Node::Limit { input, count, offset } => {
502                let max_groups = count
503                    .and_then(|count| count.checked_add(offset))
504                    .and_then(|count| usize::try_from(count).ok());
505                let below = match (plan.node(input).clone(), max_groups) {
506                    (
507                        Node::Aggregate { input: under, index, groups, aggregates },
508                        Some(max_groups),
509                    ) => {
510                        self.aggregate(input, under, index, groups, aggregates, Some(max_groups))?
511                    }
512                    _ => self.node(input)?,
513                };
514                let schema = below.schema.clone();
515                let limit = Limit::new(count, offset);
516                let counters = self.watch(reference, id, pipeline, "Limit", None);
517                below.then(Arc::new(Watched::new(limit, counters)), schema)
518            }
519            Node::TopN { input, keys, count, offset } => {
520                let below = self.node(input)?;
521                let schema = below.schema.clone();
522                let (top, out) = TopN::new(plan, &schema, keys, count, offset, memory)?;
523                let counters = self.watch(reference, id, pipeline, "TopN", None);
524                let reading = Arc::clone(&counters);
525                self.close(below, pipeline, Arc::new(Watched::new(top, counters)));
526                Segment::reading(Arc::new(Watched::new(out, reading)), schema, pipeline)
527            }
528            Node::Distinct { input, on } => {
529                let below = self.node(input)?;
530                let schema = below.schema.clone();
531                let (distinct, out) = Distinct::new(plan, &schema, on, memory)?;
532                let counters = self.watch(reference, id, pipeline, "Distinct", None);
533                let reading = Arc::clone(&counters);
534                self.close(below, pipeline, Arc::new(Watched::new(distinct, counters)));
535                Segment::reading(Arc::new(Watched::new(out, reading)), schema, pipeline)
536            }
537            Node::Join { left, right, kind, conditions } => {
538                // The right side runs first, because no left row can be answered until every right
539                // row it might match has been seen. That is the dependency edge, and it is the same
540                // one the hash join builds on. The probing side is a pipeline of its own rather than
541                // part of the one above it, because it ends in a sink, and it waits for the build
542                // side.
543                let gather_id = self.gathered(reference);
544                let gathering = self.shape.pipeline(right);
545                let right = self.node(right)?;
546                let right_schema = right.schema.clone();
547                let (gather, gathered) = Gather::new(memory);
548                let kept = self.watch(reference, gather_id, gathering, "Gather", None);
549                self.close(right, gathering, Arc::new(Watched::new(gather, kept)));
550                let mut left = self.node(left)?;
551                let side = Gathered { schema: &right_schema, rows: gathered };
552                let (join, out) =
553                    Join::new(plan, &left.schema, side, kind, conditions, self.cancel, memory);
554                let schema = join.schema().clone();
555                let counters = self.watch(reference, id, pipeline, "Join", None);
556                let reading = Arc::clone(&counters);
557                left.after.push(gathering);
558                self.close(left, pipeline, Arc::new(Watched::new(join, counters)));
559                Segment::reading(Arc::new(Watched::new(out, reading)), schema, pipeline)
560            }
561            Node::CrossProduct { left, right } => {
562                // The right side runs first and is kept as the chunks it arrived in, because it is
563                // replayed once per left row. The left side streams, which is the whole point of
564                // this operator: the product is produced a chunk at a time and never held, so the
565                // product stays in the pipeline the left rows came from rather than starting one.
566                let keep_id = self.gathered(reference);
567                let aside = self.shape.pipeline(right);
568                let right = self.node(right)?;
569                let right_schema = right.schema.clone();
570                let (keep, kept) = Keep::new(memory);
571                let held = self.watch(reference, keep_id, aside, "Keep", None);
572                self.close(right, aside, Arc::new(Watched::new(keep, held)));
573                let mut left = self.node(left)?;
574                let cross = CrossProduct::new(&left.schema, &right_schema, kept);
575                let schema = cross.schema().clone();
576                let counters = self.watch(reference, id, pipeline, "CrossProduct", None);
577                left.after.push(aside);
578                left.then(Arc::new(Watched::new(cross, counters)), schema)
579            }
580            Node::SetOp { left, right, kind, all, index } => {
581                // The right side runs first, because nothing can be said about a left row until the
582                // whole right side has been counted. That is the dependency edge, spelled out.
583                let gather_id = self.gathered(reference);
584                let counting = self.shape.pipeline(right);
585                let right = self.node(right)?;
586                let (gather, gathered) = Gather::new(memory);
587                let kept = self.watch(reference, gather_id, counting, "Gather", None);
588                self.close(right, counting, Arc::new(Watched::new(gather, kept)));
589                let mut left = self.node(left)?;
590                let (setop, out) = SetOp::new(&left.schema, gathered, kind, all, index, memory);
591                let schema = setop.schema().clone();
592                let counters = self.watch(reference, id, pipeline, "SetOp", None);
593                let reading = Arc::clone(&counters);
594                left.after.push(counting);
595                self.close(left, pipeline, Arc::new(Watched::new(setop, counters)));
596                Segment::reading(Arc::new(Watched::new(out, reading)), schema, pipeline)
597            }
598        };
599        Ok(segment)
600    }
601}