Skip to main content

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}