Skip to main content

rudb_plan/
node.rs

1//! Logical operators.
2//!
3//! One variant per operator, covering what the M0 binder can produce out of what the transformer
4//! in `rudb-parse` can produce. That is a smaller set than DuckDB's and it is smaller on purpose:
5//! an operator here that nothing constructs is an operator whose textual form, whose validation
6//! and whose rewrite rules have never been run, and the first thing that happens when the binder
7//! finally emits one is that all three turn out to be wrong.
8//!
9//! Every operator that introduces new columns carries a table index, which is the left half of a
10//! [`ColumnBinding`](crate::ColumnBinding). [`Node::Filter`], [`Node::Sort`], [`Node::Limit`],
11//! [`Node::TopN`], [`Node::Distinct`] and [`Node::Join`] do not have one, because they pass their
12//! input's columns through unchanged and a binding that survives a filter should not have to be
13//! rewritten by it.
14
15use crate::{ExprRef, NodeRef, Slice, StrRef};
16
17/// How a window frame measures its bounds.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum WindowUnit {
20    Rows,
21    Range,
22    Groups,
23}
24
25/// One end of a window frame.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum WindowBound {
28    UnboundedPreceding,
29    Preceding(ExprRef),
30    CurrentRow,
31    Following(ExprRef),
32    UnboundedFollowing,
33}
34
35/// Which peers a window frame removes after its bounds are applied.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum WindowExclude {
38    NoOthers,
39    CurrentRow,
40    Group,
41    Ties,
42}
43
44/// The complete frame shared by a compatible run of window expressions.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct WindowFrame {
47    pub unit: WindowUnit,
48    pub start: WindowBound,
49    pub end: WindowBound,
50    pub exclude: WindowExclude,
51}
52
53/// One end of a [`Node::Limit`], which is a row count or an offset.
54///
55/// Nearly every limit written is a number, and a number is what the binder writes down when it can
56/// work one out. What it cannot work out is a subquery, which has to run before there is a value,
57/// and a call that answers differently every time it is made, such as `RANDOM()` or `nextval`. The
58/// pin takes both of those and so does this, by evaluating the expression while the query runs
59/// rather than while it is planned.
60///
61/// [`Bound::Read`] is how. The binder joins the query or the call in underneath as a single row,
62/// which puts its one value in a column of every row the limit sees, and the limit reads that
63/// column off the first chunk that reaches it and uses the number for the rest of the query. The
64/// column is a column the query did not ask for, so the binder puts a projection over the limit
65/// that drops it again.
66///
67/// A [`Bound::Read`] count is why the rewrites that need a number have to check: a limit over a
68/// sort only becomes a [`Node::TopN`] when the count is known while the plan is built, and a limit
69/// cannot move below the projection that produces the column it reads.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum Bound {
72    /// Every row, which is what leaving a `LIMIT` off means. Never an offset.
73    All,
74    /// A number the binder worked out.
75    Rows(u64),
76    /// A column of the input holding the number, the same in every row.
77    Read(ExprRef),
78}
79
80impl Bound {
81    /// The number, when it is one already.
82    #[must_use]
83    pub fn rows(self) -> Option<u64> {
84        match self {
85            Self::Rows(rows) => Some(rows),
86            Self::All | Self::Read(_) => None,
87        }
88    }
89
90    /// The column this reads, when it reads one.
91    #[must_use]
92    pub fn read(self) -> Option<ExprRef> {
93        match self {
94            Self::Read(expr) => Some(expr),
95            Self::All | Self::Rows(_) => None,
96        }
97    }
98}
99
100/// The share a `LIMIT` written as a percentage names.
101///
102/// The two arms are the two things somebody can write. `LIMIT 30 PERCENT` and `LIMIT 30%` are a
103/// number the binder works out, and the check that it is between nought and a hundred happens while
104/// the query is bound. `LIMIT (SELECT 30)%` is a number nobody has before the query runs, so it
105/// arrives as a column of the input exactly the way a [`Bound::Read`] count does, and the range
106/// check moves to where the value turns up.
107///
108/// A share the query wrote as a subquery is always written with the sign rather than the word,
109/// because the grammar refuses `PERCENT` after a closing bracket, in both engines. That is a rule
110/// about spelling and not about what the node can hold.
111#[derive(Debug, Clone, Copy, PartialEq)]
112pub enum Share {
113    /// A percentage the binder worked out, from nought to a hundred.
114    Percent(f64),
115    /// A column of the input holding the percentage, the same in every row.
116    Read(ExprRef),
117}
118
119impl Share {
120    /// The percentage, when it is one already.
121    #[must_use]
122    pub fn percent(self) -> Option<f64> {
123        match self {
124            Self::Percent(percent) => Some(percent),
125            Self::Read(_) => None,
126        }
127    }
128
129    /// The column this reads, when it reads one.
130    #[must_use]
131    pub fn read(self) -> Option<ExprRef> {
132        match self {
133            Self::Read(expr) => Some(expr),
134            Self::Percent(_) => None,
135        }
136    }
137}
138
139/// One logical operator.
140///
141/// Children are the inputs, in the order [`Node::children`] returns them, which is the order they
142/// print in and the order the reader expects.
143///
144/// `PartialEq` and not `Eq`, because [`Node::LimitPercent`] holds a percentage as a `f64`.
145/// [`Value`](rudb_common::Value) is the same shape for the same reason.
146#[derive(Debug, Clone, PartialEq)]
147pub enum Node {
148    /// A base table scan.
149    ///
150    /// The projection is in `columns`, so a scan of two columns of a 105-column table is a two
151    /// column scan in the plan and not a filter over a wide one. `spec/09-optimizer.md` section
152    /// 9.2 calls projection pushdown the difference between 20 GB and 200 MB on ClickBench, and
153    /// this is the field it pushes into.
154    Get {
155        /// The catalog name.
156        catalog: StrRef,
157        /// The schema name.
158        schema: StrRef,
159        /// The table name.
160        table: StrRef,
161        /// The alias the query used, which is what an error message should say.
162        alias: StrRef,
163        /// The table index that this scan's columns bind against.
164        index: u32,
165        /// The projected columns with their types, into the field pool.
166        columns: Slice,
167    },
168    /// One row and no columns.
169    ///
170    /// What `SELECT 1` sits on top of. Not an empty result: an empty result produces no rows and
171    /// `SELECT 1` produces one, and conflating them is how a scalar subquery starts returning
172    /// nothing instead of null.
173    Dummy,
174    /// Literal rows.
175    ///
176    /// Every row has the same length as `columns`, which [`Plan::validate`](crate::Plan::validate)
177    /// checks, because a ragged `VALUES` is a wrong answer rather than a crash.
178    Values {
179        /// The table index that these columns bind against.
180        index: u32,
181        /// The output columns with their types, into the field pool.
182        columns: Slice,
183        /// The rows, into the row pool, each row a slice of the expression list pool.
184        rows: Slice,
185    },
186    /// A function call where a table goes, such as `range(10)`.
187    ///
188    /// The arguments are expressions rather than numbers, because `range(2 + 3)` is a legal call
189    /// and folding it here would mean the plan could not be printed back as what was written. They
190    /// cannot refer to a column: a table function that sees the row on its left is `LATERAL`, and
191    /// that is [`Node::LateralFunction`].
192    ///
193    /// A separate node from [`Node::Values`] even though `range(3)` and `VALUES (0), (1), (2)`
194    /// produce the same rows, because the one that produces three million rows should be three
195    /// numbers in the plan rather than three million expressions in it.
196    TableFunction {
197        /// The table index that this call's columns bind against.
198        index: u32,
199        /// Which function, as its own canonical name.
200        function: StrRef,
201        /// The arguments, into the expression list pool.
202        args: Slice,
203        /// The names of the named parameters the call was written with, into the name pool.
204        ///
205        /// `read_csv('f.csv', delim=';')` keeps the `delim` here rather than only in whatever the
206        /// binder made of it, because the executor opens the file a second time and has to open it
207        /// the same way. A parameter the binder answers on its own, such as `binary_as_string`,
208        /// is here too, so that a plan prints back as the call that was written.
209        options: Slice,
210        /// What each of those names was given, into the expression list pool and the same length.
211        ///
212        /// Constants, every one of them. The binder refuses anything else, because a parameter can
213        /// decide what the columns are and the columns are settled there.
214        settings: Slice,
215        /// The produced columns with their types, into the field pool.
216        columns: Slice,
217    },
218    /// A table function evaluated once per row of its input, which is what `LATERAL` means.
219    ///
220    /// `FROM t, range(t.n)` is this. A table function's arguments are what produce its rows rather
221    /// than something read over rows that already exist, so there is nothing underneath one for a
222    /// domain to be pushed into and nothing the rules in the unnesting pass can rewrite it into.
223    /// This is the operator those rules stop at: the domain goes in on the left, the arguments read
224    /// it, and the call is made once per row of it.
225    ///
226    /// The output is the input's columns followed by the function's, which is a cross product whose
227    /// right side is allowed to change per left row. That is what lets the join putting the rows
228    /// back beside their outer row sit above this and read the domain columns where it reads them
229    /// everywhere else.
230    ///
231    /// Only the series family reaches here. A reader takes a file name, the binder settles the
232    /// columns by opening the file, and a name that is not a constant is refused there, so a
233    /// correlated `read_csv` never gets this far.
234    LateralFunction {
235        /// The rows the call is made against, one call per row.
236        input: NodeRef,
237        /// The table index that this call's columns bind against.
238        index: u32,
239        /// Which function, as its own canonical name.
240        function: StrRef,
241        /// The arguments, into the expression list pool, read against a row of `input`.
242        args: Slice,
243        /// The names of the named parameters the call was written with, into the name pool.
244        options: Slice,
245        /// What each of those names was given, into the expression list pool and the same length.
246        settings: Slice,
247        /// The produced columns with their types, into the field pool, not counting the input's.
248        columns: Slice,
249    },
250    /// A predicate over the input, keeping the rows where it is true.
251    ///
252    /// True, not "not false". A null predicate drops the row, which is SQL's rule and is the
253    /// difference between `WHERE` and `CHECK`.
254    Filter {
255        /// The input.
256        input: NodeRef,
257        /// The predicate, which has to be `BOOLEAN`.
258        predicate: ExprRef,
259    },
260    /// A projection, producing a new set of columns from the input's.
261    Project {
262        /// The input.
263        input: NodeRef,
264        /// The table index the produced columns bind against.
265        index: u32,
266        /// The expressions, into the expression list pool.
267        exprs: Slice,
268        /// One output name per expression, into the name list pool.
269        ///
270        /// Names are carried through the whole plan rather than attached at the root, because the
271        /// thing a person reads a plan dump to answer is usually which column this is, and a dump
272        /// with the names stripped out answers that with a number.
273        names: Slice,
274    },
275    /// A grouped or ungrouped aggregation.
276    ///
277    /// The output is the group expressions followed by the aggregates, in that order, and that is
278    /// what a binding into `index` means. An ungrouped aggregate has an empty `groups` and still
279    /// produces exactly one row, including over an empty input.
280    Aggregate {
281        /// The input.
282        input: NodeRef,
283        /// The table index the produced columns bind against.
284        index: u32,
285        /// The group expressions, into the expression list pool.
286        groups: Slice,
287        /// The aggregate expressions, into the expression list pool. Every element is an
288        /// [`Expr::Aggregate`](crate::Expr::Aggregate) and this is the only place one may appear.
289        aggregates: Slice,
290    },
291    /// Window expressions that share one partition, ordering, and frame.
292    Window {
293        /// Rows over which the windows are evaluated.
294        input: NodeRef,
295        /// The table index of the appended window result columns.
296        index: u32,
297        /// Expressions that divide the input into independent partitions.
298        partition: Slice,
299        /// The ordering within each partition.
300        order: Slice,
301        /// The complete frame shared by this compatible expression run.
302        frame: WindowFrame,
303        /// Direct [`Expr::Window`](crate::Expr::Window) expressions appended to the input columns.
304        expressions: Slice,
305    },
306    /// An ordering.
307    Sort {
308        /// The input.
309        input: NodeRef,
310        /// The keys in priority order, into the sort key pool.
311        keys: Slice,
312    },
313    /// A row count limit and an offset.
314    ///
315    /// Both are a [`Bound`], which is a number when the query said one and a column of the input
316    /// when it wrote something the binder could not settle. See [`Bound`] for what puts the value
317    /// in that column and who reads it.
318    Limit {
319        /// The input.
320        input: NodeRef,
321        /// How many rows to emit, or all of them.
322        count: Bound,
323        /// How many rows to skip first.
324        offset: Bound,
325    },
326    /// A limit written as a share of the input rather than as a row count.
327    ///
328    /// `LIMIT 30 PERCENT` over ten rows is three rows, and it is a node of its own rather than a
329    /// [`Node::Limit`] with another field for three reasons. The share is of the whole input, so
330    /// this cannot emit anything until it has counted every row, where a plain limit hands each
331    /// chunk on as it arrives and stops the scan early. The rewrites that fire on a plain limit are
332    /// wrong here: a filter pushed under this one changes how many rows there are to take a share
333    /// of, and the sort underneath it cannot become a top n because the count is not known until
334    /// the sort has finished. And the pin builds a separate `Limit Percent` operator for it, which
335    /// is the same split one layer down.
336    ///
337    /// A share the binder worked out is between nought and a hundred inclusive, checked while the
338    /// query is bound, because that is where the pin refuses `LIMIT 101 PERCENT` too. The offset is
339    /// applied after the share has been worked out, so `LIMIT 30 PERCENT OFFSET 2` over ten rows is
340    /// three rows starting at the third.
341    ///
342    /// Both fields can be read off the rows instead of being a number written down here, for the
343    /// same reason a plain limit's count can. `LIMIT (SELECT 30)% OFFSET (SELECT 2)` holds two
344    /// numbers nobody has before the query runs, so each arrives as a column of the input and is
345    /// read off the first chunk. See [`Share`] and [`Bound`].
346    LimitPercent {
347        /// The input.
348        input: NodeRef,
349        /// The share of the input to emit, from nought to a hundred.
350        percent: Share,
351        /// How many rows to skip first. Never [`Bound::All`], which is not an offset.
352        offset: Bound,
353    },
354    /// A sort with a limit over it, which never holds more rows than the limit can emit.
355    ///
356    /// The same answer as a [`Node::Limit`] over a [`Node::Sort`] and a different amount of work.
357    /// A sort has to see every row before it can emit the first one, so it holds the whole input;
358    /// this holds the rows that could still come out and throws the rest away as it goes, which on
359    /// `ORDER BY x LIMIT 10` over a hundred million rows is ten rows rather than a hundred million.
360    ///
361    /// `count` is not optional, because `LIMIT ALL` over a sort is a sort and there would be nothing
362    /// to bound. The offset is part of the node rather than left above it, since the rows that are
363    /// skipped still have to be found to be skipped, so what this has to keep is `count + offset`.
364    TopN {
365        /// The input.
366        input: NodeRef,
367        /// The keys in priority order, into the sort key pool.
368        keys: Slice,
369        /// How many rows to emit.
370        count: u64,
371        /// How many rows to skip first.
372        offset: u64,
373    },
374    /// The columns of rows something below already picked out, read back from the file by ordinal.
375    ///
376    /// This is the top half of late materialisation. A `SELECT * FROM hits ORDER BY EventTime LIMIT
377    /// 10` over a hundred and five columns needs one column to decide which ten rows win and all
378    /// hundred and five of those ten rows afterwards, and a plan that carries the wide rows through
379    /// the top N reads the whole file to throw almost all of it away. The rewrite in
380    /// `rudb-opt`'s `late` module narrows the scan under the top N to the ordering columns plus the
381    /// row's ordinal inside its file, and puts this above it to read the rest for the rows that
382    /// survived.
383    ///
384    /// The ordinals come out of the input rather than being counted here, because the operator that
385    /// counted them is the scan and everything between the scan and here may have dropped rows. The
386    /// column that holds them is [`Self::Fetch::row`], and the scan produced it because the rewrite
387    /// turned `file_row_number` on.
388    ///
389    /// The produced columns are the whole row and not only the deferred part, so the answer is one
390    /// read of the file at the ordinals rather than a stitch of what was carried with what was
391    /// fetched. That costs the ordering column a second read of a few pages and saves the plan above
392    /// this from having any idea the rewrite happened.
393    Fetch {
394        /// The input, which carries each row's ordinal inside the file.
395        input: NodeRef,
396        /// The table index the produced columns bind against, which is the one the node this
397        /// replaced produced, so that nothing above has to be rebound.
398        index: u32,
399        /// The file, into the expression list pool. One constant path, because a row ordinal only
400        /// says which row when there is one file it could be in.
401        args: Slice,
402        /// The produced columns with their types, into the field pool.
403        columns: Slice,
404        /// The input column holding the ordinal, which has to be `BIGINT`.
405        row: ExprRef,
406    },
407    /// Rows of a catalog table read back by their table-wide ordinal.
408    TableFetch {
409        input: NodeRef,
410        index: u32,
411        catalog: StrRef,
412        schema: StrRef,
413        table: StrRef,
414        columns: Slice,
415        row: ExprRef,
416    },
417    /// Duplicate elimination, over the whole row or over named expressions.
418    Distinct {
419        /// The input.
420        input: NodeRef,
421        /// The `DISTINCT ON` expressions, into the expression list pool. Empty means the whole
422        /// row, which is plain `DISTINCT`.
423        on: Slice,
424    },
425    /// A join with a condition.
426    Join {
427        /// The left input.
428        left: NodeRef,
429        /// The right input.
430        right: NodeRef,
431        /// Which join.
432        kind: JoinKind,
433        /// The conditions, into the expression list pool, combined with `AND`. Empty is a join
434        /// with no condition, which for an inner join is a cross product and for an outer join
435        /// is not.
436        conditions: Slice,
437        /// Which input is gathered whole before the other one starts.
438        ///
439        /// The binder emits [`BuildSide::Right`] for everything, because at binding time there is
440        /// nothing to choose with. `rudb_opt`'s `sides` pass overwrites it from an estimate, and
441        /// the executor honours whatever it finds here.
442        build: BuildSide,
443    },
444    /// A join whose right input can refer to columns produced by its left input.
445    ///
446    /// Binding emits this for a correlated subquery. The unnesting pass has to replace every one
447    /// before execution, so the executor never evaluates the right input once per left row.
448    DependentJoin {
449        /// The outer input whose columns the right side may reference.
450        left: NodeRef,
451        /// The correlated input.
452        right: NodeRef,
453        /// Which result shape the subquery needs.
454        kind: JoinKind,
455        /// Conditions introduced while binding the subquery.
456        conditions: Slice,
457    },
458    /// An unconditional cross product.
459    ///
460    /// Separate from a [`Node::Join`] with no conditions because join ordering treats them
461    /// differently: a cross product has no edge in the join graph and section 9.4's dynamic
462    /// program enumerates connected subgraphs.
463    CrossProduct {
464        /// The left input.
465        left: NodeRef,
466        /// The right input.
467        right: NodeRef,
468    },
469    /// A `WITH name AS MATERIALIZED (...)`, which is run once and read wherever it is named.
470    ///
471    /// The left input is the definition and the right input is the query that reads it. They are
472    /// in that order because that is the order they run in: the definition is a pipeline breaker
473    /// whichever operators are in it, since nothing above may start until the rows are all there.
474    ///
475    /// A plain `WITH` is not this. The reference binary inlines one at every use whatever its
476    /// shape and however many times it is named, and the only decision left is whether the rows
477    /// are needed at all, which is why an unused one is dropped rather than run for nothing.
478    MaterializedCte {
479        /// The query whose rows are held.
480        definition: NodeRef,
481        /// The query that reads them, which is where every [`Node::CteScan`] for this one is.
482        body: NodeRef,
483        /// The name it was written with, which is what the printer and an error message say.
484        name: StrRef,
485        /// Which materialisation this is, matching the `cte` of the scans that read it.
486        ///
487        /// A number of its own rather than the table index, because a scan binds against its own
488        /// index and two scans of one materialisation have two of those.
489        cte: u32,
490        /// The held columns with their types, into the field pool.
491        columns: Slice,
492    },
493    /// A read of a [`Node::MaterializedCte`] that has already run.
494    ///
495    /// A leaf, the same way a table scan is. What it reads was computed by a node above it rather
496    /// than by a node under it, which is the one place in the plan where that is true, and it is
497    /// why the materialisation holds its body as an input rather than sitting beside it.
498    CteScan {
499        /// The table index that this read's columns bind against.
500        index: u32,
501        /// Which materialisation it reads.
502        cte: u32,
503        /// The name it was written with.
504        name: StrRef,
505        /// The produced columns with their types, into the field pool.
506        columns: Slice,
507    },
508    /// `UNION`, `EXCEPT` or `INTERSECT`.
509    SetOp {
510        /// The left input.
511        left: NodeRef,
512        /// The right input.
513        right: NodeRef,
514        /// Which operation.
515        kind: SetOpKind,
516        /// Whether duplicates are kept.
517        all: bool,
518        /// The table index the produced columns bind against, since the output is neither side's
519        /// columns.
520        index: u32,
521    },
522}
523
524impl Node {
525    /// The keyword this operator prints as, which is also what the reader dispatches on.
526    #[must_use]
527    pub fn keyword(&self) -> &'static str {
528        match self {
529            Self::Get { .. } => "Get",
530            Self::Dummy => "Dummy",
531            Self::Values { .. } => "Values",
532            Self::TableFunction { .. } => "TableFunction",
533            Self::LateralFunction { .. } => "LateralFunction",
534            Self::Filter { .. } => "Filter",
535            Self::Project { .. } => "Project",
536            Self::Aggregate { .. } => "Aggregate",
537            Self::Window { .. } => "Window",
538            Self::Sort { .. } => "Sort",
539            Self::Limit { .. } => "Limit",
540            Self::LimitPercent { .. } => "LimitPercent",
541            Self::TopN { .. } => "TopN",
542            Self::Fetch { .. } => "Fetch",
543            Self::TableFetch { .. } => "TableFetch",
544            Self::Distinct { .. } => "Distinct",
545            Self::Join { .. } => "Join",
546            Self::DependentJoin { .. } => "DependentJoin",
547            Self::CrossProduct { .. } => "CrossProduct",
548            Self::MaterializedCte { .. } => "MaterializedCte",
549            Self::CteScan { .. } => "CteScan",
550            Self::SetOp { .. } => "SetOp",
551        }
552    }
553
554    /// The inputs, in printing order.
555    ///
556    /// Two slots rather than a `Vec`, because no logical operator in this set has three inputs and
557    /// the printer walks this on every node of every dump. A caller wants
558    /// `node.children().into_iter().flatten()`.
559    #[must_use]
560    pub fn children(&self) -> [Option<NodeRef>; 2] {
561        match *self {
562            Self::Get { .. }
563            | Self::Dummy
564            | Self::Values { .. }
565            | Self::TableFunction { .. }
566            | Self::CteScan { .. } => [None, None],
567            Self::Filter { input, .. }
568            | Self::Project { input, .. }
569            | Self::Aggregate { input, .. }
570            | Self::Window { input, .. }
571            | Self::Sort { input, .. }
572            | Self::Limit { input, .. }
573            | Self::LimitPercent { input, .. }
574            | Self::TopN { input, .. }
575            | Self::Fetch { input, .. }
576            | Self::TableFetch { input, .. }
577            | Self::Distinct { input, .. }
578            | Self::LateralFunction { input, .. } => [Some(input), None],
579            Self::Join { left, right, .. }
580            | Self::DependentJoin { left, right, .. }
581            | Self::CrossProduct { left, right }
582            | Self::SetOp { left, right, .. } => [Some(left), Some(right)],
583            Self::MaterializedCte { definition, body, .. } => [Some(definition), Some(body)],
584        }
585    }
586
587    /// How many inputs this operator takes.
588    #[must_use]
589    pub fn arity(&self) -> usize {
590        self.children().into_iter().flatten().count()
591    }
592
593    /// The table index this operator introduces, if it introduces one.
594    #[must_use]
595    pub fn table_index(&self) -> Option<u32> {
596        match *self {
597            Self::Get { index, .. }
598            | Self::Values { index, .. }
599            | Self::TableFunction { index, .. }
600            | Self::LateralFunction { index, .. }
601            | Self::Project { index, .. }
602            | Self::Fetch { index, .. }
603            | Self::TableFetch { index, .. }
604            | Self::Aggregate { index, .. }
605            | Self::Window { index, .. }
606            | Self::CteScan { index, .. }
607            | Self::SetOp { index, .. } => Some(index),
608            _ => None,
609        }
610    }
611}
612
613/// Which join.
614///
615/// `Semi` and `Anti` are here because subquery unnesting produces them directly, per section 9.2,
616/// and a semi join expressed as a join plus a distinct is a semi join the executor cannot
617/// recognise.
618#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
619pub enum JoinKind {
620    /// Rows that match on both sides.
621    Inner,
622    /// Every left row, padded with nulls where the right does not match.
623    Left,
624    /// Every right row, padded with nulls where the left does not match.
625    Right,
626    /// Both of the above at once.
627    Full,
628    /// Left rows that have at least one match, each emitted once.
629    Semi,
630    /// Left rows that have no match.
631    Anti,
632    /// Left rows paired with their match, or with nulls, at most one right row each. What a
633    /// correlated scalar subquery unnests to.
634    Single,
635    /// Every left row plus a nullable boolean saying whether its condition matched the right side.
636    /// A null means no row matched and at least one comparison was unknown.
637    Mark,
638    /// The nth left row with the nth right row, which is DuckDB's `POSITIONAL JOIN`.
639    Positional,
640}
641
642impl JoinKind {
643    /// The spelling used in the textual form.
644    #[must_use]
645    pub fn keyword(self) -> &'static str {
646        match self {
647            Self::Inner => "INNER",
648            Self::Left => "LEFT",
649            Self::Right => "RIGHT",
650            Self::Full => "FULL",
651            Self::Semi => "SEMI",
652            Self::Anti => "ANTI",
653            Self::Single => "SINGLE",
654            Self::Mark => "MARK",
655            Self::Positional => "POSITIONAL",
656        }
657    }
658
659    /// Every join kind, which is what the reader searches.
660    pub(crate) const ALL: [Self; 9] = [
661        Self::Inner,
662        Self::Left,
663        Self::Right,
664        Self::Full,
665        Self::Semi,
666        Self::Anti,
667        Self::Single,
668        Self::Mark,
669        Self::Positional,
670    ];
671
672    /// The same join with its two inputs the other way round, for the kinds where there is one.
673    ///
674    /// Swapping the inputs of a `LEFT` join makes a `RIGHT` join and the other way round, because
675    /// the kind names a side. `INNER` and `FULL` name neither and are their own mirror. The rest
676    /// return `None`: `SEMI`, `ANTI`, `SINGLE` and `MARK` produce the left side's rows, or a
677    /// column about them, so their left input is not a side but the subject, and `POSITIONAL`
678    /// pairs the nth with the nth, which no reordering of one input preserves.
679    #[must_use]
680    pub fn mirrored(self) -> Option<Self> {
681        match self {
682            Self::Inner => Some(Self::Inner),
683            Self::Left => Some(Self::Right),
684            Self::Right => Some(Self::Left),
685            Self::Full => Some(Self::Full),
686            Self::Semi | Self::Anti | Self::Single | Self::Mark | Self::Positional => None,
687        }
688    }
689}
690
691/// Which input of a join is gathered whole before the other one starts.
692///
693/// A join is two inputs and a dependency edge between them: one side is finished and held, and then
694/// the other side's rows are matched against what was held. This says which side that is. It is
695/// where the hash table goes when the hash join in #62 lands, and it is the side today's nested
696/// loop turns into chunks and rescans once per row of the other one.
697///
698/// Which side that should be is not a property of the join and is not decided here. It is decided
699/// by [`sides`](../../rudb_opt/sides/index.html) from a cardinality estimate, and the rule it uses
700/// belongs to whichever operator is reading this, not to the flag.
701#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
702pub enum BuildSide {
703    /// The right input, which is what the binder emits and what every join did before this existed.
704    #[default]
705    Right,
706    /// The left input, which means the executor swaps the two and puts the answer back in order.
707    Left,
708}
709
710impl BuildSide {
711    /// The spelling used in the textual form.
712    #[must_use]
713    pub fn keyword(self) -> &'static str {
714        match self {
715            Self::Right => "right",
716            Self::Left => "left",
717        }
718    }
719
720    /// Both sides, which is what the reader searches.
721    pub(crate) const ALL: [Self; 2] = [Self::Right, Self::Left];
722}
723
724/// Which set operation.
725#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
726pub enum SetOpKind {
727    /// Rows from either side.
728    Union,
729    /// Rows from the left that are not on the right.
730    Except,
731    /// Rows on both sides.
732    Intersect,
733}
734
735impl SetOpKind {
736    /// The spelling used in the textual form.
737    #[must_use]
738    pub fn keyword(self) -> &'static str {
739        match self {
740            Self::Union => "UNION",
741            Self::Except => "EXCEPT",
742            Self::Intersect => "INTERSECT",
743        }
744    }
745
746    /// Every set operation, which is what the reader searches.
747    pub(crate) const ALL: [Self; 3] = [Self::Union, Self::Except, Self::Intersect];
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753    use crate::Slice;
754
755    /// Every node in one list, so that a variant added without a keyword, without a child slot or
756    /// without an entry in the reader's dispatch table fails here rather than at the first dump
757    /// that happens to contain one.
758    fn one_of_each() -> Vec<Node> {
759        vec![
760            Node::Get {
761                catalog: 0,
762                schema: 0,
763                table: 0,
764                alias: 0,
765                index: 0,
766                columns: Slice::EMPTY,
767            },
768            Node::Dummy,
769            Node::Values { index: 0, columns: Slice::EMPTY, rows: Slice::EMPTY },
770            Node::TableFunction {
771                index: 0,
772                function: 0,
773                args: Slice::EMPTY,
774                options: Slice::EMPTY,
775                settings: Slice::EMPTY,
776                columns: Slice::EMPTY,
777            },
778            Node::LateralFunction {
779                input: 0,
780                index: 0,
781                function: 0,
782                args: Slice::EMPTY,
783                options: Slice::EMPTY,
784                settings: Slice::EMPTY,
785                columns: Slice::EMPTY,
786            },
787            Node::Filter { input: 0, predicate: 0 },
788            Node::Project { input: 0, index: 0, exprs: Slice::EMPTY, names: Slice::EMPTY },
789            Node::Aggregate { input: 0, index: 0, groups: Slice::EMPTY, aggregates: Slice::EMPTY },
790            Node::Sort { input: 0, keys: Slice::EMPTY },
791            Node::Limit { input: 0, count: Bound::All, offset: Bound::Rows(0) },
792            Node::LimitPercent { input: 0, percent: Share::Percent(50.0), offset: Bound::Rows(0) },
793            Node::Distinct { input: 0, on: Slice::EMPTY },
794            Node::Join {
795                left: 0,
796                right: 1,
797                kind: JoinKind::Inner,
798                conditions: Slice::EMPTY,
799                build: BuildSide::default(),
800            },
801            Node::DependentJoin {
802                left: 0,
803                right: 1,
804                kind: JoinKind::Single,
805                conditions: Slice::EMPTY,
806            },
807            Node::CrossProduct { left: 0, right: 1 },
808            Node::SetOp { left: 0, right: 1, kind: SetOpKind::Union, all: true, index: 0 },
809        ]
810    }
811
812    #[test]
813    fn every_operator_has_its_own_keyword() {
814        let mut keywords: Vec<&str> = one_of_each().iter().map(Node::keyword).collect();
815        let count = keywords.len();
816        keywords.sort_unstable();
817        keywords.dedup();
818        assert_eq!(keywords.len(), count, "two operators print the same keyword");
819    }
820
821    #[test]
822    fn arity_agrees_with_the_child_slots() {
823        for node in one_of_each() {
824            let counted = node.children().into_iter().flatten().count();
825            assert_eq!(node.arity(), counted, "{} disagrees with itself", node.keyword());
826        }
827    }
828
829    /// A child slot that is `None` before a slot that is `Some` would make the printer emit the
830    /// right input as the left one, and the reader would accept it.
831    #[test]
832    fn the_child_slots_are_filled_from_the_front() {
833        for node in one_of_each() {
834            let slots = node.children();
835            assert!(
836                !(slots[0].is_none() && slots[1].is_some()),
837                "{} has a right input and no left one",
838                node.keyword()
839            );
840        }
841    }
842
843    #[test]
844    fn only_the_operators_that_introduce_columns_have_a_table_index() {
845        for node in one_of_each() {
846            let expected = matches!(
847                node,
848                Node::Get { .. }
849                    | Node::Values { .. }
850                    | Node::TableFunction { .. }
851                    | Node::LateralFunction { .. }
852                    | Node::Project { .. }
853                    | Node::Aggregate { .. }
854                    | Node::SetOp { .. }
855            );
856            assert_eq!(
857                node.table_index().is_some(),
858                expected,
859                "{} is on the wrong side of the table index rule",
860                node.keyword()
861            );
862        }
863    }
864
865    #[test]
866    fn every_join_kind_and_set_operation_is_in_the_list_the_reader_searches() {
867        assert_eq!(JoinKind::ALL.len(), 9);
868        assert_eq!(SetOpKind::ALL.len(), 3);
869        let mut names: Vec<&str> = JoinKind::ALL.iter().map(|k| k.keyword()).collect();
870        names.sort_unstable();
871        names.dedup();
872        assert_eq!(names.len(), JoinKind::ALL.len(), "two join kinds print the same keyword");
873    }
874}