use std::path::Path;
use std::time::{Duration, Instant};
use rudb_common::{LogicalType, Result, Value};
use rudb_graph::{Degrees, Form, KeyMap, Keys, NO_PARENT, link, wire};
use rudb_vector::Chunk;
use crate::section::{self, Attachment};
use crate::{Catalog, Reader, invalid, type_tag};
#[derive(Debug)]
pub struct KeyColumn<'a> {
reader: &'a Reader,
column: usize,
}
impl<'a> KeyColumn<'a> {
pub fn new(reader: &'a Reader, column: usize) -> Result<Self> {
let fields = reader.table().fields();
let Some(field) = fields.get(column) else {
return Err(invalid(&format!(
"column {column} is past the {} of table {}",
fields.len(),
reader.table().name()
)));
};
if !mappable(&field.ty) {
return Err(invalid(&format!(
"a key map over {} needs an integer key form, and {} has none",
field.name, field.ty
)));
}
Ok(Self { reader, column })
}
}
impl Keys for KeyColumn<'_> {
fn scan(&self, each: &mut dyn FnMut(Option<i128>) -> Result<()>) -> Result<()> {
for part in 0..self.reader.parts() {
let chunk = self.reader.read(part, &[self.column])?;
let values = chunk.column(0)?;
for row in 0..chunk.len() {
each(key_at(&chunk, values, row)?)?;
}
}
Ok(())
}
}
fn mappable(ty: &LogicalType) -> bool {
matches!(
ty,
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::HugeInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
| LogicalType::Date
| LogicalType::Decimal { .. }
)
}
fn key_at(chunk: &Chunk, values: &rudb_vector::Vector, row: usize) -> Result<Option<i128>> {
if let Some(key) = values.signed_at(row) {
return Ok(Some(key));
}
match chunk.value_at(row, 0) {
Value::Null => Ok(None),
Value::TinyInt(key) => Ok(Some(i128::from(key))),
Value::SmallInt(key) => Ok(Some(i128::from(key))),
Value::Integer(key) | Value::Date(key) => Ok(Some(i128::from(key))),
Value::BigInt(key) | Value::Time(key) | Value::Timestamp(key) => Ok(Some(i128::from(key))),
Value::HugeInt(key) | Value::Decimal { unscaled: key, .. } => Ok(Some(key)),
Value::UTinyInt(key) => Ok(Some(i128::from(key))),
Value::USmallInt(key) => Ok(Some(i128::from(key))),
Value::UInteger(key) => Ok(Some(i128::from(key))),
Value::UBigInt(key) => Ok(Some(i128::from(key))),
other => Err(invalid(&format!("a key column holds {other}, which is not a key"))),
}
}
#[derive(Debug, Clone, Copy)]
pub struct Built {
pub column: usize,
pub form: Form,
pub rows: u64,
pub distinct: bool,
pub bytes: usize,
pub column_bytes: u64,
pub built: bool,
pub build: Duration,
}
pub fn build_key_map(reader: &Reader, column: usize) -> Result<KeyMap> {
KeyMap::build_from(&KeyColumn::new(reader, column)?)
}
pub const BUDGET_SHARE: u64 = 10;
pub const BUDGET_FLOOR: u64 = 64 * 1024;
pub fn build_key_maps(path: &Path, table: &str, columns: &[usize]) -> Result<Vec<Built>> {
build_key_maps_within(path, table, columns, BUDGET_SHARE)
}
pub fn build_key_maps_within(
path: &Path,
table: &str,
columns: &[usize],
share: u64,
) -> Result<Vec<Built>> {
let reader = Catalog::open(path)?.table(table)?;
let column_bytes = reader.layout().columns_total();
let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
let mut spent = held_bytes(&reader, columns)?;
let mut report = Vec::with_capacity(columns.len());
let mut payloads = Vec::with_capacity(columns.len());
for &column in columns {
let start = Instant::now();
let map = build_key_map(&reader, column)?;
let payload = wire::encode(&map, type_tag(&reader.table().fields()[column].ty)?)?;
report.push(Built {
column,
form: map.form(),
rows: map.observed().rows,
distinct: map.observed().distinct,
bytes: payload.bytes.len(),
column_bytes,
built: false,
build: start.elapsed(),
});
payloads.push((column, payload));
}
let mut order = (0..payloads.len()).collect::<Vec<_>>();
order.sort_by_key(|&at| payloads[at].1.bytes.len());
let mut keep = vec![false; payloads.len()];
for at in order {
if !report[at].distinct {
continue;
}
let cost = payloads[at].1.bytes.len() as u64;
if spent.saturating_add(cost) <= allowance {
spent += cost;
keep[at] = true;
report[at].built = true;
}
}
drop(reader);
let attachments = payloads
.iter()
.zip(&keep)
.map(|((column, payload), &keep)| {
Ok(Attachment {
kind: *section::KEY_MAP,
id: u64::try_from(*column).map_err(|_| invalid("column index overflow"))?,
flags: payload.flags,
header_bytes: if keep { payload.header_bytes } else { cost(payload.bytes.len()) },
bytes: if keep { &payload.bytes } else { &[] },
})
})
.collect::<Result<Vec<_>>>()?;
crate::attach(path, table, &attachments)?;
Ok(report)
}
fn held_bytes(reader: &Reader, replacing: &[usize]) -> Result<u64> {
held_bytes_except(reader, *section::KEY_MAP, replacing)
}
#[must_use]
pub fn key_map(reader: &Reader, column: usize) -> Option<KeyMap> {
let table = reader.table();
let id = u64::try_from(column).ok()?;
let held = table
.sections()
.iter()
.find(|section| section.kind == *section::KEY_MAP && section.id == id)?;
if !held.usable(table.generation()) {
return None;
}
let (map, tag) = wire::decode(&reader.payload(held).ok()?).ok()?;
if tag != type_tag(&table.fields().get(column)?.ty).ok()? {
return None;
}
Some(map)
}
#[derive(Debug, Clone)]
pub struct Edge {
pub child: String,
pub child_column: usize,
pub parent: String,
pub parent_column: usize,
}
#[derive(Debug, Clone)]
pub struct BuiltLink {
pub edge: Edge,
pub form: Option<link::Form>,
pub children: u64,
pub linked: u64,
pub bytes: usize,
pub table_bytes: u64,
pub degrees: Option<Degrees>,
pub built: bool,
pub note: Option<String>,
pub build: Duration,
}
pub fn build_links(path: &Path, edges: &[Edge]) -> Result<Vec<BuiltLink>> {
build_links_within(path, edges, BUDGET_SHARE)
}
pub fn build_links_within(path: &Path, edges: &[Edge], share: u64) -> Result<Vec<BuiltLink>> {
let mut tables: Vec<&str> = Vec::new();
for edge in edges {
if !tables.iter().any(|held| *held == edge.child) {
tables.push(&edge.child);
}
}
let mut report = Vec::with_capacity(edges.len());
for table in tables {
let mine = edges.iter().filter(|edge| edge.child == table).cloned().collect::<Vec<Edge>>();
report.extend(links_of_one_table(path, table, &mine, share)?);
}
Ok(report)
}
fn links_of_one_table(
path: &Path,
table: &str,
edges: &[Edge],
share: u64,
) -> Result<Vec<BuiltLink>> {
let catalog = Catalog::open(path)?;
let child = catalog.table(table)?;
let column_bytes = child.layout().columns_total();
let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
let replacing = edges.iter().map(|edge| edge.child_column).collect::<Vec<usize>>();
let mut spent = held_bytes_except(&child, *section::FORWARD_LINK, &replacing)?;
let mut report = Vec::with_capacity(edges.len());
let mut payloads: Vec<Option<Vec<u8>>> = Vec::with_capacity(edges.len());
for edge in edges {
let start = Instant::now();
match one_link(&catalog, &child, edge) {
Ok((built, bytes)) => {
report.push(BuiltLink {
build: start.elapsed(),
table_bytes: column_bytes,
..built
});
payloads.push(Some(bytes));
}
Err(note) => {
report.push(BuiltLink {
edge: edge.clone(),
form: None,
children: child.table().rows() as u64,
linked: 0,
bytes: 0,
table_bytes: column_bytes,
degrees: None,
built: false,
note: Some(note),
build: start.elapsed(),
});
payloads.push(None);
}
}
}
let mut order = (0..report.len()).filter(|at| payloads[*at].is_some()).collect::<Vec<_>>();
order.sort_by(|left, right| {
let value = |at: &usize| -> f64 {
let bytes = report[*at].bytes.max(1);
report[*at].children as f64 / bytes as f64
};
value(right).partial_cmp(&value(left)).unwrap_or(std::cmp::Ordering::Equal)
});
for at in order {
let cost = report[at].bytes as u64;
if spent.saturating_add(cost) <= allowance {
spent += cost;
report[at].built = true;
} else {
report[at].note = Some(format!("over the budget of {allowance} bytes"));
}
}
drop(child);
let measured = report
.iter()
.filter(|built| built.built)
.filter_map(|built| {
let mut bytes = Vec::with_capacity(rudb_graph::degree::BYTES);
built.degrees.as_ref()?.write(&mut bytes);
Some((built.edge.child_column, bytes))
})
.collect::<Vec<_>>();
let mut attachments = report
.iter()
.zip(&payloads)
.filter(|(_, payload)| payload.is_some())
.map(|(built, payload)| {
let bytes = payload.as_ref().expect("filtered to the measured");
Ok(Attachment {
kind: *section::FORWARD_LINK,
id: u64::try_from(built.edge.child_column)
.map_err(|_| invalid("column index overflow"))?,
flags: built.form.map_or(0, |form| u32::from(form.tag())),
header_bytes: if built.built {
u32::try_from(binding_bytes(&built.edge.parent))
.map_err(|_| invalid("a parent name longer than a section header"))?
} else {
cost(bytes.len())
},
bytes: if built.built { bytes } else { &[] },
})
})
.collect::<Result<Vec<_>>>()?;
for (column, bytes) in &measured {
attachments.push(Attachment {
kind: *section::DEGREES,
id: u64::try_from(*column).map_err(|_| invalid("column index overflow"))?,
flags: 0,
header_bytes: 0,
bytes,
});
}
crate::attach(path, table, &attachments)?;
Ok(report)
}
fn one_link(
catalog: &Catalog,
child: &Reader,
edge: &Edge,
) -> std::result::Result<(BuiltLink, Vec<u8>), String> {
let parent =
catalog.table(&edge.parent).map_err(|_| format!("no table named {}", edge.parent))?;
let map = key_map(&parent, edge.parent_column)
.ok_or_else(|| format!("no key map is stored for {}", edge.parent))?;
if !map.observed().usable_as_parent() {
return Err(format!("the key of {} is not unique", edge.parent));
}
let keys = KeyColumn::new(child, edge.child_column).map_err(|error| error.to_string())?;
let mut parents_of = Vec::with_capacity(child.table().rows());
let mut failed = None;
keys.scan(&mut |key| {
let parent = match key {
None => NO_PARENT,
Some(key) => match map.lookup(key) {
Ok(found) => found.unwrap_or(NO_PARENT),
Err(error) => {
failed = Some(error.to_string());
NO_PARENT
}
},
};
parents_of.push(parent);
Ok(())
})
.map_err(|error| error.to_string())?;
if let Some(failed) = failed {
return Err(failed);
}
let link = link::Link::build(&parents_of, map.len()).map_err(|error| error.to_string())?;
let degrees = Degrees::of(&parents_of, map.len(), true);
let bytes = encode_link(&link, &parent, edge).map_err(|error| error.to_string())?;
Ok((
BuiltLink {
edge: edge.clone(),
form: Some(link.form()),
children: link.children(),
linked: link.linked(),
bytes: bytes.len(),
table_bytes: 0,
degrees: Some(degrees),
built: false,
note: None,
build: Duration::ZERO,
},
bytes,
))
}
fn cost(bytes: usize) -> u32 {
u32::try_from(bytes).unwrap_or(u32::MAX)
}
fn binding_bytes(parent: &str) -> usize {
16 + parent.len().div_ceil(8) * 8
}
fn encode_link(link: &link::Link, parent: &Reader, edge: &Edge) -> Result<Vec<u8>> {
let name = edge.parent.as_bytes();
let mut bytes = Vec::with_capacity(binding_bytes(&edge.parent) + link.bytes());
bytes.extend_from_slice(&parent.table().generation().to_le_bytes());
bytes.extend_from_slice(
&u32::try_from(edge.parent_column)
.map_err(|_| invalid("column index overflow"))?
.to_le_bytes(),
);
bytes.extend_from_slice(
&u32::try_from(name.len())
.map_err(|_| invalid("a parent name longer than a u32"))?
.to_le_bytes(),
);
bytes.extend_from_slice(name);
bytes.resize(binding_bytes(&edge.parent), 0);
link.write(&mut bytes)?;
Ok(bytes)
}
#[must_use]
pub fn stored_link(child: &Reader, parent: &Reader, edge: &Edge) -> Option<link::Link> {
let table = child.table();
let id = u64::try_from(edge.child_column).ok()?;
let held = table
.sections()
.iter()
.find(|section| section.kind == *section::FORWARD_LINK && section.id == id)?;
if !held.usable(table.generation()) {
return None;
}
let bytes = child.payload(held).ok()?;
let binding = binding_bytes(&edge.parent);
if bytes.len() < binding {
return None;
}
let generation = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
let column = u32::from_le_bytes(bytes[8..12].try_into().ok()?);
let length = u32::from_le_bytes(bytes[12..16].try_into().ok()?) as usize;
if generation != parent.table().generation()
|| column as usize != edge.parent_column
|| length != edge.parent.len()
|| &bytes[16..16 + length] != edge.parent.as_bytes()
{
return None;
}
link::Link::read(&bytes[binding..]).ok()
}
#[must_use]
pub fn stored_degrees(child: &Reader, child_column: usize) -> Option<Degrees> {
let table = child.table();
let id = u64::try_from(child_column).ok()?;
let held = table
.sections()
.iter()
.find(|section| section.kind == *section::DEGREES && section.id == id)?;
if !held.usable(table.generation()) {
return None;
}
Degrees::read(&child.payload(held).ok()?).ok()
}
#[must_use]
pub fn refused_key_map(reader: &Reader, column: usize) -> Option<(Form, u64)> {
let (form, bytes) = refused(reader, *section::KEY_MAP, column)?;
Some((Form::from_tag(form).ok()?, bytes))
}
#[must_use]
pub fn refused_link(child: &Reader, child_column: usize) -> Option<(link::Form, u64)> {
let (form, bytes) = refused(child, *section::FORWARD_LINK, child_column)?;
Some((link::Form::from_tag(form).ok()?, bytes))
}
fn refused(reader: &Reader, kind: [u8; 8], id: usize) -> Option<(u8, u64)> {
let table = reader.table();
let id = u64::try_from(id).ok()?;
let held = table.sections().iter().find(|section| section.kind == kind && section.id == id)?;
if !held.usable(table.generation()) {
return None;
}
Some((u8::try_from(held.flags).ok()?, held.refused()?))
}
fn held_bytes_except(reader: &Reader, kind: [u8; 8], replacing: &[usize]) -> Result<u64> {
let mut total = 0;
for held in reader.table().sections() {
if !held.among(section::GRAPH_KINDS) {
continue;
}
let replaced =
held.kind == kind && replacing.iter().any(|&id| u64::try_from(id) == Ok(held.id));
if replaced || !held.usable(reader.table().generation()) {
continue;
}
let Ok(extents) = reader.extents(held) else { continue };
total += extents.iter().map(|extent| u64::from(extent.length)).sum::<u64>();
}
Ok(total)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rudb_common::Field;
use rudb_graph::Rid;
use rudb_vector::Vector;
use super::*;
use crate::Writer;
fn path(label: &str) -> PathBuf {
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("time advances").as_nanos();
std::env::temp_dir().join(format!("rudb-graph-{label}-{}-{stamp}.rdb", std::process::id()))
}
fn graph_sections(reader: &Reader) -> Vec<§ion::Section> {
reader.table().sections().iter().filter(|held| held.among(section::GRAPH_KINDS)).collect()
}
fn table_of(label: &str, keys: &[Option<i64>]) -> PathBuf {
let path = path(label);
let mut writer =
Writer::create(&path, "parent", vec![Field::new("key", LogicalType::BigInt)])
.expect("new file");
for part in keys.chunks(1000) {
let values =
part.iter().map(|key| key.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
.expect("one column");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
path
}
fn resolves(keys: &[Option<i64>], map: &KeyMap) {
for (rid, key) in keys.iter().enumerate() {
let Some(key) = *key else { continue };
let found =
map.lookup(i128::from(key)).expect("lookup").expect("a key in the column resolves");
assert_eq!(found, rid as Rid, "key {key} resolved to {found} rather than {rid}");
}
}
#[test]
fn a_key_map_built_over_a_file_resolves_every_key_to_its_own_row() {
let keys = (1..=3000_i64).map(Some).collect::<Vec<_>>();
let path = table_of("identity", &keys);
let built = build_key_maps(&path, "parent", &[0]).expect("build");
assert_eq!(built.len(), 1);
assert_eq!(built[0].form, Form::Identity);
assert_eq!(built[0].rows, 3000);
assert!(built[0].distinct);
assert_eq!(built[0].bytes, wire::HEADER_BYTES, "identity is a header and nothing else");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let map = key_map(&reader, 0).expect("the map is in the file");
assert_eq!(map.form(), Form::Identity);
resolves(&keys, &map);
assert_eq!(map.lookup(0).expect("a key below the column"), None);
assert_eq!(map.lookup(3001).expect("a key past the column"), None);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_column_with_gaps_takes_the_bitmap_form_and_still_resolves() {
let keys = (0..2000_i64).map(|value| Some(value * 4 + 7)).collect::<Vec<_>>();
let path = table_of("dense", &keys);
let built = build_key_maps(&path, "parent", &[0]).expect("build");
assert_eq!(built[0].form, Form::Dense);
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let map = key_map(&reader, 0).expect("the map is in the file");
resolves(&keys, &map);
assert_eq!(map.lookup(8).expect("a value in the range but not the column"), None);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_column_out_of_order_takes_the_sorted_form_and_still_resolves() {
let keys = (0..1500_i64).map(|value| Some((value * 7919) % 100_003)).collect::<Vec<_>>();
let path = table_of("sorted", &keys);
let built = build_key_maps(&path, "parent", &[0]).expect("build");
assert_eq!(built[0].form, Form::Sorted);
assert!(built[0].distinct, "the sort settles distinctness for an unordered column");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let map = key_map(&reader, 0).expect("the map is in the file");
resolves(&keys, &map);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_null_in_the_key_column_does_not_shift_the_rows_after_it() {
let mut keys = (1..=1200_i64).map(Some).collect::<Vec<_>>();
keys[3] = None;
keys[900] = None;
let path = table_of("nulls", &keys);
let built = build_key_maps(&path, "parent", &[0]).expect("build");
assert_eq!(built[0].rows, 1198, "a null is not a key");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let map = key_map(&reader, 0).expect("the map is in the file");
resolves(&keys, &map);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_column_with_a_repeat_in_it_is_mapped_and_reported_as_no_parent() {
let mut keys = (1..=500_i64).map(Some).collect::<Vec<_>>();
keys[200] = Some(7);
let path = table_of("repeat", &keys);
let built = build_key_maps(&path, "parent", &[0]).expect("build");
assert!(!built[0].distinct, "a repeat is observed rather than declared away");
assert!(!built[0].built, "and a map no rid can be resolved through is not kept");
assert!(built[0].bytes > 0, "what it would have cost is still reported");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
assert!(key_map(&reader, 0).is_none(), "no map was written to read back");
let (form, bytes) = refused_key_map(&reader, 0).expect("the record of what it would cost");
assert_eq!(form, built[0].form);
assert_eq!(bytes, built[0].bytes as u64);
assert_eq!(graph_sections(&reader).len(), 1, "one entry, and no payload");
assert_eq!(graph_sections(&reader)[0].extents, 0);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_table_with_no_key_map_answers_with_none_rather_than_an_error() {
let path = table_of("absent", &[Some(1), Some(2)]);
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
assert!(key_map(&reader, 0).is_none());
assert!(key_map(&reader, 99).is_none(), "a column that does not exist is not a panic");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_stale_key_map_is_ignored_and_the_table_still_reads() {
let path = table_of("stale", &(1..=100_i64).map(Some).collect::<Vec<_>>());
build_key_maps(&path, "parent", &[0]).expect("build");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
assert!(key_map(&reader, 0).is_some());
let generation = reader.table().generation();
drop(reader);
let held = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let mut entry = *graph_sections(&held).first().copied().expect("the key map");
assert!(entry.usable(generation));
entry.generation = generation + 1;
assert!(!entry.usable(generation), "a rewrite invalidates rather than corrupts");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_torn_key_map_costs_the_shortcut_and_not_the_query() {
let keys = (1..=200_i64).map(Some).collect::<Vec<_>>();
let path = table_of("torn", &keys);
build_key_maps(&path, "parent", &[0]).expect("build");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let extent = reader
.extents(graph_sections(&reader).first().copied().expect("the key map"))
.expect("extent table")
.first()
.copied()
.expect("one extent");
drop(reader);
let file = fs::OpenOptions::new().write(true).open(&path).expect("reopen to corrupt");
crate::write_at(&file, extent.offset, &[0xff; 8]).expect("flip the header");
drop(file);
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
assert!(key_map(&reader, 0).is_none(), "a payload that does not checksum is not a map");
assert_eq!(reader.table().rows(), 200, "and the table is untouched");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_column_with_no_integer_key_form_is_refused_by_name() {
let path = path("varchar");
let mut writer =
Writer::create(&path, "parent", vec![Field::new("name", LogicalType::Varchar)])
.expect("new file");
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, &[Value::Varchar("a".into())])
.expect("one name"),
])
.expect("one column");
writer.append(&chunk).expect("a part");
writer.finish().expect("commit");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let error = KeyColumn::new(&reader, 0).expect_err("a string key needs its codes");
assert!(error.to_string().contains("integer key form"), "{error}");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn several_columns_are_mapped_in_one_commit() {
let path = path("two_columns");
let mut writer = Writer::create(
&path,
"parent",
vec![
Field::required("id", LogicalType::BigInt),
Field::required("code", LogicalType::Integer),
],
)
.expect("new file");
let ids = (1..=400_i64).map(Value::BigInt).collect::<Vec<_>>();
let codes = (1..=400_i32).map(|code| Value::Integer(code * 3)).collect::<Vec<_>>();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &ids).expect("ids"),
Vector::from_values(LogicalType::Integer, &codes).expect("codes"),
])
.expect("two columns");
writer.append(&chunk).expect("a part");
writer.finish().expect("commit");
let built = build_key_maps(&path, "parent", &[0, 1]).expect("build both");
assert_eq!(built.len(), 2);
assert_eq!(built[0].form, Form::Identity);
assert_eq!(built[1].form, Form::Dense);
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
assert_eq!(graph_sections(&reader).len(), 2, "one commit and two entries");
assert_eq!(key_map(&reader, 0).expect("the id map").form(), Form::Identity);
assert_eq!(key_map(&reader, 1).expect("the code map").form(), Form::Dense);
assert_eq!(
key_map(&reader, 1).expect("the code map").lookup(9).expect("lookup"),
Some(2),
"the third code is the third row"
);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn the_statistics_sections_do_not_count_against_the_graph_budget() {
let keys = (1..=3000_i64).map(Some).collect::<Vec<_>>();
let path = table_of("apart", &keys);
crate::stats::build_stats(&path, "parent", &[0]).expect("summaries first");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let statistics = reader
.table()
.sections()
.iter()
.filter(|held| held.among(section::STATISTICS_KINDS))
.count();
assert_eq!(statistics, 2, "a summary and a sketch are in the file");
assert_eq!(held_bytes(&reader, &[0]).expect("held"), 0, "and neither is the graph's");
drop(reader);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_map_that_does_not_fit_the_budget_is_measured_and_not_written() {
let keys = (0..100_000_i64).map(|value| Some(value * 8)).collect::<Vec<_>>();
let path = table_of("budget", &keys);
let built = build_key_maps(&path, "parent", &[0]).expect("build");
assert_eq!(built[0].form, Form::Dense);
assert!(!built[0].built, "a map ten times its column does not fit a tenth of it");
assert!(built[0].bytes as u64 > built[0].column_bytes, "{built:?}");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
assert!(key_map(&reader, 0).is_none(), "and no map was written");
assert_eq!(refused_key_map(&reader, 0), Some((Form::Dense, built[0].bytes as u64)));
assert_eq!(held_bytes(&reader, &[]).expect("held"), 0, "a record costs the budget nothing");
drop(reader);
let built = build_key_maps_within(&path, "parent", &[0], 100_000).expect("build");
assert!(built[0].built);
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
let map = key_map(&reader, 0).expect("the map is in the file");
resolves(&keys, &map);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn the_budget_admits_the_cheapest_maps_it_can_fit() {
let path = path("budget_order");
let mut writer = Writer::create(
&path,
"parent",
vec![
Field::required("id", LogicalType::BigInt),
Field::required("code", LogicalType::BigInt),
],
)
.expect("new file");
let ids = (1..=100_000_i64).map(Value::BigInt).collect::<Vec<_>>();
let codes = (1..=100_000_i64)
.map(|code| Value::BigInt((code * 2_147_483_647) % 999_999_937))
.collect::<Vec<_>>();
for part in 0..100 {
let at = part * 1000;
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &ids[at..at + 1000]).expect("ids"),
Vector::from_values(LogicalType::BigInt, &codes[at..at + 1000]).expect("codes"),
])
.expect("two columns");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
let built = build_key_maps(&path, "parent", &[1, 0]).expect("build");
assert_eq!(built[0].column, 1, "the report is in the order it was asked in");
assert_eq!(built[0].form, Form::Sorted);
assert!(!built[0].built, "the sorted map did not fit: {built:?}");
assert!(built[1].built, "the identity map did, and was reached second: {built:?}");
let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
assert!(key_map(&reader, 0).is_some());
assert!(key_map(&reader, 1).is_none());
fs::remove_file(&path).expect("clean up");
}
fn related(label: &str, parents: i64, foreign: &[Option<i64>]) -> PathBuf {
let path = table_of(label, &(1..=parents).map(Some).collect::<Vec<_>>());
let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
.expect("a second table");
for part in foreign.chunks(1000) {
let values =
part.iter().map(|key| key.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
.expect("one column");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
build_key_maps(&path, "parent", &[0]).expect("the parent's key map");
path
}
fn edge() -> Edge {
Edge { child: "child".into(), child_column: 0, parent: "parent".into(), parent_column: 0 }
}
fn links(path: &PathBuf, foreign: &[Option<i64>]) -> link::Link {
let catalog = Catalog::open(path).expect("reopen");
let child = catalog.table("child").expect("the child");
let parent = catalog.table("parent").expect("the parent");
let link = stored_link(&child, &parent, &edge()).expect("the link is in the file");
let map = key_map(&parent, 0).expect("the parent's key map");
for (rid, key) in foreign.iter().enumerate() {
let want = key.and_then(|key| map.lookup(i128::from(key)).expect("lookup"));
assert_eq!(link.forward(rid as Rid), want, "child {rid}");
}
link
}
#[test]
fn a_clustered_foreign_key_takes_the_monotone_form_and_answers_both_directions() {
let foreign = (0..4000_i64).map(|child| Some(child / 4 + 1)).collect::<Vec<_>>();
let path = related("monotone", 1000, &foreign);
let report = build_links(&path, &[edge()]).expect("build");
assert_eq!(report.len(), 1);
assert!(report[0].built, "{:?}", report[0].note);
assert_eq!(report[0].form, Some(link::Form::Monotone));
assert_eq!(report[0].children, 4000);
assert_eq!(report[0].linked, 4000);
let link = links(&path, &foreign);
assert_eq!(link.form(), link::Form::Monotone);
assert_eq!(link.backward(0), Some(0..4), "the first parent's four children");
assert_eq!(link.backward(999), Some(3996..4000));
assert_eq!(link.backward(1000), None, "past the last parent");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn an_unclustered_foreign_key_takes_the_packed_form_and_still_resolves() {
let foreign = (0..3000_i64).map(|child| Some((child * 7) % 1000 + 1)).collect::<Vec<_>>();
let path = related("packed", 1000, &foreign);
let report = build_links(&path, &[edge()]).expect("build");
assert!(report[0].built, "{:?}", report[0].note);
assert_eq!(report[0].form, Some(link::Form::Packed));
let link = links(&path, &foreign);
assert_eq!(link.backward(0), None, "the packed form answers one direction");
assert!(link.bytes() < 3000 * 2 + 3 * 16, "{} bytes is not bit-packed", link.bytes());
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_built_link_leaves_the_shape_of_the_relationship_beside_it() {
let foreign = (0..4000_i64).map(|child| Some(child / 4 + 1)).collect::<Vec<_>>();
let path = related("degrees", 1000, &foreign);
let report = build_links(&path, &[edge()]).expect("build");
assert!(report[0].built, "{:?}", report[0].note);
let measured = report[0].degrees.as_ref().expect("the build measured it");
assert!((measured.mean() - 4.0).abs() < 1e-9);
let catalog = Catalog::open(&path).expect("reopen");
let child = catalog.table("child").expect("the child");
let held = stored_degrees(&child, 0).expect("it is in the file");
assert_eq!(&held, measured, "what the build measured is what the file holds");
assert_eq!(held.parents(), 1000);
assert_eq!(held.highest(), 4);
assert!(held.total(), "every child found a parent");
assert!(held.unique(), "and the parent key is why there is a link at all");
let near = held.locality().expect("something to gather");
assert!((near - 999.0 / 3999.0).abs() < 1e-9, "{near}");
assert!(stored_degrees(&child, 1).is_none(), "and no other column has one");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_foreign_key_that_matches_nothing_is_a_child_with_no_parent() {
let foreign = vec![Some(1), Some(2), None, Some(9999), Some(3)];
let path = related("orphans", 10, &foreign);
let report = build_links(&path, &[edge()]).expect("build");
assert!(report[0].built, "{:?}", report[0].note);
assert_eq!(report[0].form, Some(link::Form::Packed));
assert_eq!(report[0].children, 5);
assert_eq!(report[0].linked, 3, "the null and the key that matches nothing are not links");
let link = links(&path, &foreign);
assert_eq!(link.forward(2), None, "a null is not a link");
assert_eq!(link.forward(3), None, "a key that matches nothing is not a link");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_parent_with_no_key_map_is_a_relationship_with_no_link_rather_than_an_error() {
let path = table_of("unmapped", &(1..=100_i64).map(Some).collect::<Vec<_>>());
let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
.expect("a second table");
let values = (1..=100_i64).map(Value::BigInt).collect::<Vec<_>>();
writer
.append(
&Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
.expect("one column"),
)
.expect("a part");
writer.finish().expect("commit");
let report = build_links(&path, &[edge()]).expect("build");
assert!(!report[0].built);
assert_eq!(report[0].note.as_deref(), Some("no key map is stored for parent"));
let catalog = Catalog::open(&path).expect("reopen");
let child = catalog.table("child").expect("the child");
let parent = catalog.table("parent").expect("the parent");
assert!(stored_link(&child, &parent, &edge()).is_none());
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_link_asked_for_against_the_wrong_parent_is_not_handed_over() {
let foreign = (0..500_i64).map(|child| Some(child / 5 + 1)).collect::<Vec<_>>();
let path = related("binding", 100, &foreign);
build_links(&path, &[edge()]).expect("build");
let catalog = Catalog::open(&path).expect("reopen");
let child = catalog.table("child").expect("the child");
let parent = catalog.table("parent").expect("the parent");
assert!(stored_link(&child, &parent, &edge()).is_some());
let wrong = Edge { parent: "child".into(), ..edge() };
assert!(stored_link(&child, &parent, &wrong).is_none(), "a different parent name");
let wrong = Edge { parent_column: 1, ..edge() };
assert!(stored_link(&child, &parent, &wrong).is_none(), "a different parent column");
let wrong = Edge { child_column: 1, ..edge() };
assert!(stored_link(&child, &parent, &wrong).is_none(), "a different child column");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_link_that_does_not_fit_the_budget_is_reported_rather_than_stored() {
let foreign = (0..60_000_i64).map(|child| Some((child * 7) % 1000 + 1)).collect::<Vec<_>>();
let path = related("budget", 1000, &foreign);
let report = build_links_within(&path, &[edge()], 0).expect("build");
assert!(!report[0].built);
assert!(report[0].bytes > 0, "the report says what a larger budget would buy");
assert!(report[0].note.as_deref().unwrap_or_default().contains("budget"), "{report:?}");
let catalog = Catalog::open(&path).expect("reopen");
let child = catalog.table("child").expect("the child");
let parent = catalog.table("parent").expect("the parent");
assert!(stored_link(&child, &parent, &edge()).is_none());
assert!(report[0].degrees.is_some(), "it was measured");
assert!(stored_degrees(&child, 0).is_none(), "and not written");
assert_eq!(refused_link(&child, 0), Some((link::Form::Packed, report[0].bytes as u64)));
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_parent_whose_key_repeats_gets_no_link_at_all() {
let path = table_of("repeats", &[Some(1), Some(1), Some(2)]);
let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
.expect("a second table");
let values = [Value::BigInt(1), Value::BigInt(2)];
writer
.append(
&Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
.expect("one column"),
)
.expect("a part");
writer.finish().expect("commit");
build_key_maps(&path, "parent", &[0]).expect("the parent's key map");
let report = build_links(&path, &[edge()]).expect("build");
assert!(!report[0].built);
assert_eq!(report[0].note.as_deref(), Some("no key map is stored for parent"));
fs::remove_file(&path).expect("clean up");
}
}