rudb_common/clustering.rs
1//! What order a table's rows are meant to be stored in, as a declaration the catalog keeps.
2//!
3//! Stage 1 of `$HOME/notes/Spec/2140/tenx`, written up in `04-the-clustered-layout.md`. The short
4//! version is that both we and DuckDB keep a low and a high value per fragment, and on TPC-H in
5//! the order `dbgen` writes it neither of us can skip a single fragment, because the dates are
6//! scattered and every fragment's range is the whole table's range. Sort the rows first and q6,
7//! q14, q15 and q12 come out 6.13x, 3.85x, 3.50x and 2.38x cheaper with no engine change at all.
8//!
9//! The engine already gets that far on its own: the fragment ranges are built from whatever order
10//! the rows arrive in, so `CREATE TABLE ... AS SELECT ... ORDER BY` prunes today. What is missing
11//! is that nothing writes the order down. A checkpoint rewrites the file, an insert appends to the
12//! end, and the order is gone with nothing having said so. This type is the thing that is written
13//! down.
14//!
15//! It is a declaration and not a measurement. It says what the table is supposed to look like, not
16//! what it currently does look like. `spec/storage-v3/13-order-as-a-committed-fact.md` is the other
17//! one, and the two want the same column list for different reasons.
18
19use std::fmt;
20
21use crate::error::{Error, Result};
22use crate::types::{Field, LogicalType};
23
24/// How coarsely the leading column is bucketed before the columns after it break the tie.
25///
26/// Sorting lineitem by `l_shipdate` exactly and sorting it by the month of `l_shipdate` prune the
27/// same way for a predicate a month wide or wider, and the second one leaves `l_orderkey` in order
28/// inside each bucket, which is what the joins want. That trade is the whole reason this exists
29/// rather than the declaration being a plain column list.
30///
31/// All four are measured, in `14-the-partition-width.md`. The month is the worst of them: five SF1
32/// files built by one loader in one sitting come out at 0.891 of the unsorted file's instructions
33/// sorted exactly, 0.898 at a quarter, 0.899 at a year and 0.917 at a month, because the narrower
34/// the bucket the more often a join key's hash entry is revisited and the wider the delta the sort
35/// key encodes to, while the pruning a narrow bucket buys stops mattering above a quarter. So a
36/// declaration that names no width gets [`Width::Auto`], which picks from the data and lands on the
37/// quarter at the scale that table was measured at. Note how little separates the middle three on
38/// that suite and how much separates them on the queries with a narrow date predicate in them,
39/// which is why the rule that picks is written the way it is.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
41pub enum Width {
42 /// The value itself, which is an ordinary `ORDER BY` on the column.
43 #[default]
44 Exact,
45 /// The calendar month the value falls in.
46 Month,
47 /// The calendar quarter the value falls in.
48 Quarter,
49 /// The calendar year the value falls in.
50 Year,
51 /// The month or the quarter, whichever the data asks for, decided when the rows are in front
52 /// of us.
53 ///
54 /// This is a declaration and not a bucket, which is the whole of the difference. Nothing sorts
55 /// by it and nothing writes a sort key from it: [`Width::for_span`] turns it into a real one at
56 /// the load, with the row count and the column's range in hand, and it is that answer the rows
57 /// are put in order by. A declaration that still says this after a load is a table saying it
58 /// wants whatever fits, not a table saying its rows are in no order.
59 ///
60 /// Only two of the four are reachable this way. The exact width and the year both have to be
61 /// asked for by name, for reasons [`Width::for_span`] gives.
62 Auto,
63}
64
65impl Width {
66 /// The bucket a date or a timestamp gets when nobody says which and nothing can be counted.
67 ///
68 /// Not [`Default::default`], which stays the exact value: that one is the width of a column
69 /// with no calendar in it, and a struct deriving `Default` has no column to look at. This is
70 /// the fallback for [`Width::for_span`] when the row count or the range is not there to read,
71 /// and it is the quarter because that is what SF1 measured at, in `14-the-partition-width.md`.
72 pub const DEFAULT: Self = Self::Quarter;
73
74 /// How many rows a partition should hold.
75 ///
76 /// The number the whole of [`Width::for_span`] turns on, and the one thing in this file that is
77 /// a constant fitted to a measurement rather than a fact. Section 14.6 of
78 /// `14-the-partition-width.md` says a partition of a few hundred thousand rows is the shape
79 /// that measured well, and this is the bottom of that range.
80 ///
81 /// Two hundred thousand because of what it has to separate. SF1 lineitem is six million rows
82 /// over about seven years, so a month there is seventy thousand rows a partition and a quarter
83 /// is two hundred and fourteen thousand, and the quarter is the width that suite measured best.
84 /// Any target between those two numbers reproduces that answer. SF10 lineitem is ten times the
85 /// rows over the same seven years, so a month there is seven hundred thousand, which clears the
86 /// target comfortably, and that is the case the rule exists for.
87 pub const TARGET: u64 = 200_000;
88
89 /// The width for a table of `rows` rows whose leading column runs across `days`.
90 ///
91 /// The month when a month's worth of rows clears [`Width::TARGET`] and the quarter otherwise.
92 /// Two candidates and not four, and which two is the part that was measured rather than
93 /// reasoned.
94 ///
95 /// The month is what the row count is for. A narrow partition prunes better and costs locality:
96 /// the more partitions the leading column is cut into, the more often a join key's hash entry is
97 /// revisited and the wider the deltas the sort key encodes to. Section 14.5 measured both sides
98 /// and the crossing point is a partition size rather than a calendar unit, which is the entire
99 /// reason this is a function and not a constant. At SF1 a month is under the target and loses,
100 /// at SF10 it is three times over it and the same calendar word is a different proposition.
101 ///
102 /// The year is not a candidate, and the first cut of this rule had it as one. It measured 0.899
103 /// of the unsorted file at SF1 against the quarter's 0.898, so it was never winning anything,
104 /// and letting it in cost real numbers: with the year available the rule cut `orders` yearly at
105 /// SF1, because orders is a quarter of lineitem's size and falls under the target at every
106 /// calendar width, and q4 went from 0.879 to 0.914, q10 from 0.825 to 0.841 and q3 from 0.686
107 /// to 0.701. Those three read `o_orderdate` through a predicate a quarter wide, and no
108 /// partition wider than the predicate can prune inside it however many rows it holds. That is
109 /// the limit of the row count as a rule: it says how fine to cut before locality starts costing
110 /// and it says nothing about how coarse is too coarse, because that end is set by the width of
111 /// the predicates and nothing at load time knows those.
112 ///
113 /// The exact width is not a candidate either, for a different reason. It measured two tenths of
114 /// a percent better than the quarter on SF1 and it is still the wrong answer, because it throws
115 /// away the run length layer on the join key and costs eight times the bytes on `l_orderkey`,
116 /// which loses on any workload without TPC-H's date predicates in it. Somebody who wants either
117 /// of the two can write `exact(...)` or `year(...)` and get it, and this is about what to do
118 /// when nobody said.
119 ///
120 /// `days` is the span of the column and not the number of distinct values in it, because what
121 /// matters is how many buckets the range is cut into. A table whose dates are seven years apart
122 /// and has three of them still has seven years of buckets to write down.
123 ///
124 /// A row count or a span of zero gets [`Width::DEFAULT`]. There is nothing to divide and an
125 /// empty table has no shape to fit.
126 #[must_use]
127 pub fn for_span(rows: u64, days: u64) -> Self {
128 if rows == 0 || days == 0 {
129 return Self::DEFAULT;
130 }
131 // Thirty days, which is all the arithmetic needs: the answer is how many partitions a range
132 // is cut into and a long month either way cannot move that across the target.
133 let partitions = days / 30 + 1;
134 if rows / partitions >= Self::TARGET { Self::Month } else { Self::Quarter }
135 }
136
137 /// The byte this is written as in a native file directory. Never reordered, only appended to.
138 #[must_use]
139 pub fn tag(self) -> u8 {
140 match self {
141 Self::Exact => 0,
142 Self::Month => 1,
143 Self::Quarter => 2,
144 Self::Year => 3,
145 Self::Auto => 4,
146 }
147 }
148
149 /// The width a directory byte means.
150 #[must_use]
151 pub fn from_tag(tag: u8) -> Option<Self> {
152 match tag {
153 0 => Some(Self::Exact),
154 1 => Some(Self::Month),
155 2 => Some(Self::Quarter),
156 3 => Some(Self::Year),
157 4 => Some(Self::Auto),
158 _ => None,
159 }
160 }
161}
162
163impl fmt::Display for Width {
164 fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
165 out.write_str(match self {
166 Self::Exact => "EXACT",
167 Self::Month => "MONTH",
168 Self::Quarter => "QUARTER",
169 Self::Year => "YEAR",
170 Self::Auto => "AUTO",
171 })
172 }
173}
174
175/// The order a table's rows are meant to be stored in.
176///
177/// Columns outermost first, by index into the table's column list, with the leading one bucketed
178/// at [`Clustering::width`]. The stage 0 layout for lineitem is columns `l_shipdate`, `l_orderkey`,
179/// `l_linenumber` at a width of a month, which is exactly the `ORDER BY date_trunc('month',
180/// l_shipdate), l_orderkey, l_linenumber` that produced the measured numbers.
181///
182/// Indexes and not names, because the catalog and the stored directory both already have the column
183/// list beside this and a name here would be a second copy that a rename could put out of step. The
184/// cost is that this has to be validated against a column count wherever it is built, which
185/// [`Clustering::new`] does.
186#[derive(Debug, Clone, PartialEq, Eq, Hash)]
187pub struct Clustering {
188 columns: Vec<u32>,
189 width: Width,
190}
191
192impl Clustering {
193 /// A declaration over `columns` of a table whose columns are `fields`.
194 ///
195 /// # Errors
196 ///
197 /// If the list is empty, names a column the table does not have, or names one twice. All three
198 /// are declarations that could be stored and could never be satisfied, and the only place they
199 /// can be caught is before they go in.
200 ///
201 /// And if a width other than [`Width::Exact`] lands on a column that is not a date or a
202 /// timestamp. The width is a calendar bucket and there is no calendar in an integer, so the
203 /// loader would have nothing to sort by. Checked here rather than at the load, because a
204 /// declaration is stored once and read every time the table is written.
205 pub fn new(columns: Vec<u32>, width: Width, fields: &[Field]) -> Result<Self> {
206 if columns.is_empty() {
207 return Err(Error::invalid_input("a clustering declaration names no column"));
208 }
209 for (at, &column) in columns.iter().enumerate() {
210 if column as usize >= fields.len() {
211 return Err(Error::invalid_input(
212 "a clustering declaration names a column the table does not have",
213 ));
214 }
215 if columns[..at].contains(&column) {
216 return Err(Error::invalid_input(
217 "a clustering declaration names the same column twice",
218 ));
219 }
220 }
221 let leading = &fields[columns[0] as usize];
222 if width != Width::Exact
223 && !matches!(leading.ty, LogicalType::Date | LogicalType::Timestamp)
224 {
225 return Err(Error::invalid_input(format!(
226 "a clustering declaration buckets {} by {width}, which only a date or a timestamp \
227 has",
228 leading.name
229 )));
230 }
231 Ok(Self { columns, width })
232 }
233
234 /// A declaration over `columns` that leaves the width to the leading column's type.
235 ///
236 /// A date or a timestamp gets [`Width::Auto`] and anything else is taken exactly, which is the
237 /// only width an integer or a string has. This is what a loader clustering a table it was
238 /// handed should call, since the alternative is every caller writing the same two line match
239 /// and the constant living in as many places as there are callers.
240 ///
241 /// [`Width::Auto`] and not a fixed bucket because a fixed bucket is a constant fitted at one
242 /// scale and applied at every other. A quarter of SF1 lineitem is a quarter of a million rows
243 /// and a quarter of SF100 is twenty four million, and there is no reason the second one lands
244 /// anywhere near the first on the curve section 14.5 measured. [`Clustering::fitted`] is where
245 /// the declaration meets the row count and turns into a bucket.
246 ///
247 /// # Errors
248 ///
249 /// The ones [`Clustering::new`] gives, which this is checked by. The width it picks is legal
250 /// for the column it picked it for, so the type error is not one of them.
251 pub fn over(columns: Vec<u32>, fields: &[Field]) -> Result<Self> {
252 let leading = columns.first().and_then(|&at| fields.get(at as usize));
253 let width = match leading.map(|field| &field.ty) {
254 Some(LogicalType::Date | LogicalType::Timestamp) => Width::Auto,
255 // Including the column list that is empty or out of range, which has no type to look
256 // at and is about to be refused for that rather than for its width.
257 _ => Width::Exact,
258 };
259 Self::new(columns, width, fields)
260 }
261
262 /// This declaration with [`Width::Auto`] turned into the bucket `rows` and `days` ask for.
263 ///
264 /// The one place an automatic width becomes a real one, and the reason it is a method rather
265 /// than something the loader does inline: the sort key is built from the width, so a width of
266 /// [`Width::Auto`] reaching the sort would be a `date_trunc` by a unit no calendar has. Whoever
267 /// is about to sort calls this first and what comes back can be sorted by.
268 ///
269 /// A width somebody wrote down is left exactly as they wrote it. `exact(l_shipdate)` stays
270 /// exact on a table of any size, because the declaration is what the table is asked to be and
271 /// a loader quietly widening it would make the setting a suggestion.
272 ///
273 /// `rows` is how many rows are about to be written and `days` is the span of the leading column
274 /// across them, both as well as the caller can tell. Neither has to be right: they pick between
275 /// three layouts that hold the same rows and answer the same queries, so being wrong costs some
276 /// pruning or some locality and cannot cost an answer. A caller that cannot tell at all passes
277 /// zero and gets [`Width::DEFAULT`].
278 #[must_use]
279 pub fn fitted(&self, rows: u64, days: u64) -> Self {
280 match self.width {
281 Width::Auto => {
282 Self { columns: self.columns.clone(), width: Width::for_span(rows, days) }
283 }
284 _ => self.clone(),
285 }
286 }
287
288 /// The columns, outermost first, as indexes into the table's column list.
289 #[must_use]
290 pub fn columns(&self) -> &[u32] {
291 &self.columns
292 }
293
294 /// How coarsely the leading column is bucketed.
295 #[must_use]
296 pub fn width(&self) -> Width {
297 self.width
298 }
299
300 /// The leading column, which is the one the pruning is about.
301 #[must_use]
302 pub fn partition(&self) -> u32 {
303 self.columns[0]
304 }
305
306 /// How this reads with the table's column names filled in, for an error or a `SHOW`.
307 #[must_use]
308 pub fn describe(&self, names: &[String]) -> String {
309 let named =
310 |at: u32| names.get(at as usize).cloned().unwrap_or_else(|| format!("column {at}"));
311 let leading = match self.width {
312 Width::Exact => named(self.partition()),
313 other => format!("{}({})", other.to_string().to_lowercase(), named(self.partition())),
314 };
315 let rest = self.columns[1..].iter().map(|&at| named(at)).collect::<Vec<_>>();
316 std::iter::once(leading).chain(rest).collect::<Vec<_>>().join(", ")
317 }
318}
319
320/// A declaration as somebody wrote it, before a catalog turned the names into column indexes.
321///
322/// [`Clustering`] holds indexes, which means it cannot be built without the table in hand, and the
323/// text is typed in a session that may name a table this database does not have. So the parse
324/// produces this and whoever has the catalog turns it into the real thing, which is also where the
325/// name errors come from and where they can say which table they are about.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct Declared {
328 table: String,
329 width: Option<Width>,
330 columns: Vec<String>,
331}
332
333impl Declared {
334 /// The table the declaration is about, as it was written.
335 #[must_use]
336 pub fn table(&self) -> &str {
337 &self.table
338 }
339
340 /// The width the text named, or `None` when it named none and the column's type decides.
341 #[must_use]
342 pub fn width(&self) -> Option<Width> {
343 self.width
344 }
345
346 /// The columns, outermost first, as they were written.
347 #[must_use]
348 pub fn columns(&self) -> &[String] {
349 &self.columns
350 }
351}
352
353/// Whether this name is the one the row order declaration is written under.
354///
355/// Both spellings, because a setting rudb has and DuckDB does not may be reached for under a
356/// prefix by somebody being careful about which engine they are talking to. The caller decides
357/// first that no DuckDB setting is called this, which is what keeps the compatible answer winning
358/// should upstream ever take a setting of either name.
359#[must_use]
360pub fn is_clustering_setting(name: &str) -> bool {
361 name.eq_ignore_ascii_case("cluster_by") || name.eq_ignore_ascii_case("rudb.cluster_by")
362}
363
364/// Parses the `cluster_by` session setting.
365///
366/// The grammar is a comma separated list of `table(column, column, ...)`, with the leading column
367/// optionally wrapped in the width it is bucketed at: `lineitem(quarter(l_shipdate), l_orderkey)`.
368/// That is what [`Clustering::describe`] prints with the table name put in front of it, so a
369/// declaration read back out of a table can be pasted straight back into the setting.
370///
371/// A leading column with no wrapper around it leaves the width to the column's type, which is
372/// [`Clustering::over`], so `lineitem(l_shipdate, l_orderkey)` gets [`Width::Auto`] and
373/// `orders(o_orderkey)` gets the exact value. Whitespace between tokens is free and a trailing
374/// comma is allowed, for the reason the relationship grammar allows one: a setting long enough to
375/// want a line per table is a setting somebody will edit.
376///
377/// The two tables the stage 0 measurement clusters, in this grammar:
378///
379/// ```text
380/// lineitem(quarter(l_shipdate), l_orderkey, l_linenumber),
381/// orders(quarter(o_orderdate), o_orderkey)
382/// ```
383///
384/// # Errors
385///
386/// If an entry is malformed. Nothing here can say whether a table or a column exists, since there
387/// is no catalog at this layer, so those are the caller's errors and this one's are about shape.
388pub fn parse_clustering(setting: &str) -> Result<Vec<Declared>> {
389 let mut declared = Vec::new();
390 for entry in entries(setting) {
391 declared.push(parse_entry(&entry)?);
392 }
393 Ok(declared)
394}
395
396/// The setting cut at the commas that separate tables, leaving the ones inside a column list.
397///
398/// The one real ambiguity in the grammar, the same one the relationship grammar has: a comma
399/// separates two declarations and also separates two columns of the same one. Depth tells them
400/// apart, and the width wrapper means the depth goes to two rather than one.
401fn entries(setting: &str) -> Vec<String> {
402 let mut entries = Vec::new();
403 let mut current = String::new();
404 let mut depth = 0usize;
405 for character in setting.chars() {
406 match character {
407 '(' => depth += 1,
408 ')' => depth = depth.saturating_sub(1),
409 // A comma outside every parenthesis ends a declaration. One inside a list belongs to
410 // the list, and a closing parenthesis with nothing open is left for the parse below to
411 // complain about rather than being treated as a separator.
412 ',' if depth == 0 => {
413 entries.push(std::mem::take(&mut current));
414 continue;
415 }
416 _ => {}
417 }
418 current.push(character);
419 }
420 entries.push(current);
421 entries
422 .into_iter()
423 .map(|entry| entry.trim().to_owned())
424 .filter(|entry| !entry.is_empty())
425 .collect()
426}
427
428fn parse_entry(entry: &str) -> Result<Declared> {
429 let Some((table, rest)) = entry.split_once('(') else {
430 return Err(malformed(format!("expected `table(column, ...)` and found `{entry}`")));
431 };
432 let Some(inside) = rest.trim_end().strip_suffix(')') else {
433 return Err(malformed(format!("`{entry}` is missing its closing parenthesis")));
434 };
435 let table = table.trim();
436 if table.is_empty() {
437 return Err(malformed(format!("`{entry}` names no table")));
438 }
439 let mut columns = Vec::new();
440 for column in inside.split(',').map(str::trim).filter(|column| !column.is_empty()) {
441 columns.push(column.to_owned());
442 }
443 if columns.is_empty() {
444 return Err(malformed(format!("`{entry}` names no column")));
445 }
446 // The width rides on the leading column and nowhere else, since it is the column the bucketing
447 // is about, so a wrapper anywhere after the first is a declaration nobody can honour.
448 let (width, leading) = split_width(&columns[0])?;
449 for column in &columns[1..] {
450 if column.contains('(') {
451 return Err(malformed(format!(
452 "`{column}` is bucketed and only the leading column of `{table}` can be"
453 )));
454 }
455 }
456 columns[0] = leading;
457 Ok(Declared { table: table.to_owned(), width, columns })
458}
459
460/// A leading column as the width it was wrapped in, if it was wrapped, and the column itself.
461fn split_width(leading: &str) -> Result<(Option<Width>, String)> {
462 let Some((word, rest)) = leading.split_once('(') else {
463 return Ok((None, leading.to_owned()));
464 };
465 let Some(column) = rest.trim_end().strip_suffix(')') else {
466 return Err(malformed(format!("`{leading}` is missing its closing parenthesis")));
467 };
468 let column = column.trim();
469 if column.is_empty() {
470 return Err(malformed(format!("`{leading}` names no column")));
471 }
472 let word = word.trim();
473 let width = [Width::Exact, Width::Month, Width::Quarter, Width::Year, Width::Auto]
474 .into_iter()
475 .find(|width| width.to_string().eq_ignore_ascii_case(word))
476 .ok_or_else(|| {
477 malformed(format!(
478 "`{word}` is not a partition width, which is one of exact, month, quarter, year or \
479 auto"
480 ))
481 })?;
482 Ok((Some(width), column.to_owned()))
483}
484
485fn malformed(message: impl Into<String>) -> Error {
486 Error::invalid_input(format!("invalid rudb clustering: {}", message.into()))
487}
488
489#[cfg(test)]
490mod tests {
491 use super::{Clustering, Width, parse_clustering};
492 use crate::types::{Field, LogicalType};
493
494 fn lineitem() -> Vec<Field> {
495 vec![
496 Field::new("l_orderkey", LogicalType::BigInt),
497 Field::new("l_linenumber", LogicalType::Integer),
498 Field::new("l_shipdate", LogicalType::Date),
499 ]
500 }
501
502 #[test]
503 fn a_declaration_that_could_never_be_satisfied_is_refused() {
504 let fields = lineitem();
505 assert!(Clustering::new(Vec::new(), Width::Exact, &fields).is_err(), "no column at all");
506 assert!(Clustering::new(vec![3], Width::Exact, &fields).is_err(), "past the end");
507 assert!(Clustering::new(vec![0, 1, 0], Width::Exact, &fields).is_err(), "twice");
508 assert!(Clustering::new(vec![2, 0], Width::Month, &fields).is_ok());
509 }
510
511 #[test]
512 fn a_calendar_bucket_on_a_column_with_no_calendar_in_it_is_refused() {
513 // There is no month of an order key, so a loader handed this would have nothing to sort
514 // by. The plain width is fine on the same column, which is what makes this worth checking
515 // rather than refusing every leading column that is not a date.
516 let fields = lineitem();
517 let complaint = Clustering::new(vec![0, 2], Width::Month, &fields)
518 .expect_err("a bigint has no months")
519 .to_string();
520 assert!(complaint.contains("l_orderkey"), "{complaint}");
521 assert!(complaint.contains("MONTH"), "{complaint}");
522 assert!(
523 Clustering::new(vec![0, 2], Width::Exact, &fields).is_ok(),
524 "no bucket, no problem"
525 );
526 }
527
528 /// A declaration that names no width leaves it to the data on a date, and is exact elsewhere.
529 #[test]
530 fn a_declaration_with_no_width_takes_the_quarter_on_a_date_and_nothing_elsewhere() {
531 let fields = lineitem();
532 let dated = Clustering::over(vec![2, 0, 1], &fields).expect("a date leads");
533 assert_eq!(dated.width(), Width::Auto, "nobody said, so the rows will say");
534 assert_eq!(dated.columns(), [2, 0, 1], "the columns are the ones asked for, in order");
535 // A bigint has no quarters, so the same call on one has to come back exact rather than
536 // come back an error, which is the whole reason the width is picked from the column.
537 let keyed = Clustering::over(vec![0, 2], &fields).expect("a bigint leads");
538 assert_eq!(keyed.width(), Width::Exact);
539 // The checks are the ones a written out declaration gets, since it is the same call.
540 assert!(Clustering::over(Vec::new(), &fields).is_err(), "no column at all");
541 assert!(Clustering::over(vec![7], &fields).is_err(), "past the end");
542 }
543
544 #[test]
545 fn every_width_survives_its_byte() {
546 for width in [Width::Exact, Width::Month, Width::Quarter, Width::Year, Width::Auto] {
547 assert_eq!(Width::from_tag(width.tag()), Some(width), "{width}");
548 }
549 assert_eq!(Width::from_tag(5), None, "a tag from a build that knows more than this one");
550 }
551
552 /// The rule picks a different calendar width at two scale factors of the same table.
553 ///
554 /// The whole point of the rule, and the thing a constant cannot do. Both rows are TPC-H
555 /// lineitem over the same seven years of ship dates, six million rows at SF1 and sixty million
556 /// at SF10, and the numbers are the ones `dbgen` actually produces rather than round figures.
557 ///
558 /// SF1 lands on the quarter, which is what `14-the-partition-width.md` measured best there.
559 /// SF10 lands on the month, because a month of SF10 holds seven hundred thousand rows, which is
560 /// three times what SF1's quarter held: the width that was too narrow at one scale is
561 /// comfortable at the next one up, and the calendar word never moved.
562 #[test]
563 fn the_same_table_at_two_scales_gets_two_widths() {
564 let span = 2525;
565 assert_eq!(Width::for_span(6_001_215, span), Width::Quarter, "SF1");
566 assert_eq!(Width::for_span(59_986_052, span), Width::Month, "SF10");
567 // Smaller than SF1 does not go on getting coarser. A tenth of the rows is a long way under
568 // the target at every width, and the answer is still the quarter, because what is under the
569 // target is the case for not cutting finer and says nothing about cutting coarser.
570 assert_eq!(Width::for_span(600_572, span), Width::Quarter, "SF0.1");
571 // SF1 orders is the row that made the year a mistake. A quarter of lineitem's size over the
572 // same span, under the target at every width, and measured best on a quarter all the same.
573 assert_eq!(Width::for_span(1_500_000, span), Width::Quarter, "SF1 orders");
574 // Nothing to divide. An empty table and a table whose dates are all the same day both have
575 // no shape to fit, so both get the measured default.
576 assert_eq!(Width::for_span(0, span), Width::DEFAULT);
577 assert_eq!(Width::for_span(6_001_215, 0), Width::DEFAULT);
578 }
579
580 /// An automatic width becomes a real one and a written one is left alone.
581 #[test]
582 fn fitting_a_declaration_resolves_the_automatic_width_and_only_that_one() {
583 let fields = lineitem();
584 let auto = Clustering::over(vec![2, 0, 1], &fields).expect("a date leads");
585 assert_eq!(auto.width(), Width::Auto);
586 let fitted = auto.fitted(6_001_215, 2525);
587 assert_eq!(fitted.width(), Width::Quarter, "the rows decided");
588 assert_eq!(fitted.columns(), auto.columns(), "and nothing else moved");
589 // Written down is written down. A table of any size declared exact stays exact, because the
590 // declaration is what the table is asked to be rather than a hint the loader may improve on.
591 for width in [Width::Exact, Width::Month, Width::Quarter, Width::Year] {
592 let asked = Clustering::new(vec![2, 0, 1], width, &fields).expect("valid");
593 assert_eq!(asked.fitted(59_986_052, 2525), asked, "{width}");
594 }
595 }
596
597 /// The two tables the stage 0 measurement clusters, written the way the setting takes them.
598 #[test]
599 fn the_two_clustered_tpch_tables_parse_into_two_declarations() {
600 let setting = "lineitem(quarter(l_shipdate), l_orderkey, l_linenumber), \
601 orders(quarter(o_orderdate), o_orderkey)";
602 let declared = parse_clustering(setting).expect("parse");
603 assert_eq!(declared.len(), 2, "a comma inside a column list is not a separator");
604 assert_eq!(declared[0].table(), "lineitem");
605 assert_eq!(declared[0].width(), Some(Width::Quarter));
606 assert_eq!(declared[0].columns(), ["l_shipdate", "l_orderkey", "l_linenumber"]);
607 assert_eq!(declared[1].table(), "orders");
608 assert_eq!(declared[1].columns(), ["o_orderdate", "o_orderkey"]);
609 // A trailing comma and a line per table, which is how a setting this long gets edited.
610 let spread = "lineitem(month(l_shipdate), l_orderkey),\n orders(o_orderkey),\n";
611 let declared = parse_clustering(spread).expect("parse");
612 assert_eq!(declared.len(), 2);
613 assert_eq!(declared[0].width(), Some(Width::Month));
614 // No wrapper means no width was named, which is not the same as naming the exact value:
615 // the first leaves the width to the column's type and the second overrides it.
616 assert_eq!(declared[1].width(), None);
617 assert_eq!(parse_clustering("t(exact(d))").expect("parse")[0].width(), Some(Width::Exact));
618 assert!(parse_clustering("").expect("parse").is_empty(), "a reset names no table");
619 }
620
621 /// What a declaration reads back as is what the setting takes, so one can be pasted into it.
622 ///
623 /// The three calendar widths and the automatic one round trip. The exact one does not, and that
624 /// is worth a test of its own rather than a carve out in a loop: [`Clustering::describe`] prints
625 /// no wrapper for it, the parse of a bare leading column names no width, and a bare date column
626 /// then gets the automatic width. So pasting an exactly sorted date declaration back into the
627 /// setting gives one that leaves the width to the data. Whoever wants the exact value back says
628 /// `exact(...)`, which is why that word is in the grammar at all given that no printer produces
629 /// it.
630 #[test]
631 fn what_a_declaration_describes_itself_as_parses_back_into_the_same_declaration() {
632 let fields = lineitem();
633 let names = fields.iter().map(|field| field.name.clone()).collect::<Vec<_>>();
634 let at = |name: &String| names.iter().position(|it| it == name).expect("a column") as u32;
635 for width in [Width::Month, Width::Quarter, Width::Year, Width::Auto] {
636 let asked = Clustering::new(vec![2, 0, 1], width, &fields).expect("valid");
637 let written = format!("lineitem({})", asked.describe(&names));
638 let read = parse_clustering(&written).expect("parse");
639 let columns: Vec<u32> = read[0].columns().iter().map(at).collect();
640 let again = Clustering::new(columns, read[0].width().expect("a width"), &fields)
641 .expect("valid");
642 assert_eq!(again, asked, "{written}");
643 }
644 let exact = Clustering::new(vec![2, 0, 1], Width::Exact, &fields).expect("valid");
645 let written = format!("lineitem({})", exact.describe(&names));
646 assert_eq!(written, "lineitem(l_shipdate, l_orderkey, l_linenumber)");
647 let read = parse_clustering(&written).expect("parse");
648 assert_eq!(read[0].width(), None, "a bare date column names no width");
649 let columns: Vec<u32> = read[0].columns().iter().map(at).collect();
650 assert_eq!(
651 Clustering::over(columns, &fields).expect("valid").width(),
652 Width::Auto,
653 "and a silent date declaration leaves the width to the data"
654 );
655 }
656
657 /// The shapes the parse turns away, which are the ones a catalog could never make sense of.
658 #[test]
659 fn a_declaration_that_is_not_a_table_and_a_column_list_is_refused() {
660 for bad in [
661 "lineitem",
662 "lineitem(",
663 "(l_shipdate)",
664 "lineitem()",
665 "lineitem(day(l_shipdate))",
666 "lineitem(l_shipdate, month(l_orderkey))",
667 "lineitem(month())",
668 ] {
669 let complaint = parse_clustering(bad).expect_err(bad).message().to_owned();
670 assert!(complaint.contains("clustering"), "{bad}: {complaint}");
671 }
672 // The one that reads like a mistake and is not: a width word is only a width in front of
673 // the leading column, so a column actually called `year` is still a column.
674 let declared = parse_clustering("t(year)").expect("parse");
675 assert_eq!(declared[0].columns(), ["year"]);
676 assert_eq!(declared[0].width(), None);
677 }
678
679 #[test]
680 fn a_declaration_reads_back_the_way_it_was_written() {
681 // The one thing this string is for is that somebody can check the layout is what they
682 // asked for without reading a column index against a schema by hand.
683 let fields = lineitem();
684 let names = fields.iter().map(|field| field.name.clone()).collect::<Vec<_>>();
685 let stage_zero = Clustering::new(vec![2, 0, 1], Width::Month, &fields).expect("valid");
686 assert_eq!(stage_zero.describe(&names), "month(l_shipdate), l_orderkey, l_linenumber");
687 let plain = Clustering::new(vec![0], Width::Exact, &fields).expect("valid");
688 assert_eq!(plain.describe(&names), "l_orderkey");
689 }
690}