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//! The ids are allocated as the match walks down, so operator 0 is the root and a child always has
23//! a larger id than its parent. The pipeline numbers are not worked out here. They come from
24//! [`Pipelines`], which is a walk over the plan in `rudb-plan`, because `EXPLAIN` prints the same
25//! decomposition without building anything and two versions of that rule would be right on the day
26//! they were written and disagree some time after.
27
28use std::cell::Cell;
29use std::sync::Arc;
30
31use rudb_catalog::{Catalog, QualifiedName};
32use rudb_common::{Cancel, Memory, Result};
33use rudb_functions::TableFunction;
34use rudb_metrics::{Counters, Report};
35use rudb_pipeline::{Source, Watched};
36use rudb_plan::{Node, NodeRef, Pipelines, Plan};
37
38use crate::adapt::{Broken, Fed, Paired, Pulled, Streamed};
39use crate::cancel::Guarded;
40use crate::gather::{Gather, Keep};
41use crate::group::{Aggregate, Distinct};
42use crate::join::{CrossProduct, Gathered, Join};
43use crate::operator::Operator;
44use crate::schema::Schema;
45use crate::setop::SetOp;
46use crate::sort::Sort;
47use crate::source::{Dummy, FileScan, Scan, Series, Values};
48use crate::strategies::Strategies;
49use crate::stream::{Filter, Limit, Project};
50use crate::topn::TopN;
51
52/// Builds the operator tree for a plan's root, for a query nothing will stop.
53///
54/// # Errors
55///
56/// If the plan names a table or a column the catalog does not have, if an expression is malformed
57/// in a way [`Plan::validate`] would have caught, or anything an operator's construction reports.
58pub fn build<'a>(plan: &'a Plan, catalog: &'a Catalog) -> Result<Box<dyn Operator + 'a>> {
59    build_with(plan, catalog, &Cancel::new(), &Memory::unlimited())
60}
61
62/// Builds the operator tree for a plan's root, stoppable through this token and held to this
63/// budget.
64///
65/// Every node in the tree is wrapped in a check, so the query stops at the first chunk boundary
66/// after the token says to. See the `cancel` module for why the check is uniform rather than
67/// placed in the operators that can loop.
68///
69/// The budget is not uniform, and that is the difference between the two. A streaming operator
70/// holds one chunk and gives it away again, so charging every node would count the same megabyte
71/// once per level of the tree. Only the operators that buffer without bound take a reservation, and
72/// [`rudb_common::Memory`] lists which ones those are.
73///
74/// The measurement still happens. It goes into a report nobody reads, because the alternative is
75/// two builders that drift apart, and a pair of clock readings per chunk is not a cost worth
76/// avoiding by having a second one.
77///
78/// # Errors
79///
80/// The same as [`build`].
81pub fn build_with<'a>(
82    plan: &'a Plan,
83    catalog: &'a Catalog,
84    cancel: &Cancel,
85    memory: &Memory,
86) -> Result<Box<dyn Operator + 'a>> {
87    build_measured(plan, catalog, cancel, memory, &Report::new())
88}
89
90/// Builds the operator tree, reporting what every operator in it did into `report`.
91///
92/// The report is what the caller keeps. Once the tree has been drained,
93/// [`Report::fill`] turns it into the operator and pipeline rows of a metrics document, and that
94/// document is the same one `EXPLAIN ANALYZE` prints and `--metrics` writes.
95///
96/// # Errors
97///
98/// The same as [`build`].
99pub fn build_measured<'a>(
100    plan: &'a Plan,
101    catalog: &'a Catalog,
102    cancel: &Cancel,
103    memory: &Memory,
104    report: &Report,
105) -> Result<Box<dyn Operator + 'a>> {
106    let pipelines = Pipelines::of(plan);
107    for pipeline in pipelines.all() {
108        report.pipeline(pipeline);
109        for waits_for in pipelines.waits_for(pipeline) {
110            report.depends(pipeline, *waits_for);
111        }
112    }
113    let building =
114        Building { plan, catalog, cancel, memory, report, pipelines, next_operator: Cell::new(0) };
115    building.node(plan.root())
116}
117
118/// What the walk down the plan carries with it.
119///
120/// The operator counter is a cell rather than a mutable borrow because every arm of the match below
121/// recurses while it is holding something it made, and threading a `&mut` through that would mean
122/// building each node in two halves for no reason a reader of the arms would enjoy.
123struct Building<'a, 'b> {
124    plan: &'a Plan,
125    catalog: &'a Catalog,
126    cancel: &'b Cancel,
127    memory: &'b Memory,
128    report: &'b Report,
129    pipelines: Pipelines,
130    next_operator: Cell<u32>,
131}
132
133impl<'a> Building<'a, '_> {
134    /// The id for the next operator, taken before its children are built so that a parent's id is
135    /// smaller than every id below it.
136    fn id(&self) -> u32 {
137        let id = self.next_operator.get();
138        self.next_operator.set(id + 1);
139        id
140    }
141
142    /// The counters for one operator, registered with the report.
143    ///
144    /// Everything built here is marked as a reference implementation, because at tier 0 everything
145    /// built here is one. That is not a placeholder: the marker is what stops a number measured
146    /// against the simplest correct version of an operator from being quoted as if it came from the
147    /// fast one, and it comes off an operator on the day that operator gets a second tier.
148    fn watch(&self, id: u32, pipeline: u32, kind: &str, detail: Option<&str>) -> Arc<Counters> {
149        let counters = Counters::new(id, pipeline, kind).reference();
150        let counters = match detail {
151            Some(detail) => counters.detailed(detail),
152            None => counters,
153        };
154        self.report.watch(counters)
155    }
156
157    fn node(&self, reference: NodeRef) -> Result<Box<dyn Operator + 'a>> {
158        let plan = self.plan;
159        let memory = self.memory;
160        let pipeline = self.pipelines.pipeline(reference);
161        let inner: Box<dyn Operator + 'a> = match *plan.node(reference) {
162            Node::Get { catalog: database, schema, table, index, columns, .. } => {
163                let id = self.id();
164                let name = QualifiedName::new(
165                    plan.string(database),
166                    plan.string(schema),
167                    plan.string(table),
168                );
169                let scan = Scan::new(plan, self.catalog.table(&name)?, index, columns)?;
170                let schema = scan.schema().clone();
171                let counters = self.watch(id, pipeline, "Scan", Some(plan.string(table)));
172                pulled(Watched::new(scan, counters), schema)
173            }
174            Node::Dummy => {
175                let id = self.id();
176                let dummy = Dummy::new();
177                let schema = dummy.schema().clone();
178                pulled(Watched::new(dummy, self.watch(id, pipeline, "Dummy", None)), schema)
179            }
180            Node::Values { index, columns, rows } => {
181                let id = self.id();
182                let values = Values::new(plan, index, columns, rows)?;
183                let schema = values.schema().clone();
184                pulled(Watched::new(values, self.watch(id, pipeline, "Values", None)), schema)
185            }
186            Node::TableFunction { index, function, args, options, settings, columns } => {
187                let id = self.id();
188                let name = plan.string(function);
189                match TableFunction::lookup(name) {
190                    Some(function @ (TableFunction::ReadParquet | TableFunction::ReadCsv)) => {
191                        let scan =
192                            FileScan::new(plan, index, function, args, options, settings, columns)?;
193                        let schema = scan.schema().clone();
194                        let counters = self.watch(id, pipeline, "FileScan", Some(name));
195                        pulled(Watched::new(scan, counters), schema)
196                    }
197                    Some(TableFunction::RudbStrategies) => {
198                        let table = Strategies::new(plan, index, columns)?;
199                        let schema = table.schema().clone();
200                        let counters = self.watch(id, pipeline, "Strategies", None);
201                        pulled(Watched::new(table, counters), schema)
202                    }
203                    _ => {
204                        let series = Series::new(plan, index, name, args)?;
205                        let schema = series.schema().clone();
206                        let counters = self.watch(id, pipeline, "Series", Some(name));
207                        pulled(Watched::new(series, counters), schema)
208                    }
209                }
210            }
211            Node::Filter { input, predicate } => {
212                let id = self.id();
213                let input = self.node(input)?;
214                let schema = input.schema().clone();
215                let filter = Filter::new(plan, predicate, &schema)?;
216                let counters = self.watch(id, pipeline, "Filter", None);
217                Box::new(Streamed::new(input, Watched::new(filter, counters), schema))
218            }
219            Node::Project { input, index, exprs, names } => {
220                let id = self.id();
221                let input = self.node(input)?;
222                let project = Project::new(plan, input.schema(), index, exprs, names)?;
223                let schema = project.schema().clone();
224                let counters = self.watch(id, pipeline, "Project", None);
225                Box::new(Streamed::new(input, Watched::new(project, counters), schema))
226            }
227            Node::Aggregate { input, index, groups, aggregates } => {
228                let id = self.id();
229                let input = self.node(input)?;
230                let (aggregate, out) =
231                    Aggregate::new(plan, input.schema(), index, groups, aggregates, memory)?;
232                let schema = aggregate.schema().clone();
233                let counters = self.watch(id, pipeline, "Aggregate", None);
234                Box::new(Broken::new(input, Watched::new(aggregate, counters), out, schema))
235            }
236            Node::Sort { input, keys } => {
237                let id = self.id();
238                let input = self.node(input)?;
239                let schema = input.schema().clone();
240                let (sort, out) = Sort::new(plan, &schema, keys, memory)?;
241                let counters = self.watch(id, pipeline, "Sort", None);
242                Box::new(Broken::new(input, Watched::new(sort, counters), out, schema))
243            }
244            Node::Limit { input, count, offset } => {
245                let id = self.id();
246                let input = self.node(input)?;
247                let schema = input.schema().clone();
248                let limit = Limit::new(count, offset);
249                let counters = self.watch(id, pipeline, "Limit", None);
250                Box::new(Streamed::new(input, Watched::new(limit, counters), schema))
251            }
252            Node::TopN { input, keys, count, offset } => {
253                let id = self.id();
254                let input = self.node(input)?;
255                let schema = input.schema().clone();
256                let (top, out) = TopN::new(plan, &schema, keys, count, offset, memory)?;
257                let counters = self.watch(id, pipeline, "TopN", None);
258                Box::new(Broken::new(input, Watched::new(top, counters), out, schema))
259            }
260            Node::Distinct { input, on } => {
261                let id = self.id();
262                let input = self.node(input)?;
263                let schema = input.schema().clone();
264                let (distinct, out) = Distinct::new(plan, &schema, on, memory)?;
265                let counters = self.watch(id, pipeline, "Distinct", None);
266                Box::new(Broken::new(input, Watched::new(distinct, counters), out, schema))
267            }
268            Node::Join { left, right, kind, conditions } => {
269                let id = self.id();
270                let gather_id = self.id();
271                // The right side runs first, because no left row can be answered until every right
272                // row it might match has been seen. That is the dependency edge, and it is the same
273                // one the hash join builds on. The probing side is a pipeline of its own rather than
274                // part of the one above it, because it ends in a sink, and it waits for the build
275                // side.
276                let gathering = self.pipelines.pipeline(right);
277                let right = self.node(right)?;
278                let left = self.node(left)?;
279                let (gather, gathered) = Gather::new(memory);
280                let side = Gathered { schema: right.schema(), rows: gathered };
281                let (join, out) =
282                    Join::new(plan, left.schema(), side, kind, conditions, self.cancel, memory);
283                let schema = join.schema().clone();
284                let kept = self.watch(gather_id, gathering, "Gather", None);
285                let counters = self.watch(id, pipeline, "Join", None);
286                Box::new(Paired::new(
287                    right,
288                    Watched::new(gather, kept),
289                    left,
290                    Watched::new(join, counters),
291                    out,
292                    schema,
293                ))
294            }
295            Node::CrossProduct { left, right } => {
296                let id = self.id();
297                let keep_id = self.id();
298                // The right side runs first and is kept as the chunks it arrived in, because it is
299                // replayed once per left row. The left side streams, which is the whole point of
300                // this operator: the product is produced a chunk at a time and never held, so the
301                // product stays in the pipeline the left rows came from rather than starting one.
302                let aside = self.pipelines.pipeline(right);
303                let right = self.node(right)?;
304                let left = self.node(left)?;
305                let (keep, kept) = Keep::new(memory);
306                let cross = CrossProduct::new(left.schema(), right.schema(), kept);
307                let schema = cross.schema().clone();
308                let held = self.watch(keep_id, aside, "Keep", None);
309                let counters = self.watch(id, pipeline, "CrossProduct", None);
310                Box::new(Fed::new(
311                    right,
312                    Watched::new(keep, held),
313                    Streamed::new(left, Watched::new(cross, counters), schema),
314                ))
315            }
316            Node::SetOp { left, right, kind, all, index } => {
317                let id = self.id();
318                let gather_id = self.id();
319                // The right side runs first, because nothing can be said about a left row until the
320                // whole right side has been counted. That is the dependency edge, spelled out.
321                let counting = self.pipelines.pipeline(right);
322                let right = self.node(right)?;
323                let left = self.node(left)?;
324                let (gather, gathered) = Gather::new(memory);
325                let (setop, out) = SetOp::new(left.schema(), gathered, kind, all, index, memory);
326                let schema = setop.schema().clone();
327                let kept = self.watch(gather_id, counting, "Gather", None);
328                let counters = self.watch(id, pipeline, "SetOp", None);
329                Box::new(Paired::new(
330                    right,
331                    Watched::new(gather, kept),
332                    left,
333                    Watched::new(setop, counters),
334                    out,
335                    schema,
336                ))
337            }
338        };
339        Ok(Box::new(Guarded::new(inner, self.cancel.clone())))
340    }
341}
342
343/// A leaf source with the adapter that pulls chunks out of it.
344///
345/// Every leaf is a [`Source`] and everything above it still pulls, so this is
346/// where the two meet. The schema is passed in rather than asked for through a trait, because a
347/// source says what it produces on its own type and adding a trait method to say it again would be
348/// a second answer to the same question.
349fn pulled<'a, S: Source + 'a>(source: S, schema: Schema) -> Box<dyn Operator + 'a> {
350    Box::new(Pulled::new(source, schema))
351}