Skip to main content

rudb_native/
graph.rs

1//! Building a table's graph sections from the table's own columns.
2//!
3//! This is where the two halves meet. `rudb-graph` at rank 5 knows what a key map is and knows
4//! nothing about a file; the rest of this crate knows how to put an opaque payload in a file and
5//! nothing about what one means. Neither of them can build a key map for a real table, because
6//! doing that means reading a column back, so it happens here, in the crate that is allowed to see
7//! both.
8//!
9//! Everything here obeys spec/graph/03-the-file-format.md section 3.1. A column that cannot be
10//! mapped is a column with no key map, not an error at open; a section that is stale, torn, or of a
11//! form this build does not know is a section that is not there. That is why [`key_map`] answers
12//! with an [`Option`] and not a [`Result`]: there is no failure it could report that is not
13//! answered by running the query the way it ran before the section existed.
14
15use std::path::Path;
16use std::time::{Duration, Instant};
17
18use rudb_common::{LogicalType, Result, Value};
19use rudb_graph::{Degrees, Form, KeyMap, Keys, NO_PARENT, link, wire};
20use rudb_vector::Chunk;
21
22use crate::section::{self, Attachment};
23use crate::{Catalog, Reader, invalid, type_tag};
24
25/// One column of a committed table, scanned in `rid` order.
26///
27/// A `rid` is a row's position in append order, and the parts of a table are in append order, so a
28/// scan of the parts in order is a scan in `rid` order and there is nothing to look up. That is the
29/// whole of the correspondence and it is worth stating, because a build that read the parts in any
30/// other order would produce a map that resolved every key to the wrong row without failing.
31#[derive(Debug)]
32pub struct KeyColumn<'a> {
33    reader: &'a Reader,
34    column: usize,
35}
36
37impl<'a> KeyColumn<'a> {
38    /// Names a column of a table as the key column of a relationship's parent side.
39    ///
40    /// # Errors
41    ///
42    /// If there is no such column, or if its type has no integer key form. `VARCHAR` is the second
43    /// of those today: section 2.2 says a string key is mapped through its dictionary codes rather
44    /// than its text, and the code path is not built yet. None of TPC-H's eight relationships needs
45    /// it, so it is refused by name rather than approximated.
46    pub fn new(reader: &'a Reader, column: usize) -> Result<Self> {
47        let fields = reader.table().fields();
48        let Some(field) = fields.get(column) else {
49            return Err(invalid(&format!(
50                "column {column} is past the {} of table {}",
51                fields.len(),
52                reader.table().name()
53            )));
54        };
55        if !mappable(&field.ty) {
56            return Err(invalid(&format!(
57                "a key map over {} needs an integer key form, and {} has none",
58                field.name, field.ty
59            )));
60        }
61        Ok(Self { reader, column })
62    }
63}
64
65impl Keys for KeyColumn<'_> {
66    fn scan(&self, each: &mut dyn FnMut(Option<i128>) -> Result<()>) -> Result<()> {
67        for part in 0..self.reader.parts() {
68            let chunk = self.reader.read(part, &[self.column])?;
69            let values = chunk.column(0)?;
70            for row in 0..chunk.len() {
71                each(key_at(&chunk, values, row)?)?;
72            }
73        }
74        Ok(())
75    }
76}
77
78/// Whether a column of this type can be a key at all.
79fn mappable(ty: &LogicalType) -> bool {
80    matches!(
81        ty,
82        LogicalType::TinyInt
83            | LogicalType::SmallInt
84            | LogicalType::Integer
85            | LogicalType::BigInt
86            | LogicalType::HugeInt
87            | LogicalType::UTinyInt
88            | LogicalType::USmallInt
89            | LogicalType::UInteger
90            | LogicalType::UBigInt
91            | LogicalType::Date
92            | LogicalType::Decimal { .. }
93    )
94}
95
96/// One key out of a decoded part.
97///
98/// The fast answer first, because it covers the flat and dictionary forms and is a load. It hands
99/// back `None` for a null and for a form it cannot read, and those two are not the same thing at
100/// all: a null shifts every row after it and a value this could not read would shift nothing while
101/// silently becoming one. So the slow path settles which it was, and a value that is neither is an
102/// error rather than a null.
103fn key_at(chunk: &Chunk, values: &rudb_vector::Vector, row: usize) -> Result<Option<i128>> {
104    if let Some(key) = values.signed_at(row) {
105        return Ok(Some(key));
106    }
107    match chunk.value_at(row, 0) {
108        Value::Null => Ok(None),
109        Value::TinyInt(key) => Ok(Some(i128::from(key))),
110        Value::SmallInt(key) => Ok(Some(i128::from(key))),
111        Value::Integer(key) | Value::Date(key) => Ok(Some(i128::from(key))),
112        Value::BigInt(key) | Value::Time(key) | Value::Timestamp(key) => Ok(Some(i128::from(key))),
113        Value::HugeInt(key) | Value::Decimal { unscaled: key, .. } => Ok(Some(key)),
114        Value::UTinyInt(key) => Ok(Some(i128::from(key))),
115        Value::USmallInt(key) => Ok(Some(i128::from(key))),
116        Value::UInteger(key) => Ok(Some(i128::from(key))),
117        Value::UBigInt(key) => Ok(Some(i128::from(key))),
118        other => Err(invalid(&format!("a key column holds {other}, which is not a key"))),
119    }
120}
121
122/// What building one key map cost and what it bought.
123///
124/// G1's exit measurement in spec/graph/10-milestones.md wants build time and bytes reported per
125/// table, so the build reports them rather than being timed from outside. The form is here because
126/// it is the number that explains the bytes: an identity map over fifteen million rows is the same
127/// size as one over five.
128#[derive(Debug, Clone, Copy)]
129pub struct Built {
130    /// Which column was mapped.
131    pub column: usize,
132    /// Which of the three forms the measurement chose.
133    pub form: Form,
134    /// Non-null keys in the column.
135    pub rows: u64,
136    /// Whether every key was distinct, which is section 2.3's verification and decides whether a
137    /// link may be built on this column at all.
138    pub distinct: bool,
139    /// What the map takes in the file, header included, or would have taken when it was not kept.
140    pub bytes: usize,
141    /// What the column it maps takes in the file, which is what the budget is a share of.
142    pub column_bytes: u64,
143    /// Whether the map was kept. False means it was built, measured, and found to cost more than
144    /// section 3.7 allows, so the file does not have it and the query plans as though key maps had
145    /// never been implemented.
146    pub built: bool,
147    /// How long the build took, the reading of the column included.
148    pub build: Duration,
149}
150
151/// Builds the key map for one column of a committed table.
152///
153/// # Errors
154///
155/// If the column cannot be read, is not a key type, or holds a value that is not a key.
156pub fn build_key_map(reader: &Reader, column: usize) -> Result<KeyMap> {
157    KeyMap::build_from(&KeyColumn::new(reader, column)?)
158}
159
160/// The share of a table's stored column bytes its graph sections are allowed to cost together.
161///
162/// Section 3.7. Ten percent, and the number matters less than the fact that there is one: a layer
163/// that can only make queries faster is a layer with no reason to stop, and this is the reason.
164/// What does not fit is not built, and the report says what it would have cost, so whether a larger
165/// budget would buy anything is a measurement rather than an argument. The `graph_budget` setting
166/// is what will move it, which is why the builder below takes it rather than reading this.
167pub const BUDGET_SHARE: u64 = 10;
168
169/// The size below which a table's graph sections always fit, whatever the share works out to.
170///
171/// A percentage of the stored bytes is the right rule for a structure whose size is worth arguing
172/// about, and it stops making sense at the bottom. An identity key map is forty bytes on a table of
173/// any size, and a key column of sequential integers is a constant delta, which encodes to almost
174/// nothing: ten percent of almost nothing is less than forty bytes, so the pure rule throws away
175/// the cheapest structure in the system for being expensive. What it would be measuring there is
176/// how well the column compressed, not what the cache costs.
177///
178/// Sixty four kilobytes is the point below which no answer to "should this be kept" is worth the
179/// cost of asking. It is four pages, it is invisible next to any table the graph layer is for, and
180/// it leaves every budget decision that matters to the share above.
181pub const BUDGET_FLOOR: u64 = 64 * 1024;
182
183/// Builds a key map for each of these columns and attaches them all in one commit.
184///
185/// One commit and not one each, because a checkpoint that built six maps and published six
186/// generations would be six chances to be interrupted halfway and six directories written where one
187/// would do.
188///
189/// # Errors
190///
191/// If the file cannot be opened, a column cannot be mapped, or the attach fails.
192pub fn build_key_maps(path: &Path, table: &str, columns: &[usize]) -> Result<Vec<Built>> {
193    build_key_maps_within(path, table, columns, BUDGET_SHARE)
194}
195
196/// The same, against a budget of `share` percent of the table's stored column bytes.
197///
198/// The budget is over the table and not over a column, because that is what section 3.7 says and
199/// because a per column rule would throw away the cheapest maps there are: an identity map is forty
200/// bytes whatever the table, and a narrow, well compressed key column can be smaller than four
201/// hundred. The sections already in the file that this call does not replace are counted as spent.
202///
203/// When the budget binds, the cheapest maps are admitted first. Section 3.7 orders by expected
204/// value, child rows over section bytes, and for a key map on its own the numerator is not yet
205/// known: nothing has declared a relationship over these columns, so no column is worth more than
206/// another and the ordering degenerates to the denominator. Cheapest first is that, and it is also
207/// the order that fits the most maps in the room there is. The forward link builder is where the
208/// numerator arrives.
209///
210/// # Errors
211///
212/// If the file cannot be opened, a column cannot be mapped, or the attach fails.
213pub fn build_key_maps_within(
214    path: &Path,
215    table: &str,
216    columns: &[usize],
217    share: u64,
218) -> Result<Vec<Built>> {
219    let reader = Catalog::open(path)?.table(table)?;
220    let column_bytes = reader.layout().columns_total();
221    let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
222    let mut spent = held_bytes(&reader, columns)?;
223    let mut report = Vec::with_capacity(columns.len());
224    let mut payloads = Vec::with_capacity(columns.len());
225    for &column in columns {
226        let start = Instant::now();
227        let map = build_key_map(&reader, column)?;
228        let payload = wire::encode(&map, type_tag(&reader.table().fields()[column].ty)?)?;
229        report.push(Built {
230            column,
231            form: map.form(),
232            rows: map.observed().rows,
233            distinct: map.observed().distinct,
234            bytes: payload.bytes.len(),
235            column_bytes,
236            built: false,
237            build: start.elapsed(),
238        });
239        payloads.push((column, payload));
240    }
241    // Cheapest first, and the report keeps the order it was asked in, so the two are walked through
242    // an index rather than by sorting either of them.
243    let mut order = (0..payloads.len()).collect::<Vec<_>>();
244    order.sort_by_key(|&at| payloads[at].1.bytes.len());
245    let mut keep = vec![false; payloads.len()];
246    for at in order {
247        // Before the budget, because this is not a budget decision. A key map over a column whose
248        // key repeats cannot answer a rid for any of its keys, so keeping it would spend the
249        // table's allowance on something no join may read, and the report already says what it
250        // would have cost.
251        if !report[at].distinct {
252            continue;
253        }
254        let cost = payloads[at].1.bytes.len() as u64;
255        if spent.saturating_add(cost) <= allowance {
256            spent += cost;
257            keep[at] = true;
258            report[at].built = true;
259        }
260    }
261    // The reader holds the file open and the attach opens it again to write. Dropping it first is
262    // not required by any platform we build for, and it is done anyway so that the moment the
263    // file is being written is a moment nothing else in this function is reading it.
264    drop(reader);
265    // Every column that was asked for gets an entry, and a column whose map was not kept gets one
266    // with no bytes. That is section 3.7's budget record: what it would have cost is in the entry
267    // rather than in a payload, so `rudb_links()` reports a number instead of a silence and the
268    // file grows by fifty six bytes for the columns it decided against.
269    let attachments = payloads
270        .iter()
271        .zip(&keep)
272        .map(|((column, payload), &keep)| {
273            Ok(Attachment {
274                kind: *section::KEY_MAP,
275                id: u64::try_from(*column).map_err(|_| invalid("column index overflow"))?,
276                flags: payload.flags,
277                header_bytes: if keep { payload.header_bytes } else { cost(payload.bytes.len()) },
278                bytes: if keep { &payload.bytes } else { &[] },
279            })
280        })
281        .collect::<Result<Vec<_>>>()?;
282    crate::attach(path, table, &attachments)?;
283    Ok(report)
284}
285
286/// What the table's existing graph sections cost, leaving out the key maps this build is replacing.
287///
288/// Graph sections only. The statistics layer has its own two percent per `spec/stats` section 3.8,
289/// and a budget that counted the other layer's sections would be a budget the other layer eats,
290/// which is the thing the two shares being separate numbers exists to prevent.
291///
292/// Reading the extent tables is what this costs, which is one small read per section and not a read
293/// of a payload. A section whose extent table does not checksum is counted as nothing, because it
294/// is a section that is already not there.
295fn held_bytes(reader: &Reader, replacing: &[usize]) -> Result<u64> {
296    held_bytes_except(reader, *section::KEY_MAP, replacing)
297}
298
299/// The key map this table carries for a column, when it carries one this build can use.
300///
301/// `None` covers every reason there is not one, and covering them all is the point rather than an
302/// omission. Section 3.1 says deleting every graph section changes no answer, only the time, so
303/// there is no reason to distinguish *no map was ever built* from *the map is stale*, *the payload
304/// does not checksum*, or *the form is one a later build invented*: the answer to all four is to
305/// run the query the way it ran before key maps existed. A caller that wants to know which it was
306/// reads the entry out of [`crate::Table::sections`], which is where `rudb_links()` will look.
307#[must_use]
308pub fn key_map(reader: &Reader, column: usize) -> Option<KeyMap> {
309    let table = reader.table();
310    let id = u64::try_from(column).ok()?;
311    let held = table
312        .sections()
313        .iter()
314        .find(|section| section.kind == *section::KEY_MAP && section.id == id)?;
315    if !held.usable(table.generation()) {
316        return None;
317    }
318    let (map, tag) = wire::decode(&reader.payload(held).ok()?).ok()?;
319    // A map built against a different type than the column now has is a map built for a table that
320    // is no longer this one. It should be unreachable, since changing a column's type rewrites the
321    // table and moves its generation, and it is checked rather than assumed because the cost of
322    // being wrong is every key resolving to a plausible wrong row.
323    if tag != type_tag(&table.fields().get(column)?.ty).ok()? {
324        return None;
325    }
326    Some(map)
327}
328
329/// One relationship, with both sides resolved to a table and a column of it.
330///
331/// Names and not [`rudb_graph::Relationship`], because by the time a build runs the caller has
332/// already turned a declaration's column names into positions against the catalog, and doing it
333/// again here would be a second place for the two to disagree.
334#[derive(Debug, Clone)]
335pub struct Edge {
336    /// The many side, which is where the link is stored.
337    pub child: String,
338    /// Which column of it holds the key.
339    pub child_column: usize,
340    /// The one side, which is where the key map is.
341    pub parent: String,
342    /// Which column of it holds the key.
343    pub parent_column: usize,
344}
345
346/// What building one forward link cost and what it bought.
347#[derive(Debug, Clone)]
348pub struct BuiltLink {
349    /// The relationship this is a link for.
350    pub edge: Edge,
351    /// Which form section 3.4's measurement chose, or `None` when nothing was built.
352    pub form: Option<link::Form>,
353    /// Rows in the child table.
354    pub children: u64,
355    /// Children that found a parent. Below `children` means the foreign key is not total, which is
356    /// legal and is also what keeps the relationship out of the monotone form.
357    pub linked: u64,
358    /// What the link takes in the file, header included, or would have taken when it was not kept.
359    pub bytes: usize,
360    /// The stored column bytes of the child table, which is what section 3.7's budget is a share
361    /// of and what the size claim of section 9.1 is measured against.
362    pub table_bytes: u64,
363    /// What the build measured of the relationship's shape, or `None` when nothing was built.
364    ///
365    /// These ride along with the link rather than being computed for their own sake, because the
366    /// pass that resolves every child's parent is the pass that counts degrees. They are stored in
367    /// their own section and are what `rudb_links()` reports in its degree columns.
368    pub degrees: Option<Degrees>,
369    /// Whether it is in the file.
370    pub built: bool,
371    /// Why not, when not. `None` when it is.
372    pub note: Option<String>,
373    /// How long the build took, the reading of the child column included.
374    pub build: Duration,
375}
376
377/// Builds a forward link for each relationship and attaches each child table's in one commit.
378///
379/// The parent's key map has to be in the file already. Section 3.8 is explicit that this is a
380/// second pass at checkpoint time for exactly that reason, so a missing key map here is a note on
381/// the report rather than an error: the relationship is one the file does not accelerate, and by
382/// section 3.1 that changes no answer.
383///
384/// # Errors
385///
386/// If the file cannot be opened, a child key column cannot be read, or the attach fails.
387pub fn build_links(path: &Path, edges: &[Edge]) -> Result<Vec<BuiltLink>> {
388    build_links_within(path, edges, BUDGET_SHARE)
389}
390
391/// The same, against a budget of `share` percent of each child table's stored column bytes.
392///
393/// One commit per child table, for the reason [`build_key_maps`] commits once: a checkpoint that
394/// published a generation per section would be a chance to be interrupted per section.
395///
396/// The budget is where a link differs from a key map. Section 3.7 orders by expected value, child
397/// rows over section bytes, and for a link both numbers are in hand: the child rows are the rows
398/// the link would skip a hash table for. So this sorts by rows over bytes descending, which admits
399/// the monotone links first on any TPC-H sized file, because they are the ones with the most rows
400/// behind the fewest bytes.
401///
402/// # Errors
403///
404/// If the file cannot be opened, a child key column cannot be read, or the attach fails.
405pub fn build_links_within(path: &Path, edges: &[Edge], share: u64) -> Result<Vec<BuiltLink>> {
406    let mut tables: Vec<&str> = Vec::new();
407    for edge in edges {
408        if !tables.iter().any(|held| *held == edge.child) {
409            tables.push(&edge.child);
410        }
411    }
412    let mut report = Vec::with_capacity(edges.len());
413    for table in tables {
414        let mine = edges.iter().filter(|edge| edge.child == table).cloned().collect::<Vec<Edge>>();
415        report.extend(links_of_one_table(path, table, &mine, share)?);
416    }
417    Ok(report)
418}
419
420/// Every link stored in one child table, built and admitted and attached together.
421fn links_of_one_table(
422    path: &Path,
423    table: &str,
424    edges: &[Edge],
425    share: u64,
426) -> Result<Vec<BuiltLink>> {
427    let catalog = Catalog::open(path)?;
428    let child = catalog.table(table)?;
429    let column_bytes = child.layout().columns_total();
430    let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
431    let replacing = edges.iter().map(|edge| edge.child_column).collect::<Vec<usize>>();
432    let mut spent = held_bytes_except(&child, *section::FORWARD_LINK, &replacing)?;
433    let mut report = Vec::with_capacity(edges.len());
434    let mut payloads: Vec<Option<Vec<u8>>> = Vec::with_capacity(edges.len());
435    for edge in edges {
436        let start = Instant::now();
437        match one_link(&catalog, &child, edge) {
438            Ok((built, bytes)) => {
439                report.push(BuiltLink {
440                    build: start.elapsed(),
441                    table_bytes: column_bytes,
442                    ..built
443                });
444                payloads.push(Some(bytes));
445            }
446            Err(note) => {
447                report.push(BuiltLink {
448                    edge: edge.clone(),
449                    form: None,
450                    children: child.table().rows() as u64,
451                    linked: 0,
452                    bytes: 0,
453                    table_bytes: column_bytes,
454                    degrees: None,
455                    built: false,
456                    note: Some(note),
457                    build: start.elapsed(),
458                });
459                payloads.push(None);
460            }
461        }
462    }
463    let mut order = (0..report.len()).filter(|at| payloads[*at].is_some()).collect::<Vec<_>>();
464    // Most rows per byte first. A link over no rows is worth nothing per byte and sorts last
465    // rather than dividing by zero.
466    order.sort_by(|left, right| {
467        let value = |at: &usize| -> f64 {
468            let bytes = report[*at].bytes.max(1);
469            report[*at].children as f64 / bytes as f64
470        };
471        value(right).partial_cmp(&value(left)).unwrap_or(std::cmp::Ordering::Equal)
472    });
473    for at in order {
474        let cost = report[at].bytes as u64;
475        if spent.saturating_add(cost) <= allowance {
476            spent += cost;
477            report[at].built = true;
478        } else {
479            report[at].note = Some(format!("over the budget of {allowance} bytes"));
480        }
481    }
482    drop(child);
483    // The degree payloads are held here rather than built inside the loop below, because an
484    // attachment borrows its bytes and a temporary would not outlive the call.
485    let measured = report
486        .iter()
487        .filter(|built| built.built)
488        .filter_map(|built| {
489            let mut bytes = Vec::with_capacity(rudb_graph::degree::BYTES);
490            built.degrees.as_ref()?.write(&mut bytes);
491            Some((built.edge.child_column, bytes))
492        })
493        .collect::<Vec<_>>();
494    // A relationship whose link was built gets the link. One that was measured and then turned away
495    // gets an entry with no bytes, holding the form it would have taken and what it would have cost,
496    // which is section 3.7's budget record and is what exit criterion 3 of G3 reads. One that could
497    // not be built at all gets nothing, because there is no size to report: the note on the report
498    // is the whole of what is known about it.
499    let mut attachments = report
500        .iter()
501        .zip(&payloads)
502        .filter(|(_, payload)| payload.is_some())
503        .map(|(built, payload)| {
504            let bytes = payload.as_ref().expect("filtered to the measured");
505            Ok(Attachment {
506                kind: *section::FORWARD_LINK,
507                id: u64::try_from(built.edge.child_column)
508                    .map_err(|_| invalid("column index overflow"))?,
509                flags: built.form.map_or(0, |form| u32::from(form.tag())),
510                header_bytes: if built.built {
511                    u32::try_from(binding_bytes(&built.edge.parent))
512                        .map_err(|_| invalid("a parent name longer than a section header"))?
513                } else {
514                    cost(bytes.len())
515                },
516                bytes: if built.built { bytes } else { &[] },
517            })
518        })
519        .collect::<Result<Vec<_>>>()?;
520    // The same id as the link, so that a rebuild replaces both and a reader that wants the shape of
521    // a relationship it can resolve finds them the same way. The degrees are attached only for a
522    // link that was kept: on their own they would describe a relationship the file cannot follow,
523    // which is a planning hint for a plan that is not available.
524    for (column, bytes) in &measured {
525        attachments.push(Attachment {
526            kind: *section::DEGREES,
527            id: u64::try_from(*column).map_err(|_| invalid("column index overflow"))?,
528            flags: 0,
529            header_bytes: 0,
530            bytes,
531        });
532    }
533    crate::attach(path, table, &attachments)?;
534    Ok(report)
535}
536
537/// Builds one link, or says in one sentence why there is not one.
538///
539/// The error type is a `String` and not an [`rudb_common::Error`] on purpose. Every reason a link
540/// cannot be built here is a reason to not have one, which section 3.1 says is a slower query and
541/// not a failed one, so the caller's response is the same for all of them and a message is what it
542/// needs. A genuine I/O failure still arrives as an error, through the `?` on the scan.
543fn one_link(
544    catalog: &Catalog,
545    child: &Reader,
546    edge: &Edge,
547) -> std::result::Result<(BuiltLink, Vec<u8>), String> {
548    let parent =
549        catalog.table(&edge.parent).map_err(|_| format!("no table named {}", edge.parent))?;
550    let map = key_map(&parent, edge.parent_column)
551        .ok_or_else(|| format!("no key map is stored for {}", edge.parent))?;
552    if !map.observed().usable_as_parent() {
553        return Err(format!("the key of {} is not unique", edge.parent));
554    }
555    let keys = KeyColumn::new(child, edge.child_column).map_err(|error| error.to_string())?;
556    let mut parents_of = Vec::with_capacity(child.table().rows());
557    let mut failed = None;
558    keys.scan(&mut |key| {
559        let parent = match key {
560            None => NO_PARENT,
561            Some(key) => match map.lookup(key) {
562                Ok(found) => found.unwrap_or(NO_PARENT),
563                Err(error) => {
564                    failed = Some(error.to_string());
565                    NO_PARENT
566                }
567            },
568        };
569        parents_of.push(parent);
570        Ok(())
571    })
572    .map_err(|error| error.to_string())?;
573    if let Some(failed) = failed {
574        return Err(failed);
575    }
576    let link = link::Link::build(&parents_of, map.len()).map_err(|error| error.to_string())?;
577    // The parent key is unique, because the check above refused the relationship otherwise. So the
578    // certificate is recorded here rather than discovered: a link only exists over a key map whose
579    // parent side was counted and found distinct.
580    //
581    // Its own pass over the same slice rather than a loop fused into the one above. The cost of
582    // measuring degrees is the scattered increment into a counter per parent and not the sequential
583    // read of the child column, which the build makes twice already, so fusing would save the cheap
584    // half and put a histogram inside a function whose job is to choose a form.
585    let degrees = Degrees::of(&parents_of, map.len(), true);
586    let bytes = encode_link(&link, &parent, edge).map_err(|error| error.to_string())?;
587    Ok((
588        BuiltLink {
589            edge: edge.clone(),
590            form: Some(link.form()),
591            children: link.children(),
592            linked: link.linked(),
593            bytes: bytes.len(),
594            table_bytes: 0,
595            degrees: Some(degrees),
596            built: false,
597            note: None,
598            build: Duration::ZERO,
599        },
600        bytes,
601    ))
602}
603
604/// What a structure that did not fit is recorded as having cost.
605///
606/// Saturating rather than erroring, because the number is a budget record and not a length: a
607/// structure past four gigabytes did not fit any budget this project sets, and refusing to write the
608/// record would turn a relationship that is merely too big into a build that fails.
609fn cost(bytes: usize) -> u32 {
610    u32::try_from(bytes).unwrap_or(u32::MAX)
611}
612
613/// Bytes of binding in front of a link's payload: which parent table, column and generation.
614///
615/// Eight for the generation, four for the column, four for the name's length, then the name padded
616/// out to eight so that the link's own header lands on a boundary.
617fn binding_bytes(parent: &str) -> usize {
618    16 + parent.len().div_ceil(8) * 8
619}
620
621/// The payload: the binding, then the link.
622///
623/// The binding is here and not in `rudb-graph`'s [`link::Link`], because a table name and a
624/// generation are file concepts and that crate is not allowed to know what a file is. It exists
625/// because the section's own id says only which child column the link is for, and a link resolved
626/// against the wrong parent is the one failure in this layer that is a wrong answer rather than a
627/// slow one. Section 3.1's staleness rule is *ignore, do not repair*, and this is what gives
628/// [`stored_link`] something to check before it believes a payload.
629fn encode_link(link: &link::Link, parent: &Reader, edge: &Edge) -> Result<Vec<u8>> {
630    let name = edge.parent.as_bytes();
631    let mut bytes = Vec::with_capacity(binding_bytes(&edge.parent) + link.bytes());
632    bytes.extend_from_slice(&parent.table().generation().to_le_bytes());
633    bytes.extend_from_slice(
634        &u32::try_from(edge.parent_column)
635            .map_err(|_| invalid("column index overflow"))?
636            .to_le_bytes(),
637    );
638    bytes.extend_from_slice(
639        &u32::try_from(name.len())
640            .map_err(|_| invalid("a parent name longer than a u32"))?
641            .to_le_bytes(),
642    );
643    bytes.extend_from_slice(name);
644    bytes.resize(binding_bytes(&edge.parent), 0);
645    link.write(&mut bytes)?;
646    Ok(bytes)
647}
648
649/// The forward link this child table carries for a column, when it carries one this build can use
650/// and the parent it was built against is still the parent being asked about.
651///
652/// `None` for every reason there might not be one, for the reason [`key_map`] answers the same way.
653/// The extra check here is the binding: a link whose stored parent name, column or generation is
654/// not the one the caller is asking for is a link built against a table that has since been
655/// rewritten, and resolving through it would produce a plausible wrong row rather than an error.
656#[must_use]
657pub fn stored_link(child: &Reader, parent: &Reader, edge: &Edge) -> Option<link::Link> {
658    let table = child.table();
659    let id = u64::try_from(edge.child_column).ok()?;
660    let held = table
661        .sections()
662        .iter()
663        .find(|section| section.kind == *section::FORWARD_LINK && section.id == id)?;
664    if !held.usable(table.generation()) {
665        return None;
666    }
667    let bytes = child.payload(held).ok()?;
668    let binding = binding_bytes(&edge.parent);
669    if bytes.len() < binding {
670        return None;
671    }
672    let generation = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
673    let column = u32::from_le_bytes(bytes[8..12].try_into().ok()?);
674    let length = u32::from_le_bytes(bytes[12..16].try_into().ok()?) as usize;
675    if generation != parent.table().generation()
676        || column as usize != edge.parent_column
677        || length != edge.parent.len()
678        || &bytes[16..16 + length] != edge.parent.as_bytes()
679    {
680        return None;
681    }
682    link::Link::read(&bytes[binding..]).ok()
683}
684
685/// What the build measured of a relationship's shape, when the child table carries it.
686///
687/// There is no binding to check, unlike [`stored_link`], because there is nothing here to resolve
688/// against the parent. Every number is about the child column and the generation stamp is the whole
689/// of what makes one of these current. A caller that wants to know the relationship is still the
690/// one it means asks [`stored_link`] as well, which it is doing anyway if it plans to follow it.
691#[must_use]
692pub fn stored_degrees(child: &Reader, child_column: usize) -> Option<Degrees> {
693    let table = child.table();
694    let id = u64::try_from(child_column).ok()?;
695    let held = table
696        .sections()
697        .iter()
698        .find(|section| section.kind == *section::DEGREES && section.id == id)?;
699    if !held.usable(table.generation()) {
700        return None;
701    }
702    Degrees::read(&child.payload(held).ok()?).ok()
703}
704
705/// What a key map over this column would have cost, when a build measured one and did not keep it.
706///
707/// This and [`key_map`] are exclusive: an entry either holds a map or records the absence of one,
708/// and which it is comes off the entry rather than out of a payload, so asking this costs nothing.
709/// Both answer `None` for a column no build has looked at, which is the third state and is the one
710/// where `rudb_links()` should say nothing rather than zero.
711#[must_use]
712pub fn refused_key_map(reader: &Reader, column: usize) -> Option<(Form, u64)> {
713    let (form, bytes) = refused(reader, *section::KEY_MAP, column)?;
714    Some((Form::from_tag(form).ok()?, bytes))
715}
716
717/// What a forward link for this column would have cost, when a build measured one and did not keep
718/// it. The counterpart of [`stored_link`], the way [`refused_key_map`] is the counterpart of
719/// [`key_map`].
720#[must_use]
721pub fn refused_link(child: &Reader, child_column: usize) -> Option<(link::Form, u64)> {
722    let (form, bytes) = refused(child, *section::FORWARD_LINK, child_column)?;
723    Some((link::Form::from_tag(form).ok()?, bytes))
724}
725
726/// The form tag and the size out of a budget record, when the table holds one for this id.
727fn refused(reader: &Reader, kind: [u8; 8], id: usize) -> Option<(u8, u64)> {
728    let table = reader.table();
729    let id = u64::try_from(id).ok()?;
730    let held = table.sections().iter().find(|section| section.kind == kind && section.id == id)?;
731    if !held.usable(table.generation()) {
732        return None;
733    }
734    Some((u8::try_from(held.flags).ok()?, held.refused()?))
735}
736
737/// What the table's sections of one kind cost, leaving out the ids this build is replacing.
738fn held_bytes_except(reader: &Reader, kind: [u8; 8], replacing: &[usize]) -> Result<u64> {
739    let mut total = 0;
740    for held in reader.table().sections() {
741        if !held.among(section::GRAPH_KINDS) {
742            continue;
743        }
744        let replaced =
745            held.kind == kind && replacing.iter().any(|&id| u64::try_from(id) == Ok(held.id));
746        if replaced || !held.usable(reader.table().generation()) {
747            continue;
748        }
749        let Ok(extents) = reader.extents(held) else { continue };
750        total += extents.iter().map(|extent| u64::from(extent.length)).sum::<u64>();
751    }
752    Ok(total)
753}
754
755#[cfg(test)]
756mod tests {
757    use std::fs;
758    use std::path::PathBuf;
759    use std::time::{SystemTime, UNIX_EPOCH};
760
761    use rudb_common::Field;
762    use rudb_graph::Rid;
763    use rudb_vector::Vector;
764
765    use super::*;
766    use crate::Writer;
767
768    fn path(label: &str) -> PathBuf {
769        let stamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("time advances").as_nanos();
770        std::env::temp_dir().join(format!("rudb-graph-{label}-{}-{stamp}.rdb", std::process::id()))
771    }
772
773    /// The graph sections of a table, which is every section this module could have written.
774    ///
775    /// A table carries a summary and a sketch per column out of the write itself now, and a test
776    /// about key maps is not about those. Filtering by kind rather than subtracting a count, so a
777    /// table whose summaries did not fit the budget does not quietly change what is asserted.
778    fn graph_sections(reader: &Reader) -> Vec<&section::Section> {
779        reader.table().sections().iter().filter(|held| held.among(section::GRAPH_KINDS)).collect()
780    }
781
782    /// A one column table of these keys, written a thousand rows to a part.
783    fn table_of(label: &str, keys: &[Option<i64>]) -> PathBuf {
784        let path = path(label);
785        let mut writer =
786            Writer::create(&path, "parent", vec![Field::new("key", LogicalType::BigInt)])
787                .expect("new file");
788        for part in keys.chunks(1000) {
789            let values =
790                part.iter().map(|key| key.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
791            let chunk =
792                Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
793                    .expect("one column");
794            writer.append(&chunk).expect("a part");
795        }
796        writer.finish().expect("commit");
797        path
798    }
799
800    /// Every key in the column resolves to the row that holds it.
801    fn resolves(keys: &[Option<i64>], map: &KeyMap) {
802        for (rid, key) in keys.iter().enumerate() {
803            let Some(key) = *key else { continue };
804            let found =
805                map.lookup(i128::from(key)).expect("lookup").expect("a key in the column resolves");
806            assert_eq!(found, rid as Rid, "key {key} resolved to {found} rather than {rid}");
807        }
808    }
809
810    #[test]
811    fn a_key_map_built_over_a_file_resolves_every_key_to_its_own_row() {
812        // The whole point, end to end: the column goes to disk, comes back through the reader, and
813        // every key finds the row it was written in. Three thousand rows so that the scan crosses
814        // part boundaries, because a build that read the parts in the wrong order would be right
815        // for one part and wrong for the rest.
816        let keys = (1..=3000_i64).map(Some).collect::<Vec<_>>();
817        let path = table_of("identity", &keys);
818        let built = build_key_maps(&path, "parent", &[0]).expect("build");
819        assert_eq!(built.len(), 1);
820        assert_eq!(built[0].form, Form::Identity);
821        assert_eq!(built[0].rows, 3000);
822        assert!(built[0].distinct);
823        assert_eq!(built[0].bytes, wire::HEADER_BYTES, "identity is a header and nothing else");
824
825        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
826        let map = key_map(&reader, 0).expect("the map is in the file");
827        assert_eq!(map.form(), Form::Identity);
828        resolves(&keys, &map);
829        assert_eq!(map.lookup(0).expect("a key below the column"), None);
830        assert_eq!(map.lookup(3001).expect("a key past the column"), None);
831
832        fs::remove_file(&path).expect("clean up");
833    }
834
835    #[test]
836    fn a_column_with_gaps_takes_the_bitmap_form_and_still_resolves() {
837        let keys = (0..2000_i64).map(|value| Some(value * 4 + 7)).collect::<Vec<_>>();
838        let path = table_of("dense", &keys);
839        let built = build_key_maps(&path, "parent", &[0]).expect("build");
840        assert_eq!(built[0].form, Form::Dense);
841
842        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
843        let map = key_map(&reader, 0).expect("the map is in the file");
844        resolves(&keys, &map);
845        assert_eq!(map.lookup(8).expect("a value in the range but not the column"), None);
846
847        fs::remove_file(&path).expect("clean up");
848    }
849
850    #[test]
851    fn a_column_out_of_order_takes_the_sorted_form_and_still_resolves() {
852        let keys = (0..1500_i64).map(|value| Some((value * 7919) % 100_003)).collect::<Vec<_>>();
853        let path = table_of("sorted", &keys);
854        let built = build_key_maps(&path, "parent", &[0]).expect("build");
855        assert_eq!(built[0].form, Form::Sorted);
856        assert!(built[0].distinct, "the sort settles distinctness for an unordered column");
857
858        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
859        let map = key_map(&reader, 0).expect("the map is in the file");
860        resolves(&keys, &map);
861
862        fs::remove_file(&path).expect("clean up");
863    }
864
865    #[test]
866    fn a_null_in_the_key_column_does_not_shift_the_rows_after_it() {
867        // The failure this whole crate is most exposed to. A null is not a key, but it is a row, so
868        // a form that answers with a count of keys below a value answers one short for every row
869        // after it. It does not crash and it does not look wrong: it resolves every key to a
870        // neighbour of the right row.
871        let mut keys = (1..=1200_i64).map(Some).collect::<Vec<_>>();
872        keys[3] = None;
873        keys[900] = None;
874        let path = table_of("nulls", &keys);
875        let built = build_key_maps(&path, "parent", &[0]).expect("build");
876        assert_eq!(built[0].rows, 1198, "a null is not a key");
877
878        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
879        let map = key_map(&reader, 0).expect("the map is in the file");
880        resolves(&keys, &map);
881
882        fs::remove_file(&path).expect("clean up");
883    }
884
885    #[test]
886    fn a_column_with_a_repeat_in_it_is_mapped_and_reported_as_no_parent() {
887        // Section 2.3: a parent side that is not unique is not an error and is not a link. It is
888        // also not a key map. The repeat here is not next to itself, so only the sort can find it
889        // and the bytes are spent before anybody knows, which is why the report carries what it
890        // cost and the file does not.
891        let mut keys = (1..=500_i64).map(Some).collect::<Vec<_>>();
892        keys[200] = Some(7);
893        let path = table_of("repeat", &keys);
894        let built = build_key_maps(&path, "parent", &[0]).expect("build");
895        assert!(!built[0].distinct, "a repeat is observed rather than declared away");
896        assert!(!built[0].built, "and a map no rid can be resolved through is not kept");
897        assert!(built[0].bytes > 0, "what it would have cost is still reported");
898
899        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
900        assert!(key_map(&reader, 0).is_none(), "no map was written to read back");
901        // What is written is the entry that says so, with no bytes behind it. Section 3.7 wants the
902        // size to survive the build that decided against it, and fifty six bytes of entry is the
903        // whole of what a refusal costs.
904        let (form, bytes) = refused_key_map(&reader, 0).expect("the record of what it would cost");
905        assert_eq!(form, built[0].form);
906        assert_eq!(bytes, built[0].bytes as u64);
907        assert_eq!(graph_sections(&reader).len(), 1, "one entry, and no payload");
908        assert_eq!(graph_sections(&reader)[0].extents, 0);
909
910        fs::remove_file(&path).expect("clean up");
911    }
912
913    #[test]
914    fn a_table_with_no_key_map_answers_with_none_rather_than_an_error() {
915        // Section 3.1 at the API. Every query has to be answerable with no section in the file, so
916        // asking for a map that is not there is a question with an answer and not a failure.
917        let path = table_of("absent", &[Some(1), Some(2)]);
918        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
919        assert!(key_map(&reader, 0).is_none());
920        assert!(key_map(&reader, 99).is_none(), "a column that does not exist is not a panic");
921        fs::remove_file(&path).expect("clean up");
922    }
923
924    #[test]
925    fn a_stale_key_map_is_ignored_and_the_table_still_reads() {
926        let path = table_of("stale", &(1..=100_i64).map(Some).collect::<Vec<_>>());
927        build_key_maps(&path, "parent", &[0]).expect("build");
928
929        // A second table in the same file moves the file's generation and not this table's, so the
930        // map stays current: that is the distinction `Table::generation` exists to make.
931        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
932        assert!(key_map(&reader, 0).is_some());
933        let generation = reader.table().generation();
934        drop(reader);
935
936        // And a map stamped against a generation this table is not at is dropped rather than used.
937        let held = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
938        let mut entry = *graph_sections(&held).first().copied().expect("the key map");
939        assert!(entry.usable(generation));
940        entry.generation = generation + 1;
941        assert!(!entry.usable(generation), "a rewrite invalidates rather than corrupts");
942
943        fs::remove_file(&path).expect("clean up");
944    }
945
946    #[test]
947    fn a_torn_key_map_costs_the_shortcut_and_not_the_query() {
948        let keys = (1..=200_i64).map(Some).collect::<Vec<_>>();
949        let path = table_of("torn", &keys);
950        build_key_maps(&path, "parent", &[0]).expect("build");
951
952        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
953        let extent = reader
954            .extents(graph_sections(&reader).first().copied().expect("the key map"))
955            .expect("extent table")
956            .first()
957            .copied()
958            .expect("one extent");
959        drop(reader);
960        let file = fs::OpenOptions::new().write(true).open(&path).expect("reopen to corrupt");
961        crate::write_at(&file, extent.offset, &[0xff; 8]).expect("flip the header");
962        drop(file);
963
964        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
965        assert!(key_map(&reader, 0).is_none(), "a payload that does not checksum is not a map");
966        assert_eq!(reader.table().rows(), 200, "and the table is untouched");
967
968        fs::remove_file(&path).expect("clean up");
969    }
970
971    #[test]
972    fn a_column_with_no_integer_key_form_is_refused_by_name() {
973        let path = path("varchar");
974        let mut writer =
975            Writer::create(&path, "parent", vec![Field::new("name", LogicalType::Varchar)])
976                .expect("new file");
977        let chunk = Chunk::new(vec![
978            Vector::from_values(LogicalType::Varchar, &[Value::Varchar("a".into())])
979                .expect("one name"),
980        ])
981        .expect("one column");
982        writer.append(&chunk).expect("a part");
983        writer.finish().expect("commit");
984
985        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
986        let error = KeyColumn::new(&reader, 0).expect_err("a string key needs its codes");
987        assert!(error.to_string().contains("integer key form"), "{error}");
988
989        fs::remove_file(&path).expect("clean up");
990    }
991
992    #[test]
993    fn several_columns_are_mapped_in_one_commit() {
994        let path = path("two_columns");
995        let mut writer = Writer::create(
996            &path,
997            "parent",
998            vec![
999                Field::required("id", LogicalType::BigInt),
1000                Field::required("code", LogicalType::Integer),
1001            ],
1002        )
1003        .expect("new file");
1004        let ids = (1..=400_i64).map(Value::BigInt).collect::<Vec<_>>();
1005        let codes = (1..=400_i32).map(|code| Value::Integer(code * 3)).collect::<Vec<_>>();
1006        let chunk = Chunk::new(vec![
1007            Vector::from_values(LogicalType::BigInt, &ids).expect("ids"),
1008            Vector::from_values(LogicalType::Integer, &codes).expect("codes"),
1009        ])
1010        .expect("two columns");
1011        writer.append(&chunk).expect("a part");
1012        writer.finish().expect("commit");
1013
1014        let built = build_key_maps(&path, "parent", &[0, 1]).expect("build both");
1015        assert_eq!(built.len(), 2);
1016        assert_eq!(built[0].form, Form::Identity);
1017        assert_eq!(built[1].form, Form::Dense);
1018
1019        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1020        assert_eq!(graph_sections(&reader).len(), 2, "one commit and two entries");
1021        assert_eq!(key_map(&reader, 0).expect("the id map").form(), Form::Identity);
1022        assert_eq!(key_map(&reader, 1).expect("the code map").form(), Form::Dense);
1023        assert_eq!(
1024            key_map(&reader, 1).expect("the code map").lookup(9).expect("lookup"),
1025            Some(2),
1026            "the third code is the third row"
1027        );
1028
1029        fs::remove_file(&path).expect("clean up");
1030    }
1031
1032    #[test]
1033    fn the_statistics_sections_do_not_count_against_the_graph_budget() {
1034        // The two shares are ten percent and two percent of the same column bytes, and separate
1035        // means each counts only what it owns. A graph build that counted summaries would be a
1036        // graph budget the statistics layer eats, and a table would lose key maps for a reason
1037        // that has nothing to do with key maps. The kind lists in `section` are what keeps the two
1038        // apart, and this is the direction of that which lives in this file.
1039        let keys = (1..=3000_i64).map(Some).collect::<Vec<_>>();
1040        let path = table_of("apart", &keys);
1041        crate::stats::build_stats(&path, "parent", &[0]).expect("summaries first");
1042
1043        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1044        let statistics = reader
1045            .table()
1046            .sections()
1047            .iter()
1048            .filter(|held| held.among(section::STATISTICS_KINDS))
1049            .count();
1050        assert_eq!(statistics, 2, "a summary and a sketch are in the file");
1051        assert_eq!(held_bytes(&reader, &[0]).expect("held"), 0, "and neither is the graph's");
1052
1053        drop(reader);
1054        fs::remove_file(&path).expect("clean up");
1055    }
1056
1057    #[test]
1058    fn a_map_that_does_not_fit_the_budget_is_measured_and_not_written() {
1059        // A column of twenty thousand even numbers is about the worst case there is for this: the
1060        // column encodes to a few hundred bytes because it is a run of a constant delta, and the
1061        // bitmap over it cannot be smaller than one bit per value in its range. So the map is an
1062        // order of magnitude larger than the column it maps and section 3.7 says it does not go in
1063        // the file. What comes back is the number, which is the point: a budget that silently drops
1064        // things teaches nobody anything.
1065        let keys = (0..100_000_i64).map(|value| Some(value * 8)).collect::<Vec<_>>();
1066        let path = table_of("budget", &keys);
1067        let built = build_key_maps(&path, "parent", &[0]).expect("build");
1068        assert_eq!(built[0].form, Form::Dense);
1069        assert!(!built[0].built, "a map ten times its column does not fit a tenth of it");
1070        assert!(built[0].bytes as u64 > built[0].column_bytes, "{built:?}");
1071
1072        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1073        assert!(key_map(&reader, 0).is_none(), "and no map was written");
1074        // The record of what it would have cost is what somebody raising `graph_budget` reads, and
1075        // it is the number the build reported rather than a rounding of it.
1076        assert_eq!(refused_key_map(&reader, 0), Some((Form::Dense, built[0].bytes as u64)));
1077        assert_eq!(held_bytes(&reader, &[]).expect("held"), 0, "a record costs the budget nothing");
1078        drop(reader);
1079
1080        // The same build against a budget that allows it keeps it, which is what `graph_budget`
1081        // will be for. Nothing else about the build changes.
1082        let built = build_key_maps_within(&path, "parent", &[0], 100_000).expect("build");
1083        assert!(built[0].built);
1084        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1085        let map = key_map(&reader, 0).expect("the map is in the file");
1086        resolves(&keys, &map);
1087
1088        fs::remove_file(&path).expect("clean up");
1089    }
1090
1091    #[test]
1092    fn the_budget_admits_the_cheapest_maps_it_can_fit() {
1093        // Two columns and room for one of them. The ids take the identity form, which is forty
1094        // bytes whatever the row count, and the scattered keys take the sorted form, which is the
1095        // keys and a permutation and so is larger than the tenth of the table it would need. So the
1096        // budget keeps the first and reports what the second would have cost, and it does that
1097        // although the second was asked for first.
1098        let path = path("budget_order");
1099        let mut writer = Writer::create(
1100            &path,
1101            "parent",
1102            vec![
1103                Field::required("id", LogicalType::BigInt),
1104                Field::required("code", LogicalType::BigInt),
1105            ],
1106        )
1107        .expect("new file");
1108        let ids = (1..=100_000_i64).map(Value::BigInt).collect::<Vec<_>>();
1109        let codes = (1..=100_000_i64)
1110            .map(|code| Value::BigInt((code * 2_147_483_647) % 999_999_937))
1111            .collect::<Vec<_>>();
1112        for part in 0..100 {
1113            let at = part * 1000;
1114            let chunk = Chunk::new(vec![
1115                Vector::from_values(LogicalType::BigInt, &ids[at..at + 1000]).expect("ids"),
1116                Vector::from_values(LogicalType::BigInt, &codes[at..at + 1000]).expect("codes"),
1117            ])
1118            .expect("two columns");
1119            writer.append(&chunk).expect("a part");
1120        }
1121        writer.finish().expect("commit");
1122
1123        let built = build_key_maps(&path, "parent", &[1, 0]).expect("build");
1124        assert_eq!(built[0].column, 1, "the report is in the order it was asked in");
1125        assert_eq!(built[0].form, Form::Sorted);
1126        assert!(!built[0].built, "the sorted map did not fit: {built:?}");
1127        assert!(built[1].built, "the identity map did, and was reached second: {built:?}");
1128
1129        let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1130        assert!(key_map(&reader, 0).is_some());
1131        assert!(key_map(&reader, 1).is_none());
1132
1133        fs::remove_file(&path).expect("clean up");
1134    }
1135
1136    /// A parent table of `parents` sequential keys and a child table of these foreign keys, with
1137    /// the parent's key map already built, which is the state section 3.8 says a link build starts
1138    /// from.
1139    fn related(label: &str, parents: i64, foreign: &[Option<i64>]) -> PathBuf {
1140        let path = table_of(label, &(1..=parents).map(Some).collect::<Vec<_>>());
1141        let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
1142            .expect("a second table");
1143        for part in foreign.chunks(1000) {
1144            let values =
1145                part.iter().map(|key| key.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
1146            let chunk =
1147                Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
1148                    .expect("one column");
1149            writer.append(&chunk).expect("a part");
1150        }
1151        writer.finish().expect("commit");
1152        build_key_maps(&path, "parent", &[0]).expect("the parent's key map");
1153        path
1154    }
1155
1156    fn edge() -> Edge {
1157        Edge { child: "child".into(), child_column: 0, parent: "parent".into(), parent_column: 0 }
1158    }
1159
1160    /// Reads the link back out of the file and checks every child against the key it was built
1161    /// from, which is the only assertion that catches a link that is off by a row.
1162    fn links(path: &PathBuf, foreign: &[Option<i64>]) -> link::Link {
1163        let catalog = Catalog::open(path).expect("reopen");
1164        let child = catalog.table("child").expect("the child");
1165        let parent = catalog.table("parent").expect("the parent");
1166        let link = stored_link(&child, &parent, &edge()).expect("the link is in the file");
1167        let map = key_map(&parent, 0).expect("the parent's key map");
1168        for (rid, key) in foreign.iter().enumerate() {
1169            let want = key.and_then(|key| map.lookup(i128::from(key)).expect("lookup"));
1170            assert_eq!(link.forward(rid as Rid), want, "child {rid}");
1171        }
1172        link
1173    }
1174
1175    #[test]
1176    fn a_clustered_foreign_key_takes_the_monotone_form_and_answers_both_directions() {
1177        // The shape `lineitem` has against `orders`, which is the relationship section 3.4's
1178        // arithmetic is about. Four children each of a thousand parents, in order.
1179        let foreign = (0..4000_i64).map(|child| Some(child / 4 + 1)).collect::<Vec<_>>();
1180        let path = related("monotone", 1000, &foreign);
1181        let report = build_links(&path, &[edge()]).expect("build");
1182        assert_eq!(report.len(), 1);
1183        assert!(report[0].built, "{:?}", report[0].note);
1184        assert_eq!(report[0].form, Some(link::Form::Monotone));
1185        assert_eq!(report[0].children, 4000);
1186        assert_eq!(report[0].linked, 4000);
1187
1188        let link = links(&path, &foreign);
1189        assert_eq!(link.form(), link::Form::Monotone);
1190        assert_eq!(link.backward(0), Some(0..4), "the first parent's four children");
1191        assert_eq!(link.backward(999), Some(3996..4000));
1192        assert_eq!(link.backward(1000), None, "past the last parent");
1193
1194        fs::remove_file(&path).expect("clean up");
1195    }
1196
1197    #[test]
1198    fn an_unclustered_foreign_key_takes_the_packed_form_and_still_resolves() {
1199        let foreign = (0..3000_i64).map(|child| Some((child * 7) % 1000 + 1)).collect::<Vec<_>>();
1200        let path = related("packed", 1000, &foreign);
1201        let report = build_links(&path, &[edge()]).expect("build");
1202        assert!(report[0].built, "{:?}", report[0].note);
1203        assert_eq!(report[0].form, Some(link::Form::Packed));
1204
1205        let link = links(&path, &foreign);
1206        assert_eq!(link.backward(0), None, "the packed form answers one direction");
1207        // Ten bits a child, a min and a max per part, and the header. The check is that it is a rid
1208        // per child and not a byte per child, because a link stored as a u64 array would also pass
1209        // every assertion above it.
1210        assert!(link.bytes() < 3000 * 2 + 3 * 16, "{} bytes is not bit-packed", link.bytes());
1211
1212        fs::remove_file(&path).expect("clean up");
1213    }
1214
1215    #[test]
1216    fn a_built_link_leaves_the_shape_of_the_relationship_beside_it() {
1217        // The same clustered shape as the monotone test, so the expected numbers are arithmetic
1218        // rather than an observation: four children each of a thousand parents, in order.
1219        let foreign = (0..4000_i64).map(|child| Some(child / 4 + 1)).collect::<Vec<_>>();
1220        let path = related("degrees", 1000, &foreign);
1221        let report = build_links(&path, &[edge()]).expect("build");
1222        assert!(report[0].built, "{:?}", report[0].note);
1223        let measured = report[0].degrees.as_ref().expect("the build measured it");
1224        assert!((measured.mean() - 4.0).abs() < 1e-9);
1225
1226        let catalog = Catalog::open(&path).expect("reopen");
1227        let child = catalog.table("child").expect("the child");
1228        let held = stored_degrees(&child, 0).expect("it is in the file");
1229        assert_eq!(&held, measured, "what the build measured is what the file holds");
1230        assert_eq!(held.parents(), 1000);
1231        assert_eq!(held.highest(), 4);
1232        assert!(held.total(), "every child found a parent");
1233        assert!(held.unique(), "and the parent key is why there is a link at all");
1234        // Three thousand nine hundred and ninety nine steps between adjacent children, of which the
1235        // nine hundred and ninety nine that cross into the next parent move by one and the rest
1236        // stay put. Which is what a clustered foreign key is, expressed as a number.
1237        let near = held.locality().expect("something to gather");
1238        assert!((near - 999.0 / 3999.0).abs() < 1e-9, "{near}");
1239        assert!(stored_degrees(&child, 1).is_none(), "and no other column has one");
1240
1241        fs::remove_file(&path).expect("clean up");
1242    }
1243
1244    #[test]
1245    fn a_foreign_key_that_matches_nothing_is_a_child_with_no_parent() {
1246        // Not an error and not a refusal. A foreign key that is not total is legal, and what it
1247        // costs is the monotone form, because every bit of that vector is already spoken for.
1248        let foreign = vec![Some(1), Some(2), None, Some(9999), Some(3)];
1249        let path = related("orphans", 10, &foreign);
1250        let report = build_links(&path, &[edge()]).expect("build");
1251        assert!(report[0].built, "{:?}", report[0].note);
1252        assert_eq!(report[0].form, Some(link::Form::Packed));
1253        assert_eq!(report[0].children, 5);
1254        assert_eq!(report[0].linked, 3, "the null and the key that matches nothing are not links");
1255
1256        let link = links(&path, &foreign);
1257        assert_eq!(link.forward(2), None, "a null is not a link");
1258        assert_eq!(link.forward(3), None, "a key that matches nothing is not a link");
1259
1260        fs::remove_file(&path).expect("clean up");
1261    }
1262
1263    #[test]
1264    fn a_parent_with_no_key_map_is_a_relationship_with_no_link_rather_than_an_error() {
1265        // Section 3.8's ordering is the reason: the key map has to exist first, and a checkpoint
1266        // that has not built one yet is a normal state rather than a broken one.
1267        let path = table_of("unmapped", &(1..=100_i64).map(Some).collect::<Vec<_>>());
1268        let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
1269            .expect("a second table");
1270        let values = (1..=100_i64).map(Value::BigInt).collect::<Vec<_>>();
1271        writer
1272            .append(
1273                &Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
1274                    .expect("one column"),
1275            )
1276            .expect("a part");
1277        writer.finish().expect("commit");
1278
1279        let report = build_links(&path, &[edge()]).expect("build");
1280        assert!(!report[0].built);
1281        assert_eq!(report[0].note.as_deref(), Some("no key map is stored for parent"));
1282
1283        let catalog = Catalog::open(&path).expect("reopen");
1284        let child = catalog.table("child").expect("the child");
1285        let parent = catalog.table("parent").expect("the parent");
1286        assert!(stored_link(&child, &parent, &edge()).is_none());
1287
1288        fs::remove_file(&path).expect("clean up");
1289    }
1290
1291    #[test]
1292    fn a_link_asked_for_against_the_wrong_parent_is_not_handed_over() {
1293        // The binding check. The section's own id says which child column the link is for and
1294        // nothing about which table it points into, so a caller that asked with a different parent
1295        // would otherwise be handed rids of a table it never named.
1296        let foreign = (0..500_i64).map(|child| Some(child / 5 + 1)).collect::<Vec<_>>();
1297        let path = related("binding", 100, &foreign);
1298        build_links(&path, &[edge()]).expect("build");
1299
1300        let catalog = Catalog::open(&path).expect("reopen");
1301        let child = catalog.table("child").expect("the child");
1302        let parent = catalog.table("parent").expect("the parent");
1303        assert!(stored_link(&child, &parent, &edge()).is_some());
1304        let wrong = Edge { parent: "child".into(), ..edge() };
1305        assert!(stored_link(&child, &parent, &wrong).is_none(), "a different parent name");
1306        let wrong = Edge { parent_column: 1, ..edge() };
1307        assert!(stored_link(&child, &parent, &wrong).is_none(), "a different parent column");
1308        let wrong = Edge { child_column: 1, ..edge() };
1309        assert!(stored_link(&child, &parent, &wrong).is_none(), "a different child column");
1310
1311        fs::remove_file(&path).expect("clean up");
1312    }
1313
1314    #[test]
1315    fn a_link_that_does_not_fit_the_budget_is_reported_rather_than_stored() {
1316        // Zero percent, which the floor lifts to sixty four kilobytes, against a packed link over
1317        // sixty thousand children at ten bits each, which is seventy five.
1318        let foreign = (0..60_000_i64).map(|child| Some((child * 7) % 1000 + 1)).collect::<Vec<_>>();
1319        let path = related("budget", 1000, &foreign);
1320        let report = build_links_within(&path, &[edge()], 0).expect("build");
1321        assert!(!report[0].built);
1322        assert!(report[0].bytes > 0, "the report says what a larger budget would buy");
1323        assert!(report[0].note.as_deref().unwrap_or_default().contains("budget"), "{report:?}");
1324
1325        let catalog = Catalog::open(&path).expect("reopen");
1326        let child = catalog.table("child").expect("the child");
1327        let parent = catalog.table("parent").expect("the parent");
1328        assert!(stored_link(&child, &parent, &edge()).is_none());
1329        // Measured before it was refused, and not written, because the shape of a relationship the
1330        // file cannot follow describes a plan nobody can make.
1331        assert!(report[0].degrees.is_some(), "it was measured");
1332        assert!(stored_degrees(&child, 0).is_none(), "and not written");
1333        // What does survive is the size and the form, which is exit criterion 3 of G3: somebody
1334        // deciding whether to raise `graph_budget` reads this rather than rebuilding to find out.
1335        assert_eq!(refused_link(&child, 0), Some((link::Form::Packed, report[0].bytes as u64)));
1336
1337        fs::remove_file(&path).expect("clean up");
1338    }
1339
1340    #[test]
1341    fn a_parent_whose_key_repeats_gets_no_link_at_all() {
1342        // Section 2.3's verification, which is the one check in this layer that is about
1343        // correctness rather than speed: a link over a non-unique parent resolves to one of the
1344        // rows that held the key, and which one is an accident of the build.
1345        let path = table_of("repeats", &[Some(1), Some(1), Some(2)]);
1346        let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
1347            .expect("a second table");
1348        let values = [Value::BigInt(1), Value::BigInt(2)];
1349        writer
1350            .append(
1351                &Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
1352                    .expect("one column"),
1353            )
1354            .expect("a part");
1355        writer.finish().expect("commit");
1356        build_key_maps(&path, "parent", &[0]).expect("the parent's key map");
1357
1358        let report = build_links(&path, &[edge()]).expect("build");
1359        assert!(!report[0].built);
1360        assert_eq!(report[0].note.as_deref(), Some("no key map is stored for parent"));
1361
1362        fs::remove_file(&path).expect("clean up");
1363    }
1364}