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