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::chooser::{Chooser, EXHAUSTIVE};
67use crate::fsst::SymbolTable;
68use crate::integer;
69use crate::reader::Reader;
70
71/// How deep the recursion goes. A dictionary of a dictionary is not a thing, so this only has to
72/// stop the dictionary's own entries from being dictionary encoded again.
73const MAX_DEPTH: u8 = 2;
74
75/// How little sharing between neighbours is still worth offering front coding for, as one over
76/// this. A twentieth of the column is around where the prefix lengths start paying for themselves,
77/// and below it the candidate is an encode of the whole column that loses.
78const SHARE_DIVISOR: usize = 20;
79
80/// How many bytes of a column the symbol table is trained on.
81///
82/// The paper trains on about 16 KB. This is four times that, because training happens once per
83/// chunk here rather than once per block, and because the cost of a symbol that is only in the
84/// sample by accident is paid on every value in the chunk.
85pub(crate) const SAMPLE_BYTES: usize = 64 * 1024;
86
87/// What a string chunk is encoded as. The discriminant is the tag byte and is part of the format.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum Kind {
90    /// One value repeated.
91    Constant = 0,
92    /// Lengths and raw bytes.
93    Plain = 1,
94    /// Lengths, a symbol table, and FSST compressed bytes.
95    Fsst = 2,
96    /// The distinct values as a string chunk of their own, and codes into it as an integer chunk.
97    Dict = 3,
98    /// Shared prefix lengths as an integer chunk, and what is left of each value as a string chunk.
99    Front = 4,
100}
101
102impl Kind {
103    fn tag(self) -> u8 {
104        self as u8
105    }
106
107    fn from_tag(tag: u8) -> Result<Self> {
108        match tag {
109            0 => Ok(Self::Constant),
110            1 => Ok(Self::Plain),
111            2 => Ok(Self::Fsst),
112            3 => Ok(Self::Dict),
113            4 => Ok(Self::Front),
114            other => Err(Error::internal(format!("unknown string encoding tag {other}"))),
115        }
116    }
117
118    /// The name that goes in a report.
119    #[must_use]
120    pub fn name(self) -> &'static str {
121        match self {
122            Self::Constant => "CONSTANT",
123            Self::Plain => "PLAIN",
124            Self::Fsst => "FSST",
125            Self::Dict => "DICT",
126            Self::Front => "FRONT",
127        }
128    }
129}
130
131/// Encodes a chunk of strings, choosing whatever comes out smallest.
132///
133/// Every candidate that applies is encoded in full and the smallest is kept, which is what this has
134/// always done and is what every size this crate has reported came out of. [`encode_with`] is the
135/// same thing with the search made swappable.
136///
137/// # Errors
138///
139/// If the chunk is longer than `u32::MAX` values, or if an encoding produces something its own
140/// decoder would not accept.
141pub fn encode(values: &[&[u8]]) -> Result<Vec<u8>> {
142    encode_with(values, &EXHAUSTIVE)
143}
144
145/// [`encode`] with somebody else deciding which candidates are worth encoding in full.
146///
147/// A chooser narrows the list and nothing else. It cannot offer a candidate that does not apply, so
148/// whatever it picks still has to encode the whole chunk and still has to decode, and the worst a
149/// bad one can do is come out bigger than [`encode`] would have.
150///
151/// # Errors
152///
153/// As [`encode`].
154pub fn encode_with(values: &[&[u8]], chooser: &dyn Chooser) -> Result<Vec<u8>> {
155    encode_at(values, 0, chooser)
156}
157
158/// Decodes a chunk that sits at the front of a longer buffer, and says how many bytes it took.
159///
160/// A column group holds one of these per column, and the decoder on that side cannot know where
161/// one ends until it has been read.
162///
163/// # Errors
164///
165/// As [`decode`], except that trailing bytes are what the caller asked about rather than an error.
166pub fn decode_prefix(bytes: &[u8]) -> Result<(Vec<Vec<u8>>, usize)> {
167    let mut reader = Reader::new(bytes);
168    let values = decode_chunk(&mut reader)?;
169    Ok((values, reader.used()))
170}
171
172/// [`describe`] over a chunk at the front of a longer buffer, and how many bytes it took.
173///
174/// # Errors
175///
176/// As [`decode_prefix`].
177pub fn describe_prefix(bytes: &[u8]) -> Result<(String, usize)> {
178    let mut reader = Reader::new(bytes);
179    let text = describe_chunk(&mut reader)?;
180    Ok((text, reader.used()))
181}
182
183/// Decodes a chunk written by [`encode`].
184///
185/// # Errors
186///
187/// If the bytes are truncated, carry an unknown tag, or describe a chunk whose parts disagree.
188pub fn decode(bytes: &[u8]) -> Result<Vec<Vec<u8>>> {
189    let mut reader = Reader::new(bytes);
190    let values = decode_chunk(&mut reader)?;
191    if reader.remaining() != 0 {
192        return Err(Error::internal(format!(
193            "{} bytes left over after decoding a string chunk",
194            reader.remaining()
195        )));
196    }
197    Ok(values)
198}
199
200/// The size of every candidate that applies, for a report that wants to say what was chosen over
201/// what.
202///
203/// # Errors
204///
205/// As [`encode`].
206pub fn candidate_sizes(values: &[&[u8]]) -> Result<Vec<(Kind, usize)>> {
207    let mut sizes = Vec::new();
208    for kind in candidates(values, 0) {
209        if let Some(bytes) = encode_as(kind, values, 0, &EXHAUSTIVE)? {
210            sizes.push((kind, bytes.len()));
211        }
212    }
213    Ok(sizes)
214}
215
216/// Which candidates [`encode`] would try on this chunk, in the order it tries them.
217///
218/// The chooser is exhaustive, so this is also the list of encodes it pays for to return one of
219/// them. A caller measuring where the encode time goes needs the list separately from the sizes,
220/// because a candidate that is offered and turns out not to apply still costs whatever it spent
221/// finding that out.
222#[must_use]
223pub fn offered(values: &[&[u8]]) -> Vec<Kind> {
224    candidates(values, 0)
225}
226
227/// One candidate on its own, which is what the chooser calls once per entry in [`offered`].
228///
229/// `None` when the encoding does not apply, which is what the chooser treats as a candidate that
230/// did not run rather than as a failure. This is here so that the time the chooser spends can be
231/// attributed to the candidate that spent it, which is the measurement F2 wants before anybody
232/// replaces the exhaustive search with a sampled one. It is not how a writer encodes a chunk:
233/// [`encode`] is, and picking a kind by hand gives up the only thing the chooser is for.
234///
235/// # Errors
236///
237/// As [`encode`].
238pub fn encode_only(kind: Kind, values: &[&[u8]]) -> Result<Option<Vec<u8>>> {
239    encode_as(kind, values, 0, &EXHAUSTIVE)
240}
241
242/// How big one candidate comes out, which is all a sampling chooser needs from it.
243///
244/// The bytes are thrown away, so this says nothing [`encode_only`] does not. It is `pub(crate)` and
245/// separate so that the sampler in [`crate::chooser`] is not handing back buffers it will not read.
246pub(crate) fn size_as(kind: Kind, values: &[&[u8]], depth: u8) -> Result<Option<usize>> {
247    Ok(encode_as(kind, values, depth, &EXHAUSTIVE)?.map(|bytes| bytes.len()))
248}
249
250/// The shape a chunk was encoded as, as a line of text like `DICT(FSST, RLE(...))`.
251///
252/// # Errors
253///
254/// As [`decode`].
255pub fn describe(bytes: &[u8]) -> Result<String> {
256    let mut reader = Reader::new(bytes);
257    describe_chunk(&mut reader)
258}
259
260fn encode_at(values: &[&[u8]], depth: u8, chooser: &dyn Chooser) -> Result<Vec<u8>> {
261    let offered = candidates(values, depth);
262    let mut best: Option<Vec<u8>> = None;
263    for kind in chooser.narrow_strings(values, &offered, depth) {
264        let Some(bytes) = encode_as(kind, values, depth, chooser)? else {
265            continue;
266        };
267        if best.as_ref().is_none_or(|current| bytes.len() < current.len()) {
268            best = Some(bytes);
269        }
270    }
271    best.ok_or_else(|| Error::internal("no string encoding applied to the chunk"))
272}
273
274fn candidates(values: &[&[u8]], depth: u8) -> Vec<Kind> {
275    let mut kinds = vec![Kind::Plain];
276    if values.is_empty() {
277        return kinds;
278    }
279    if values.iter().all(|value| *value == values[0]) {
280        return vec![Kind::Constant];
281    }
282    kinds.push(Kind::Fsst);
283    if depth < MAX_DEPTH && has_duplicates(values) {
284        kinds.push(Kind::Dict);
285    }
286    if depth < MAX_DEPTH && sharing_of(values) >= total_len(values) / SHARE_DIVISOR {
287        kinds.push(Kind::Front);
288    }
289    kinds
290}
291
292/// How many bytes each value shares with the value before it, added up.
293///
294/// This is a full pass over the column, and it is here rather than on a sample because it is byte
295/// comparisons that stop at the first difference, which on a column with nothing to share stops
296/// immediately. Against training a symbol table and compressing the whole column, which is what
297/// offering the candidate would cost, it is not worth sampling.
298fn sharing_of(values: &[&[u8]]) -> usize {
299    let mut shared = 0;
300    for pair in values.windows(2) {
301        shared += shared_prefix(pair[0], pair[1]);
302    }
303    shared
304}
305
306/// Every value split into the bytes it shares with the value before it and the bytes it does not.
307///
308/// The suffixes point into the values, so this costs the prefix lengths and nothing else. It is
309/// shared with [`crate::multi`], which front codes a column before compressing it against a symbol
310/// table that belongs to the whole group.
311pub(crate) fn front_code<'a>(values: &[&'a [u8]]) -> (Vec<i64>, Vec<&'a [u8]>) {
312    let mut prefixes = Vec::with_capacity(values.len());
313    let mut suffixes: Vec<&'a [u8]> = Vec::with_capacity(values.len());
314    let mut previous: &[u8] = b"";
315    for value in values {
316        let value: &'a [u8] = value;
317        let shared = shared_prefix(previous, value);
318        prefixes.push(shared as i64);
319        suffixes.push(&value[shared..]);
320        previous = value;
321    }
322    (prefixes, suffixes)
323}
324
325/// The other half. The suffixes are consumed because the values are built out of them.
326///
327/// # Errors
328///
329/// If a prefix is negative or is longer than the value it is a prefix of, which is what a corrupt
330/// or hand written chunk looks like from here.
331pub(crate) fn front_decode(prefixes: &[i64], suffixes: Vec<Vec<u8>>) -> Result<Vec<Vec<u8>>> {
332    let mut values: Vec<Vec<u8>> = Vec::with_capacity(suffixes.len());
333    for (index, suffix) in suffixes.into_iter().enumerate() {
334        let shared = usize::try_from(prefixes[index])
335            .map_err(|_| Error::internal("a negative shared prefix length"))?;
336        let previous: &[u8] = if index == 0 { b"" } else { &values[index - 1] };
337        if shared > previous.len() {
338            return Err(Error::internal(format!(
339                "a value shares {shared} bytes with a value {} bytes long",
340                previous.len()
341            )));
342        }
343        let mut value = Vec::with_capacity(shared + suffix.len());
344        value.extend_from_slice(&previous[..shared]);
345        value.extend_from_slice(&suffix);
346        values.push(value);
347    }
348    Ok(values)
349}
350
351fn shared_prefix(previous: &[u8], value: &[u8]) -> usize {
352    let limit = previous.len().min(value.len());
353    let mut shared = 0;
354    while shared < limit && previous[shared] == value[shared] {
355        shared += 1;
356    }
357    shared
358}
359
360fn total_len(values: &[&[u8]]) -> usize {
361    values.iter().map(|value| value.len()).sum()
362}
363
364fn encode_as(
365    kind: Kind,
366    values: &[&[u8]],
367    depth: u8,
368    chooser: &dyn Chooser,
369) -> Result<Option<Vec<u8>>> {
370    let mut out = vec![kind.tag()];
371    put_u32(&mut out, u32::try_from(values.len()).map_err(|_| too_long(values.len()))?);
372    match kind {
373        Kind::Constant => {
374            let Some(first) = values.first() else {
375                return Ok(None);
376            };
377            if values.iter().any(|value| value != first) {
378                return Ok(None);
379            }
380            put_u32(&mut out, u32::try_from(first.len()).map_err(|_| too_long(first.len()))?);
381            out.extend_from_slice(first);
382        }
383        Kind::Plain => {
384            out.extend_from_slice(&encode_lengths(values, chooser)?);
385            for value in values {
386                out.extend_from_slice(value);
387            }
388        }
389        Kind::Fsst => {
390            let sample = sample_of(values);
391            let table = SymbolTable::train(&sample);
392            if table.is_empty() {
393                return Ok(None);
394            }
395            let mut compressed = Vec::new();
396            let mut lengths = Vec::with_capacity(values.len());
397            for value in values {
398                let before = compressed.len();
399                table.compress(value, &mut compressed);
400                lengths.push((compressed.len() - before) as i64);
401            }
402            table.serialize(&mut out);
403            out.extend_from_slice(&integer::encode_with(&lengths, chooser)?);
404            out.extend_from_slice(&compressed);
405        }
406        Kind::Dict => {
407            let (entries, codes) = dictionary_of(values);
408            if entries.is_empty() {
409                return Ok(None);
410            }
411            out.extend_from_slice(&encode_at(&entries, depth + 1, chooser)?);
412            out.extend_from_slice(&integer::encode_with(&codes, chooser)?);
413        }
414        Kind::Front => {
415            let (prefixes, suffixes) = front_code(values);
416            out.extend_from_slice(&integer::encode_with(&prefixes, chooser)?);
417            out.extend_from_slice(&encode_at(&suffixes, depth + 1, chooser)?);
418        }
419    }
420    Ok(Some(out))
421}
422
423fn decode_chunk(reader: &mut Reader<'_>) -> Result<Vec<Vec<u8>>> {
424    let kind = Kind::from_tag(reader.u8()?)?;
425    let count = reader.u32()? as usize;
426    match kind {
427        Kind::Constant => {
428            let len = reader.u32()? as usize;
429            let value = reader.bytes(len)?.to_vec();
430            Ok(vec![value; count])
431        }
432        Kind::Plain => {
433            let lengths = decode_lengths(reader, count)?;
434            let mut values = Vec::with_capacity(count);
435            for length in lengths {
436                values.push(reader.bytes(length)?.to_vec());
437            }
438            Ok(values)
439        }
440        Kind::Fsst => {
441            let (table, used) = SymbolTable::deserialize(reader.rest())?;
442            reader.skip(used)?;
443            let lengths = decode_lengths(reader, count)?;
444            let mut values = Vec::with_capacity(count);
445            for length in lengths {
446                let compressed = reader.bytes(length)?;
447                let mut value = Vec::new();
448                table.decompress(compressed, &mut value)?;
449                values.push(value);
450            }
451            Ok(values)
452        }
453        Kind::Dict => {
454            let dictionary = decode_chunk(reader)?;
455            let codes = decode_integers(reader)?;
456            if codes.len() != count {
457                return Err(Error::internal(format!(
458                    "a dictionary chunk says it holds {count} values and has {} codes",
459                    codes.len()
460                )));
461            }
462            let mut values = Vec::with_capacity(count);
463            for code in codes {
464                let entry =
465                    usize::try_from(code).ok().and_then(|index| dictionary.get(index)).ok_or_else(
466                        || Error::internal(format!("code {code} is not in the dictionary")),
467                    )?;
468                values.push(entry.clone());
469            }
470            Ok(values)
471        }
472        Kind::Front => {
473            let prefixes = decode_integers(reader)?;
474            let suffixes = decode_chunk(reader)?;
475            if prefixes.len() != count || suffixes.len() != count {
476                return Err(Error::internal(format!(
477                    "a front coded chunk says it holds {count} values and has {} prefixes and {} suffixes",
478                    prefixes.len(),
479                    suffixes.len()
480                )));
481            }
482            front_decode(&prefixes, suffixes)
483        }
484    }
485}
486
487fn describe_chunk(reader: &mut Reader<'_>) -> Result<String> {
488    let kind = Kind::from_tag(reader.u8()?)?;
489    let count = reader.u32()? as usize;
490    Ok(match kind {
491        Kind::Constant => {
492            let len = reader.u32()? as usize;
493            reader.bytes(len)?;
494            "CONSTANT".to_string()
495        }
496        Kind::Plain => {
497            let (shape, lengths) = describe_lengths(reader, count)?;
498            reader.skip(lengths.iter().sum())?;
499            format!("PLAIN({shape})")
500        }
501        Kind::Fsst => {
502            let (table, used) = SymbolTable::deserialize(reader.rest())?;
503            reader.skip(used)?;
504            let (shape, lengths) = describe_lengths(reader, count)?;
505            reader.skip(lengths.iter().sum())?;
506            format!("FSST[{}]({shape})", table.len())
507        }
508        Kind::Dict => {
509            let entries = describe_chunk(reader)?;
510            let codes = describe_integers(reader)?;
511            format!("DICT({entries}, {codes})")
512        }
513        Kind::Front => {
514            let prefixes = describe_integers(reader)?;
515            let suffixes = describe_chunk(reader)?;
516            format!("FRONT({prefixes}, {suffixes})")
517        }
518    })
519}
520
521/// The shape of the length array and the lengths themselves, because a describe has to walk past
522/// the payload to leave the reader where the next chunk starts and the payload size is the sum of
523/// the lengths.
524fn describe_lengths(reader: &mut Reader<'_>, count: usize) -> Result<(String, Vec<usize>)> {
525    let (shape, _) = integer::describe_prefix(reader.rest())?;
526    let lengths = decode_lengths(reader, count)?;
527    Ok((shape, lengths))
528}
529
530fn encode_lengths(values: &[&[u8]], chooser: &dyn Chooser) -> Result<Vec<u8>> {
531    let lengths: Vec<i64> = values.iter().map(|value| value.len() as i64).collect();
532    integer::encode_with(&lengths, chooser)
533}
534
535fn decode_lengths(reader: &mut Reader<'_>, count: usize) -> Result<Vec<usize>> {
536    let lengths = decode_integers(reader)?;
537    if lengths.len() != count {
538        return Err(Error::internal(format!(
539            "a string chunk says it holds {count} values and has {} lengths",
540            lengths.len()
541        )));
542    }
543    lengths
544        .into_iter()
545        .map(|length| {
546            usize::try_from(length).map_err(|_| Error::internal("a negative string length"))
547        })
548        .collect()
549}
550
551/// Reads one nested integer chunk. The integer decoder wants a slice of exactly its own chunk and
552/// the reader does not know how long that is, so it decodes from the rest of the buffer and is told
553/// afterwards how much it used.
554fn decode_integers(reader: &mut Reader<'_>) -> Result<Vec<i64>> {
555    let (values, used) = integer::decode_prefix(reader.rest())?;
556    reader.skip(used)?;
557    Ok(values)
558}
559
560fn describe_integers(reader: &mut Reader<'_>) -> Result<String> {
561    let (text, used) = integer::describe_prefix(reader.rest())?;
562    reader.skip(used)?;
563    Ok(text)
564}
565
566/// A sample of the column spread across the whole of it, taken at random skips rather than at a
567/// fixed stride.
568///
569/// Section 6.3 makes the point about choosing an encoding from a sample and it applies at least as
570/// much to training a symbol table. Column data is frequently sorted or clustered, so the first
571/// 64 KB of a URL column is the hosts that sort first and a table trained on it escapes most of the
572/// rest of the column.
573///
574/// The skips are random rather than fixed because a fixed stride aliases. Column data is also
575/// frequently periodic, and a stride that shares a factor with the period samples one phase of it
576/// and never sees the others. That is not a hypothetical: the first version of this took every
577/// `n`th value, and on a test column whose values cycle with a period that the stride happened to
578/// divide, the table it trained was 3.4 times worse than one trained on the whole column, because
579/// it learned eight byte symbols that only line up with the phase it saw and had no shorter symbols
580/// left to fall back on.
581///
582/// The generator is a fixed seed xorshift, so the sample is a function of the column and encoding
583/// the same values twice produces the same bytes.
584pub(crate) fn sample_of<'a>(values: &[&'a [u8]]) -> Vec<&'a [u8]> {
585    sample_bytes_of(values, SAMPLE_BYTES)
586}
587
588/// [`sample_of`] with the byte budget spelled out, for a caller training one table over several
589/// columns that has to split the budget between them.
590pub(crate) fn sample_bytes_of<'a>(values: &[&'a [u8]], budget: usize) -> Vec<&'a [u8]> {
591    let budget = budget.max(1);
592    let total: usize = values.iter().map(|value| value.len()).sum();
593    if total <= budget {
594        return values.to_vec();
595    }
596    let stride = total.div_ceil(budget).max(1);
597    let span = (stride * 2 - 1).max(1) as u64;
598    let mut state = 0x2545_f491_4f6c_dd1du64;
599    let mut sample = Vec::with_capacity(values.len() / stride + 1);
600    let mut at = 0usize;
601    while at < values.len() {
602        sample.push(values[at]);
603        state ^= state << 13;
604        state ^= state >> 7;
605        state ^= state << 17;
606        at += 1 + (state % span) as usize;
607    }
608    sample
609}
610
611/// The distinct values in sorted order and the code of every value, in one pass over one sort.
612///
613/// The dictionary is sorted for the same reason the integer one is: an ordered dictionary turns a
614/// range predicate into a code range rather than a code set, and front coding over the entries needs
615/// them sorted anyway.
616///
617/// It sorts a permutation of indices rather than the values, which is the whole point. Sorting the
618/// values means copying every one of them onto the heap first, and the codes then have to be found
619/// by searching the dictionary back for each value, which is a binary search of string comparisons
620/// per row. Walking the permutation gives the codes away for free, because the position a value
621/// sorted to is the position its code was assigned at.
622fn dictionary_of<'a>(values: &[&'a [u8]]) -> (Vec<&'a [u8]>, Vec<i64>) {
623    let mut order: Vec<u32> = (0..values.len() as u32).collect();
624    order.sort_unstable_by(|left, right| values[*left as usize].cmp(values[*right as usize]));
625    let mut entries: Vec<&'a [u8]> = Vec::new();
626    let mut codes = vec![0i64; values.len()];
627    for &index in &order {
628        let value = values[index as usize];
629        if entries.last() != Some(&value) {
630            entries.push(value);
631        }
632        codes[index as usize] = (entries.len() - 1) as i64;
633    }
634    (entries, codes)
635}
636
637/// Whether any value appears twice, which is the only thing the candidate list wants to know.
638///
639/// This used to build the whole sorted dictionary and compare its length against the input, which
640/// is a copy of the chunk and a sort of it paid on every chunk at every level whether the dictionary
641/// was ever encoded or not. It is a linear probe over hashes instead: expected O(n), no allocation
642/// per value, and it stops at the first duplicate it finds, which on a column with any repetition at
643/// all is immediately.
644///
645/// A hash collision is resolved by comparing the bytes, so the answer is exact rather than probable.
646fn has_duplicates(values: &[&[u8]]) -> bool {
647    let Some(slots) = values.len().checked_mul(2).map(usize::next_power_of_two) else {
648        return false;
649    };
650    let mask = slots - 1;
651    let mut table = vec![u32::MAX; slots];
652    for (index, value) in values.iter().enumerate() {
653        let mut at = hash_of(value) as usize & mask;
654        loop {
655            let held = table[at];
656            if held == u32::MAX {
657                table[at] = index as u32;
658                break;
659            }
660            if values[held as usize] == *value {
661                return true;
662            }
663            at = (at + 1) & mask;
664        }
665    }
666    false
667}
668
669/// FNV-1a over the bytes, eight at a time.
670///
671/// Good enough for a table that verifies every hit, and it is not part of the format, so nothing
672/// depends on which hash this is. Eight bytes at a time because a URL column is long values and a
673/// byte at a time over a hundred bytes of every one of 122,880 rows is the loop this is here to
674/// avoid.
675fn hash_of(value: &[u8]) -> u64 {
676    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
677    let mut chunks = value.chunks_exact(8);
678    for chunk in &mut chunks {
679        let word = u64::from_le_bytes(chunk.try_into().expect("chunks_exact(8) gives eight bytes"));
680        hash = (hash ^ word).wrapping_mul(0x1_0000_01b3);
681    }
682    for byte in chunks.remainder() {
683        hash = (hash ^ u64::from(*byte)).wrapping_mul(0x1_0000_01b3);
684    }
685    (hash ^ (value.len() as u64)).wrapping_mul(0x1_0000_01b3)
686}
687
688fn too_long(len: usize) -> Error {
689    Error::internal(format!("a string chunk of {len} is longer than the format allows"))
690}
691
692fn put_u32(out: &mut Vec<u8>, value: u32) {
693    out.extend_from_slice(&value.to_le_bytes());
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699
700    fn urls(count: usize) -> Vec<Vec<u8>> {
701        let hosts = ["www.example.com", "shop.example.com", "news.other.example.org"];
702        let paths = ["/index.html", "/catalog/item", "/search", "/user/profile/settings"];
703        (0..count)
704            .map(|index| {
705                let host = hosts[index % hosts.len()];
706                let path = paths[(index / 3) % paths.len()];
707                format!("http://{host}{path}?session={}&ref=google", index * 7).into_bytes()
708            })
709            .collect()
710    }
711
712    /// The same values with a scrambled identifier stuck on the front of each, for the tests that
713    /// need neighbouring values to have nothing in common. Shuffling the order is not enough,
714    /// because two URLs picked at random still agree on a scheme and often on a host.
715    fn keyed(values: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
716        values
717            .into_iter()
718            .enumerate()
719            .map(|(index, value)| {
720                let key = (index as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15) % 1_000_000_007;
721                let mut out = format!("{key:010}/").into_bytes();
722                out.extend_from_slice(&value);
723                out
724            })
725            .collect()
726    }
727
728    fn borrow(values: &[Vec<u8>]) -> Vec<&[u8]> {
729        values.iter().map(Vec::as_slice).collect()
730    }
731
732    fn round_trip(values: &[Vec<u8>]) -> Vec<u8> {
733        let borrowed = borrow(values);
734        let bytes = encode(&borrowed).unwrap();
735        let back = decode(&bytes).unwrap();
736        assert_eq!(back, values, "{}", describe(&bytes).unwrap());
737        bytes
738    }
739
740    fn kind_of(bytes: &[u8]) -> Kind {
741        Kind::from_tag(bytes[0]).unwrap()
742    }
743
744    #[test]
745    fn the_dictionary_is_sorted_and_the_codes_point_back_at_the_values() {
746        // The two things the dictionary path has to get right, and the reason it is one function
747        // now rather than a sort followed by a binary search per row.
748        let values = vec![
749            b"pear".to_vec(),
750            b"apple".to_vec(),
751            b"pear".to_vec(),
752            b"cherry".to_vec(),
753            b"apple".to_vec(),
754        ];
755        let borrowed = borrow(&values);
756        let (entries, codes) = dictionary_of(&borrowed);
757        assert_eq!(entries, vec![b"apple".as_slice(), b"cherry".as_slice(), b"pear".as_slice()]);
758        assert_eq!(codes, vec![2, 0, 2, 1, 0]);
759        for (code, value) in codes.iter().zip(&borrowed) {
760            assert_eq!(entries[*code as usize], *value);
761        }
762    }
763
764    #[test]
765    fn a_column_with_nothing_repeated_has_no_duplicates_and_one_with_anything_does() {
766        let distinct: Vec<Vec<u8>> =
767            (0..5000).map(|index| format!("value-{index}").into_bytes()).collect();
768        assert!(!has_duplicates(&borrow(&distinct)));
769
770        // One repeat at the far end, so a check that gave up early would miss it.
771        let mut repeated = distinct.clone();
772        repeated.push(b"value-0".to_vec());
773        assert!(has_duplicates(&borrow(&repeated)));
774
775        assert!(!has_duplicates(&borrow(&Vec::new())));
776        assert!(!has_duplicates(&borrow(&[b"one".to_vec()])));
777        assert!(has_duplicates(&borrow(&vec![b"same".to_vec(); 2])));
778    }
779
780    #[test]
781    fn long_values_that_differ_only_at_the_end_are_not_confused_for_each_other() {
782        // The hash is eight bytes at a time and the table verifies every hit, so this is the case
783        // that says the verify is really there rather than the hash being trusted.
784        let stem = "http://www.example.com/a/very/long/path/that/goes/on?session=";
785        let values: Vec<Vec<u8>> =
786            (0..2000).map(|index| format!("{stem}{index}").into_bytes()).collect();
787        assert!(!has_duplicates(&borrow(&values)));
788        let (entries, codes) = dictionary_of(&borrow(&values));
789        assert_eq!(entries.len(), values.len());
790        assert_eq!(codes.len(), values.len());
791    }
792
793    #[test]
794    fn what_the_chooser_returns_is_the_smallest_of_what_it_was_offered() {
795        // `offered` and `encode_only` are what `cargo xtask encode` splits the chooser's seconds
796        // with, so they have to describe the chooser that actually runs rather than a second copy
797        // of its rules that drifts. This is the assertion that keeps the two the same thing: walk
798        // the list, encode each one alone, and the smallest has to be byte for byte what `encode`
799        // came back with.
800        for values in [urls(400), keyed(urls(400)), vec![b"same".to_vec(); 50], Vec::new()] {
801            let borrowed = borrow(&values);
802            let chosen = encode(&borrowed).unwrap();
803            let mut smallest: Option<Vec<u8>> = None;
804            for kind in offered(&borrowed) {
805                let Some(bytes) = encode_only(kind, &borrowed).unwrap() else {
806                    continue;
807                };
808                if smallest.as_ref().is_none_or(|best| bytes.len() < best.len()) {
809                    smallest = Some(bytes);
810                }
811            }
812            assert_eq!(smallest.as_deref(), Some(chosen.as_slice()), "{}", values.len());
813        }
814    }
815
816    fn raw_size(values: &[Vec<u8>]) -> usize {
817        values.iter().map(Vec::len).sum::<usize>() + values.len() * 4
818    }
819
820    #[test]
821    fn an_empty_chunk_round_trips() {
822        let bytes = round_trip(&[]);
823        assert_eq!(kind_of(&bytes), Kind::Plain);
824    }
825
826    #[test]
827    fn a_constant_column_costs_what_one_value_costs() {
828        let values = vec![b"https://www.example.com/".to_vec(); 100_000];
829        let bytes = round_trip(&values);
830        assert_eq!(kind_of(&bytes), Kind::Constant);
831        assert_eq!(bytes.len(), 9 + 24);
832    }
833
834    #[test]
835    fn a_url_column_of_unique_values_uses_fsst() {
836        // Every value distinct, so a dictionary is the values plus an index and cannot win, and
837        // every value starts with an identifier of its own, so neighbours share nothing and front
838        // coding cannot win either. What is left is a column with a lot of repeated vocabulary in
839        // it and no structure that anything but a symbol table can reach. Section 6.5 says the high
840        // cardinality end of `URL` falls back to FSST only and this is that case.
841        let values = keyed(urls(20_000));
842        let bytes = round_trip(&values);
843        assert_eq!(kind_of(&bytes), Kind::Fsst);
844        // Eleven bytes of every value are the identifier and a separator and nothing compresses
845        // them, so the ratio here is lower than the one FSST gets on the URLs on their own.
846        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
847        assert!(ratio > 4.0, "{ratio:.2}x");
848    }
849
850    #[test]
851    fn a_sample_of_a_periodic_column_learns_every_phase_of_it() {
852        // This column is periodic and its period is what a fixed stride would have divided. The
853        // sample has to see all of it, because a table trained on one phase learns eight byte
854        // symbols that only line up with that phase and has nothing shorter to fall back on. The
855        // measured cost of getting this wrong was 3.4 times the compressed size.
856        let values = urls(20_000);
857        let borrowed = borrow(&values);
858        let sample = sample_of(&borrowed);
859        let mut phases: Vec<&[u8]> = sample
860            .iter()
861            .map(|value| {
862                let query =
863                    value.iter().position(|byte| *byte == b'?').expect("every value has a query");
864                &value[..query]
865            })
866            .collect();
867        phases.sort_unstable();
868        phases.dedup();
869        // Three hosts and four paths, and the sample has to contain all twelve of the combinations.
870        assert_eq!(phases.len(), 12);
871        let whole = SymbolTable::train(&borrowed);
872        let sampled = SymbolTable::train(&sample);
873        let mut on_whole = Vec::new();
874        let mut on_sample = Vec::new();
875        for value in &borrowed {
876            whole.compress(value, &mut on_whole);
877            sampled.compress(value, &mut on_sample);
878        }
879        // Training on a twentieth of the column is allowed to cost something. It is not allowed to
880        // cost a factor.
881        assert!(
882            on_sample.len() < on_whole.len() * 5 / 4,
883            "{} against {}",
884            on_sample.len(),
885            on_whole.len()
886        );
887    }
888
889    #[test]
890    fn a_repeating_column_becomes_a_dictionary_of_compressed_entries() {
891        // The DICT_FSST row of the section 6.2 table, which is not an encoding of its own here: it
892        // is a dictionary whose entries went back through the chooser. A dictionary sorts its
893        // entries, so what comes back on anything URL shaped is front coding with the leftovers
894        // FSST compressed, and nobody had to name that shape for the chooser to arrive at it.
895        let distinct = urls(500);
896        let values: Vec<Vec<u8>> =
897            (0..50_000).map(|index| distinct[index * 7919 % distinct.len()].clone()).collect();
898        let bytes = round_trip(&values);
899        assert_eq!(kind_of(&bytes), Kind::Dict);
900        let shape = describe(&bytes).unwrap();
901        assert!(shape.starts_with("DICT(FRONT("), "{shape}");
902        assert!(shape.contains("FSST"), "{shape}");
903        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
904        assert!(ratio > 20.0, "{ratio:.2}x, {shape}");
905    }
906
907    #[test]
908    fn a_column_of_long_runs_costs_almost_nothing() {
909        // A dictionary makes the codes an integer chunk, and the integer chunk knows what to do
910        // with runs, so run length encoding of strings falls out of the recursion.
911        let distinct = urls(50);
912        let mut values = Vec::new();
913        for entry in &distinct {
914            values.extend(std::iter::repeat_n(entry.clone(), 1000));
915        }
916        let bytes = round_trip(&values);
917        let shape = describe(&bytes).unwrap();
918        assert!(shape.contains("RLE"), "{shape}");
919        assert!(bytes.len() < 2000, "{} bytes: {shape}", bytes.len());
920    }
921
922    #[test]
923    fn incompressible_strings_stay_close_to_their_own_size() {
924        // The case where nothing works. It has to land on PLAIN or on an FSST that is not much
925        // worse, rather than on a dictionary of every value in the column.
926        let mut state = 0x2545_f491_4f6c_dd1du64;
927        let values: Vec<Vec<u8>> = (0..2000)
928            .map(|_| {
929                (0..32)
930                    .map(|_| {
931                        state ^= state << 13;
932                        state ^= state >> 7;
933                        state ^= state << 17;
934                        state as u8
935                    })
936                    .collect()
937            })
938            .collect();
939        let bytes = round_trip(&values);
940        assert!(bytes.len() < 2000 * 32 + 3000, "{} bytes", bytes.len());
941    }
942
943    #[test]
944    fn lengths_are_stored_rather_than_offsets() {
945        // Every value is 24 bytes, so the lengths are a constant chunk and cost 13 bytes for the
946        // whole column. Offsets would be 100,000 increasing integers.
947        let values: Vec<Vec<u8>> =
948            (0..100_000).map(|index| format!("{index:024}").into_bytes()).collect();
949        let borrowed = borrow(&values);
950        let bytes = encode_only(Kind::Plain, &borrowed).unwrap().unwrap();
951        assert_eq!(bytes.len(), 5 + 13 + 100_000 * 24);
952    }
953
954    #[test]
955    fn empty_strings_are_values_and_not_nulls() {
956        let values = vec![Vec::new(), b"a".to_vec(), Vec::new(), b"bb".to_vec()];
957        round_trip(&values);
958    }
959
960    #[test]
961    fn a_chunk_with_one_value_round_trips() {
962        round_trip(&[b"only".to_vec()]);
963    }
964
965    #[test]
966    fn every_candidate_that_applies_decodes_to_the_input() {
967        let values = urls(3000);
968        let borrowed = borrow(&values);
969        let applicable = candidates(&borrowed, 0);
970        assert!(applicable.len() >= 2, "{applicable:?}");
971        for kind in applicable {
972            let bytes = encode_only(kind, &borrowed).unwrap().unwrap();
973            assert_eq!(decode(&bytes).unwrap(), values, "{}", kind.name());
974        }
975    }
976
977    #[test]
978    fn the_chooser_picks_the_smallest_candidate() {
979        let values = urls(2000);
980        let borrowed = borrow(&values);
981        let chosen = encode(&borrowed).unwrap();
982        for (_, size) in candidate_sizes(&borrowed).unwrap() {
983            assert!(chosen.len() <= size);
984        }
985    }
986
987    #[test]
988    fn a_truncated_chunk_is_an_error_and_not_a_panic() {
989        let values = urls(40);
990        let bytes = encode(&borrow(&values)).unwrap();
991        for len in 0..bytes.len() {
992            assert!(decode(&bytes[..len]).is_err(), "{len} bytes decoded");
993        }
994    }
995
996    #[test]
997    fn trailing_bytes_are_an_error() {
998        let mut bytes = encode(&borrow(&urls(10))).unwrap();
999        bytes.push(0);
1000        let error = decode(&bytes).unwrap_err();
1001        assert!(error.message().contains("left over"), "{error}");
1002    }
1003
1004    #[test]
1005    fn an_unknown_tag_is_an_error() {
1006        let error = decode(&[99, 0, 0, 0, 0]).unwrap_err();
1007        assert!(error.message().contains("unknown string encoding tag"), "{error}");
1008    }
1009
1010    #[test]
1011    fn a_dictionary_code_outside_the_dictionary_is_an_error() {
1012        let mut bytes = vec![Kind::Dict.tag()];
1013        put_u32(&mut bytes, 1);
1014        bytes.extend_from_slice(&encode(&[b"one".as_slice()]).unwrap());
1015        bytes.extend_from_slice(&integer::encode(&[9]).unwrap());
1016        let error = decode(&bytes).unwrap_err();
1017        assert!(error.message().contains("not in the dictionary"), "{error}");
1018    }
1019
1020    #[test]
1021    fn a_sorted_column_of_urls_is_front_coded() {
1022        // The M1 finding, in a test. Sorted URLs share a host and most of a path with the URL next
1023        // to them, FSST cannot reach those bytes because it compresses each value on its own, and
1024        // front coding is the shape that reaches them.
1025        let mut values = urls(20_000);
1026        values.sort();
1027        let bytes = round_trip(&values);
1028        assert_eq!(kind_of(&bytes), Kind::Front);
1029        let shape = describe(&bytes).unwrap();
1030        let mut plain = Vec::new();
1031        let borrowed = borrow(&values);
1032        for (kind, size) in candidate_sizes(&borrowed).unwrap() {
1033            if kind == Kind::Fsst {
1034                plain.push(size);
1035            }
1036        }
1037        let fsst = plain[0];
1038        assert!(bytes.len() * 2 < fsst, "{} against FSST {fsst}: {shape}", bytes.len());
1039    }
1040
1041    #[test]
1042    fn a_column_with_nothing_to_share_is_not_offered_front_coding() {
1043        // The candidate costs an encode of the whole column, so a column whose neighbours have
1044        // nothing in common must not be paying for it.
1045        let mut state = 0x9e37_79b9_7f4a_7c15u64;
1046        let values: Vec<Vec<u8>> = (0..2000)
1047            .map(|_| {
1048                (0..24)
1049                    .map(|_| {
1050                        state ^= state << 13;
1051                        state ^= state >> 7;
1052                        state ^= state << 17;
1053                        (state % 251) as u8
1054                    })
1055                    .collect()
1056            })
1057            .collect();
1058        let borrowed = borrow(&values);
1059        assert!(!candidates(&borrowed, 0).contains(&Kind::Front));
1060    }
1061
1062    #[test]
1063    fn a_prefix_longer_than_the_value_before_it_is_an_error() {
1064        let mut bytes = vec![Kind::Front.tag()];
1065        put_u32(&mut bytes, 2);
1066        bytes.extend_from_slice(&integer::encode(&[0, 9]).unwrap());
1067        bytes.extend_from_slice(&encode(&[b"one".as_slice(), b"two".as_slice()]).unwrap());
1068        let error = decode(&bytes).unwrap_err();
1069        assert!(error.message().contains("shares 9 bytes"), "{error}");
1070    }
1071
1072    #[test]
1073    fn a_negative_prefix_is_an_error() {
1074        let mut bytes = vec![Kind::Front.tag()];
1075        put_u32(&mut bytes, 1);
1076        bytes.extend_from_slice(&integer::encode(&[-1]).unwrap());
1077        bytes.extend_from_slice(&encode(&[b"one".as_slice()]).unwrap());
1078        let error = decode(&bytes).unwrap_err();
1079        assert!(error.message().contains("negative shared prefix"), "{error}");
1080    }
1081
1082    #[test]
1083    fn a_negative_length_is_an_error() {
1084        let mut bytes = vec![Kind::Plain.tag()];
1085        put_u32(&mut bytes, 1);
1086        bytes.extend_from_slice(&integer::encode(&[-1]).unwrap());
1087        let error = decode(&bytes).unwrap_err();
1088        assert!(error.message().contains("negative string length"), "{error}");
1089    }
1090
1091    #[test]
1092    fn the_sample_is_spread_across_the_chunk_and_not_taken_from_the_front() {
1093        // A sorted column whose first 64 KB says nothing about the rest of it. If the sample were
1094        // the front, the table would learn `aaaa` and escape every `zzzz`.
1095        let mut values: Vec<Vec<u8>> = Vec::new();
1096        for index in 0..20_000 {
1097            let head = if index < 10_000 { "aaaaaaaaaaaaaaaa" } else { "zzzzzzzzzzzzzzzz" };
1098            values.push(format!("{head}/{index:08}").into_bytes());
1099        }
1100        let borrowed = borrow(&values);
1101        let sample = sample_of(&borrowed);
1102        let first_half = sample.iter().filter(|value| value.starts_with(b"aaaa")).count();
1103        let second_half = sample.len() - first_half;
1104        assert!(first_half > 0 && second_half > 0, "{first_half} and {second_half}");
1105        let bytes = round_trip(&values);
1106        let ratio = raw_size(&values) as f64 / bytes.len() as f64;
1107        assert!(ratio > 4.0, "{ratio:.2}x");
1108    }
1109}