Skip to main content

rudb_encoding/
string.rs

1//! The string column, which is offsets, bytes, and the choice between compressing the bytes and
2//! not storing most of them at all.
3//!
4//! ClickBench `hits` is a string dataset before it is anything else. `URL`, `Referer`, `Title` and
5//! the referer derived columns are most of the 20.46 GB DuckDB writes for it, so most of what
6//! `spec/02-the-goal.md` promises on the resource axis has to come out of this file.
7//!
8//! ## The four shapes
9//!
10//! `CONSTANT` when every value is the same. `PLAIN`, which is lengths and raw bytes and is the
11//! baseline the others have to beat. `FSST`, which is a symbol table and the same lengths over
12//! compressed bytes. `DICT`, which is the distinct values and an array of codes.
13//!
14//! `DICT_FSST` from the section 6.2 table is not a fifth shape. A dictionary's entries are a string
15//! column, and encoding them goes back through the same chooser, so a dictionary whose entries are
16//! FSST compressed is what the chooser produces on its own whenever that is smaller. The same
17//! recursion gives run length encoding of strings for free, because the codes are an integer chunk
18//! and `crate::integer` already knows what to do with a column of long runs.
19//!
20//! ## Lengths, not offsets
21//!
22//! The usual layout is `n + 1` offsets and Arrow does it that way because a slice of an array has
23//! to be free. On disk the offsets are a monotonically increasing sequence whose differences are
24//! the lengths, and the differences are what compress: URL lengths in a real column are a few dozen
25//! distinct values in a narrow band, which the integer cascade turns into a handful of bits each,
26//! while the offsets themselves need enough bits to address the whole chunk. The integer cascade
27//! would find that by choosing DELTA, and storing lengths directly gets to the same place without
28//! spending a level of the cascade on it. Offsets are a prefix sum away and that is a decode time
29//! cost of one add per value.
30//!
31//! ## What is not here
32//!
33//! Nulls. A chunk here is N byte strings and an empty string is a value like any other. Validity is
34//! a bitmap that belongs to the column rather than to the encoding, per `spec/05-storage.md`, and
35//! `ROARING` in the section 6.2 table is what encodes it.
36//!
37//! Shared symbol tables and shared dictionaries across columns, which are section 6.4 and are the
38//! measurement this milestone exists for. Everything here is one column on its own, which is the
39//! baseline they get compared against.
40
41use rudb_common::{Error, Result};
42
43use crate::fsst::SymbolTable;
44use crate::integer;
45use crate::reader::Reader;
46
47/// How deep the recursion goes. A dictionary of a dictionary is not a thing, so this only has to
48/// stop the dictionary's own entries from being dictionary encoded again.
49const MAX_DEPTH: u8 = 2;
50
51/// How many bytes of a column the symbol table is trained on.
52///
53/// The paper trains on about 16 KB. This is four times that, because training happens once per
54/// chunk here rather than once per block, and because the cost of a symbol that is only in the
55/// sample by accident is paid on every value in the chunk.
56pub(crate) const SAMPLE_BYTES: usize = 64 * 1024;
57
58/// What a string chunk is encoded as. The discriminant is the tag byte and is part of the format.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Kind {
61    /// One value repeated.
62    Constant = 0,
63    /// Lengths and raw bytes.
64    Plain = 1,
65    /// Lengths, a symbol table, and FSST compressed bytes.
66    Fsst = 2,
67    /// The distinct values as a string chunk of their own, and codes into it as an integer chunk.
68    Dict = 3,
69}
70
71impl Kind {
72    fn tag(self) -> u8 {
73        self as u8
74    }
75
76    fn from_tag(tag: u8) -> Result<Self> {
77        match tag {
78            0 => Ok(Self::Constant),
79            1 => Ok(Self::Plain),
80            2 => Ok(Self::Fsst),
81            3 => Ok(Self::Dict),
82            other => Err(Error::internal(format!("unknown string encoding tag {other}"))),
83        }
84    }
85
86    /// The name that goes in a report.
87    #[must_use]
88    pub fn name(self) -> &'static str {
89        match self {
90            Self::Constant => "CONSTANT",
91            Self::Plain => "PLAIN",
92            Self::Fsst => "FSST",
93            Self::Dict => "DICT",
94        }
95    }
96}
97
98/// Encodes a chunk of strings, choosing whatever comes out smallest.
99///
100/// # Errors
101///
102/// If the chunk is longer than `u32::MAX` values, or if an encoding produces something its own
103/// decoder would not accept.
104pub fn encode(values: &[&[u8]]) -> Result<Vec<u8>> {
105    encode_at(values, 0)
106}
107
108/// Decodes a chunk that sits at the front of a longer buffer, and says how many bytes it took.
109///
110/// A column group holds one of these per column, and the decoder on that side cannot know where
111/// one ends until it has been read.
112///
113/// # Errors
114///
115/// As [`decode`], except that trailing bytes are what the caller asked about rather than an error.
116pub fn decode_prefix(bytes: &[u8]) -> Result<(Vec<Vec<u8>>, usize)> {
117    let mut reader = Reader::new(bytes);
118    let values = decode_chunk(&mut reader)?;
119    Ok((values, reader.used()))
120}
121
122/// [`describe`] over a chunk at the front of a longer buffer, and how many bytes it took.
123///
124/// # Errors
125///
126/// As [`decode_prefix`].
127pub fn describe_prefix(bytes: &[u8]) -> Result<(String, usize)> {
128    let mut reader = Reader::new(bytes);
129    let text = describe_chunk(&mut reader)?;
130    Ok((text, reader.used()))
131}
132
133/// Decodes a chunk written by [`encode`].
134///
135/// # Errors
136///
137/// If the bytes are truncated, carry an unknown tag, or describe a chunk whose parts disagree.
138pub fn decode(bytes: &[u8]) -> Result<Vec<Vec<u8>>> {
139    let mut reader = Reader::new(bytes);
140    let values = decode_chunk(&mut reader)?;
141    if reader.remaining() != 0 {
142        return Err(Error::internal(format!(
143            "{} bytes left over after decoding a string chunk",
144            reader.remaining()
145        )));
146    }
147    Ok(values)
148}
149
150/// The size of every candidate that applies, for a report that wants to say what was chosen over
151/// what.
152///
153/// # Errors
154///
155/// As [`encode`].
156pub fn candidate_sizes(values: &[&[u8]]) -> Result<Vec<(Kind, usize)>> {
157    let mut sizes = Vec::new();
158    for kind in candidates(values, 0) {
159        if let Some(bytes) = encode_as(kind, values, 0)? {
160            sizes.push((kind, bytes.len()));
161        }
162    }
163    Ok(sizes)
164}
165
166/// The shape a chunk was encoded as, as a line of text like `DICT(FSST, RLE(...))`.
167///
168/// # Errors
169///
170/// As [`decode`].
171pub fn describe(bytes: &[u8]) -> Result<String> {
172    let mut reader = Reader::new(bytes);
173    describe_chunk(&mut reader)
174}
175
176fn encode_at(values: &[&[u8]], depth: u8) -> Result<Vec<u8>> {
177    let mut best: Option<Vec<u8>> = None;
178    for kind in candidates(values, depth) {
179        let Some(bytes) = encode_as(kind, values, depth)? else {
180            continue;
181        };
182        if best.as_ref().is_none_or(|current| bytes.len() < current.len()) {
183            best = Some(bytes);
184        }
185    }
186    best.ok_or_else(|| Error::internal("no string encoding applied to the chunk"))
187}
188
189fn candidates(values: &[&[u8]], depth: u8) -> Vec<Kind> {
190    let mut kinds = vec![Kind::Plain];
191    if values.is_empty() {
192        return kinds;
193    }
194    if values.iter().all(|value| *value == values[0]) {
195        return vec![Kind::Constant];
196    }
197    kinds.push(Kind::Fsst);
198    if depth < MAX_DEPTH && distinct_values(values).len() < values.len() {
199        kinds.push(Kind::Dict);
200    }
201    kinds
202}
203
204fn encode_as(kind: Kind, values: &[&[u8]], depth: u8) -> Result<Option<Vec<u8>>> {
205    let mut out = vec![kind.tag()];
206    put_u32(&mut out, u32::try_from(values.len()).map_err(|_| too_long(values.len()))?);
207    match kind {
208        Kind::Constant => {
209            let Some(first) = values.first() else {
210                return Ok(None);
211            };
212            if values.iter().any(|value| value != first) {
213                return Ok(None);
214            }
215            put_u32(&mut out, u32::try_from(first.len()).map_err(|_| too_long(first.len()))?);
216            out.extend_from_slice(first);
217        }
218        Kind::Plain => {
219            out.extend_from_slice(&encode_lengths(values)?);
220            for value in values {
221                out.extend_from_slice(value);
222            }
223        }
224        Kind::Fsst => {
225            let sample = sample_of(values);
226            let table = SymbolTable::train(&sample);
227            if table.is_empty() {
228                return Ok(None);
229            }
230            let mut compressed = Vec::new();
231            let mut lengths = Vec::with_capacity(values.len());
232            for value in values {
233                let before = compressed.len();
234                table.compress(value, &mut compressed);
235                lengths.push((compressed.len() - before) as i64);
236            }
237            table.serialize(&mut out);
238            out.extend_from_slice(&integer::encode(&lengths)?);
239            out.extend_from_slice(&compressed);
240        }
241        Kind::Dict => {
242            let dictionary = distinct_values(values);
243            if dictionary.is_empty() {
244                return Ok(None);
245            }
246            let codes = codes_over(values, &dictionary);
247            let entries: Vec<&[u8]> = dictionary.iter().map(Vec::as_slice).collect();
248            out.extend_from_slice(&encode_at(&entries, depth + 1)?);
249            out.extend_from_slice(&integer::encode(&codes)?);
250        }
251    }
252    Ok(Some(out))
253}
254
255fn decode_chunk(reader: &mut Reader<'_>) -> Result<Vec<Vec<u8>>> {
256    let kind = Kind::from_tag(reader.u8()?)?;
257    let count = reader.u32()? as usize;
258    match kind {
259        Kind::Constant => {
260            let len = reader.u32()? as usize;
261            let value = reader.bytes(len)?.to_vec();
262            Ok(vec![value; count])
263        }
264        Kind::Plain => {
265            let lengths = decode_lengths(reader, count)?;
266            let mut values = Vec::with_capacity(count);
267            for length in lengths {
268                values.push(reader.bytes(length)?.to_vec());
269            }
270            Ok(values)
271        }
272        Kind::Fsst => {
273            let (table, used) = SymbolTable::deserialize(reader.rest())?;
274            reader.skip(used)?;
275            let lengths = decode_lengths(reader, count)?;
276            let mut values = Vec::with_capacity(count);
277            for length in lengths {
278                let compressed = reader.bytes(length)?;
279                let mut value = Vec::new();
280                table.decompress(compressed, &mut value)?;
281                values.push(value);
282            }
283            Ok(values)
284        }
285        Kind::Dict => {
286            let dictionary = decode_chunk(reader)?;
287            let codes = decode_integers(reader)?;
288            if codes.len() != count {
289                return Err(Error::internal(format!(
290                    "a dictionary chunk says it holds {count} values and has {} codes",
291                    codes.len()
292                )));
293            }
294            let mut values = Vec::with_capacity(count);
295            for code in codes {
296                let entry =
297                    usize::try_from(code).ok().and_then(|index| dictionary.get(index)).ok_or_else(
298                        || Error::internal(format!("code {code} is not in the dictionary")),
299                    )?;
300                values.push(entry.clone());
301            }
302            Ok(values)
303        }
304    }
305}
306
307fn describe_chunk(reader: &mut Reader<'_>) -> Result<String> {
308    let kind = Kind::from_tag(reader.u8()?)?;
309    let count = reader.u32()? as usize;
310    Ok(match kind {
311        Kind::Constant => {
312            let len = reader.u32()? as usize;
313            reader.bytes(len)?;
314            "CONSTANT".to_string()
315        }
316        Kind::Plain => {
317            let (shape, lengths) = describe_lengths(reader, count)?;
318            reader.skip(lengths.iter().sum())?;
319            format!("PLAIN({shape})")
320        }
321        Kind::Fsst => {
322            let (table, used) = SymbolTable::deserialize(reader.rest())?;
323            reader.skip(used)?;
324            let (shape, lengths) = describe_lengths(reader, count)?;
325            reader.skip(lengths.iter().sum())?;
326            format!("FSST[{}]({shape})", table.len())
327        }
328        Kind::Dict => {
329            let entries = describe_chunk(reader)?;
330            let codes = describe_integers(reader)?;
331            format!("DICT({entries}, {codes})")
332        }
333    })
334}
335
336/// The shape of the length array and the lengths themselves, because a describe has to walk past
337/// the payload to leave the reader where the next chunk starts and the payload size is the sum of
338/// the lengths.
339fn describe_lengths(reader: &mut Reader<'_>, count: usize) -> Result<(String, Vec<usize>)> {
340    let (shape, _) = integer::describe_prefix(reader.rest())?;
341    let lengths = decode_lengths(reader, count)?;
342    Ok((shape, lengths))
343}
344
345fn encode_lengths(values: &[&[u8]]) -> Result<Vec<u8>> {
346    let lengths: Vec<i64> = values.iter().map(|value| value.len() as i64).collect();
347    integer::encode(&lengths)
348}
349
350fn decode_lengths(reader: &mut Reader<'_>, count: usize) -> Result<Vec<usize>> {
351    let lengths = decode_integers(reader)?;
352    if lengths.len() != count {
353        return Err(Error::internal(format!(
354            "a string chunk says it holds {count} values and has {} lengths",
355            lengths.len()
356        )));
357    }
358    lengths
359        .into_iter()
360        .map(|length| {
361            usize::try_from(length).map_err(|_| Error::internal("a negative string length"))
362        })
363        .collect()
364}
365
366/// Reads one nested integer chunk. The integer decoder wants a slice of exactly its own chunk and
367/// the reader does not know how long that is, so it decodes from the rest of the buffer and is told
368/// afterwards how much it used.
369fn decode_integers(reader: &mut Reader<'_>) -> Result<Vec<i64>> {
370    let (values, used) = integer::decode_prefix(reader.rest())?;
371    reader.skip(used)?;
372    Ok(values)
373}
374
375fn describe_integers(reader: &mut Reader<'_>) -> Result<String> {
376    let (text, used) = integer::describe_prefix(reader.rest())?;
377    reader.skip(used)?;
378    Ok(text)
379}
380
381/// A sample of the column spread across the whole of it, taken at random skips rather than at a
382/// fixed stride.
383///
384/// Section 6.3 makes the point about choosing an encoding from a sample and it applies at least as
385/// much to training a symbol table. Column data is frequently sorted or clustered, so the first
386/// 64 KB of a URL column is the hosts that sort first and a table trained on it escapes most of the
387/// rest of the column.
388///
389/// The skips are random rather than fixed because a fixed stride aliases. Column data is also
390/// frequently periodic, and a stride that shares a factor with the period samples one phase of it
391/// and never sees the others. That is not a hypothetical: the first version of this took every
392/// `n`th value, and on a test column whose values cycle with a period that the stride happened to
393/// divide, the table it trained was 3.4 times worse than one trained on the whole column, because
394/// it learned eight byte symbols that only line up with the phase it saw and had no shorter symbols
395/// left to fall back on.
396///
397/// The generator is a fixed seed xorshift, so the sample is a function of the column and encoding
398/// the same values twice produces the same bytes.
399pub(crate) fn sample_of<'a>(values: &[&'a [u8]]) -> Vec<&'a [u8]> {
400    sample_bytes_of(values, SAMPLE_BYTES)
401}
402
403/// [`sample_of`] with the byte budget spelled out, for a caller training one table over several
404/// columns that has to split the budget between them.
405pub(crate) fn sample_bytes_of<'a>(values: &[&'a [u8]], budget: usize) -> Vec<&'a [u8]> {
406    let budget = budget.max(1);
407    let total: usize = values.iter().map(|value| value.len()).sum();
408    if total <= budget {
409        return values.to_vec();
410    }
411    let stride = total.div_ceil(budget).max(1);
412    let span = (stride * 2 - 1).max(1) as u64;
413    let mut state = 0x2545_f491_4f6c_dd1du64;
414    let mut sample = Vec::with_capacity(values.len() / stride + 1);
415    let mut at = 0usize;
416    while at < values.len() {
417        sample.push(values[at]);
418        state ^= state << 13;
419        state ^= state >> 7;
420        state ^= state << 17;
421        at += 1 + (state % span) as usize;
422    }
423    sample
424}
425
426/// The distinct values in sorted order, for the same reason the integer dictionary is sorted: an
427/// ordered dictionary turns a range predicate into a code range rather than a code set.
428fn distinct_values(values: &[&[u8]]) -> Vec<Vec<u8>> {
429    let mut distinct: Vec<Vec<u8>> = values.iter().map(|value| value.to_vec()).collect();
430    distinct.sort_unstable();
431    distinct.dedup();
432    distinct
433}
434
435fn codes_over(values: &[&[u8]], dictionary: &[Vec<u8>]) -> Vec<i64> {
436    values
437        .iter()
438        .map(|value| {
439            dictionary
440                .binary_search_by(|entry| entry.as_slice().cmp(value))
441                .expect("the dictionary is the distinct values of this chunk") as i64
442        })
443        .collect()
444}
445
446fn too_long(len: usize) -> Error {
447    Error::internal(format!("a string chunk of {len} is longer than the format allows"))
448}
449
450fn put_u32(out: &mut Vec<u8>, value: u32) {
451    out.extend_from_slice(&value.to_le_bytes());
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    fn urls(count: usize) -> Vec<Vec<u8>> {
459        let hosts = ["www.example.com", "shop.example.com", "news.other.example.org"];
460        let paths = ["/index.html", "/catalog/item", "/search", "/user/profile/settings"];
461        (0..count)
462            .map(|index| {
463                let host = hosts[index % hosts.len()];
464                let path = paths[(index / 3) % paths.len()];
465                format!("http://{host}{path}?session={}&ref=google", index * 7).into_bytes()
466            })
467            .collect()
468    }
469
470    fn borrow(values: &[Vec<u8>]) -> Vec<&[u8]> {
471        values.iter().map(Vec::as_slice).collect()
472    }
473
474    fn round_trip(values: &[Vec<u8>]) -> Vec<u8> {
475        let borrowed = borrow(values);
476        let bytes = encode(&borrowed).unwrap();
477        let back = decode(&bytes).unwrap();
478        assert_eq!(back, values, "{}", describe(&bytes).unwrap());
479        bytes
480    }
481
482    fn kind_of(bytes: &[u8]) -> Kind {
483        Kind::from_tag(bytes[0]).unwrap()
484    }
485
486    fn raw_size(values: &[Vec<u8>]) -> usize {
487        values.iter().map(Vec::len).sum::<usize>() + values.len() * 4
488    }
489
490    #[test]
491    fn an_empty_chunk_round_trips() {
492        let bytes = round_trip(&[]);
493        assert_eq!(kind_of(&bytes), Kind::Plain);
494    }
495
496    #[test]
497    fn a_constant_column_costs_what_one_value_costs() {
498        let values = vec![b"https://www.example.com/".to_vec(); 100_000];
499        let bytes = round_trip(&values);
500        assert_eq!(kind_of(&bytes), Kind::Constant);
501        assert_eq!(bytes.len(), 9 + 24);
502    }
503
504    #[test]
505    fn a_url_column_of_unique_values_uses_fsst() {
506        // Every value distinct, so a dictionary is the values plus an index and cannot win. This is
507        // the shape of `WatchID` and of the high cardinality end of `URL`, which section 6.5 says
508        // falls back to FSST only.
509        let values = urls(20_000);
510        let bytes = round_trip(&values);
511        assert_eq!(kind_of(&bytes), Kind::Fsst);
512        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
513        assert!(ratio > 5.0, "{ratio:.2}x");
514    }
515
516    #[test]
517    fn a_sample_of_a_periodic_column_learns_every_phase_of_it() {
518        // This column is periodic and its period is what a fixed stride would have divided. The
519        // sample has to see all of it, because a table trained on one phase learns eight byte
520        // symbols that only line up with that phase and has nothing shorter to fall back on. The
521        // measured cost of getting this wrong was 3.4 times the compressed size.
522        let values = urls(20_000);
523        let borrowed = borrow(&values);
524        let sample = sample_of(&borrowed);
525        let mut phases: Vec<&[u8]> = sample
526            .iter()
527            .map(|value| {
528                let query =
529                    value.iter().position(|byte| *byte == b'?').expect("every value has a query");
530                &value[..query]
531            })
532            .collect();
533        phases.sort_unstable();
534        phases.dedup();
535        // Three hosts and four paths, and the sample has to contain all twelve of the combinations.
536        assert_eq!(phases.len(), 12);
537        let whole = SymbolTable::train(&borrowed);
538        let sampled = SymbolTable::train(&sample);
539        let mut on_whole = Vec::new();
540        let mut on_sample = Vec::new();
541        for value in &borrowed {
542            whole.compress(value, &mut on_whole);
543            sampled.compress(value, &mut on_sample);
544        }
545        // Training on a twentieth of the column is allowed to cost something. It is not allowed to
546        // cost a factor.
547        assert!(
548            on_sample.len() < on_whole.len() * 5 / 4,
549            "{} against {}",
550            on_sample.len(),
551            on_whole.len()
552        );
553    }
554
555    #[test]
556    fn a_repeating_column_becomes_a_dictionary_of_compressed_entries() {
557        // The DICT_FSST row of the section 6.2 table, which is not a fifth encoding here: it is a
558        // dictionary whose entries went back through the chooser and came out as FSST.
559        let distinct = urls(500);
560        let values: Vec<Vec<u8>> =
561            (0..50_000).map(|index| distinct[index * 7919 % distinct.len()].clone()).collect();
562        let bytes = round_trip(&values);
563        assert_eq!(kind_of(&bytes), Kind::Dict);
564        let shape = describe(&bytes).unwrap();
565        assert!(shape.starts_with("DICT(FSST"), "{shape}");
566        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
567        assert!(ratio > 20.0, "{ratio:.2}x, {shape}");
568    }
569
570    #[test]
571    fn a_column_of_long_runs_costs_almost_nothing() {
572        // A dictionary makes the codes an integer chunk, and the integer chunk knows what to do
573        // with runs, so run length encoding of strings falls out of the recursion.
574        let distinct = urls(50);
575        let mut values = Vec::new();
576        for entry in &distinct {
577            values.extend(std::iter::repeat_n(entry.clone(), 1000));
578        }
579        let bytes = round_trip(&values);
580        let shape = describe(&bytes).unwrap();
581        assert!(shape.contains("RLE"), "{shape}");
582        assert!(bytes.len() < 2000, "{} bytes: {shape}", bytes.len());
583    }
584
585    #[test]
586    fn incompressible_strings_stay_close_to_their_own_size() {
587        // The case where nothing works. It has to land on PLAIN or on an FSST that is not much
588        // worse, rather than on a dictionary of every value in the column.
589        let mut state = 0x2545_f491_4f6c_dd1du64;
590        let values: Vec<Vec<u8>> = (0..2000)
591            .map(|_| {
592                (0..32)
593                    .map(|_| {
594                        state ^= state << 13;
595                        state ^= state >> 7;
596                        state ^= state << 17;
597                        state as u8
598                    })
599                    .collect()
600            })
601            .collect();
602        let bytes = round_trip(&values);
603        assert!(bytes.len() < 2000 * 32 + 3000, "{} bytes", bytes.len());
604    }
605
606    #[test]
607    fn lengths_are_stored_rather_than_offsets() {
608        // Every value is 24 bytes, so the lengths are a constant chunk and cost 13 bytes for the
609        // whole column. Offsets would be 100,000 increasing integers.
610        let values: Vec<Vec<u8>> =
611            (0..100_000).map(|index| format!("{index:024}").into_bytes()).collect();
612        let borrowed = borrow(&values);
613        let bytes = encode_as(Kind::Plain, &borrowed, 0).unwrap().unwrap();
614        assert_eq!(bytes.len(), 5 + 13 + 100_000 * 24);
615    }
616
617    #[test]
618    fn empty_strings_are_values_and_not_nulls() {
619        let values = vec![Vec::new(), b"a".to_vec(), Vec::new(), b"bb".to_vec()];
620        round_trip(&values);
621    }
622
623    #[test]
624    fn a_chunk_with_one_value_round_trips() {
625        round_trip(&[b"only".to_vec()]);
626    }
627
628    #[test]
629    fn every_candidate_that_applies_decodes_to_the_input() {
630        let values = urls(3000);
631        let borrowed = borrow(&values);
632        let applicable = candidates(&borrowed, 0);
633        assert!(applicable.len() >= 2, "{applicable:?}");
634        for kind in applicable {
635            let bytes = encode_as(kind, &borrowed, 0).unwrap().unwrap();
636            assert_eq!(decode(&bytes).unwrap(), values, "{}", kind.name());
637        }
638    }
639
640    #[test]
641    fn the_chooser_picks_the_smallest_candidate() {
642        let values = urls(2000);
643        let borrowed = borrow(&values);
644        let chosen = encode(&borrowed).unwrap();
645        for (_, size) in candidate_sizes(&borrowed).unwrap() {
646            assert!(chosen.len() <= size);
647        }
648    }
649
650    #[test]
651    fn a_truncated_chunk_is_an_error_and_not_a_panic() {
652        let values = urls(40);
653        let bytes = encode(&borrow(&values)).unwrap();
654        for len in 0..bytes.len() {
655            assert!(decode(&bytes[..len]).is_err(), "{len} bytes decoded");
656        }
657    }
658
659    #[test]
660    fn trailing_bytes_are_an_error() {
661        let mut bytes = encode(&borrow(&urls(10))).unwrap();
662        bytes.push(0);
663        let error = decode(&bytes).unwrap_err();
664        assert!(error.message().contains("left over"), "{error}");
665    }
666
667    #[test]
668    fn an_unknown_tag_is_an_error() {
669        let error = decode(&[99, 0, 0, 0, 0]).unwrap_err();
670        assert!(error.message().contains("unknown string encoding tag"), "{error}");
671    }
672
673    #[test]
674    fn a_dictionary_code_outside_the_dictionary_is_an_error() {
675        let mut bytes = vec![Kind::Dict.tag()];
676        put_u32(&mut bytes, 1);
677        bytes.extend_from_slice(&encode(&[b"one".as_slice()]).unwrap());
678        bytes.extend_from_slice(&integer::encode(&[9]).unwrap());
679        let error = decode(&bytes).unwrap_err();
680        assert!(error.message().contains("not in the dictionary"), "{error}");
681    }
682
683    #[test]
684    fn a_negative_length_is_an_error() {
685        let mut bytes = vec![Kind::Plain.tag()];
686        put_u32(&mut bytes, 1);
687        bytes.extend_from_slice(&integer::encode(&[-1]).unwrap());
688        let error = decode(&bytes).unwrap_err();
689        assert!(error.message().contains("negative string length"), "{error}");
690    }
691
692    #[test]
693    fn the_sample_is_spread_across_the_chunk_and_not_taken_from_the_front() {
694        // A sorted column whose first 64 KB says nothing about the rest of it. If the sample were
695        // the front, the table would learn `aaaa` and escape every `zzzz`.
696        let mut values: Vec<Vec<u8>> = Vec::new();
697        for index in 0..20_000 {
698            let head = if index < 10_000 { "aaaaaaaaaaaaaaaa" } else { "zzzzzzzzzzzzzzzz" };
699            values.push(format!("{head}/{index:08}").into_bytes());
700        }
701        let borrowed = borrow(&values);
702        let sample = sample_of(&borrowed);
703        let first_half = sample.iter().filter(|value| value.starts_with(b"aaaa")).count();
704        let second_half = sample.len() - first_half;
705        assert!(first_half > 0 && second_half > 0, "{first_half} and {second_half}");
706        let bytes = round_trip(&values);
707        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
708        assert!(ratio > 4.0, "{ratio:.2}x");
709    }
710}