Skip to main content

rudb_encoding/
chooser.rs

1//! How the encoder decides which candidate to keep.
2//!
3//! The encoders in [`crate::string`] and [`crate::integer`] are the work. This is the search. They
4//! are separate things and until now they were the same thing, because `encode` both offered every
5//! candidate and encoded every candidate it offered, and there was no way to have one without the
6//! other.
7//!
8//! # Why it is worth separating
9//!
10//! `cargo xtask encode` over a million rows of ClickBench `hits` says where the encoder's seconds
11//! go. String `FRONT` is 32.2 percent of them and is kept once in 142 chunks. String `FSST` is 11.2
12//! percent and is kept twice in 223. Integer `DELTA` is 10.8 percent and is kept never in 607.
13//! String `PLAIN` is 8.7 percent and is kept four times in 223. Those four are 62.9 percent of the
14//! encoder's time and they were kept seven times out of 1,195 offers.
15//!
16//! That is not a bug in any encoder. It is what an exhaustive search costs, and the search is worth
17//! something: the shapes it arrives at are five to one on `hits` and nobody wrote them down in
18//! advance. The question is how much of the search is needed, which is a question about the data
19//! and therefore a question to measure rather than argue about. F2 asks for exactly this, as "the
20//! encoder chooser as a seam, with exhaustive and sampled implementations", with the ablation being
21//! how much size the sampled one gives up.
22//!
23//! # What a chooser sees and what it does not
24//!
25//! A chooser is asked once per chunk per level of the cascade, never once per value. It is handed
26//! the values and the candidates that apply and it returns the ones worth encoding in full. It
27//! cannot invent a candidate that does not apply, so nothing it does can produce a chunk that will
28//! not decode, and the worst a bad chooser can do is pick a bigger encoding than another one would
29//! have. That is the property that makes this safe to swap.
30//!
31//! # Not a `rudb-seam` seam yet, and why
32//!
33//! `SeamId::StorageEncoder` exists and says "how a block of values is encoded on the way to disk",
34//! and this is what belongs behind it. It cannot be registered here: `rudb-seam` is rank 2 and so is
35//! this crate, so the `Strategy` supertrait every seam trait needs is not visible from here. The
36//! registry goes in `rudb-storage` at rank 5, next to the write path, and there is no write path
37//! yet. Until there is, this is a plain trait with two implementations and an ablation, which is
38//! the part that can be measured today.
39
40use std::sync::Arc;
41use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
42
43use crate::fsst::SymbolTable;
44use crate::{integer, string};
45
46/// Which of the candidates that apply are worth encoding in full.
47///
48/// Crossed once per chunk per level of the cascade. No method here sees a single value on its own,
49/// which is the rule that lets the decision be indirect at all.
50pub trait Chooser: std::fmt::Debug + Sync {
51    /// The name that goes in a report.
52    fn name(&self) -> &'static str;
53
54    /// Which of `offered` to encode in full, for a chunk of strings at `depth`.
55    ///
56    /// `offered` is what applies, in the order the exhaustive chooser would try them. The return
57    /// has to be a subset of it and has to be non empty, because a chunk with no candidate is a
58    /// chunk that cannot be written.
59    fn narrow_strings(
60        &self,
61        values: &[&[u8]],
62        offered: &[string::Kind],
63        depth: u8,
64    ) -> Vec<string::Kind>;
65
66    /// Which of `offered` to encode in full, for a chunk of integers at `depth`.
67    fn narrow_integers(
68        &self,
69        values: &[i64],
70        offered: &[integer::Kind],
71        depth: u8,
72    ) -> Vec<integer::Kind>;
73
74    /// Whether `kind` can ever be in what [`Chooser::narrow_integers`] returns at `depth`.
75    ///
76    /// Asked before the candidates are worked out, so a kind this rules out is never tested for.
77    /// That matters because the test is not free: finding out whether a dictionary or a sparse
78    /// encoding applies used to sort a copy of the chunk, at every level of the cascade, for a
79    /// chooser that was going to throw both away. Saying yes to a kind that is then dropped only
80    /// costs the test. Saying no to a kind the narrowing would have kept changes what gets written,
81    /// so the default is yes and an implementation only says no where its narrowing always would.
82    fn considers_integer(&self, kind: integer::Kind, depth: u8) -> bool {
83        let _ = (kind, depth);
84        true
85    }
86
87    /// A symbol table already trained for FSST at `depth`, or `None` to train one on the chunk.
88    ///
89    /// Training is five passes over a sample of up to 64 KiB, and a caller encoding thousands of
90    /// chunks cut out of one column trains the same table thousands of times. Such a caller trains
91    /// it once over the column and hands it out here. The table travels with every chunk either
92    /// way, so a chunk compressed against a table it was not trained on still reads back.
93    fn symbols(&self, depth: u8) -> Option<&SymbolTable> {
94        let _ = depth;
95        None
96    }
97}
98
99/// Encode every candidate that applies and keep the smallest.
100///
101/// The reference, and what `encode` has always done. It is the thing to beat rather than the thing
102/// to ship: every size this crate has ever reported came out of it, so an alternative's ablation is
103/// against this and a build that wants the old bytes exactly asks for this.
104#[derive(Debug, Clone, Copy, Default)]
105pub struct Exhaustive;
106
107/// The one of these that does not have to be constructed, since it holds nothing.
108pub const EXHAUSTIVE: Exhaustive = Exhaustive;
109
110impl Chooser for Exhaustive {
111    fn name(&self) -> &'static str {
112        "exhaustive"
113    }
114
115    fn narrow_strings(
116        &self,
117        _values: &[&[u8]],
118        offered: &[string::Kind],
119        _depth: u8,
120    ) -> Vec<string::Kind> {
121        offered.to_vec()
122    }
123
124    fn narrow_integers(
125        &self,
126        _values: &[i64],
127        offered: &[integer::Kind],
128        _depth: u8,
129    ) -> Vec<integer::Kind> {
130        offered.to_vec()
131    }
132}
133
134/// Encode every candidate on a sample, then encode only the winner on the whole chunk.
135///
136/// The bet is that a chunk of 122,880 values and a sample of 8,192 drawn from it agree about which
137/// encoding suits them, which is a bet about the data and is what the ablation settles. Where it is
138/// wrong the cost is size and never correctness, because the winner still has to apply to the whole
139/// chunk and is still encoded over all of it.
140///
141/// The sample is windows of consecutive values rather than values picked one at a time, because
142/// three of the candidates are about what a value has in common with the value before it. A sample
143/// of scattered singletons would show `FRONT` and `RLE` nothing to find and would rule them out on
144/// every column, which is the wrong answer arrived at quickly.
145///
146/// There are two guards on whether to sample at all and both of them are there because a measurement
147/// said so. A chunk with fewer values than the sample is not sampled, because encoding every
148/// candidate on something the size of the chunk and then encoding the winner on the chunk is more
149/// work than the exhaustive chooser for the same answer. A chunk holding less than a page of bytes is
150/// not sampled either, because the cost of the search scales with the bytes in the chunk and not
151/// with how many values they are spread over, so on a narrow column there is nothing to save and a
152/// sample that misses the structure gives up real size for it.
153#[derive(Debug, Clone, Copy)]
154pub struct Sampled {
155    window: usize,
156    regions: usize,
157}
158
159/// How many consecutive values one window of the sample holds.
160///
161/// The tile, which is what a bit packing kernel works in and is the smallest run of a column that
162/// has the column's local structure in it rather than one value's worth of accident.
163const WINDOW: usize = 1024;
164
165/// How many windows the sample is drawn from.
166///
167/// Eight windows of a tile each is 8,192 values, a fifteenth of a chunk. Spread across the chunk
168/// rather than taken off the front, because the front of a sorted column is one value repeated and
169/// a chooser that saw only that would pick `CONSTANT` for everything.
170const REGIONS: usize = 8;
171
172/// How few bytes a chunk can hold before sampling it is not worth the risk.
173///
174/// The ablation in #559 found `Params` at a million rows encoding to 21,782 bytes exhaustively and
175/// 128,455 bytes sampled, which is 490 percent for a column that is almost entirely empty strings.
176/// It passed the value count guard because it has a million values, and then the sample missed what
177/// little structure it had. The exhaustive search over a column that small costs almost nothing,
178/// which is the same fact from the other side, so a floor on bytes takes the whole class of column
179/// out of the sampler's hands and gives up nothing to do it.
180///
181/// 256 KiB is one page, which is the smallest unit the format moves. Below that the search is not
182/// where the time is.
183const FLOOR: usize = 256 * 1024;
184
185impl Default for Sampled {
186    fn default() -> Self {
187        Self { window: WINDOW, regions: REGIONS }
188    }
189}
190
191impl Sampled {
192    /// The default sample, which is eight windows of 1,024 values.
193    #[must_use]
194    pub fn new() -> Self {
195        Self::default()
196    }
197
198    /// A sample of a size somebody else picked, which is what the ablation sweeps.
199    #[must_use]
200    pub fn over(window: usize, regions: usize) -> Self {
201        Self { window: window.max(1), regions: regions.max(1) }
202    }
203
204    /// How many values the sample holds, which is one of the two things that decide whether
205    /// sampling is worth doing.
206    #[must_use]
207    pub fn size(self) -> usize {
208        self.window * self.regions
209    }
210
211    /// Whether a chunk of `count` values holding `bytes` bytes is worth sampling.
212    fn worth_it(self, count: usize, bytes: usize) -> bool {
213        count > self.size() && bytes >= FLOOR
214    }
215}
216
217impl Chooser for Sampled {
218    fn name(&self) -> &'static str {
219        "sampled"
220    }
221
222    fn narrow_strings(
223        &self,
224        values: &[&[u8]],
225        offered: &[string::Kind],
226        depth: u8,
227    ) -> Vec<string::Kind> {
228        let bytes = values.iter().map(|value| value.len()).sum();
229        if offered.len() < 2 || !self.worth_it(values.len(), bytes) {
230            return offered.to_vec();
231        }
232        let sample = sample(values, self.window, self.regions);
233        let mut best: Option<(string::Kind, usize)> = None;
234        for &kind in offered {
235            let Ok(Some(size)) = string::size_as(kind, &sample, depth) else {
236                continue;
237            };
238            if best.is_none_or(|(_, smallest)| size < smallest) {
239                best = Some((kind, size));
240            }
241        }
242        // Nothing applied to the sample, which should not happen and is not worth a wrong answer
243        // if it does. Hand back everything and let the exhaustive path sort it out.
244        best.map_or_else(|| offered.to_vec(), |(kind, _)| vec![kind])
245    }
246
247    fn narrow_integers(
248        &self,
249        values: &[i64],
250        offered: &[integer::Kind],
251        depth: u8,
252    ) -> Vec<integer::Kind> {
253        if offered.len() < 2 || !self.worth_it(values.len(), values.len() * 8) {
254            return offered.to_vec();
255        }
256        let sample = sample(values, self.window, self.regions);
257        let mut best: Option<(integer::Kind, usize)> = None;
258        for &kind in offered {
259            let Ok(Some(size)) = integer::size_as(kind, &sample, depth) else {
260                continue;
261            };
262            if best.is_none_or(|(_, smallest)| size < smallest) {
263                best = Some((kind, size));
264            }
265        }
266        best.map_or_else(|| offered.to_vec(), |(kind, _)| vec![kind])
267    }
268}
269
270/// Encode one shape that somebody else settled on, and do not search at all.
271///
272/// [`Sampled`] decides per chunk, which is right when a chunk is big enough to pay for the sample
273/// and when neighbouring chunks are different from each other. Neither holds for a caller that has
274/// thousands of small chunks cut out of one column, because the sample would cost as much as the
275/// encode and because the answer would come out the same thousands of times. Such a caller decides
276/// once, over as much of the column as it likes, and hands the answer here.
277///
278/// A shape is one kind per level of the cascade, which is a simplification of a real one: `FRONT`
279/// produces an integer chunk of prefixes and a string chunk of suffixes at the next level, and both
280/// are narrowed to the same entry. That is enough on real data because the tree is narrow and
281/// because the levels below the second are small. Any level the shape does not reach is searched
282/// exhaustively, which is what makes the shape a hint about the expensive part rather than a
283/// decision about all of it.
284///
285/// An entry that does not apply to a chunk is ignored and the chunk is searched instead. The kinds
286/// that apply are a property of the values, and this is a chooser rather than a way round the
287/// filter, so a shape can never produce something that will not decode.
288#[derive(Debug, Clone)]
289pub struct Settled {
290    strings: Vec<string::Kind>,
291    integers: Vec<integer::Kind>,
292    /// The level FSST runs at and the table it compresses against there, when one was trained for
293    /// the whole column. See [`string::with_symbols`].
294    symbols: Option<(u8, Arc<SymbolTable>)>,
295}
296
297impl Settled {
298    /// A shape, outermost level first, for the string levels and the integer levels.
299    #[must_use]
300    pub fn new(strings: Vec<string::Kind>, integers: Vec<integer::Kind>) -> Self {
301        Self { strings, integers, symbols: None }
302    }
303
304    /// The same shape, compressing against `table` wherever FSST is tried at `depth`.
305    #[must_use]
306    pub fn with_symbols(mut self, depth: u8, table: SymbolTable) -> Self {
307        self.symbols = Some((depth, Arc::new(table)));
308        self
309    }
310
311    /// The string kinds of the shape, outermost first, which is what a report prints.
312    #[must_use]
313    pub fn strings(&self) -> &[string::Kind] {
314        &self.strings
315    }
316}
317
318impl Chooser for Settled {
319    fn name(&self) -> &'static str {
320        "settled"
321    }
322
323    fn narrow_strings(
324        &self,
325        _values: &[&[u8]],
326        offered: &[string::Kind],
327        depth: u8,
328    ) -> Vec<string::Kind> {
329        match self.strings.get(depth as usize) {
330            Some(kind) if offered.contains(kind) => vec![*kind],
331            _ => offered.to_vec(),
332        }
333    }
334
335    fn narrow_integers(
336        &self,
337        _values: &[i64],
338        offered: &[integer::Kind],
339        depth: u8,
340    ) -> Vec<integer::Kind> {
341        match self.integers.get(depth as usize) {
342            Some(kind) if offered.contains(kind) => vec![*kind],
343            _ => offered.to_vec(),
344        }
345    }
346
347    fn considers_integer(&self, kind: integer::Kind, depth: u8) -> bool {
348        // `Packed` applies to every chunk the search offers anything else on, so a level settled on
349        // it keeps nothing else and the tests for the other kinds are thrown away unread. A global
350        // dictionary block settles its integers on `Packed`, and its lengths, prefixes and token
351        // arrays each ran those tests, a sorted copy for the dictionary one among them, on every
352        // block. Any other settled kind may not be offered, and then the level is searched, so it
353        // has to say yes to everything.
354        match self.integers.get(depth as usize) {
355            Some(integer::Kind::Packed) => kind == integer::Kind::Packed,
356            _ => true,
357        }
358    }
359
360    fn symbols(&self, depth: u8) -> Option<&SymbolTable> {
361        self.symbols.as_ref().filter(|(at, _)| *at == depth).map(|(_, table)| &**table)
362    }
363}
364
365/// Encode an integer chunk the way an earlier one came out, and search only where it stops fitting.
366///
367/// [`Settled`] holds one kind per level, which is too coarse for a cascade that branches: an `RLE`
368/// wants its run values packed and its run lengths constant, and a shape of one kind per level
369/// cannot say both. This holds every level's kind in the order the encoder asks for them, which is
370/// what [`integer::shape`] reads back out of an encoded chunk, and hands them back one per question.
371///
372/// The first question whose answer is not among the kinds offered ends the replay, and from there
373/// on every question goes to `fallback`. A chunk only offers kinds that apply to it, so a shape
374/// that stops fitting costs a search and never a chunk that will not decode. The order of the
375/// questions is the order of the kinds only while every answer is a single kind, which is why the
376/// replay does not pick back up after a search.
377///
378/// A shape that still fits can still be the wrong one. Bit packing applies to everything, so a
379/// shape settled on a stretch of noise replays happily over a column that has since become one
380/// value with exceptions, at forty times the size. What does change when the column does is the set
381/// of kinds the top level offers, so a replay can be told the set its shape was searched under with
382/// [`Replay::expecting`], and searches from the top when the chunk offers anything else. The set is
383/// worked out for the chunk whatever the chooser, so the check costs nothing.
384///
385/// One of these is for one chunk. The position is kept in atomics because a chooser is shared
386/// between threads by contract, not because a chunk's encode is ever split between them.
387#[derive(Debug)]
388pub struct Replay<'a> {
389    kinds: &'a [integer::Kind],
390    next: AtomicUsize,
391    lost: AtomicBool,
392    /// The kinds the top level offered, one bit per tag, once it has been asked.
393    first: AtomicU8,
394    /// The set the top level has to offer for the replay to go ahead, when there is one.
395    expected: Option<u8>,
396    fallback: &'a dyn Chooser,
397}
398
399impl<'a> Replay<'a> {
400    /// A replay of `kinds`, with `fallback` answering once they stop fitting.
401    #[must_use]
402    pub fn new(kinds: &'a [integer::Kind], fallback: &'a dyn Chooser) -> Self {
403        Self {
404            kinds,
405            next: AtomicUsize::new(0),
406            lost: AtomicBool::new(false),
407            first: AtomicU8::new(0),
408            expected: None,
409            fallback,
410        }
411    }
412
413    /// The same replay, going ahead only on a chunk whose top level offers exactly `offered`.
414    #[must_use]
415    pub fn expecting(mut self, offered: &[integer::Kind]) -> Self {
416        self.expected = Some(bits(offered));
417        self
418    }
419
420    /// What the top level of the chunk offered, in tag order, or nothing before it was asked.
421    #[must_use]
422    pub fn first_offered(&self) -> Vec<integer::Kind> {
423        let first = self.first.load(Ordering::Relaxed);
424        integer::Kind::ALL.into_iter().filter(|kind| first & (1 << *kind as u8) != 0).collect()
425    }
426
427    /// Whether every question was answered from the shape, which is whether the chunk came out
428    /// the shape it was given.
429    #[must_use]
430    pub fn held(&self) -> bool {
431        !self.lost.load(Ordering::Relaxed) && self.next.load(Ordering::Relaxed) == self.kinds.len()
432    }
433}
434
435impl Chooser for Replay<'_> {
436    fn name(&self) -> &'static str {
437        "replay"
438    }
439
440    fn narrow_strings(
441        &self,
442        values: &[&[u8]],
443        offered: &[string::Kind],
444        depth: u8,
445    ) -> Vec<string::Kind> {
446        self.fallback.narrow_strings(values, offered, depth)
447    }
448
449    fn narrow_integers(
450        &self,
451        values: &[i64],
452        offered: &[integer::Kind],
453        depth: u8,
454    ) -> Vec<integer::Kind> {
455        if depth == 0 {
456            self.first.store(bits(offered), Ordering::Relaxed);
457            if self.expected.is_some_and(|expected| expected != bits(offered)) {
458                self.lost.store(true, Ordering::Relaxed);
459            }
460        }
461        if !self.lost.load(Ordering::Relaxed) {
462            let at = self.next.fetch_add(1, Ordering::Relaxed);
463            match self.kinds.get(at) {
464                Some(kind) if offered.contains(kind) => return vec![*kind],
465                _ => self.lost.store(true, Ordering::Relaxed),
466            }
467        }
468        self.fallback.narrow_integers(values, offered, depth)
469    }
470
471    fn considers_integer(&self, kind: integer::Kind, depth: u8) -> bool {
472        // Asked before the question the replay answers, so it has to say yes to the kind the shape
473        // is about to hand back as well as to anything the fallback might keep.
474        self.kinds.contains(&kind) || self.fallback.considers_integer(kind, depth)
475    }
476}
477
478/// A set of integer kinds as one bit per tag.
479fn bits(kinds: &[integer::Kind]) -> u8 {
480    kinds.iter().fold(0, |set, kind| set | 1 << *kind as u8)
481}
482
483/// `regions` windows of `window` consecutive values each, spread evenly across the input.
484///
485/// The starts are spread over the whole range a window can start at, so the first window begins at
486/// the first value and the last one ends at the last value. A chunk of 122,880 values sampled at
487/// eight windows of 1,024 gives windows starting at 0, 17,408, 34,816 and so on up to 121,856, which
488/// crosses every part of the chunk including both ends of it.
489///
490/// Spreading to the end rather than striding by `len / regions` matters on the columns this is for.
491/// A stride would leave the last stride minus one window of the chunk unsampled, and the tail of a
492/// chunk is exactly where a column that is sorted or clustered stops looking like its front.
493pub(crate) fn sample<T: Copy>(values: &[T], window: usize, regions: usize) -> Vec<T> {
494    let wanted = window * regions;
495    if values.len() <= wanted {
496        return values.to_vec();
497    }
498    let last = values.len() - window;
499    let mut out = Vec::with_capacity(wanted);
500    for region in 0..regions {
501        let from = if regions == 1 { 0 } else { region * last / (regions - 1) };
502        out.extend_from_slice(&values[from..from + window]);
503    }
504    out
505}
506
507#[cfg(test)]
508mod tests {
509    use super::{Chooser, EXHAUSTIVE, Replay, Sampled, Settled, sample};
510    use crate::{integer, string};
511
512    /// A settled shape that tests for every kind, which is what [`Settled`] did before it said
513    /// which kinds it could never keep.
514    #[derive(Debug)]
515    struct TestsEverything<'a>(&'a Settled);
516
517    impl Chooser for TestsEverything<'_> {
518        fn name(&self) -> &'static str {
519            "tests everything"
520        }
521
522        fn narrow_strings(
523            &self,
524            values: &[&[u8]],
525            offered: &[string::Kind],
526            depth: u8,
527        ) -> Vec<string::Kind> {
528            self.0.narrow_strings(values, offered, depth)
529        }
530
531        fn narrow_integers(
532            &self,
533            values: &[i64],
534            offered: &[integer::Kind],
535            depth: u8,
536        ) -> Vec<integer::Kind> {
537            self.0.narrow_integers(values, offered, depth)
538        }
539    }
540
541    /// A shape settled on `Packed` writes the same bytes whether or not the other kinds are tested
542    /// for, over integers of every shape and over strings whose lengths, prefixes and tokens are
543    /// integer arrays under it.
544    #[test]
545    fn a_shape_settled_on_packed_writes_what_testing_everything_wrote() {
546        for values in shaped_columns() {
547            let settled = Settled::new(Vec::new(), vec![integer::Kind::Packed]);
548            let quick = integer::encode_with(&values, &settled).unwrap();
549            let full = integer::encode_with(&values, &TestsEverything(&settled)).unwrap();
550            assert_eq!(quick, full, "{}", integer::describe(&full).unwrap());
551        }
552        let texts = (0..2048)
553            .map(|row| match row % 5 {
554                0 => format!("https://example.com/item/{row}"),
555                1 => format!("https://example.com/item/{}", row / 7),
556                2 => String::new(),
557                3 => "same".repeat(row % 11),
558                _ => format!("{:x}", row * 2_654_435_761_usize),
559            })
560            .collect::<Vec<_>>();
561        let values = texts.iter().map(|text| text.as_bytes()).collect::<Vec<_>>();
562        for strings in [
563            vec![string::Kind::Front, string::Kind::Lz],
564            vec![string::Kind::Lz, string::Kind::Fsst],
565            vec![string::Kind::Lz, string::Kind::Plain],
566            vec![string::Kind::Fsst],
567            vec![string::Kind::Plain],
568        ] {
569            let settled = Settled::new(strings, vec![integer::Kind::Packed]);
570            let quick = string::encode_with(&values, &settled).unwrap();
571            let full = string::encode_with(&values, &TestsEverything(&settled)).unwrap();
572            assert_eq!(quick, full, "{:?}", settled.strings());
573        }
574    }
575
576    /// Columns of the shapes a writer meets: a climbing timestamp, runs, one value with exceptions,
577    /// a stride, noise, and a short tail.
578    fn shaped_columns() -> Vec<Vec<i64>> {
579        let mut state = 0x9e37_79b9_7f4a_7c15_u64;
580        let mut noise = || {
581            state ^= state << 13;
582            state ^= state >> 7;
583            state ^= state << 17;
584            (state % 1_000_000) as i64
585        };
586        vec![
587            (0..2048).map(|row| 1_600_000_000_000_000 + row * 1_000_000 + row % 7).collect(),
588            (0..2048).map(|row| row / 300).collect(),
589            (0..2048).map(|row| if row % 97 == 0 { row } else { 42 }).collect(),
590            (0..2048).map(|row| 5 + row * 1_000_000).collect(),
591            (0..2048).map(|_| noise()).collect(),
592            (0..37).map(|row| row * row).collect(),
593        ]
594    }
595
596    /// Replaying the shape a chunk came out as gives the same bytes, and asks no question the shape
597    /// did not answer, which is the whole of what a writer is relying on when it stops searching.
598    #[test]
599    fn a_chunk_replayed_through_its_own_shape_comes_out_the_same() {
600        for values in shaped_columns() {
601            let searched = integer::encode_with(&values, &EXHAUSTIVE).unwrap();
602            let kinds = integer::shape(&searched).unwrap();
603            let replay = Replay::new(&kinds, &EXHAUSTIVE);
604            let replayed = integer::encode_with(&values, &replay).unwrap();
605            assert_eq!(replayed, searched, "{}", integer::describe(&searched).unwrap());
606            assert!(replay.held(), "{}", integer::describe(&searched).unwrap());
607        }
608    }
609
610    /// A shape that fits but was searched under a different offer is not replayed. Bit packing
611    /// fits everything, so without the check a shape settled on noise would pack a column of one
612    /// value with exceptions, which the search writes in a fraction of the bytes.
613    #[test]
614    fn a_shape_searched_under_another_offer_searches_again() {
615        let columns = shaped_columns();
616        let (noise, sparse) = (&columns[4], &columns[2]);
617        let first = Replay::new(&[], &EXHAUSTIVE);
618        let searched = integer::encode_with(noise, &first).unwrap();
619        let kinds = integer::shape(&searched).unwrap();
620        let offered = first.first_offered();
621        assert_eq!(offered, integer::offered(noise));
622
623        let blind = Replay::new(&kinds, &EXHAUSTIVE);
624        let packed = integer::encode_with(sparse, &blind).unwrap();
625        assert!(blind.held(), "packing fits any column, which is the trouble");
626
627        let checked = Replay::new(&kinds, &EXHAUSTIVE).expecting(&offered);
628        let written = integer::encode_with(sparse, &checked).unwrap();
629        assert!(!checked.held());
630        assert_eq!(written, integer::encode_with(sparse, &EXHAUSTIVE).unwrap());
631        assert!(written.len() * 4 < packed.len(), "{} against {}", written.len(), packed.len());
632    }
633
634    /// A shape from one column on another column it does not fit still writes that column, because
635    /// the replay stops at the first kind that is not offered and searches from there.
636    #[test]
637    fn a_shape_that_does_not_fit_still_writes_values_that_read_back() {
638        let columns = shaped_columns();
639        for from in &columns {
640            let kinds = integer::shape(&integer::encode_with(from, &EXHAUSTIVE).unwrap()).unwrap();
641            for values in &columns {
642                let replay = Replay::new(&kinds, &EXHAUSTIVE);
643                let bytes = integer::encode_with(values, &replay).unwrap();
644                assert_eq!(&integer::decode(&bytes).unwrap(), values);
645            }
646        }
647    }
648
649    #[test]
650    fn a_sample_covers_the_whole_input_and_not_one_end_of_it() {
651        let values: Vec<i64> = (0..8000).collect();
652        let taken = sample(&values, 10, 4);
653        assert_eq!(taken.len(), 40);
654        assert_eq!(taken[0], 0);
655        assert_eq!(taken[10], 2663);
656        assert_eq!(taken[20], 5326);
657        assert_eq!(taken[30], 7990);
658        assert_eq!(taken[39], 7999);
659    }
660
661    #[test]
662    fn an_input_no_bigger_than_the_sample_is_the_sample() {
663        let values: Vec<i64> = (0..30).collect();
664        assert_eq!(sample(&values, 10, 4), values);
665    }
666
667    #[test]
668    fn the_last_window_does_not_run_off_the_end() {
669        // Two windows of 40 over 100 values puts the second one at 60, which is the last start that
670        // fits. Windows that overlap because there are more of them than the input has room for is
671        // fine and double counts a few values. Reading past the end is not.
672        let values: Vec<i64> = (0..100).collect();
673        let taken = sample(&values, 40, 2);
674        assert_eq!(taken.len(), 80);
675        assert_eq!(*taken.last().expect("the sample is not empty"), 99);
676    }
677
678    #[test]
679    fn the_exhaustive_chooser_hands_back_exactly_what_it_was_offered() {
680        let offered = [string::Kind::Plain, string::Kind::Fsst, string::Kind::Dict];
681        assert_eq!(EXHAUSTIVE.narrow_strings(&[b"a".as_slice()], &offered, 0), offered);
682        let offered = [integer::Kind::Packed, integer::Kind::Delta];
683        assert_eq!(EXHAUSTIVE.narrow_integers(&[1, 2], &offered, 0), offered);
684    }
685
686    #[test]
687    fn a_chunk_no_bigger_than_the_sample_is_not_narrowed_at_all() {
688        // Sampling a chunk that is smaller than the sample would encode every candidate on
689        // something the size of the chunk and then encode the winner on the chunk, which is more
690        // work than the exhaustive chooser for the same answer.
691        let sampled = Sampled::over(4, 2);
692        let values: Vec<i64> = (0..8).collect();
693        let offered = [integer::Kind::Packed, integer::Kind::Delta];
694        assert_eq!(sampled.narrow_integers(&values, &offered, 0), offered);
695    }
696
697    #[test]
698    fn a_sampled_chooser_returns_one_of_what_it_was_offered() {
699        let sampled = Sampled::over(16, 2);
700        let values: Vec<i64> = (0..40_000).map(|index| index / 200).collect();
701        let offered = [integer::Kind::Packed, integer::Kind::Rle, integer::Kind::Dict];
702        let narrowed = sampled.narrow_integers(&values, &offered, 0);
703        assert_eq!(narrowed.len(), 1);
704        assert!(offered.contains(&narrowed[0]), "{narrowed:?}");
705    }
706
707    #[test]
708    fn a_chunk_with_plenty_of_values_and_hardly_any_bytes_is_not_sampled() {
709        // ClickBench Params at a million rows: a value per row and almost all of them empty. It
710        // passes the value count guard and the exhaustive chooser encodes it in 21,782 bytes while
711        // the sampler took 128,455, so the byte floor is what keeps it out of the sampler's hands.
712        let sampled = Sampled::over(16, 2);
713        let empty = Vec::new();
714        let values: Vec<&[u8]> = vec![empty.as_slice(); 40_000];
715        let offered = [string::Kind::Plain, string::Kind::Fsst, string::Kind::Dict];
716        assert_eq!(sampled.narrow_strings(&values, &offered, 0), offered);
717    }
718}