Skip to main content

rudb_plan/
lib.rs

1//! The logical plan, its textual form, and the parser that reads that form back.
2//!
3//! Rank 9 in the layer rule. See `xtask/layers.toml` and `spec/18-package-layout.md`.
4//!
5//! This is the bound logical plan: what the binder produces, what the optimizer rewrites, and what
6//! the physical planner consumes. `spec/04-architecture.md` calls it "a bound logical plan with
7//! fully resolved types", and both halves of that are enforced here rather than assumed. Every
8//! expression carries a [`LogicalType`](rudb_common::LogicalType) that is stored next to it, and
9//! every column reference is a [`ColumnBinding`] naming the operator that produced the column and
10//! the position within that operator's output. There are no names in an expression and nothing in
11//! this crate looks anything up in a catalog. Name resolution happened in the binder and a plan
12//! that still needs it is a plan that is not bound.
13//!
14//! # The textual form
15//!
16//! `spec/00-README.md` requires that every layer has a textual form and a round-trip parser, and
17//! that requirement is the reason this crate exists before there is an optimizer to rewrite
18//! anything. A plan prints as an indented tree, two spaces a level, parent before children:
19//!
20//! ```text
21//! Project #2 [#1.0::VARCHAR AS SearchPhrase, #1.1::BIGINT AS c]
22//!   Limit 10 offset 0
23//!     Sort [#1.1::BIGINT DESC NULLS LAST]
24//!       Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[count_star()::BIGINT]
25//!         Filter (#0.0::VARCHAR <> ''::VARCHAR)::BOOLEAN
26//!           Get memory.main.hits AS hits #0 [SearchPhrase::VARCHAR]
27//! ```
28//!
29//! [`Plan::parse`] reads that back, and printing the result produces the same text. That fixed
30//! point is a test rather than a claim, and it is the thing that makes a plan diffable across a
31//! rewrite, fuzzable on its own, and bisectable when a pass starts returning a wrong answer.
32//!
33//! Every expression is written `form::TYPE`. The annotation is on every node and not only on the
34//! ones where a reader would need it, because the alternative is a parser that has to re-derive
35//! types, and re-deriving types means consulting the function catalog, and a dump that cannot be
36//! read without a catalog is not a dump. It is verbose. It is also exact, and exact is the whole
37//! job here.
38//!
39//! # Why an arena
40//!
41//! Nodes, expressions and their lists all live in flat vectors and refer to each other by `u32`
42//! index, the same shape [`rudb_parse::Ast`](https://docs.rs/rudb-parse) uses. A plan is rewritten
43//! many times by `spec/09-optimizer.md`'s fixed pass sequence, and a rewrite of a boxed tree is a
44//! traversal that allocates at every node. It also makes a plan one owned value that clones with
45//! three memcpys, which is what lets a pass be a pure function from plan to plan without that
46//! being expensive.
47//!
48//! The cost is that a reference is a number and a number can point at the wrong thing.
49//! [`Plan::validate`] is the answer to that, and it is what section 9.1 means by the invariant
50//! every pass has to preserve.
51//!
52//! # What is not here yet
53//!
54//! Window functions, subquery expressions, correlated references, `UNNEST`, lambdas, prepared
55//! statement parameters, and everything on the write side. The M0 transformer cannot produce any
56//! of them, so a representation for them here would be a representation nothing has ever
57//! constructed, which is a representation that is wrong in a way nobody finds out about. Neither
58//! [`Expr`] nor [`Node`] is `#[non_exhaustive]`, which is deliberate: adding a plan node should
59//! stop the build in every optimizer pass that has to decide what to do about it.
60
61#![forbid(unsafe_code)]
62
63mod expr;
64mod node;
65mod parse;
66mod plan;
67mod print;
68
69pub use expr::{Arm, ColumnBinding, CompareOp, ConjunctionOp, Expr, SortKey};
70pub use node::{JoinKind, Node, SetOpKind};
71pub use plan::Plan;
72
73/// A reference to an expression in [`Plan`]'s expression arena.
74pub type ExprRef = u32;
75
76/// A reference to a node in [`Plan`]'s node arena.
77pub type NodeRef = u32;
78
79/// A reference to an interned string in [`Plan`]'s string table.
80pub type StrRef = u32;
81
82/// A reference to a constant in [`Plan`]'s value table.
83pub type ValueRef = u32;
84
85/// A contiguous run in one of [`Plan`]'s pools.
86///
87/// Which pool is decided by the field that holds the slice, the same way a `u32` reference is only
88/// meaningful in the arena it came from. A slice is `Copy` and eight bytes, so a node holding four
89/// of them is still a node that fits in a cache line.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub struct Slice {
92    /// Index of the first element.
93    pub start: u32,
94    /// Number of elements.
95    pub len: u32,
96}
97
98impl Slice {
99    /// The empty slice.
100    pub const EMPTY: Self = Self { start: 0, len: 0 };
101
102    /// Whether the run has no elements.
103    #[must_use]
104    pub fn is_empty(self) -> bool {
105        self.len == 0
106    }
107
108    /// The run as a range, for indexing the pool it belongs to.
109    #[must_use]
110    pub fn range(self) -> std::ops::Range<usize> {
111        self.start as usize..(self.start as usize + self.len as usize)
112    }
113}