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 five 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. `FRONT`, which is
13//! the length of the prefix each value shares with the one before it and the rest of the value.
14//!
15//! `DICT_FSST` from the section 6.2 table is not a sixth shape. A dictionary's entries are a string
16//! column, and encoding them goes back through the same chooser, so a dictionary whose entries are
17//! FSST compressed is what the chooser produces on its own whenever that is smaller. The same
18//! recursion gives run length encoding of strings for free, because the codes are an integer chunk
19//! and `crate::integer` already knows what to do with a column of long runs.
20//!
21//! ## Why front coding is here
22//!
23//! The whole file measurement in M1 says the chooser produces 11.65 GB for `hits` against Parquet's
24//! 13.76 GB, and that `URL`, `Referer` and `OriginalURL` are 6.11 GB of it, and that on those three
25//! the chooser loses to Parquet's Snappy. The shape it picked on all three was `DICT(FSST[255])`,
26//! so the cascade was working and FSST was still losing.
27//!
28//! The reason is structural. FSST compresses each value on its own against a 255 symbol table, and
29//! a block compressor has the previous few kilobytes of the page to point back into. Two URLs that
30//! share a host and half a path are most of a back reference to each other and are nothing at all
31//! to a symbol table, which can only spend eight bytes of a symbol on the part they share and has
32//! to spend it again on every value. On a sorted dictionary of URLs the value before is the closest
33//! thing in the column to the value in hand, and the bytes they share are the redundancy Snappy was
34//! finding. Front coding is what reaches those bytes, and it composes with everything else here:
35//! the suffixes it leaves behind are a string column and go back through the chooser, so
36//! `DICT(FRONT(FSST))` is a shape the chooser can arrive at without anyone naming it.
37//!
38//! The chain has no restarts, so reading entry `n` means walking from entry zero. That is the right
39//! trade while a dictionary is decoded whole, which is what `decode` does. When something wants one
40//! entry out of a dictionary without materialising the rest, the answer is a restart every so many
41//! entries, and it costs one full value per block.
42//!
43//! ## Lengths, not offsets
44//!
45//! The usual layout is `n + 1` offsets and Arrow does it that way because a slice of an array has
46//! to be free. On disk the offsets are a monotonically increasing sequence whose differences are
47//! the lengths, and the differences are what compress: URL lengths in a real column are a few dozen
48//! distinct values in a narrow band, which the integer cascade turns into a handful of bits each,
49//! while the offsets themselves need enough bits to address the whole chunk. The integer cascade
50//! would find that by choosing DELTA, and storing lengths directly gets to the same place without
51//! spending a level of the cascade on it. Offsets are a prefix sum away and that is a decode time
52//! cost of one add per value.
53//!
54//! ## What is not here
55//!
56//! Nulls. A chunk here is N byte strings and an empty string is a value like any other. Validity is
57//! a bitmap that belongs to the column rather than to the encoding, per `spec/05-storage.md`, and
58//! `ROARING` in the section 6.2 table is what encodes it.
59//!
60//! Shared symbol tables and shared dictionaries across columns, which are section 6.4 and are the
61//! measurement this milestone exists for. Everything here is one column on its own, which is the
62//! baseline they get compared against.
63
64use rudb_common::{Error, Result};
65
66use crate::fsst::SymbolTable;
67use crate::integer;
68use crate::reader::Reader;
69
70/// How deep the recursion goes. A dictionary of a dictionary is not a thing, so this only has to
71/// stop the dictionary's own entries from being dictionary encoded again.
72const MAX_DEPTH: u8 = 2;
73
74/// How little sharing between neighbours is still worth offering front coding for, as one over
75/// this. A twentieth of the column is around where the prefix lengths start paying for themselves,
76/// and below it the candidate is an encode of the whole column that loses.
77const SHARE_DIVISOR: usize = 20;
78
79/// How many bytes of a column the symbol table is trained on.
80///
81/// The paper trains on about 16 KB. This is four times that, because training happens once per
82/// chunk here rather than once per block, and because the cost of a symbol that is only in the
83/// sample by accident is paid on every value in the chunk.
84pub(crate) const SAMPLE_BYTES: usize = 64 * 1024;
85
86/// What a string chunk is encoded as. The discriminant is the tag byte and is part of the format.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum Kind {
89    /// One value repeated.
90    Constant = 0,
91    /// Lengths and raw bytes.
92    Plain = 1,
93    /// Lengths, a symbol table, and FSST compressed bytes.
94    Fsst = 2,
95    /// The distinct values as a string chunk of their own, and codes into it as an integer chunk.
96    Dict = 3,
97    /// Shared prefix lengths as an integer chunk, and what is left of each value as a string chunk.
98    Front = 4,
99}
100
101impl Kind {
102    fn tag(self) -> u8 {
103        self as u8
104    }
105
106    fn from_tag(tag: u8) -> Result<Self> {
107        match tag {
108            0 => Ok(Self::Constant),
109            1 => Ok(Self::Plain),
110            2 => Ok(Self::Fsst),
111            3 => Ok(Self::Dict),
112            4 => Ok(Self::Front),
113            other => Err(Error::internal(format!("unknown string encoding tag {other}"))),
114        }
115    }
116
117    /// The name that goes in a report.
118    #[must_use]
119    pub fn name(self) -> &'static str {
120        match self {
121            Self::Constant => "CONSTANT",
122            Self::Plain => "PLAIN",
123            Self::Fsst => "FSST",
124            Self::Dict => "DICT",
125            Self::Front => "FRONT",
126        }
127    }
128}
129
130/// Encodes a chunk of strings, choosing whatever comes out smallest.
131///
132/// # Errors
133///
134/// If the chunk is longer than `u32::MAX` values, or if an encoding produces something its own
135/// decoder would not accept.
136pub fn encode(values: &[&[u8]]) -> Result<Vec<u8>> {
137    encode_at(values, 0)
138}
139
140/// Decodes a chunk that sits at the front of a longer buffer, and says how many bytes it took.
141///
142/// A column group holds one of these per column, and the decoder on that side cannot know where
143/// one ends until it has been read.
144///
145/// # Errors
146///
147/// As [`decode`], except that trailing bytes are what the caller asked about rather than an error.
148pub fn decode_prefix(bytes: &[u8]) -> Result<(Vec<Vec<u8>>, usize)> {
149    let mut reader = Reader::new(bytes);
150    let values = decode_chunk(&mut reader)?;
151    Ok((values, reader.used()))
152}
153
154/// [`describe`] over a chunk at the front of a longer buffer, and how many bytes it took.
155///
156/// # Errors
157///
158/// As [`decode_prefix`].
159pub fn describe_prefix(bytes: &[u8]) -> Result<(String, usize)> {
160    let mut reader = Reader::new(bytes);
161    let text = describe_chunk(&mut reader)?;
162    Ok((text, reader.used()))
163}
164
165/// Decodes a chunk written by [`encode`].
166///
167/// # Errors
168///
169/// If the bytes are truncated, carry an unknown tag, or describe a chunk whose parts disagree.
170pub fn decode(bytes: &[u8]) -> Result<Vec<Vec<u8>>> {
171    let mut reader = Reader::new(bytes);
172    let values = decode_chunk(&mut reader)?;
173    if reader.remaining() != 0 {
174        return Err(Error::internal(format!(
175            "{} bytes left over after decoding a string chunk",
176            reader.remaining()
177        )));
178    }
179    Ok(values)
180}
181
182/// The size of every candidate that applies, for a report that wants to say what was chosen over
183/// what.
184///
185/// # Errors
186///
187/// As [`encode`].
188pub fn candidate_sizes(values: &[&[u8]]) -> Result<Vec<(Kind, usize)>> {
189    let mut sizes = Vec::new();
190    for kind in candidates(values, 0) {
191        if let Some(bytes) = encode_as(kind, values, 0)? {
192            sizes.push((kind, bytes.len()));
193        }
194    }
195    Ok(sizes)
196}
197
198/// The shape a chunk was encoded as, as a line of text like `DICT(FSST, RLE(...))`.
199///
200/// # Errors
201///
202/// As [`decode`].
203pub fn describe(bytes: &[u8]) -> Result<String> {
204    let mut reader = Reader::new(bytes);
205    describe_chunk(&mut reader)
206}
207
208fn encode_at(values: &[&[u8]], depth: u8) -> Result<Vec<u8>> {
209    let mut best: Option<Vec<u8>> = None;
210    for kind in candidates(values, depth) {
211        let Some(bytes) = encode_as(kind, values, depth)? else {
212            continue;
213        };
214        if best.as_ref().is_none_or(|current| bytes.len() < current.len()) {
215            best = Some(bytes);
216        }
217    }
218    best.ok_or_else(|| Error::internal("no string encoding applied to the chunk"))
219}
220
221fn candidates(values: &[&[u8]], depth: u8) -> Vec<Kind> {
222    let mut kinds = vec![Kind::Plain];
223    if values.is_empty() {
224        return kinds;
225    }
226    if values.iter().all(|value| *value == values[0]) {
227        return vec![Kind::Constant];
228    }
229    kinds.push(Kind::Fsst);
230    if depth < MAX_DEPTH && distinct_values(values).len() < values.len() {
231        kinds.push(Kind::Dict);
232    }
233    if depth < MAX_DEPTH && sharing_of(values) >= total_len(values) / SHARE_DIVISOR {
234        kinds.push(Kind::Front);
235    }
236    kinds
237}
238
239/// How many bytes each value shares with the value before it, added up.
240///
241/// This is a full pass over the column, and it is here rather than on a sample because it is byte
242/// comparisons that stop at the first difference, which on a column with nothing to share stops
243/// immediately. Against training a symbol table and compressing the whole column, which is what
244/// offering the candidate would cost, it is not worth sampling.
245fn sharing_of(values: &[&[u8]]) -> usize {
246    let mut shared = 0;
247    for pair in values.windows(2) {
248        shared += shared_prefix(pair[0], pair[1]);
249    }
250    shared
251}
252
253/// Every value split into the bytes it shares with the value before it and the bytes it does not.
254///
255/// The suffixes point into the values, so this costs the prefix lengths and nothing else. It is
256/// shared with [`crate::multi`], which front codes a column before compressing it against a symbol
257/// table that belongs to the whole group.
258pub(crate) fn front_code<'a>(values: &[&'a [u8]]) -> (Vec<i64>, Vec<&'a [u8]>) {
259    let mut prefixes = Vec::with_capacity(values.len());
260    let mut suffixes: Vec<&'a [u8]> = Vec::with_capacity(values.len());
261    let mut previous: &[u8] = b"";
262    for value in values {
263        let value: &'a [u8] = value;
264        let shared = shared_prefix(previous, value);
265        prefixes.push(shared as i64);
266        suffixes.push(&value[shared..]);
267        previous = value;
268    }
269    (prefixes, suffixes)
270}
271
272/// The other half. The suffixes are consumed because the values are built out of them.
273///
274/// # Errors
275///
276/// If a prefix is negative or is longer than the value it is a prefix of, which is what a corrupt
277/// or hand written chunk looks like from here.
278pub(crate) fn front_decode(prefixes: &[i64], suffixes: Vec<Vec<u8>>) -> Result<Vec<Vec<u8>>> {
279    let mut values: Vec<Vec<u8>> = Vec::with_capacity(suffixes.len());
280    for (index, suffix) in suffixes.into_iter().enumerate() {
281        let shared = usize::try_from(prefixes[index])
282            .map_err(|_| Error::internal("a negative shared prefix length"))?;
283        let previous: &[u8] = if index == 0 { b"" } else { &values[index - 1] };
284        if shared > previous.len() {
285            return Err(Error::internal(format!(
286                "a value shares {shared} bytes with a value {} bytes long",
287                previous.len()
288            )));
289        }
290        let mut value = Vec::with_capacity(shared + suffix.len());
291        value.extend_from_slice(&previous[..shared]);
292        value.extend_from_slice(&suffix);
293        values.push(value);
294    }
295    Ok(values)
296}
297
298fn shared_prefix(previous: &[u8], value: &[u8]) -> usize {
299    let limit = previous.len().min(value.len());
300    let mut shared = 0;
301    while shared < limit && previous[shared] == value[shared] {
302        shared += 1;
303    }
304    shared
305}
306
307fn total_len(values: &[&[u8]]) -> usize {
308    values.iter().map(|value| value.len()).sum()
309}
310
311fn encode_as(kind: Kind, values: &[&[u8]], depth: u8) -> Result<Option<Vec<u8>>> {
312    let mut out = vec![kind.tag()];
313    put_u32(&mut out, u32::try_from(values.len()).map_err(|_| too_long(values.len()))?);
314    match kind {
315        Kind::Constant => {
316            let Some(first) = values.first() else {
317                return Ok(None);
318            };
319            if values.iter().any(|value| value != first) {
320                return Ok(None);
321            }
322            put_u32(&mut out, u32::try_from(first.len()).map_err(|_| too_long(first.len()))?);
323            out.extend_from_slice(first);
324        }
325        Kind::Plain => {
326            out.extend_from_slice(&encode_lengths(values)?);
327            for value in values {
328                out.extend_from_slice(value);
329            }
330        }
331        Kind::Fsst => {
332            let sample = sample_of(values);
333            let table = SymbolTable::train(&sample);
334            if table.is_empty() {
335                return Ok(None);
336            }
337            let mut compressed = Vec::new();
338            let mut lengths = Vec::with_capacity(values.len());
339            for value in values {
340                let before = compressed.len();
341                table.compress(value, &mut compressed);
342                lengths.push((compressed.len() - before) as i64);
343            }
344            table.serialize(&mut out);
345            out.extend_from_slice(&integer::encode(&lengths)?);
346            out.extend_from_slice(&compressed);
347        }
348        Kind::Dict => {
349            let dictionary = distinct_values(values);
350            if dictionary.is_empty() {
351                return Ok(None);
352            }
353            let codes = codes_over(values, &dictionary);
354            let entries: Vec<&[u8]> = dictionary.iter().map(Vec::as_slice).collect();
355            out.extend_from_slice(&encode_at(&entries, depth + 1)?);
356            out.extend_from_slice(&integer::encode(&codes)?);
357        }
358        Kind::Front => {
359            let (prefixes, suffixes) = front_code(values);
360            out.extend_from_slice(&integer::encode(&prefixes)?);
361            out.extend_from_slice(&encode_at(&suffixes, depth + 1)?);
362        }
363    }
364    Ok(Some(out))
365}
366
367fn decode_chunk(reader: &mut Reader<'_>) -> Result<Vec<Vec<u8>>> {
368    let kind = Kind::from_tag(reader.u8()?)?;
369    let count = reader.u32()? as usize;
370    match kind {
371        Kind::Constant => {
372            let len = reader.u32()? as usize;
373            let value = reader.bytes(len)?.to_vec();
374            Ok(vec![value; count])
375        }
376        Kind::Plain => {
377            let lengths = decode_lengths(reader, count)?;
378            let mut values = Vec::with_capacity(count);
379            for length in lengths {
380                values.push(reader.bytes(length)?.to_vec());
381            }
382            Ok(values)
383        }
384        Kind::Fsst => {
385            let (table, used) = SymbolTable::deserialize(reader.rest())?;
386            reader.skip(used)?;
387            let lengths = decode_lengths(reader, count)?;
388            let mut values = Vec::with_capacity(count);
389            for length in lengths {
390                let compressed = reader.bytes(length)?;
391                let mut value = Vec::new();
392                table.decompress(compressed, &mut value)?;
393                values.push(value);
394            }
395            Ok(values)
396        }
397        Kind::Dict => {
398            let dictionary = decode_chunk(reader)?;
399            let codes = decode_integers(reader)?;
400            if codes.len() != count {
401                return Err(Error::internal(format!(
402                    "a dictionary chunk says it holds {count} values and has {} codes",
403                    codes.len()
404                )));
405            }
406            let mut values = Vec::with_capacity(count);
407            for code in codes {
408                let entry =
409                    usize::try_from(code).ok().and_then(|index| dictionary.get(index)).ok_or_else(
410                        || Error::internal(format!("code {code} is not in the dictionary")),
411                    )?;
412                values.push(entry.clone());
413            }
414            Ok(values)
415        }
416        Kind::Front => {
417            let prefixes = decode_integers(reader)?;
418            let suffixes = decode_chunk(reader)?;
419            if prefixes.len() != count || suffixes.len() != count {
420                return Err(Error::internal(format!(
421                    "a front coded chunk says it holds {count} values and has {} prefixes and {} suffixes",
422                    prefixes.len(),
423                    suffixes.len()
424                )));
425            }
426            front_decode(&prefixes, suffixes)
427        }
428    }
429}
430
431fn describe_chunk(reader: &mut Reader<'_>) -> Result<String> {
432    let kind = Kind::from_tag(reader.u8()?)?;
433    let count = reader.u32()? as usize;
434    Ok(match kind {
435        Kind::Constant => {
436            let len = reader.u32()? as usize;
437            reader.bytes(len)?;
438            "CONSTANT".to_string()
439        }
440        Kind::Plain => {
441            let (shape, lengths) = describe_lengths(reader, count)?;
442            reader.skip(lengths.iter().sum())?;
443            format!("PLAIN({shape})")
444        }
445        Kind::Fsst => {
446            let (table, used) = SymbolTable::deserialize(reader.rest())?;
447            reader.skip(used)?;
448            let (shape, lengths) = describe_lengths(reader, count)?;
449            reader.skip(lengths.iter().sum())?;
450            format!("FSST[{}]({shape})", table.len())
451        }
452        Kind::Dict => {
453            let entries = describe_chunk(reader)?;
454            let codes = describe_integers(reader)?;
455            format!("DICT({entries}, {codes})")
456        }
457        Kind::Front => {
458            let prefixes = describe_integers(reader)?;
459            let suffixes = describe_chunk(reader)?;
460            format!("FRONT({prefixes}, {suffixes})")
461        }
462    })
463}
464
465/// The shape of the length array and the lengths themselves, because a describe has to walk past
466/// the payload to leave the reader where the next chunk starts and the payload size is the sum of
467/// the lengths.
468fn describe_lengths(reader: &mut Reader<'_>, count: usize) -> Result<(String, Vec<usize>)> {
469    let (shape, _) = integer::describe_prefix(reader.rest())?;
470    let lengths = decode_lengths(reader, count)?;
471    Ok((shape, lengths))
472}
473
474fn encode_lengths(values: &[&[u8]]) -> Result<Vec<u8>> {
475    let lengths: Vec<i64> = values.iter().map(|value| value.len() as i64).collect();
476    integer::encode(&lengths)
477}
478
479fn decode_lengths(reader: &mut Reader<'_>, count: usize) -> Result<Vec<usize>> {
480    let lengths = decode_integers(reader)?;
481    if lengths.len() != count {
482        return Err(Error::internal(format!(
483            "a string chunk says it holds {count} values and has {} lengths",
484            lengths.len()
485        )));
486    }
487    lengths
488        .into_iter()
489        .map(|length| {
490            usize::try_from(length).map_err(|_| Error::internal("a negative string length"))
491        })
492        .collect()
493}
494
495/// Reads one nested integer chunk. The integer decoder wants a slice of exactly its own chunk and
496/// the reader does not know how long that is, so it decodes from the rest of the buffer and is told
497/// afterwards how much it used.
498fn decode_integers(reader: &mut Reader<'_>) -> Result<Vec<i64>> {
499    let (values, used) = integer::decode_prefix(reader.rest())?;
500    reader.skip(used)?;
501    Ok(values)
502}
503
504fn describe_integers(reader: &mut Reader<'_>) -> Result<String> {
505    let (text, used) = integer::describe_prefix(reader.rest())?;
506    reader.skip(used)?;
507    Ok(text)
508}
509
510/// A sample of the column spread across the whole of it, taken at random skips rather than at a
511/// fixed stride.
512///
513/// Section 6.3 makes the point about choosing an encoding from a sample and it applies at least as
514/// much to training a symbol table. Column data is frequently sorted or clustered, so the first
515/// 64 KB of a URL column is the hosts that sort first and a table trained on it escapes most of the
516/// rest of the column.
517///
518/// The skips are random rather than fixed because a fixed stride aliases. Column data is also
519/// frequently periodic, and a stride that shares a factor with the period samples one phase of it
520/// and never sees the others. That is not a hypothetical: the first version of this took every
521/// `n`th value, and on a test column whose values cycle with a period that the stride happened to
522/// divide, the table it trained was 3.4 times worse than one trained on the whole column, because
523/// it learned eight byte symbols that only line up with the phase it saw and had no shorter symbols
524/// left to fall back on.
525///
526/// The generator is a fixed seed xorshift, so the sample is a function of the column and encoding
527/// the same values twice produces the same bytes.
528pub(crate) fn sample_of<'a>(values: &[&'a [u8]]) -> Vec<&'a [u8]> {
529    sample_bytes_of(values, SAMPLE_BYTES)
530}
531
532/// [`sample_of`] with the byte budget spelled out, for a caller training one table over several
533/// columns that has to split the budget between them.
534pub(crate) fn sample_bytes_of<'a>(values: &[&'a [u8]], budget: usize) -> Vec<&'a [u8]> {
535    let budget = budget.max(1);
536    let total: usize = values.iter().map(|value| value.len()).sum();
537    if total <= budget {
538        return values.to_vec();
539    }
540    let stride = total.div_ceil(budget).max(1);
541    let span = (stride * 2 - 1).max(1) as u64;
542    let mut state = 0x2545_f491_4f6c_dd1du64;
543    let mut sample = Vec::with_capacity(values.len() / stride + 1);
544    let mut at = 0usize;
545    while at < values.len() {
546        sample.push(values[at]);
547        state ^= state << 13;
548        state ^= state >> 7;
549        state ^= state << 17;
550        at += 1 + (state % span) as usize;
551    }
552    sample
553}
554
555/// The distinct values in sorted order, for the same reason the integer dictionary is sorted: an
556/// ordered dictionary turns a range predicate into a code range rather than a code set.
557fn distinct_values(values: &[&[u8]]) -> Vec<Vec<u8>> {
558    let mut distinct: Vec<Vec<u8>> = values.iter().map(|value| value.to_vec()).collect();
559    distinct.sort_unstable();
560    distinct.dedup();
561    distinct
562}
563
564fn codes_over(values: &[&[u8]], dictionary: &[Vec<u8>]) -> Vec<i64> {
565    values
566        .iter()
567        .map(|value| {
568            dictionary
569                .binary_search_by(|entry| entry.as_slice().cmp(value))
570                .expect("the dictionary is the distinct values of this chunk") as i64
571        })
572        .collect()
573}
574
575fn too_long(len: usize) -> Error {
576    Error::internal(format!("a string chunk of {len} is longer than the format allows"))
577}
578
579fn put_u32(out: &mut Vec<u8>, value: u32) {
580    out.extend_from_slice(&value.to_le_bytes());
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    fn urls(count: usize) -> Vec<Vec<u8>> {
588        let hosts = ["www.example.com", "shop.example.com", "news.other.example.org"];
589        let paths = ["/index.html", "/catalog/item", "/search", "/user/profile/settings"];
590        (0..count)
591            .map(|index| {
592                let host = hosts[index % hosts.len()];
593                let path = paths[(index / 3) % paths.len()];
594                format!("http://{host}{path}?session={}&ref=google", index * 7).into_bytes()
595            })
596            .collect()
597    }
598
599    /// The same values with a scrambled identifier stuck on the front of each, for the tests that
600    /// need neighbouring values to have nothing in common. Shuffling the order is not enough,
601    /// because two URLs picked at random still agree on a scheme and often on a host.
602    fn keyed(values: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
603        values
604            .into_iter()
605            .enumerate()
606            .map(|(index, value)| {
607                let key = (index as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15) % 1_000_000_007;
608                let mut out = format!("{key:010}/").into_bytes();
609                out.extend_from_slice(&value);
610                out
611            })
612            .collect()
613    }
614
615    fn borrow(values: &[Vec<u8>]) -> Vec<&[u8]> {
616        values.iter().map(Vec::as_slice).collect()
617    }
618
619    fn round_trip(values: &[Vec<u8>]) -> Vec<u8> {
620        let borrowed = borrow(values);
621        let bytes = encode(&borrowed).unwrap();
622        let back = decode(&bytes).unwrap();
623        assert_eq!(back, values, "{}", describe(&bytes).unwrap());
624        bytes
625    }
626
627    fn kind_of(bytes: &[u8]) -> Kind {
628        Kind::from_tag(bytes[0]).unwrap()
629    }
630
631    fn raw_size(values: &[Vec<u8>]) -> usize {
632        values.iter().map(Vec::len).sum::<usize>() + values.len() * 4
633    }
634
635    #[test]
636    fn an_empty_chunk_round_trips() {
637        let bytes = round_trip(&[]);
638        assert_eq!(kind_of(&bytes), Kind::Plain);
639    }
640
641    #[test]
642    fn a_constant_column_costs_what_one_value_costs() {
643        let values = vec![b"https://www.example.com/".to_vec(); 100_000];
644        let bytes = round_trip(&values);
645        assert_eq!(kind_of(&bytes), Kind::Constant);
646        assert_eq!(bytes.len(), 9 + 24);
647    }
648
649    #[test]
650    fn a_url_column_of_unique_values_uses_fsst() {
651        // Every value distinct, so a dictionary is the values plus an index and cannot win, and
652        // every value starts with an identifier of its own, so neighbours share nothing and front
653        // coding cannot win either. What is left is a column with a lot of repeated vocabulary in
654        // it and no structure that anything but a symbol table can reach. Section 6.5 says the high
655        // cardinality end of `URL` falls back to FSST only and this is that case.
656        let values = keyed(urls(20_000));
657        let bytes = round_trip(&values);
658        assert_eq!(kind_of(&bytes), Kind::Fsst);
659        // Eleven bytes of every value are the identifier and a separator and nothing compresses
660        // them, so the ratio here is lower than the one FSST gets on the URLs on their own.
661        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
662        assert!(ratio > 4.0, "{ratio:.2}x");
663    }
664
665    #[test]
666    fn a_sample_of_a_periodic_column_learns_every_phase_of_it() {
667        // This column is periodic and its period is what a fixed stride would have divided. The
668        // sample has to see all of it, because a table trained on one phase learns eight byte
669        // symbols that only line up with that phase and has nothing shorter to fall back on. The
670        // measured cost of getting this wrong was 3.4 times the compressed size.
671        let values = urls(20_000);
672        let borrowed = borrow(&values);
673        let sample = sample_of(&borrowed);
674        let mut phases: Vec<&[u8]> = sample
675            .iter()
676            .map(|value| {
677                let query =
678                    value.iter().position(|byte| *byte == b'?').expect("every value has a query");
679                &value[..query]
680            })
681            .collect();
682        phases.sort_unstable();
683        phases.dedup();
684        // Three hosts and four paths, and the sample has to contain all twelve of the combinations.
685        assert_eq!(phases.len(), 12);
686        let whole = SymbolTable::train(&borrowed);
687        let sampled = SymbolTable::train(&sample);
688        let mut on_whole = Vec::new();
689        let mut on_sample = Vec::new();
690        for value in &borrowed {
691            whole.compress(value, &mut on_whole);
692            sampled.compress(value, &mut on_sample);
693        }
694        // Training on a twentieth of the column is allowed to cost something. It is not allowed to
695        // cost a factor.
696        assert!(
697            on_sample.len() < on_whole.len() * 5 / 4,
698            "{} against {}",
699            on_sample.len(),
700            on_whole.len()
701        );
702    }
703
704    #[test]
705    fn a_repeating_column_becomes_a_dictionary_of_compressed_entries() {
706        // The DICT_FSST row of the section 6.2 table, which is not an encoding of its own here: it
707        // is a dictionary whose entries went back through the chooser. A dictionary sorts its
708        // entries, so what comes back on anything URL shaped is front coding with the leftovers
709        // FSST compressed, and nobody had to name that shape for the chooser to arrive at it.
710        let distinct = urls(500);
711        let values: Vec<Vec<u8>> =
712            (0..50_000).map(|index| distinct[index * 7919 % distinct.len()].clone()).collect();
713        let bytes = round_trip(&values);
714        assert_eq!(kind_of(&bytes), Kind::Dict);
715        let shape = describe(&bytes).unwrap();
716        assert!(shape.starts_with("DICT(FRONT("), "{shape}");
717        assert!(shape.contains("FSST"), "{shape}");
718        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
719        assert!(ratio > 20.0, "{ratio:.2}x, {shape}");
720    }
721
722    #[test]
723    fn a_column_of_long_runs_costs_almost_nothing() {
724        // A dictionary makes the codes an integer chunk, and the integer chunk knows what to do
725        // with runs, so run length encoding of strings falls out of the recursion.
726        let distinct = urls(50);
727        let mut values = Vec::new();
728        for entry in &distinct {
729            values.extend(std::iter::repeat_n(entry.clone(), 1000));
730        }
731        let bytes = round_trip(&values);
732        let shape = describe(&bytes).unwrap();
733        assert!(shape.contains("RLE"), "{shape}");
734        assert!(bytes.len() < 2000, "{} bytes: {shape}", bytes.len());
735    }
736
737    #[test]
738    fn incompressible_strings_stay_close_to_their_own_size() {
739        // The case where nothing works. It has to land on PLAIN or on an FSST that is not much
740        // worse, rather than on a dictionary of every value in the column.
741        let mut state = 0x2545_f491_4f6c_dd1du64;
742        let values: Vec<Vec<u8>> = (0..2000)
743            .map(|_| {
744                (0..32)
745                    .map(|_| {
746                        state ^= state << 13;
747                        state ^= state >> 7;
748                        state ^= state << 17;
749                        state as u8
750                    })
751                    .collect()
752            })
753            .collect();
754        let bytes = round_trip(&values);
755        assert!(bytes.len() < 2000 * 32 + 3000, "{} bytes", bytes.len());
756    }
757
758    #[test]
759    fn lengths_are_stored_rather_than_offsets() {
760        // Every value is 24 bytes, so the lengths are a constant chunk and cost 13 bytes for the
761        // whole column. Offsets would be 100,000 increasing integers.
762        let values: Vec<Vec<u8>> =
763            (0..100_000).map(|index| format!("{index:024}").into_bytes()).collect();
764        let borrowed = borrow(&values);
765        let bytes = encode_as(Kind::Plain, &borrowed, 0).unwrap().unwrap();
766        assert_eq!(bytes.len(), 5 + 13 + 100_000 * 24);
767    }
768
769    #[test]
770    fn empty_strings_are_values_and_not_nulls() {
771        let values = vec![Vec::new(), b"a".to_vec(), Vec::new(), b"bb".to_vec()];
772        round_trip(&values);
773    }
774
775    #[test]
776    fn a_chunk_with_one_value_round_trips() {
777        round_trip(&[b"only".to_vec()]);
778    }
779
780    #[test]
781    fn every_candidate_that_applies_decodes_to_the_input() {
782        let values = urls(3000);
783        let borrowed = borrow(&values);
784        let applicable = candidates(&borrowed, 0);
785        assert!(applicable.len() >= 2, "{applicable:?}");
786        for kind in applicable {
787            let bytes = encode_as(kind, &borrowed, 0).unwrap().unwrap();
788            assert_eq!(decode(&bytes).unwrap(), values, "{}", kind.name());
789        }
790    }
791
792    #[test]
793    fn the_chooser_picks_the_smallest_candidate() {
794        let values = urls(2000);
795        let borrowed = borrow(&values);
796        let chosen = encode(&borrowed).unwrap();
797        for (_, size) in candidate_sizes(&borrowed).unwrap() {
798            assert!(chosen.len() <= size);
799        }
800    }
801
802    #[test]
803    fn a_truncated_chunk_is_an_error_and_not_a_panic() {
804        let values = urls(40);
805        let bytes = encode(&borrow(&values)).unwrap();
806        for len in 0..bytes.len() {
807            assert!(decode(&bytes[..len]).is_err(), "{len} bytes decoded");
808        }
809    }
810
811    #[test]
812    fn trailing_bytes_are_an_error() {
813        let mut bytes = encode(&borrow(&urls(10))).unwrap();
814        bytes.push(0);
815        let error = decode(&bytes).unwrap_err();
816        assert!(error.message().contains("left over"), "{error}");
817    }
818
819    #[test]
820    fn an_unknown_tag_is_an_error() {
821        let error = decode(&[99, 0, 0, 0, 0]).unwrap_err();
822        assert!(error.message().contains("unknown string encoding tag"), "{error}");
823    }
824
825    #[test]
826    fn a_dictionary_code_outside_the_dictionary_is_an_error() {
827        let mut bytes = vec![Kind::Dict.tag()];
828        put_u32(&mut bytes, 1);
829        bytes.extend_from_slice(&encode(&[b"one".as_slice()]).unwrap());
830        bytes.extend_from_slice(&integer::encode(&[9]).unwrap());
831        let error = decode(&bytes).unwrap_err();
832        assert!(error.message().contains("not in the dictionary"), "{error}");
833    }
834
835    #[test]
836    fn a_sorted_column_of_urls_is_front_coded() {
837        // The M1 finding, in a test. Sorted URLs share a host and most of a path with the URL next
838        // to them, FSST cannot reach those bytes because it compresses each value on its own, and
839        // front coding is the shape that reaches them.
840        let mut values = urls(20_000);
841        values.sort();
842        let bytes = round_trip(&values);
843        assert_eq!(kind_of(&bytes), Kind::Front);
844        let shape = describe(&bytes).unwrap();
845        let mut plain = Vec::new();
846        let borrowed = borrow(&values);
847        for (kind, size) in candidate_sizes(&borrowed).unwrap() {
848            if kind == Kind::Fsst {
849                plain.push(size);
850            }
851        }
852        let fsst = plain[0];
853        assert!(bytes.len() * 2 < fsst, "{} against FSST {fsst}: {shape}", bytes.len());
854    }
855
856    #[test]
857    fn a_column_with_nothing_to_share_is_not_offered_front_coding() {
858        // The candidate costs an encode of the whole column, so a column whose neighbours have
859        // nothing in common must not be paying for it.
860        let mut state = 0x9e37_79b9_7f4a_7c15u64;
861        let values: Vec<Vec<u8>> = (0..2000)
862            .map(|_| {
863                (0..24)
864                    .map(|_| {
865                        state ^= state << 13;
866                        state ^= state >> 7;
867                        state ^= state << 17;
868                        (state % 251) as u8
869                    })
870                    .collect()
871            })
872            .collect();
873        let borrowed = borrow(&values);
874        assert!(!candidates(&borrowed, 0).contains(&Kind::Front));
875    }
876
877    #[test]
878    fn a_prefix_longer_than_the_value_before_it_is_an_error() {
879        let mut bytes = vec![Kind::Front.tag()];
880        put_u32(&mut bytes, 2);
881        bytes.extend_from_slice(&integer::encode(&[0, 9]).unwrap());
882        bytes.extend_from_slice(&encode(&[b"one".as_slice(), b"two".as_slice()]).unwrap());
883        let error = decode(&bytes).unwrap_err();
884        assert!(error.message().contains("shares 9 bytes"), "{error}");
885    }
886
887    #[test]
888    fn a_negative_prefix_is_an_error() {
889        let mut bytes = vec![Kind::Front.tag()];
890        put_u32(&mut bytes, 1);
891        bytes.extend_from_slice(&integer::encode(&[-1]).unwrap());
892        bytes.extend_from_slice(&encode(&[b"one".as_slice()]).unwrap());
893        let error = decode(&bytes).unwrap_err();
894        assert!(error.message().contains("negative shared prefix"), "{error}");
895    }
896
897    #[test]
898    fn a_negative_length_is_an_error() {
899        let mut bytes = vec![Kind::Plain.tag()];
900        put_u32(&mut bytes, 1);
901        bytes.extend_from_slice(&integer::encode(&[-1]).unwrap());
902        let error = decode(&bytes).unwrap_err();
903        assert!(error.message().contains("negative string length"), "{error}");
904    }
905
906    #[test]
907    fn the_sample_is_spread_across_the_chunk_and_not_taken_from_the_front() {
908        // A sorted column whose first 64 KB says nothing about the rest of it. If the sample were
909        // the front, the table would learn `aaaa` and escape every `zzzz`.
910        let mut values: Vec<Vec<u8>> = Vec::new();
911        for index in 0..20_000 {
912            let head = if index < 10_000 { "aaaaaaaaaaaaaaaa" } else { "zzzzzzzzzzzzzzzz" };
913            values.push(format!("{head}/{index:08}").into_bytes());
914        }
915        let borrowed = borrow(&values);
916        let sample = sample_of(&borrowed);
917        let first_half = sample.iter().filter(|value| value.starts_with(b"aaaa")).count();
918        let second_half = sample.len() - first_half;
919        assert!(first_half > 0 && second_half > 0, "{first_half} and {second_half}");
920        let bytes = round_trip(&values);
921        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
922        assert!(ratio > 4.0, "{ratio:.2}x");
923    }
924}