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}