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}