Skip to main content

rudb_native/
section.rs

1//! The section table: one general mechanism for carrying a graph structure in a rudb file.
2//!
3//! spec/graph/03-the-file-format.md section 3.2 asks for one mechanism and three section kinds
4//! rather than three mechanisms. A section is an opaque payload with a kind, an identity, a
5//! generation stamp and a list of extents, and this module is the whole of what the format knows
6//! about one. What a key map or a forward link *means* lives in `rudb-graph` at rank 5, which is
7//! below the format on purpose: a key map that could see a page would be a key map that could only
8//! be tested through a file.
9//!
10//! Three rules make the mechanism the last one the format needs.
11//!
12//! A reader ignores a kind it does not know. That is what [`Section::kind`] being eight opaque
13//! bytes rather than an enum is for: a build that meets `RUDBAJ1\0` before backward adjacency
14//! exists carries the entry through, does not read the payload, and answers the query without it.
15//! Section 3.1 guarantees the answer is the same either way, so ignoring is always available and no
16//! future section kind needs another format bump.
17//!
18//! A section is a list of extents of at most [`MAX_EXTENT`] bytes, each independently checksummed
19//! and readable. Issue #745 is what this rule is for: a single buffer works until it does not, and
20//! an SF100 `lineitem` neighbour array is two gigabytes. Splitting is not an optimization here, it
21//! is the difference between a structure that exists at scale and one that does not.
22//!
23//! Sections are written before the directory and committed by the two-generation header swap the
24//! format already performs. So a crash during a section build leaves unreferenced trailing bytes in
25//! the file and nothing else, and there is no new recovery path to write or to test.
26
27use rudb_common::{Error, Result};
28
29/// Bytes one section table entry takes on disk.
30///
31/// Fifty six, per section 3.2, and fixed rather than variable because the entry list is walked at
32/// open to decide which sections this build understands and a fixed stride makes that a multiply.
33pub(crate) const ENTRY_BYTES: usize = 56;
34
35/// The largest one extent may be.
36///
37/// Sixty four megabytes. Small enough that a reader can hold one while it checksums it, and large
38/// enough that even an SF100 `lineitem` forward link is tens of extents rather than thousands.
39pub const MAX_EXTENT: u32 = 64 * 1024 * 1024;
40
41/// The most extents one section may have.
42///
43/// Sixty four megabytes each, so this bounds a section at a terabyte. The bound exists so that a
44/// torn directory naming four billion extents is refused at decode rather than turned into an
45/// allocation.
46pub const MAX_EXTENTS: u32 = 16 * 1024;
47
48/// A key map, per section 3.3.
49pub const KEY_MAP: &[u8; 8] = b"RUDBKM1\0";
50
51/// A forward link column, per section 3.4.
52pub const FORWARD_LINK: &[u8; 8] = b"RUDBFL1\0";
53
54/// A backward adjacency list, per section 3.5.
55pub const ADJACENCY: &[u8; 8] = b"RUDBAJ1\0";
56
57/// A column summary, per `spec/stats/03-the-file-format.md` section 3.3.
58///
59/// The first kind here that is not from the graph document, which is the point of the mechanism
60/// rather than a complication of it. A statistics section is carried, stamped, split and ignored by
61/// exactly the rules above, and adding it took two constants and one arm below.
62pub const SUMMARY: &[u8; 8] = b"RUDBCS1\0";
63
64/// A column's sketches, per `spec/stats/03-the-file-format.md` section 3.4.
65pub const SKETCHES: &[u8; 8] = b"RUDBSK1\0";
66
67/// A relationship's degree distribution and certificates, per `spec/stats/07-graph-statistics.md`.
68///
69/// Written by the graph layer, because it comes out of the pass the forward link build is already
70/// making, and owned by the statistics document, because nothing in it is needed to resolve a
71/// relationship. Its id is the child column, the same as the forward link it describes, so the two
72/// are found the same way and a rebuild replaces both.
73pub const DEGREES: &[u8; 8] = b"RUDBGD1\0";
74
75/// Rows sorted by one column and covering a second column. The payload holds row values,
76/// not grouped counts. A changed table generation makes the section stale.
77pub const SORTED_PROJECTION: &[u8; 8] = b"RUDBSP1\0";
78
79/// Row-preserving run encoding of a projection ordered by one signed integer column.
80pub const RUN_PROJECTION: &[u8; 8] = b"RUDBRP1\0";
81
82/// A word per row of a long text column with a bit for each run of three bytes its value holds,
83/// per `spec/graph/12-the-order-the-suite-asks-for.md` section 12.6. Paid for out of its own share,
84/// so it is on neither budget list below.
85pub const TEXT_GRAMS: &[u8; 8] = b"RUDBTG1\0";
86
87/// The kinds the graph document owns, which share its ten percent of the column bytes.
88pub const GRAPH_KINDS: &[&[u8; 8]] = &[KEY_MAP, FORWARD_LINK, ADJACENCY];
89
90/// The kinds the statistics document owns, which share its two percent.
91///
92/// Ownership here is about which budget pays, not about which builder writes. [`DEGREES`] is
93/// written by the link build and is on this list, because it is a planning hint that a reader can
94/// drop without losing a relationship, which is the line the two documents are divided along.
95///
96/// Two lists rather than one because the two budgets are separate, and separate means each counts
97/// only what it owns. A statistics build that counted the key maps as already spent would be a
98/// statistics budget the graph layer eats: a TPC-H SF10 file's key maps are 7.7 MB against a two
99/// percent allowance of 54 MB, so a seventh of the statistics budget would go to sections that have
100/// their own.
101///
102/// A kind in neither list is one a later build wrote, and it counts against neither. There is no
103/// better answer available, since this build cannot know which document invented it, and charging
104/// it to both would make every budget here tighter than the document says by an amount that depends
105/// on what some other build did.
106pub const STATISTICS_KINDS: &[&[u8; 8]] = &[SUMMARY, SKETCHES, DEGREES];
107
108/// One entry in a table's section table.
109///
110/// The payload is not here. This is the entry that says where the payload is, what it is, and
111/// whether it is still current, and it is all a reader needs to decide whether to read the payload
112/// at all.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct Section {
115    /// Which kind of structure this is: one of [`KEY_MAP`], [`FORWARD_LINK`], [`ADJACENCY`], or
116    /// something a later build wrote that this one carries through untouched.
117    pub kind: [u8; 8],
118    /// Which structure of that kind. For a key map this identifies the column, for a forward link
119    /// the relationship. The format does not interpret it; `rudb-graph` assigns it.
120    pub id: u64,
121    /// The table generation this section was built against.
122    ///
123    /// A section whose stamp does not match the table's is stale, and section 3.1 says stale means
124    /// ignored rather than repaired. So this field is the whole of the maintenance story: there is
125    /// no repair path in this crate because a mismatch here removes the section from consideration
126    /// and the query runs the way it ran before the section existed.
127    pub generation: u64,
128    /// How many extents the payload is split into.
129    pub extents: u32,
130    /// Where the extent table starts.
131    pub extent_page: u64,
132    /// How many bytes the extent table takes.
133    pub extent_bytes: u32,
134    /// Checksum over the extent table, so a torn one is found before it is believed.
135    pub hash: u64,
136    /// Kind-specific flags. For a key map this carries which of the three forms was chosen, which
137    /// is why a reader never has to guess a form.
138    pub flags: u32,
139    /// Bytes of kind-specific header at the front of the first extent, or, when there are no
140    /// extents, what the structure would have cost. See [`Self::refused`].
141    pub header_bytes: u32,
142}
143
144impl Section {
145    /// Appends this entry's fifty six bytes.
146    ///
147    /// # Errors
148    ///
149    /// If the entry describes something that cannot exist: more extents than [`MAX_EXTENTS`], or an
150    /// extent table larger than one extent. Both are caught here rather than at decode because a
151    /// writer that produced one has a bug, and the bug should stop at the write.
152    pub(crate) fn encode(&self, out: &mut Vec<u8>) -> Result<()> {
153        if self.extents > MAX_EXTENTS {
154            return Err(malformed(format!(
155                "a section of {} extents exceeds the bound of {MAX_EXTENTS}",
156                self.extents
157            )));
158        }
159        if self.extent_bytes > MAX_EXTENT {
160            return Err(malformed("a section's extent table is larger than one extent"));
161        }
162        let before = out.len();
163        out.extend_from_slice(&self.kind);
164        out.extend_from_slice(&self.id.to_le_bytes());
165        out.extend_from_slice(&self.generation.to_le_bytes());
166        out.extend_from_slice(&self.extents.to_le_bytes());
167        out.extend_from_slice(&self.extent_page.to_le_bytes());
168        out.extend_from_slice(&self.extent_bytes.to_le_bytes());
169        out.extend_from_slice(&self.hash.to_le_bytes());
170        out.extend_from_slice(&self.flags.to_le_bytes());
171        out.extend_from_slice(&self.header_bytes.to_le_bytes());
172        debug_assert_eq!(out.len() - before, ENTRY_BYTES, "a section entry is fifty six bytes");
173        Ok(())
174    }
175
176    /// Reads one entry from exactly [`ENTRY_BYTES`] bytes.
177    ///
178    /// # Errors
179    ///
180    /// If the slice is the wrong length, or if the entry names more extents than [`MAX_EXTENTS`] or
181    /// an extent table larger than one extent. A bad entry is an error and not a panic because the
182    /// caller's answer to one is to drop the section and open the table anyway.
183    pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
184        if bytes.len() != ENTRY_BYTES {
185            return Err(malformed("a section entry is not fifty six bytes"));
186        }
187        let section = Self {
188            kind: bytes[0..8].try_into().expect("eight bytes"),
189            id: u64::from_le_bytes(bytes[8..16].try_into().expect("eight bytes")),
190            generation: u64::from_le_bytes(bytes[16..24].try_into().expect("eight bytes")),
191            extents: u32::from_le_bytes(bytes[24..28].try_into().expect("four bytes")),
192            extent_page: u64::from_le_bytes(bytes[28..36].try_into().expect("eight bytes")),
193            extent_bytes: u32::from_le_bytes(bytes[36..40].try_into().expect("four bytes")),
194            hash: u64::from_le_bytes(bytes[40..48].try_into().expect("eight bytes")),
195            flags: u32::from_le_bytes(bytes[48..52].try_into().expect("four bytes")),
196            header_bytes: u32::from_le_bytes(bytes[52..56].try_into().expect("four bytes")),
197        };
198        if section.extents > MAX_EXTENTS {
199            return Err(malformed("a section names more extents than the bound allows"));
200        }
201        if section.extent_bytes > MAX_EXTENT {
202            return Err(malformed("a section's extent table is larger than one extent"));
203        }
204        Ok(section)
205    }
206
207    /// Whether this build understands this section's kind.
208    ///
209    /// The five it knows are the three the graph document's section 3.2 names and the two the
210    /// statistics document's sections 3.3 and 3.4 name. Everything else is a section a later build
211    /// wrote, and the answer is to leave it alone: the entry is carried through a rewrite so that
212    /// opening a file with an old build and closing it does not silently discard work, and the
213    /// payload is never read.
214    #[must_use]
215    pub fn known(&self) -> bool {
216        matches!(
217            &self.kind,
218            KEY_MAP
219                | FORWARD_LINK
220                | ADJACENCY
221                | SUMMARY
222                | SKETCHES
223                | DEGREES
224                | SORTED_PROJECTION
225                | RUN_PROJECTION
226                | TEXT_GRAMS
227        )
228    }
229
230    /// Whether this section's kind is one of these, which is how a budget finds what it owns.
231    #[must_use]
232    pub fn among(&self, kinds: &[&[u8; 8]]) -> bool {
233        kinds.iter().any(|kind| self.kind == **kind)
234    }
235
236    /// Whether this section was built against this table generation.
237    #[must_use]
238    pub fn current(&self, generation: u64) -> bool {
239        self.generation == generation
240    }
241
242    /// Whether this section is one this build should read: a kind it knows, at the current
243    /// generation.
244    #[must_use]
245    pub fn usable(&self, generation: u64) -> bool {
246        self.known() && self.current(generation)
247    }
248
249    /// What this structure would have cost, when the entry is a record of one that did not fit.
250    ///
251    /// Section 3.7 asks for a relationship that did not fit the budget to be recorded with its size
252    /// rather than forgotten, so that raising `graph_budget` is a decision somebody can make from a
253    /// number. An entry with no extents is that record, and the number is in [`Self::header_bytes`],
254    /// which has nothing else to mean when there is no first extent to have a header at the front
255    /// of. [`Self::flags`] keeps the meaning it has for a built section of the same kind, so a
256    /// record says which form the structure would have taken as well as what it would have cost.
257    ///
258    /// `None` for a section that is in the file, which is the ordinary case and is the one where
259    /// the size is the payload's own length.
260    ///
261    /// A size past four gigabytes saturates, because the field is a `u32`. The largest structure
262    /// this project expects to refuse is a packed forward link over an SF100 `lineitem`, which is
263    /// about 2.1 GB, so the saturation is a bound rather than a rounding, and a saturated record
264    /// still says *far more than the budget* correctly.
265    #[must_use]
266    pub fn refused(&self) -> Option<u64> {
267        (self.extents == 0).then(|| u64::from(self.header_bytes))
268    }
269}
270
271/// One section to be written into a file, handed to [`crate::attach`].
272///
273/// The payload is bytes and the format keeps it that way. Which of the three key map forms is in
274/// `flags`, and what the first `header_bytes` bytes mean, are questions `rudb-graph` answers and
275/// this crate never asks, which is what makes the first of section 3.2's three rules true rather
276/// than intended: a mechanism that had to understand a payload could not carry one it had never
277/// heard of.
278#[derive(Debug, Clone, Copy)]
279pub struct Attachment<'a> {
280    /// Which kind of structure this is, usually one of [`KEY_MAP`], [`FORWARD_LINK`],
281    /// [`ADJACENCY`].
282    pub kind: [u8; 8],
283    /// Which structure of that kind. An attachment replaces any section already in the table with
284    /// the same kind and id, which is what makes rebuilding a key map a write rather than a
285    /// question about what to do with the old one.
286    pub id: u64,
287    /// Kind-specific flags, copied into the entry and not interpreted.
288    pub flags: u32,
289    /// How many bytes at the front of `bytes` are the kind's own header.
290    pub header_bytes: u32,
291    /// The payload. Empty is legal and is how section 3.7 records a relationship that did not fit
292    /// the budget: an entry with no extents, its size reported by `rudb_links()`, and nothing in
293    /// the file to read.
294    pub bytes: &'a [u8],
295}
296
297/// Where one extent of a section's payload lives.
298///
299/// Each carries its own checksum, which is the second of section 3.2's three rules: an extent is
300/// independently readable, so a reduction that only needs the third extent of a forward link reads
301/// and verifies one extent rather than two gigabytes.
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub struct Extent {
304    /// Where the extent's bytes start.
305    pub offset: u64,
306    /// How many bytes it holds, at most [`MAX_EXTENT`].
307    pub length: u32,
308    /// Checksum over those bytes.
309    pub hash: u64,
310    /// How many logical elements precede this extent, so that a random access can find the extent
311    /// holding an element without reading any of them.
312    pub first: u64,
313}
314
315/// Bytes one extent entry takes in an extent table.
316pub const EXTENT_BYTES: usize = 28;
317
318impl Extent {
319    /// Appends this extent's twenty eight bytes.
320    ///
321    /// # Errors
322    ///
323    /// If the extent is larger than [`MAX_EXTENT`], which is the rule the split exists to keep.
324    pub(crate) fn encode(&self, out: &mut Vec<u8>) -> Result<()> {
325        if self.length > MAX_EXTENT {
326            return Err(malformed(format!(
327                "an extent of {} bytes exceeds the maximum of {MAX_EXTENT}",
328                self.length
329            )));
330        }
331        out.extend_from_slice(&self.offset.to_le_bytes());
332        out.extend_from_slice(&self.length.to_le_bytes());
333        out.extend_from_slice(&self.hash.to_le_bytes());
334        out.extend_from_slice(&self.first.to_le_bytes());
335        Ok(())
336    }
337
338    /// Reads one extent from exactly [`EXTENT_BYTES`] bytes.
339    ///
340    /// # Errors
341    ///
342    /// If the slice is the wrong length or the extent is oversized.
343    pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
344        if bytes.len() != EXTENT_BYTES {
345            return Err(malformed("an extent entry is not twenty eight bytes"));
346        }
347        let extent = Self {
348            offset: u64::from_le_bytes(bytes[0..8].try_into().expect("eight bytes")),
349            length: u32::from_le_bytes(bytes[8..12].try_into().expect("four bytes")),
350            hash: u64::from_le_bytes(bytes[12..20].try_into().expect("eight bytes")),
351            first: u64::from_le_bytes(bytes[20..28].try_into().expect("eight bytes")),
352        };
353        if extent.length > MAX_EXTENT {
354            return Err(malformed("an extent is larger than the maximum extent"));
355        }
356        Ok(extent)
357    }
358}
359
360/// Encodes a whole extent table, checking that it describes a contiguous run of elements.
361///
362/// # Errors
363///
364/// If an extent is oversized, if the `first` counts are not increasing, or if there are more
365/// extents than [`MAX_EXTENTS`]. The increasing check is what makes a binary search over the table
366/// meaningful, and an unchecked one would be a search that silently returned the wrong extent.
367pub fn encode_extents(extents: &[Extent], out: &mut Vec<u8>) -> Result<()> {
368    if extents.len() > MAX_EXTENTS as usize {
369        return Err(malformed("a section names more extents than the bound allows"));
370    }
371    for (at, extent) in extents.iter().enumerate() {
372        if at == 0 {
373            if extent.first != 0 {
374                return Err(malformed("a section's first extent does not start at element zero"));
375            }
376        } else if extent.first <= extents[at - 1].first {
377            return Err(malformed("a section's extents are not in element order"));
378        }
379        extent.encode(out)?;
380    }
381    Ok(())
382}
383
384/// Decodes a whole extent table.
385///
386/// # Errors
387///
388/// If the byte count is not a multiple of an entry, if an entry is malformed, or if the entries are
389/// not in element order.
390pub fn decode_extents(bytes: &[u8]) -> Result<Vec<Extent>> {
391    if !bytes.len().is_multiple_of(EXTENT_BYTES) {
392        return Err(malformed("an extent table is not a whole number of entries"));
393    }
394    let mut extents: Vec<Extent> = Vec::with_capacity(bytes.len() / EXTENT_BYTES);
395    for chunk in bytes.chunks(EXTENT_BYTES) {
396        let extent = Extent::decode(chunk)?;
397        match extents.last() {
398            None if extent.first != 0 => {
399                return Err(malformed("a section's first extent does not start at element zero"));
400            }
401            Some(previous) if extent.first <= previous.first => {
402                return Err(malformed("a section's extents are not in element order"));
403            }
404            _ => {}
405        }
406        extents.push(extent);
407    }
408    Ok(extents)
409}
410
411/// Which extent holds a given logical element, by binary search over the table.
412///
413/// Returns the index into `extents` and the element's offset within that extent's elements, or
414/// `None` when there are no extents at all, which is the not-built entry of section 3.7.
415///
416/// It does not bound the element from above, because an extent table cannot: the last extent's
417/// length is in bytes and only the caller knows how many elements a byte holds. So an element past
418/// the end answers with an offset past the end of the last extent, and the caller checks that
419/// against the count it already has. `None` rather than an error for the empty case because a
420/// stale link may name a structure that is no longer there, and section 3.1 wants staleness
421/// ignored.
422#[must_use]
423pub fn locate(extents: &[Extent], element: u64) -> Option<(usize, u64)> {
424    let at = extents.partition_point(|extent| extent.first <= element);
425    if at == 0 {
426        return None;
427    }
428    Some((at - 1, element - extents[at - 1].first))
429}
430
431fn malformed(message: impl Into<String>) -> Error {
432    Error::invalid_input(format!("invalid rudb section table: {}", message.into()))
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    fn entry() -> Section {
440        Section {
441            kind: *KEY_MAP,
442            id: 7,
443            generation: 42,
444            extents: 3,
445            extent_page: 1 << 20,
446            extent_bytes: 84,
447            hash: 0xdead_beef_cafe_f00d,
448            flags: 2,
449            header_bytes: 24,
450        }
451    }
452
453    #[test]
454    fn an_entry_takes_fifty_six_bytes_and_round_trips() {
455        let mut bytes = Vec::new();
456        entry().encode(&mut bytes).expect("encode");
457        assert_eq!(bytes.len(), ENTRY_BYTES, "section 3.2 says fifty six");
458        assert_eq!(Section::decode(&bytes).expect("decode"), entry());
459    }
460
461    #[test]
462    fn an_unknown_kind_is_carried_and_not_read() {
463        // The rule that makes this the last bump the mechanism needs. A build that met this entry
464        // before the kind existed has to be able to hold it, report it as not understood, and open
465        // the table anyway.
466        let mut unknown = entry();
467        unknown.kind = *b"RUDBZZ9\0";
468        let mut bytes = Vec::new();
469        unknown.encode(&mut bytes).expect("an unknown kind still encodes");
470        let read = Section::decode(&bytes).expect("an unknown kind still decodes");
471        assert_eq!(read, unknown, "the entry survives a build that does not know it");
472        assert!(!read.known());
473        assert!(!read.usable(42), "a kind this build does not know is never read");
474    }
475
476    #[test]
477    fn the_kinds_the_two_documents_name_are_known() {
478        for kind in [KEY_MAP, FORWARD_LINK, ADJACENCY, SUMMARY, SKETCHES] {
479            let mut section = entry();
480            section.kind = *kind;
481            assert!(section.known(), "{}", String::from_utf8_lossy(kind));
482        }
483    }
484
485    #[test]
486    fn no_two_kinds_share_a_tag() {
487        // Worth a test now that two documents assign them. A collision would mean one kind's payload
488        // read by the other's decoder, which is the one thing an opaque payload cannot defend
489        // against by itself.
490        let all = [KEY_MAP, FORWARD_LINK, ADJACENCY, SUMMARY, SKETCHES];
491        for (at, one) in all.iter().enumerate() {
492            for other in &all[at + 1..] {
493                assert_ne!(one, other, "{}", String::from_utf8_lossy(*one));
494            }
495        }
496    }
497
498    #[test]
499    fn a_stale_section_is_ignored_rather_than_repaired() {
500        // Section 3.1's staleness rule, which is the whole of the maintenance story: the generation
501        // stamp not matching removes the section from consideration, and there is no third state
502        // between usable and ignored for a repair path to live in.
503        let section = entry();
504        assert!(section.usable(42));
505        assert!(!section.usable(43), "a rewrite invalidates rather than corrupts");
506        assert!(section.known(), "staleness is not the same question as familiarity");
507    }
508
509    #[test]
510    fn an_entry_naming_more_extents_than_the_bound_is_refused_at_both_ends() {
511        let mut oversized = entry();
512        oversized.extents = MAX_EXTENTS + 1;
513        assert!(oversized.encode(&mut Vec::new()).is_err(), "a writer's bug stops at the write");
514
515        let mut bytes = Vec::new();
516        entry().encode(&mut bytes).expect("encode");
517        bytes[24..28].copy_from_slice(&(MAX_EXTENTS + 1).to_le_bytes());
518        assert!(Section::decode(&bytes).is_err(), "a torn count is not turned into an allocation");
519    }
520
521    #[test]
522    fn a_short_entry_is_refused_rather_than_read_past() {
523        let mut bytes = Vec::new();
524        entry().encode(&mut bytes).expect("encode");
525        bytes.pop();
526        assert!(Section::decode(&bytes).is_err());
527        assert!(Section::decode(&[]).is_err());
528    }
529
530    #[test]
531    fn an_extent_at_the_maximum_is_allowed_and_one_past_it_is_not() {
532        // The bound is the point of the split, so the boundary is the case worth pinning: sixty
533        // four megabytes exactly has to work, because a payload that is a multiple of it would
534        // otherwise be unwritable.
535        let at_bound = Extent { offset: 4096, length: MAX_EXTENT, hash: 9, first: 0 };
536        let mut bytes = Vec::new();
537        at_bound.encode(&mut bytes).expect("an extent at the bound encodes");
538        assert_eq!(bytes.len(), EXTENT_BYTES);
539        assert_eq!(Extent::decode(&bytes).expect("decode"), at_bound);
540
541        let past = Extent { offset: 4096, length: MAX_EXTENT + 1, hash: 9, first: 0 };
542        assert!(past.encode(&mut Vec::new()).is_err());
543    }
544
545    fn table() -> Vec<Extent> {
546        vec![
547            Extent { offset: 1024, length: MAX_EXTENT, hash: 1, first: 0 },
548            Extent {
549                offset: 1024 + u64::from(MAX_EXTENT),
550                length: MAX_EXTENT,
551                hash: 2,
552                first: 100,
553            },
554            Extent { offset: 1024 + 2 * u64::from(MAX_EXTENT), length: 512, hash: 3, first: 250 },
555        ]
556    }
557
558    #[test]
559    fn an_extent_table_round_trips() {
560        let mut bytes = Vec::new();
561        encode_extents(&table(), &mut bytes).expect("encode");
562        assert_eq!(bytes.len(), 3 * EXTENT_BYTES);
563        assert_eq!(decode_extents(&bytes).expect("decode"), table());
564    }
565
566    #[test]
567    fn an_extent_table_out_of_element_order_is_refused() {
568        // The order is what makes the binary search in `locate` mean anything, so an unordered
569        // table has to be refused rather than searched: a search over one would return a plausible
570        // extent holding the wrong elements.
571        let mut out_of_order = table();
572        out_of_order.swap(1, 2);
573        assert!(encode_extents(&out_of_order, &mut Vec::new()).is_err());
574
575        let mut bytes = Vec::new();
576        encode_extents(&table(), &mut bytes).expect("encode");
577        bytes[EXTENT_BYTES + 20..EXTENT_BYTES + 28].copy_from_slice(&0_u64.to_le_bytes());
578        assert!(decode_extents(&bytes).is_err(), "a torn element order is refused");
579    }
580
581    #[test]
582    fn an_extent_table_not_starting_at_element_zero_is_refused() {
583        let mut shifted = table();
584        shifted[0].first = 1;
585        assert!(encode_extents(&shifted, &mut Vec::new()).is_err());
586    }
587
588    #[test]
589    fn a_partial_extent_table_is_refused_rather_than_truncated() {
590        let mut bytes = Vec::new();
591        encode_extents(&table(), &mut bytes).expect("encode");
592        bytes.truncate(bytes.len() - 1);
593        assert!(decode_extents(&bytes).is_err());
594    }
595
596    #[test]
597    fn an_empty_extent_table_is_a_section_with_no_payload() {
598        // A relationship recorded as not built, per section 3.7, is an entry with no extents. It
599        // has to be legal, because that is how `rudb_links()` reports what a larger budget would
600        // buy.
601        let mut bytes = Vec::new();
602        encode_extents(&[] as &[Extent], &mut bytes).expect("encode");
603        assert!(bytes.is_empty());
604        assert!(decode_extents(&bytes).expect("decode").is_empty());
605        assert_eq!(locate(&[], 0), None);
606    }
607
608    #[test]
609    fn an_element_resolves_to_the_extent_holding_it() {
610        let extents = table();
611        assert_eq!(locate(&extents, 0), Some((0, 0)));
612        assert_eq!(locate(&extents, 99), Some((0, 99)));
613        assert_eq!(locate(&extents, 100), Some((1, 0)), "the first element of the second extent");
614        assert_eq!(locate(&extents, 249), Some((1, 149)));
615        assert_eq!(locate(&extents, 250), Some((2, 0)));
616        assert_eq!(locate(&extents, 1_000_000), Some((2, 999_750)), "past the end of the elements");
617    }
618
619    #[test]
620    fn a_two_gigabyte_payload_is_tens_of_extents_and_not_one_buffer() {
621        // The arithmetic issue #745 is about, and the reason the split is a rule rather than an
622        // option. An SF100 lineitem forward link is 600,037,902 rows at 28 bits, which is 2.10 GB,
623        // and no reader should be asked to hold that in one buffer to checksum it.
624        let payload = 600_037_902_u64 * 28 / 8;
625        let extents = payload.div_ceil(u64::from(MAX_EXTENT));
626        assert!(extents > 30, "{extents} extents");
627        assert!(extents < u64::from(MAX_EXTENTS), "{extents} extents is inside the bound");
628    }
629}