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