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/// One entry in a table's section table.
68///
69/// The payload is not here. This is the entry that says where the payload is, what it is, and
70/// whether it is still current, and it is all a reader needs to decide whether to read the payload
71/// at all.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct Section {
74 /// Which kind of structure this is: one of [`KEY_MAP`], [`FORWARD_LINK`], [`ADJACENCY`], or
75 /// something a later build wrote that this one carries through untouched.
76 pub kind: [u8; 8],
77 /// Which structure of that kind. For a key map this identifies the column, for a forward link
78 /// the relationship. The format does not interpret it; `rudb-graph` assigns it.
79 pub id: u64,
80 /// The table generation this section was built against.
81 ///
82 /// A section whose stamp does not match the table's is stale, and section 3.1 says stale means
83 /// ignored rather than repaired. So this field is the whole of the maintenance story: there is
84 /// no repair path in this crate because a mismatch here removes the section from consideration
85 /// and the query runs the way it ran before the section existed.
86 pub generation: u64,
87 /// How many extents the payload is split into.
88 pub extents: u32,
89 /// Where the extent table starts.
90 pub extent_page: u64,
91 /// How many bytes the extent table takes.
92 pub extent_bytes: u32,
93 /// Checksum over the extent table, so a torn one is found before it is believed.
94 pub hash: u64,
95 /// Kind-specific flags. For a key map this carries which of the three forms was chosen, which
96 /// is why a reader never has to guess a form.
97 pub flags: u32,
98 /// Bytes of kind-specific header at the front of the first extent.
99 pub header_bytes: u32,
100}
101
102impl Section {
103 /// Appends this entry's fifty six bytes.
104 ///
105 /// # Errors
106 ///
107 /// If the entry describes something that cannot exist: more extents than [`MAX_EXTENTS`], or an
108 /// extent table larger than one extent. Both are caught here rather than at decode because a
109 /// writer that produced one has a bug, and the bug should stop at the write.
110 pub(crate) fn encode(&self, out: &mut Vec<u8>) -> Result<()> {
111 if self.extents > MAX_EXTENTS {
112 return Err(malformed(format!(
113 "a section of {} extents exceeds the bound of {MAX_EXTENTS}",
114 self.extents
115 )));
116 }
117 if self.extent_bytes > MAX_EXTENT {
118 return Err(malformed("a section's extent table is larger than one extent"));
119 }
120 let before = out.len();
121 out.extend_from_slice(&self.kind);
122 out.extend_from_slice(&self.id.to_le_bytes());
123 out.extend_from_slice(&self.generation.to_le_bytes());
124 out.extend_from_slice(&self.extents.to_le_bytes());
125 out.extend_from_slice(&self.extent_page.to_le_bytes());
126 out.extend_from_slice(&self.extent_bytes.to_le_bytes());
127 out.extend_from_slice(&self.hash.to_le_bytes());
128 out.extend_from_slice(&self.flags.to_le_bytes());
129 out.extend_from_slice(&self.header_bytes.to_le_bytes());
130 debug_assert_eq!(out.len() - before, ENTRY_BYTES, "a section entry is fifty six bytes");
131 Ok(())
132 }
133
134 /// Reads one entry from exactly [`ENTRY_BYTES`] bytes.
135 ///
136 /// # Errors
137 ///
138 /// If the slice is the wrong length, or if the entry names more extents than [`MAX_EXTENTS`] or
139 /// an extent table larger than one extent. A bad entry is an error and not a panic because the
140 /// caller's answer to one is to drop the section and open the table anyway.
141 pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
142 if bytes.len() != ENTRY_BYTES {
143 return Err(malformed("a section entry is not fifty six bytes"));
144 }
145 let section = Self {
146 kind: bytes[0..8].try_into().expect("eight bytes"),
147 id: u64::from_le_bytes(bytes[8..16].try_into().expect("eight bytes")),
148 generation: u64::from_le_bytes(bytes[16..24].try_into().expect("eight bytes")),
149 extents: u32::from_le_bytes(bytes[24..28].try_into().expect("four bytes")),
150 extent_page: u64::from_le_bytes(bytes[28..36].try_into().expect("eight bytes")),
151 extent_bytes: u32::from_le_bytes(bytes[36..40].try_into().expect("four bytes")),
152 hash: u64::from_le_bytes(bytes[40..48].try_into().expect("eight bytes")),
153 flags: u32::from_le_bytes(bytes[48..52].try_into().expect("four bytes")),
154 header_bytes: u32::from_le_bytes(bytes[52..56].try_into().expect("four bytes")),
155 };
156 if section.extents > MAX_EXTENTS {
157 return Err(malformed("a section names more extents than the bound allows"));
158 }
159 if section.extent_bytes > MAX_EXTENT {
160 return Err(malformed("a section's extent table is larger than one extent"));
161 }
162 Ok(section)
163 }
164
165 /// Whether this build understands this section's kind.
166 ///
167 /// The five it knows are the three the graph document's section 3.2 names and the two the
168 /// statistics document's sections 3.3 and 3.4 name. Everything else is a section a later build
169 /// wrote, and the answer is to leave it alone: the entry is carried through a rewrite so that
170 /// opening a file with an old build and closing it does not silently discard work, and the
171 /// payload is never read.
172 #[must_use]
173 pub fn known(&self) -> bool {
174 matches!(&self.kind, KEY_MAP | FORWARD_LINK | ADJACENCY | SUMMARY | SKETCHES)
175 }
176
177 /// Whether this section was built against this table generation.
178 #[must_use]
179 pub fn current(&self, generation: u64) -> bool {
180 self.generation == generation
181 }
182
183 /// Whether this section is one this build should read: a kind it knows, at the current
184 /// generation.
185 #[must_use]
186 pub fn usable(&self, generation: u64) -> bool {
187 self.known() && self.current(generation)
188 }
189}
190
191/// One section to be written into a file, handed to [`crate::attach`].
192///
193/// The payload is bytes and the format keeps it that way. Which of the three key map forms is in
194/// `flags`, and what the first `header_bytes` bytes mean, are questions `rudb-graph` answers and
195/// this crate never asks, which is what makes the first of section 3.2's three rules true rather
196/// than intended: a mechanism that had to understand a payload could not carry one it had never
197/// heard of.
198#[derive(Debug, Clone, Copy)]
199pub struct Attachment<'a> {
200 /// Which kind of structure this is, usually one of [`KEY_MAP`], [`FORWARD_LINK`],
201 /// [`ADJACENCY`].
202 pub kind: [u8; 8],
203 /// Which structure of that kind. An attachment replaces any section already in the table with
204 /// the same kind and id, which is what makes rebuilding a key map a write rather than a
205 /// question about what to do with the old one.
206 pub id: u64,
207 /// Kind-specific flags, copied into the entry and not interpreted.
208 pub flags: u32,
209 /// How many bytes at the front of `bytes` are the kind's own header.
210 pub header_bytes: u32,
211 /// The payload. Empty is legal and is how section 3.7 records a relationship that did not fit
212 /// the budget: an entry with no extents, its size reported by `rudb_links()`, and nothing in
213 /// the file to read.
214 pub bytes: &'a [u8],
215}
216
217/// Where one extent of a section's payload lives.
218///
219/// Each carries its own checksum, which is the second of section 3.2's three rules: an extent is
220/// independently readable, so a reduction that only needs the third extent of a forward link reads
221/// and verifies one extent rather than two gigabytes.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct Extent {
224 /// Where the extent's bytes start.
225 pub offset: u64,
226 /// How many bytes it holds, at most [`MAX_EXTENT`].
227 pub length: u32,
228 /// Checksum over those bytes.
229 pub hash: u64,
230 /// How many logical elements precede this extent, so that a random access can find the extent
231 /// holding an element without reading any of them.
232 pub first: u64,
233}
234
235/// Bytes one extent entry takes in an extent table.
236pub const EXTENT_BYTES: usize = 28;
237
238impl Extent {
239 /// Appends this extent's twenty eight bytes.
240 ///
241 /// # Errors
242 ///
243 /// If the extent is larger than [`MAX_EXTENT`], which is the rule the split exists to keep.
244 pub(crate) fn encode(&self, out: &mut Vec<u8>) -> Result<()> {
245 if self.length > MAX_EXTENT {
246 return Err(malformed(format!(
247 "an extent of {} bytes exceeds the maximum of {MAX_EXTENT}",
248 self.length
249 )));
250 }
251 out.extend_from_slice(&self.offset.to_le_bytes());
252 out.extend_from_slice(&self.length.to_le_bytes());
253 out.extend_from_slice(&self.hash.to_le_bytes());
254 out.extend_from_slice(&self.first.to_le_bytes());
255 Ok(())
256 }
257
258 /// Reads one extent from exactly [`EXTENT_BYTES`] bytes.
259 ///
260 /// # Errors
261 ///
262 /// If the slice is the wrong length or the extent is oversized.
263 pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
264 if bytes.len() != EXTENT_BYTES {
265 return Err(malformed("an extent entry is not twenty eight bytes"));
266 }
267 let extent = Self {
268 offset: u64::from_le_bytes(bytes[0..8].try_into().expect("eight bytes")),
269 length: u32::from_le_bytes(bytes[8..12].try_into().expect("four bytes")),
270 hash: u64::from_le_bytes(bytes[12..20].try_into().expect("eight bytes")),
271 first: u64::from_le_bytes(bytes[20..28].try_into().expect("eight bytes")),
272 };
273 if extent.length > MAX_EXTENT {
274 return Err(malformed("an extent is larger than the maximum extent"));
275 }
276 Ok(extent)
277 }
278}
279
280/// Encodes a whole extent table, checking that it describes a contiguous run of elements.
281///
282/// # Errors
283///
284/// If an extent is oversized, if the `first` counts are not increasing, or if there are more
285/// extents than [`MAX_EXTENTS`]. The increasing check is what makes a binary search over the table
286/// meaningful, and an unchecked one would be a search that silently returned the wrong extent.
287pub fn encode_extents(extents: &[Extent], out: &mut Vec<u8>) -> Result<()> {
288 if extents.len() > MAX_EXTENTS as usize {
289 return Err(malformed("a section names more extents than the bound allows"));
290 }
291 for (at, extent) in extents.iter().enumerate() {
292 if at == 0 {
293 if extent.first != 0 {
294 return Err(malformed("a section's first extent does not start at element zero"));
295 }
296 } else if extent.first <= extents[at - 1].first {
297 return Err(malformed("a section's extents are not in element order"));
298 }
299 extent.encode(out)?;
300 }
301 Ok(())
302}
303
304/// Decodes a whole extent table.
305///
306/// # Errors
307///
308/// If the byte count is not a multiple of an entry, if an entry is malformed, or if the entries are
309/// not in element order.
310pub fn decode_extents(bytes: &[u8]) -> Result<Vec<Extent>> {
311 if bytes.len() % EXTENT_BYTES != 0 {
312 return Err(malformed("an extent table is not a whole number of entries"));
313 }
314 let mut extents: Vec<Extent> = Vec::with_capacity(bytes.len() / EXTENT_BYTES);
315 for chunk in bytes.chunks(EXTENT_BYTES) {
316 let extent = Extent::decode(chunk)?;
317 match extents.last() {
318 None if extent.first != 0 => {
319 return Err(malformed("a section's first extent does not start at element zero"));
320 }
321 Some(previous) if extent.first <= previous.first => {
322 return Err(malformed("a section's extents are not in element order"));
323 }
324 _ => {}
325 }
326 extents.push(extent);
327 }
328 Ok(extents)
329}
330
331/// Which extent holds a given logical element, by binary search over the table.
332///
333/// Returns the index into `extents` and the element's offset within that extent's elements, or
334/// `None` when there are no extents at all, which is the not-built entry of section 3.7.
335///
336/// It does not bound the element from above, because an extent table cannot: the last extent's
337/// length is in bytes and only the caller knows how many elements a byte holds. So an element past
338/// the end answers with an offset past the end of the last extent, and the caller checks that
339/// against the count it already has. `None` rather than an error for the empty case because a
340/// stale link may name a structure that is no longer there, and section 3.1 wants staleness
341/// ignored.
342#[must_use]
343pub fn locate(extents: &[Extent], element: u64) -> Option<(usize, u64)> {
344 let at = extents.partition_point(|extent| extent.first <= element);
345 if at == 0 {
346 return None;
347 }
348 Some((at - 1, element - extents[at - 1].first))
349}
350
351fn malformed(message: impl Into<String>) -> Error {
352 Error::invalid_input(format!("invalid rudb section table: {}", message.into()))
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 fn entry() -> Section {
360 Section {
361 kind: *KEY_MAP,
362 id: 7,
363 generation: 42,
364 extents: 3,
365 extent_page: 1 << 20,
366 extent_bytes: 84,
367 hash: 0xdead_beef_cafe_f00d,
368 flags: 2,
369 header_bytes: 24,
370 }
371 }
372
373 #[test]
374 fn an_entry_takes_fifty_six_bytes_and_round_trips() {
375 let mut bytes = Vec::new();
376 entry().encode(&mut bytes).expect("encode");
377 assert_eq!(bytes.len(), ENTRY_BYTES, "section 3.2 says fifty six");
378 assert_eq!(Section::decode(&bytes).expect("decode"), entry());
379 }
380
381 #[test]
382 fn an_unknown_kind_is_carried_and_not_read() {
383 // The rule that makes this the last bump the mechanism needs. A build that met this entry
384 // before the kind existed has to be able to hold it, report it as not understood, and open
385 // the table anyway.
386 let mut unknown = entry();
387 unknown.kind = *b"RUDBZZ9\0";
388 let mut bytes = Vec::new();
389 unknown.encode(&mut bytes).expect("an unknown kind still encodes");
390 let read = Section::decode(&bytes).expect("an unknown kind still decodes");
391 assert_eq!(read, unknown, "the entry survives a build that does not know it");
392 assert!(!read.known());
393 assert!(!read.usable(42), "a kind this build does not know is never read");
394 }
395
396 #[test]
397 fn the_kinds_the_two_documents_name_are_known() {
398 for kind in [KEY_MAP, FORWARD_LINK, ADJACENCY, SUMMARY, SKETCHES] {
399 let mut section = entry();
400 section.kind = *kind;
401 assert!(section.known(), "{}", String::from_utf8_lossy(kind));
402 }
403 }
404
405 #[test]
406 fn no_two_kinds_share_a_tag() {
407 // Worth a test now that two documents assign them. A collision would mean one kind's payload
408 // read by the other's decoder, which is the one thing an opaque payload cannot defend
409 // against by itself.
410 let all = [KEY_MAP, FORWARD_LINK, ADJACENCY, SUMMARY, SKETCHES];
411 for (at, one) in all.iter().enumerate() {
412 for other in &all[at + 1..] {
413 assert_ne!(one, other, "{}", String::from_utf8_lossy(*one));
414 }
415 }
416 }
417
418 #[test]
419 fn a_stale_section_is_ignored_rather_than_repaired() {
420 // Section 3.1's staleness rule, which is the whole of the maintenance story: the generation
421 // stamp not matching removes the section from consideration, and there is no third state
422 // between usable and ignored for a repair path to live in.
423 let section = entry();
424 assert!(section.usable(42));
425 assert!(!section.usable(43), "a rewrite invalidates rather than corrupts");
426 assert!(section.known(), "staleness is not the same question as familiarity");
427 }
428
429 #[test]
430 fn an_entry_naming_more_extents_than_the_bound_is_refused_at_both_ends() {
431 let mut oversized = entry();
432 oversized.extents = MAX_EXTENTS + 1;
433 assert!(oversized.encode(&mut Vec::new()).is_err(), "a writer's bug stops at the write");
434
435 let mut bytes = Vec::new();
436 entry().encode(&mut bytes).expect("encode");
437 bytes[24..28].copy_from_slice(&(MAX_EXTENTS + 1).to_le_bytes());
438 assert!(Section::decode(&bytes).is_err(), "a torn count is not turned into an allocation");
439 }
440
441 #[test]
442 fn a_short_entry_is_refused_rather_than_read_past() {
443 let mut bytes = Vec::new();
444 entry().encode(&mut bytes).expect("encode");
445 bytes.pop();
446 assert!(Section::decode(&bytes).is_err());
447 assert!(Section::decode(&[]).is_err());
448 }
449
450 #[test]
451 fn an_extent_at_the_maximum_is_allowed_and_one_past_it_is_not() {
452 // The bound is the point of the split, so the boundary is the case worth pinning: sixty
453 // four megabytes exactly has to work, because a payload that is a multiple of it would
454 // otherwise be unwritable.
455 let at_bound = Extent { offset: 4096, length: MAX_EXTENT, hash: 9, first: 0 };
456 let mut bytes = Vec::new();
457 at_bound.encode(&mut bytes).expect("an extent at the bound encodes");
458 assert_eq!(bytes.len(), EXTENT_BYTES);
459 assert_eq!(Extent::decode(&bytes).expect("decode"), at_bound);
460
461 let past = Extent { offset: 4096, length: MAX_EXTENT + 1, hash: 9, first: 0 };
462 assert!(past.encode(&mut Vec::new()).is_err());
463 }
464
465 fn table() -> Vec<Extent> {
466 vec![
467 Extent { offset: 1024, length: MAX_EXTENT, hash: 1, first: 0 },
468 Extent {
469 offset: 1024 + u64::from(MAX_EXTENT),
470 length: MAX_EXTENT,
471 hash: 2,
472 first: 100,
473 },
474 Extent { offset: 1024 + 2 * u64::from(MAX_EXTENT), length: 512, hash: 3, first: 250 },
475 ]
476 }
477
478 #[test]
479 fn an_extent_table_round_trips() {
480 let mut bytes = Vec::new();
481 encode_extents(&table(), &mut bytes).expect("encode");
482 assert_eq!(bytes.len(), 3 * EXTENT_BYTES);
483 assert_eq!(decode_extents(&bytes).expect("decode"), table());
484 }
485
486 #[test]
487 fn an_extent_table_out_of_element_order_is_refused() {
488 // The order is what makes the binary search in `locate` mean anything, so an unordered
489 // table has to be refused rather than searched: a search over one would return a plausible
490 // extent holding the wrong elements.
491 let mut out_of_order = table();
492 out_of_order.swap(1, 2);
493 assert!(encode_extents(&out_of_order, &mut Vec::new()).is_err());
494
495 let mut bytes = Vec::new();
496 encode_extents(&table(), &mut bytes).expect("encode");
497 bytes[EXTENT_BYTES + 20..EXTENT_BYTES + 28].copy_from_slice(&0_u64.to_le_bytes());
498 assert!(decode_extents(&bytes).is_err(), "a torn element order is refused");
499 }
500
501 #[test]
502 fn an_extent_table_not_starting_at_element_zero_is_refused() {
503 let mut shifted = table();
504 shifted[0].first = 1;
505 assert!(encode_extents(&shifted, &mut Vec::new()).is_err());
506 }
507
508 #[test]
509 fn a_partial_extent_table_is_refused_rather_than_truncated() {
510 let mut bytes = Vec::new();
511 encode_extents(&table(), &mut bytes).expect("encode");
512 bytes.truncate(bytes.len() - 1);
513 assert!(decode_extents(&bytes).is_err());
514 }
515
516 #[test]
517 fn an_empty_extent_table_is_a_section_with_no_payload() {
518 // A relationship recorded as not built, per section 3.7, is an entry with no extents. It
519 // has to be legal, because that is how `rudb_links()` reports what a larger budget would
520 // buy.
521 let mut bytes = Vec::new();
522 encode_extents(&[] as &[Extent], &mut bytes).expect("encode");
523 assert!(bytes.is_empty());
524 assert!(decode_extents(&bytes).expect("decode").is_empty());
525 assert_eq!(locate(&[], 0), None);
526 }
527
528 #[test]
529 fn an_element_resolves_to_the_extent_holding_it() {
530 let extents = table();
531 assert_eq!(locate(&extents, 0), Some((0, 0)));
532 assert_eq!(locate(&extents, 99), Some((0, 99)));
533 assert_eq!(locate(&extents, 100), Some((1, 0)), "the first element of the second extent");
534 assert_eq!(locate(&extents, 249), Some((1, 149)));
535 assert_eq!(locate(&extents, 250), Some((2, 0)));
536 assert_eq!(locate(&extents, 1_000_000), Some((2, 999_750)), "past the end of the elements");
537 }
538
539 #[test]
540 fn a_two_gigabyte_payload_is_tens_of_extents_and_not_one_buffer() {
541 // The arithmetic issue #745 is about, and the reason the split is a rule rather than an
542 // option. An SF100 lineitem forward link is 600,037,902 rows at 28 bits, which is 2.10 GB,
543 // and no reader should be asked to hold that in one buffer to checksum it.
544 let payload = 600_037_902_u64 * 28 / 8;
545 let extents = payload.div_ceil(u64::from(MAX_EXTENT));
546 assert!(extents > 30, "{extents} extents");
547 assert!(extents < u64::from(MAX_EXTENTS), "{extents} extents is inside the bound");
548 }
549}