1use 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#[derive(Debug)]
32pub struct KeyColumn<'a> {
33 reader: &'a Reader,
34 column: usize,
35}
36
37impl<'a> KeyColumn<'a> {
38 pub fn new(reader: &'a Reader, column: usize) -> Result<Self> {
47 let fields = reader.table().fields();
48 let Some(field) = fields.get(column) else {
49 return Err(invalid(&format!(
50 "column {column} is past the {} of table {}",
51 fields.len(),
52 reader.table().name()
53 )));
54 };
55 if !mappable(&field.ty) {
56 return Err(invalid(&format!(
57 "a key map over {} needs an integer key form, and {} has none",
58 field.name, field.ty
59 )));
60 }
61 Ok(Self { reader, column })
62 }
63}
64
65impl Keys for KeyColumn<'_> {
66 fn scan(&self, each: &mut dyn FnMut(Option<i128>) -> Result<()>) -> Result<()> {
67 for part in 0..self.reader.parts() {
68 let chunk = self.reader.read(part, &[self.column])?;
69 let values = chunk.column(0)?;
70 for row in 0..chunk.len() {
71 each(key_at(&chunk, values, row)?)?;
72 }
73 }
74 Ok(())
75 }
76}
77
78fn mappable(ty: &LogicalType) -> bool {
80 matches!(
81 ty,
82 LogicalType::TinyInt
83 | LogicalType::SmallInt
84 | LogicalType::Integer
85 | LogicalType::BigInt
86 | LogicalType::HugeInt
87 | LogicalType::UTinyInt
88 | LogicalType::USmallInt
89 | LogicalType::UInteger
90 | LogicalType::UBigInt
91 | LogicalType::Date
92 | LogicalType::Decimal { .. }
93 )
94}
95
96fn key_at(chunk: &Chunk, values: &rudb_vector::Vector, row: usize) -> Result<Option<i128>> {
104 if let Some(key) = values.signed_at(row) {
105 return Ok(Some(key));
106 }
107 match chunk.value_at(row, 0) {
108 Value::Null => Ok(None),
109 Value::TinyInt(key) => Ok(Some(i128::from(key))),
110 Value::SmallInt(key) => Ok(Some(i128::from(key))),
111 Value::Integer(key) | Value::Date(key) => Ok(Some(i128::from(key))),
112 Value::BigInt(key) | Value::Time(key) | Value::Timestamp(key) => Ok(Some(i128::from(key))),
113 Value::HugeInt(key) | Value::Decimal { unscaled: key, .. } => Ok(Some(key)),
114 Value::UTinyInt(key) => Ok(Some(i128::from(key))),
115 Value::USmallInt(key) => Ok(Some(i128::from(key))),
116 Value::UInteger(key) => Ok(Some(i128::from(key))),
117 Value::UBigInt(key) => Ok(Some(i128::from(key))),
118 other => Err(invalid(&format!("a key column holds {other}, which is not a key"))),
119 }
120}
121
122#[derive(Debug, Clone, Copy)]
129pub struct Built {
130 pub column: usize,
132 pub form: Form,
134 pub rows: u64,
136 pub distinct: bool,
139 pub bytes: usize,
141 pub column_bytes: u64,
143 pub built: bool,
147 pub build: Duration,
149}
150
151pub fn build_key_map(reader: &Reader, column: usize) -> Result<KeyMap> {
157 KeyMap::build_from(&KeyColumn::new(reader, column)?)
158}
159
160pub const BUDGET_SHARE: u64 = 10;
168
169pub const BUDGET_FLOOR: u64 = 64 * 1024;
182
183pub fn build_key_maps(path: &Path, table: &str, columns: &[usize]) -> Result<Vec<Built>> {
193 build_key_maps_within(path, table, columns, BUDGET_SHARE)
194}
195
196pub fn build_key_maps_within(
214 path: &Path,
215 table: &str,
216 columns: &[usize],
217 share: u64,
218) -> Result<Vec<Built>> {
219 let reader = Catalog::open(path)?.table(table)?;
220 let column_bytes = reader.layout().columns_total();
221 let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
222 let mut spent = held_bytes(&reader, columns)?;
223 let mut report = Vec::with_capacity(columns.len());
224 let mut payloads = Vec::with_capacity(columns.len());
225 for &column in columns {
226 let start = Instant::now();
227 let map = build_key_map(&reader, column)?;
228 let payload = wire::encode(&map, type_tag(&reader.table().fields()[column].ty)?)?;
229 report.push(Built {
230 column,
231 form: map.form(),
232 rows: map.observed().rows,
233 distinct: map.observed().distinct,
234 bytes: payload.bytes.len(),
235 column_bytes,
236 built: false,
237 build: start.elapsed(),
238 });
239 payloads.push((column, payload));
240 }
241 let mut order = (0..payloads.len()).collect::<Vec<_>>();
244 order.sort_by_key(|&at| payloads[at].1.bytes.len());
245 let mut keep = vec![false; payloads.len()];
246 for at in order {
247 if !report[at].distinct {
252 continue;
253 }
254 let cost = payloads[at].1.bytes.len() as u64;
255 if spent.saturating_add(cost) <= allowance {
256 spent += cost;
257 keep[at] = true;
258 report[at].built = true;
259 }
260 }
261 drop(reader);
265 let attachments = payloads
270 .iter()
271 .zip(&keep)
272 .map(|((column, payload), &keep)| {
273 Ok(Attachment {
274 kind: *section::KEY_MAP,
275 id: u64::try_from(*column).map_err(|_| invalid("column index overflow"))?,
276 flags: payload.flags,
277 header_bytes: if keep { payload.header_bytes } else { cost(payload.bytes.len()) },
278 bytes: if keep { &payload.bytes } else { &[] },
279 })
280 })
281 .collect::<Result<Vec<_>>>()?;
282 crate::attach(path, table, &attachments)?;
283 Ok(report)
284}
285
286fn held_bytes(reader: &Reader, replacing: &[usize]) -> Result<u64> {
296 held_bytes_except(reader, *section::KEY_MAP, replacing)
297}
298
299#[must_use]
308pub fn key_map(reader: &Reader, column: usize) -> Option<KeyMap> {
309 let table = reader.table();
310 let id = u64::try_from(column).ok()?;
311 let held = table
312 .sections()
313 .iter()
314 .find(|section| section.kind == *section::KEY_MAP && section.id == id)?;
315 if !held.usable(table.generation()) {
316 return None;
317 }
318 let (map, tag) = wire::decode(&reader.payload(held).ok()?).ok()?;
319 if tag != type_tag(&table.fields().get(column)?.ty).ok()? {
324 return None;
325 }
326 Some(map)
327}
328
329#[derive(Debug, Clone)]
335pub struct Edge {
336 pub child: String,
338 pub child_column: usize,
340 pub parent: String,
342 pub parent_column: usize,
344}
345
346#[derive(Debug, Clone)]
348pub struct BuiltLink {
349 pub edge: Edge,
351 pub form: Option<link::Form>,
353 pub children: u64,
355 pub linked: u64,
358 pub bytes: usize,
360 pub table_bytes: u64,
363 pub degrees: Option<Degrees>,
369 pub built: bool,
371 pub note: Option<String>,
373 pub build: Duration,
375}
376
377pub fn build_links(path: &Path, edges: &[Edge]) -> Result<Vec<BuiltLink>> {
388 build_links_within(path, edges, BUDGET_SHARE)
389}
390
391pub fn build_links_within(path: &Path, edges: &[Edge], share: u64) -> Result<Vec<BuiltLink>> {
406 let mut tables: Vec<&str> = Vec::new();
407 for edge in edges {
408 if !tables.iter().any(|held| *held == edge.child) {
409 tables.push(&edge.child);
410 }
411 }
412 let mut report = Vec::with_capacity(edges.len());
413 for table in tables {
414 let mine = edges.iter().filter(|edge| edge.child == table).cloned().collect::<Vec<Edge>>();
415 report.extend(links_of_one_table(path, table, &mine, share)?);
416 }
417 Ok(report)
418}
419
420fn links_of_one_table(
422 path: &Path,
423 table: &str,
424 edges: &[Edge],
425 share: u64,
426) -> Result<Vec<BuiltLink>> {
427 let catalog = Catalog::open(path)?;
428 let child = catalog.table(table)?;
429 let column_bytes = child.layout().columns_total();
430 let allowance = (column_bytes.saturating_mul(share) / 100).max(BUDGET_FLOOR);
431 let replacing = edges.iter().map(|edge| edge.child_column).collect::<Vec<usize>>();
432 let mut spent = held_bytes_except(&child, *section::FORWARD_LINK, &replacing)?;
433 let mut report = Vec::with_capacity(edges.len());
434 let mut payloads: Vec<Option<Vec<u8>>> = Vec::with_capacity(edges.len());
435 for edge in edges {
436 let start = Instant::now();
437 match one_link(&catalog, &child, edge) {
438 Ok((built, bytes)) => {
439 report.push(BuiltLink {
440 build: start.elapsed(),
441 table_bytes: column_bytes,
442 ..built
443 });
444 payloads.push(Some(bytes));
445 }
446 Err(note) => {
447 report.push(BuiltLink {
448 edge: edge.clone(),
449 form: None,
450 children: child.table().rows() as u64,
451 linked: 0,
452 bytes: 0,
453 table_bytes: column_bytes,
454 degrees: None,
455 built: false,
456 note: Some(note),
457 build: start.elapsed(),
458 });
459 payloads.push(None);
460 }
461 }
462 }
463 let mut order = (0..report.len()).filter(|at| payloads[*at].is_some()).collect::<Vec<_>>();
464 order.sort_by(|left, right| {
467 let value = |at: &usize| -> f64 {
468 let bytes = report[*at].bytes.max(1);
469 report[*at].children as f64 / bytes as f64
470 };
471 value(right).partial_cmp(&value(left)).unwrap_or(std::cmp::Ordering::Equal)
472 });
473 for at in order {
474 let cost = report[at].bytes as u64;
475 if spent.saturating_add(cost) <= allowance {
476 spent += cost;
477 report[at].built = true;
478 } else {
479 report[at].note = Some(format!("over the budget of {allowance} bytes"));
480 }
481 }
482 drop(child);
483 let measured = report
486 .iter()
487 .filter(|built| built.built)
488 .filter_map(|built| {
489 let mut bytes = Vec::with_capacity(rudb_graph::degree::BYTES);
490 built.degrees.as_ref()?.write(&mut bytes);
491 Some((built.edge.child_column, bytes))
492 })
493 .collect::<Vec<_>>();
494 let mut attachments = report
500 .iter()
501 .zip(&payloads)
502 .filter(|(_, payload)| payload.is_some())
503 .map(|(built, payload)| {
504 let bytes = payload.as_ref().expect("filtered to the measured");
505 Ok(Attachment {
506 kind: *section::FORWARD_LINK,
507 id: u64::try_from(built.edge.child_column)
508 .map_err(|_| invalid("column index overflow"))?,
509 flags: built.form.map_or(0, |form| u32::from(form.tag())),
510 header_bytes: if built.built {
511 u32::try_from(binding_bytes(&built.edge.parent))
512 .map_err(|_| invalid("a parent name longer than a section header"))?
513 } else {
514 cost(bytes.len())
515 },
516 bytes: if built.built { bytes } else { &[] },
517 })
518 })
519 .collect::<Result<Vec<_>>>()?;
520 for (column, bytes) in &measured {
525 attachments.push(Attachment {
526 kind: *section::DEGREES,
527 id: u64::try_from(*column).map_err(|_| invalid("column index overflow"))?,
528 flags: 0,
529 header_bytes: 0,
530 bytes,
531 });
532 }
533 crate::attach(path, table, &attachments)?;
534 Ok(report)
535}
536
537fn one_link(
544 catalog: &Catalog,
545 child: &Reader,
546 edge: &Edge,
547) -> std::result::Result<(BuiltLink, Vec<u8>), String> {
548 let parent =
549 catalog.table(&edge.parent).map_err(|_| format!("no table named {}", edge.parent))?;
550 let map = key_map(&parent, edge.parent_column)
551 .ok_or_else(|| format!("no key map is stored for {}", edge.parent))?;
552 if !map.observed().usable_as_parent() {
553 return Err(format!("the key of {} is not unique", edge.parent));
554 }
555 let keys = KeyColumn::new(child, edge.child_column).map_err(|error| error.to_string())?;
556 let mut parents_of = Vec::with_capacity(child.table().rows());
557 let mut failed = None;
558 keys.scan(&mut |key| {
559 let parent = match key {
560 None => NO_PARENT,
561 Some(key) => match map.lookup(key) {
562 Ok(found) => found.unwrap_or(NO_PARENT),
563 Err(error) => {
564 failed = Some(error.to_string());
565 NO_PARENT
566 }
567 },
568 };
569 parents_of.push(parent);
570 Ok(())
571 })
572 .map_err(|error| error.to_string())?;
573 if let Some(failed) = failed {
574 return Err(failed);
575 }
576 let link = link::Link::build(&parents_of, map.len()).map_err(|error| error.to_string())?;
577 let degrees = Degrees::of(&parents_of, map.len(), true);
586 let bytes = encode_link(&link, &parent, edge).map_err(|error| error.to_string())?;
587 Ok((
588 BuiltLink {
589 edge: edge.clone(),
590 form: Some(link.form()),
591 children: link.children(),
592 linked: link.linked(),
593 bytes: bytes.len(),
594 table_bytes: 0,
595 degrees: Some(degrees),
596 built: false,
597 note: None,
598 build: Duration::ZERO,
599 },
600 bytes,
601 ))
602}
603
604fn cost(bytes: usize) -> u32 {
610 u32::try_from(bytes).unwrap_or(u32::MAX)
611}
612
613fn binding_bytes(parent: &str) -> usize {
618 16 + parent.len().div_ceil(8) * 8
619}
620
621fn encode_link(link: &link::Link, parent: &Reader, edge: &Edge) -> Result<Vec<u8>> {
630 let name = edge.parent.as_bytes();
631 let mut bytes = Vec::with_capacity(binding_bytes(&edge.parent) + link.bytes());
632 bytes.extend_from_slice(&parent.table().generation().to_le_bytes());
633 bytes.extend_from_slice(
634 &u32::try_from(edge.parent_column)
635 .map_err(|_| invalid("column index overflow"))?
636 .to_le_bytes(),
637 );
638 bytes.extend_from_slice(
639 &u32::try_from(name.len())
640 .map_err(|_| invalid("a parent name longer than a u32"))?
641 .to_le_bytes(),
642 );
643 bytes.extend_from_slice(name);
644 bytes.resize(binding_bytes(&edge.parent), 0);
645 link.write(&mut bytes)?;
646 Ok(bytes)
647}
648
649#[must_use]
657pub fn stored_link(child: &Reader, parent: &Reader, edge: &Edge) -> Option<link::Link> {
658 let table = child.table();
659 let id = u64::try_from(edge.child_column).ok()?;
660 let held = table
661 .sections()
662 .iter()
663 .find(|section| section.kind == *section::FORWARD_LINK && section.id == id)?;
664 if !held.usable(table.generation()) {
665 return None;
666 }
667 let bytes = child.payload(held).ok()?;
668 let binding = binding_bytes(&edge.parent);
669 if bytes.len() < binding {
670 return None;
671 }
672 let generation = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
673 let column = u32::from_le_bytes(bytes[8..12].try_into().ok()?);
674 let length = u32::from_le_bytes(bytes[12..16].try_into().ok()?) as usize;
675 if generation != parent.table().generation()
676 || column as usize != edge.parent_column
677 || length != edge.parent.len()
678 || &bytes[16..16 + length] != edge.parent.as_bytes()
679 {
680 return None;
681 }
682 link::Link::read(&bytes[binding..]).ok()
683}
684
685#[must_use]
692pub fn stored_degrees(child: &Reader, child_column: usize) -> Option<Degrees> {
693 let table = child.table();
694 let id = u64::try_from(child_column).ok()?;
695 let held = table
696 .sections()
697 .iter()
698 .find(|section| section.kind == *section::DEGREES && section.id == id)?;
699 if !held.usable(table.generation()) {
700 return None;
701 }
702 Degrees::read(&child.payload(held).ok()?).ok()
703}
704
705#[must_use]
712pub fn refused_key_map(reader: &Reader, column: usize) -> Option<(Form, u64)> {
713 let (form, bytes) = refused(reader, *section::KEY_MAP, column)?;
714 Some((Form::from_tag(form).ok()?, bytes))
715}
716
717#[must_use]
721pub fn refused_link(child: &Reader, child_column: usize) -> Option<(link::Form, u64)> {
722 let (form, bytes) = refused(child, *section::FORWARD_LINK, child_column)?;
723 Some((link::Form::from_tag(form).ok()?, bytes))
724}
725
726fn refused(reader: &Reader, kind: [u8; 8], id: usize) -> Option<(u8, u64)> {
728 let table = reader.table();
729 let id = u64::try_from(id).ok()?;
730 let held = table.sections().iter().find(|section| section.kind == kind && section.id == id)?;
731 if !held.usable(table.generation()) {
732 return None;
733 }
734 Some((u8::try_from(held.flags).ok()?, held.refused()?))
735}
736
737fn held_bytes_except(reader: &Reader, kind: [u8; 8], replacing: &[usize]) -> Result<u64> {
739 let mut total = 0;
740 for held in reader.table().sections() {
741 if !held.among(section::GRAPH_KINDS) {
742 continue;
743 }
744 let replaced =
745 held.kind == kind && replacing.iter().any(|&id| u64::try_from(id) == Ok(held.id));
746 if replaced || !held.usable(reader.table().generation()) {
747 continue;
748 }
749 let Ok(extents) = reader.extents(held) else { continue };
750 total += extents.iter().map(|extent| u64::from(extent.length)).sum::<u64>();
751 }
752 Ok(total)
753}
754
755#[cfg(test)]
756mod tests {
757 use std::fs;
758 use std::path::PathBuf;
759 use std::time::{SystemTime, UNIX_EPOCH};
760
761 use rudb_common::Field;
762 use rudb_graph::Rid;
763 use rudb_vector::Vector;
764
765 use super::*;
766 use crate::Writer;
767
768 fn path(label: &str) -> PathBuf {
769 let stamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("time advances").as_nanos();
770 std::env::temp_dir().join(format!("rudb-graph-{label}-{}-{stamp}.rdb", std::process::id()))
771 }
772
773 fn graph_sections(reader: &Reader) -> Vec<§ion::Section> {
779 reader.table().sections().iter().filter(|held| held.among(section::GRAPH_KINDS)).collect()
780 }
781
782 fn table_of(label: &str, keys: &[Option<i64>]) -> PathBuf {
784 let path = path(label);
785 let mut writer =
786 Writer::create(&path, "parent", vec![Field::new("key", LogicalType::BigInt)])
787 .expect("new file");
788 for part in keys.chunks(1000) {
789 let values =
790 part.iter().map(|key| key.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
791 let chunk =
792 Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
793 .expect("one column");
794 writer.append(&chunk).expect("a part");
795 }
796 writer.finish().expect("commit");
797 path
798 }
799
800 fn resolves(keys: &[Option<i64>], map: &KeyMap) {
802 for (rid, key) in keys.iter().enumerate() {
803 let Some(key) = *key else { continue };
804 let found =
805 map.lookup(i128::from(key)).expect("lookup").expect("a key in the column resolves");
806 assert_eq!(found, rid as Rid, "key {key} resolved to {found} rather than {rid}");
807 }
808 }
809
810 #[test]
811 fn a_key_map_built_over_a_file_resolves_every_key_to_its_own_row() {
812 let keys = (1..=3000_i64).map(Some).collect::<Vec<_>>();
817 let path = table_of("identity", &keys);
818 let built = build_key_maps(&path, "parent", &[0]).expect("build");
819 assert_eq!(built.len(), 1);
820 assert_eq!(built[0].form, Form::Identity);
821 assert_eq!(built[0].rows, 3000);
822 assert!(built[0].distinct);
823 assert_eq!(built[0].bytes, wire::HEADER_BYTES, "identity is a header and nothing else");
824
825 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
826 let map = key_map(&reader, 0).expect("the map is in the file");
827 assert_eq!(map.form(), Form::Identity);
828 resolves(&keys, &map);
829 assert_eq!(map.lookup(0).expect("a key below the column"), None);
830 assert_eq!(map.lookup(3001).expect("a key past the column"), None);
831
832 fs::remove_file(&path).expect("clean up");
833 }
834
835 #[test]
836 fn a_column_with_gaps_takes_the_bitmap_form_and_still_resolves() {
837 let keys = (0..2000_i64).map(|value| Some(value * 4 + 7)).collect::<Vec<_>>();
838 let path = table_of("dense", &keys);
839 let built = build_key_maps(&path, "parent", &[0]).expect("build");
840 assert_eq!(built[0].form, Form::Dense);
841
842 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
843 let map = key_map(&reader, 0).expect("the map is in the file");
844 resolves(&keys, &map);
845 assert_eq!(map.lookup(8).expect("a value in the range but not the column"), None);
846
847 fs::remove_file(&path).expect("clean up");
848 }
849
850 #[test]
851 fn a_column_out_of_order_takes_the_sorted_form_and_still_resolves() {
852 let keys = (0..1500_i64).map(|value| Some((value * 7919) % 100_003)).collect::<Vec<_>>();
853 let path = table_of("sorted", &keys);
854 let built = build_key_maps(&path, "parent", &[0]).expect("build");
855 assert_eq!(built[0].form, Form::Sorted);
856 assert!(built[0].distinct, "the sort settles distinctness for an unordered column");
857
858 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
859 let map = key_map(&reader, 0).expect("the map is in the file");
860 resolves(&keys, &map);
861
862 fs::remove_file(&path).expect("clean up");
863 }
864
865 #[test]
866 fn a_null_in_the_key_column_does_not_shift_the_rows_after_it() {
867 let mut keys = (1..=1200_i64).map(Some).collect::<Vec<_>>();
872 keys[3] = None;
873 keys[900] = None;
874 let path = table_of("nulls", &keys);
875 let built = build_key_maps(&path, "parent", &[0]).expect("build");
876 assert_eq!(built[0].rows, 1198, "a null is not a key");
877
878 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
879 let map = key_map(&reader, 0).expect("the map is in the file");
880 resolves(&keys, &map);
881
882 fs::remove_file(&path).expect("clean up");
883 }
884
885 #[test]
886 fn a_column_with_a_repeat_in_it_is_mapped_and_reported_as_no_parent() {
887 let mut keys = (1..=500_i64).map(Some).collect::<Vec<_>>();
892 keys[200] = Some(7);
893 let path = table_of("repeat", &keys);
894 let built = build_key_maps(&path, "parent", &[0]).expect("build");
895 assert!(!built[0].distinct, "a repeat is observed rather than declared away");
896 assert!(!built[0].built, "and a map no rid can be resolved through is not kept");
897 assert!(built[0].bytes > 0, "what it would have cost is still reported");
898
899 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
900 assert!(key_map(&reader, 0).is_none(), "no map was written to read back");
901 let (form, bytes) = refused_key_map(&reader, 0).expect("the record of what it would cost");
905 assert_eq!(form, built[0].form);
906 assert_eq!(bytes, built[0].bytes as u64);
907 assert_eq!(graph_sections(&reader).len(), 1, "one entry, and no payload");
908 assert_eq!(graph_sections(&reader)[0].extents, 0);
909
910 fs::remove_file(&path).expect("clean up");
911 }
912
913 #[test]
914 fn a_table_with_no_key_map_answers_with_none_rather_than_an_error() {
915 let path = table_of("absent", &[Some(1), Some(2)]);
918 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
919 assert!(key_map(&reader, 0).is_none());
920 assert!(key_map(&reader, 99).is_none(), "a column that does not exist is not a panic");
921 fs::remove_file(&path).expect("clean up");
922 }
923
924 #[test]
925 fn a_stale_key_map_is_ignored_and_the_table_still_reads() {
926 let path = table_of("stale", &(1..=100_i64).map(Some).collect::<Vec<_>>());
927 build_key_maps(&path, "parent", &[0]).expect("build");
928
929 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
932 assert!(key_map(&reader, 0).is_some());
933 let generation = reader.table().generation();
934 drop(reader);
935
936 let held = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
938 let mut entry = *graph_sections(&held).first().copied().expect("the key map");
939 assert!(entry.usable(generation));
940 entry.generation = generation + 1;
941 assert!(!entry.usable(generation), "a rewrite invalidates rather than corrupts");
942
943 fs::remove_file(&path).expect("clean up");
944 }
945
946 #[test]
947 fn a_torn_key_map_costs_the_shortcut_and_not_the_query() {
948 let keys = (1..=200_i64).map(Some).collect::<Vec<_>>();
949 let path = table_of("torn", &keys);
950 build_key_maps(&path, "parent", &[0]).expect("build");
951
952 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
953 let extent = reader
954 .extents(graph_sections(&reader).first().copied().expect("the key map"))
955 .expect("extent table")
956 .first()
957 .copied()
958 .expect("one extent");
959 drop(reader);
960 let file = fs::OpenOptions::new().write(true).open(&path).expect("reopen to corrupt");
961 crate::write_at(&file, extent.offset, &[0xff; 8]).expect("flip the header");
962 drop(file);
963
964 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
965 assert!(key_map(&reader, 0).is_none(), "a payload that does not checksum is not a map");
966 assert_eq!(reader.table().rows(), 200, "and the table is untouched");
967
968 fs::remove_file(&path).expect("clean up");
969 }
970
971 #[test]
972 fn a_column_with_no_integer_key_form_is_refused_by_name() {
973 let path = path("varchar");
974 let mut writer =
975 Writer::create(&path, "parent", vec![Field::new("name", LogicalType::Varchar)])
976 .expect("new file");
977 let chunk = Chunk::new(vec![
978 Vector::from_values(LogicalType::Varchar, &[Value::Varchar("a".into())])
979 .expect("one name"),
980 ])
981 .expect("one column");
982 writer.append(&chunk).expect("a part");
983 writer.finish().expect("commit");
984
985 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
986 let error = KeyColumn::new(&reader, 0).expect_err("a string key needs its codes");
987 assert!(error.to_string().contains("integer key form"), "{error}");
988
989 fs::remove_file(&path).expect("clean up");
990 }
991
992 #[test]
993 fn several_columns_are_mapped_in_one_commit() {
994 let path = path("two_columns");
995 let mut writer = Writer::create(
996 &path,
997 "parent",
998 vec![
999 Field::required("id", LogicalType::BigInt),
1000 Field::required("code", LogicalType::Integer),
1001 ],
1002 )
1003 .expect("new file");
1004 let ids = (1..=400_i64).map(Value::BigInt).collect::<Vec<_>>();
1005 let codes = (1..=400_i32).map(|code| Value::Integer(code * 3)).collect::<Vec<_>>();
1006 let chunk = Chunk::new(vec![
1007 Vector::from_values(LogicalType::BigInt, &ids).expect("ids"),
1008 Vector::from_values(LogicalType::Integer, &codes).expect("codes"),
1009 ])
1010 .expect("two columns");
1011 writer.append(&chunk).expect("a part");
1012 writer.finish().expect("commit");
1013
1014 let built = build_key_maps(&path, "parent", &[0, 1]).expect("build both");
1015 assert_eq!(built.len(), 2);
1016 assert_eq!(built[0].form, Form::Identity);
1017 assert_eq!(built[1].form, Form::Dense);
1018
1019 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1020 assert_eq!(graph_sections(&reader).len(), 2, "one commit and two entries");
1021 assert_eq!(key_map(&reader, 0).expect("the id map").form(), Form::Identity);
1022 assert_eq!(key_map(&reader, 1).expect("the code map").form(), Form::Dense);
1023 assert_eq!(
1024 key_map(&reader, 1).expect("the code map").lookup(9).expect("lookup"),
1025 Some(2),
1026 "the third code is the third row"
1027 );
1028
1029 fs::remove_file(&path).expect("clean up");
1030 }
1031
1032 #[test]
1033 fn the_statistics_sections_do_not_count_against_the_graph_budget() {
1034 let keys = (1..=3000_i64).map(Some).collect::<Vec<_>>();
1040 let path = table_of("apart", &keys);
1041 crate::stats::build_stats(&path, "parent", &[0]).expect("summaries first");
1042
1043 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1044 let statistics = reader
1045 .table()
1046 .sections()
1047 .iter()
1048 .filter(|held| held.among(section::STATISTICS_KINDS))
1049 .count();
1050 assert_eq!(statistics, 2, "a summary and a sketch are in the file");
1051 assert_eq!(held_bytes(&reader, &[0]).expect("held"), 0, "and neither is the graph's");
1052
1053 drop(reader);
1054 fs::remove_file(&path).expect("clean up");
1055 }
1056
1057 #[test]
1058 fn a_map_that_does_not_fit_the_budget_is_measured_and_not_written() {
1059 let keys = (0..100_000_i64).map(|value| Some(value * 8)).collect::<Vec<_>>();
1066 let path = table_of("budget", &keys);
1067 let built = build_key_maps(&path, "parent", &[0]).expect("build");
1068 assert_eq!(built[0].form, Form::Dense);
1069 assert!(!built[0].built, "a map ten times its column does not fit a tenth of it");
1070 assert!(built[0].bytes as u64 > built[0].column_bytes, "{built:?}");
1071
1072 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1073 assert!(key_map(&reader, 0).is_none(), "and no map was written");
1074 assert_eq!(refused_key_map(&reader, 0), Some((Form::Dense, built[0].bytes as u64)));
1077 assert_eq!(held_bytes(&reader, &[]).expect("held"), 0, "a record costs the budget nothing");
1078 drop(reader);
1079
1080 let built = build_key_maps_within(&path, "parent", &[0], 100_000).expect("build");
1083 assert!(built[0].built);
1084 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1085 let map = key_map(&reader, 0).expect("the map is in the file");
1086 resolves(&keys, &map);
1087
1088 fs::remove_file(&path).expect("clean up");
1089 }
1090
1091 #[test]
1092 fn the_budget_admits_the_cheapest_maps_it_can_fit() {
1093 let path = path("budget_order");
1099 let mut writer = Writer::create(
1100 &path,
1101 "parent",
1102 vec![
1103 Field::required("id", LogicalType::BigInt),
1104 Field::required("code", LogicalType::BigInt),
1105 ],
1106 )
1107 .expect("new file");
1108 let ids = (1..=100_000_i64).map(Value::BigInt).collect::<Vec<_>>();
1109 let codes = (1..=100_000_i64)
1110 .map(|code| Value::BigInt((code * 2_147_483_647) % 999_999_937))
1111 .collect::<Vec<_>>();
1112 for part in 0..100 {
1113 let at = part * 1000;
1114 let chunk = Chunk::new(vec![
1115 Vector::from_values(LogicalType::BigInt, &ids[at..at + 1000]).expect("ids"),
1116 Vector::from_values(LogicalType::BigInt, &codes[at..at + 1000]).expect("codes"),
1117 ])
1118 .expect("two columns");
1119 writer.append(&chunk).expect("a part");
1120 }
1121 writer.finish().expect("commit");
1122
1123 let built = build_key_maps(&path, "parent", &[1, 0]).expect("build");
1124 assert_eq!(built[0].column, 1, "the report is in the order it was asked in");
1125 assert_eq!(built[0].form, Form::Sorted);
1126 assert!(!built[0].built, "the sorted map did not fit: {built:?}");
1127 assert!(built[1].built, "the identity map did, and was reached second: {built:?}");
1128
1129 let reader = Catalog::open(&path).expect("reopen").table("parent").expect("the table");
1130 assert!(key_map(&reader, 0).is_some());
1131 assert!(key_map(&reader, 1).is_none());
1132
1133 fs::remove_file(&path).expect("clean up");
1134 }
1135
1136 fn related(label: &str, parents: i64, foreign: &[Option<i64>]) -> PathBuf {
1140 let path = table_of(label, &(1..=parents).map(Some).collect::<Vec<_>>());
1141 let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
1142 .expect("a second table");
1143 for part in foreign.chunks(1000) {
1144 let values =
1145 part.iter().map(|key| key.map_or(Value::Null, Value::BigInt)).collect::<Vec<_>>();
1146 let chunk =
1147 Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
1148 .expect("one column");
1149 writer.append(&chunk).expect("a part");
1150 }
1151 writer.finish().expect("commit");
1152 build_key_maps(&path, "parent", &[0]).expect("the parent's key map");
1153 path
1154 }
1155
1156 fn edge() -> Edge {
1157 Edge { child: "child".into(), child_column: 0, parent: "parent".into(), parent_column: 0 }
1158 }
1159
1160 fn links(path: &PathBuf, foreign: &[Option<i64>]) -> link::Link {
1163 let catalog = Catalog::open(path).expect("reopen");
1164 let child = catalog.table("child").expect("the child");
1165 let parent = catalog.table("parent").expect("the parent");
1166 let link = stored_link(&child, &parent, &edge()).expect("the link is in the file");
1167 let map = key_map(&parent, 0).expect("the parent's key map");
1168 for (rid, key) in foreign.iter().enumerate() {
1169 let want = key.and_then(|key| map.lookup(i128::from(key)).expect("lookup"));
1170 assert_eq!(link.forward(rid as Rid), want, "child {rid}");
1171 }
1172 link
1173 }
1174
1175 #[test]
1176 fn a_clustered_foreign_key_takes_the_monotone_form_and_answers_both_directions() {
1177 let foreign = (0..4000_i64).map(|child| Some(child / 4 + 1)).collect::<Vec<_>>();
1180 let path = related("monotone", 1000, &foreign);
1181 let report = build_links(&path, &[edge()]).expect("build");
1182 assert_eq!(report.len(), 1);
1183 assert!(report[0].built, "{:?}", report[0].note);
1184 assert_eq!(report[0].form, Some(link::Form::Monotone));
1185 assert_eq!(report[0].children, 4000);
1186 assert_eq!(report[0].linked, 4000);
1187
1188 let link = links(&path, &foreign);
1189 assert_eq!(link.form(), link::Form::Monotone);
1190 assert_eq!(link.backward(0), Some(0..4), "the first parent's four children");
1191 assert_eq!(link.backward(999), Some(3996..4000));
1192 assert_eq!(link.backward(1000), None, "past the last parent");
1193
1194 fs::remove_file(&path).expect("clean up");
1195 }
1196
1197 #[test]
1198 fn an_unclustered_foreign_key_takes_the_packed_form_and_still_resolves() {
1199 let foreign = (0..3000_i64).map(|child| Some((child * 7) % 1000 + 1)).collect::<Vec<_>>();
1200 let path = related("packed", 1000, &foreign);
1201 let report = build_links(&path, &[edge()]).expect("build");
1202 assert!(report[0].built, "{:?}", report[0].note);
1203 assert_eq!(report[0].form, Some(link::Form::Packed));
1204
1205 let link = links(&path, &foreign);
1206 assert_eq!(link.backward(0), None, "the packed form answers one direction");
1207 assert!(link.bytes() < 3000 * 2 + 3 * 16, "{} bytes is not bit-packed", link.bytes());
1211
1212 fs::remove_file(&path).expect("clean up");
1213 }
1214
1215 #[test]
1216 fn a_built_link_leaves_the_shape_of_the_relationship_beside_it() {
1217 let foreign = (0..4000_i64).map(|child| Some(child / 4 + 1)).collect::<Vec<_>>();
1220 let path = related("degrees", 1000, &foreign);
1221 let report = build_links(&path, &[edge()]).expect("build");
1222 assert!(report[0].built, "{:?}", report[0].note);
1223 let measured = report[0].degrees.as_ref().expect("the build measured it");
1224 assert!((measured.mean() - 4.0).abs() < 1e-9);
1225
1226 let catalog = Catalog::open(&path).expect("reopen");
1227 let child = catalog.table("child").expect("the child");
1228 let held = stored_degrees(&child, 0).expect("it is in the file");
1229 assert_eq!(&held, measured, "what the build measured is what the file holds");
1230 assert_eq!(held.parents(), 1000);
1231 assert_eq!(held.highest(), 4);
1232 assert!(held.total(), "every child found a parent");
1233 assert!(held.unique(), "and the parent key is why there is a link at all");
1234 let near = held.locality().expect("something to gather");
1238 assert!((near - 999.0 / 3999.0).abs() < 1e-9, "{near}");
1239 assert!(stored_degrees(&child, 1).is_none(), "and no other column has one");
1240
1241 fs::remove_file(&path).expect("clean up");
1242 }
1243
1244 #[test]
1245 fn a_foreign_key_that_matches_nothing_is_a_child_with_no_parent() {
1246 let foreign = vec![Some(1), Some(2), None, Some(9999), Some(3)];
1249 let path = related("orphans", 10, &foreign);
1250 let report = build_links(&path, &[edge()]).expect("build");
1251 assert!(report[0].built, "{:?}", report[0].note);
1252 assert_eq!(report[0].form, Some(link::Form::Packed));
1253 assert_eq!(report[0].children, 5);
1254 assert_eq!(report[0].linked, 3, "the null and the key that matches nothing are not links");
1255
1256 let link = links(&path, &foreign);
1257 assert_eq!(link.forward(2), None, "a null is not a link");
1258 assert_eq!(link.forward(3), None, "a key that matches nothing is not a link");
1259
1260 fs::remove_file(&path).expect("clean up");
1261 }
1262
1263 #[test]
1264 fn a_parent_with_no_key_map_is_a_relationship_with_no_link_rather_than_an_error() {
1265 let path = table_of("unmapped", &(1..=100_i64).map(Some).collect::<Vec<_>>());
1268 let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
1269 .expect("a second table");
1270 let values = (1..=100_i64).map(Value::BigInt).collect::<Vec<_>>();
1271 writer
1272 .append(
1273 &Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
1274 .expect("one column"),
1275 )
1276 .expect("a part");
1277 writer.finish().expect("commit");
1278
1279 let report = build_links(&path, &[edge()]).expect("build");
1280 assert!(!report[0].built);
1281 assert_eq!(report[0].note.as_deref(), Some("no key map is stored for parent"));
1282
1283 let catalog = Catalog::open(&path).expect("reopen");
1284 let child = catalog.table("child").expect("the child");
1285 let parent = catalog.table("parent").expect("the parent");
1286 assert!(stored_link(&child, &parent, &edge()).is_none());
1287
1288 fs::remove_file(&path).expect("clean up");
1289 }
1290
1291 #[test]
1292 fn a_link_asked_for_against_the_wrong_parent_is_not_handed_over() {
1293 let foreign = (0..500_i64).map(|child| Some(child / 5 + 1)).collect::<Vec<_>>();
1297 let path = related("binding", 100, &foreign);
1298 build_links(&path, &[edge()]).expect("build");
1299
1300 let catalog = Catalog::open(&path).expect("reopen");
1301 let child = catalog.table("child").expect("the child");
1302 let parent = catalog.table("parent").expect("the parent");
1303 assert!(stored_link(&child, &parent, &edge()).is_some());
1304 let wrong = Edge { parent: "child".into(), ..edge() };
1305 assert!(stored_link(&child, &parent, &wrong).is_none(), "a different parent name");
1306 let wrong = Edge { parent_column: 1, ..edge() };
1307 assert!(stored_link(&child, &parent, &wrong).is_none(), "a different parent column");
1308 let wrong = Edge { child_column: 1, ..edge() };
1309 assert!(stored_link(&child, &parent, &wrong).is_none(), "a different child column");
1310
1311 fs::remove_file(&path).expect("clean up");
1312 }
1313
1314 #[test]
1315 fn a_link_that_does_not_fit_the_budget_is_reported_rather_than_stored() {
1316 let foreign = (0..60_000_i64).map(|child| Some((child * 7) % 1000 + 1)).collect::<Vec<_>>();
1319 let path = related("budget", 1000, &foreign);
1320 let report = build_links_within(&path, &[edge()], 0).expect("build");
1321 assert!(!report[0].built);
1322 assert!(report[0].bytes > 0, "the report says what a larger budget would buy");
1323 assert!(report[0].note.as_deref().unwrap_or_default().contains("budget"), "{report:?}");
1324
1325 let catalog = Catalog::open(&path).expect("reopen");
1326 let child = catalog.table("child").expect("the child");
1327 let parent = catalog.table("parent").expect("the parent");
1328 assert!(stored_link(&child, &parent, &edge()).is_none());
1329 assert!(report[0].degrees.is_some(), "it was measured");
1332 assert!(stored_degrees(&child, 0).is_none(), "and not written");
1333 assert_eq!(refused_link(&child, 0), Some((link::Form::Packed, report[0].bytes as u64)));
1336
1337 fs::remove_file(&path).expect("clean up");
1338 }
1339
1340 #[test]
1341 fn a_parent_whose_key_repeats_gets_no_link_at_all() {
1342 let path = table_of("repeats", &[Some(1), Some(1), Some(2)]);
1346 let mut writer = Writer::open(&path, "child", vec![Field::new("fk", LogicalType::BigInt)])
1347 .expect("a second table");
1348 let values = [Value::BigInt(1), Value::BigInt(2)];
1349 writer
1350 .append(
1351 &Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &values).expect("keys")])
1352 .expect("one column"),
1353 )
1354 .expect("a part");
1355 writer.finish().expect("commit");
1356 build_key_maps(&path, "parent", &[0]).expect("the parent's key map");
1357
1358 let report = build_links(&path, &[edge()]).expect("build");
1359 assert!(!report[0].built);
1360 assert_eq!(report[0].note.as_deref(), Some("no key map is stored for parent"));
1361
1362 fs::remove_file(&path).expect("clean up");
1363 }
1364}