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 /// Turning a join's build side into an exact set of driving rows through a stored link, rather
74 /// than into a Bloom filter. Under [`Rule::GraphSections`], so it is off whenever the layer is.
75 GraphReduction,
76 /// Closing a group of an aggregate as soon as its key moves past it, when the key is a column the
77 /// table is stored in ascending order of.
78 ClosedGroups,
79}
80
81impl Rule {
82 /// Every rule, in the order a report lists them.
83 pub const ALL: [Self; 12] = [
84 Self::StatsAll,
85 Self::Presize,
86 Self::DirectAddressing,
87 Self::ValidityFree,
88 Self::FilterOrder,
89 Self::TopNSeed,
90 Self::NarrowArithmetic,
91 Self::JoinElimination,
92 Self::MemoryReservation,
93 Self::GraphSections,
94 Self::GraphReduction,
95 Self::ClosedGroups,
96 ];
97
98 /// The canonical name, which is what a setting reads back as.
99 #[must_use]
100 pub const fn name(self) -> &'static str {
101 match self {
102 Self::StatsAll => "stats.all",
103 Self::Presize => "stats.presize",
104 Self::DirectAddressing => "stats.direct_addressing",
105 Self::ValidityFree => "stats.validity_free",
106 Self::FilterOrder => "stats.filter_order",
107 Self::TopNSeed => "stats.top_n_seed",
108 Self::NarrowArithmetic => "stats.narrow_arithmetic",
109 Self::JoinElimination => "stats.join_elimination",
110 Self::MemoryReservation => "stats.memory_reservation",
111 Self::GraphSections => "graph.sections",
112 Self::GraphReduction => "graph.reduction",
113 Self::ClosedGroups => "stats.closed_groups",
114 }
115 }
116
117 /// The rule that turns this one off from above, if there is one.
118 ///
119 /// A per rule switch is not enough on its own: the ablation of section 9.3 is one statement and
120 /// it has to reach every consumer, including the ones added after it was written.
121 #[must_use]
122 pub const fn master(self) -> Option<Self> {
123 match self {
124 Self::StatsAll | Self::GraphSections => None,
125 Self::GraphReduction => Some(Self::GraphSections),
126 _ => Some(Self::StatsAll),
127 }
128 }
129
130 /// Whether a fresh database has this rule on.
131 ///
132 /// Everything does except [`Rule::GraphSections`], and the reason is in the module docs.
133 #[must_use]
134 pub const fn starts_on(self) -> bool {
135 !matches!(self, Self::GraphSections)
136 }
137
138 /// The rule a settings key names, in any of its spellings.
139 #[must_use]
140 pub fn from_name(key: &str) -> Option<Self> {
141 let name = canonical(key);
142 Self::ALL.into_iter().find(|rule| rule.name() == name)
143 }
144}
145
146/// Whether a settings name is a rule rather than one of the settings DuckDB has.
147///
148/// True for a misspelled rule as well as a correct one, so that `SET stats.presise = false` gets
149/// the error naming the rules rather than the one naming the DuckDB settings. A mistyped switch is
150/// the commonest way to get a run that measured the wrong thing, so the message has to say which
151/// list to look in.
152#[must_use]
153pub fn looks_like_rule(key: &str) -> bool {
154 let name = canonical(key);
155 name.starts_with("stats.") || name.starts_with("graph.") || Rule::from_name(&name).is_some()
156}
157
158/// Every rule name, for the sentence that says what the list is.
159#[must_use]
160pub fn rule_names() -> String {
161 Rule::ALL.map(Rule::name).join(", ")
162}
163
164/// The canonical spelling of a key, which is the dotted lowercase one.
165///
166/// The first underscore of an undotted name becomes the dot, so `stats_top_n_seed` and
167/// `stats.top_n_seed` are one name and the underscores inside a rule's own name survive. The two
168/// specification spellings are handled here because neither of them is derivable.
169fn canonical(key: &str) -> String {
170 let lower = key.to_ascii_lowercase();
171 match lower.as_str() {
172 "statistics" => return Rule::StatsAll.name().to_string(),
173 "graph_sections" => return Rule::GraphSections.name().to_string(),
174 _ => {}
175 }
176 if lower.contains('.') {
177 return lower;
178 }
179 match lower.split_once('_') {
180 Some((head, rest)) => format!("{head}.{rest}"),
181 None => lower,
182 }
183}
184
185/// Which switches are on, as the statements have left them.
186///
187/// A bitset rather than a map, because there are eleven of them, because a session copies this once
188/// per statement, and because the set is fixed at compile time.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct Rules(u16);
191
192impl Default for Rules {
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198impl Rules {
199 /// What a fresh database has, which is every statistics rule on and the graph sections off.
200 ///
201 /// The statistics rules are on because their whole point is that they change no answer, so a
202 /// database that had to be told to use them would be a database where nobody used them. The
203 /// graph sections are off because they are a stored structure that nothing writes yet and a new
204 /// path through the executor when they arrive, and a new path is worth having on by default only
205 /// once the measurement says it is better. G3 in tamnd/rudb#763 is where that is decided.
206 #[must_use]
207 pub const fn new() -> Self {
208 let mut bits = 0;
209 let mut index = 0;
210 while index < Rule::ALL.len() {
211 let rule = Rule::ALL[index];
212 if rule.starts_on() {
213 bits |= bit(rule);
214 }
215 index += 1;
216 }
217 Self(bits)
218 }
219
220 /// Whether this rule may fire, which is its own switch and its master's.
221 #[must_use]
222 pub fn enabled(self, rule: Rule) -> bool {
223 match rule.master() {
224 Some(master) if !self.is_set(master) => false,
225 _ => self.is_set(rule),
226 }
227 }
228
229 /// Whether this rule's own switch is on, ignoring its master.
230 ///
231 /// What a setting reads back as, because a settings surface that does not round trip is a
232 /// settings surface somebody reports as a bug. [`Rules::enabled`] is what is in effect.
233 #[must_use]
234 pub fn is_set(self, rule: Rule) -> bool {
235 self.0 & bit(rule) != 0
236 }
237
238 /// Turns one rule on or off.
239 pub fn set(&mut self, rule: Rule, enabled: bool) {
240 if enabled {
241 self.0 |= bit(rule);
242 } else {
243 self.0 &= !bit(rule);
244 }
245 }
246
247 /// Turns one rule on or off by name.
248 ///
249 /// # Errors
250 ///
251 /// [`ErrorCode::Catalog`](crate::ErrorCode::Catalog) when nothing is called that, with the list
252 /// of rules in the message.
253 pub fn set_named(&mut self, key: &str, enabled: bool) -> Result<()> {
254 let Some(rule) = Rule::from_name(key) else { return Err(no_such_rule(key)) };
255 self.set(rule, enabled);
256 Ok(())
257 }
258
259 /// Puts one rule back where a fresh database has it, which is what `RESET` means.
260 ///
261 /// Not the same as setting it on, because [`Rule::GraphSections`] starts off and a reset that
262 /// turned it on would be a reset that left the database somewhere it has never been.
263 ///
264 /// # Errors
265 ///
266 /// [`ErrorCode::Catalog`](crate::ErrorCode::Catalog) when nothing is called that, with the list
267 /// of rules in the message.
268 pub fn reset_named(&mut self, key: &str) -> Result<()> {
269 let Some(rule) = Rule::from_name(key) else { return Err(no_such_rule(key)) };
270 self.set(rule, rule.starts_on());
271 Ok(())
272 }
273
274 /// What one rule's setting reads back as, or `None` when nothing is called that.
275 #[must_use]
276 pub fn named(self, key: &str) -> Option<bool> {
277 Rule::from_name(key).map(|rule| self.is_set(rule))
278 }
279
280 /// Every rule and its own switch, in report order.
281 ///
282 /// `spec/stats/09-measurement.md` section 9.7 requires a statistics report to carry the settings
283 /// state for every rule, which is this.
284 pub fn states(self) -> impl Iterator<Item = (&'static str, bool)> {
285 Rule::ALL.into_iter().map(move |rule| (rule.name(), self.is_set(rule)))
286 }
287
288 /// The rules that are not where a fresh database left them, which is what a run records when it
289 /// says what it measured.
290 pub fn changed(self) -> impl Iterator<Item = (&'static str, bool)> {
291 let fresh = Self::new();
292 Rule::ALL
293 .into_iter()
294 .filter(move |&rule| self.is_set(rule) != fresh.is_set(rule))
295 .map(move |rule| (rule.name(), self.is_set(rule)))
296 }
297}
298
299const fn bit(rule: Rule) -> u16 {
300 1 << (rule as u16)
301}
302
303fn no_such_rule(key: &str) -> Error {
304 Error::catalog(format!("no rule called {key}, the rules are {}", rule_names()))
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn every_statistics_rule_starts_on_and_the_graph_sections_start_off() {
313 let rules = Rules::new();
314 for rule in Rule::ALL {
315 if rule == Rule::GraphSections || rule == Rule::GraphReduction {
316 assert!(!rules.enabled(rule), "{} should start off", rule.name());
317 } else {
318 assert!(rules.enabled(rule), "{} should start on", rule.name());
319 }
320 }
321 // A fresh database has nothing to report, off switch included.
322 assert_eq!(rules.changed().count(), 0);
323 }
324
325 #[test]
326 fn turning_the_graph_sections_on_is_a_change_worth_reporting() {
327 let mut rules = Rules::new();
328 rules.set(Rule::GraphSections, true);
329 assert!(rules.enabled(Rule::GraphSections));
330 assert_eq!(rules.changed().collect::<Vec<_>>(), vec![("graph.sections", true)]);
331 }
332
333 #[test]
334 fn the_master_turns_off_the_rules_under_it() {
335 let mut rules = Rules::new();
336 rules.set(Rule::StatsAll, false);
337 assert!(!rules.enabled(Rule::Presize));
338 assert!(!rules.enabled(Rule::NarrowArithmetic));
339 // The graph sections are their own layer and their own ablation, so the statistics master
340 // does not reach them either way.
341 rules.set(Rule::GraphSections, true);
342 assert!(rules.enabled(Rule::GraphSections));
343 // The switch underneath is still where the session left it, which is what it reads back as.
344 assert!(rules.is_set(Rule::Presize));
345 }
346
347 #[test]
348 fn one_rule_goes_off_without_taking_the_others_with_it() {
349 let mut rules = Rules::new();
350 rules.set(Rule::FilterOrder, false);
351 assert!(!rules.enabled(Rule::FilterOrder));
352 assert!(rules.enabled(Rule::Presize));
353 assert!(rules.enabled(Rule::StatsAll));
354 assert_eq!(rules.changed().collect::<Vec<_>>(), vec![("stats.filter_order", false)]);
355 }
356
357 #[test]
358 fn the_spellings_all_reach_the_same_rule() {
359 for spelling in ["stats.all", "stats_all", "statistics", "STATISTICS", "Stats.All"] {
360 assert_eq!(Rule::from_name(spelling), Some(Rule::StatsAll), "{spelling}");
361 }
362 for spelling in ["graph.sections", "graph_sections", "GRAPH.SECTIONS"] {
363 assert_eq!(Rule::from_name(spelling), Some(Rule::GraphSections), "{spelling}");
364 }
365 for spelling in ["stats.top_n_seed", "stats_top_n_seed"] {
366 assert_eq!(Rule::from_name(spelling), Some(Rule::TopNSeed), "{spelling}");
367 }
368 }
369
370 #[test]
371 fn a_name_nobody_has_is_not_a_rule() {
372 assert_eq!(Rule::from_name("memory_limit"), None);
373 assert_eq!(Rule::from_name("stats.presise"), None);
374 assert!(!looks_like_rule("memory_limit"));
375 assert!(!looks_like_rule("threads"));
376 // A misspelled rule is still a rule for the purpose of choosing the error message.
377 assert!(looks_like_rule("stats.presise"));
378 assert!(looks_like_rule("graph_adjacency"));
379 }
380
381 #[test]
382 fn setting_by_name_says_what_the_names_are() {
383 let mut rules = Rules::new();
384 rules.set_named("stats_presize", false).expect("a rule by its underscore spelling");
385 assert!(!rules.enabled(Rule::Presize));
386 assert_eq!(rules.named("stats.presize"), Some(false));
387
388 let refused = rules.set_named("stats.presise", false).expect_err("no such rule");
389 assert!(refused.to_string().contains("stats.presize"), "{refused}");
390 }
391
392 #[test]
393 fn every_rule_has_its_own_bit() {
394 let mut seen = Vec::new();
395 for rule in Rule::ALL {
396 assert!(!seen.contains(&bit(rule)), "{} shares a bit", rule.name());
397 seen.push(bit(rule));
398 }
399 }
400
401 #[test]
402 fn a_report_lists_every_rule() {
403 let states = Rules::new().states().collect::<Vec<_>>();
404 assert_eq!(states.len(), Rule::ALL.len());
405 assert_eq!(states[0], ("stats.all", true));
406 }
407}