Skip to main content

rudb_common/
rules.rs

1//! The switches that turn one optimization off, and the two that turn a whole layer off.
2//!
3//! `spec/stats/09-measurement.md` section 9.2 asks for a setting per rule rather than one master
4//! switch, and the reason is accounting: a layer that ships twenty rules and reports one total
5//! cannot say which of them earned anything, and the twenty first gets added on the strength of a
6//! number the third one produced. A rule that cannot be turned off cannot be measured, and a rule
7//! that has not been measured on its own is not known to be worth its complexity.
8//!
9//! On top of those there are two switches that turn off everything below them, because three
10//! documents ask for the same ablation and it should be one implementation of one idea rather than
11//! three. [`Rule::StatsAll`] is `statistics = off` from `spec/stats/09-measurement.md` section 9.3,
12//! where every consumer gets `Unknown` and every operator takes the path it takes today.
13//! [`Rule::GraphSections`] is `graph_sections = off` from `spec/graph/09-measurement.md` section
14//! 9.2, where every query takes the hash join, the nested loop and the ordinary scan. Both runs must
15//! produce identical answers, and that comparison runs on every commit rather than at a milestone.
16//!
17//! The two masters do not start in the same place. `statistics` starts on, because a better estimate
18//! of a number the planner already needed is not a new behaviour and nobody should have to ask for
19//! it. `graph_sections` starts off, which is what tamnd/rudb#760 asks for, because a stored section
20//! and a new operator are a new behaviour, and a new behaviour earns its default by measuring better
21//! rather than by being written.
22//!
23//! # Why these are not in `Settings::NAMES`
24//!
25//! The same reason the seam settings are not, which `crates/rudb/src/settings.rs` states: a name in
26//! that list is a name `duckdb_settings()` prints, and none of these is a setting the binary we
27//! claim compatibility with has ever heard of. They go through the same `SET` path anyway, because
28//! a second door into the settings is a second place for a scope rule to be wrong.
29//!
30//! # Three spellings, one rule
31//!
32//! The value is a boolean, and `on` and `off` are accepted beside `true` and `false` because that
33//! is how the specification documents write these switches. In SQL the value is quoted, so the
34//! statement the ablation runs is `SET statistics = 'off'`, since a bare word on the right of a
35//! `SET` is a column reference and the binder says so.
36//!
37//! `stats.presize` is the name to write in a script and the name this module canonicalizes to.
38//! `stats_presize` is the one that fits through `SET` without quoting, because the statement takes
39//! an identifier and DuckDB's grammar has no dot in one. `statistics` and `graph_sections` are the
40//! spellings the specification documents use for the two masters, and they are here because a
41//! person who has read the specification should be able to type what it says.
42
43use crate::{Error, Result};
44
45/// One switch.
46///
47/// Every statistics variant is on by default, so a fresh database behaves as it did before any of
48/// this existed and an ablation is something a run asks for rather than something it inherits.
49/// [`Rule::GraphSections`] is the exception and starts off, because it is a stored structure and a
50/// new path through the executor rather than a better answer to a question already being asked.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum Rule {
53    /// Every statistics consumer. Off means every question answers `Stat::Unknown`.
54    StatsAll,
55    /// Presizing a hash aggregate from the distinct count of its grouping key.
56    Presize,
57    /// Choosing direct addressing over hashing when the key range is small and dense.
58    DirectAddressing,
59    /// Dropping the validity handling in a kernel whose input has an exact zero null count.
60    ValidityFree,
61    /// Ordering the conjuncts of a filter by selectivity over evaluation cost.
62    FilterOrder,
63    /// Seeding a top-n threshold from a certified quantile instead of from infinity.
64    TopNSeed,
65    /// Summing a decimal column in `i64` when its exact bounds fit, rather than in `i128`.
66    NarrowArithmetic,
67    /// Removing a join whose relationship is verified and whose columns nothing above it reads.
68    JoinElimination,
69    /// Reserving memory for an operator from what its input statistics say it will need.
70    MemoryReservation,
71    /// Every stored graph section. Off means the sections are not read and no plan uses one.
72    GraphSections,
73}
74
75impl Rule {
76    /// Every rule, in the order a report lists them.
77    pub const ALL: [Self; 10] = [
78        Self::StatsAll,
79        Self::Presize,
80        Self::DirectAddressing,
81        Self::ValidityFree,
82        Self::FilterOrder,
83        Self::TopNSeed,
84        Self::NarrowArithmetic,
85        Self::JoinElimination,
86        Self::MemoryReservation,
87        Self::GraphSections,
88    ];
89
90    /// The canonical name, which is what a setting reads back as.
91    #[must_use]
92    pub const fn name(self) -> &'static str {
93        match self {
94            Self::StatsAll => "stats.all",
95            Self::Presize => "stats.presize",
96            Self::DirectAddressing => "stats.direct_addressing",
97            Self::ValidityFree => "stats.validity_free",
98            Self::FilterOrder => "stats.filter_order",
99            Self::TopNSeed => "stats.top_n_seed",
100            Self::NarrowArithmetic => "stats.narrow_arithmetic",
101            Self::JoinElimination => "stats.join_elimination",
102            Self::MemoryReservation => "stats.memory_reservation",
103            Self::GraphSections => "graph.sections",
104        }
105    }
106
107    /// The rule that turns this one off from above, if there is one.
108    ///
109    /// A per rule switch is not enough on its own: the ablation of section 9.3 is one statement and
110    /// it has to reach every consumer, including the ones added after it was written.
111    #[must_use]
112    pub const fn master(self) -> Option<Self> {
113        match self {
114            Self::StatsAll | Self::GraphSections => None,
115            _ => Some(Self::StatsAll),
116        }
117    }
118
119    /// Whether a fresh database has this rule on.
120    ///
121    /// Everything does except [`Rule::GraphSections`], and the reason is in the module docs.
122    #[must_use]
123    pub const fn starts_on(self) -> bool {
124        !matches!(self, Self::GraphSections)
125    }
126
127    /// The rule a settings key names, in any of its spellings.
128    #[must_use]
129    pub fn from_name(key: &str) -> Option<Self> {
130        let name = canonical(key);
131        Self::ALL.into_iter().find(|rule| rule.name() == name)
132    }
133}
134
135/// Whether a settings name is a rule rather than one of the settings DuckDB has.
136///
137/// True for a misspelled rule as well as a correct one, so that `SET stats.presise = false` gets
138/// the error naming the rules rather than the one naming the DuckDB settings. A mistyped switch is
139/// the commonest way to get a run that measured the wrong thing, so the message has to say which
140/// list to look in.
141#[must_use]
142pub fn looks_like_rule(key: &str) -> bool {
143    let name = canonical(key);
144    name.starts_with("stats.") || name.starts_with("graph.") || Rule::from_name(&name).is_some()
145}
146
147/// Every rule name, for the sentence that says what the list is.
148#[must_use]
149pub fn rule_names() -> String {
150    Rule::ALL.map(Rule::name).join(", ")
151}
152
153/// The canonical spelling of a key, which is the dotted lowercase one.
154///
155/// The first underscore of an undotted name becomes the dot, so `stats_top_n_seed` and
156/// `stats.top_n_seed` are one name and the underscores inside a rule's own name survive. The two
157/// specification spellings are handled here because neither of them is derivable.
158fn canonical(key: &str) -> String {
159    let lower = key.to_ascii_lowercase();
160    match lower.as_str() {
161        "statistics" => return Rule::StatsAll.name().to_string(),
162        "graph_sections" => return Rule::GraphSections.name().to_string(),
163        _ => {}
164    }
165    if lower.contains('.') {
166        return lower;
167    }
168    match lower.split_once('_') {
169        Some((head, rest)) => format!("{head}.{rest}"),
170        None => lower,
171    }
172}
173
174/// Which switches are on, as the statements have left them.
175///
176/// A bitset rather than a map, because there are ten of them, because a session copies this once
177/// per statement, and because the set is fixed at compile time.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct Rules(u16);
180
181impl Default for Rules {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187impl Rules {
188    /// What a fresh database has, which is every statistics rule on and the graph sections off.
189    ///
190    /// The statistics rules are on because their whole point is that they change no answer, so a
191    /// database that had to be told to use them would be a database where nobody used them. The
192    /// graph sections are off because they are a stored structure that nothing writes yet and a new
193    /// path through the executor when they arrive, and a new path is worth having on by default only
194    /// once the measurement says it is better. G3 in tamnd/rudb#763 is where that is decided.
195    #[must_use]
196    pub const fn new() -> Self {
197        let mut bits = 0;
198        let mut index = 0;
199        while index < Rule::ALL.len() {
200            let rule = Rule::ALL[index];
201            if rule.starts_on() {
202                bits |= bit(rule);
203            }
204            index += 1;
205        }
206        Self(bits)
207    }
208
209    /// Whether this rule may fire, which is its own switch and its master's.
210    #[must_use]
211    pub fn enabled(self, rule: Rule) -> bool {
212        match rule.master() {
213            Some(master) if !self.is_set(master) => false,
214            _ => self.is_set(rule),
215        }
216    }
217
218    /// Whether this rule's own switch is on, ignoring its master.
219    ///
220    /// What a setting reads back as, because a settings surface that does not round trip is a
221    /// settings surface somebody reports as a bug. [`Rules::enabled`] is what is in effect.
222    #[must_use]
223    pub fn is_set(self, rule: Rule) -> bool {
224        self.0 & bit(rule) != 0
225    }
226
227    /// Turns one rule on or off.
228    pub fn set(&mut self, rule: Rule, enabled: bool) {
229        if enabled {
230            self.0 |= bit(rule);
231        } else {
232            self.0 &= !bit(rule);
233        }
234    }
235
236    /// Turns one rule on or off by name.
237    ///
238    /// # Errors
239    ///
240    /// [`ErrorCode::Catalog`](crate::ErrorCode::Catalog) when nothing is called that, with the list
241    /// of rules in the message.
242    pub fn set_named(&mut self, key: &str, enabled: bool) -> Result<()> {
243        let Some(rule) = Rule::from_name(key) else { return Err(no_such_rule(key)) };
244        self.set(rule, enabled);
245        Ok(())
246    }
247
248    /// Puts one rule back where a fresh database has it, which is what `RESET` means.
249    ///
250    /// Not the same as setting it on, because [`Rule::GraphSections`] starts off and a reset that
251    /// turned it on would be a reset that left the database somewhere it has never been.
252    ///
253    /// # Errors
254    ///
255    /// [`ErrorCode::Catalog`](crate::ErrorCode::Catalog) when nothing is called that, with the list
256    /// of rules in the message.
257    pub fn reset_named(&mut self, key: &str) -> Result<()> {
258        let Some(rule) = Rule::from_name(key) else { return Err(no_such_rule(key)) };
259        self.set(rule, rule.starts_on());
260        Ok(())
261    }
262
263    /// What one rule's setting reads back as, or `None` when nothing is called that.
264    #[must_use]
265    pub fn named(self, key: &str) -> Option<bool> {
266        Rule::from_name(key).map(|rule| self.is_set(rule))
267    }
268
269    /// Every rule and its own switch, in report order.
270    ///
271    /// `spec/stats/09-measurement.md` section 9.7 requires a statistics report to carry the settings
272    /// state for every rule, which is this.
273    pub fn states(self) -> impl Iterator<Item = (&'static str, bool)> {
274        Rule::ALL.into_iter().map(move |rule| (rule.name(), self.is_set(rule)))
275    }
276
277    /// The rules that are not where a fresh database left them, which is what a run records when it
278    /// says what it measured.
279    pub fn changed(self) -> impl Iterator<Item = (&'static str, bool)> {
280        let fresh = Self::new();
281        Rule::ALL
282            .into_iter()
283            .filter(move |&rule| self.is_set(rule) != fresh.is_set(rule))
284            .map(move |rule| (rule.name(), self.is_set(rule)))
285    }
286}
287
288const fn bit(rule: Rule) -> u16 {
289    1 << (rule as u16)
290}
291
292fn no_such_rule(key: &str) -> Error {
293    Error::catalog(format!("no rule called {key}, the rules are {}", rule_names()))
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn every_statistics_rule_starts_on_and_the_graph_sections_start_off() {
302        let rules = Rules::new();
303        for rule in Rule::ALL {
304            if rule == Rule::GraphSections {
305                assert!(!rules.enabled(rule), "the graph sections should start off");
306            } else {
307                assert!(rules.enabled(rule), "{} should start on", rule.name());
308            }
309        }
310        // A fresh database has nothing to report, off switch included.
311        assert_eq!(rules.changed().count(), 0);
312    }
313
314    #[test]
315    fn turning_the_graph_sections_on_is_a_change_worth_reporting() {
316        let mut rules = Rules::new();
317        rules.set(Rule::GraphSections, true);
318        assert!(rules.enabled(Rule::GraphSections));
319        assert_eq!(rules.changed().collect::<Vec<_>>(), vec![("graph.sections", true)]);
320    }
321
322    #[test]
323    fn the_master_turns_off_the_rules_under_it() {
324        let mut rules = Rules::new();
325        rules.set(Rule::StatsAll, false);
326        assert!(!rules.enabled(Rule::Presize));
327        assert!(!rules.enabled(Rule::NarrowArithmetic));
328        // The graph sections are their own layer and their own ablation, so the statistics master
329        // does not reach them either way.
330        rules.set(Rule::GraphSections, true);
331        assert!(rules.enabled(Rule::GraphSections));
332        // The switch underneath is still where the session left it, which is what it reads back as.
333        assert!(rules.is_set(Rule::Presize));
334    }
335
336    #[test]
337    fn one_rule_goes_off_without_taking_the_others_with_it() {
338        let mut rules = Rules::new();
339        rules.set(Rule::FilterOrder, false);
340        assert!(!rules.enabled(Rule::FilterOrder));
341        assert!(rules.enabled(Rule::Presize));
342        assert!(rules.enabled(Rule::StatsAll));
343        assert_eq!(rules.changed().collect::<Vec<_>>(), vec![("stats.filter_order", false)]);
344    }
345
346    #[test]
347    fn the_spellings_all_reach_the_same_rule() {
348        for spelling in ["stats.all", "stats_all", "statistics", "STATISTICS", "Stats.All"] {
349            assert_eq!(Rule::from_name(spelling), Some(Rule::StatsAll), "{spelling}");
350        }
351        for spelling in ["graph.sections", "graph_sections", "GRAPH.SECTIONS"] {
352            assert_eq!(Rule::from_name(spelling), Some(Rule::GraphSections), "{spelling}");
353        }
354        for spelling in ["stats.top_n_seed", "stats_top_n_seed"] {
355            assert_eq!(Rule::from_name(spelling), Some(Rule::TopNSeed), "{spelling}");
356        }
357    }
358
359    #[test]
360    fn a_name_nobody_has_is_not_a_rule() {
361        assert_eq!(Rule::from_name("memory_limit"), None);
362        assert_eq!(Rule::from_name("stats.presise"), None);
363        assert!(!looks_like_rule("memory_limit"));
364        assert!(!looks_like_rule("threads"));
365        // A misspelled rule is still a rule for the purpose of choosing the error message.
366        assert!(looks_like_rule("stats.presise"));
367        assert!(looks_like_rule("graph_adjacency"));
368    }
369
370    #[test]
371    fn setting_by_name_says_what_the_names_are() {
372        let mut rules = Rules::new();
373        rules.set_named("stats_presize", false).expect("a rule by its underscore spelling");
374        assert!(!rules.enabled(Rule::Presize));
375        assert_eq!(rules.named("stats.presize"), Some(false));
376
377        let refused = rules.set_named("stats.presise", false).expect_err("no such rule");
378        assert!(refused.to_string().contains("stats.presize"), "{refused}");
379    }
380
381    #[test]
382    fn every_rule_has_its_own_bit() {
383        let mut seen = Vec::new();
384        for rule in Rule::ALL {
385            assert!(!seen.contains(&bit(rule)), "{} shares a bit", rule.name());
386            seen.push(bit(rule));
387        }
388    }
389
390    #[test]
391    fn a_report_lists_every_rule() {
392        let states = Rules::new().states().collect::<Vec<_>>();
393        assert_eq!(states.len(), Rule::ALL.len());
394        assert_eq!(states[0], ("stats.all", true));
395    }
396}