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 logical operator.
54///
55/// Children are the inputs, in the order [`Node::children`] returns them, which is the order they
56/// print in and the order the reader expects.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum Node {
59    /// A base table scan.
60    ///
61    /// The projection is in `columns`, so a scan of two columns of a 105-column table is a two
62    /// column scan in the plan and not a filter over a wide one. `spec/09-optimizer.md` section
63    /// 9.2 calls projection pushdown the difference between 20 GB and 200 MB on ClickBench, and
64    /// this is the field it pushes into.
65    Get {
66        /// The catalog name.
67        catalog: StrRef,
68        /// The schema name.
69        schema: StrRef,
70        /// The table name.
71        table: StrRef,
72        /// The alias the query used, which is what an error message should say.
73        alias: StrRef,
74        /// The table index that this scan's columns bind against.
75        index: u32,
76        /// The projected columns with their types, into the field pool.
77        columns: Slice,
78    },
79    /// One row and no columns.
80    ///
81    /// What `SELECT 1` sits on top of. Not an empty result: an empty result produces no rows and
82    /// `SELECT 1` produces one, and conflating them is how a scalar subquery starts returning
83    /// nothing instead of null.
84    Dummy,
85    /// Literal rows.
86    ///
87    /// Every row has the same length as `columns`, which [`Plan::validate`](crate::Plan::validate)
88    /// checks, because a ragged `VALUES` is a wrong answer rather than a crash.
89    Values {
90        /// The table index that these columns bind against.
91        index: u32,
92        /// The output columns with their types, into the field pool.
93        columns: Slice,
94        /// The rows, into the row pool, each row a slice of the expression list pool.
95        rows: Slice,
96    },
97    /// A function call where a table goes, such as `range(10)`.
98    ///
99    /// The arguments are expressions rather than numbers, because `range(2 + 3)` is a legal call
100    /// and folding it here would mean the plan could not be printed back as what was written. They
101    /// cannot refer to a column: a table function that sees the row on its left is `LATERAL`, which
102    /// is a different node and is not here yet.
103    ///
104    /// A separate node from [`Node::Values`] even though `range(3)` and `VALUES (0), (1), (2)`
105    /// produce the same rows, because the one that produces three million rows should be three
106    /// numbers in the plan rather than three million expressions in it.
107    TableFunction {
108        /// The table index that this call's columns bind against.
109        index: u32,
110        /// Which function, as its own canonical name.
111        function: StrRef,
112        /// The arguments, into the expression list pool.
113        args: Slice,
114        /// The names of the named parameters the call was written with, into the name pool.
115        ///
116        /// `read_csv('f.csv', delim=';')` keeps the `delim` here rather than only in whatever the
117        /// binder made of it, because the executor opens the file a second time and has to open it
118        /// the same way. A parameter the binder answers on its own, such as `binary_as_string`,
119        /// is here too, so that a plan prints back as the call that was written.
120        options: Slice,
121        /// What each of those names was given, into the expression list pool and the same length.
122        ///
123        /// Constants, every one of them. The binder refuses anything else, because a parameter can
124        /// decide what the columns are and the columns are settled there.
125        settings: Slice,
126        /// The produced columns with their types, into the field pool.
127        columns: Slice,
128    },
129    /// A predicate over the input, keeping the rows where it is true.
130    ///
131    /// True, not "not false". A null predicate drops the row, which is SQL's rule and is the
132    /// difference between `WHERE` and `CHECK`.
133    Filter {
134        /// The input.
135        input: NodeRef,
136        /// The predicate, which has to be `BOOLEAN`.
137        predicate: ExprRef,
138    },
139    /// A projection, producing a new set of columns from the input's.
140    Project {
141        /// The input.
142        input: NodeRef,
143        /// The table index the produced columns bind against.
144        index: u32,
145        /// The expressions, into the expression list pool.
146        exprs: Slice,
147        /// One output name per expression, into the name list pool.
148        ///
149        /// Names are carried through the whole plan rather than attached at the root, because the
150        /// thing a person reads a plan dump to answer is usually which column this is, and a dump
151        /// with the names stripped out answers that with a number.
152        names: Slice,
153    },
154    /// A grouped or ungrouped aggregation.
155    ///
156    /// The output is the group expressions followed by the aggregates, in that order, and that is
157    /// what a binding into `index` means. An ungrouped aggregate has an empty `groups` and still
158    /// produces exactly one row, including over an empty input.
159    Aggregate {
160        /// The input.
161        input: NodeRef,
162        /// The table index the produced columns bind against.
163        index: u32,
164        /// The group expressions, into the expression list pool.
165        groups: Slice,
166        /// The aggregate expressions, into the expression list pool. Every element is an
167        /// [`Expr::Aggregate`](crate::Expr::Aggregate) and this is the only place one may appear.
168        aggregates: Slice,
169    },
170    /// Window expressions that share one partition, ordering, and frame.
171    Window {
172        /// Rows over which the windows are evaluated.
173        input: NodeRef,
174        /// The table index of the appended window result columns.
175        index: u32,
176        /// Expressions that divide the input into independent partitions.
177        partition: Slice,
178        /// The ordering within each partition.
179        order: Slice,
180        /// The complete frame shared by this compatible expression run.
181        frame: WindowFrame,
182        /// Direct [`Expr::Window`](crate::Expr::Window) expressions appended to the input columns.
183        expressions: Slice,
184    },
185    /// An ordering.
186    Sort {
187        /// The input.
188        input: NodeRef,
189        /// The keys in priority order, into the sort key pool.
190        keys: Slice,
191    },
192    /// A row count limit and an offset.
193    ///
194    /// Both are constants. `LIMIT` over an expression is legal SQL and DuckDB evaluates it before
195    /// the plan runs, so by the time it is here it is a number or the query did not bind.
196    Limit {
197        /// The input.
198        input: NodeRef,
199        /// How many rows to emit, or all of them.
200        count: Option<u64>,
201        /// How many rows to skip first.
202        offset: u64,
203    },
204    /// A sort with a limit over it, which never holds more rows than the limit can emit.
205    ///
206    /// The same answer as a [`Node::Limit`] over a [`Node::Sort`] and a different amount of work.
207    /// A sort has to see every row before it can emit the first one, so it holds the whole input;
208    /// this holds the rows that could still come out and throws the rest away as it goes, which on
209    /// `ORDER BY x LIMIT 10` over a hundred million rows is ten rows rather than a hundred million.
210    ///
211    /// `count` is not optional, because `LIMIT ALL` over a sort is a sort and there would be nothing
212    /// to bound. The offset is part of the node rather than left above it, since the rows that are
213    /// skipped still have to be found to be skipped, so what this has to keep is `count + offset`.
214    TopN {
215        /// The input.
216        input: NodeRef,
217        /// The keys in priority order, into the sort key pool.
218        keys: Slice,
219        /// How many rows to emit.
220        count: u64,
221        /// How many rows to skip first.
222        offset: u64,
223    },
224    /// The columns of rows something below already picked out, read back from the file by ordinal.
225    ///
226    /// This is the top half of late materialisation. A `SELECT * FROM hits ORDER BY EventTime LIMIT
227    /// 10` over a hundred and five columns needs one column to decide which ten rows win and all
228    /// hundred and five of those ten rows afterwards, and a plan that carries the wide rows through
229    /// the top N reads the whole file to throw almost all of it away. The rewrite in
230    /// `rudb-opt`'s `late` module narrows the scan under the top N to the ordering columns plus the
231    /// row's ordinal inside its file, and puts this above it to read the rest for the rows that
232    /// survived.
233    ///
234    /// The ordinals come out of the input rather than being counted here, because the operator that
235    /// counted them is the scan and everything between the scan and here may have dropped rows. The
236    /// column that holds them is [`Self::Fetch::row`], and the scan produced it because the rewrite
237    /// turned `file_row_number` on.
238    ///
239    /// The produced columns are the whole row and not only the deferred part, so the answer is one
240    /// read of the file at the ordinals rather than a stitch of what was carried with what was
241    /// fetched. That costs the ordering column a second read of a few pages and saves the plan above
242    /// this from having any idea the rewrite happened.
243    Fetch {
244        /// The input, which carries each row's ordinal inside the file.
245        input: NodeRef,
246        /// The table index the produced columns bind against, which is the one the node this
247        /// replaced produced, so that nothing above has to be rebound.
248        index: u32,
249        /// The file, into the expression list pool. One constant path, because a row ordinal only
250        /// says which row when there is one file it could be in.
251        args: Slice,
252        /// The produced columns with their types, into the field pool.
253        columns: Slice,
254        /// The input column holding the ordinal, which has to be `BIGINT`.
255        row: ExprRef,
256    },
257    /// Rows of a catalog table read back by their table-wide ordinal.
258    TableFetch {
259        input: NodeRef,
260        index: u32,
261        catalog: StrRef,
262        schema: StrRef,
263        table: StrRef,
264        columns: Slice,
265        row: ExprRef,
266    },
267    /// Duplicate elimination, over the whole row or over named expressions.
268    Distinct {
269        /// The input.
270        input: NodeRef,
271        /// The `DISTINCT ON` expressions, into the expression list pool. Empty means the whole
272        /// row, which is plain `DISTINCT`.
273        on: Slice,
274    },
275    /// A join with a condition.
276    Join {
277        /// The left input.
278        left: NodeRef,
279        /// The right input.
280        right: NodeRef,
281        /// Which join.
282        kind: JoinKind,
283        /// The conditions, into the expression list pool, combined with `AND`. Empty is a join
284        /// with no condition, which for an inner join is a cross product and for an outer join
285        /// is not.
286        conditions: Slice,
287        /// Which input is gathered whole before the other one starts.
288        ///
289        /// The binder emits [`BuildSide::Right`] for everything, because at binding time there is
290        /// nothing to choose with. `rudb_opt`'s `sides` pass overwrites it from an estimate, and
291        /// the executor honours whatever it finds here.
292        build: BuildSide,
293    },
294    /// A join whose right input can refer to columns produced by its left input.
295    ///
296    /// Binding emits this for a correlated subquery. The unnesting pass has to replace every one
297    /// before execution, so the executor never evaluates the right input once per left row.
298    DependentJoin {
299        /// The outer input whose columns the right side may reference.
300        left: NodeRef,
301        /// The correlated input.
302        right: NodeRef,
303        /// Which result shape the subquery needs.
304        kind: JoinKind,
305        /// Conditions introduced while binding the subquery.
306        conditions: Slice,
307    },
308    /// An unconditional cross product.
309    ///
310    /// Separate from a [`Node::Join`] with no conditions because join ordering treats them
311    /// differently: a cross product has no edge in the join graph and section 9.4's dynamic
312    /// program enumerates connected subgraphs.
313    CrossProduct {
314        /// The left input.
315        left: NodeRef,
316        /// The right input.
317        right: NodeRef,
318    },
319    /// A `WITH name AS MATERIALIZED (...)`, which is run once and read wherever it is named.
320    ///
321    /// The left input is the definition and the right input is the query that reads it. They are
322    /// in that order because that is the order they run in: the definition is a pipeline breaker
323    /// whichever operators are in it, since nothing above may start until the rows are all there.
324    ///
325    /// A plain `WITH` is not this. The reference binary inlines one at every use whatever its
326    /// shape and however many times it is named, and the only decision left is whether the rows
327    /// are needed at all, which is why an unused one is dropped rather than run for nothing.
328    MaterializedCte {
329        /// The query whose rows are held.
330        definition: NodeRef,
331        /// The query that reads them, which is where every [`Node::CteScan`] for this one is.
332        body: NodeRef,
333        /// The name it was written with, which is what the printer and an error message say.
334        name: StrRef,
335        /// Which materialisation this is, matching the `cte` of the scans that read it.
336        ///
337        /// A number of its own rather than the table index, because a scan binds against its own
338        /// index and two scans of one materialisation have two of those.
339        cte: u32,
340        /// The held columns with their types, into the field pool.
341        columns: Slice,
342    },
343    /// A read of a [`Node::MaterializedCte`] that has already run.
344    ///
345    /// A leaf, the same way a table scan is. What it reads was computed by a node above it rather
346    /// than by a node under it, which is the one place in the plan where that is true, and it is
347    /// why the materialisation holds its body as an input rather than sitting beside it.
348    CteScan {
349        /// The table index that this read's columns bind against.
350        index: u32,
351        /// Which materialisation it reads.
352        cte: u32,
353        /// The name it was written with.
354        name: StrRef,
355        /// The produced columns with their types, into the field pool.
356        columns: Slice,
357    },
358    /// `UNION`, `EXCEPT` or `INTERSECT`.
359    SetOp {
360        /// The left input.
361        left: NodeRef,
362        /// The right input.
363        right: NodeRef,
364        /// Which operation.
365        kind: SetOpKind,
366        /// Whether duplicates are kept.
367        all: bool,
368        /// The table index the produced columns bind against, since the output is neither side's
369        /// columns.
370        index: u32,
371    },
372}
373
374impl Node {
375    /// The keyword this operator prints as, which is also what the reader dispatches on.
376    #[must_use]
377    pub fn keyword(&self) -> &'static str {
378        match self {
379            Self::Get { .. } => "Get",
380            Self::Dummy => "Dummy",
381            Self::Values { .. } => "Values",
382            Self::TableFunction { .. } => "TableFunction",
383            Self::Filter { .. } => "Filter",
384            Self::Project { .. } => "Project",
385            Self::Aggregate { .. } => "Aggregate",
386            Self::Window { .. } => "Window",
387            Self::Sort { .. } => "Sort",
388            Self::Limit { .. } => "Limit",
389            Self::TopN { .. } => "TopN",
390            Self::Fetch { .. } => "Fetch",
391            Self::TableFetch { .. } => "TableFetch",
392            Self::Distinct { .. } => "Distinct",
393            Self::Join { .. } => "Join",
394            Self::DependentJoin { .. } => "DependentJoin",
395            Self::CrossProduct { .. } => "CrossProduct",
396            Self::MaterializedCte { .. } => "MaterializedCte",
397            Self::CteScan { .. } => "CteScan",
398            Self::SetOp { .. } => "SetOp",
399        }
400    }
401
402    /// The inputs, in printing order.
403    ///
404    /// Two slots rather than a `Vec`, because no logical operator in this set has three inputs and
405    /// the printer walks this on every node of every dump. A caller wants
406    /// `node.children().into_iter().flatten()`.
407    #[must_use]
408    pub fn children(&self) -> [Option<NodeRef>; 2] {
409        match *self {
410            Self::Get { .. }
411            | Self::Dummy
412            | Self::Values { .. }
413            | Self::TableFunction { .. }
414            | Self::CteScan { .. } => [None, None],
415            Self::Filter { input, .. }
416            | Self::Project { input, .. }
417            | Self::Aggregate { input, .. }
418            | Self::Window { input, .. }
419            | Self::Sort { input, .. }
420            | Self::Limit { input, .. }
421            | Self::TopN { input, .. }
422            | Self::Fetch { input, .. }
423            | Self::TableFetch { input, .. }
424            | Self::Distinct { input, .. } => [Some(input), None],
425            Self::Join { left, right, .. }
426            | Self::DependentJoin { left, right, .. }
427            | Self::CrossProduct { left, right }
428            | Self::SetOp { left, right, .. } => [Some(left), Some(right)],
429            Self::MaterializedCte { definition, body, .. } => [Some(definition), Some(body)],
430        }
431    }
432
433    /// How many inputs this operator takes.
434    #[must_use]
435    pub fn arity(&self) -> usize {
436        self.children().into_iter().flatten().count()
437    }
438
439    /// The table index this operator introduces, if it introduces one.
440    #[must_use]
441    pub fn table_index(&self) -> Option<u32> {
442        match *self {
443            Self::Get { index, .. }
444            | Self::Values { index, .. }
445            | Self::TableFunction { index, .. }
446            | Self::Project { index, .. }
447            | Self::Fetch { index, .. }
448            | Self::TableFetch { index, .. }
449            | Self::Aggregate { index, .. }
450            | Self::Window { index, .. }
451            | Self::CteScan { index, .. }
452            | Self::SetOp { index, .. } => Some(index),
453            _ => None,
454        }
455    }
456}
457
458/// Which join.
459///
460/// `Semi` and `Anti` are here because subquery unnesting produces them directly, per section 9.2,
461/// and a semi join expressed as a join plus a distinct is a semi join the executor cannot
462/// recognise.
463#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
464pub enum JoinKind {
465    /// Rows that match on both sides.
466    Inner,
467    /// Every left row, padded with nulls where the right does not match.
468    Left,
469    /// Every right row, padded with nulls where the left does not match.
470    Right,
471    /// Both of the above at once.
472    Full,
473    /// Left rows that have at least one match, each emitted once.
474    Semi,
475    /// Left rows that have no match.
476    Anti,
477    /// Left rows paired with their match, or with nulls, at most one right row each. What a
478    /// correlated scalar subquery unnests to.
479    Single,
480    /// Every left row plus a nullable boolean saying whether its condition matched the right side.
481    /// A null means no row matched and at least one comparison was unknown.
482    Mark,
483    /// The nth left row with the nth right row, which is DuckDB's `POSITIONAL JOIN`.
484    Positional,
485}
486
487impl JoinKind {
488    /// The spelling used in the textual form.
489    #[must_use]
490    pub fn keyword(self) -> &'static str {
491        match self {
492            Self::Inner => "INNER",
493            Self::Left => "LEFT",
494            Self::Right => "RIGHT",
495            Self::Full => "FULL",
496            Self::Semi => "SEMI",
497            Self::Anti => "ANTI",
498            Self::Single => "SINGLE",
499            Self::Mark => "MARK",
500            Self::Positional => "POSITIONAL",
501        }
502    }
503
504    /// Every join kind, which is what the reader searches.
505    pub(crate) const ALL: [Self; 9] = [
506        Self::Inner,
507        Self::Left,
508        Self::Right,
509        Self::Full,
510        Self::Semi,
511        Self::Anti,
512        Self::Single,
513        Self::Mark,
514        Self::Positional,
515    ];
516
517    /// The same join with its two inputs the other way round, for the kinds where there is one.
518    ///
519    /// Swapping the inputs of a `LEFT` join makes a `RIGHT` join and the other way round, because
520    /// the kind names a side. `INNER` and `FULL` name neither and are their own mirror. The rest
521    /// return `None`: `SEMI`, `ANTI`, `SINGLE` and `MARK` produce the left side's rows, or a
522    /// column about them, so their left input is not a side but the subject, and `POSITIONAL`
523    /// pairs the nth with the nth, which no reordering of one input preserves.
524    #[must_use]
525    pub fn mirrored(self) -> Option<Self> {
526        match self {
527            Self::Inner => Some(Self::Inner),
528            Self::Left => Some(Self::Right),
529            Self::Right => Some(Self::Left),
530            Self::Full => Some(Self::Full),
531            Self::Semi | Self::Anti | Self::Single | Self::Mark | Self::Positional => None,
532        }
533    }
534}
535
536/// Which input of a join is gathered whole before the other one starts.
537///
538/// A join is two inputs and a dependency edge between them: one side is finished and held, and then
539/// the other side's rows are matched against what was held. This says which side that is. It is
540/// where the hash table goes when the hash join in #62 lands, and it is the side today's nested
541/// loop turns into chunks and rescans once per row of the other one.
542///
543/// Which side that should be is not a property of the join and is not decided here. It is decided
544/// by [`sides`](../../rudb_opt/sides/index.html) from a cardinality estimate, and the rule it uses
545/// belongs to whichever operator is reading this, not to the flag.
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
547pub enum BuildSide {
548    /// The right input, which is what the binder emits and what every join did before this existed.
549    #[default]
550    Right,
551    /// The left input, which means the executor swaps the two and puts the answer back in order.
552    Left,
553}
554
555impl BuildSide {
556    /// The spelling used in the textual form.
557    #[must_use]
558    pub fn keyword(self) -> &'static str {
559        match self {
560            Self::Right => "right",
561            Self::Left => "left",
562        }
563    }
564
565    /// Both sides, which is what the reader searches.
566    pub(crate) const ALL: [Self; 2] = [Self::Right, Self::Left];
567}
568
569/// Which set operation.
570#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
571pub enum SetOpKind {
572    /// Rows from either side.
573    Union,
574    /// Rows from the left that are not on the right.
575    Except,
576    /// Rows on both sides.
577    Intersect,
578}
579
580impl SetOpKind {
581    /// The spelling used in the textual form.
582    #[must_use]
583    pub fn keyword(self) -> &'static str {
584        match self {
585            Self::Union => "UNION",
586            Self::Except => "EXCEPT",
587            Self::Intersect => "INTERSECT",
588        }
589    }
590
591    /// Every set operation, which is what the reader searches.
592    pub(crate) const ALL: [Self; 3] = [Self::Union, Self::Except, Self::Intersect];
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598    use crate::Slice;
599
600    /// Every node in one list, so that a variant added without a keyword, without a child slot or
601    /// without an entry in the reader's dispatch table fails here rather than at the first dump
602    /// that happens to contain one.
603    fn one_of_each() -> Vec<Node> {
604        vec![
605            Node::Get {
606                catalog: 0,
607                schema: 0,
608                table: 0,
609                alias: 0,
610                index: 0,
611                columns: Slice::EMPTY,
612            },
613            Node::Dummy,
614            Node::Values { index: 0, columns: Slice::EMPTY, rows: Slice::EMPTY },
615            Node::TableFunction {
616                index: 0,
617                function: 0,
618                args: Slice::EMPTY,
619                options: Slice::EMPTY,
620                settings: Slice::EMPTY,
621                columns: Slice::EMPTY,
622            },
623            Node::Filter { input: 0, predicate: 0 },
624            Node::Project { input: 0, index: 0, exprs: Slice::EMPTY, names: Slice::EMPTY },
625            Node::Aggregate { input: 0, index: 0, groups: Slice::EMPTY, aggregates: Slice::EMPTY },
626            Node::Sort { input: 0, keys: Slice::EMPTY },
627            Node::Limit { input: 0, count: None, offset: 0 },
628            Node::Distinct { input: 0, on: Slice::EMPTY },
629            Node::Join {
630                left: 0,
631                right: 1,
632                kind: JoinKind::Inner,
633                conditions: Slice::EMPTY,
634                build: BuildSide::default(),
635            },
636            Node::DependentJoin {
637                left: 0,
638                right: 1,
639                kind: JoinKind::Single,
640                conditions: Slice::EMPTY,
641            },
642            Node::CrossProduct { left: 0, right: 1 },
643            Node::SetOp { left: 0, right: 1, kind: SetOpKind::Union, all: true, index: 0 },
644        ]
645    }
646
647    #[test]
648    fn every_operator_has_its_own_keyword() {
649        let mut keywords: Vec<&str> = one_of_each().iter().map(Node::keyword).collect();
650        let count = keywords.len();
651        keywords.sort_unstable();
652        keywords.dedup();
653        assert_eq!(keywords.len(), count, "two operators print the same keyword");
654    }
655
656    #[test]
657    fn arity_agrees_with_the_child_slots() {
658        for node in one_of_each() {
659            let counted = node.children().into_iter().flatten().count();
660            assert_eq!(node.arity(), counted, "{} disagrees with itself", node.keyword());
661        }
662    }
663
664    /// A child slot that is `None` before a slot that is `Some` would make the printer emit the
665    /// right input as the left one, and the reader would accept it.
666    #[test]
667    fn the_child_slots_are_filled_from_the_front() {
668        for node in one_of_each() {
669            let slots = node.children();
670            assert!(
671                !(slots[0].is_none() && slots[1].is_some()),
672                "{} has a right input and no left one",
673                node.keyword()
674            );
675        }
676    }
677
678    #[test]
679    fn only_the_operators_that_introduce_columns_have_a_table_index() {
680        for node in one_of_each() {
681            let expected = matches!(
682                node,
683                Node::Get { .. }
684                    | Node::Values { .. }
685                    | Node::TableFunction { .. }
686                    | Node::Project { .. }
687                    | Node::Aggregate { .. }
688                    | Node::SetOp { .. }
689            );
690            assert_eq!(
691                node.table_index().is_some(),
692                expected,
693                "{} is on the wrong side of the table index rule",
694                node.keyword()
695            );
696        }
697    }
698
699    #[test]
700    fn every_join_kind_and_set_operation_is_in_the_list_the_reader_searches() {
701        assert_eq!(JoinKind::ALL.len(), 9);
702        assert_eq!(SetOpKind::ALL.len(), 3);
703        let mut names: Vec<&str> = JoinKind::ALL.iter().map(|k| k.keyword()).collect();
704        names.sort_unstable();
705        names.dedup();
706        assert_eq!(names.len(), JoinKind::ALL.len(), "two join kinds print the same keyword");
707    }
708}