Skip to main content

rudb_native/
stats.rs

1//! Building a table's statistics sections from the table's own columns.
2//!
3//! The same meeting place `graph` is, for the other document. `rudb-stats` at rank 5 knows what a
4//! column summary says and knows nothing about a file; the rest of this crate knows how to put an
5//! opaque payload in a file and nothing about what one means. Building a summary for a real table
6//! means reading the column back, so it happens here, in the crate allowed to see both.
7//!
8//! Everything here obeys `spec/stats/03-the-file-format.md` section 3.1, which is the graph
9//! document's section 3.1 applied to a second kind of payload: delete every statistics section and
10//! no query changes its answer, only the time. That is why [`summary`] and [`sketches`] answer with
11//! an [`Option`] and not a [`Result`]. There is no failure they could report that is not answered
12//! by planning the query the way it was planned before the section existed.
13//!
14//! # The invariant has teeth here that it does not have in the graph layer
15//!
16//! A key map can only make a join faster. A summary can answer a query: a `COUNT(DISTINCT c)` comes
17//! out of one without the column being touched. So the thing that has to survive is not only *is
18//! the section there* but *is the number in it exact*, and [`Summary::distinct_class`] is where that
19//! lives. This module's job is to never write [`Class::Exact`] onto a number that is not, which in
20//! practice means one rule: the sketch says whether it overflowed, and everything else follows from
21//! that answer rather than from what the writer hoped.
22//!
23//! # One pass, and what that costs
24//!
25//! Section 3.7 gives the statistics build ten percent of the native write time, and the way to stay
26//! inside it is not to be clever but to read the column once. [`build_summary`] takes one scan and
27//! computes every field of the summary and the sketch from it, so the cost of statistics on a write
28//! is the cost of one more read of each column asked for, and no column is read twice.
29//!
30//! # The per stripe rule
31//!
32//! Section 3.8 says per stripe structures are written only for the columns that get read, and the
33//! arithmetic behind that is not close: sixteen `lineitem` columns at SF100, sketched per stripe
34//! even at the small k a stripe sketch keeps, come to several hundred megabytes against a budget of
35//! two percent. So the default is a merged sketch and nothing else, and [`Sketches::stripes`] being
36//! empty is the state the rule says most columns are in rather than a degraded one.
37//!
38//! [`read_columns`] is what this build promotes a column with. It reads the promoted set off the
39//! file, which today means the columns that already carry a key map or a forward link, because
40//! those are the columns something has declared a relationship or a key over and section 3.8 names
41//! them directly. Document 06's observation log is the other source the spec names and it is not
42//! built yet, so when it arrives it adds columns to this list and changes nothing else here.
43//!
44//! Promotion costs no extra hashing. The column is read once and hashed once either way, and what
45//! changes is where the counting is reset. [`build_summary_for`] has the argument.
46
47use std::cmp::Ordering;
48use std::path::Path;
49use std::time::{Duration, Instant};
50
51use rudb_common::bounds::{self, Bound};
52use rudb_common::stat::Class;
53use rudb_common::{LogicalType, Result, Value};
54use rudb_encoding::sketch::{DEFAULT_K, Sketch};
55use rudb_stats::{Order, STRIPE_K, Sketches, Summary, sketches::HEADER_BYTES as SKETCH_HEADER};
56use rudb_storage::count::{Counts, countable};
57use rudb_vector::Vector;
58
59use crate::section::{self, Attachment};
60use crate::{Catalog, Reader, invalid};
61
62/// The share of a table's stored column bytes its statistics sections are allowed to cost together.
63///
64/// Two percent, per section 3.8, and kept apart from the graph layer's ten percent rather than
65/// pooled with it. Two budgets that share a pot are two budgets where the one that runs first wins,
66/// and a table whose key maps happened to be built before its summaries would then have no
67/// summaries for a reason that has nothing to do with summaries. They are counted separately for the
68/// same reason they are two documents.
69pub const BUDGET_SHARE: u64 = 2;
70
71/// The size below which a table's statistics sections always fit, whatever the share works out to.
72///
73/// The same floor and the same argument as `graph::BUDGET_FLOOR`. A summary is a few hundred bytes
74/// on a table of any size and two percent of a small, well compressed column is less than that, so
75/// the pure rule would throw away the cheapest structure in the system for being expensive.
76pub const BUDGET_FLOOR: u64 = 64 * 1024;
77
78/// What one column's statistics cost and what they say.
79#[derive(Debug, Clone)]
80pub struct Built {
81    /// Which column was summarized.
82    pub column: usize,
83    /// Rows in the column, nulls included.
84    pub rows: u64,
85    /// Distinct non-null values, as the summary reports them.
86    pub distinct: u64,
87    /// Whether that distinct count is exact rather than a sketch estimate.
88    pub exact: bool,
89    /// Which way the values run.
90    pub order: Order,
91    /// What the summary section takes in the file.
92    pub summary_bytes: usize,
93    /// What the sketches section takes in the file.
94    pub sketch_bytes: usize,
95    /// How many per stripe sketches went in it, which is zero for a column the per stripe rule did
96    /// not promote and is most of them.
97    pub stripes: usize,
98    /// What the column takes in the file, which is what the budget is a share of.
99    pub column_bytes: u64,
100    /// Whether the sections were kept. False means they were built, measured, and found to cost more
101    /// than section 3.8 allows, so the file does not have them and every query plans as though
102    /// statistics had never been implemented.
103    pub built: bool,
104    /// How long the build took, the reading of the column included.
105    pub build: Duration,
106}
107
108impl Built {
109    /// Both sections together, which is what the budget spends.
110    #[must_use]
111    pub fn bytes(&self) -> usize {
112        self.summary_bytes + self.sketch_bytes
113    }
114}
115
116/// A column's summary and its sketches, which are built together because they are one pass.
117#[derive(Debug, Clone)]
118pub struct Stats {
119    /// What the column says about itself.
120    pub summary: Summary,
121    /// The sketch the distinct count came out of.
122    pub sketches: Sketches,
123}
124
125/// Builds the summary and the sketches for one column of a committed table.
126///
127/// # Errors
128///
129/// If the column cannot be read, is past the end of the table, or is of a type with no hash rule.
130/// The last one is refused by name rather than approximated: the types without a rule are the
131/// interval and the nested ones, a summary of one would carry a distinct count of zero that nothing
132/// could tell from a column of nulls, and none of TPC-H or ClickBench has one.
133pub fn build_summary(reader: &Reader, column: usize) -> Result<Stats> {
134    build_summary_for(reader, column, false)
135}
136
137/// The same, keeping a sketch per stripe as well as the merged one when `per_stripe` is set.
138///
139/// Whether to set it is section 3.8's rule and not a caller's taste: per stripe structures are
140/// written only for the columns that get read, because sixteen `lineitem` columns at SF100 come to
141/// several hundred megabytes of them against a budget of two percent. [`read_columns`] is what this
142/// build answers that question with.
143///
144/// The extra sketches cost no extra hashing. Each stripe is counted into its own [`Counts`] at the
145/// column's k, the merged sketch is the union of those, which is exact because they are all at the
146/// same k, and each one is written down at [`rudb_stats::STRIPE_K`] through [`Sketch::narrowed`],
147/// which is exact because a bottom-k of a bottom-k is a bottom-k. So the column is read once and
148/// hashed once either way, and the difference between a promoted column and an ordinary one is
149/// where the counting is reset and how much of it is written.
150///
151/// # Errors
152///
153/// If the column cannot be read, is past the end of the table, or is of a type with no hash rule.
154/// The last one is refused by name rather than approximated: the types without a rule are the
155/// interval and the nested ones, a summary of one would carry a distinct count of zero that nothing
156/// could tell from a column of nulls, and none of TPC-H or ClickBench has one.
157pub fn build_summary_for(reader: &Reader, column: usize, per_stripe: bool) -> Result<Stats> {
158    let fields = reader.table().fields();
159    let Some(field) = fields.get(column) else {
160        return Err(invalid(&format!(
161            "column {column} is past the {} of table {}",
162            fields.len(),
163            reader.table().name()
164        )));
165    };
166    if !countable(&field.ty) {
167        return Err(invalid(&format!(
168            "a summary of {} needs a hash rule, and {} has none",
169            field.name, field.ty
170        )));
171    }
172    let blind = || {
173        // A blind column: a form `rudb_storage::count` has no arm for turned up, so its sketch is
174        // missing rows and says nothing about which. A distinct count that is too low is the one
175        // error an estimator has no defence against, so the column gets no summary at all rather
176        // than a summary with a number in it nothing can check.
177        invalid(&format!(
178            "column {} of {} holds a form with no hash rule, so it has no sketch",
179            field.name,
180            reader.table().name()
181        ))
182    };
183
184    let mut whole = Counts::new(1);
185    let mut stripes = Vec::new();
186    let mut pass = Pass::new(&field.ty, reader.table().generation());
187    for stripe in reader.stripe_parts() {
188        pass.open_stripe();
189        let mut counted = per_stripe.then(|| Counts::new(1));
190        for part in stripe {
191            let chunk = reader.read(part, &[column])?;
192            match counted.as_mut() {
193                Some(counted) => counted.add(&chunk),
194                None => whole.add(&chunk),
195            }
196            pass.scan(chunk.column(0)?);
197        }
198        pass.close_stripe();
199        if let Some(counted) = counted {
200            stripes.push(counted.sketch(0).ok_or_else(blind)?);
201        }
202    }
203    if !per_stripe {
204        return Ok(pass.finish(whole.sketch(0).ok_or_else(blind)?, Vec::new()));
205    }
206    let mut merged = Sketch::new(DEFAULT_K)?;
207    for stripe in &stripes {
208        merged = merged.union(stripe)?;
209    }
210    let narrowed =
211        stripes.iter().map(|stripe| stripe.narrowed(STRIPE_K)).collect::<Result<Vec<_>>>()?;
212    Ok(pass.finish(merged, narrowed))
213}
214
215/// One scan of one column, in `rid` order, for everything the sketch does not answer.
216///
217/// In `rid` order because the order fields depend on it. A pass that read the parts in any other
218/// order would report a column as unordered that is sorted, which costs a plan and not an answer,
219/// and would report the run count of a shuffle, which is worse because it is a number rather than a
220/// flag and looks like it was measured.
221///
222/// The distinct count is not here. That is `rudb_storage::count::Counts`, which walks a vector by
223/// its form rather than a row at a time and which a column of a million runs costs one hash. Doing
224/// it twice would double the expensive half of the build and the budget is ten percent of the write.
225struct Pass {
226    rows: u64,
227    nulls: u64,
228    low: Option<Bound>,
229    high: Option<Bound>,
230    /// False once a non-null value turned up that has no ordered bound, which makes both ends
231    /// unusable rather than merely absent.
232    bounded: bool,
233    ascending: bool,
234    descending: bool,
235    runs: u64,
236    previous: Option<Bound>,
237    bytes: u64,
238    widest: u64,
239    generation: u64,
240    /// The two ends of the stripe being read, kept apart rather than as a pair so that each one can
241    /// be compared against and refilled on its own. A pair would have to be taken out and put back
242    /// whole, which is the move that made this pass allocate.
243    stripe_low: Option<Bound>,
244    stripe_high: Option<Bound>,
245    stripes: Vec<(Bound, Bound)>,
246    /// What one value of this column takes, when every value takes the same.
247    ///
248    /// Read off the type once rather than off each value, because for every fixed width column it is
249    /// a constant and asking a value for it is a branch a hundred million times to hear the same
250    /// number. `None` is a variable width type and those are measured per value.
251    fixed: Option<u64>,
252    /// The scale of a decimal column, so that an integer read out of a vector becomes the bound the
253    /// column's other writers would have written for the same value.
254    scale: Option<u8>,
255}
256
257impl Pass {
258    fn new(ty: &LogicalType, generation: u64) -> Self {
259        Self {
260            rows: 0,
261            nulls: 0,
262            low: None,
263            high: None,
264            bounded: true,
265            ascending: true,
266            descending: true,
267            runs: 0,
268            previous: None,
269            bytes: 0,
270            widest: 0,
271            generation,
272            stripe_low: None,
273            stripe_high: None,
274            stripes: Vec::new(),
275            fixed: fixed_width(ty),
276            scale: bounds::scale_of(ty),
277        }
278    }
279
280    /// One vector of the column, typed rather than a value at a time where the type allows it.
281    ///
282    /// `signed_at` and `bytes_at` between them cover every integer, date, timestamp, decimal, string
283    /// and blob column, which is all sixteen of TPC-H `lineitem` and all but a handful of
284    /// ClickBench. Both are a load against a slice. The fall back below builds a `Value`, and it is
285    /// there for the float columns and for the forms the two fast paths cannot read, not as the
286    /// ordinary path.
287    fn scan(&mut self, vector: &Vector) {
288        // row at a time: the run count and the order flags are a sequential dependency. Whether this
289        // value is below the one before it is a question about a pair of adjacent rows, so there is
290        // no shape of this loop that answers it a vector at a time, and the two typed accessors
291        // below are loads against a slice rather than value construction. What the checker is
292        // looking for is the third arm, which does build a `Value`, and that one runs for a float
293        // column and for a form the first two cannot read and for nothing else.
294        for row in 0..vector.len() {
295            self.rows += 1;
296            if vector.is_null_at(row) {
297                self.nulls += 1;
298                continue;
299            }
300            if let Some(signed) = vector.signed_at(row) {
301                let bound = match self.scale {
302                    Some(scale) => Bound::Scaled { unscaled: signed, scale },
303                    None => Bound::Int(signed),
304                };
305                self.value(bound, self.fixed.unwrap_or(8));
306                continue;
307            }
308            if let Some(bytes) = vector.bytes_at(row) {
309                self.bytes_value(bytes);
310                continue;
311            }
312            // row at a time: a float and a form neither typed accessor above can read have no slice
313            // to walk, so the value is built for this row and for no other.
314            let value = vector.value_at(row);
315            let width = self.fixed.unwrap_or_else(|| width(&value));
316            match Bound::of_value(&value) {
317                Some(bound) => self.value(bound, width),
318                None => {
319                    // A non-null value with no ordered bound. Both ends go rather than the value
320                    // being skipped, because an end computed from only the values that had bounds is
321                    // an end that answers a MIN with a value the column does not hold.
322                    self.bytes = self.bytes.saturating_add(width);
323                    self.widest = self.widest.max(width);
324                    self.bounded = false;
325                    self.ascending = false;
326                    self.descending = false;
327                }
328            }
329        }
330    }
331
332    /// One non-null value, as its bound and its width.
333    ///
334    /// Every end is compared before it is copied. The obvious way to write this is to hand the
335    /// bound to each end and let the end keep whichever is smaller, and that costs a clone a row per
336    /// end whether or not the row is one. For an integer that is four copies of a machine word and
337    /// hardly matters. For a string it is four allocations a row, and on SF1 `l_comment` that is
338    /// twenty four million of them for a column with two ends. Compared first, an end is copied once
339    /// on a sorted column and about log n times on a shuffled one.
340    fn value(&mut self, bound: Bound, width: u64) {
341        self.measure(width);
342        let ordering = self.previous.as_ref().map(|previous| previous.order(&bound));
343        self.run(ordering);
344        if takes(&self.low, &bound, Ordering::Less) {
345            self.low = Some(bound.clone());
346        }
347        if takes(&self.high, &bound, Ordering::Greater) {
348            self.high = Some(bound.clone());
349        }
350        if takes(&self.stripe_low, &bound, Ordering::Less) {
351            self.stripe_low = Some(bound.clone());
352        }
353        if takes(&self.stripe_high, &bound, Ordering::Greater) {
354            self.stripe_high = Some(bound.clone());
355        }
356        self.previous = Some(bound);
357    }
358
359    /// The same for a byte string, without a `Vec` a row.
360    ///
361    /// A string column is where the pass above still allocates, because the bound it is handed had
362    /// to be built out of the slice before it could be compared to anything, and the row it keeps as
363    /// the previous one is a new `Vec` every row whether or not any end moved. Here nothing is built
364    /// to be compared, and the buffer the previous row owns is refilled rather than replaced, which
365    /// is an allocation on the first row of the column and none after it.
366    ///
367    /// This is the difference between statistics costing a tenth of the write and costing as much as
368    /// it. At SF1, `lineitem`'s five string columns took nineteen of the pass's twenty eight seconds
369    /// before this and its eleven numeric columns took the other nine.
370    fn bytes_value(&mut self, bytes: &[u8]) {
371        self.measure(bytes.len() as u64);
372        let ordering = match &self.previous {
373            None => None,
374            Some(Bound::Bytes(previous)) => Some(Some(previous.as_slice().cmp(bytes))),
375            // A bound of another domain in a byte column, which a column of one type cannot hold.
376            Some(_) => Some(None),
377        };
378        self.run(ordering);
379        if takes_bytes(&self.low, bytes, Ordering::Less) {
380            fill(&mut self.low, bytes);
381        }
382        if takes_bytes(&self.high, bytes, Ordering::Greater) {
383            fill(&mut self.high, bytes);
384        }
385        if takes_bytes(&self.stripe_low, bytes, Ordering::Less) {
386            fill(&mut self.stripe_low, bytes);
387        }
388        if takes_bytes(&self.stripe_high, bytes, Ordering::Greater) {
389            fill(&mut self.stripe_high, bytes);
390        }
391        fill(&mut self.previous, bytes);
392    }
393
394    /// What one value costs, which is the byte total and the widest of them.
395    fn measure(&mut self, width: u64) {
396        self.bytes = self.bytes.saturating_add(width);
397        self.widest = self.widest.max(width);
398    }
399
400    /// What this value standing above, below or level with the one before it does to the order flags.
401    ///
402    /// The outer `None` is the first value of the column. The inner one is a pair this build cannot
403    /// order, which a column of one type cannot produce and which costs an order claim rather than
404    /// being assumed away.
405    fn run(&mut self, ordering: Option<Option<Ordering>>) {
406        match ordering {
407            None => self.runs = 1,
408            Some(Some(Ordering::Less)) => self.descending = false,
409            Some(Some(Ordering::Greater)) => {
410                self.ascending = false;
411                self.runs += 1;
412            }
413            Some(Some(Ordering::Equal)) => {}
414            Some(None) => {
415                self.ascending = false;
416                self.descending = false;
417            }
418        }
419    }
420
421    fn open_stripe(&mut self) {
422        self.stripe_low = None;
423        self.stripe_high = None;
424    }
425
426    fn close_stripe(&mut self) {
427        // Both taken whatever happens, so that a stripe of nothing but nulls leaves neither end
428        // behind for the next stripe to be compared against.
429        if let (Some(low), Some(high)) = (self.stripe_low.take(), self.stripe_high.take()) {
430            self.stripes.push((low, high));
431        }
432    }
433
434    fn finish(self, sketch: Sketch, stripes: Vec<Sketch>) -> Stats {
435        let present = self.rows - self.nulls;
436        // The one rule the module doc names. An exact distinct count is one the sketch never had to
437        // throw a value away to keep, and everything downstream of the count follows from this
438        // answer rather than from what the writer hoped.
439        let exact = sketch.is_exact();
440        let distinct = if exact {
441            sketch.len() as u64
442        } else {
443            // Rounded rather than truncated, and clamped under the rows it cannot exceed. An
444            // estimate above the row count is arithmetically possible and is always wrong, and a
445            // planner that sees one concludes a column has more distinct values than rows.
446            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
447            let estimate = sketch.distinct().round().max(0.0) as u64;
448            estimate.min(present)
449        };
450        let summary = Summary {
451            rows: self.rows,
452            nulls: self.nulls,
453            low: if self.bounded { self.low } else { None },
454            high: if self.bounded { self.high } else { None },
455            // Every end here came from a value the column holds, because this pass read them all.
456            // That is the whole difference between a summary and a zone map, which is allowed to be
457            // wider than its column and so can only skip and never answer.
458            ends_exact: self.bounded,
459            distinct,
460            // Exact or estimated, and never certified. A KMV sketch's relative error is about one
461            // over the square root of k, which is a standard error and not a bound, and Certified
462            // in this codebase means a bound that holds. Calling a one and a half percent standard
463            // error a guarantee is how an estimate gets treated as an answer.
464            distinct_class: if exact { Class::Exact } else { Class::Estimated },
465            // Only from an exact count. A sketch that overflowed cannot tell a column of a million
466            // unique values from one where two of them repeat, and uniqueness is the claim a key
467            // map is built on.
468            unique: exact && distinct == present,
469            order: if present == 0 {
470                Order::Neither
471            } else if self.ascending {
472                Order::Ascending
473            } else if self.descending {
474                Order::Descending
475            } else {
476                Order::Neither
477            },
478            runs: self.runs,
479            overlapping: overlapping(&self.stripes),
480            bytes: self.bytes,
481            widest: self.widest,
482            newest: self.generation,
483        };
484        // `new` rather than `merged` even for the empty case, because the two differ only in
485        // whether the list is checked and an empty list passes. A stripe sketch that is not at
486        // STRIPE_K is a bug in this file and is worth hearing about here rather than at the read.
487        let sketches = match Sketches::new(sketch.clone(), stripes) {
488            Ok(sketches) => sketches,
489            // Unreachable, since every stripe sketch above came out of `narrowed(STRIPE_K)` and a
490            // table cannot hold a million stripes. The merged sketch alone is the answer anyway:
491            // per stripe sketches are an optimization over a summary that is complete without
492            // them, so losing them costs a skipped stripe and never an answer.
493            Err(_) => Sketches::merged(sketch),
494        };
495        Stats { summary, sketches }
496    }
497}
498
499/// Whether an end has to become this bound, which is the question that replaces a clone.
500///
501/// `want` is [`Ordering::Less`] for a low end and [`Ordering::Greater`] for a high one. An end that
502/// is not there yet takes any value. A pair this build cannot order leaves the end alone, which is
503/// what [`Bound::smaller`] does across domains and which a column of one type cannot reach anyway.
504fn takes(held: &Option<Bound>, bound: &Bound, want: Ordering) -> bool {
505    match held {
506        None => true,
507        Some(held) => bound.order(held) == Some(want),
508    }
509}
510
511/// The same question asked of a slice, so that nothing is built to ask it.
512fn takes_bytes(held: &Option<Bound>, bytes: &[u8], want: Ordering) -> bool {
513    match held {
514        None => true,
515        Some(Bound::Bytes(held)) => bytes.cmp(held.as_slice()) == want,
516        Some(_) => false,
517    }
518}
519
520/// Puts these bytes in an end, reusing the buffer that is already there.
521///
522/// The whole of the byte path's advantage. A `Vec` that is cleared and refilled does not allocate
523/// once it is wide enough, and these ends plus the previous row are where every allocation of the
524/// value path went.
525fn fill(held: &mut Option<Bound>, bytes: &[u8]) {
526    match held {
527        Some(Bound::Bytes(held)) => {
528            held.clear();
529            held.extend_from_slice(bytes);
530        }
531        held => *held = Some(Bound::Bytes(bytes.to_vec())),
532    }
533}
534
535/// Whether any two of these stripe ranges overlap.
536///
537/// Sorted by low end and then walked, so this is one sort rather than the square. A pair this cannot
538/// order counts as overlapping, which is the answer that costs a skipped stripe rather than a wrong
539/// one.
540fn overlapping(stripes: &[(Bound, Bound)]) -> bool {
541    let mut order = (0..stripes.len()).collect::<Vec<_>>();
542    order
543        .sort_by(|&one, &other| stripes[one].0.order(&stripes[other].0).unwrap_or(Ordering::Equal));
544    order.windows(2).any(|pair| {
545        let before = &stripes[pair[0]].1;
546        let after = &stripes[pair[1]].0;
547        before.order(after) != Some(Ordering::Less)
548    })
549}
550
551/// What every value of this type takes, when they all take the same.
552///
553/// `None` for the variable width types, which is the two string ones and nothing else. Read off the
554/// type once by `Pass::new` rather than off each value.
555fn fixed_width(ty: &LogicalType) -> Option<u64> {
556    Some(match ty {
557        LogicalType::Boolean | LogicalType::TinyInt | LogicalType::UTinyInt => 1,
558        LogicalType::SmallInt | LogicalType::USmallInt => 2,
559        LogicalType::Integer | LogicalType::UInteger | LogicalType::Float | LogicalType::Date => 4,
560        LogicalType::HugeInt | LogicalType::UHugeInt | LogicalType::Decimal { .. } => 16,
561        LogicalType::Varchar | LogicalType::Blob => return None,
562        // The eight byte types: the two big integers, the double, and the four time ones. Anything
563        // else that reaches here is refused a summary by `countable` long before this.
564        _ => 8,
565    })
566}
567
568/// What one value takes, for the byte total and the widest value.
569///
570/// The logical width and not the stored one. The stored width is what the column's encoding chose
571/// and is already in the layout; this is what the value costs a plan that has to materialize it,
572/// which is the number a hash table sizing decision wants.
573fn width(value: &Value) -> u64 {
574    match value {
575        Value::Null => 0,
576        Value::Boolean(_) | Value::TinyInt(_) | Value::UTinyInt(_) => 1,
577        Value::SmallInt(_) | Value::USmallInt(_) => 2,
578        Value::Integer(_) | Value::UInteger(_) | Value::Float(_) | Value::Date(_) => 4,
579        Value::HugeInt(_) | Value::UHugeInt(_) | Value::Decimal { .. } => 16,
580        Value::Varchar(text) => text.len() as u64,
581        Value::Blob(bytes) => bytes.len() as u64,
582        // The eight byte types and anything else, which is every remaining scalar. A nested value
583        // reaching here would be counted at eight and is refused a summary long before this by
584        // `countable`.
585        _ => 8,
586    }
587}
588
589/// Builds the statistics for each of these columns and attaches them all in one commit.
590///
591/// One commit and not one each, for the reason `graph::build_key_maps` gives: a checkpoint that
592/// published one generation per column would be one chance per column of being interrupted halfway.
593///
594/// # Errors
595///
596/// If the file cannot be opened, a column cannot be summarized, or the attach fails.
597pub fn build_stats(path: &Path, table: &str, columns: &[usize]) -> Result<Vec<Built>> {
598    build_stats_within(path, table, columns, BUDGET_SHARE)
599}
600
601/// The columns of this table the per stripe rule promotes, in column order.
602///
603/// Section 3.8's default set: the columns something has declared a relationship or a key over. What
604/// this build has to go on for that is the file itself, so the answer is the columns that already
605/// carry a graph section, which is a key map or a forward link. That is not a proxy for the
606/// question, it is the same question asked of the only party that has been told the answer: a key
607/// map exists on a column because something declared it a key.
608///
609/// Empty is the ordinary answer and it is the right one. A table nothing has declared anything over
610/// gets table level summaries and no per stripe sketches, which is what section 3.8 says and what
611/// keeps SF100 inside two percent.
612///
613/// The other source the spec names is document 06's observation log, which promotes a column that
614/// queries turned out to read at the next checkpoint. It is not built yet. When it is, it adds
615/// columns here and nothing else in this file changes.
616#[must_use]
617pub fn read_columns(reader: &Reader) -> Vec<usize> {
618    let generation = reader.table().generation();
619    let mut promoted = reader
620        .table()
621        .sections()
622        .iter()
623        .filter(|held| held.among(section::GRAPH_KINDS) && held.usable(generation))
624        .filter_map(|held| usize::try_from(held.id).ok())
625        .collect::<Vec<_>>();
626    promoted.sort_unstable();
627    promoted.dedup();
628    promoted
629}
630
631/// The same, against a budget of `share` percent of the table's stored column bytes.
632///
633/// The budget is over the table rather than over a column, and when it binds the cheapest columns
634/// are admitted first. That is the same degenerate case section 3.7's expected value ordering has
635/// for a key map with no relationship over it: nothing has said which column a plan will ask about,
636/// so no summary is worth more than another and the ordering falls back to the denominator. Cheapest
637/// first is also the order that fits the most summaries in the room there is.
638///
639/// A column is all or nothing. Its summary and its sketches are admitted together or neither is,
640/// because a summary whose distinct count came from a sketch that was then dropped is a number with
641/// nothing behind it to check it against.
642///
643/// # Errors
644///
645/// If the file cannot be opened, a column cannot be summarized, or the attach fails.
646pub fn build_stats_within(
647    path: &Path,
648    table: &str,
649    columns: &[usize],
650    share: u64,
651) -> Result<Vec<Built>> {
652    let promoted = read_columns(&Catalog::open(path)?.table(table)?);
653    build_stats_for(path, table, columns, &promoted, share)
654}
655
656/// The same, with the per stripe set named rather than read off the file.
657///
658/// For a caller that knows something this build does not, which today is the measurement harness and
659/// tomorrow is whatever reads document 06's observation log. [`build_stats_within`] is the ordinary
660/// entry point and it asks [`read_columns`].
661///
662/// A column in `per_stripe` that is not in `columns` is ignored rather than refused, because the two
663/// lists answer different questions and a caller that names a promoted column it is not building is
664/// not making a mistake worth stopping for.
665///
666/// # Errors
667///
668/// If the file cannot be opened, a column cannot be summarized, or the attach fails.
669pub fn build_stats_for(
670    path: &Path,
671    table: &str,
672    columns: &[usize],
673    per_stripe: &[usize],
674    share: u64,
675) -> Result<Vec<Built>> {
676    let reader = Catalog::open(path)?.table(table)?;
677    let column_bytes = reader.layout().columns_total();
678    let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
679    let mut spent = held_bytes(&reader, columns)?;
680    let mut report = Vec::with_capacity(columns.len());
681    let mut payloads = Vec::with_capacity(columns.len());
682    for &column in columns {
683        let start = Instant::now();
684        let stats = build_summary_for(&reader, column, per_stripe.contains(&column))?;
685        let mut summary = Vec::new();
686        stats.summary.encode(&mut summary)?;
687        let mut sketches = Vec::new();
688        stats.sketches.encode(&mut sketches)?;
689        report.push(Built {
690            column,
691            rows: stats.summary.rows,
692            distinct: stats.summary.distinct,
693            exact: stats.summary.distinct_class == Class::Exact,
694            order: stats.summary.order,
695            summary_bytes: summary.len(),
696            sketch_bytes: sketches.len(),
697            stripes: stats.sketches.stripes.len(),
698            column_bytes,
699            built: false,
700            build: start.elapsed(),
701        });
702        payloads.push((column, summary, sketches));
703    }
704    let mut order = (0..payloads.len()).collect::<Vec<_>>();
705    order.sort_by_key(|&at| report[at].bytes());
706    let mut keep = vec![false; payloads.len()];
707    for at in order {
708        let cost = report[at].bytes() as u64;
709        if spent.saturating_add(cost) <= allowance {
710            spent += cost;
711            keep[at] = true;
712            report[at].built = true;
713        }
714    }
715    // The reader holds the file open and the attach opens it again to write, so it is dropped first
716    // for the reason `graph` drops it: the moment the file is written is a moment nothing else in
717    // this function is reading it.
718    drop(reader);
719    let mut attachments = Vec::with_capacity(payloads.len() * 2);
720    for ((column, summary, sketches), _) in payloads.iter().zip(&keep).filter(|&(_, &keep)| keep) {
721        let id = u64::try_from(*column).map_err(|_| invalid("column index overflow"))?;
722        attachments.push(Attachment {
723            kind: *section::SUMMARY,
724            id,
725            flags: 0,
726            // A summary is a header the whole way down. There is nothing behind it that a reader
727            // could decide not to read, which is the shape section 3.2's field is for and not a
728            // misuse of it: the answer to "how much do I read to know what this says" is all of it.
729            header_bytes: u32::try_from(summary.len())
730                .map_err(|_| invalid("a summary longer than a u32 can count"))?,
731            bytes: summary,
732        });
733        attachments.push(Attachment {
734            kind: *section::SKETCHES,
735            id,
736            flags: 0,
737            header_bytes: SKETCH_HEADER,
738            bytes: sketches,
739        });
740    }
741    crate::attach(path, table, &attachments)?;
742    Ok(report)
743}
744
745/// What the table's existing statistics sections cost, leaving out the ones this build is replacing.
746///
747/// Statistics sections only. The two percent of section 3.8 and the graph layer's ten percent are
748/// separate shares of the same column bytes, and separate means each counts only what it owns. A
749/// TPC-H SF10 file's key maps are 7.7 MB against a two percent allowance of 54 MB, so counting them
750/// here would hand a seventh of the statistics budget to sections that already have one of their
751/// own, and a table would lose summaries for a reason that has nothing to do with summaries.
752///
753/// Reading the extent tables is what this costs, which is one small read per section and not a read
754/// of a payload. A section whose extent table does not checksum is counted as nothing, because it
755/// is a section that is already not there.
756fn held_bytes(reader: &Reader, replacing: &[usize]) -> Result<u64> {
757    let mut total = 0;
758    for held in reader.table().sections() {
759        if !held.among(section::STATISTICS_KINDS) {
760            continue;
761        }
762        let replaced = replacing.iter().any(|&column| u64::try_from(column) == Ok(held.id));
763        if replaced || !held.usable(reader.table().generation()) {
764            continue;
765        }
766        let Ok(extents) = reader.extents(held) else { continue };
767        total += extents.iter().map(|extent| u64::from(extent.length)).sum::<u64>();
768    }
769    Ok(total)
770}
771
772/// The summary this table carries for a column, when it carries one this build can use.
773///
774/// `None` covers every reason there is not one and covering them all is the point. Section 3.1 says
775/// deleting every statistics section changes no answer, so there is no reason to distinguish *no
776/// summary was built* from *the summary is stale*, *the payload does not checksum*, or *the layout
777/// is one a later build invented*. The answer to all four is to plan the query the way it was
778/// planned before summaries existed.
779#[must_use]
780pub fn summary(reader: &Reader, column: usize) -> Option<Summary> {
781    let bytes = payload(reader, column, section::SUMMARY)?;
782    Summary::decode(&bytes).ok()
783}
784
785/// The sketches this table carries for a column, same.
786///
787/// One more reason for `None` here than above: a sketch built by a hash this build does not use is
788/// declined by [`Sketches::decode`] rather than merged into anything, which costs a rebuild where
789/// merging would cost an answer.
790#[must_use]
791pub fn sketches(reader: &Reader, column: usize) -> Option<Sketches> {
792    let bytes = payload(reader, column, section::SKETCHES)?;
793    Sketches::decode(&bytes).ok()
794}
795
796fn payload(reader: &Reader, column: usize, kind: &[u8; 8]) -> Option<Vec<u8>> {
797    let table = reader.table();
798    let id = u64::try_from(column).ok()?;
799    let held = table.sections().iter().find(|section| section.kind == *kind && section.id == id)?;
800    if !held.usable(table.generation()) {
801        return None;
802    }
803    reader.payload(held).ok()
804}
805
806/// Whether a type can be summarized at all, which is whether it has a hash rule.
807#[must_use]
808pub fn summarizable(ty: &LogicalType) -> bool {
809    countable(ty)
810}
811
812#[cfg(test)]
813mod tests {
814    use std::fs;
815    use std::path::PathBuf;
816    use std::time::{SystemTime, UNIX_EPOCH};
817
818    use rudb_common::Field;
819    use rudb_encoding::sketch::hash64;
820    use rudb_storage::count::hash_value;
821    use rudb_vector::{Chunk, Vector};
822
823    use super::*;
824    use crate::Writer;
825
826    fn path(label: &str) -> PathBuf {
827        let stamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("time advances").as_nanos();
828        std::env::temp_dir().join(format!("rudb-stats-{label}-{}-{stamp}.rdb", std::process::id()))
829    }
830
831    /// A one column table of these values, written a thousand rows to a part.
832    fn table_of(label: &str, values: &[Option<i64>]) -> PathBuf {
833        let path = path(label);
834        let mut writer =
835            Writer::create(&path, "t", vec![Field::new("v", LogicalType::BigInt)]).expect("new");
836        for part in values.chunks(1000) {
837            let held =
838                part.iter().map(|v| v.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
839            let chunk =
840                Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("values")])
841                    .expect("one column");
842            writer.append(&chunk).expect("a part");
843        }
844        writer.finish().expect("commit");
845        path
846    }
847
848    /// The same, with the part size named, for a test that needs more than one stripe.
849    ///
850    /// A stripe is up to `STRIPE_PARTS` parts, so small parts are how a test crosses a stripe
851    /// boundary without writing a hundred and thirty thousand rows to do it.
852    fn table_of_parts(label: &str, values: &[Option<i64>], per_part: usize) -> PathBuf {
853        let path = path(label);
854        let mut writer =
855            Writer::create(&path, "t", vec![Field::new("v", LogicalType::BigInt)]).expect("new");
856        for part in values.chunks(per_part) {
857            let held =
858                part.iter().map(|v| v.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
859            let chunk =
860                Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("values")])
861                    .expect("one column");
862            writer.append(&chunk).expect("a part");
863        }
864        writer.finish().expect("commit");
865        path
866    }
867
868    /// Every value of the one column, in rid order, which is what a scan of this table answers.
869    fn rows_of(reader: &Reader) -> Vec<Value> {
870        let mut out = Vec::new();
871        for part in 0..reader.parts() {
872            let chunk = reader.read(part, &[0]).expect("a part reads back");
873            for row in 0..chunk.len() {
874                out.push(chunk.value_at(0, row));
875            }
876        }
877        out
878    }
879
880    fn reopen(path: &PathBuf) -> Reader {
881        Catalog::open(path).expect("reopen").table("t").expect("the table")
882    }
883
884    #[test]
885    fn a_summary_built_over_a_file_says_what_the_column_holds() {
886        // End to end: the column goes to disk, comes back through the reader, and every field of
887        // the summary is the truth about it. Three thousand rows so the scan crosses parts, because
888        // a pass that read them in the wrong order would be right about one part and wrong about
889        // the order fields for the rest.
890        let values = (1..=3000_i64).map(Some).collect::<Vec<_>>();
891        let path = table_of("sorted", &values);
892        let built = build_stats(&path, "t", &[0]).expect("build");
893        assert_eq!(built.len(), 1);
894        assert!(built[0].built, "a one column table is nowhere near the budget");
895        assert_eq!(built[0].rows, 3000);
896        assert_eq!(built[0].distinct, 3000);
897        assert!(built[0].exact, "three thousand values is under the default k");
898        assert_eq!(built[0].order, Order::Ascending);
899
900        let reader = reopen(&path);
901        let summary = summary(&reader, 0).expect("the summary is in the file");
902        assert_eq!(summary.rows, 3000);
903        assert_eq!(summary.nulls, 0);
904        assert_eq!(summary.low, Some(Bound::Int(1)));
905        assert_eq!(summary.high, Some(Bound::Int(3000)));
906        assert!(summary.ends_exact);
907        assert!(summary.unique, "a sorted run of distinct values is a key candidate");
908        assert_eq!(summary.runs, 1, "one ascending run");
909        assert_eq!(summary.distinct_class, Class::Exact);
910        assert_eq!(summary.newest, reader.table().generation());
911
912        let sketches = sketches(&reader, 0).expect("the sketches are in the file");
913        assert!(sketches.merged.is_exact());
914        assert!(sketches.stripes.is_empty(), "the per stripe rule gives this column none");
915
916        fs::remove_file(&path).expect("clean up");
917    }
918
919    #[test]
920    fn nulls_are_counted_and_do_not_reach_the_ends_or_the_sketch() {
921        // The distinction that costs an answer if it is got wrong. A null is a row and is not a
922        // value, so it moves `rows` and `nulls` and moves nothing else.
923        let values: Vec<Option<i64>> =
924            (0..2000).map(|at| if at % 3 == 0 { None } else { Some(at) }).collect();
925        let path = table_of("nulls", &values);
926        build_stats(&path, "t", &[0]).expect("build");
927
928        let reader = reopen(&path);
929        let summary = summary(&reader, 0).expect("the summary");
930        let nulls = values.iter().filter(|v| v.is_none()).count() as u64;
931        assert_eq!(summary.rows, 2000);
932        assert_eq!(summary.nulls, nulls);
933        assert_eq!(summary.present(), 2000 - nulls);
934        assert_eq!(summary.distinct, 2000 - nulls, "a null is not a distinct value");
935        assert_eq!(summary.low, Some(Bound::Int(1)), "zero is null here");
936        assert!(summary.unique);
937
938        fs::remove_file(&path).expect("clean up");
939    }
940
941    #[test]
942    fn a_column_that_repeats_is_not_reported_unique_and_a_descending_one_is_seen() {
943        let values = (0..2000_i64).map(|at| Some(-(at / 2))).collect::<Vec<_>>();
944        let path = table_of("repeats", &values);
945        build_stats(&path, "t", &[0]).expect("build");
946
947        let reader = reopen(&path);
948        let summary = summary(&reader, 0).expect("the summary");
949        assert_eq!(summary.distinct, 1000);
950        assert!(!summary.unique, "every value appears twice");
951        assert_eq!(summary.order, Order::Descending);
952        assert_eq!(summary.runs, 1000, "a descending column is a run per distinct value");
953
954        fs::remove_file(&path).expect("clean up");
955    }
956
957    #[test]
958    fn a_column_past_the_default_k_is_estimated_and_says_so() {
959        // The rule the module doc names, at the point where it bites. Past k the sketch threw values
960        // away, so the count is an estimate, and the class has to say so or a COUNT(DISTINCT) is
961        // answered out of metadata with a number that is close and wrong.
962        let values = (0..20_000_i64).map(Some).collect::<Vec<_>>();
963        let path = table_of("estimated", &values);
964        let built = build_stats(&path, "t", &[0]).expect("build");
965        assert!(!built[0].exact, "twenty thousand values is past the default k");
966
967        let reader = reopen(&path);
968        let summary = summary(&reader, 0).expect("the summary");
969        assert_eq!(summary.distinct_class, Class::Estimated);
970        assert!(!summary.unique, "uniqueness is never claimed off an estimate");
971        assert!(summary.distinct > 17_000 && summary.distinct <= 20_000, "{}", summary.distinct);
972        assert!(summary.distinct <= summary.present(), "more distinct values than rows");
973
974        fs::remove_file(&path).expect("clean up");
975    }
976
977    #[test]
978    fn a_shuffled_column_is_neither_ordered_nor_one_run() {
979        let values = (0..2000_i64).map(|at| Some((at * 7919) % 2000)).collect::<Vec<_>>();
980        let path = table_of("shuffled", &values);
981        build_stats(&path, "t", &[0]).expect("build");
982
983        let reader = reopen(&path);
984        let summary = summary(&reader, 0).expect("the summary");
985        assert_eq!(summary.order, Order::Neither);
986        assert!(summary.runs > 100, "a shuffle is many runs, not one: {}", summary.runs);
987        assert_eq!(summary.low, Some(Bound::Int(0)));
988        assert_eq!(summary.high, Some(Bound::Int(1999)));
989
990        fs::remove_file(&path).expect("clean up");
991    }
992
993    #[test]
994    fn a_column_something_declared_a_key_over_is_sketched_per_stripe_and_a_plain_one_is_not() {
995        // Section 3.8's rule, both halves of it. Nothing has declared anything over this column, so
996        // the first build gives it the table level summary and no per stripe sketches, which is the
997        // state most columns are in and is what keeps SF100 inside two percent. A key map is then
998        // built over it, which is something declaring it a key, and the next build promotes it.
999        let values = (1..=19_200_i64).map(Some).collect::<Vec<_>>();
1000        let path = table_of_parts("promoted", &values, 100);
1001
1002        let plain = build_stats(&path, "t", &[0]).expect("build");
1003        assert_eq!(plain[0].stripes, 0, "nothing has declared anything over this column yet");
1004
1005        crate::graph::build_key_maps(&path, "t", &[0]).expect("a key map declares it a key");
1006        let promoted = build_stats(&path, "t", &[0]).expect("rebuild");
1007        assert!(promoted[0].stripes > 1, "{} stripes, wanted more than one", promoted[0].stripes);
1008        assert!(promoted[0].built, "and they fit");
1009        // The equality rather than a tolerance. The merged sketch of a promoted column is the union
1010        // of its stripe sketches at the column's own k, and a union of bottom-k sketches at one k
1011        // is the bottom-k of everything they saw, so it holds the same hashes as the single sketch
1012        // the plain build made. Promotion changes where the counting is reset and nothing else.
1013        assert_eq!(promoted[0].distinct, plain[0].distinct, "the merged count did not move");
1014
1015        let reader = reopen(&path);
1016        let sketches = sketches(&reader, 0).expect("the sketches came back");
1017        assert_eq!(sketches.stripes.len(), promoted[0].stripes);
1018        assert!(
1019            sketches.stripes.iter().all(|stripe| stripe.k() == STRIPE_K),
1020            "a stripe sketch is written down at the smaller k"
1021        );
1022        let floor = sketches.floor(0, sketches.stripes.len()).expect("a floor over every stripe");
1023        let actual = 19_200.0;
1024        assert!(
1025            (floor - actual).abs() / actual < 0.25,
1026            "{floor:.0} over every stripe against {actual:.0}"
1027        );
1028
1029        drop(reader);
1030        fs::remove_file(&path).expect("clean up");
1031    }
1032
1033    #[test]
1034    fn a_file_from_before_the_section_table_opens_and_every_statistic_is_unknown() {
1035        // Exit criterion 3 of #762, the statistics half of it. A build that knows about summaries
1036        // opens a file written by a build that did not, with no rewrite and no repair, states
1037        // nothing about that file's columns, and reads back exactly what the same rows read back
1038        // out of a file this build wrote.
1039        //
1040        // `None` is what `Unknown` is at this layer, and the two readers answer it for every reason
1041        // there is rather than distinguishing them, which is section 3.1: there is nothing a caller
1042        // could do differently on hearing *the file predates statistics* rather than *the section
1043        // does not checksum*, because both are answered by planning the query the way it was
1044        // planned before statistics existed.
1045        //
1046        // The older file is this build's file with the version stamped back and nothing attached,
1047        // for the reason the format 22 test in `lib.rs` gives: the two formats differ only in a
1048        // trailing directory block, so a file that never had one is a format 22 file already and
1049        // the stamp is the only thing left to change. No fixture to go stale and no second encoder
1050        // to drift.
1051        let values = (1..=3000_i64).map(Some).collect::<Vec<_>>();
1052        let current = table_of("with_sections", &values);
1053        let older = table_of("before_sections", &values);
1054        build_stats(&current, "t", &[0]).expect("this build states what its columns hold");
1055
1056        let file = fs::OpenOptions::new().write(true).open(&older).expect("reopen to patch");
1057        crate::write_at(&file, 8, &22_u32.to_le_bytes()).expect("stamp the older format");
1058        drop(file);
1059
1060        let new = reopen(&current);
1061        assert!(summary(&new, 0).is_some(), "the file this build wrote says what it holds");
1062
1063        let old = reopen(&older);
1064        assert!(old.table().sections().is_empty(), "an older file names no sections");
1065        assert!(summary(&old, 0).is_none(), "and so says nothing about its columns");
1066        assert!(sketches(&old, 0).is_none());
1067        assert!(read_columns(&old).is_empty(), "nor promotes any of them");
1068        assert_eq!(rows_of(&old), rows_of(&new), "and answers what the newer file answers");
1069
1070        drop(new);
1071        drop(old);
1072        fs::remove_file(&current).expect("clean up");
1073        fs::remove_file(&older).expect("clean up");
1074    }
1075
1076    #[test]
1077    fn the_stripe_ends_say_whether_a_scan_can_skip_and_a_shuffle_says_it_cannot() {
1078        // The per stripe ends, which is the one thing the pass tracks that nothing else checks and
1079        // which a scan reads to skip a whole stripe. A sorted column's stripes do not overlap and a
1080        // shuffled column's every stripe spans the column, so the same rows in a different order
1081        // give the opposite answer. Three stripes, so that the ends are opened and closed more than
1082        // once and a pass that never reset them would be caught.
1083        let sorted = (1..=19_200_i64).map(Some).collect::<Vec<_>>();
1084        let ordered = table_of_parts("stripes_sorted", &sorted, 100);
1085        build_stats(&ordered, "t", &[0]).expect("build");
1086        let reader = reopen(&ordered);
1087        let ordered_summary = summary(&reader, 0).expect("the summary");
1088        assert!(!ordered_summary.overlapping, "a sorted column's stripes are disjoint");
1089        assert_eq!(ordered_summary.low, Some(Bound::Int(1)));
1090        assert_eq!(ordered_summary.high, Some(Bound::Int(19_200)));
1091        drop(reader);
1092
1093        // A fixed stride rather than a random shuffle, so a failure is the same failure twice. The
1094        // stride and the row count share no factor, so this visits every value exactly once and
1095        // every stripe ends up holding values from very nearly the whole range.
1096        let shuffled = (0..19_200_i64).map(|at| Some(1 + at * 7919 % 19_200)).collect::<Vec<_>>();
1097        let mixed = table_of_parts("stripes_shuffled", &shuffled, 100);
1098        build_stats(&mixed, "t", &[0]).expect("build");
1099        let reader = reopen(&mixed);
1100        let mixed_summary = summary(&reader, 0).expect("the summary");
1101        assert!(mixed_summary.overlapping, "a shuffled column's stripes all span it");
1102        assert_eq!(mixed_summary.low, Some(Bound::Int(1)), "the same values in a different order");
1103        assert_eq!(mixed_summary.high, Some(Bound::Int(19_200)));
1104        drop(reader);
1105
1106        fs::remove_file(&ordered).expect("clean up");
1107        fs::remove_file(&mixed).expect("clean up");
1108    }
1109
1110    #[test]
1111    fn the_graph_sections_do_not_count_against_the_statistics_budget() {
1112        // The direction of box 4 that costs more, because the two percent is the smaller share. A
1113        // TPC-H SF10 file's key maps are 7.7 MB against an allowance of 54 MB, so a statistics
1114        // build that counted them would start a seventh of the way through a budget it was given
1115        // all of, and columns at the far end of a wide table would go unsummarized for a reason
1116        // that has nothing to do with summaries.
1117        let values = (1..=3000_i64).map(Some).collect::<Vec<_>>();
1118        let path = table_of("apart", &values);
1119        crate::graph::build_key_maps(&path, "t", &[0]).expect("a key map first");
1120
1121        let reader = reopen(&path);
1122        let graph = reader
1123            .table()
1124            .sections()
1125            .iter()
1126            .filter(|held| held.among(section::GRAPH_KINDS))
1127            .count();
1128        assert_eq!(graph, 1, "the key map is in the file");
1129        assert_eq!(held_bytes(&reader, &[0]).expect("held"), 0, "and it is not the statistics'");
1130
1131        drop(reader);
1132        fs::remove_file(&path).expect("clean up");
1133    }
1134
1135    #[test]
1136    fn deleting_the_sections_changes_nothing_but_whether_they_are_there() {
1137        // Section 3.1, as close to directly as a test can put it. The same file, read once with the
1138        // sections and once with the generation moved past them, and the reader opens and scans the
1139        // same either way.
1140        let values = (1..=1500_i64).map(Some).collect::<Vec<_>>();
1141        let path = table_of("invariant", &values);
1142        build_stats(&path, "t", &[0]).expect("build");
1143
1144        let reader = reopen(&path);
1145        assert!(summary(&reader, 0).is_some());
1146        let generation = reader.table().generation();
1147        let held: Vec<_> = reader
1148            .table()
1149            .sections()
1150            .iter()
1151            .filter(|s| s.kind == *section::SUMMARY || s.kind == *section::SKETCHES)
1152            .copied()
1153            .collect();
1154        assert_eq!(held.len(), 2, "a summary and a sketch section");
1155        for section in &held {
1156            assert!(section.usable(generation));
1157            assert!(!section.usable(generation + 1), "a rewrite invalidates rather than corrupts");
1158        }
1159        let rows: usize =
1160            (0..reader.parts()).map(|part| reader.read(part, &[0]).expect("a part").len()).sum();
1161        assert_eq!(rows, 1500, "the scan is the scan whether the sections are read or not");
1162
1163        fs::remove_file(&path).expect("clean up");
1164    }
1165
1166    #[test]
1167    fn a_string_column_is_read_through_the_typed_path_and_measured_by_its_bytes() {
1168        // The other fast path. A varchar has no fixed width, so the byte total and the widest value
1169        // are measured per value, and the ends are the string ends rather than the hash ends.
1170        let path = path("strings");
1171        let mut writer =
1172            Writer::create(&path, "t", vec![Field::new("v", LogicalType::Varchar)]).expect("new");
1173        let words = ["alpha", "bravo", "charlie", "delta", "alpha"];
1174        let held = words.iter().map(|w| Value::Varchar((*w).into())).collect::<Vec<_>>();
1175        let chunk =
1176            Chunk::new(vec![Vector::from_values(LogicalType::Varchar, &held).expect("words")])
1177                .expect("one column");
1178        writer.append(&chunk).expect("a part");
1179        writer.finish().expect("commit");
1180        build_stats(&path, "t", &[0]).expect("build");
1181
1182        let reader = reopen(&path);
1183        let summary = summary(&reader, 0).expect("the summary");
1184        assert_eq!(summary.rows, 5);
1185        assert_eq!(summary.distinct, 4, "alpha twice");
1186        assert!(!summary.unique);
1187        assert_eq!(summary.bytes, words.iter().map(|w| w.len() as u64).sum::<u64>());
1188        assert_eq!(summary.widest, 7, "charlie");
1189        assert_eq!(summary.low, Some(Bound::Bytes(b"alpha".to_vec())));
1190        assert_eq!(summary.high, Some(Bound::Bytes(b"delta".to_vec())));
1191
1192        drop(reader);
1193        fs::remove_file(&path).expect("clean up");
1194    }
1195
1196    #[test]
1197    fn a_type_with_no_hash_rule_is_refused_by_name_rather_than_summarized_as_empty() {
1198        let path = table_of("refused", &[Some(1)]);
1199        let reader = reopen(&path);
1200        assert!(summarizable(&LogicalType::BigInt));
1201        assert!(!summarizable(&LogicalType::Interval));
1202        assert!(build_summary(&reader, 1).is_err(), "a column past the end");
1203        drop(reader);
1204        fs::remove_file(&path).expect("clean up");
1205    }
1206
1207    #[test]
1208    fn the_stored_sketch_depends_on_the_value_rule_and_not_only_on_the_hash() {
1209        // HASH_IDENTITY pins `hash64`, which is half of what a stored sketch depends on. The other
1210        // half is the rule that turns a value into the bytes `hash64` sees, and that rule lives in
1211        // `rudb_storage::count`. Changing it without bumping HASH_IDENTITY would leave every stored
1212        // sketch readable, accepted, and built over a different universe than the one a new sketch
1213        // is built over, which is exactly the merge the identity exists to prevent.
1214        //
1215        // So the rule is pinned here. If this fails because `hash_value` changed on purpose, the fix
1216        // is to bump HASH_IDENTITY and then update these numbers, in that order.
1217        assert_eq!(hash_value(&Value::BigInt(1)), Some(hash64(&1_u128.to_le_bytes())));
1218        assert_eq!(hash_value(&Value::Integer(1)), hash_value(&Value::BigInt(1)));
1219        assert_eq!(hash_value(&Value::Varchar("a".into())), Some(hash64(b"a")));
1220        assert_eq!(hash_value(&Value::Null), None);
1221    }
1222}