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//! # What is deliberately not here yet
31//!
32//! Per stripe sketches. Section 3.8's rule is that per stripe structures are written only for the
33//! columns that get read, and which columns those are comes out of document 06's observation log at
34//! the next checkpoint. So every sketch this builds is a merged one and [`Sketches::stripes`] is
35//! empty, which is not a degraded state but the state the rule says most columns are in. The
36//! promotion path is the next piece of work and it does not change any byte written here.
37
38use std::cmp::Ordering;
39use std::path::Path;
40use std::time::{Duration, Instant};
41
42use rudb_common::bounds::{self, Bound};
43use rudb_common::stat::Class;
44use rudb_common::{LogicalType, Result, Value};
45use rudb_encoding::sketch::Sketch;
46use rudb_stats::{Order, Sketches, Summary, sketches::HEADER_BYTES as SKETCH_HEADER};
47use rudb_storage::count::{Counts, countable};
48use rudb_vector::Vector;
49
50use crate::section::{self, Attachment};
51use crate::{Catalog, Reader, invalid};
52
53/// The share of a table's stored column bytes its statistics sections are allowed to cost together.
54///
55/// Two percent, per section 3.8, and kept apart from the graph layer's ten percent rather than
56/// pooled with it. Two budgets that share a pot are two budgets where the one that runs first wins,
57/// and a table whose key maps happened to be built before its summaries would then have no
58/// summaries for a reason that has nothing to do with summaries. They are counted separately for the
59/// same reason they are two documents.
60pub const BUDGET_SHARE: u64 = 2;
61
62/// The size below which a table's statistics sections always fit, whatever the share works out to.
63///
64/// The same floor and the same argument as `graph::BUDGET_FLOOR`. A summary is a few hundred bytes
65/// on a table of any size and two percent of a small, well compressed column is less than that, so
66/// the pure rule would throw away the cheapest structure in the system for being expensive.
67pub const BUDGET_FLOOR: u64 = 64 * 1024;
68
69/// What one column's statistics cost and what they say.
70#[derive(Debug, Clone)]
71pub struct Built {
72    /// Which column was summarized.
73    pub column: usize,
74    /// Rows in the column, nulls included.
75    pub rows: u64,
76    /// Distinct non-null values, as the summary reports them.
77    pub distinct: u64,
78    /// Whether that distinct count is exact rather than a sketch estimate.
79    pub exact: bool,
80    /// Which way the values run.
81    pub order: Order,
82    /// What the summary section takes in the file.
83    pub summary_bytes: usize,
84    /// What the sketches section takes in the file.
85    pub sketch_bytes: usize,
86    /// What the column takes in the file, which is what the budget is a share of.
87    pub column_bytes: u64,
88    /// Whether the sections were kept. False means they were built, measured, and found to cost more
89    /// than section 3.8 allows, so the file does not have them and every query plans as though
90    /// statistics had never been implemented.
91    pub built: bool,
92    /// How long the build took, the reading of the column included.
93    pub build: Duration,
94}
95
96impl Built {
97    /// Both sections together, which is what the budget spends.
98    #[must_use]
99    pub fn bytes(&self) -> usize {
100        self.summary_bytes + self.sketch_bytes
101    }
102}
103
104/// A column's summary and its sketches, which are built together because they are one pass.
105#[derive(Debug, Clone)]
106pub struct Stats {
107    /// What the column says about itself.
108    pub summary: Summary,
109    /// The sketch the distinct count came out of.
110    pub sketches: Sketches,
111}
112
113/// Builds the summary and the sketches for one column of a committed table.
114///
115/// # Errors
116///
117/// If the column cannot be read, is past the end of the table, or is of a type with no hash rule.
118/// The last one is refused by name rather than approximated: the types without a rule are the
119/// interval and the nested ones, a summary of one would carry a distinct count of zero that nothing
120/// could tell from a column of nulls, and none of TPC-H or ClickBench has one.
121pub fn build_summary(reader: &Reader, column: usize) -> Result<Stats> {
122    let fields = reader.table().fields();
123    let Some(field) = fields.get(column) else {
124        return Err(invalid(&format!(
125            "column {column} is past the {} of table {}",
126            fields.len(),
127            reader.table().name()
128        )));
129    };
130    if !countable(&field.ty) {
131        return Err(invalid(&format!(
132            "a summary of {} needs a hash rule, and {} has none",
133            field.name, field.ty
134        )));
135    }
136
137    let mut counts = Counts::new(1);
138    let mut pass = Pass::new(&field.ty, reader.table().generation());
139    for stripe in reader.stripe_parts() {
140        pass.open_stripe();
141        for part in stripe {
142            let chunk = reader.read(part, &[column])?;
143            counts.add(&chunk);
144            pass.scan(chunk.column(0)?);
145        }
146        pass.close_stripe();
147    }
148    let Some(sketch) = counts.sketch(0) else {
149        // A blind column: a form `rudb_storage::count` has no arm for turned up, so its sketch is
150        // missing rows and says nothing about which. A distinct count that is too low is the one
151        // error an estimator has no defence against, so the column gets no summary at all rather
152        // than a summary with a number in it nothing can check.
153        return Err(invalid(&format!(
154            "column {} of {} holds a form with no hash rule, so it has no sketch",
155            field.name,
156            reader.table().name()
157        )));
158    };
159    Ok(pass.finish(sketch))
160}
161
162/// One scan of one column, in `rid` order, for everything the sketch does not answer.
163///
164/// In `rid` order because the order fields depend on it. A pass that read the parts in any other
165/// order would report a column as unordered that is sorted, which costs a plan and not an answer,
166/// and would report the run count of a shuffle, which is worse because it is a number rather than a
167/// flag and looks like it was measured.
168///
169/// The distinct count is not here. That is `rudb_storage::count::Counts`, which walks a vector by
170/// its form rather than a row at a time and which a column of a million runs costs one hash. Doing
171/// it twice would double the expensive half of the build and the budget is ten percent of the write.
172struct Pass {
173    rows: u64,
174    nulls: u64,
175    low: Option<Bound>,
176    high: Option<Bound>,
177    /// False once a non-null value turned up that has no ordered bound, which makes both ends
178    /// unusable rather than merely absent.
179    bounded: bool,
180    ascending: bool,
181    descending: bool,
182    runs: u64,
183    previous: Option<Bound>,
184    bytes: u64,
185    widest: u64,
186    generation: u64,
187    stripe: Option<(Bound, Bound)>,
188    stripes: Vec<(Bound, Bound)>,
189    /// What one value of this column takes, when every value takes the same.
190    ///
191    /// Read off the type once rather than off each value, because for every fixed width column it is
192    /// a constant and asking a value for it is a branch a hundred million times to hear the same
193    /// number. `None` is a variable width type and those are measured per value.
194    fixed: Option<u64>,
195    /// The scale of a decimal column, so that an integer read out of a vector becomes the bound the
196    /// column's other writers would have written for the same value.
197    scale: Option<u8>,
198}
199
200impl Pass {
201    fn new(ty: &LogicalType, generation: u64) -> Self {
202        Self {
203            rows: 0,
204            nulls: 0,
205            low: None,
206            high: None,
207            bounded: true,
208            ascending: true,
209            descending: true,
210            runs: 0,
211            previous: None,
212            bytes: 0,
213            widest: 0,
214            generation,
215            stripe: None,
216            stripes: Vec::new(),
217            fixed: fixed_width(ty),
218            scale: bounds::scale_of(ty),
219        }
220    }
221
222    /// One vector of the column, typed rather than a value at a time where the type allows it.
223    ///
224    /// `signed_at` and `bytes_at` between them cover every integer, date, timestamp, decimal, string
225    /// and blob column, which is all sixteen of TPC-H `lineitem` and all but a handful of
226    /// ClickBench. Both are a load against a slice. The fall back below builds a `Value`, and it is
227    /// there for the float columns and for the forms the two fast paths cannot read, not as the
228    /// ordinary path.
229    fn scan(&mut self, vector: &Vector) {
230        // row at a time: the run count and the order flags are a sequential dependency. Whether this
231        // value is below the one before it is a question about a pair of adjacent rows, so there is
232        // no shape of this loop that answers it a vector at a time, and the two typed accessors
233        // below are loads against a slice rather than value construction. What the checker is
234        // looking for is the third arm, which does build a `Value`, and that one runs for a float
235        // column and for a form the first two cannot read and for nothing else.
236        for row in 0..vector.len() {
237            self.rows += 1;
238            if vector.is_null_at(row) {
239                self.nulls += 1;
240                continue;
241            }
242            if let Some(signed) = vector.signed_at(row) {
243                let bound = match self.scale {
244                    Some(scale) => Bound::Scaled { unscaled: signed, scale },
245                    None => Bound::Int(signed),
246                };
247                self.value(bound, self.fixed.unwrap_or(8));
248                continue;
249            }
250            if let Some(bytes) = vector.bytes_at(row) {
251                let width = bytes.len() as u64;
252                self.value(Bound::Bytes(bytes.to_vec()), width);
253                continue;
254            }
255            // row at a time: a float and a form neither typed accessor above can read have no slice
256            // to walk, so the value is built for this row and for no other.
257            let value = vector.value_at(row);
258            let width = self.fixed.unwrap_or_else(|| width(&value));
259            match Bound::of_value(&value) {
260                Some(bound) => self.value(bound, width),
261                None => {
262                    // A non-null value with no ordered bound. Both ends go rather than the value
263                    // being skipped, because an end computed from only the values that had bounds is
264                    // an end that answers a MIN with a value the column does not hold.
265                    self.bytes = self.bytes.saturating_add(width);
266                    self.widest = self.widest.max(width);
267                    self.bounded = false;
268                    self.ascending = false;
269                    self.descending = false;
270                }
271            }
272        }
273    }
274
275    /// One non-null value, as its bound and its width.
276    fn value(&mut self, bound: Bound, width: u64) {
277        self.bytes = self.bytes.saturating_add(width);
278        self.widest = self.widest.max(width);
279        match &self.previous {
280            None => self.runs = 1,
281            Some(previous) => match previous.order(&bound) {
282                Some(Ordering::Less) => self.descending = false,
283                Some(Ordering::Greater) => {
284                    self.ascending = false;
285                    self.runs += 1;
286                }
287                Some(Ordering::Equal) => {}
288                // Two bounds of different domains in one column. It should be unreachable, since a
289                // column has one type, and it costs an order claim rather than being assumed away.
290                None => {
291                    self.ascending = false;
292                    self.descending = false;
293                }
294            },
295        }
296        self.low = Some(match self.low.take() {
297            Some(held) => held.smaller(bound.clone()),
298            None => bound.clone(),
299        });
300        self.high = Some(match self.high.take() {
301            Some(held) => held.larger(bound.clone()),
302            None => bound.clone(),
303        });
304        self.stripe = Some(match self.stripe.take() {
305            Some((low, high)) => (low.smaller(bound.clone()), high.larger(bound.clone())),
306            None => (bound.clone(), bound.clone()),
307        });
308        self.previous = Some(bound);
309    }
310
311    fn open_stripe(&mut self) {
312        self.stripe = None;
313    }
314
315    fn close_stripe(&mut self) {
316        if let Some(range) = self.stripe.take() {
317            self.stripes.push(range);
318        }
319    }
320
321    fn finish(self, sketch: Sketch) -> Stats {
322        let present = self.rows - self.nulls;
323        // The one rule the module doc names. An exact distinct count is one the sketch never had to
324        // throw a value away to keep, and everything downstream of the count follows from this
325        // answer rather than from what the writer hoped.
326        let exact = sketch.is_exact();
327        let distinct = if exact {
328            sketch.len() as u64
329        } else {
330            // Rounded rather than truncated, and clamped under the rows it cannot exceed. An
331            // estimate above the row count is arithmetically possible and is always wrong, and a
332            // planner that sees one concludes a column has more distinct values than rows.
333            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
334            let estimate = sketch.distinct().round().max(0.0) as u64;
335            estimate.min(present)
336        };
337        let summary = Summary {
338            rows: self.rows,
339            nulls: self.nulls,
340            low: if self.bounded { self.low } else { None },
341            high: if self.bounded { self.high } else { None },
342            // Every end here came from a value the column holds, because this pass read them all.
343            // That is the whole difference between a summary and a zone map, which is allowed to be
344            // wider than its column and so can only skip and never answer.
345            ends_exact: self.bounded,
346            distinct,
347            // Exact or estimated, and never certified. A KMV sketch's relative error is about one
348            // over the square root of k, which is a standard error and not a bound, and Certified
349            // in this codebase means a bound that holds. Calling a one and a half percent standard
350            // error a guarantee is how an estimate gets treated as an answer.
351            distinct_class: if exact { Class::Exact } else { Class::Estimated },
352            // Only from an exact count. A sketch that overflowed cannot tell a column of a million
353            // unique values from one where two of them repeat, and uniqueness is the claim a key
354            // map is built on.
355            unique: exact && distinct == present,
356            order: if present == 0 {
357                Order::Neither
358            } else if self.ascending {
359                Order::Ascending
360            } else if self.descending {
361                Order::Descending
362            } else {
363                Order::Neither
364            },
365            runs: self.runs,
366            overlapping: overlapping(&self.stripes),
367            bytes: self.bytes,
368            widest: self.widest,
369            newest: self.generation,
370        };
371        Stats { summary, sketches: Sketches::merged(sketch) }
372    }
373}
374
375/// Whether any two of these stripe ranges overlap.
376///
377/// Sorted by low end and then walked, so this is one sort rather than the square. A pair this cannot
378/// order counts as overlapping, which is the answer that costs a skipped stripe rather than a wrong
379/// one.
380fn overlapping(stripes: &[(Bound, Bound)]) -> bool {
381    let mut order = (0..stripes.len()).collect::<Vec<_>>();
382    order
383        .sort_by(|&one, &other| stripes[one].0.order(&stripes[other].0).unwrap_or(Ordering::Equal));
384    order.windows(2).any(|pair| {
385        let before = &stripes[pair[0]].1;
386        let after = &stripes[pair[1]].0;
387        before.order(after) != Some(Ordering::Less)
388    })
389}
390
391/// What every value of this type takes, when they all take the same.
392///
393/// `None` for the variable width types, which is the two string ones and nothing else. Read off the
394/// type once by `Pass::new` rather than off each value.
395fn fixed_width(ty: &LogicalType) -> Option<u64> {
396    Some(match ty {
397        LogicalType::Boolean | LogicalType::TinyInt | LogicalType::UTinyInt => 1,
398        LogicalType::SmallInt | LogicalType::USmallInt => 2,
399        LogicalType::Integer | LogicalType::UInteger | LogicalType::Float | LogicalType::Date => 4,
400        LogicalType::HugeInt | LogicalType::UHugeInt | LogicalType::Decimal { .. } => 16,
401        LogicalType::Varchar | LogicalType::Blob => return None,
402        // The eight byte types: the two big integers, the double, and the four time ones. Anything
403        // else that reaches here is refused a summary by `countable` long before this.
404        _ => 8,
405    })
406}
407
408/// What one value takes, for the byte total and the widest value.
409///
410/// The logical width and not the stored one. The stored width is what the column's encoding chose
411/// and is already in the layout; this is what the value costs a plan that has to materialize it,
412/// which is the number a hash table sizing decision wants.
413fn width(value: &Value) -> u64 {
414    match value {
415        Value::Null => 0,
416        Value::Boolean(_) | Value::TinyInt(_) | Value::UTinyInt(_) => 1,
417        Value::SmallInt(_) | Value::USmallInt(_) => 2,
418        Value::Integer(_) | Value::UInteger(_) | Value::Float(_) | Value::Date(_) => 4,
419        Value::HugeInt(_) | Value::UHugeInt(_) | Value::Decimal { .. } => 16,
420        Value::Varchar(text) => text.len() as u64,
421        Value::Blob(bytes) => bytes.len() as u64,
422        // The eight byte types and anything else, which is every remaining scalar. A nested value
423        // reaching here would be counted at eight and is refused a summary long before this by
424        // `countable`.
425        _ => 8,
426    }
427}
428
429/// Builds the statistics for each of these columns and attaches them all in one commit.
430///
431/// One commit and not one each, for the reason `graph::build_key_maps` gives: a checkpoint that
432/// published one generation per column would be one chance per column of being interrupted halfway.
433///
434/// # Errors
435///
436/// If the file cannot be opened, a column cannot be summarized, or the attach fails.
437pub fn build_stats(path: &Path, table: &str, columns: &[usize]) -> Result<Vec<Built>> {
438    build_stats_within(path, table, columns, BUDGET_SHARE)
439}
440
441/// The same, against a budget of `share` percent of the table's stored column bytes.
442///
443/// The budget is over the table rather than over a column, and when it binds the cheapest columns
444/// are admitted first. That is the same degenerate case section 3.7's expected value ordering has
445/// for a key map with no relationship over it: nothing has said which column a plan will ask about,
446/// so no summary is worth more than another and the ordering falls back to the denominator. Cheapest
447/// first is also the order that fits the most summaries in the room there is.
448///
449/// A column is all or nothing. Its summary and its sketches are admitted together or neither is,
450/// because a summary whose distinct count came from a sketch that was then dropped is a number with
451/// nothing behind it to check it against.
452///
453/// # Errors
454///
455/// If the file cannot be opened, a column cannot be summarized, or the attach fails.
456pub fn build_stats_within(
457    path: &Path,
458    table: &str,
459    columns: &[usize],
460    share: u64,
461) -> Result<Vec<Built>> {
462    let reader = Catalog::open(path)?.table(table)?;
463    let column_bytes = reader.layout().columns_total();
464    let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
465    let mut spent = held_bytes(&reader, columns)?;
466    let mut report = Vec::with_capacity(columns.len());
467    let mut payloads = Vec::with_capacity(columns.len());
468    for &column in columns {
469        let start = Instant::now();
470        let stats = build_summary(&reader, column)?;
471        let mut summary = Vec::new();
472        stats.summary.encode(&mut summary)?;
473        let mut sketches = Vec::new();
474        stats.sketches.encode(&mut sketches)?;
475        report.push(Built {
476            column,
477            rows: stats.summary.rows,
478            distinct: stats.summary.distinct,
479            exact: stats.summary.distinct_class == Class::Exact,
480            order: stats.summary.order,
481            summary_bytes: summary.len(),
482            sketch_bytes: sketches.len(),
483            column_bytes,
484            built: false,
485            build: start.elapsed(),
486        });
487        payloads.push((column, summary, sketches));
488    }
489    let mut order = (0..payloads.len()).collect::<Vec<_>>();
490    order.sort_by_key(|&at| report[at].bytes());
491    let mut keep = vec![false; payloads.len()];
492    for at in order {
493        let cost = report[at].bytes() as u64;
494        if spent.saturating_add(cost) <= allowance {
495            spent += cost;
496            keep[at] = true;
497            report[at].built = true;
498        }
499    }
500    // The reader holds the file open and the attach opens it again to write, so it is dropped first
501    // for the reason `graph` drops it: the moment the file is written is a moment nothing else in
502    // this function is reading it.
503    drop(reader);
504    let mut attachments = Vec::with_capacity(payloads.len() * 2);
505    for ((column, summary, sketches), _) in payloads.iter().zip(&keep).filter(|&(_, &keep)| keep) {
506        let id = u64::try_from(*column).map_err(|_| invalid("column index overflow"))?;
507        attachments.push(Attachment {
508            kind: *section::SUMMARY,
509            id,
510            flags: 0,
511            // A summary is a header the whole way down. There is nothing behind it that a reader
512            // could decide not to read, which is the shape section 3.2's field is for and not a
513            // misuse of it: the answer to "how much do I read to know what this says" is all of it.
514            header_bytes: u32::try_from(summary.len())
515                .map_err(|_| invalid("a summary longer than a u32 can count"))?,
516            bytes: summary,
517        });
518        attachments.push(Attachment {
519            kind: *section::SKETCHES,
520            id,
521            flags: 0,
522            header_bytes: SKETCH_HEADER,
523            bytes: sketches,
524        });
525    }
526    crate::attach(path, table, &attachments)?;
527    Ok(report)
528}
529
530/// What the table's existing sections cost, leaving out the statistics this build is replacing.
531///
532/// Every section counts, the graph ones included, because the file is one file. The two budgets are
533/// separate shares of the same column bytes and each is checked against what is already spent, which
534/// is how one layer overrunning is visible to the other rather than silently doubling the total.
535fn held_bytes(reader: &Reader, replacing: &[usize]) -> Result<u64> {
536    let mut total = 0;
537    for held in reader.table().sections() {
538        let mine = held.kind == *section::SUMMARY || held.kind == *section::SKETCHES;
539        let replaced = mine && replacing.iter().any(|&column| u64::try_from(column) == Ok(held.id));
540        if replaced || !held.usable(reader.table().generation()) {
541            continue;
542        }
543        let Ok(extents) = reader.extents(held) else { continue };
544        total += extents.iter().map(|extent| u64::from(extent.length)).sum::<u64>();
545    }
546    Ok(total)
547}
548
549/// The summary this table carries for a column, when it carries one this build can use.
550///
551/// `None` covers every reason there is not one and covering them all is the point. Section 3.1 says
552/// deleting every statistics section changes no answer, so there is no reason to distinguish *no
553/// summary was built* from *the summary is stale*, *the payload does not checksum*, or *the layout
554/// is one a later build invented*. The answer to all four is to plan the query the way it was
555/// planned before summaries existed.
556#[must_use]
557pub fn summary(reader: &Reader, column: usize) -> Option<Summary> {
558    let bytes = payload(reader, column, section::SUMMARY)?;
559    Summary::decode(&bytes).ok()
560}
561
562/// The sketches this table carries for a column, same.
563///
564/// One more reason for `None` here than above: a sketch built by a hash this build does not use is
565/// declined by [`Sketches::decode`] rather than merged into anything, which costs a rebuild where
566/// merging would cost an answer.
567#[must_use]
568pub fn sketches(reader: &Reader, column: usize) -> Option<Sketches> {
569    let bytes = payload(reader, column, section::SKETCHES)?;
570    Sketches::decode(&bytes).ok()
571}
572
573fn payload(reader: &Reader, column: usize, kind: &[u8; 8]) -> Option<Vec<u8>> {
574    let table = reader.table();
575    let id = u64::try_from(column).ok()?;
576    let held = table.sections().iter().find(|section| section.kind == *kind && section.id == id)?;
577    if !held.usable(table.generation()) {
578        return None;
579    }
580    reader.payload(held).ok()
581}
582
583/// Whether a type can be summarized at all, which is whether it has a hash rule.
584#[must_use]
585pub fn summarizable(ty: &LogicalType) -> bool {
586    countable(ty)
587}
588
589#[cfg(test)]
590mod tests {
591    use std::fs;
592    use std::path::PathBuf;
593    use std::time::{SystemTime, UNIX_EPOCH};
594
595    use rudb_common::Field;
596    use rudb_encoding::sketch::hash64;
597    use rudb_storage::count::hash_value;
598    use rudb_vector::{Chunk, Vector};
599
600    use super::*;
601    use crate::Writer;
602
603    fn path(label: &str) -> PathBuf {
604        let stamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("time advances").as_nanos();
605        std::env::temp_dir().join(format!("rudb-stats-{label}-{}-{stamp}.rdb", std::process::id()))
606    }
607
608    /// A one column table of these values, written a thousand rows to a part.
609    fn table_of(label: &str, values: &[Option<i64>]) -> PathBuf {
610        let path = path(label);
611        let mut writer =
612            Writer::create(&path, "t", vec![Field::new("v", LogicalType::BigInt)]).expect("new");
613        for part in values.chunks(1000) {
614            let held =
615                part.iter().map(|v| v.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
616            let chunk =
617                Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("values")])
618                    .expect("one column");
619            writer.append(&chunk).expect("a part");
620        }
621        writer.finish().expect("commit");
622        path
623    }
624
625    fn reopen(path: &PathBuf) -> Reader {
626        Catalog::open(path).expect("reopen").table("t").expect("the table")
627    }
628
629    #[test]
630    fn a_summary_built_over_a_file_says_what_the_column_holds() {
631        // End to end: the column goes to disk, comes back through the reader, and every field of
632        // the summary is the truth about it. Three thousand rows so the scan crosses parts, because
633        // a pass that read them in the wrong order would be right about one part and wrong about
634        // the order fields for the rest.
635        let values = (1..=3000_i64).map(Some).collect::<Vec<_>>();
636        let path = table_of("sorted", &values);
637        let built = build_stats(&path, "t", &[0]).expect("build");
638        assert_eq!(built.len(), 1);
639        assert!(built[0].built, "a one column table is nowhere near the budget");
640        assert_eq!(built[0].rows, 3000);
641        assert_eq!(built[0].distinct, 3000);
642        assert!(built[0].exact, "three thousand values is under the default k");
643        assert_eq!(built[0].order, Order::Ascending);
644
645        let reader = reopen(&path);
646        let summary = summary(&reader, 0).expect("the summary is in the file");
647        assert_eq!(summary.rows, 3000);
648        assert_eq!(summary.nulls, 0);
649        assert_eq!(summary.low, Some(Bound::Int(1)));
650        assert_eq!(summary.high, Some(Bound::Int(3000)));
651        assert!(summary.ends_exact);
652        assert!(summary.unique, "a sorted run of distinct values is a key candidate");
653        assert_eq!(summary.runs, 1, "one ascending run");
654        assert_eq!(summary.distinct_class, Class::Exact);
655        assert_eq!(summary.newest, reader.table().generation());
656
657        let sketches = sketches(&reader, 0).expect("the sketches are in the file");
658        assert!(sketches.merged.is_exact());
659        assert!(sketches.stripes.is_empty(), "the per stripe rule gives this column none");
660
661        fs::remove_file(&path).expect("clean up");
662    }
663
664    #[test]
665    fn nulls_are_counted_and_do_not_reach_the_ends_or_the_sketch() {
666        // The distinction that costs an answer if it is got wrong. A null is a row and is not a
667        // value, so it moves `rows` and `nulls` and moves nothing else.
668        let values: Vec<Option<i64>> =
669            (0..2000).map(|at| if at % 3 == 0 { None } else { Some(at) }).collect();
670        let path = table_of("nulls", &values);
671        build_stats(&path, "t", &[0]).expect("build");
672
673        let reader = reopen(&path);
674        let summary = summary(&reader, 0).expect("the summary");
675        let nulls = values.iter().filter(|v| v.is_none()).count() as u64;
676        assert_eq!(summary.rows, 2000);
677        assert_eq!(summary.nulls, nulls);
678        assert_eq!(summary.present(), 2000 - nulls);
679        assert_eq!(summary.distinct, 2000 - nulls, "a null is not a distinct value");
680        assert_eq!(summary.low, Some(Bound::Int(1)), "zero is null here");
681        assert!(summary.unique);
682
683        fs::remove_file(&path).expect("clean up");
684    }
685
686    #[test]
687    fn a_column_that_repeats_is_not_reported_unique_and_a_descending_one_is_seen() {
688        let values = (0..2000_i64).map(|at| Some(-(at / 2))).collect::<Vec<_>>();
689        let path = table_of("repeats", &values);
690        build_stats(&path, "t", &[0]).expect("build");
691
692        let reader = reopen(&path);
693        let summary = summary(&reader, 0).expect("the summary");
694        assert_eq!(summary.distinct, 1000);
695        assert!(!summary.unique, "every value appears twice");
696        assert_eq!(summary.order, Order::Descending);
697        assert_eq!(summary.runs, 1000, "a descending column is a run per distinct value");
698
699        fs::remove_file(&path).expect("clean up");
700    }
701
702    #[test]
703    fn a_column_past_the_default_k_is_estimated_and_says_so() {
704        // The rule the module doc names, at the point where it bites. Past k the sketch threw values
705        // away, so the count is an estimate, and the class has to say so or a COUNT(DISTINCT) is
706        // answered out of metadata with a number that is close and wrong.
707        let values = (0..20_000_i64).map(Some).collect::<Vec<_>>();
708        let path = table_of("estimated", &values);
709        let built = build_stats(&path, "t", &[0]).expect("build");
710        assert!(!built[0].exact, "twenty thousand values is past the default k");
711
712        let reader = reopen(&path);
713        let summary = summary(&reader, 0).expect("the summary");
714        assert_eq!(summary.distinct_class, Class::Estimated);
715        assert!(!summary.unique, "uniqueness is never claimed off an estimate");
716        assert!(summary.distinct > 17_000 && summary.distinct <= 20_000, "{}", summary.distinct);
717        assert!(summary.distinct <= summary.present(), "more distinct values than rows");
718
719        fs::remove_file(&path).expect("clean up");
720    }
721
722    #[test]
723    fn a_shuffled_column_is_neither_ordered_nor_one_run() {
724        let values = (0..2000_i64).map(|at| Some((at * 7919) % 2000)).collect::<Vec<_>>();
725        let path = table_of("shuffled", &values);
726        build_stats(&path, "t", &[0]).expect("build");
727
728        let reader = reopen(&path);
729        let summary = summary(&reader, 0).expect("the summary");
730        assert_eq!(summary.order, Order::Neither);
731        assert!(summary.runs > 100, "a shuffle is many runs, not one: {}", summary.runs);
732        assert_eq!(summary.low, Some(Bound::Int(0)));
733        assert_eq!(summary.high, Some(Bound::Int(1999)));
734
735        fs::remove_file(&path).expect("clean up");
736    }
737
738    #[test]
739    fn deleting_the_sections_changes_nothing_but_whether_they_are_there() {
740        // Section 3.1, as close to directly as a test can put it. The same file, read once with the
741        // sections and once with the generation moved past them, and the reader opens and scans the
742        // same either way.
743        let values = (1..=1500_i64).map(Some).collect::<Vec<_>>();
744        let path = table_of("invariant", &values);
745        build_stats(&path, "t", &[0]).expect("build");
746
747        let reader = reopen(&path);
748        assert!(summary(&reader, 0).is_some());
749        let generation = reader.table().generation();
750        let held: Vec<_> = reader
751            .table()
752            .sections()
753            .iter()
754            .filter(|s| s.kind == *section::SUMMARY || s.kind == *section::SKETCHES)
755            .copied()
756            .collect();
757        assert_eq!(held.len(), 2, "a summary and a sketch section");
758        for section in &held {
759            assert!(section.usable(generation));
760            assert!(!section.usable(generation + 1), "a rewrite invalidates rather than corrupts");
761        }
762        let rows: usize =
763            (0..reader.parts()).map(|part| reader.read(part, &[0]).expect("a part").len()).sum();
764        assert_eq!(rows, 1500, "the scan is the scan whether the sections are read or not");
765
766        fs::remove_file(&path).expect("clean up");
767    }
768
769    #[test]
770    fn a_string_column_is_read_through_the_typed_path_and_measured_by_its_bytes() {
771        // The other fast path. A varchar has no fixed width, so the byte total and the widest value
772        // are measured per value, and the ends are the string ends rather than the hash ends.
773        let path = path("strings");
774        let mut writer =
775            Writer::create(&path, "t", vec![Field::new("v", LogicalType::Varchar)]).expect("new");
776        let words = ["alpha", "bravo", "charlie", "delta", "alpha"];
777        let held = words.iter().map(|w| Value::Varchar((*w).into())).collect::<Vec<_>>();
778        let chunk =
779            Chunk::new(vec![Vector::from_values(LogicalType::Varchar, &held).expect("words")])
780                .expect("one column");
781        writer.append(&chunk).expect("a part");
782        writer.finish().expect("commit");
783        build_stats(&path, "t", &[0]).expect("build");
784
785        let reader = reopen(&path);
786        let summary = summary(&reader, 0).expect("the summary");
787        assert_eq!(summary.rows, 5);
788        assert_eq!(summary.distinct, 4, "alpha twice");
789        assert!(!summary.unique);
790        assert_eq!(summary.bytes, words.iter().map(|w| w.len() as u64).sum::<u64>());
791        assert_eq!(summary.widest, 7, "charlie");
792        assert_eq!(summary.low, Some(Bound::Bytes(b"alpha".to_vec())));
793        assert_eq!(summary.high, Some(Bound::Bytes(b"delta".to_vec())));
794
795        drop(reader);
796        fs::remove_file(&path).expect("clean up");
797    }
798
799    #[test]
800    fn a_type_with_no_hash_rule_is_refused_by_name_rather_than_summarized_as_empty() {
801        let path = table_of("refused", &[Some(1)]);
802        let reader = reopen(&path);
803        assert!(summarizable(&LogicalType::BigInt));
804        assert!(!summarizable(&LogicalType::Interval));
805        assert!(build_summary(&reader, 1).is_err(), "a column past the end");
806        drop(reader);
807        fs::remove_file(&path).expect("clean up");
808    }
809
810    #[test]
811    fn the_stored_sketch_depends_on_the_value_rule_and_not_only_on_the_hash() {
812        // HASH_IDENTITY pins `hash64`, which is half of what a stored sketch depends on. The other
813        // half is the rule that turns a value into the bytes `hash64` sees, and that rule lives in
814        // `rudb_storage::count`. Changing it without bumping HASH_IDENTITY would leave every stored
815        // sketch readable, accepted, and built over a different universe than the one a new sketch
816        // is built over, which is exactly the merge the identity exists to prevent.
817        //
818        // So the rule is pinned here. If this fails because `hash_value` changed on purpose, the fix
819        // is to bump HASH_IDENTITY and then update these numbers, in that order.
820        assert_eq!(hash_value(&Value::BigInt(1)), Some(hash64(&1_u128.to_le_bytes())));
821        assert_eq!(hash_value(&Value::Integer(1)), hash_value(&Value::BigInt(1)));
822        assert_eq!(hash_value(&Value::Varchar("a".into())), Some(hash64(b"a")));
823        assert_eq!(hash_value(&Value::Null), None);
824    }
825}