Skip to main content

rudb_exec/
build.rs

1//! Turning a bound plan into a tree of operators.
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 tree borrows the plan and the catalog for as long as it exists. A scan reads its rows out of
10//! the catalog's table rather than copying them, and an expression reads its constants, its function
11//! names and its types out of the plan's arena, so a plan that outlives the query it built is the
12//! whole of the lifetime story here.
13//!
14//! # Where the measurement comes from
15//!
16//! Every operator this module makes is wrapped in [`Watched`] before it goes into the tree, and the
17//! counters it reports into are registered with the [`Report`] the caller passed in. That is the
18//! only place the wrapping happens, which is what makes it impossible for an operator to be left
19//! out: an arm that forgets to wrap is an arm that does not compile, because the id it was handed
20//! has to go somewhere.
21//!
22//! Neither the ids nor the pipeline numbers are worked out here. They come from [`Shape`], which is
23//! one walk over the plan in `rudb-plan`, because `EXPLAIN` prints the same numbering and the same
24//! decomposition without building anything, and two versions of that rule would be right on the day
25//! they were written and disagree some time after. What this module does is ask which operator a
26//! node is and wrap it.
27
28use std::sync::Arc;
29
30use rudb_catalog::{Catalog, QualifiedName};
31use rudb_common::{Cancel, Memory, Result};
32use rudb_functions::TableFunction;
33use rudb_metrics::{Counters, Report};
34use rudb_pipeline::{Source, Watched};
35use rudb_plan::{Node, NodeRef, Plan, Shape, Slice, seams_of};
36use rudb_seam::Settings;
37
38use crate::adapt::{Broken, Fed, Paired, Pulled, Streamed};
39use crate::cancel::Guarded;
40use crate::fetch::Fetch;
41use crate::gather::{Gather, Keep};
42use crate::group::{Aggregate, Distinct};
43use crate::join::{CrossProduct, Gathered, Join};
44use crate::operator::Operator;
45use crate::register::registries;
46use crate::schema::Schema;
47use crate::setop::SetOp;
48use crate::sort::Sort;
49use crate::source::{Dummy, FileScan, Scan, Series, Values};
50use crate::strategies::Strategies;
51use crate::stream::{Filter, Limit, Project};
52use crate::topn::TopN;
53
54/// Builds the operator tree for a plan's root, for a query nothing will stop.
55///
56/// Every seam is left at its default, which is what a caller with no session behind it wants and is
57/// what the tests in this crate are written against.
58///
59/// # Errors
60///
61/// If the plan names a table or a column the catalog does not have, if an expression is malformed
62/// in a way [`Plan::validate`] would have caught, or anything an operator's construction reports.
63pub fn build<'a>(plan: &'a Plan, catalog: &'a Catalog) -> Result<Box<dyn Operator + 'a>> {
64    build_with(plan, catalog, &Cancel::new(), &Memory::unlimited(), &Settings::new())
65}
66
67/// Builds the operator tree for a plan's root, stoppable through this token and held to this
68/// budget.
69///
70/// Every node in the tree is wrapped in a check, so the query stops at the first chunk boundary
71/// after the token says to. See the `cancel` module for why the check is uniform rather than
72/// placed in the operators that can loop.
73///
74/// The budget is not uniform, and that is the difference between the two. A streaming operator
75/// holds one chunk and gives it away again, so charging every node would count the same megabyte
76/// once per level of the tree. Only the operators that buffer without bound take a reservation, and
77/// [`rudb_common::Memory`] lists which ones those are.
78///
79/// The measurement still happens. It goes into a report nobody reads, because the alternative is
80/// two builders that drift apart, and a pair of clock readings per chunk is not a cost worth
81/// avoiding by having a second one.
82///
83/// The seam settings are the session's with the statement's hints on top, and they are read here
84/// rather than looked up later, because a choice made while the tree is built is a choice `EXPLAIN`
85/// can print before the query runs. An operator that sits on a seam chooses once, in its
86/// constructor, and holds what it chose.
87///
88/// # Errors
89///
90/// The same as [`build`].
91pub fn build_with<'a>(
92    plan: &'a Plan,
93    catalog: &'a Catalog,
94    cancel: &Cancel,
95    memory: &Memory,
96    seams: &Settings,
97) -> Result<Box<dyn Operator + 'a>> {
98    build_measured(plan, catalog, cancel, memory, seams, &Report::new())
99}
100
101/// Builds the operator tree, reporting what every operator in it did into `report`.
102///
103/// The report is what the caller keeps. Once the tree has been drained,
104/// [`Report::fill`] turns it into the operator and pipeline rows of a metrics document, and that
105/// document is the same one `EXPLAIN ANALYZE` prints and `--metrics` writes.
106///
107/// # Errors
108///
109/// The same as [`build`].
110pub fn build_measured<'a>(
111    plan: &'a Plan,
112    catalog: &'a Catalog,
113    cancel: &Cancel,
114    memory: &Memory,
115    seams: &Settings,
116    report: &Report,
117) -> Result<Box<dyn Operator + 'a>> {
118    let shape = Shape::of(plan);
119    for pipeline in shape.all() {
120        report.pipeline(pipeline);
121        for waits_for in shape.waits_for(pipeline) {
122            report.depends(pipeline, *waits_for);
123        }
124    }
125    let building = Building { plan, catalog, cancel, memory, seams, report, shape };
126    building.node(plan.root())
127}
128
129/// What the walk down the plan carries with it.
130struct Building<'a, 'b> {
131    plan: &'a Plan,
132    catalog: &'a Catalog,
133    cancel: &'b Cancel,
134    memory: &'b Memory,
135    seams: &'b Settings,
136    report: &'b Report,
137    shape: Shape,
138}
139
140impl<'a> Building<'a, '_> {
141    /// The id of the operator holding the side of this node that has to finish first.
142    ///
143    /// # Panics
144    ///
145    /// If the node has one input, which is a node whose arm below should not have called this.
146    fn gathered(&self, node: NodeRef) -> u32 {
147        self.shape.gathered(node).expect("a node with two inputs has a second operator")
148    }
149
150    /// The counters for one operator, registered with the report.
151    ///
152    /// The row records what this operator picked at each seam it sits on, which is `seams_of` on
153    /// its plan node crossed with what is registered and what the statement pinned. That is the
154    /// same three things `EXPLAIN` puts its reference marker from, and it is read here rather than
155    /// asserted here for a reason worth writing down: this used to mark every operator as a
156    /// reference implementation unconditionally, so every ClickBench run said 41 of 41 operators
157    /// ran the slow path no matter what had actually run, and the fold that reported it was read as
158    /// if it meant something.
159    ///
160    /// An operator that sits on no registered seam records nothing and stays marked as a reference,
161    /// because there is one implementation of it and that one is the obvious correct one. The
162    /// marker comes off by itself on the day a seam under it has something else registered and
163    /// chosen, with nothing to remember to change here.
164    fn watch(
165        &self,
166        node: NodeRef,
167        id: u32,
168        pipeline: u32,
169        kind: &str,
170        detail: Option<&str>,
171    ) -> Arc<Counters> {
172        let mut counters = Counters::new(id, pipeline, kind);
173        if let Some(detail) = detail {
174            counters = counters.detailed(detail);
175        }
176        for seam in seams_of(self.plan.node(node)) {
177            if let Some(running) = registries().running(*seam, self.seams) {
178                counters = counters.chose(seam.name(), &running.name, running.is_reference);
179            }
180        }
181        self.report.watch(counters)
182    }
183
184    fn aggregate(
185        &self,
186        reference: NodeRef,
187        input: NodeRef,
188        index: u32,
189        groups: Slice,
190        aggregates: Slice,
191        max_groups: Option<usize>,
192    ) -> Result<Box<dyn Operator + 'a>> {
193        let child = self.node(input)?;
194        let (aggregate, out) =
195            Aggregate::new(self.plan, child.schema(), index, groups, aggregates, self.memory)?;
196        let aggregate = match max_groups {
197            Some(limit) => aggregate.limit_groups(limit),
198            None => aggregate,
199        };
200        let schema = aggregate.schema().clone();
201        let id = self.shape.operator(reference);
202        let pipeline = self.shape.pipeline(reference);
203        let counters = self.watch(reference, id, pipeline, "Aggregate", None);
204        let made = Arc::clone(&counters);
205        let driver = self.report.driving(pipeline);
206        Ok(Box::new(Broken::new(
207            child,
208            Watched::new(aggregate, counters),
209            driver,
210            out,
211            made,
212            schema,
213        )))
214    }
215
216    fn node(&self, reference: NodeRef) -> Result<Box<dyn Operator + 'a>> {
217        let plan = self.plan;
218        let memory = self.memory;
219        let id = self.shape.operator(reference);
220        let pipeline = self.shape.pipeline(reference);
221        let inner: Box<dyn Operator + 'a> = match *plan.node(reference) {
222            Node::Get { catalog: database, schema, table, index, columns, .. } => {
223                let name = QualifiedName::new(
224                    plan.string(database),
225                    plan.string(schema),
226                    plan.string(table),
227                );
228                let scan = Scan::new(plan, self.catalog.table(&name)?, index, columns)?;
229                let schema = scan.schema().clone();
230                let counters =
231                    self.watch(reference, id, pipeline, "Scan", Some(plan.string(table)));
232                pulled(Watched::new(scan, counters), schema)
233            }
234            Node::Dummy => {
235                let dummy = Dummy::new();
236                let schema = dummy.schema().clone();
237                pulled(
238                    Watched::new(dummy, self.watch(reference, id, pipeline, "Dummy", None)),
239                    schema,
240                )
241            }
242            Node::Values { index, columns, rows } => {
243                let values = Values::new(plan, index, columns, rows)?;
244                let schema = values.schema().clone();
245                pulled(
246                    Watched::new(values, self.watch(reference, id, pipeline, "Values", None)),
247                    schema,
248                )
249            }
250            Node::TableFunction { index, function, args, options, settings, columns } => {
251                let name = plan.string(function);
252                match TableFunction::lookup(name) {
253                    Some(function @ (TableFunction::ReadParquet | TableFunction::ReadCsv)) => {
254                        let counters = self.watch(reference, id, pipeline, "FileScan", Some(name));
255                        let scan =
256                            FileScan::new(plan, index, function, args, options, settings, columns)?
257                                .watched(counters.clone());
258                        let schema = scan.schema().clone();
259                        pulled(Watched::new(scan, counters), schema)
260                    }
261                    Some(TableFunction::RudbStrategies) => {
262                        let table = Strategies::new(plan, index, columns)?;
263                        let schema = table.schema().clone();
264                        let counters = self.watch(reference, id, pipeline, "Strategies", None);
265                        pulled(Watched::new(table, counters), schema)
266                    }
267                    _ => {
268                        let series = Series::new(plan, index, name, args)?;
269                        let schema = series.schema().clone();
270                        let counters = self.watch(reference, id, pipeline, "Series", Some(name));
271                        pulled(Watched::new(series, counters), schema)
272                    }
273                }
274            }
275            Node::Fetch { input, index, args, columns, row } => {
276                let input = self.node(input)?;
277                let counters = self.watch(reference, id, pipeline, "Fetch", None);
278                let fetch = Fetch::new(plan, input.schema(), index, args, columns, row)?
279                    .watched(counters.clone());
280                let schema = fetch.schema().clone();
281                Box::new(Streamed::new(input, Watched::new(fetch, counters), schema))
282            }
283            Node::Filter { input, predicate } => {
284                let input = self.node(input)?;
285                let schema = input.schema().clone();
286                let filter = Filter::new(plan, reference, predicate, &schema, self.seams)?;
287                let counters = self.watch(reference, id, pipeline, "Filter", None);
288                Box::new(Streamed::new(input, Watched::new(filter, counters), schema))
289            }
290            Node::Project { input, index, exprs, names } => {
291                let input = self.node(input)?;
292                let project = Project::new(plan, input.schema(), index, exprs, names)?;
293                let schema = project.schema().clone();
294                let counters = self.watch(reference, id, pipeline, "Project", None);
295                Box::new(Streamed::new(input, Watched::new(project, counters), schema))
296            }
297            Node::Aggregate { input, index, groups, aggregates } => {
298                self.aggregate(reference, input, index, groups, aggregates, None)?
299            }
300            Node::Sort { input, keys } => {
301                let input = self.node(input)?;
302                let schema = input.schema().clone();
303                let (sort, out) = Sort::new(plan, &schema, keys, memory)?;
304                let counters = self.watch(reference, id, pipeline, "Sort", None);
305                let made = Arc::clone(&counters);
306                let driver = self.report.driving(pipeline);
307                Box::new(Broken::new(
308                    input,
309                    Watched::new(sort, counters),
310                    driver,
311                    out,
312                    made,
313                    schema,
314                ))
315            }
316            Node::Limit { input, count, offset } => {
317                let max_groups = count
318                    .and_then(|count| count.checked_add(offset))
319                    .and_then(|count| usize::try_from(count).ok());
320                let input = match (plan.node(input).clone(), max_groups) {
321                    (
322                        Node::Aggregate { input: below, index, groups, aggregates },
323                        Some(max_groups),
324                    ) => {
325                        self.aggregate(input, below, index, groups, aggregates, Some(max_groups))?
326                    }
327                    _ => self.node(input)?,
328                };
329                let schema = input.schema().clone();
330                let limit = Limit::new(count, offset);
331                let counters = self.watch(reference, id, pipeline, "Limit", None);
332                Box::new(Streamed::new(input, Watched::new(limit, counters), schema))
333            }
334            Node::TopN { input, keys, count, offset } => {
335                let input = self.node(input)?;
336                let schema = input.schema().clone();
337                let (top, out) = TopN::new(plan, &schema, keys, count, offset, memory)?;
338                let counters = self.watch(reference, id, pipeline, "TopN", None);
339                let made = Arc::clone(&counters);
340                let driver = self.report.driving(pipeline);
341                Box::new(Broken::new(input, Watched::new(top, counters), driver, out, made, schema))
342            }
343            Node::Distinct { input, on } => {
344                let input = self.node(input)?;
345                let schema = input.schema().clone();
346                let (distinct, out) = Distinct::new(plan, &schema, on, memory)?;
347                let counters = self.watch(reference, id, pipeline, "Distinct", None);
348                let made = Arc::clone(&counters);
349                let driver = self.report.driving(pipeline);
350                Box::new(Broken::new(
351                    input,
352                    Watched::new(distinct, counters),
353                    driver,
354                    out,
355                    made,
356                    schema,
357                ))
358            }
359            Node::Join { left, right, kind, conditions } => {
360                // The right side runs first, because no left row can be answered until every right
361                // row it might match has been seen. That is the dependency edge, and it is the same
362                // one the hash join builds on. The probing side is a pipeline of its own rather than
363                // part of the one above it, because it ends in a sink, and it waits for the build
364                // side.
365                let gather_id = self.gathered(reference);
366                let gathering = self.shape.pipeline(right);
367                let right = self.node(right)?;
368                let left = self.node(left)?;
369                let (gather, gathered) = Gather::new(memory);
370                let side = Gathered { schema: right.schema(), rows: gathered };
371                let (join, out) =
372                    Join::new(plan, left.schema(), side, kind, conditions, self.cancel, memory);
373                let schema = join.schema().clone();
374                let kept = self.watch(reference, gather_id, gathering, "Gather", None);
375                let counters = self.watch(reference, id, pipeline, "Join", None);
376                let made = Arc::clone(&counters);
377                Box::new(Paired::new(
378                    right,
379                    Watched::new(gather, kept),
380                    self.report.driving(gathering),
381                    left,
382                    Watched::new(join, counters),
383                    self.report.driving(pipeline),
384                    out,
385                    made,
386                    schema,
387                ))
388            }
389            Node::CrossProduct { left, right } => {
390                // The right side runs first and is kept as the chunks it arrived in, because it is
391                // replayed once per left row. The left side streams, which is the whole point of
392                // this operator: the product is produced a chunk at a time and never held, so the
393                // product stays in the pipeline the left rows came from rather than starting one.
394                let keep_id = self.gathered(reference);
395                let aside = self.shape.pipeline(right);
396                let right = self.node(right)?;
397                let left = self.node(left)?;
398                let (keep, kept) = Keep::new(memory);
399                let cross = CrossProduct::new(left.schema(), right.schema(), kept);
400                let schema = cross.schema().clone();
401                let held = self.watch(reference, keep_id, aside, "Keep", None);
402                let counters = self.watch(reference, id, pipeline, "CrossProduct", None);
403                Box::new(Fed::new(
404                    right,
405                    Watched::new(keep, held),
406                    self.report.driving(aside),
407                    Streamed::new(left, Watched::new(cross, counters), schema),
408                ))
409            }
410            Node::SetOp { left, right, kind, all, index } => {
411                // The right side runs first, because nothing can be said about a left row until the
412                // whole right side has been counted. That is the dependency edge, spelled out.
413                let gather_id = self.gathered(reference);
414                let counting = self.shape.pipeline(right);
415                let right = self.node(right)?;
416                let left = self.node(left)?;
417                let (gather, gathered) = Gather::new(memory);
418                let (setop, out) = SetOp::new(left.schema(), gathered, kind, all, index, memory);
419                let schema = setop.schema().clone();
420                let kept = self.watch(reference, gather_id, counting, "Gather", None);
421                let counters = self.watch(reference, id, pipeline, "SetOp", None);
422                let made = Arc::clone(&counters);
423                Box::new(Paired::new(
424                    right,
425                    Watched::new(gather, kept),
426                    self.report.driving(counting),
427                    left,
428                    Watched::new(setop, counters),
429                    self.report.driving(pipeline),
430                    out,
431                    made,
432                    schema,
433                ))
434            }
435        };
436        Ok(Box::new(Guarded::new(inner, self.cancel.clone())))
437    }
438}
439
440/// A leaf source with the adapter that pulls chunks out of it.
441///
442/// Every leaf is a [`Source`] and everything above it still pulls, so this is
443/// where the two meet. The schema is passed in rather than asked for through a trait, because a
444/// source says what it produces on its own type and adding a trait method to say it again would be
445/// a second answer to the same question.
446fn pulled<'a, S: Source + 'a>(source: S, schema: Schema) -> Box<dyn Operator + 'a> {
447    Box::new(Pulled::new(source, schema))
448}