#![forbid(unsafe_code)]
use std::borrow::Cow;
use std::cmp::{Ordering, Reverse};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::fs::File;
use std::mem::{size_of, size_of_val};
use std::path::Path;
use std::slice;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as Atomic};
use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError, Weak};
use rudb_common::bounds::{self, Bound, Op, scaled_as};
use rudb_common::{Clustering, Error, Field, LogicalType, PhysicalType, Result, Value, Width};
use rudb_encoding::{bitpack, chooser, integer, string};
use rudb_io::{Filesystem, OpenMode, RealFilesystem};
use rudb_metrics::{LoadProfile, Stage};
use rudb_storage::sieve::Sieve;
use rudb_storage::{Probe, Range, Zone};
use rudb_vector::string::StringColumn;
use rudb_vector::validity::Validity;
use rudb_vector::{Buffer, Chunk, Data, Packed, TextSource, Vector, search_below};
mod distinct;
pub mod graph;
pub mod host;
mod prepare;
mod projection;
mod run_projection;
use prepare::Lent;
pub mod section;
pub mod stats;
mod zones;
pub use prepare::{Building, DICTIONARY_CAP_BYTES, Merged, Merger, Paged, Prepared, Preparer};
pub use projection::build_sorted_projection;
pub use run_projection::build_run_projection;
pub use section::Section;
pub use zones::{Common, Stripes, ascending, distincts};
const MAGIC: &[u8; 8] = b"RUDBNV10";
const DIRECTORY: &[u8; 8] = b"RUDBDI10";
const CATALOG: &[u8; 8] = b"RUDBCA10";
const NONZERO_COUNTS: &[u8; 8] = b"RUDBNZ10";
const AGGREGATE_SUMS: &[u8; 8] = b"RUDBAG10";
const DISTINCT_COUNTS: &[u8; 8] = b"RUDBDC10";
const INTEGER_EXTREMES: &[u8; 8] = b"RUDBEX10";
const COMPLETE_FREQUENCIES: &[u8; 8] = b"RUDBFQ10";
const MAX_CATALOG_FREQUENCIES: usize = 64;
const FORMAT: u32 = 29;
const READABLE: &[u32] = &[22, 23, 24, 25, 26, 27, 28, FORMAT];
const HEADER: u64 = 80;
const SLOT_BYTES: usize = 28;
const MAX_PAGE: usize = 256 * 1024 * 1024;
const MAX_DIRECTORY: usize = 128 * 1024 * 1024;
const FREQUENCIES_V2: &[u8; 8] = b"RUDBFQ2\0";
const FREQUENCIES: &[u8; 8] = b"RUDBFQ3\0";
const FREQUENCY_TEXTS: &[u8; 8] = b"RUDBFT1\0";
const HOST_GROUPS: &[u8; 8] = b"RUDBHG1\0";
const PAIR_FREQUENCIES: &[u8; 8] = b"RUDBPF1\0";
const CLUSTERING: &[u8; 8] = b"RUDBCL1\0";
const DEMOTED: &[u8; 8] = b"RUDBDM1\0";
const SECTIONS: &[u8; 8] = b"RUDBSE1\0";
const DICTIONARY_PAYLOADS: &[u8; 8] = b"RUDBDP1\0";
const MAX_SECTIONS: usize = 4096;
const FREQUENCY_CANDIDATES: usize = 32_768;
const FREQUENCY_ENTRIES: usize = 512;
const FREQUENCY_BUILD_RANK: usize = 10;
const FREQUENCY_ORDINALS: usize = 131_072;
const MAX_PAIR_FREQUENCIES: usize = 1024;
const FREQUENCY_TEXT_BUDGET: usize = 1024 * 1024;
const MAX_FREQUENCY_WORKERS: usize = 32;
fn close_workers() -> usize {
std::thread::available_parallelism().map_or(1, usize::from).min(MAX_FREQUENCY_WORKERS)
}
const CLOSE_BYTES: usize = 1 << 30;
const NUMERIC_CLOSE_BYTES: usize = 4 << 20;
const MAX_ENCODE_WORKERS: usize = 32;
const WRITEBACK_STRETCH: u64 = 32 << 20;
const SIEVE_BUDGET: usize = 8 * 1024;
const PART_BOUND_BYTES: usize = 24;
fn io(error: std::io::Error) -> Error {
Error::io(error.to_string())
}
fn invalid(message: &str) -> Error {
Error::invalid_input(format!("invalid rudb native file: {message}"))
}
fn sum(counts: impl Iterator<Item = u64>) -> u64 {
counts.fold(0, u64::saturating_add)
}
fn span_bytes(spans: &[Span], at: usize) -> u64 {
spans.get(at).map_or(0, |span| u64::from(span.length))
}
fn page_bytes(pages: &[Option<Page>], at: usize) -> u64 {
pages.get(at).and_then(Option::as_ref).map_or(0, Page::bytes)
}
fn dictionary_bytes(table: &Table, at: usize) -> u64 {
page_bytes(&table.dictionaries, at)
.saturating_add(table.dictionary_payloads.get(at).copied().unwrap_or(0))
}
fn checksum(bytes: &[u8]) -> u64 {
seeded_checksum(bytes, 0)
}
#[must_use]
pub fn content_name(bytes: &[u8]) -> u128 {
let seed = u64::from(FORMAT);
u128::from(seeded_checksum(bytes, seed)) << 64 | u128::from(seeded_checksum(bytes, !seed))
}
#[derive(Debug, Clone)]
pub struct ContentNamer {
seeds: [u64; 2],
lanes: [[u64; 4]; 2],
held: [u8; 32],
filled: usize,
length: u64,
}
impl Default for ContentNamer {
fn default() -> Self {
let seed = u64::from(FORMAT);
let seeds = [seed, !seed];
let lanes = seeds.map(|seed| {
[
seed.wrapping_add(XXH_P1).wrapping_add(XXH_P2),
seed.wrapping_add(XXH_P2),
seed,
seed.wrapping_sub(XXH_P1),
]
});
Self { seeds, lanes, held: [0; 32], filled: 0, length: 0 }
}
}
impl ContentNamer {
pub fn update(&mut self, mut bytes: &[u8]) {
self.length += bytes.len() as u64;
if self.filled > 0 {
let take = (32 - self.filled).min(bytes.len());
self.held[self.filled..self.filled + take].copy_from_slice(&bytes[..take]);
self.filled += take;
bytes = &bytes[take..];
if self.filled < 32 {
return;
}
let block = self.held;
self.lanes.iter_mut().for_each(|lanes| checksum_block(lanes, &block));
self.filled = 0;
}
let mut blocks = bytes.chunks_exact(32);
for block in blocks.by_ref() {
self.lanes.iter_mut().for_each(|lanes| checksum_block(lanes, block));
}
let rest = blocks.remainder();
self.held[..rest.len()].copy_from_slice(rest);
self.filled = rest.len();
}
#[must_use]
pub fn finish(&self) -> u128 {
let rest = &self.held[..self.filled];
let [first, second] = [0, 1].map(|at| {
if self.length < 32 {
checksum_tail(self.seeds[at].wrapping_add(XXH_P5).wrapping_add(self.length), rest)
} else {
finish_checksum(self.lanes[at], rest, self.length)
}
});
u128::from(first) << 64 | u128::from(second)
}
}
fn seeded_checksum(bytes: &[u8], seed: u64) -> u64 {
let mut blocks = bytes.chunks_exact(32);
let rest = blocks.remainder();
if bytes.len() < 32 {
return checksum_tail(seed.wrapping_add(XXH_P5).wrapping_add(bytes.len() as u64), rest);
}
let mut lanes = [
seed.wrapping_add(XXH_P1).wrapping_add(XXH_P2),
seed.wrapping_add(XXH_P2),
seed,
seed.wrapping_sub(XXH_P1),
];
for block in blocks.by_ref() {
checksum_block(&mut lanes, block);
}
finish_checksum(lanes, rest, bytes.len() as u64)
}
const XXH_P1: u64 = 11_400_714_785_074_694_791;
const XXH_P2: u64 = 14_029_467_366_897_019_727;
const XXH_P3: u64 = 1_609_587_929_392_839_161;
const XXH_P4: u64 = 9_650_029_242_287_828_579;
const XXH_P5: u64 = 2_870_177_450_012_600_261;
fn checksum_round(state: u64, word: u64) -> u64 {
state.wrapping_add(word.wrapping_mul(XXH_P2)).rotate_left(31).wrapping_mul(XXH_P1)
}
fn checksum_word(chunk: &[u8]) -> u64 {
u64::from_le_bytes(chunk.try_into().expect("eight checksum bytes"))
}
fn checksum_block(lanes: &mut [u64; 4], block: &[u8]) {
for (lane, chunk) in lanes.iter_mut().zip(block.chunks_exact(8)) {
*lane = checksum_round(*lane, checksum_word(chunk));
}
}
fn finish_checksum(lanes: [u64; 4], rest: &[u8], length: u64) -> u64 {
let merge = |state: u64, lane: u64| {
(state ^ checksum_round(0, lane)).wrapping_mul(XXH_P1).wrapping_add(XXH_P4)
};
let [one, two, three, four] = lanes;
let combined = one
.rotate_left(1)
.wrapping_add(two.rotate_left(7))
.wrapping_add(three.rotate_left(12))
.wrapping_add(four.rotate_left(18));
let hash = merge(merge(merge(merge(combined, one), two), three), four);
checksum_tail(hash.wrapping_add(length), rest)
}
fn checksum_tail(mut hash: u64, mut rest: &[u8]) -> u64 {
let mut words = rest.chunks_exact(8);
for chunk in words.by_ref() {
hash ^= checksum_round(0, checksum_word(chunk));
hash = hash.rotate_left(27).wrapping_mul(XXH_P1).wrapping_add(XXH_P4);
}
rest = words.remainder();
if rest.len() >= 4 {
let (head, tail) = rest.split_at(4);
let quarter = u32::from_le_bytes(head.try_into().expect("four checksum bytes"));
hash ^= u64::from(quarter).wrapping_mul(XXH_P1);
hash = hash.rotate_left(23).wrapping_mul(XXH_P2).wrapping_add(XXH_P3);
rest = tail;
}
for &byte in rest {
hash ^= u64::from(byte).wrapping_mul(XXH_P5);
hash = hash.rotate_left(11).wrapping_mul(XXH_P1);
}
hash ^= hash >> 33;
hash = hash.wrapping_mul(XXH_P2);
hash ^= hash >> 29;
hash = hash.wrapping_mul(XXH_P3);
hash ^ (hash >> 32)
}
fn file_checksum(file: &File, offset: u64, length: usize) -> Result<u64> {
walk_checksummed(file, offset, length, DIRECTORY_WINDOW, |_| Ok(()))
}
fn walk_checksummed(
file: &File,
offset: u64,
length: usize,
window: usize,
mut each: impl FnMut(&[u8]) -> Result<()>,
) -> Result<u64> {
debug_assert!(window % 32 == 0 && window > 0, "a window is whole blocks of the hash");
if length < 32 {
let mut bytes = vec![0; length];
read_at(file, offset, &mut bytes)?;
each(&bytes)?;
return Ok(checksum(&bytes));
}
let mut lanes = [XXH_P1.wrapping_add(XXH_P2), XXH_P2, 0, 0_u64.wrapping_sub(XXH_P1)];
let mut buffer = vec![0; window.min(length)];
let mut read = 0;
let (mut whole, mut filled) = (0, 0);
while read < length {
filled = buffer.len().min(length - read);
read_at(file, offset + read as u64, &mut buffer[..filled])?;
read += filled;
each(&buffer[..filled])?;
whole = filled / 32 * 32;
for block in buffer[..whole].chunks_exact(32) {
checksum_block(&mut lanes, block);
}
}
Ok(finish_checksum(lanes, &buffer[whole..filled], length as u64))
}
#[derive(Debug, Clone, Copy)]
struct Slot {
offset: u64,
length: u32,
generation: u64,
hash: u64,
}
impl Slot {
fn bytes(self) -> [u8; SLOT_BYTES] {
let mut result = [0; SLOT_BYTES];
result[..8].copy_from_slice(&self.offset.to_le_bytes());
result[8..12].copy_from_slice(&self.length.to_le_bytes());
result[12..20].copy_from_slice(&self.generation.to_le_bytes());
result[20..28].copy_from_slice(&self.hash.to_le_bytes());
result
}
fn read(bytes: &[u8]) -> Self {
Self {
offset: u64::from_le_bytes(bytes[..8].try_into().expect("eight bytes")),
length: u32::from_le_bytes(bytes[8..12].try_into().expect("four bytes")),
generation: u64::from_le_bytes(bytes[12..20].try_into().expect("eight bytes")),
hash: u64::from_le_bytes(bytes[20..28].try_into().expect("eight bytes")),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Page {
offset: u64,
length: u32,
hash: u64,
}
impl Page {
fn bytes(&self) -> u64 {
u64::from(self.length)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum FrequencyValue {
Null,
Integer(i128),
Code(u32),
}
type FrequencyMap<V> = HashMap<u64, V, Spread>;
#[derive(Debug)]
struct Candidates {
slots: Vec<Candidate>,
held: usize,
nulls: u32,
decrements: u64,
survivors: Vec<Candidate>,
}
#[derive(Debug, Default, Clone, Copy)]
struct Candidate {
bits: u64,
count: u32,
}
const FIRST_CANDIDATE_SLOTS: usize = 64;
impl Default for Candidates {
fn default() -> Self {
Self {
slots: vec![Candidate::default(); FIRST_CANDIDATE_SLOTS],
held: 0,
nulls: 0,
decrements: 0,
survivors: Vec::new(),
}
}
}
impl Candidates {
fn add(&mut self, bits: Option<u64>, mut times: u32) {
while times > 0 {
let room = self.held + usize::from(self.nulls != 0) < FREQUENCY_CANDIDATES;
match bits {
Some(bits) => {
let (at, found) = self.find(bits);
if found {
self.slots[at].count = self.slots[at].count.saturating_add(times);
return;
}
if room {
self.place(at, bits, times);
return;
}
}
None if self.nulls != 0 => {
self.nulls = self.nulls.saturating_add(times);
return;
}
None if room => {
self.nulls = times;
return;
}
None => {}
}
self.decrement();
times -= 1;
}
}
fn find(&self, bits: u64) -> (usize, bool) {
let mask = self.slots.len() - 1;
let mut at = home(bits, self.slots.len());
loop {
let slot = self.slots[at];
if slot.count == 0 {
return (at, false);
}
if slot.bits == bits {
return (at, true);
}
at = (at + 1) & mask;
}
}
fn position(&self, bits: u64) -> Option<usize> {
match self.find(bits) {
(at, true) => Some(at),
(_, false) => None,
}
}
fn place(&mut self, at: usize, bits: u64, count: u32) {
let at = if (self.held + 1) * 2 > self.slots.len() {
let wider = self.slots.len() * 2;
let old = std::mem::replace(&mut self.slots, vec![Candidate::default(); wider]);
for slot in old.into_iter().filter(|slot| slot.count != 0) {
let (to, _) = self.find(slot.bits);
self.slots[to] = slot;
}
self.find(bits).0
} else {
at
};
self.slots[at] = Candidate { bits, count };
self.held += 1;
}
fn decrement(&mut self) {
let mut survivors = std::mem::take(&mut self.survivors);
survivors.clear();
survivors.extend(
self.slots
.iter()
.filter(|slot| slot.count > 1)
.map(|slot| Candidate { bits: slot.bits, count: slot.count - 1 }),
);
self.slots.fill(Candidate::default());
self.held = survivors.len();
for &slot in &survivors {
let (at, _) = self.find(slot.bits);
self.slots[at] = slot;
}
self.survivors = survivors;
self.nulls = self.nulls.saturating_sub(1);
self.decrements = self.decrements.saturating_add(1);
}
fn pairs(&self) -> impl Iterator<Item = (u64, u32)> + '_ {
self.slots.iter().filter(|slot| slot.count != 0).map(|slot| (slot.bits, slot.count))
}
}
fn home(bits: u64, slots: usize) -> usize {
(bits.wrapping_mul(0x9E37_79B9_7F4A_7C15) >> (64 - slots.trailing_zeros())) as usize
}
#[derive(Debug, Default)]
struct Run {
bits: Option<u64>,
times: u32,
}
impl Run {
fn push(&mut self, bits: Option<u64>) -> Option<(Option<u64>, u32)> {
if self.times != 0 && self.bits == bits && self.times < u32::MAX {
self.times += 1;
return None;
}
let ended = self.take();
self.bits = bits;
self.times = 1;
ended
}
fn take(&mut self) -> Option<(Option<u64>, u32)> {
let times = std::mem::take(&mut self.times);
(times != 0).then_some((self.bits, times))
}
}
#[derive(Debug, Default, Clone, Copy)]
struct Spread;
impl std::hash::BuildHasher for Spread {
type Hasher = SpreadHasher;
fn build_hasher(&self) -> SpreadHasher {
SpreadHasher(0)
}
}
#[derive(Debug)]
struct SpreadHasher(u64);
impl SpreadHasher {
fn mix(&mut self, word: u64) {
let product = u128::from(self.0 ^ word) * 0x9E37_79B9_7F4A_7C15_u128;
self.0 = (product as u64) ^ ((product >> 64) as u64);
}
}
impl std::hash::Hasher for SpreadHasher {
fn write(&mut self, bytes: &[u8]) {
for part in bytes.chunks(8) {
let mut word = [0; 8];
word[..part.len()].copy_from_slice(part);
self.mix(u64::from_le_bytes(word));
}
}
fn write_u32(&mut self, value: u32) {
self.mix(u64::from(value));
}
fn write_u64(&mut self, value: u64) {
self.mix(value);
}
fn write_i128(&mut self, value: i128) {
self.mix(value as u64);
self.mix((value >> 64) as u64);
}
fn write_isize(&mut self, value: isize) {
self.mix(value as u64);
}
fn finish(&self) -> u64 {
self.0
}
}
#[derive(Debug, Clone)]
struct FrequencyEntry {
value: FrequencyValue,
count: u64,
}
#[derive(Debug, Clone)]
struct FrequencySummary {
entries: Vec<FrequencyEntry>,
omitted_max: u64,
ordinals: Vec<u64>,
ordinal_entries: Vec<u16>,
}
#[derive(Debug, Clone)]
struct PairFrequencyEntry {
first_entry: u16,
second: Option<u32>,
count: u64,
}
#[derive(Debug, Clone)]
struct PairFrequencySummary {
first: u16,
second: u16,
entries: Vec<PairFrequencyEntry>,
omitted_max: u64,
}
#[derive(Debug, Clone)]
enum Frequencies {
Held(FrequencySummary),
Stored {
span: Span,
values: bool,
},
}
#[derive(Debug, Clone)]
pub struct FrequencyPrefix {
pub entries: Vec<(Value, u64)>,
pub omitted_max: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FrequencyOccurrences {
pub omitted_max: u64,
pub ordinals: Vec<u64>,
pub anchors: Vec<Value>,
pub anchor_indices: Vec<u16>,
}
pub type PairFrequencyCounts = Vec<(Vec<Value>, u64)>;
#[derive(Debug, Clone, Copy, Default)]
struct Span {
offset: u64,
length: u32,
}
#[derive(Debug, Clone, Default)]
struct Pages {
columns: usize,
held: Box<[StripePage]>,
}
#[derive(Debug, Clone, Copy)]
struct StripePage {
offset: u64,
hash: u64,
length: u32,
column: u32,
}
impl Pages {
fn from_slots(slots: Vec<Option<Page>>) -> Result<Self> {
let mut held = Vec::with_capacity(slots.iter().flatten().count());
for (column, page) in slots.iter().enumerate() {
if let Some(page) = page {
let column =
u32::try_from(column).map_err(|_| invalid("too many columns for a page"))?;
held.push(StripePage {
offset: page.offset,
hash: page.hash,
length: page.length,
column,
});
}
}
Ok(Self { columns: slots.len(), held: held.into_boxed_slice() })
}
fn get(&self, column: usize) -> Option<Page> {
let at = self.held.binary_search_by_key(&column, |placed| placed.column as usize).ok()?;
let placed = self.held[at];
Some(Page { offset: placed.offset, length: placed.length, hash: placed.hash })
}
fn slots(&self) -> impl Iterator<Item = Option<Page>> + '_ {
(0..self.columns).map(|column| self.get(column))
}
fn bytes(&self, column: usize) -> u64 {
self.get(column).map_or(0, |page| page.bytes())
}
}
#[derive(Debug, Clone)]
pub struct Stripe {
rows: usize,
parts: Vec<u32>,
index: Span,
pages: Vec<Span>,
memberships: Pages,
sieves: Pages,
part_ranges: Pages,
zone: Zone,
}
impl Stripe {
#[must_use]
pub fn rows(&self) -> usize {
self.rows
}
#[must_use]
pub fn parts(&self) -> usize {
self.parts.len()
}
#[must_use]
pub fn zone(&self) -> &Zone {
&self.zone
}
}
#[derive(Debug, Clone)]
pub struct Table {
name: String,
fields: Vec<Field>,
stripes: Vec<Stripe>,
rows: usize,
dictionaries: Vec<Option<Page>>,
dictionary_payloads: Vec<u64>,
demoted: Vec<bool>,
frequencies: Vec<Option<Frequencies>>,
pair_frequencies: Vec<PairFrequencySummary>,
frequency_texts: Vec<Vec<Option<Vec<u8>>>>,
host_groups: Option<host::HostSummary>,
distincts: Vec<Option<u64>>,
clustering: Option<Clustering>,
generation: u64,
sections: Vec<Section>,
}
impl Table {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn fields(&self) -> &[Field] {
&self.fields
}
#[must_use]
pub fn rows(&self) -> usize {
self.rows
}
#[must_use]
pub fn stripes(&self) -> &[Stripe] {
&self.stripes
}
#[must_use]
pub fn clustering(&self) -> Option<&Clustering> {
self.clustering.as_ref()
}
#[must_use]
pub fn generation(&self) -> u64 {
self.generation
}
#[must_use]
pub fn sections(&self) -> &[Section] {
&self.sections
}
}
#[derive(Debug, Clone)]
struct Entry {
name: String,
fields: Vec<Field>,
rows: usize,
directory: Page,
nonzero: Vec<Option<u64>>,
aggregates: Vec<Option<(i128, u64)>>,
distincts: Vec<Option<u64>>,
extremes: Vec<StoredIntegerExtremes>,
frequencies: Vec<StoredNumericFrequencies>,
}
type StoredIntegerExtremes = Option<Option<(i128, i128)>>;
type StoredNumericFrequencies = Option<NumericFrequencies>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ViewEntry {
pub name: String,
pub sql: String,
pub statement: String,
pub aliases: Vec<String>,
pub columns: Vec<Field>,
}
#[derive(Debug, Clone)]
pub struct ColumnLayout {
pub name: String,
pub kind: String,
pub pages: u64,
pub memberships: u64,
pub sieves: u64,
pub part_ranges: u64,
pub dictionary: u64,
}
impl ColumnLayout {
#[must_use]
pub fn total(&self) -> u64 {
self.pages
.saturating_add(self.memberships)
.saturating_add(self.sieves)
.saturating_add(self.part_ranges)
.saturating_add(self.dictionary)
}
}
#[derive(Debug, Clone)]
pub struct Layout {
pub file: u64,
pub rows: usize,
pub stripes: usize,
pub parts: usize,
pub columns: Vec<ColumnLayout>,
pub indexes: u64,
pub directory: u64,
pub header: u64,
}
impl Layout {
#[must_use]
pub fn columns_total(&self) -> u64 {
self.columns.iter().map(ColumnLayout::total).fold(0, u64::saturating_add)
}
#[must_use]
pub fn unaccounted(&self) -> u64 {
self.file
.saturating_sub(self.columns_total())
.saturating_sub(self.indexes)
.saturating_sub(self.directory)
.saturating_sub(self.header)
}
}
#[derive(Debug, Clone)]
pub struct StoredPart {
pub stripe: usize,
pub part: usize,
pub row: usize,
pub rows: usize,
pub encoding: String,
pub bytes: u64,
pub page: u64,
pub offset: u64,
pub low: Option<Value>,
pub high: Option<Value>,
pub nulls: Option<usize>,
}
const DICTIONARY_CHECK_SEED: u64 = 11_400_714_819_323_198_485;
#[derive(Debug)]
struct GlobalDictionary {
primary: HashMap<u64, u32, Spread>,
collisions: HashMap<u64, Vec<u32>, Spread>,
checks: Vec<u64>,
ends: Vec<u32>,
counts: Vec<u64>,
nulls: u64,
filling: Vec<u8>,
grams: Vec<[u8; TEXT_GRAM_BYTES]>,
waiting: Vec<(usize, Vec<u8>)>,
sample: Vec<(usize, Vec<u8>)>,
stride: usize,
shape: Option<chooser::Settled>,
settled: usize,
blocks: Vec<Vec<u8>>,
early: BTreeMap<usize, EncodedBlock>,
placed: Vec<Placed>,
charged: u64,
demoted: bool,
}
#[derive(Debug, Clone, Copy)]
struct Placed {
start: u64,
length: u64,
hash: u64,
}
type RankedDictionary = (Vec<(u64, u32)>, Vec<u8>, Vec<u64>);
impl GlobalDictionary {
fn new() -> Self {
Self {
primary: HashMap::default(),
collisions: HashMap::default(),
checks: Vec::new(),
ends: Vec::new(),
counts: Vec::new(),
nulls: 0,
filling: Vec::new(),
grams: Vec::new(),
waiting: Vec::new(),
sample: Vec::new(),
stride: 1,
shape: None,
settled: 0,
blocks: Vec::new(),
early: BTreeMap::new(),
placed: Vec::new(),
charged: 0,
demoted: false,
}
}
fn values(&self) -> usize {
self.ends.len()
}
fn closing_bytes(&self) -> usize {
let values = self.values();
let decoded = (0..values.div_ceil(TEXT_PAYLOAD_VALUES))
.map(|block| self.ends[((block + 1) * TEXT_PAYLOAD_VALUES).min(values) - 1] as usize)
.sum::<usize>();
decoded.saturating_add(values.saturating_mul(size_of::<(u64, u32)>() + size_of::<u32>()))
}
fn held_bytes(&self) -> u64 {
fn table<K, V, S>(map: &HashMap<K, V, S>) -> usize {
(map.capacity() * 8 / 7).next_power_of_two() * (size_of::<(K, V)>() + 1)
}
fn spilled<T>(values: &Vec<T>) -> usize {
values.capacity() * size_of::<T>()
}
let raw = |blocks: &Vec<(usize, Vec<u8>)>| {
spilled(blocks) + blocks.iter().map(|(_, block)| block.capacity()).sum::<usize>()
};
let bytes = table(&self.primary)
+ table(&self.collisions)
+ self.collisions.values().map(spilled).sum::<usize>()
+ spilled(&self.checks)
+ spilled(&self.ends)
+ spilled(&self.counts)
+ self.filling.capacity()
+ spilled(&self.grams)
+ raw(&self.waiting)
+ raw(&self.sample)
+ self.blocks.iter().map(Vec::capacity).sum::<usize>()
+ spilled(&self.placed);
bytes as u64
}
fn recharge(&mut self, profile: Option<&LoadProfile>) -> (u64, u64) {
let before = self.charged;
let now = self.held_bytes();
if let Some(profile) = profile {
if now >= before {
profile.hold(now - before);
} else {
profile.release(before - now);
}
}
self.charged = now;
(before, now)
}
fn demote(&mut self) {
if self.demoted {
return;
}
self.seal_rest();
self.release_lookup();
self.demoted = true;
}
fn release_lookup(&mut self) {
self.primary = HashMap::default();
self.collisions = HashMap::default();
self.checks = Vec::new();
self.sample = Vec::new();
self.filling = Vec::new();
}
fn encoded(&self) -> usize {
self.placed.len() + self.blocks.len()
}
#[cfg(test)]
fn code(&mut self, text: &str) -> Result<u32> {
let bytes = text.as_bytes();
self.code_hashed(bytes, checksum(bytes), seeded_checksum(bytes, DICTIONARY_CHECK_SEED))
}
fn code_hashed(&mut self, text: &[u8], hash: u64, check: u64) -> Result<u32> {
if let Some(&code) = self.primary.get(&hash) {
if self.checks.get(code as usize) == Some(&check) {
return Ok(code);
}
if let Some(codes) = self.collisions.get(&hash) {
if let Some(code) =
codes.iter().copied().find(|&code| self.checks[code as usize] == check)
{
return Ok(code);
}
}
let code = self.insert(text, check)?;
self.collisions.entry(hash).or_default().push(code);
return Ok(code);
}
let code = self.insert(text, check)?;
self.primary.insert(hash, code);
Ok(code)
}
fn insert(&mut self, text: &[u8], check: u64) -> Result<u32> {
if self.demoted {
return Err(Error::internal("a value was coded against a demoted dictionary"));
}
let code = u32::try_from(self.ends.len())
.map_err(|_| invalid("global dictionary has too many values"))?;
self.filling.extend_from_slice(text);
self.ends.push(
u32::try_from(self.filling.len())
.map_err(|_| invalid("a global dictionary value exceeds 4 GiB"))?,
);
self.checks.push(check);
self.counts.push(0);
if self.ends.len() % TEXT_PAYLOAD_VALUES == 0 {
self.seal();
}
Ok(code)
}
fn seal(&mut self) {
let at = self.ends.len().div_ceil(TEXT_PAYLOAD_VALUES) - 1;
let bytes = std::mem::take(&mut self.filling);
if at % self.stride == 0 {
self.sample.push((at, bytes.clone()));
if self.sample.len() > PAYLOAD_SAMPLE_BLOCKS {
self.stride *= 2;
let stride = self.stride;
self.sample.retain(|(at, _)| at % stride == 0);
}
}
self.waiting.push((at, bytes));
}
fn slices<'a>(&self, at: usize, bytes: &'a [u8]) -> Vec<&'a [u8]> {
block_values(self.block_ends(at), bytes)
}
fn block_ends(&self, at: usize) -> &[u32] {
let first = (at * TEXT_PAYLOAD_VALUES).min(self.ends.len());
let last = (first + TEXT_PAYLOAD_VALUES).min(self.ends.len());
&self.ends[first..last]
}
fn hand_out(&mut self, column: usize) -> Vec<Unencoded> {
let Some(shape) = &self.shape else { return Vec::new() };
let waiting = std::mem::take(&mut self.waiting);
waiting
.into_iter()
.map(|(at, bytes)| Unencoded {
column,
at,
ends: self.block_ends(at).to_vec(),
bytes,
shape: shape.clone(),
})
.collect()
}
fn take_back(&mut self, at: usize, block: EncodedBlock) -> Result<()> {
if at < self.encoded() || self.early.insert(at, block).is_some() {
return Err(Error::internal("a dictionary block came back twice"));
}
while let Some(block) = self.early.remove(&self.encoded()) {
self.push_block(block);
}
Ok(())
}
fn push_block(&mut self, (bytes, grams): EncodedBlock) {
self.blocks.push(bytes);
self.grams.push(*grams);
}
fn settle(&mut self) -> Result<()> {
if self.sample.len() < PAYLOAD_SAMPLE_BLOCKS {
return Ok(());
}
self.settle_on_sample()
}
fn settle_rest(&mut self) -> Result<()> {
if self.shape.is_some() || self.sample.is_empty() {
return Ok(());
}
self.settle_on_sample()
}
fn settle_on_sample(&mut self) -> Result<()> {
let complete = self.ends.len() / TEXT_PAYLOAD_VALUES;
if self.shape.is_some() && complete < self.settled.saturating_mul(4) {
return Ok(());
}
let sample =
self.sample.iter().map(|(at, bytes)| self.slices(*at, bytes)).collect::<Vec<_>>();
self.shape = Some(string::with_symbols(settle_shape(&sample)?, &sample));
self.settled = complete;
Ok(())
}
fn seal_rest(&mut self) {
if !self.demoted && self.ends.len() % TEXT_PAYLOAD_VALUES != 0 {
self.seal();
}
}
fn encode_waiting(&self, at: usize) -> Result<EncodedBlock> {
let (block, bytes) = &self.waiting[at];
let values = self.slices(*block, bytes);
let encoded = match &self.shape {
Some(shape) => string::encode_with(&values, shape)?,
None => string::encode(&values)?,
};
Ok((encoded, block_grams(&values)))
}
#[cfg(test)]
fn finish_blocks(&mut self) -> Result<()> {
self.seal_rest();
let made = (0..self.waiting.len())
.map(|at| self.encode_waiting(at))
.collect::<Result<Vec<_>>>()?;
for ((at, _), block) in std::mem::take(&mut self.waiting).into_iter().zip(made) {
if self.encoded() != at {
return Err(Error::internal("a dictionary block was encoded out of order"));
}
self.push_block(block);
}
Ok(())
}
fn decoded(&self, file: Option<&dyn rudb_io::File>) -> Result<(Vec<u8>, Vec<u64>)> {
let count = self.placed.len() + self.blocks.len();
if count != self.values().div_ceil(TEXT_PAYLOAD_VALUES) {
return Err(invalid("global dictionary blocks do not cover its values"));
}
let mut bases = Vec::with_capacity(count);
let mut total = 0_usize;
for block in 0..count {
bases.push(total as u64);
let last = ((block + 1) * TEXT_PAYLOAD_VALUES).min(self.values()) - 1;
total = total
.checked_add(self.ends[last] as usize)
.ok_or_else(|| invalid("global dictionary does not fit in memory"))?;
}
let mut flat = vec![0_u8; total];
let mut outs = Vec::with_capacity(count);
let mut rest = flat.as_mut_slice();
for block in 0..count {
let end = bases.get(block + 1).map_or(total, |&base| base as usize);
let (out, after) = rest.split_at_mut(end - bases[block] as usize);
outs.push((block, out));
rest = after;
}
let one = |run: &mut [(usize, &mut [u8])]| -> Result<()> {
let mut stored = Vec::new();
for (block, out) in run {
let encoded = match self.placed.get(*block) {
Some(place) => {
let file = file.ok_or_else(|| {
Error::internal("a written dictionary block has no file")
})?;
let length = usize::try_from(place.length).map_err(|_| {
invalid("global dictionary block does not fit in memory")
})?;
stored.resize(length, 0);
read_at(file, place.start, &mut stored)?;
if checksum(&stored) != place.hash {
return Err(invalid(
"a global dictionary block did not read back as written",
));
}
stored.as_slice()
}
None => &self.blocks[*block - self.placed.len()],
};
let decoded = string::decode_flat(encoded)?;
if decoded.bytes().len() != out.len() {
return Err(invalid(
"a global dictionary block is not the length its ends say",
));
}
out.copy_from_slice(decoded.bytes());
}
Ok(())
};
let workers = close_workers().min(count / 16).max(1);
if workers <= 1 {
one(&mut outs)?;
} else {
let per = count.div_ceil(workers);
std::thread::scope(|scope| {
outs.chunks_mut(per)
.map(|run| scope.spawn(|| one(run)))
.collect::<Vec<_>>()
.into_iter()
.try_for_each(|handle| {
handle.join().map_err(|_| {
Error::internal("a global dictionary decode worker panicked")
})?
})
})?;
}
drop(outs);
Ok((flat, bases))
}
fn value_span(ends: &[u32], bases: &[u64], code: usize) -> (usize, usize) {
let Some(&base) = bases.get(code / TEXT_PAYLOAD_VALUES) else { return (0, 0) };
let Some(&end) = ends.get(code) else { return (0, 0) };
let base = base as usize;
let from = if code % TEXT_PAYLOAD_VALUES == 0 { 0 } else { ends[code - 1] as usize };
(base + from, base + end as usize)
}
fn ranked_with_values(&self, file: Option<&dyn rudb_io::File>) -> Result<RankedDictionary> {
let (flat, bases) = self.decoded(file)?;
let value = |code: u32| {
let (from, to) = Self::value_span(&self.ends, &bases, code as usize);
flat.get(from..to).unwrap_or_default()
};
let mut codes = (0..self.values() as u32).collect::<Vec<_>>();
sort_by_value_across(&mut codes, value, close_workers());
let order = codes.into_iter().map(|code| (head(value(code)), code)).collect();
Ok((order, flat, bases))
}
#[cfg(test)]
fn ranked(&self, file: Option<&dyn rudb_io::File>) -> Result<Vec<(u64, u32)>> {
self.ranked_with_values(file).map(|(order, _, _)| order)
}
}
#[derive(Debug)]
pub struct Writer {
file: Box<dyn rudb_io::File>,
at: u64,
written_back: u64,
table: Table,
generation: u64,
order: Vec<((u64, u64), (u64, u64))>,
next_order: u64,
dictionaries: Vec<Option<GlobalDictionary>>,
coded: Arc<prepare::Coding>,
gathers: Vec<Option<stats::Gather>>,
lent: Option<Arc<Lent>>,
pending: Vec<PendingChunk>,
closed: Vec<Entry>,
views: Vec<ViewEntry>,
profile: Option<Arc<LoadProfile>>,
}
#[derive(Debug)]
struct PendingChunk {
order: (u64, u64),
chunk: Chunk,
}
#[derive(Debug, Clone, Copy)]
struct Part {
order: (u64, u64),
rows: usize,
footprint: usize,
}
impl Part {
fn of(pending: &PendingChunk) -> Self {
Self {
order: pending.order,
rows: pending.chunk.len(),
footprint: pending.chunk.footprint(),
}
}
}
#[derive(Debug, Default)]
struct ColumnStripe {
pages: Vec<Vec<u8>>,
codes: Vec<Option<Vec<u32>>>,
sieves: Vec<Option<Sieve>>,
ranges: Vec<Range>,
}
fn coded_type(ty: &LogicalType) -> bool {
matches!(ty, LogicalType::Varchar | LogicalType::Blob)
}
fn dictionary_tag(ty: &LogicalType) -> u8 {
if ty == &LogicalType::Blob { 2 } else { 1 }
}
fn weight(ty: &LogicalType) -> usize {
match ty {
LogicalType::Varchar | LogicalType::Blob | LogicalType::Bit => 64,
LogicalType::HugeInt
| LogicalType::UHugeInt
| LogicalType::Uuid
| LogicalType::Interval => 16,
LogicalType::BigInt
| LogicalType::UBigInt
| LogicalType::Timestamp
| LogicalType::Time
| LogicalType::TimeTz
| LogicalType::TimestampTz
| LogicalType::TimestampS
| LogicalType::TimestampMs
| LogicalType::TimestampNs
| LogicalType::Double
| LogicalType::Decimal { .. } => 8,
LogicalType::Integer | LogicalType::UInteger | LogicalType::Date | LogicalType::Float => 4,
LogicalType::SmallInt | LogicalType::USmallInt => 2,
_ => 1,
}
}
pub const STRIPE_PARTS: usize = 64;
const DICTIONARY_DECIDE_ROWS: usize = 4_096;
const DICTIONARY_DISTINCT_IN_TEN: usize = 9;
const INDEX_ENTRY: usize = size_of::<u32>() + size_of::<u64>();
fn index_section(parts: usize) -> Result<usize> {
parts
.checked_mul(INDEX_ENTRY)
.and_then(|bytes| bytes.checked_add(size_of::<u64>()))
.ok_or_else(|| invalid("index page length overflow"))
}
impl Writer {
pub fn open(
path: impl AsRef<Path>,
name: impl Into<String>,
fields: Vec<Field>,
) -> Result<Self> {
Self::open_in(&RealFilesystem::new(), path, name, fields)
}
pub fn open_in(
fs: &dyn Filesystem,
path: impl AsRef<Path>,
name: impl Into<String>,
fields: Vec<Field>,
) -> Result<Self> {
for field in &fields {
type_tag(&field.ty)?;
}
let name = name.into();
let file = fs.open(path.as_ref(), OpenMode::ReadWrite)?;
let size = file.len()?;
let (slot, bytes, _) = committed_slot(&*file, size)?;
let (mut closed, views) = decode_catalog(&bytes, size)?;
if let Some(at) = closed.iter().position(|held| held.name == name) {
if closed[at].rows > 0 {
return Err(invalid("two tables in one native file have the same name"));
}
closed.remove(at);
}
let generation = slot
.generation
.checked_add(1)
.ok_or_else(|| invalid("native file generation overflow"))?;
Ok(Self {
file,
at: size,
written_back: size,
dictionaries: fields
.iter()
.map(|field| coded_type(&field.ty).then(GlobalDictionary::new))
.collect(),
coded: Arc::new(prepare::Coding::new(fields.iter().map(|field| coded_type(&field.ty)))),
gathers: fields.iter().map(|field| stats::Gather::new(&field.ty, generation)).collect(),
lent: None,
table: Table {
name,
dictionaries: vec![None; fields.len()],
dictionary_payloads: Vec::new(),
demoted: Vec::new(),
distincts: vec![None; fields.len()],
fields,
stripes: Vec::new(),
rows: 0,
frequencies: Vec::new(),
pair_frequencies: Vec::new(),
frequency_texts: Vec::new(),
host_groups: None,
clustering: None,
generation,
sections: Vec::new(),
},
generation,
order: Vec::new(),
next_order: 0,
pending: Vec::with_capacity(STRIPE_PARTS),
closed,
views,
profile: None,
})
}
pub fn create(
path: impl AsRef<Path>,
name: impl Into<String>,
fields: Vec<Field>,
) -> Result<Self> {
Self::create_in(&RealFilesystem::new(), path, name, fields)
}
pub fn create_in(
fs: &dyn Filesystem,
path: impl AsRef<Path>,
name: impl Into<String>,
fields: Vec<Field>,
) -> Result<Self> {
for field in &fields {
type_tag(&field.ty)?;
}
let file = fs.open(path.as_ref(), OpenMode::CreateNew)?;
let mut header = [0; HEADER as usize];
header[..8].copy_from_slice(MAGIC);
header[8..12].copy_from_slice(&FORMAT.to_le_bytes());
file.write_at(0, &header)?;
Ok(Self {
file,
at: HEADER,
written_back: HEADER,
dictionaries: fields
.iter()
.map(|field| coded_type(&field.ty).then(GlobalDictionary::new))
.collect(),
coded: Arc::new(prepare::Coding::new(fields.iter().map(|field| coded_type(&field.ty)))),
gathers: fields.iter().map(|field| stats::Gather::new(&field.ty, 1)).collect(),
lent: None,
table: Table {
name: name.into(),
dictionaries: vec![None; fields.len()],
dictionary_payloads: Vec::new(),
demoted: Vec::new(),
distincts: vec![None; fields.len()],
fields,
stripes: Vec::new(),
rows: 0,
frequencies: Vec::new(),
pair_frequencies: Vec::new(),
frequency_texts: Vec::new(),
host_groups: None,
clustering: None,
generation: 1,
sections: Vec::new(),
},
generation: 1,
order: Vec::new(),
next_order: 0,
pending: Vec::with_capacity(STRIPE_PARTS),
closed: Vec::new(),
views: Vec::new(),
profile: None,
})
}
pub fn empty(path: impl AsRef<Path>, views: &[ViewEntry]) -> Result<()> {
let file = RealFilesystem::new().open(path.as_ref(), OpenMode::CreateNew)?;
let mut header = [0; HEADER as usize];
header[..8].copy_from_slice(MAGIC);
header[8..12].copy_from_slice(&FORMAT.to_le_bytes());
file.write_at(0, &header)?;
let catalog = encode_catalog(&[], views)?;
file.write_at(HEADER, &catalog)?;
file.sync()?;
let slot = Slot {
offset: HEADER,
length: u32::try_from(catalog.len()).map_err(|_| invalid("catalog length overflow"))?,
generation: 1,
hash: checksum(&catalog),
};
file.write_at(slot_offset(1), &slot.bytes())?;
file.sync()?;
Ok(())
}
pub fn next(mut self, name: impl Into<String>, fields: Vec<Field>) -> Result<Self> {
for field in &fields {
type_tag(&field.ty)?;
}
let name = name.into();
let entry = self.close()?;
if self.closed.iter().chain(std::iter::once(&entry)).any(|held| held.name == name) {
return Err(invalid("two tables in one native file have the same name"));
}
let Self { file, at, generation, mut closed, views, .. } = self;
closed.push(entry);
Ok(Self {
file,
written_back: at,
at,
generation,
closed,
views,
profile: None,
dictionaries: fields
.iter()
.map(|field| coded_type(&field.ty).then(GlobalDictionary::new))
.collect(),
coded: Arc::new(prepare::Coding::new(fields.iter().map(|field| coded_type(&field.ty)))),
gathers: fields.iter().map(|field| stats::Gather::new(&field.ty, generation)).collect(),
lent: None,
table: Table {
name,
dictionaries: vec![None; fields.len()],
dictionary_payloads: Vec::new(),
demoted: Vec::new(),
distincts: vec![None; fields.len()],
fields,
stripes: Vec::new(),
rows: 0,
frequencies: Vec::new(),
pair_frequencies: Vec::new(),
frequency_texts: Vec::new(),
host_groups: None,
clustering: None,
generation,
sections: Vec::new(),
},
order: Vec::new(),
next_order: 0,
pending: Vec::with_capacity(STRIPE_PARTS),
})
}
#[must_use]
pub fn with_views(mut self, views: Vec<ViewEntry>) -> Self {
self.views = views;
self
}
#[must_use]
pub fn with_profile(mut self, profile: Arc<LoadProfile>) -> Self {
self.profile = Some(profile);
self
}
#[must_use]
pub fn with_dictionary_cap(self, bytes: u64) -> Self {
self.coded.cap(bytes);
self
}
pub fn declare(mut self, clustering: Clustering) -> Result<Self> {
self.table.clustering = Some(Clustering::new(
clustering.columns().to_vec(),
clustering.width(),
&self.table.fields,
)?);
Ok(self)
}
fn put(&mut self, bytes: &[u8]) -> Result<()> {
self.file.write_at(self.at, bytes)?;
self.at = self
.at
.checked_add(bytes.len() as u64)
.ok_or_else(|| invalid("native file length overflow"))?;
if self.at - self.written_back >= WRITEBACK_STRETCH {
self.file.start_writeback(self.written_back, self.at - self.written_back);
self.written_back = self.at;
}
Ok(())
}
pub fn append(&mut self, chunk: &Chunk) -> Result<()> {
let order = (self.next_order, 0);
self.next_order = self.next_order.saturating_add(1);
self.append_at(order, chunk)
}
pub fn append_at(&mut self, order: (u64, u64), chunk: &Chunk) -> Result<()> {
if chunk.is_empty() {
return Ok(());
}
self.admit(chunk)?;
if self.pending.last().is_some_and(|last| last.order > order) {
self.flush_pending()?;
}
self.pending.push(PendingChunk { order, chunk: chunk.clone() });
if self.pending.len() == STRIPE_PARTS {
self.flush_pending()?;
}
Ok(())
}
pub fn append_stripe(&mut self, parts: Vec<((u64, u64), Chunk)>) -> Result<()> {
if parts.len() > STRIPE_PARTS {
return Err(invalid("a stripe was handed more parts than it holds"));
}
self.flush_pending()?;
for (order, chunk) in parts {
if chunk.is_empty() {
continue;
}
self.admit(&chunk)?;
self.pending.push(PendingChunk { order, chunk });
}
self.flush_pending()
}
fn admit(&mut self, chunk: &Chunk) -> Result<()> {
if chunk.width() != self.table.fields.len() {
return Err(invalid("chunk width differs from table schema"));
}
for (index, field) in self.table.fields.iter().enumerate() {
if chunk.column(index)?.logical_type() != &field.ty {
return Err(invalid("chunk type differs from table schema"));
}
}
self.table.rows = self
.table
.rows
.checked_add(chunk.len())
.ok_or_else(|| invalid("row count overflow"))?;
Ok(())
}
fn encode_pages(columns: &[&Vector]) -> Result<ColumnStripe> {
let mut stripe = ColumnStripe {
pages: Vec::with_capacity(columns.len()),
codes: Vec::with_capacity(columns.len()),
sieves: Vec::with_capacity(columns.len()),
ranges: Vec::with_capacity(columns.len()),
};
let mut settling = Settling::default();
for &column in columns {
Self::encode_page(&mut stripe, &mut settling, column)?;
}
Ok(stripe)
}
fn encode_page(
stripe: &mut ColumnStripe,
settling: &mut Settling,
column: &Vector,
) -> Result<()> {
let bytes = encode(column, settling)?;
if bytes.len() > MAX_PAGE {
return Err(invalid("column page exceeds the configured bound"));
}
let range = Range::of(column);
let sieve =
Sieve::of(column, &range, SIEVE_BUDGET).filter(|sieve| sieve.len() < bytes.len());
stripe.pages.push(bytes);
stripe.codes.push(None);
stripe.sieves.push(sieve);
stripe.ranges.push(range);
Ok(())
}
fn place_blocks(&mut self) -> Result<()> {
if let Some(lent) = self.lent.clone() {
return self.place_lent_blocks(&lent);
}
let mut dictionaries = std::mem::take(&mut self.dictionaries);
let placed = dictionaries.iter_mut().flatten().try_for_each(|dictionary| {
for block in std::mem::take(&mut dictionary.blocks) {
let start = self.at;
self.put(&block)?;
dictionary.placed.push(Placed {
start,
length: block.len() as u64,
hash: checksum(&block),
});
}
Ok(())
});
self.dictionaries = dictionaries;
placed
}
fn place_lent_blocks(&mut self, lent: &Lent) -> Result<()> {
for column in lent.columns() {
let Ok(mut held) = column.try_lock() else { continue };
let Some(dictionary) = held.dictionary.as_mut() else { continue };
for block in std::mem::take(&mut dictionary.blocks) {
let start = self.at;
self.put(&block)?;
dictionary.placed.push(Placed {
start,
length: block.len() as u64,
hash: checksum(&block),
});
}
}
Ok(())
}
fn reclaim(&mut self) -> Result<()> {
let Some(lent) = self.lent.take() else { return Ok(()) };
let (dictionaries, gathers) = lent.reclaim()?;
self.dictionaries = dictionaries;
self.gathers = gathers;
Ok(())
}
fn flush_pending(&mut self) -> Result<()> {
if self.pending.is_empty() {
return Ok(());
}
let held = std::mem::take(&mut self.pending);
let prepared = self.preparer().prepare_held(held)?;
let merged = self.merge_held(prepared)?;
let paged = merged.pages()?;
self.write_paged(paged)
}
fn write_stripe(&mut self, held: &[Part], encoded: Vec<ColumnStripe>) -> Result<()> {
let width = self.table.fields.len();
let parts = held.len();
if encoded.len() != width {
return Err(Error::internal("a stripe came to the writer with the wrong columns"));
}
let profile = self.profile.clone();
if let Some(profile) = &profile {
let rows = held.iter().map(|part| part.rows as u64).sum();
let raw = held.iter().map(|part| part.footprint as u64).sum();
let pages =
encoded.iter().flat_map(|stripe| &stripe.pages).map(|page| page.len() as u64).sum();
profile.moved(Stage::Pages, raw, pages, rows);
}
let timing = profile.as_deref().map(|profile| profile.span(Stage::Dictionary));
let before = self.at;
self.place_blocks()?;
drop(timing);
if let Some(profile) = &profile {
profile.moved(Stage::Dictionary, 0, self.at - before, 0);
}
let timing = profile.as_deref().map(|profile| profile.span(Stage::Write));
let before = self.at;
let mut pages = Vec::with_capacity(width);
let mut memberships = vec![None; width];
let mut ranges = Vec::with_capacity(width);
let mut index = Vec::with_capacity(width.saturating_mul(index_section(parts)?));
for stripe in &encoded {
let offset = self.at;
let section = index.len();
let mut length = 0_usize;
for bytes in &stripe.pages {
self.file.write_at(self.at + length as u64, bytes)?;
put_u32(
&mut index,
u32::try_from(bytes.len()).map_err(|_| invalid("part length overflow"))?,
);
put_u64(&mut index, checksum(bytes));
length = length
.checked_add(bytes.len())
.ok_or_else(|| invalid("column page length overflow"))?;
}
let hash = checksum(&index[section..]);
put_u64(&mut index, hash);
if length > MAX_PAGE {
return Err(invalid("column page exceeds the configured bound"));
}
self.at = self
.at
.checked_add(length as u64)
.ok_or_else(|| invalid("native file length overflow"))?;
pages.push(Span {
offset,
length: u32::try_from(length).map_err(|_| invalid("page length overflow"))?,
});
ranges.push(merged_range(stripe.ranges.iter().cloned()));
}
for (membership, stripe) in memberships.iter_mut().zip(&encoded) {
if stripe.codes.iter().all(Option::is_none) {
continue;
}
let lists = stripe
.codes
.iter()
.map(|codes| codes.clone().unwrap_or_default())
.collect::<Vec<_>>();
let bytes = encode_membership(&merged_codes(lists));
let offset = self.at;
self.put(&bytes)?;
*membership = Some(Page {
offset,
length: u32::try_from(bytes.len())
.map_err(|_| invalid("membership page length overflow"))?,
hash: checksum(&bytes),
});
}
let mut sieves = vec![None; width];
for (page, stripe) in sieves.iter_mut().zip(&encoded) {
if stripe.sieves.iter().all(Option::is_none) {
continue;
}
let bytes = encode_sieves(stripe.sieves.iter())?;
let offset = self.at;
self.put(&bytes)?;
*page = Some(Page {
offset,
length: u32::try_from(bytes.len())
.map_err(|_| invalid("sieve page length overflow"))?,
hash: checksum(&bytes),
});
}
let mut part_ranges = vec![None; width];
if parts > 1 {
for ((page, stripe), span) in part_ranges.iter_mut().zip(&encoded).zip(&pages) {
let bytes = encode_part_ranges(&stripe.ranges)?;
if bytes.len() >= span.length as usize {
continue;
}
let offset = self.at;
self.put(&bytes)?;
*page = Some(Page {
offset,
length: u32::try_from(bytes.len())
.map_err(|_| invalid("part range page length overflow"))?,
hash: checksum(&bytes),
});
}
}
let offset = self.at;
self.put(&index)?;
let index = Span {
offset,
length: u32::try_from(index.len())
.map_err(|_| invalid("index page length overflow"))?,
};
let mut rows = 0_usize;
let mut lengths = Vec::with_capacity(parts);
let mut span = None;
for part in held {
rows = rows.checked_add(part.rows).ok_or_else(|| invalid("row count overflow"))?;
lengths.push(u32::try_from(part.rows).map_err(|_| invalid("part row count overflow"))?);
span = Some(span.map_or((part.order, part.order), |(first, _)| (first, part.order)));
}
self.order.push(span.ok_or_else(|| invalid("a stripe was flushed with no parts"))?);
self.table.stripes.push(Stripe {
rows,
parts: lengths,
index,
pages,
memberships: Pages::from_slots(memberships)?,
sieves: Pages::from_slots(sieves)?,
part_ranges: Pages::from_slots(part_ranges)?,
zone: Zone::from_ranges(ranges),
});
drop(timing);
if let Some(profile) = &profile {
profile.moved(Stage::Write, 0, self.at - before, rows as u64);
}
Ok(())
}
fn numeric_frequency(
&self,
column: usize,
counted: bool,
) -> Result<(Option<FrequencySummary>, Option<u64>)> {
let signed = match self.table.fields[column].ty {
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::Date
| LogicalType::Timestamp => true,
LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt => false,
_ => return Ok((None, None)),
};
let value_of = |bits: Option<u64>| match bits {
None => FrequencyValue::Null,
Some(bits) => integer_value(bits, signed),
};
let tallied = self
.gathers
.get(column)
.and_then(Option::as_ref)
.filter(|gather| gather.rows() == self.table.rows as u64)
.and_then(stats::Gather::frequencies)
.and_then(|(values, nulls)| {
let entries = values
.iter()
.map(|(value, count)| {
let value = value_of(Some(frequency_bits(value)?));
Some(FrequencyEntry { value, count: *count })
})
.chain((nulls != 0).then_some(Some(FrequencyEntry {
value: FrequencyValue::Null,
count: nulls,
})))
.collect::<Option<Vec<_>>>()?;
Some((entries, values.len() as u64))
});
let exact = match (&tallied, counted) {
(None, true) => self.exact_frequency(column, signed)?,
_ => None,
};
let (mut entries, decrements, distinct_count) = match (tallied, exact) {
(Some((entries, distinct)), _) => (entries, 0, Some(distinct)),
(None, Some((Some(entries), distinct))) => (entries, 0, Some(distinct)),
(None, Some((None, distinct))) => return Ok((None, Some(distinct))),
(None, None) => {
let mut first = Candidates::default();
let mut run = Run::default();
self.visit_numeric(column, signed, |_, bits| {
if let Some((ended, times)) = run.push(bits) {
first.add(ended, times);
}
})?;
if let Some((bits, times)) = run.take() {
first.add(bits, times);
}
let (nulls, decrements) = (first.nulls, first.decrements);
let distinct_count = (decrements == 0).then_some(first.held as u64);
let (exact, null_count) = if decrements == 0 {
let exact = first
.pairs()
.map(|(bits, count)| (bits, u64::from(count)))
.collect::<FrequencyMap<_>>();
(exact, (nulls != 0).then_some(u64::from(nulls)))
} else {
let mut lower = first.pairs().map(|(_, count)| count).collect::<Vec<_>>();
if nulls != 0 {
lower.push(nulls);
}
lower.sort_unstable_by(|left, right| right.cmp(left));
if lower.len() < FREQUENCY_BUILD_RANK
|| u64::from(lower[FREQUENCY_BUILD_RANK - 1]) <= decrements
{
return Ok((None, distinct_count));
}
let mut recounts = vec![0_u64; first.slots.len()];
let mut null_count = (nulls != 0).then_some(0_u64);
let mut recount = |bits: Option<u64>, times: u32| {
let held = match bits {
Some(bits) => first.position(bits).map(|at| &mut recounts[at]),
None => null_count.as_mut(),
};
if let Some(count) = held {
*count = count.saturating_add(u64::from(times));
}
};
let mut run = Run::default();
self.visit_numeric(column, signed, |_, bits| {
if let Some((bits, times)) = run.push(bits) {
recount(bits, times);
}
})?;
if let Some((bits, times)) = run.take() {
recount(bits, times);
}
let exact = first
.slots
.iter()
.zip(&recounts)
.filter(|(slot, _)| slot.count != 0)
.map(|(slot, &count)| (slot.bits, count))
.collect::<FrequencyMap<_>>();
(exact, null_count)
};
let entries = exact
.into_iter()
.map(|(bits, count)| FrequencyEntry { value: value_of(Some(bits)), count })
.chain(
null_count
.map(|count| FrequencyEntry { value: FrequencyValue::Null, count }),
)
.collect::<Vec<_>>();
(entries, decrements, distinct_count)
}
};
let mut omitted_max = keep_most_frequent(&mut entries).max(decrements);
if omitted_max == 0 && entries.len() > 1 {
let retained = entries.len().saturating_sub(1).min(2);
omitted_max = entries[retained].count;
entries.truncate(retained);
}
let kept_rows = entries.iter().try_fold(0_u64, |total, entry| {
total.checked_add(entry.count).filter(|&total| total <= FREQUENCY_ORDINALS as u64)
});
let mut ordinals = Vec::new();
let mut ordinal_entries = Vec::new();
if let Some(kept_rows) = kept_rows {
let mut kept = FrequencyMap::default();
let mut null_kept = None;
for (at, entry) in entries.iter().enumerate() {
let at = u16::try_from(at)
.map_err(|_| invalid("too many retained frequency entries"))?;
match entry.value {
FrequencyValue::Integer(value) => {
kept.insert(value as u64, at);
}
FrequencyValue::Null => null_kept = Some(at),
FrequencyValue::Code(_) => {}
}
}
ordinals.reserve(usize::try_from(kept_rows).unwrap_or(FREQUENCY_ORDINALS));
ordinal_entries.reserve(usize::try_from(kept_rows).unwrap_or(FREQUENCY_ORDINALS));
self.visit_numeric(column, signed, |ordinal, bits| {
let held = match bits {
Some(bits) => kept.get(&bits).copied(),
None => null_kept,
};
if let Some(entry) = held {
ordinals.push(ordinal);
ordinal_entries.push(entry);
}
})?;
}
Ok((
Some(FrequencySummary { entries, omitted_max, ordinals, ordinal_entries }),
distinct_count,
))
}
fn exact_frequency(
&self,
column: usize,
signed: bool,
) -> Result<Option<(Option<Vec<FrequencyEntry>>, u64)>> {
let mut set = distinct::ExactCounts::new();
let mut nulls = 0_u64;
let mut run = Run::default();
let mut add = |bits: Option<u64>, times: u32| match bits {
Some(bits) => set.insert(bits, times),
None => nulls += u64::from(times),
};
self.visit_numeric(column, signed, |_, bits| {
if let Some((bits, times)) = run.push(bits) {
add(bits, times);
}
})?;
if let Some((bits, times)) = run.take() {
add(bits, times);
}
let Some(distinct) = set.count() else {
return Ok(None);
};
let mut top = std::collections::BinaryHeap::with_capacity(FREQUENCY_ENTRIES + 2);
let mut rank = |count: u64| {
if top.len() <= FREQUENCY_ENTRIES {
top.push(Reverse(count));
} else if top.peek().is_some_and(|&Reverse(least)| count > least) {
top.pop();
top.push(Reverse(count));
}
};
set.visit(|_, count| rank(count));
if nulls != 0 {
rank(nulls);
}
let top = top.into_sorted_vec();
let values = distinct + u64::from(nulls != 0);
if values > FREQUENCY_CANDIDATES as u64 {
let bound = self.table.rows as u64 / (FREQUENCY_CANDIDATES as u64 + 1);
if top.get(FREQUENCY_BUILD_RANK - 1).is_none_or(|&Reverse(count)| count <= bound) {
return Ok(Some((None, distinct)));
}
}
let least = top.get(FREQUENCY_ENTRIES).map_or(0, |&Reverse(count)| count);
let mut entries = Vec::with_capacity(FREQUENCY_ENTRIES + 1);
set.visit(|bits, count| {
if count >= least {
entries.push(FrequencyEntry { value: integer_value(bits, signed), count });
}
});
if nulls != 0 && nulls >= least {
entries.push(FrequencyEntry { value: FrequencyValue::Null, count: nulls });
}
Ok(Some((Some(entries), distinct)))
}
fn visit_numeric(
&self,
column: usize,
signed: bool,
mut visit: impl FnMut(u64, Option<u64>),
) -> Result<()> {
let ty = &self.table.fields[column].ty;
let mut start = 0_u64;
let mut block = Vec::new();
for stripe in &self.table.stripes {
let spans = read_index(&self.file, stripe, column)?;
let page = stripe.pages[column];
let mut bytes = vec![0; page.length as usize];
read_at(&self.file, page.offset, &mut bytes)?;
for (span, &rows) in spans.iter().zip(&stripe.parts) {
let part = part_bytes(&bytes, *span)?;
if checksum(part) != span.hash {
return Err(invalid("column page checksum differs while building frequencies"));
}
let rows = rows as usize;
let vector = decode(ty, rows, part, None)?;
if signed && vector.signed_block(&mut block) && block.len() == rows {
if vector.none_null() {
for (row, &value) in block.iter().enumerate() {
visit(start.saturating_add(row as u64), Some(value as u64));
}
} else {
for (row, &value) in block.iter().enumerate() {
let bits = (!vector.is_null_at(row)).then_some(value as u64);
visit(start.saturating_add(row as u64), bits);
}
}
start = start.saturating_add(rows as u64);
continue;
}
for row in 0..rows {
let bits = if vector.is_null_at(row) {
None
} else {
let widened = match vector.signed_at(row) {
Some(value) => Some(value as u64),
None => match vector.value_at(row) {
Value::UTinyInt(value) => Some(u64::from(value)),
Value::USmallInt(value) => Some(u64::from(value)),
Value::UInteger(value) => Some(u64::from(value)),
Value::UBigInt(value) => Some(value),
_ => None,
},
};
Some(widened.ok_or_else(|| {
invalid("numeric frequency page did not contain an integer value")
})?)
};
visit(start.saturating_add(row as u64), bits);
}
start = start.saturating_add(rows as u64);
}
}
Ok(())
}
fn numeric_columns(&self) -> Vec<usize> {
self.table
.fields
.iter()
.enumerate()
.filter_map(|(column, field)| {
matches!(
field.ty,
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
| LogicalType::Date
| LogicalType::Timestamp
)
.then_some(column)
})
.collect()
}
#[allow(dead_code)]
fn stable_codes_at(&self, column: usize, ordinals: &[u64]) -> Result<Option<Vec<Option<u32>>>> {
if self.dictionaries.get(column).and_then(Option::as_ref).is_none() {
return Ok(None);
}
if ordinals.windows(2).any(|pair| pair[0] >= pair[1]) {
return Err(invalid("frequency ordinals are not sorted and unique"));
}
let mut out = Vec::with_capacity(ordinals.len());
let mut wanted = 0;
let mut stripe_start = 0_u64;
for stripe in &self.table.stripes {
let stripe_end = stripe_start.saturating_add(stripe.rows as u64);
if wanted == ordinals.len() || ordinals[wanted] >= stripe_end {
stripe_start = stripe_end;
continue;
}
let spans = read_index(&self.file, stripe, column)?;
let page = stripe.pages[column];
let mut bytes = vec![0; page.length as usize];
read_at(&self.file, page.offset, &mut bytes)?;
let mut part_start = stripe_start;
for (span, &rows) in spans.iter().zip(&stripe.parts) {
let part_end = part_start.saturating_add(u64::from(rows));
if wanted < ordinals.len() && ordinals[wanted] < part_end {
let part = part_bytes(&bytes, *span)?;
if checksum(part) != span.hash {
return Err(invalid(
"column page checksum differs while building pair frequencies",
));
}
let upto = ordinals.partition_point(|&ordinal| ordinal < part_end);
let positions = ordinals[wanted..upto]
.iter()
.map(|&ordinal| {
usize::try_from(ordinal.saturating_sub(part_start))
.map_err(|_| invalid("frequency row offset does not fit in memory"))
})
.collect::<Result<Vec<_>>>()?;
if !decode_selected_stable_codes(rows as usize, part, &positions, &mut out)? {
return Ok(None);
}
wanted = upto;
}
part_start = part_end;
}
stripe_start = stripe_end;
}
if wanted != ordinals.len() {
return Err(invalid("frequency ordinal is outside the table"));
}
Ok(Some(out))
}
#[allow(dead_code)]
fn pair_frequencies(
&self,
frequencies: &[Option<Frequencies>],
) -> Result<Vec<PairFrequencySummary>> {
let anchors = frequencies
.iter()
.enumerate()
.filter_map(|(column, summary)| {
match summary {
Some(Frequencies::Held(summary)) => Some(summary),
_ => None,
}
.filter(|summary| {
!summary.ordinals.is_empty()
&& summary.ordinal_entries.len() == summary.ordinals.len()
})
.cloned()
.map(|summary| (column, summary))
})
.collect::<Vec<_>>();
let strings = self
.dictionaries
.iter()
.enumerate()
.filter_map(|(column, dictionary)| dictionary.as_ref().map(|_| column))
.collect::<Vec<_>>();
let mut summaries = Vec::new();
for (first, anchors) in anchors {
for &second in &strings {
if summaries.len() == MAX_PAIR_FREQUENCIES {
return Ok(summaries);
}
let Some(codes) = self.stable_codes_at(second, &anchors.ordinals)? else {
continue;
};
if codes.len() != anchors.ordinal_entries.len() {
return Err(invalid("pair frequency columns have different lengths"));
}
let mut counts = HashMap::<(u16, Option<u32>), u64>::new();
for (&anchor, code) in anchors.ordinal_entries.iter().zip(codes) {
*counts.entry((anchor, code)).or_default() += 1;
}
let mut entries = counts
.into_iter()
.map(|((first_entry, second), count)| PairFrequencyEntry {
first_entry,
second,
count,
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|left, right| {
right
.count
.cmp(&left.count)
.then_with(|| left.first_entry.cmp(&right.first_entry))
.then_with(|| left.second.cmp(&right.second))
});
let pair_omitted = entries.get(FREQUENCY_ENTRIES).map_or(0, |entry| entry.count);
entries.truncate(FREQUENCY_ENTRIES);
summaries.push(PairFrequencySummary {
first: u16::try_from(first)
.map_err(|_| invalid("pair frequency column index overflows"))?,
second: u16::try_from(second)
.map_err(|_| invalid("pair frequency column index overflows"))?,
entries,
omitted_max: anchors.omitted_max.max(pair_omitted),
});
}
}
Ok(summaries)
}
fn close(&mut self) -> Result<Entry> {
self.reclaim()?;
self.flush_pending()?;
let profile = self.profile.clone();
let timing = profile.as_deref().map(|profile| profile.span(Stage::Publish));
let before = self.at;
let mut stripes = std::mem::take(&mut self.order)
.into_iter()
.zip(std::mem::take(&mut self.table.stripes))
.collect::<Vec<_>>();
stripes.sort_by_key(|(order, _)| order.0);
let mut previous: Option<(u64, u64)> = None;
for ((first, last), _) in &stripes {
if previous.is_some_and(|previous| previous >= *first) {
return Err(invalid("chunks did not arrive in source order"));
}
previous = Some(*last);
}
self.table.stripes = stripes.into_iter().map(|(_, stripe)| stripe).collect();
drop(timing);
let timing = profile.as_deref().map(|profile| profile.span(Stage::Dictionary));
let placing = self.at;
finish_dictionaries(&mut self.dictionaries)?;
self.place_blocks()?;
for dictionary in self.dictionaries.iter_mut().flatten() {
dictionary.release_lookup();
dictionary.recharge(profile.as_deref());
}
let (numeric, closed) = self.close_columns()?;
let (frequencies, distincts): (Vec<Option<FrequencySummary>>, Vec<_>) =
numeric.into_iter().unzip();
let frequencies =
frequencies.into_iter().map(|held| held.map(Frequencies::Held)).collect::<Vec<_>>();
let pairs = Vec::new();
self.table.frequencies = frequencies;
self.table.distincts = distincts;
self.table.pair_frequencies = pairs;
if let Some(profile) = &profile {
profile.release(self.dictionaries.iter().flatten().map(|held| held.charged).sum());
}
self.table.demoted = self
.dictionaries
.iter()
.map(|dictionary| dictionary.as_ref().is_some_and(|held| held.demoted))
.collect();
if !self.table.demoted.contains(&true) {
self.table.demoted = Vec::new();
}
self.dictionaries = Vec::new();
self.table.dictionary_payloads = vec![0; self.table.fields.len()];
self.table.frequency_texts = vec![Vec::new(); self.table.fields.len()];
self.table.host_groups = None;
for (index, closed) in closed.into_iter().enumerate() {
let Some(closed) = closed else { continue };
let ClosedDictionary { distinct, frequencies, texts, hosts, encoded, payload } = closed;
self.table.distincts[index] = distinct;
self.table.frequencies[index] = frequencies.map(Frequencies::Held);
self.table.frequency_texts[index] = texts;
if hosts.is_some() {
self.table.host_groups = hosts;
}
let offset = self.at;
self.put(&encoded.index)?;
self.put(&encoded.ranks)?;
self.put(&encoded.grams)?;
self.table.dictionary_payloads[index] = payload;
let length = encoded
.index
.len()
.checked_add(encoded.ranks.len())
.and_then(|len| len.checked_add(encoded.grams.len()))
.ok_or_else(|| invalid("dictionary page length overflow"))?;
self.table.dictionaries[index] = Some(Page {
offset,
length: u32::try_from(length)
.map_err(|_| invalid("dictionary page length overflow"))?,
hash: checksum(&encoded.index),
});
}
drop(timing);
let timing = profile.as_deref().map(|profile| profile.span(Stage::Publish));
let placed = self.at - placing;
self.write_stats()?;
let directory = encode_directory(&self.table)?;
if directory.len() > MAX_DIRECTORY {
return Err(invalid("directory exceeds the configured bound"));
}
let offset = self.at;
self.put(&directory)?;
drop(timing);
if let Some(profile) = &profile {
profile.moved(Stage::Dictionary, 0, placed, 0);
profile.moved(Stage::Publish, 0, self.at - before - placed, 0);
}
Ok(Entry {
name: self.table.name.clone(),
fields: self.table.fields.clone(),
rows: self.table.rows,
nonzero: vec![None; self.table.fields.len()],
aggregates: table_aggregate_sums(&self.table),
distincts: self.table.distincts.clone(),
extremes: table_integer_extremes(&self.table),
frequencies: table_complete_numeric_frequencies(&self.table),
directory: Page {
offset,
length: u32::try_from(directory.len())
.map_err(|_| invalid("directory length overflow"))?,
hash: checksum(&directory),
},
})
}
#[allow(clippy::type_complexity)]
fn close_columns(
&self,
) -> Result<(Vec<(Option<FrequencySummary>, Option<u64>)>, Vec<Option<ClosedDictionary>>)> {
let numeric = self.numeric_columns().into_iter().map(|column| {
let estimate =
self.gathers.get(column).and_then(Option::as_ref).and_then(stats::Gather::distinct);
let counted = !estimate.is_some_and(distinct::beyond);
let set =
if counted { distinct::bytes_for(estimate.unwrap_or(f64::INFINITY)) } else { 0 };
let cost = self.table.rows.saturating_mul(weight(&self.table.fields[column].ty));
(Closing::Numeric { column, counted }, NUMERIC_CLOSE_BYTES + set, cost)
});
let dictionaries =
self.dictionaries.iter().enumerate().filter_map(|(index, dictionary)| {
let dictionary = dictionary.as_ref()?;
let bytes = dictionary.closing_bytes();
Some((Closing::Dictionary { index, dictionary }, bytes, bytes))
});
let mut jobs = numeric.chain(dictionaries).collect::<Vec<_>>();
jobs.sort_by_key(|&(_, _, cost)| cost);
let columns = self.table.fields.len();
let mut frequencies = vec![(None, None); columns];
let mut closed = (0..columns).map(|_| None).collect::<Vec<_>>();
let profile = self.profile.as_deref();
let run = |job: Closing<'_>, bytes: usize| -> Result<Closed> {
let _holding = profile.map(|profile| profile.holding(bytes as u64));
match job {
Closing::Numeric { column, counted } => {
let _timing = profile.map(|profile| profile.span(Stage::Publish));
Ok(Closed::Numeric(column, self.numeric_frequency(column, counted)?))
}
Closing::Dictionary { index, dictionary } => {
let _timing = profile.map(|profile| profile.span(Stage::Dictionary));
Ok(Closed::Dictionary(index, self.close_dictionary(index, dictionary)?))
}
}
};
let workers = close_workers().min(jobs.len());
let pieces = if workers <= 1 {
jobs.into_iter().map(|(job, bytes, _)| run(job, bytes)).collect::<Result<Vec<_>>>()?
} else {
let state = Mutex::new((jobs, 0_usize));
let finished = Condvar::new();
std::thread::scope(|scope| {
(0..workers)
.map(|_| {
scope.spawn(|| {
let mut mine = Vec::new();
loop {
let mut held = state.lock().map_err(|_| {
Error::internal("a native close worker panicked")
})?;
let (job, bytes) = loop {
let (jobs, busy) = &mut *held;
if jobs.is_empty() {
return Ok(mine);
}
let fits = jobs.iter().rposition(|&(_, bytes, _)| {
*busy == 0 || busy.saturating_add(bytes) <= CLOSE_BYTES
});
if let Some(at) = fits {
let (job, bytes, _) = jobs.remove(at);
*busy += bytes;
break (job, bytes);
}
held = finished.wait(held).map_err(|_| {
Error::internal("a native close worker panicked")
})?;
};
drop(held);
let _room = Room { state: &state, finished: &finished, bytes };
mine.push(run(job, bytes)?);
}
})
})
.collect::<Vec<_>>()
.into_iter()
.map(|handle| {
handle
.join()
.map_err(|_| Error::internal("a native close worker panicked"))?
})
.collect::<Result<Vec<_>>>()
})?
.into_iter()
.flatten()
.collect()
};
for piece in pieces {
match piece {
Closed::Numeric(column, summary) => frequencies[column] = summary,
Closed::Dictionary(index, one) => closed[index] = Some(one),
}
}
Ok((frequencies, closed))
}
fn close_dictionary(
&self,
_index: usize,
dictionary: &GlobalDictionary,
) -> Result<ClosedDictionary> {
let (order, flat, bases) = dictionary.ranked_with_values(Some(&*self.file))?;
let (distinct, frequencies, texts) = if dictionary.demoted {
(None, None, Vec::new())
} else {
let distinct = dictionary.counts.iter().filter(|count| **count != 0).count() as u64;
let (frequencies, texts) = code_frequency(dictionary, &flat, &bases)?;
(Some(distinct), Some(frequencies), texts)
};
let hosts = None;
drop(flat);
drop(bases);
let encoded = encode_global_dictionary(dictionary, &order, &dictionary.placed, true)?;
let payload = dictionary
.placed
.iter()
.try_fold(0_u64, |sum, place| sum.checked_add(place.length))
.ok_or_else(|| invalid("global dictionary payload overflow"))?;
Ok(ClosedDictionary { distinct, frequencies, texts, hosts, encoded, payload })
}
fn write_stats(&mut self) -> Result<()> {
let gathers = std::mem::take(&mut self.gathers);
let rows = self.table.rows as u64;
let mut payloads = Vec::new();
for (column, gather) in gathers.into_iter().enumerate() {
let Some(gather) = gather else { continue };
if gather.rows() != rows {
continue;
}
let Some(stats) = gather.finish() else { continue };
let mut summary = Vec::new();
stats.summary.encode(&mut summary)?;
let mut sketches = Vec::new();
stats.sketches.encode(&mut sketches)?;
payloads.push((column, summary, sketches));
}
if payloads.is_empty() {
return Ok(());
}
let costs = payloads
.iter()
.map(|(_, summary, sketches)| summary.len() + sketches.len())
.collect::<Vec<_>>();
let allowance = stats::allowance(stats::column_bytes(&self.table), stats::BUDGET_SHARE);
let keep = stats::within(&costs, allowance, 0);
for ((column, summary, sketches), _) in
payloads.iter().zip(&keep).filter(|&(_, &keep)| keep)
{
let id = u64::try_from(*column).map_err(|_| invalid("column index overflow"))?;
for (kind, bytes, header_bytes) in [
(*section::SUMMARY, summary, summary.len() as u32),
(*section::SKETCHES, sketches, rudb_stats::sketches::HEADER_BYTES),
] {
let written = write_section(
&*self.file,
&mut self.at,
§ion::Attachment { kind, id, flags: 0, header_bytes, bytes },
self.generation,
)?;
self.table.sections.push(written);
}
}
if self.table.sections.len() > MAX_SECTIONS {
return Err(invalid("the table would name more sections than the bound allows"));
}
Ok(())
}
pub fn finish(mut self) -> Result<Table> {
let entry = self.close()?;
let profile = self.profile.take();
let _timing = profile.as_deref().map(|profile| profile.span(Stage::Publish));
let mut tables = std::mem::take(&mut self.closed);
tables.push(entry);
let catalog = encode_catalog(&tables, &self.views)?;
if catalog.len() > MAX_DIRECTORY {
return Err(invalid("catalog exceeds the configured bound"));
}
let offset = self.at;
self.put(&catalog)?;
if let Some(profile) = &profile {
profile.moved(Stage::Publish, 0, catalog.len() as u64, 0);
}
synced(&*self.file, profile.as_deref())?;
let slot = Slot {
offset,
length: u32::try_from(catalog.len()).map_err(|_| invalid("catalog length overflow"))?,
generation: self.generation,
hash: checksum(&catalog),
};
self.file.write_at(slot_offset(self.generation), &slot.bytes())?;
synced(&*self.file, profile.as_deref())?;
Ok(self.table)
}
pub fn restate(path: impl AsRef<Path>, views: &[ViewEntry]) -> Result<()> {
let file = RealFilesystem::new().open(path.as_ref(), OpenMode::ReadWrite)?;
let size = file.len()?;
let (slot, bytes, _) = committed_slot(&*file, size)?;
let (closed, _) = decode_catalog(&bytes, size)?;
let generation = slot
.generation
.checked_add(1)
.ok_or_else(|| invalid("native file generation overflow"))?;
let catalog = encode_catalog(&closed, views)?;
if catalog.len() > MAX_DIRECTORY {
return Err(invalid("catalog exceeds the configured bound"));
}
file.write_at(size, &catalog)?;
file.sync()?;
let slot = Slot {
offset: size,
length: u32::try_from(catalog.len()).map_err(|_| invalid("catalog length overflow"))?,
generation,
hash: checksum(&catalog),
};
file.write_at(slot_offset(generation), &slot.bytes())?;
file.sync()?;
Ok(())
}
pub fn certify_summaries(path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
let (_, size, slot, bytes, _) = slot_bytes(path)?;
let (mut entries, views) = decode_catalog(&bytes, size)?;
let native = Catalog::open(path)?;
for entry in &mut entries {
let reader = native.table(&entry.name)?;
entry.nonzero.fill(None);
entry.aggregates = reader_aggregate_sums(&reader)?;
entry.distincts = (0..entry.fields.len())
.map(|column| reader.distinct_values(column))
.collect::<Result<Vec<_>>>()?;
entry.extremes = reader_integer_extremes(&reader)?;
entry.frequencies = reader_complete_numeric_frequencies(&reader)?;
}
let generation = slot
.generation
.checked_add(1)
.ok_or_else(|| invalid("native file generation overflow"))?;
let catalog = encode_catalog(&entries, &views)?;
if catalog.len() > MAX_DIRECTORY {
return Err(invalid("catalog exceeds the configured bound"));
}
let file = RealFilesystem::new().open(path, OpenMode::ReadWrite)?;
file.write_at(size, &catalog)?;
file.sync()?;
let slot = Slot {
offset: size,
length: u32::try_from(catalog.len()).map_err(|_| invalid("catalog length overflow"))?,
generation,
hash: checksum(&catalog),
};
file.write_at(slot_offset(generation), &slot.bytes())?;
file.sync()?;
Ok(())
}
pub fn certify_counts(path: impl AsRef<Path>) -> Result<()> {
Self::certify_summaries(path)
}
}
fn append(file: &dyn rudb_io::File, at: &mut u64, bytes: &[u8]) -> Result<u64> {
let offset = *at;
file.write_at(offset, bytes)?;
*at =
at.checked_add(bytes.len() as u64).ok_or_else(|| invalid("native file length overflow"))?;
Ok(offset)
}
fn write_section(
file: &dyn rudb_io::File,
at: &mut u64,
one: §ion::Attachment<'_>,
generation: u64,
) -> Result<Section> {
if !one.bytes.is_empty() && one.header_bytes as usize > one.bytes.len() {
return Err(invalid("a section's header is longer than its payload"));
}
let mut extents = Vec::new();
let mut first = 0_u64;
let extent_size =
if one.kind == *section::SORTED_PROJECTION || one.kind == *section::RUN_PROJECTION {
1 << 19
} else {
section::MAX_EXTENT as usize
};
for chunk in one.bytes.chunks(extent_size) {
let offset = append(file, at, chunk)?;
extents.push(section::Extent {
offset,
length: u32::try_from(chunk.len()).map_err(|_| invalid("extent length overflow"))?,
hash: checksum(chunk),
first,
});
first += chunk.len() as u64;
}
let mut table = Vec::with_capacity(extents.len() * section::EXTENT_BYTES);
section::encode_extents(&extents, &mut table)?;
let extent_page = if table.is_empty() { 0 } else { append(file, at, &table)? };
Ok(Section {
kind: one.kind,
id: one.id,
generation,
extents: u32::try_from(extents.len()).map_err(|_| invalid("too many extents"))?,
extent_page,
extent_bytes: u32::try_from(table.len()).map_err(|_| invalid("extent table overflow"))?,
hash: checksum(&table),
flags: one.flags,
header_bytes: one.header_bytes,
})
}
pub fn attach(
path: impl AsRef<Path>,
table: &str,
attachments: &[section::Attachment<'_>],
) -> Result<Table> {
let file = RealFilesystem::new().open(path.as_ref(), OpenMode::ReadWrite)?;
let file = &*file;
let size = file.len()?;
let (slot, bytes, _) = committed_slot(file, size)?;
let (mut entries, views) = decode_catalog(&bytes, size)?;
let at = entries
.iter()
.position(|entry| entry.name == table)
.ok_or_else(|| invalid(&format!("the file holds no table called {table}")))?;
let mut version = [0; 4];
read_at(file, 8, &mut version)?;
let version = u32::from_le_bytes(version);
if version != FORMAT {
return Err(invalid(&format!(
"the file is format {version} and a graph section needs format {FORMAT}, so it has \
to be written again"
)));
}
let mut directory = vec![0; entries[at].directory.length as usize];
read_at(file, entries[at].directory.offset, &mut directory)?;
if checksum(&directory) != entries[at].directory.hash {
return Err(invalid(&format!("the directory of table {table} does not checksum")));
}
let mut held = decode_directory(&directory, size)?;
let mut cursor = size;
for one in attachments {
let written = write_section(file, &mut cursor, one, held.generation)?;
held.sections.retain(|old| !(old.kind == one.kind && old.id == one.id));
held.sections.push(written);
}
if held.sections.len() > MAX_SECTIONS {
return Err(invalid("the table would name more sections than the bound allows"));
}
let encoded = encode_directory(&held)?;
if encoded.len() > MAX_DIRECTORY {
return Err(invalid("directory exceeds the configured bound"));
}
let offset = append(file, &mut cursor, &encoded)?;
entries[at].directory = Page {
offset,
length: u32::try_from(encoded.len()).map_err(|_| invalid("directory length overflow"))?,
hash: checksum(&encoded),
};
let catalog = encode_catalog(&entries, &views)?;
if catalog.len() > MAX_DIRECTORY {
return Err(invalid("catalog exceeds the configured bound"));
}
let offset = append(file, &mut cursor, &catalog)?;
file.sync()?;
let generation =
slot.generation.checked_add(1).ok_or_else(|| invalid("native file generation overflow"))?;
let committed = Slot {
offset,
length: u32::try_from(catalog.len()).map_err(|_| invalid("catalog length overflow"))?,
generation,
hash: checksum(&catalog),
};
file.write_at(slot_offset(generation), &committed.bytes())?;
file.sync()?;
Ok(held)
}
type Synopsis = Arc<Vec<(Value, u64)>>;
#[derive(Debug, Clone)]
pub struct Reader {
file: Arc<File>,
table: Arc<Table>,
dictionaries: Arc<Vec<OnceLock<Arc<Vector>>>>,
loading: Arc<Vec<Mutex<()>>>,
frequency_values: Arc<Vec<OnceLock<Synopsis>>>,
frequency_summaries: Arc<Vec<OnceLock<Arc<FrequencySummary>>>>,
opened: Arc<AtomicUsize>,
sieves: Arc<Vec<Vec<SieveSlot>>>,
part_ranges: Arc<Vec<Vec<RangeSlot>>>,
places: Arc<Vec<Place>>,
cache: Arc<Shelf>,
pool: PagePool,
pages: Arc<AtomicUsize>,
indexes: Arc<AtomicUsize>,
size: u64,
directory: u64,
opening: Opening,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Opening {
pub reads: u32,
pub bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Reads {
pub opening: Opening,
pub pages: usize,
pub indexes: usize,
pub dictionaries: usize,
}
#[derive(Debug, Clone, Copy)]
struct Place {
stripe: u32,
part: u32,
rows: u32,
}
#[derive(Debug, Clone, Copy)]
struct PartSpan {
start: usize,
length: usize,
hash: u64,
}
#[derive(Debug, Clone)]
struct CachedColumn {
stripe: usize,
index: Arc<Vec<PartSpan>>,
page: Option<Arc<HeldPage>>,
}
#[derive(Debug)]
struct HeldPage {
bytes: Vec<u8>,
checked: Vec<AtomicBool>,
}
impl HeldPage {
fn part(&self, part: usize, span: PartSpan) -> Result<&[u8]> {
let bytes = part_bytes(&self.bytes, span)?;
let checked = self.checked.get(part).ok_or_else(|| invalid("part index out of range"))?;
if !checked.load(Atomic::Relaxed) {
verify_part(bytes, span)?;
checked.store(true, Atomic::Relaxed);
}
Ok(bytes)
}
}
fn verify_part(bytes: &[u8], span: PartSpan) -> Result<()> {
let got = checksum(bytes);
if got != span.hash {
return Err(invalid(&format!(
"column page checksum differs, part at {}+{} bytes, wanted {:016x} and got {got:016x}",
span.start, span.length, span.hash,
)));
}
Ok(())
}
#[derive(Debug, Default)]
struct Cached {
pages: Vec<Option<Resident>>,
loading: Vec<usize>,
index: Vec<Option<Arc<Vec<PartSpan>>>>,
seen: Vec<bool>,
passing: VecDeque<usize>,
}
#[derive(Debug, Clone)]
struct Resident {
page: Arc<HeldPage>,
used: Arc<AtomicBool>,
}
#[derive(Debug)]
struct Shelf {
columns: Vec<Mutex<Cached>>,
held: Vec<AtomicUsize>,
kept: AtomicUsize,
}
#[derive(Debug, Clone, Default)]
pub struct PagePool {
ring: Arc<Mutex<Ring>>,
budget: Arc<AtomicUsize>,
}
#[derive(Debug, Default)]
struct Ring {
held: VecDeque<Held>,
bytes: usize,
}
#[derive(Debug)]
struct Held {
shelf: Weak<Shelf>,
column: usize,
stripe: usize,
bytes: usize,
used: Arc<AtomicBool>,
}
impl PagePool {
#[must_use]
pub fn new(budget: usize) -> Self {
let pool = Self::default();
pool.budget.store(budget, Atomic::Relaxed);
pool
}
#[must_use]
pub fn bytes(&self) -> usize {
self.ring.lock().map_or(0, |ring| ring.bytes)
}
fn admit(&self, held: Held) {
let budget = self.budget.load(Atomic::Relaxed);
let mut gone = Vec::new();
{
let Ok(mut ring) = self.ring.lock() else { return };
ring.bytes += held.bytes;
ring.held.push_back(held);
let mut looked = 0;
let limit = ring.held.len();
while ring.bytes > budget && looked < limit {
looked += 1;
let Some(entry) = ring.held.pop_front() else { break };
let Some(shelf) = entry.shelf.upgrade() else {
ring.bytes -= entry.bytes;
continue;
};
if entry.used.swap(false, Atomic::Relaxed) {
ring.held.push_back(entry);
continue;
}
let count = &shelf.held[entry.column];
if count.load(Atomic::Relaxed) <= shelf.kept.load(Atomic::Relaxed).max(1) {
ring.held.push_back(entry);
continue;
}
count.fetch_sub(1, Atomic::Relaxed);
ring.bytes -= entry.bytes;
gone.push((shelf, entry));
}
while ring.held.front().is_some_and(|entry| entry.shelf.strong_count() == 0) {
if let Some(entry) = ring.held.pop_front() {
ring.bytes -= entry.bytes;
}
}
}
for (shelf, entry) in gone {
let Ok(mut cached) = shelf.columns[entry.column].lock() else { continue };
if let Some(slot) = cached.pages.get_mut(entry.stripe) {
if slot.as_ref().is_some_and(|slot| Arc::ptr_eq(&slot.used, &entry.used)) {
*slot = None;
}
}
}
}
}
const CACHED_STRIPES_PER_COLUMN: usize = 4;
type SieveSlot = OnceLock<Arc<Vec<Option<Sieve>>>>;
type RangeSlot = OnceLock<Arc<Vec<Range>>>;
#[derive(Debug)]
struct NativeText {
file: Arc<File>,
values: usize,
offsets: Vec<u8>,
offset_bits: usize,
value_ends: OnceLock<Option<Vec<u32>>>,
value_lens: OnceLock<Option<Lengths>>,
ends_asked: AtomicUsize,
ranks: usize,
rank_at: u64,
rank_ends: Vec<u64>,
rank_hashes: Vec<u64>,
rank_blocks: Vec<OnceLock<Result<Vec<u8>>>>,
code_bits: usize,
code_ranks: OnceLock<Option<Vec<u32>>>,
starts: Vec<u64>,
lengths: Vec<u64>,
hashes: Vec<u64>,
grams: Option<NativeGrams>,
blocks: Vec<OnceLock<Result<Vec<u8>>>>,
char_lens: Vec<OnceLock<Box<[u32]>>>,
keep_budget: usize,
payload_kept: AtomicUsize,
swept: Vec<AtomicBool>,
visit_dropped: AtomicUsize,
searched: Mutex<HashMap<Vec<u8>, (usize, bool)>>,
}
#[derive(Debug)]
struct NativeGrams {
start: u64,
length: usize,
width: usize,
hash: u64,
verdicts: Mutex<Vec<Verdict>>,
}
type Verdict = (Vec<u8>, Arc<[bool]>);
const GRAM_VERDICTS: usize = 8;
impl NativeGrams {
fn verdicts(&self, file: &File, literal: &[u8]) -> Result<Arc<[bool]>> {
let mut held = self.verdicts.lock().map_err(|_| invalid("a poisoned signature verdict"))?;
if let Some((_, verdict)) = held.iter().find(|(asked, _)| asked == literal) {
return Ok(Arc::clone(verdict));
}
let wanted = literal.windows(4).map(|gram| gram_bits(gram, self.width)).collect::<Vec<_>>();
let mut verdict = Vec::with_capacity(self.length / self.width);
let window = GRAM_WINDOW / self.width * self.width;
let hash = walk_checksummed(file, self.start, self.length, window, |bytes| {
verdict.extend(bytes.chunks(self.width).map(|bits| {
wanted
.iter()
.flatten()
.all(|&bit| bits.get(bit / 8).is_some_and(|byte| byte & (1 << (bit % 8)) != 0))
}));
Ok(())
})?;
if hash != self.hash {
return Err(invalid("global dictionary substring signatures checksum differs"));
}
let verdict: Arc<[bool]> = verdict.into();
if held.len() >= GRAM_VERDICTS {
held.remove(0);
}
held.push((literal.to_vec(), Arc::clone(&verdict)));
Ok(verdict)
}
fn footprint(&self) -> usize {
self.verdicts.lock().map_or(0, |held| {
held.iter().map(|(asked, verdict)| asked.capacity() + verdict.len()).sum()
})
}
}
const TEXT_SEARCH_MEMO: usize = 64;
const TEXT_PAYLOAD_VALUES: usize = 1024;
const TEXT_GRAM_BYTES: usize = 8192;
const NARROW_GRAM_BYTES: usize = 2048;
const GRAM_WINDOW: usize = 256 << 10;
fn gram_bits(bytes: &[u8], width: usize) -> [usize; 2] {
let original = u32::from_le_bytes(bytes.try_into().expect("a four-byte gram"));
let mut first = original ^ (original >> 16);
first = first.wrapping_mul(0x7feb_352d);
first ^= first >> 15;
let mut second = original ^ (original >> 17);
second = second.wrapping_mul(0x846c_a68b);
second ^= second >> 16;
let mask = width * 8 - 1;
[(first as usize) & mask, (second as usize) & mask]
}
const TEXT_KEEP_BUDGET: usize = 256 * 1024 * 1024;
#[derive(Debug)]
enum Lengths {
Narrow(Vec<u16>),
Wide(Vec<u32>),
}
impl Lengths {
fn extend_at(&self, indices: &[u32], into: &mut Vec<i64>) {
match self {
Lengths::Narrow(lens) => into.extend(
indices
.iter()
.map(|&index| lens.get(index as usize).map_or(0, |&len| i64::from(len))),
),
Lengths::Wide(lens) => into.extend(
indices
.iter()
.map(|&index| lens.get(index as usize).map_or(0, |&len| i64::from(len))),
),
}
}
fn footprint(&self) -> usize {
match self {
Lengths::Narrow(lens) => lens.capacity() * size_of::<u16>(),
Lengths::Wide(lens) => lens.capacity() * size_of::<u32>(),
}
}
}
fn lengths_of(ends: &[u32]) -> Option<Lengths> {
match lengths_as::<u16>(ends)? {
Some(narrow) => Some(Lengths::Narrow(narrow)),
None => lengths_as::<u32>(ends)?.map(Lengths::Wide),
}
}
fn lengths_as<T: TryFrom<u32>>(ends: &[u32]) -> Option<Option<Vec<T>>> {
let mut lens = Vec::with_capacity(ends.len());
for block in ends.chunks(TEXT_PAYLOAD_VALUES) {
let mut start = 0;
for &end in block {
let Ok(len) = T::try_from(end.checked_sub(start)?) else {
return Some(None);
};
lens.push(len);
start = end;
}
}
Some(Some(lens))
}
const TEXT_OFFSET_RUN: usize = 512;
const DICTIONARY_HEADER: usize = 16;
const DICTIONARY_SCATTERED: u32 = 1 << 31;
const DICTIONARY_GRAMS: u32 = 1 << 30;
const DICTIONARY_WIDE_GRAMS: u32 = 1 << 29;
const DICTIONARY_FLAGS: u32 = DICTIONARY_SCATTERED | DICTIONARY_GRAMS | DICTIONARY_WIDE_GRAMS;
const TEXT_RANK_BLOCK: usize = 512;
const RANK_BLOCK_HEADER: usize = size_of::<u64>() + 1;
impl NativeText {
fn payload_block(&self, block: usize) -> Result<Option<&[u8]>> {
let Some(slot) = self.blocks.get(block) else { return Ok(None) };
let bytes = slot.get_or_init(|| self.decode_block(block)).as_ref().map_err(Clone::clone)?;
Ok(Some(bytes.as_slice()))
}
fn block_chars(&self, block: usize) -> Result<&[u32]> {
let slot = self
.char_lens
.get(block)
.ok_or_else(|| invalid("a block past the global dictionary"))?;
if let Some(lens) = slot.get() {
return Ok(lens);
}
let decoded;
let bytes: &[u8] = match self.blocks.get(block).and_then(OnceLock::get) {
Some(Ok(kept)) => kept,
_ => {
decoded = self.decode_block(block)?;
&decoded
}
};
let first = block * TEXT_PAYLOAD_VALUES;
let last = (first + TEXT_PAYLOAD_VALUES).min(self.values);
let ends = self.ends_within(first, last)?;
if ends.len() != last - first {
return Err(invalid("global dictionary offsets are short"));
}
let mut lens = Vec::with_capacity(ends.len());
let mut start = u64::from(self.start_within(first)?);
for &end in &ends {
let value = usize::try_from(start)
.ok()
.zip(usize::try_from(end).ok())
.and_then(|(from, to)| bytes.get(from..to))
.ok_or_else(|| invalid("global dictionary value is past its block"))?;
let characters = value.iter().filter(|byte| (**byte as i8) >= -0x40).count();
lens.push(u32::try_from(characters).unwrap_or(u32::MAX));
start = end;
}
Ok(slot.get_or_init(|| lens.into_boxed_slice()))
}
fn decode_block(&self, block: usize) -> Result<Vec<u8>> {
let len = self.lengths[block];
let mut stored = vec![
0;
usize::try_from(len).map_err(|_| invalid(
"global dictionary block does not fit in memory"
))?
];
read_at(&self.file, self.starts[block], &mut stored)?;
if checksum(&stored) != self.hashes[block] {
return Err(invalid("global dictionary payload checksum differs"));
}
let first = block * TEXT_PAYLOAD_VALUES;
let last = (first + TEXT_PAYLOAD_VALUES).min(self.values);
let want = self.end_within(last - 1)? as usize;
let values = string::decode_flat(&stored)?;
if values.len() != last - first {
return Err(invalid("global dictionary block holds the wrong value count"));
}
let bytes = values.into_bytes();
if bytes.len() != want {
return Err(invalid("global dictionary block decodes to the wrong length"));
}
Ok(bytes)
}
fn loaned_block<'a>(
&'a self,
block: usize,
decoded: &'a mut Vec<u8>,
scattered: bool,
) -> Result<&'a [u8]> {
let kept = self.blocks.get(block).and_then(OnceLock::get);
if let Some(Ok(kept)) = kept {
return Ok(kept);
}
let again = kept.is_none()
&& self.swept.get(block).is_some_and(|swept| swept.swap(true, Atomic::Relaxed));
let keep = again
&& (self.payload_kept.load(Atomic::Relaxed) < self.keep_budget
|| (scattered && self.visit_dropped.load(Atomic::Relaxed) >= self.blocks.len()));
if keep {
let kept = self
.payload_block(block)?
.ok_or_else(|| invalid("global dictionary block is past the payload"))?;
self.payload_kept.fetch_add(kept.len(), Atomic::Relaxed);
return Ok(kept);
}
*decoded = self.decode_block(block)?;
if scattered && again {
self.visit_dropped.fetch_add(1, Atomic::Relaxed);
}
Ok(decoded)
}
fn ends_worth_unpacking(&self) -> usize {
self.values.max(TEXT_PAYLOAD_VALUES)
}
fn value_ends(&self) -> Option<&[u32]> {
if let Some(built) = self.value_ends.get() {
return built.as_deref();
}
if self.ends_asked.fetch_add(1, Atomic::Relaxed) < self.ends_worth_unpacking() {
return None;
}
self.value_ends.get_or_init(|| self.unpack_ends()).as_deref()
}
fn unpack_ends(&self) -> Option<Vec<u32>> {
let mut ends = vec![0u32; self.values];
for (run, into) in ends.chunks_mut(TEXT_OFFSET_RUN).enumerate() {
let bytes = self.packed().get(run * TEXT_OFFSET_RUN / 8 * self.offset_bits..)?;
bitpack::unpack_tail_into(bytes, self.offset_bits, into, |bits| {
u32::try_from(bits).unwrap_or(u32::MAX)
})
.ok()?;
}
if ends.contains(&u32::MAX) { None } else { Some(ends) }
}
fn packed(&self) -> &[u8] {
self.offsets.get(DICTIONARY_HEADER..).unwrap_or_default()
}
fn end_within(&self, index: usize) -> Result<u32> {
if let Some(ends) = self.value_ends() {
return ends
.get(index)
.copied()
.ok_or_else(|| invalid("global dictionary offsets are short"));
}
let run = index / TEXT_OFFSET_RUN;
let bytes = self
.packed()
.get(run * TEXT_OFFSET_RUN / 8 * self.offset_bits..)
.ok_or_else(|| invalid("global dictionary offsets are short"))?;
let end = bitpack::tail_at(bytes, self.offset_bits, index % TEXT_OFFSET_RUN)
.map_err(|_| invalid("global dictionary offsets are short"))?;
u32::try_from(end).map_err(|_| invalid("global dictionary offset is past the payload"))
}
fn ends_within(&self, first: usize, last: usize) -> Result<Vec<u64>> {
let mut ends = vec![0u64; last.saturating_sub(first)];
let mut scratch = Vec::new();
let mut at = first;
while at < last {
let run = at / TEXT_OFFSET_RUN;
let stop = ((run + 1) * TEXT_OFFSET_RUN).min(last);
let held = self.values.saturating_sub(run * TEXT_OFFSET_RUN).min(TEXT_OFFSET_RUN);
let bytes = self
.packed()
.get(run * TEXT_OFFSET_RUN / 8 * self.offset_bits..)
.ok_or_else(|| invalid("global dictionary offsets are short"))?;
let from = at % TEXT_OFFSET_RUN;
let upto = stop - run * TEXT_OFFSET_RUN;
if upto > held || bytes.len() < bitpack::tail_len(held, self.offset_bits) {
return Err(invalid("global dictionary offsets are short"));
}
let into = &mut ends[at - first..stop - first];
if from == 0 {
bitpack::unpack_tail_into(bytes, self.offset_bits, into, |bits| bits)
.map_err(|_| invalid("global dictionary offsets are short"))?;
} else {
scratch.resize(held, 0);
bitpack::unpack_tail_into(bytes, self.offset_bits, &mut scratch, |bits| bits)
.map_err(|_| invalid("global dictionary offsets are short"))?;
into.copy_from_slice(&scratch[from..upto]);
}
at = stop;
}
Ok(ends)
}
fn start_within(&self, index: usize) -> Result<u32> {
if index % TEXT_PAYLOAD_VALUES == 0 { Ok(0) } else { self.end_within(index - 1) }
}
fn span_within(&self, index: usize) -> Result<(u32, u32)> {
if let Some(ends) = self.value_ends() {
let end =
*ends.get(index).ok_or_else(|| invalid("global dictionary offsets are short"))?;
let start = if index % TEXT_PAYLOAD_VALUES == 0 { 0 } else { ends[index - 1] };
if start > end {
return Err(invalid("global dictionary value ends before it starts"));
}
return Ok((start, end));
}
let within = index % TEXT_OFFSET_RUN;
let (start, end) = if within == 0 {
(self.start_within(index)?, self.end_within(index)?)
} else {
let run = index / TEXT_OFFSET_RUN;
let bytes = self
.packed()
.get(run * TEXT_OFFSET_RUN / 8 * self.offset_bits..)
.ok_or_else(|| invalid("global dictionary offsets are short"))?;
let (start, end) = bitpack::tail_pair(bytes, self.offset_bits, within)
.map_err(|_| invalid("global dictionary offsets are short"))?;
let ends = u32::try_from(end)
.map_err(|_| invalid("global dictionary offset is past the payload"))?;
let starts = u32::try_from(start)
.map_err(|_| invalid("global dictionary offset is past the payload"))?;
(starts, ends)
};
if start > end {
return Err(invalid("global dictionary value ends before it starts"));
}
Ok((start, end))
}
fn rank_parts(&self, rank: usize) -> Result<(&[u8], usize)> {
let slot = self
.rank_blocks
.get(rank / TEXT_RANK_BLOCK)
.ok_or_else(|| invalid("global dictionary rank is past the order"))?;
let block = slot
.get_or_init(|| {
let mut bytes = Vec::new();
self.read_rank_block(rank / TEXT_RANK_BLOCK, &mut bytes)?;
Ok(bytes)
})
.as_ref()
.map_err(Clone::clone)?;
Ok((block.as_slice(), rank % TEXT_RANK_BLOCK))
}
fn read_rank_block(&self, which: usize, bytes: &mut Vec<u8>) -> Result<()> {
let start = if which == 0 { 0 } else { self.rank_ends[which - 1] };
let end = self.rank_ends[which];
bytes.clear();
bytes.resize((end - start) as usize, 0);
read_at(&self.file, self.rank_at + start, bytes)?;
let expected = self
.rank_hashes
.get(which)
.ok_or_else(|| invalid("global dictionary rank block has no checksum"))?;
if checksum(bytes) != *expected {
return Err(invalid("global dictionary rank checksum differs"));
}
Ok(())
}
fn head_at(&self, rank: usize) -> Result<u64> {
let (block, within) = self.rank_parts(rank)?;
let (base, width, packed) = rank_heads(block)?;
let above = bitpack::tail_at(packed, width, within)
.map_err(|_| invalid("global dictionary rank block is short of heads"))?;
Ok(base.wrapping_add(above))
}
fn rank_codes<'block>(&self, block: &'block [u8], count: usize) -> Result<&'block [u8]> {
let (_, width, packed) = rank_heads(block)?;
packed
.get(bitpack::tail_len(count, width)..)
.ok_or_else(|| invalid("global dictionary rank block is short of codes"))
}
fn rank_block_len(&self, rank: usize) -> usize {
let first = rank / TEXT_RANK_BLOCK * TEXT_RANK_BLOCK;
TEXT_RANK_BLOCK.min(self.ranks - first)
}
}
fn rank_heads(block: &[u8]) -> Result<(u64, usize, &[u8])> {
let header = block
.get(..RANK_BLOCK_HEADER)
.ok_or_else(|| invalid("global dictionary rank block is short"))?;
let base = u64::from_le_bytes(header[..8].try_into().expect("eight bytes"));
let width = header[8] as usize;
if width > 64 {
return Err(invalid("global dictionary rank block packs heads past a word"));
}
Ok((base, width, &block[RANK_BLOCK_HEADER..]))
}
fn offset_width(ends: &[u32]) -> usize {
let span = ends.iter().copied().max().unwrap_or(0);
(u32::BITS - span.leading_zeros()) as usize
}
fn offset_bytes(values: usize, bits: usize) -> usize {
let full = values / TEXT_OFFSET_RUN;
let rest = values % TEXT_OFFSET_RUN;
full * TEXT_OFFSET_RUN / 8 * bits + bitpack::tail_len(rest, bits)
}
fn encode_offsets(ends: &[u32], bits: usize, out: &mut Vec<u8>) -> Result<()> {
let mut run = Vec::with_capacity(TEXT_OFFSET_RUN);
for chunk in ends.chunks(TEXT_OFFSET_RUN) {
run.clear();
run.extend(chunk.iter().map(|&end| u64::from(end)));
bitpack::pack_tail(&run, bits, out)
.map_err(|_| invalid("global dictionary offsets do not pack"))?;
}
Ok(())
}
fn code_width(values: usize) -> usize {
match u64::try_from(values).unwrap_or(u64::MAX) {
0 | 1 => 0,
last => (u64::BITS - (last - 1).leading_zeros()) as usize,
}
}
impl TextSource for NativeText {
fn len(&self) -> usize {
self.values
}
fn might_contain(&self, first: usize, literal: &[u8]) -> Result<bool> {
let Some(grams) = &self.grams else { return Ok(true) };
if literal.len() < 4 || first >= self.values {
return Ok(true);
}
let verdict = grams.verdicts(&self.file, literal)?;
Ok(verdict.get(first / TEXT_PAYLOAD_VALUES).copied().unwrap_or(true))
}
fn bytes_at(&self, index: usize) -> Result<Option<&[u8]>> {
if index >= self.values {
return Ok(None);
}
let (start, end) = self.span_within(index)?;
if start == end {
return Ok(Some(&[]));
}
let block = index / TEXT_PAYLOAD_VALUES;
let Some(bytes) = self.payload_block(block)? else { return Ok(None) };
Ok(bytes.get(start as usize..end as usize))
}
fn bytes_len_at(&self, index: usize) -> Result<Option<usize>> {
if index >= self.values {
return Ok(None);
}
let (start, end) = self.span_within(index)?;
Ok(Some((end - start) as usize))
}
fn bytes_lens_at(&self, indices: &[u32], into: &mut Vec<i64>) -> Result<()> {
self.ends_asked.fetch_add(indices.len(), Atomic::Relaxed);
into.reserve(indices.len());
let Some(ends) = self.value_ends() else {
for &index in indices {
into.push(
self.bytes_len_at(index as usize)?
.map_or(0, |len| i64::try_from(len).unwrap_or(i64::MAX)),
);
}
return Ok(());
};
if let Some(lens) = self.value_lens.get_or_init(|| lengths_of(ends)) {
lens.extend_at(indices, into);
return Ok(());
}
for &index in indices {
let index = index as usize;
let Some(&end) = ends.get(index) else {
into.push(0);
continue;
};
let start = if index % TEXT_PAYLOAD_VALUES == 0 { 0 } else { ends[index - 1] };
if start > end {
return Err(invalid("global dictionary value ends before it starts"));
}
into.push(i64::from(end - start));
}
Ok(())
}
fn chars_lens_at(&self, indices: &[u32], into: &mut Vec<i64>) -> Result<()> {
into.reserve(indices.len());
for &index in indices {
let index = index as usize;
if index >= self.values {
into.push(0);
continue;
}
let lens = self.block_chars(index / TEXT_PAYLOAD_VALUES)?;
let len = lens
.get(index % TEXT_PAYLOAD_VALUES)
.ok_or_else(|| invalid("global dictionary block holds the wrong value count"))?;
into.push(i64::from(*len));
}
Ok(())
}
fn sweep(
&self,
first: usize,
limit: usize,
body: &mut dyn FnMut(usize, &[u8]) -> Result<()>,
) -> Result<usize> {
let limit = limit.min(self.values);
if first >= limit {
return Ok(first);
}
let block = first / TEXT_PAYLOAD_VALUES;
let last = ((block + 1) * TEXT_PAYLOAD_VALUES).min(limit);
let mut decoded = Vec::new();
let bytes = self.loaned_block(block, &mut decoded, false)?;
let ends = self.ends_within(first, last)?;
if ends.len() != last - first {
return Err(invalid("global dictionary offsets are short"));
}
let mut start = u64::from(self.start_within(first)?);
for (index, &end) in (first..last).zip(&ends) {
let value = usize::try_from(start)
.ok()
.zip(usize::try_from(end).ok())
.and_then(|(from, to)| bytes.get(from..to))
.ok_or_else(|| invalid("global dictionary value is past its block"))?;
body(index, value)?;
start = end;
}
Ok(last)
}
fn visit_at(
&self,
indices: &[u32],
body: &mut dyn FnMut(usize, &[u8]) -> Result<()>,
) -> Result<()> {
let mut order = (0..indices.len()).collect::<Vec<_>>();
order.sort_unstable_by_key(|&at| indices[at]);
let block_of = |at: usize| {
let index = indices[at] as usize;
(index < self.values).then_some(index / TEXT_PAYLOAD_VALUES)
};
let mut decoded = Vec::new();
let mut run = 0;
while run < order.len() {
let Some(block) = block_of(order[run]) else {
for &at in &order[run..] {
body(at, &[])?;
}
break;
};
let upto = run + order[run..].partition_point(|&at| block_of(at) == Some(block));
let bytes = self.loaned_block(block, &mut decoded, true)?;
for &at in &order[run..upto] {
let (start, end) = self.span_within(indices[at] as usize)?;
let value = bytes
.get(start as usize..end as usize)
.ok_or_else(|| invalid("global dictionary value is past its block"))?;
body(at, value)?;
}
run = upto;
}
Ok(())
}
fn visit(
&self,
indices: &[usize],
body: &mut dyn FnMut(usize, &[u8]) -> Result<()>,
) -> Result<()> {
let mut at = 0;
while at < indices.len() {
let block = indices[at] / TEXT_PAYLOAD_VALUES;
let upto =
at + indices[at..].partition_point(|&index| index / TEXT_PAYLOAD_VALUES == block);
let wanted = &indices[at..upto];
if wanted.iter().any(|&index| index >= self.values) {
return Err(invalid("a visited value is past the global dictionary"));
}
let decoded;
let bytes: &[u8] = match self.blocks.get(block).and_then(OnceLock::get) {
Some(Ok(kept)) => kept,
_ => {
decoded = self.decode_block(block)?;
&decoded
}
};
for (offset, &index) in wanted.iter().enumerate() {
let (start, end) = self.span_within(index)?;
let value = bytes
.get(start as usize..end as usize)
.ok_or_else(|| invalid("global dictionary value is past its block"))?;
body(at + offset, value)?;
}
at = upto;
}
Ok(())
}
fn ranks(&self) -> Option<usize> {
(self.ranks > 0).then_some(self.ranks)
}
fn below(&self, ranks: usize, wanted: &[u8]) -> Result<(usize, bool)> {
let mut memo = self.searched.lock().map_err(|_| invalid("a poisoned dictionary search"))?;
if let Some(&answer) = memo.get(wanted) {
return Ok(answer);
}
let answer = search_below(self, ranks, wanted)?;
if memo.len() >= TEXT_SEARCH_MEMO {
memo.clear();
}
memo.insert(wanted.to_vec(), answer);
Ok(answer)
}
fn compare_rank(&self, rank: usize, wanted: &[u8]) -> Result<Ordering> {
let settled = self.head_at(rank)?.cmp(&head(wanted));
if settled != Ordering::Equal {
return Ok(settled);
}
let code = self.code_at_rank(rank)?;
let bytes = self
.bytes_at(code as usize)?
.ok_or_else(|| invalid("global dictionary order names a code it does not have"))?;
Ok(bytes.cmp(wanted))
}
fn code_at_rank(&self, rank: usize) -> Result<u32> {
let (block, within) = self.rank_parts(rank)?;
let codes = self.rank_codes(block, self.rank_block_len(rank))?;
let code = bitpack::tail_at(codes, self.code_bits, within)
.map_err(|_| invalid("global dictionary rank block is short of codes"))?;
let code = u32::try_from(code)
.map_err(|_| invalid("global dictionary order names a code it does not have"))?;
if code as usize >= self.len() {
return Err(invalid("global dictionary order names a code it does not have"));
}
Ok(code)
}
fn code_ranks(&self) -> Option<&[u32]> {
if self.ranks == 0 || self.ranks != self.len() {
return None;
}
self.code_ranks
.get_or_init(|| {
let mut ranks = vec![u32::MAX; self.ranks];
let mut scratch = Vec::new();
let mut codes = vec![0u64; TEXT_RANK_BLOCK];
for first in (0..self.ranks).step_by(TEXT_RANK_BLOCK) {
let which = first / TEXT_RANK_BLOCK;
let block = match self.rank_blocks.get(which)?.get() {
Some(kept) => kept.as_ref().ok()?.as_slice(),
None => {
self.read_rank_block(which, &mut scratch).ok()?;
scratch.as_slice()
}
};
let count = self.rank_block_len(first);
let packed = self.rank_codes(block, count).ok()?;
let codes = codes.get_mut(..count)?;
bitpack::unpack_tail_into(packed, self.code_bits, codes, |bits| bits).ok()?;
for (within, &code) in codes.iter().enumerate() {
let code = usize::try_from(code).ok()?;
*ranks.get_mut(code)? = u32::try_from(first + within).ok()?;
}
}
if ranks.contains(&u32::MAX) {
return None;
}
Some(ranks)
})
.as_deref()
}
fn footprint(&self) -> usize {
self.offsets.capacity()
+ self
.value_ends
.get()
.and_then(Option::as_ref)
.map_or(0, |ends| ends.capacity() * size_of::<u32>())
+ self.value_lens.get().and_then(Option::as_ref).map_or(0, Lengths::footprint)
+ self
.code_ranks
.get()
.and_then(Option::as_ref)
.map_or(0, |ranks| ranks.capacity() * size_of::<u32>())
+ self.rank_hashes.capacity() * size_of::<u64>()
+ self.rank_ends.capacity() * size_of::<u64>()
+ self.rank_blocks.capacity() * size_of::<OnceLock<Result<Vec<u8>>>>()
+ self
.rank_blocks
.iter()
.filter_map(OnceLock::get)
.filter_map(|result| result.as_ref().ok())
.map(Vec::capacity)
.sum::<usize>()
+ self.blocks.capacity() * size_of::<OnceLock<Result<Vec<u8>>>>()
+ self.char_lens.capacity() * size_of::<OnceLock<Box<[u32]>>>()
+ self
.char_lens
.iter()
.filter_map(OnceLock::get)
.map(|lens| lens.len() * size_of::<u32>())
.sum::<usize>()
+ self.hashes.capacity() * size_of::<u64>()
+ self.starts.capacity() * size_of::<u64>()
+ self.lengths.capacity() * size_of::<u64>()
+ self.grams.as_ref().map_or(0, NativeGrams::footprint)
+ self
.blocks
.iter()
.filter_map(OnceLock::get)
.filter_map(|result| result.as_ref().ok())
.map(Vec::capacity)
.sum::<usize>()
}
}
fn places(table: &Table) -> Result<Vec<Place>> {
let mut places = Vec::with_capacity(table.stripes.len().saturating_mul(STRIPE_PARTS));
for (at, stripe) in table.stripes.iter().enumerate() {
let index = u32::try_from(at).map_err(|_| invalid("too many stripes"))?;
for (part, &rows) in stripe.parts.iter().enumerate() {
places.push(Place {
stripe: index,
part: u32::try_from(part).map_err(|_| invalid("too many parts in a stripe"))?,
rows,
});
}
}
Ok(places)
}
fn read_index<F: Positional + ?Sized>(
file: &F,
stripe: &Stripe,
column: usize,
) -> Result<Vec<PartSpan>> {
let page = stripe.pages.get(column).ok_or_else(|| invalid("stripe page is missing"))?;
read_index_span(file, stripe.index, *page, stripe.parts.len(), column)
}
fn read_index_span<F: Positional + ?Sized>(
file: &F,
index: Span,
page: Span,
parts: usize,
column: usize,
) -> Result<Vec<PartSpan>> {
let section = index_section(parts)?;
let at = column.checked_mul(section).ok_or_else(|| invalid("index page offset overflow"))?;
let end = at.checked_add(section).ok_or_else(|| invalid("index page offset overflow"))?;
if end > index.length as usize {
return Err(invalid("index page is shorter than its columns"));
}
let mut bytes = vec![0; section];
let offset =
index.offset.checked_add(at as u64).ok_or_else(|| invalid("index page offset overflow"))?;
read_at(file, offset, &mut bytes)?;
let entries = section - size_of::<u64>();
let stored = u64::from_le_bytes(bytes[entries..].try_into().expect("eight bytes"));
if checksum(&bytes[..entries]) != stored {
return Err(invalid(&format!(
"index page section checksum differs, column {column} of {parts} parts at {offset}, \
wanted {stored:016x} and got {:016x}",
checksum(&bytes[..entries]),
)));
}
let mut spans = Vec::with_capacity(parts);
let mut start = 0_usize;
for part in 0..parts {
let at = part * INDEX_ENTRY;
let length = u32::from_le_bytes(bytes[at..at + 4].try_into().expect("four bytes")) as usize;
let hash = u64::from_le_bytes(bytes[at + 4..at + 12].try_into().expect("eight bytes"));
spans.push(PartSpan { start, length, hash });
start = start.checked_add(length).ok_or_else(|| invalid("column page length overflow"))?;
}
if start != page.length as usize {
return Err(invalid("column page length differs from its index"));
}
Ok(spans)
}
fn part_bytes(page: &[u8], span: PartSpan) -> Result<&[u8]> {
let end = span.start.checked_add(span.length).ok_or_else(|| invalid("part range overflow"))?;
page.get(span.start..end).ok_or_else(|| invalid("part exceeds its column page"))
}
fn remember(cached: &mut Cached, held: &CachedColumn) -> Option<(usize, Arc<AtomicBool>)> {
if let Some(slot) = cached.index.get_mut(held.stripe) {
if slot.is_none() {
*slot = Some(Arc::clone(&held.index));
}
}
let page = held.page.clone()?;
let slot = cached.pages.get_mut(held.stripe)?;
if slot.is_some() {
return None;
}
let bytes = page.bytes.len();
let used = Arc::new(AtomicBool::new(true));
*slot = Some(Resident { page, used: Arc::clone(&used) });
Some((bytes, used))
}
#[derive(Debug, Clone)]
pub struct Catalog {
file: Arc<File>,
size: u64,
entries: Arc<Vec<Entry>>,
views: Arc<Vec<ViewEntry>>,
opening: Opening,
pool: PagePool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CertifiedSums {
pub columns: Vec<(i128, u64)>,
pub rows: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegerExtremes {
Null,
Values { low: i128, high: i128 },
}
pub type NumericFrequencies = Vec<(Option<i128>, u64)>;
impl Catalog {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
Self::open_in(path, &PagePool::default())
}
pub fn open_in(path: impl AsRef<Path>, pool: &PagePool) -> Result<Self> {
let (file, size, _, bytes, opening) = slot_bytes(path)?;
let (entries, views) = decode_catalog(&bytes, size)?;
Ok(Self {
file: Arc::new(file),
size,
entries: Arc::new(entries),
views: Arc::new(views),
opening,
pool: pool.clone(),
})
}
pub fn names(&self) -> impl ExactSizeIterator<Item = &str> {
self.entries.iter().map(|entry| entry.name.as_str())
}
pub fn rows(&self) -> impl ExactSizeIterator<Item = (&str, usize)> {
self.entries.iter().map(|entry| (entry.name.as_str(), entry.rows))
}
pub fn views(&self) -> impl ExactSizeIterator<Item = &ViewEntry> {
self.views.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn table(&self, name: &str) -> Result<Reader> {
let entry = self
.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| invalid(&format!("the file holds no table called {name}")))?;
let (offset, length) = (entry.directory.offset, entry.directory.length as usize);
if file_checksum(&self.file, offset, length)? != entry.directory.hash {
return Err(invalid(&format!("the directory of table {name} does not checksum")));
}
let mut opening = self.opening;
opening.reads += 1;
opening.bytes += u64::from(entry.directory.length);
Reader::build(
Arc::clone(&self.file),
self.size,
read_directory(Cursor::over(&self.file, offset, length), self.size, Some(offset))?,
u64::from(entry.directory.length),
opening,
self.pool.clone(),
)
}
pub fn integer_tally(&self, name: &str, column: usize) -> Result<Option<Vec<(i64, u64)>>> {
let mut counts = BTreeMap::<i64, u64>::new();
let Some(()) = self.integer_fold(name, column, |value, count| {
let held = counts.entry(value).or_default();
*held = held.checked_add(count).ok_or_else(|| invalid("integer count overflow"))?;
Ok(())
})?
else {
return Ok(None);
};
Ok(Some(counts.into_iter().collect()))
}
pub fn integer_fold(
&self,
name: &str,
column: usize,
mut emit: impl FnMut(i64, u64) -> Result<()>,
) -> Result<Option<()>> {
let entry = self
.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| invalid(&format!("the file holds no table called {name}")))?;
let field =
entry.fields.get(column).ok_or_else(|| invalid("integer column index out of range"))?;
if !signed_integer(&field.ty) {
return Ok(None);
}
let (offset, length) = (entry.directory.offset, entry.directory.length as usize);
if file_checksum(&self.file, offset, length)? != entry.directory.hash {
return Err(invalid(&format!("the directory of table {name} does not checksum")));
}
quick_integer_fold(
&self.file,
Cursor::over(&self.file, offset, length),
entry,
self.size,
column,
&mut emit,
)?;
Ok(Some(()))
}
pub fn nonzero_count(&self, name: &str, column: usize) -> Result<Option<u64>> {
let entry = self
.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| invalid(&format!("the file holds no table called {name}")))?;
let Some(field) = entry.fields.get(column) else {
return Err(invalid("frequency column index out of range"));
};
if !matches!(
field.ty,
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
) {
return Ok(None);
}
let (offset, length) = (entry.directory.offset, entry.directory.length as usize);
if file_checksum(&self.file, offset, length)? != entry.directory.hash {
return Err(invalid(&format!("the directory of table {name} does not checksum")));
}
if let Some(Some(frequencies)) = entry.frequencies.get(column) {
return frequencies
.iter()
.filter(|(value, _)| value.is_some_and(|value| value != 0))
.try_fold(0_u64, |total, (_, count)| total.checked_add(*count))
.map(Some)
.ok_or_else(|| invalid("numeric frequency count overflow"));
}
quick_nonzero(
Cursor::over(&self.file, offset, length),
&entry.name,
&entry.fields,
entry.rows,
column,
)
}
pub fn aggregate_sums(&self, name: &str, columns: &[usize]) -> Result<Option<CertifiedSums>> {
let entry = self
.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| invalid(&format!("the file holds no table called {name}")))?;
let mut sums = Vec::with_capacity(columns.len());
for &column in columns {
let Some(field) = entry.fields.get(column) else {
return Err(invalid("aggregate column index out of range"));
};
if !signed_integer(&field.ty) {
return Ok(None);
}
let Some(sum) = entry.aggregates[column] else {
return Ok(None);
};
sums.push(sum);
}
let (offset, length) = (entry.directory.offset, entry.directory.length as usize);
if file_checksum(&self.file, offset, length)? != entry.directory.hash {
return Err(invalid(&format!("the directory of table {name} does not checksum")));
}
Ok(Some(CertifiedSums { columns: sums, rows: entry.rows as u64 }))
}
pub fn distinct_count(&self, name: &str, column: usize) -> Result<Option<u64>> {
let entry = self
.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| invalid(&format!("the file holds no table called {name}")))?;
let Some(count) = entry.distincts.get(column).copied() else {
return Err(invalid("distinct column index out of range"));
};
let Some(count) = count else { return Ok(None) };
let (offset, length) = (entry.directory.offset, entry.directory.length as usize);
if file_checksum(&self.file, offset, length)? != entry.directory.hash {
return Err(invalid(&format!("the directory of table {name} does not checksum")));
}
Ok(Some(count))
}
pub fn integer_extremes(&self, name: &str, column: usize) -> Result<Option<IntegerExtremes>> {
let entry = self
.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| invalid(&format!("the file holds no table called {name}")))?;
let Some(extremes) = entry.extremes.get(column).copied() else {
return Err(invalid("extremes column index out of range"));
};
let Some(extremes) = extremes else { return Ok(None) };
let (offset, length) = (entry.directory.offset, entry.directory.length as usize);
if file_checksum(&self.file, offset, length)? != entry.directory.hash {
return Err(invalid(&format!("the directory of table {name} does not checksum")));
}
Ok(Some(match extremes {
None => IntegerExtremes::Null,
Some((low, high)) => IntegerExtremes::Values { low, high },
}))
}
pub fn exact_numeric_frequencies(
&self,
name: &str,
column: usize,
) -> Result<Option<NumericFrequencies>> {
let entry = self
.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| invalid(&format!("the file holds no table called {name}")))?;
let Some(frequencies) = entry.frequencies.get(column).cloned() else {
return Err(invalid("numeric frequency column index out of range"));
};
let Some(frequencies) = frequencies else { return Ok(None) };
let (offset, length) = (entry.directory.offset, entry.directory.length as usize);
if file_checksum(&self.file, offset, length)? != entry.directory.hash {
return Err(invalid(&format!("the directory of table {name} does not checksum")));
}
Ok(Some(frequencies))
}
pub fn table_fields(&self, name: &str) -> Option<&[Field]> {
self.entries.iter().find(|entry| entry.name == name).map(|entry| entry.fields.as_slice())
}
}
fn slot_offset(generation: u64) -> u64 {
16 + (generation - 1) % 2 * SLOT_BYTES as u64
}
fn slot_bytes(path: impl AsRef<Path>) -> Result<(File, u64, Slot, Vec<u8>, Opening)> {
let file = File::open(path).map_err(io)?;
let size = file.metadata().map_err(io)?.len();
let (slot, bytes, opening) = committed_slot(&file, size)?;
Ok((file, size, slot, bytes, opening))
}
fn committed_slot<F: Positional + ?Sized>(file: &F, size: u64) -> Result<(Slot, Vec<u8>, Opening)> {
if size < HEADER {
return Err(invalid("file is shorter than its header"));
}
let mut header = [0; HEADER as usize];
read_at(file, 0, &mut header)?;
let mut opening = Opening { reads: 1, bytes: HEADER };
let version = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
if &header[..8] != MAGIC {
return Err(invalid("the header does not begin with a rudb native magic"));
}
if !READABLE.contains(&version) {
return Err(invalid(&format!(
"the file is format {version} and this build reads format {FORMAT}, so it has to \
be written again"
)));
}
let mut selected = None;
for start in [16, 16 + SLOT_BYTES] {
let slot = Slot::read(&header[start..start + SLOT_BYTES]);
if slot.generation == 0 || slot.length == 0 || slot.length as usize > MAX_DIRECTORY {
continue;
}
let Some(end) = slot.offset.checked_add(u64::from(slot.length)) else { continue };
if slot.offset < HEADER || end > size {
continue;
}
let mut bytes = vec![0; slot.length as usize];
read_at(file, slot.offset, &mut bytes)?;
opening.reads += 1;
opening.bytes += u64::from(slot.length);
if checksum(&bytes) == slot.hash
&& selected
.as_ref()
.is_none_or(|(old, _): &(Slot, Vec<u8>)| old.generation < slot.generation)
{
selected = Some((slot, bytes));
}
}
let (slot, bytes) = selected.ok_or_else(|| invalid("no committed directory slot is valid"))?;
Ok((slot, bytes, opening))
}
impl Reader {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let catalog = Catalog::open(path)?;
let mut names = catalog.names();
let name = names.next().ok_or_else(|| invalid("the file holds no table"))?.to_string();
if names.next().is_some() {
return Err(invalid(
"the file holds more than one table, so it has to be opened by name",
));
}
catalog.table(&name)
}
fn build(
file: Arc<File>,
size: u64,
table: Table,
directory: u64,
opening: Opening,
pool: PagePool,
) -> Result<Self> {
let places = places(&table)?;
let dictionaries = (0..table.fields.len()).map(|_| OnceLock::new()).collect();
let table_fields = table.fields.len();
let stripes = table.stripes.len();
let columns = (0..table.fields.len())
.map(|_| {
Mutex::new(Cached {
pages: (0..stripes).map(|_| None).collect(),
index: (0..stripes).map(|_| None).collect(),
seen: vec![false; stripes],
..Cached::default()
})
})
.collect::<Vec<_>>();
let cache = Shelf {
columns,
held: (0..table_fields).map(|_| AtomicUsize::new(0)).collect(),
kept: AtomicUsize::new(CACHED_STRIPES_PER_COLUMN),
};
let sieves: Vec<Vec<SieveSlot>> = (0..table.fields.len())
.map(|_| table.stripes.iter().map(|_| OnceLock::new()).collect())
.collect();
let part_ranges: Vec<Vec<RangeSlot>> = (0..table.fields.len())
.map(|_| table.stripes.iter().map(|_| OnceLock::new()).collect())
.collect();
Ok(Self {
file,
table: Arc::new(table),
dictionaries: Arc::new(dictionaries),
loading: Arc::new((0..table_fields).map(|_| Mutex::new(())).collect()),
frequency_values: Arc::new((0..table_fields).map(|_| OnceLock::new()).collect()),
frequency_summaries: Arc::new((0..table_fields).map(|_| OnceLock::new()).collect()),
opened: Arc::new(AtomicUsize::new(0)),
sieves: Arc::new(sieves),
part_ranges: Arc::new(part_ranges),
places: Arc::new(places),
cache: Arc::new(cache),
pool,
pages: Arc::new(AtomicUsize::new(0)),
indexes: Arc::new(AtomicUsize::new(0)),
size,
directory,
opening,
})
}
#[must_use]
pub fn reads(&self) -> Reads {
Reads {
opening: self.opening,
pages: self.pages.load(Atomic::Relaxed),
indexes: self.indexes.load(Atomic::Relaxed),
dictionaries: self.opened.load(Atomic::Relaxed),
}
}
#[must_use]
pub fn layout(&self) -> Layout {
let table = &self.table;
let stripes = table.stripes.as_slice();
let columns = table
.fields
.iter()
.enumerate()
.map(|(at, field)| ColumnLayout {
name: field.name.clone(),
kind: field.ty.to_string(),
pages: sum(stripes.iter().map(|stripe| span_bytes(&stripe.pages, at))),
memberships: sum(stripes.iter().map(|stripe| stripe.memberships.bytes(at))),
sieves: sum(stripes.iter().map(|stripe| stripe.sieves.bytes(at))),
part_ranges: sum(stripes.iter().map(|stripe| stripe.part_ranges.bytes(at))),
dictionary: dictionary_bytes(table, at),
})
.collect();
Layout {
file: self.size,
rows: table.rows,
stripes: stripes.len(),
parts: self.places.len(),
columns,
indexes: sum(stripes.iter().map(|stripe| u64::from(stripe.index.length))),
directory: self.directory,
header: HEADER,
}
}
pub fn stored(&self, column: usize) -> Result<Vec<StoredPart>> {
let field = self
.table
.fields
.get(column)
.ok_or_else(|| invalid("stored column index out of range"))?;
let mut stored = Vec::with_capacity(self.places.len());
let mut row = 0;
for (at, stripe) in self.table.stripes.iter().enumerate() {
let page = stripe.pages.get(column).ok_or_else(|| invalid("stripe page is missing"))?;
let index = read_index(&self.file, stripe, column)?;
let mut bytes = vec![0; page.length as usize];
read_at(&self.file, page.offset, &mut bytes)?;
let ranges = self.stripe_part_ranges(at, column);
for (part, &rows) in stripe.parts.iter().enumerate() {
let span = *index.get(part).ok_or_else(|| invalid("part index out of range"))?;
let held = part_bytes(&bytes, span)?;
let range = ranges.and_then(|held| held.get(part));
stored.push(StoredPart {
stripe: at,
part,
row,
rows: rows as usize,
encoding: page_encoding(&field.ty, rows as usize, held),
bytes: span.length as u64,
page: page.offset,
offset: span.start as u64,
low: range
.and_then(|range| range.low.clone())
.and_then(|bound| bound.into_value(&field.ty)),
high: range
.and_then(|range| range.high.clone())
.and_then(|bound| bound.into_value(&field.ty)),
nulls: range.map(|range| range.nulls),
});
row += rows as usize;
}
}
Ok(stored)
}
#[must_use]
pub fn parts(&self) -> usize {
self.places.len()
}
#[must_use]
pub fn stripe_parts(&self) -> Vec<std::ops::Range<usize>> {
let mut runs = Vec::with_capacity(self.table.stripes.len());
let mut start = 0;
for stripe in &self.table.stripes {
let end = start + stripe.parts.len();
runs.push(start..end);
start = end;
}
runs
}
#[must_use]
pub fn stripe_rows(&self, stripe: usize) -> usize {
self.table.stripes.get(stripe).map_or(0, |held| held.rows)
}
pub fn keep_stripes(&self, stripes: usize) {
self.cache.kept.fetch_max(stripes, Atomic::Relaxed);
}
#[must_use]
pub fn part_rows(&self, at: usize) -> usize {
self.places.get(at).map_or(0, |place| place.rows as usize)
}
#[must_use]
pub fn table(&self) -> &Table {
&self.table
}
pub fn top_frequencies(&self, column: usize, top: usize) -> Result<Option<Vec<(Value, u64)>>> {
let field = self
.table
.fields
.get(column)
.ok_or_else(|| invalid("frequency column index out of range"))?;
let Some(summary) = self.frequency_summary(column)? else {
return Ok(None);
};
if top == 0 || summary.entries.len() < top {
return Ok(None);
}
let boundary = summary.entries[top - 1].count;
if boundary <= summary.omitted_max {
return Ok(None);
}
self.decode_frequencies(column, &field.ty, &summary.entries).map(Some)
}
pub fn top_pair_frequencies(
&self,
first: usize,
second: usize,
_top: usize,
) -> Result<Option<PairFrequencyCounts>> {
if first >= self.table.fields.len() || second >= self.table.fields.len() {
return Err(invalid("pair frequency column index out of range"));
}
Ok(None)
}
pub fn exact_frequencies(&self, column: usize) -> Result<Option<Vec<(Value, u64)>>> {
let Some(prefix) = self.frequency_prefix(column)? else {
return Ok(None);
};
Ok((prefix.omitted_max == 0).then_some(prefix.entries))
}
pub fn frequency_prefix(&self, column: usize) -> Result<Option<FrequencyPrefix>> {
let field = self
.table
.fields
.get(column)
.ok_or_else(|| invalid("frequency column index out of range"))?;
let Some(summary) = self.frequency_summary(column)? else {
return Ok(None);
};
let entries = self.decode_frequencies(column, &field.ty, &summary.entries)?;
Ok(Some(FrequencyPrefix { entries, omitted_max: summary.omitted_max }))
}
fn frequency_summary(&self, column: usize) -> Result<Option<Cow<'_, FrequencySummary>>> {
Ok(match self.table.frequencies.get(column) {
None | Some(None) => None,
Some(Some(Frequencies::Held(summary))) => Some(Cow::Borrowed(summary)),
Some(Some(Frequencies::Stored { span, values })) => {
let slot = self
.frequency_summaries
.get(column)
.ok_or_else(|| invalid("frequency column index out of range"))?;
if let Some(summary) = slot.get() {
return Ok(Some(Cow::Borrowed(summary.as_ref())));
}
let field = self
.table
.fields
.get(column)
.ok_or_else(|| invalid("frequency column index out of range"))?;
let mut bytes = vec![0; span.length as usize];
read_at(&self.file, span.offset, &mut bytes)?;
let summary =
decode_summary(&mut Cursor::new(&bytes), field, self.table.rows, *values)?;
let summary = summary.ok_or_else(|| invalid("a stored synopsis is missing"))?;
let _ = slot.set(Arc::new(summary));
Some(Cow::Borrowed(slot.get().expect("the decoded summary was stored").as_ref()))
}
})
}
fn decode_frequencies(
&self,
column: usize,
ty: &LogicalType,
entries: &[FrequencyEntry],
) -> Result<Vec<(Value, u64)>> {
if let Some(values) = self.frequency_values.get(column).and_then(OnceLock::get) {
return Ok(values.as_ref().clone());
}
let values = self.decode_frequencies_once(column, ty, entries)?;
if let Some(slot) = self.frequency_values.get(column) {
let _ = slot.set(Arc::new(values.clone()));
}
Ok(values)
}
fn decode_frequencies_once(
&self,
column: usize,
ty: &LogicalType,
entries: &[FrequencyEntry],
) -> Result<Vec<(Value, u64)>> {
let stored_texts = self.table.frequency_texts.get(column).filter(|texts| !texts.is_empty());
if stored_texts.is_some_and(|texts| texts.len() != entries.len()) {
return Err(invalid("frequency text count differs from its synopsis"));
}
let dictionary =
if coded_type(ty) && stored_texts.is_none() { self.dictionary(column)? } else { None };
let mut codes = entries
.iter()
.filter_map(|entry| match entry.value {
FrequencyValue::Code(code) => Some(code as usize),
_ => None,
})
.collect::<Vec<_>>();
codes.sort_unstable();
codes.dedup();
let texts = match &dictionary {
Some(dictionary) if !codes.is_empty() => dictionary.try_values_visited(&codes)?,
_ => Vec::new(),
};
let mut out = Vec::with_capacity(entries.len());
for (entry_at, entry) in entries.iter().enumerate() {
let value = match entry.value {
FrequencyValue::Null => {
if stored_texts.and_then(|texts| texts[entry_at].as_ref()).is_some() {
return Err(invalid("a null frequency entry has text"));
}
Value::Null
}
FrequencyValue::Integer(value) => match *ty {
LogicalType::TinyInt => Value::TinyInt(
i8::try_from(value)
.map_err(|_| invalid("frequency TINYINT is out of range"))?,
),
LogicalType::UTinyInt => Value::UTinyInt(
u8::try_from(value)
.map_err(|_| invalid("frequency UTINYINT is out of range"))?,
),
LogicalType::USmallInt => Value::USmallInt(
u16::try_from(value)
.map_err(|_| invalid("frequency USMALLINT is out of range"))?,
),
LogicalType::UInteger => Value::UInteger(
u32::try_from(value)
.map_err(|_| invalid("frequency UINTEGER is out of range"))?,
),
LogicalType::UBigInt => Value::UBigInt(
u64::try_from(value)
.map_err(|_| invalid("frequency UBIGINT is out of range"))?,
),
LogicalType::SmallInt => Value::SmallInt(
i16::try_from(value)
.map_err(|_| invalid("frequency SMALLINT is out of range"))?,
),
LogicalType::Integer => Value::Integer(
i32::try_from(value)
.map_err(|_| invalid("frequency INTEGER is out of range"))?,
),
LogicalType::BigInt => Value::BigInt(
i64::try_from(value)
.map_err(|_| invalid("frequency BIGINT is out of range"))?,
),
LogicalType::Date => Value::Date(
i32::try_from(value)
.map_err(|_| invalid("frequency DATE is out of range"))?,
),
LogicalType::Timestamp => Value::Timestamp(
i64::try_from(value)
.map_err(|_| invalid("frequency TIMESTAMP is out of range"))?,
),
_ => return Err(invalid("integer frequency belongs to another type")),
},
FrequencyValue::Code(code) => {
if let Some(text) = stored_texts.and_then(|texts| texts[entry_at].as_ref()) {
if *ty == LogicalType::Blob {
Value::Blob(text.clone())
} else {
Value::Varchar(
String::from_utf8(text.clone())
.map_err(|_| invalid("frequency text is not UTF-8"))?,
)
}
} else {
if dictionary.is_none() {
return Err(invalid("frequency code has no dictionary or stored text"));
}
let at = codes
.binary_search(&(code as usize))
.map_err(|_| invalid("frequency code was not among the codes read"))?;
texts[at].clone()
}
}
};
out.push((value, entry.count));
}
Ok(out)
}
pub fn frequency_occurrences(&self, column: usize) -> Result<Option<FrequencyOccurrences>> {
let field = self
.table
.fields
.get(column)
.ok_or_else(|| invalid("frequency column index out of range"))?;
let Some(summary) = self.frequency_summary(column)? else {
return Ok(None);
};
if summary.ordinals.is_empty() {
return Ok(None);
}
let (anchors, anchor_indices) = if summary.ordinal_entries.len() == summary.ordinals.len() {
let entries = self.decode_frequencies(column, &field.ty, &summary.entries)?;
(entries.into_iter().map(|(value, _)| value).collect(), summary.ordinal_entries.clone())
} else {
(Vec::new(), Vec::new())
};
Ok(Some(FrequencyOccurrences {
omitted_max: summary.omitted_max,
ordinals: summary.ordinals.clone(),
anchors,
anchor_indices,
}))
}
pub fn distinct_values(&self, column: usize) -> Result<Option<u64>> {
self.table
.distincts
.get(column)
.copied()
.ok_or_else(|| invalid("distinct column index out of range"))
}
pub fn null_count(&self, column: usize) -> Result<u64> {
if column >= self.table.fields.len() {
return Err(invalid("null count column index out of range"));
}
let mut nulls = 0_u64;
for stripe in &self.table.stripes {
let range = stripe
.zone
.column(column)
.ok_or_else(|| invalid("stripe zone is narrower than the schema"))?;
nulls = nulls
.checked_add(range.nulls as u64)
.ok_or_else(|| invalid("null count overflow"))?;
}
Ok(nulls)
}
pub fn text_extremes(&self, column: usize) -> Result<Option<(Value, Value)>> {
if self.null_count(column)? > 0 || self.demoted(column) {
return Ok(None);
}
let Some(dictionary) = self.dictionary(column)? else { return Ok(None) };
let Some(ranks) = dictionary.ranks() else { return Ok(None) };
if ranks == 0 {
return Ok(None);
}
let low = text_at_rank(&dictionary, 0)?;
let high = text_at_rank(&dictionary, ranks - 1)?;
Ok(Some((low, high)))
}
pub fn exact_extremes(&self, column: usize) -> Result<Option<(Bound, Bound)>> {
if column >= self.table.fields.len() {
return Err(invalid("extremes column index out of range"));
}
let mut low: Option<Bound> = None;
let mut high: Option<Bound> = None;
for stripe in &self.table.stripes {
let range = stripe
.zone
.column(column)
.ok_or_else(|| invalid("stripe zone is narrower than the schema"))?;
if !range.exact {
return Ok(None);
}
let (Some(small), Some(large)) = (range.low.as_ref(), range.high.as_ref()) else {
if stripe.rows > range.nulls {
return Ok(None);
}
continue;
};
low = Some(low.map_or_else(|| small.clone(), |held| held.smaller(small.clone())));
high = Some(high.map_or_else(|| large.clone(), |held| held.larger(large.clone())));
}
Ok(low.zip(high))
}
pub fn exact_sum(&self, column: usize) -> Result<Option<(i128, u64)>> {
if column >= self.table.fields.len() {
return Err(invalid("sum column index out of range"));
}
let mut total = 0_i128;
let mut rows = 0_u64;
for stripe in &self.table.stripes {
let range = stripe
.zone
.column(column)
.ok_or_else(|| invalid("stripe zone is narrower than the schema"))?;
let Some(part) = range.sum else { return Ok(None) };
let Some(sum) = total.checked_add(part) else { return Ok(None) };
total = sum;
rows = rows.saturating_add(stripe.rows as u64 - range.nulls as u64);
}
Ok(Some((total, rows)))
}
pub fn host_groups(
&self,
column: usize,
_minimum_count: u64,
) -> Result<Option<Vec<host::HostEntry>>> {
if column >= self.table.fields.len() {
return Err(invalid("host group column index out of range"));
}
Ok(None)
}
#[must_use]
pub fn demoted(&self, column: usize) -> bool {
self.table.demoted.get(column).copied().unwrap_or(false)
}
fn dictionary(&self, column: usize) -> Result<Option<Arc<Vector>>> {
let Some(page) = self.table.dictionaries[column] else { return Ok(None) };
if let Some(dictionary) = self.dictionaries[column].get() {
return Ok(Some(Arc::clone(dictionary)));
}
let _queued = self.loading[column].lock().map_err(|_| invalid("a poisoned dictionary"))?;
if let Some(dictionary) = self.dictionaries[column].get() {
return Ok(Some(Arc::clone(dictionary)));
}
self.opened.fetch_add(1, Atomic::Relaxed);
let dictionary = Arc::new(open_global_dictionary(
Arc::clone(&self.file),
page,
&self.table.fields[column].ty,
TEXT_KEEP_BUDGET,
)?);
let _ = self.dictionaries[column].set(Arc::clone(&dictionary));
Ok(Some(dictionary))
}
pub fn extents(&self, of: &Section) -> Result<Vec<section::Extent>> {
if of.extent_bytes == 0 {
return Ok(Vec::new());
}
let mut bytes = vec![0; of.extent_bytes as usize];
read_at(&self.file, of.extent_page, &mut bytes)?;
if checksum(&bytes) != of.hash {
return Err(invalid("a section's extent table does not checksum"));
}
let extents = section::decode_extents(&bytes)?;
if extents.len() != of.extents as usize {
return Err(invalid("a section's extent table is not the length the entry says"));
}
Ok(extents)
}
pub fn extent(&self, of: §ion::Extent) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
self.extent_into(of, &mut bytes)?;
Ok(bytes)
}
fn extent_into(&self, of: §ion::Extent, bytes: &mut Vec<u8>) -> Result<()> {
let end = of
.offset
.checked_add(u64::from(of.length))
.ok_or_else(|| invalid("an extent overflows the file"))?;
if of.offset < HEADER || end > self.size {
return Err(invalid("an extent is outside the file"));
}
bytes.resize(of.length as usize, 0);
read_at(&self.file, of.offset, bytes)?;
if checksum(bytes) != of.hash {
return Err(invalid("an extent does not checksum"));
}
Ok(())
}
pub fn payload(&self, of: &Section) -> Result<Vec<u8>> {
let extents = self.extents(of)?;
let mut bytes =
Vec::with_capacity(sum(extents.iter().map(|one| u64::from(one.length))) as usize);
for one in &extents {
if one.first != bytes.len() as u64 {
return Err(invalid("a section's extents do not join up"));
}
bytes.extend_from_slice(&self.extent(one)?);
}
if !bytes.is_empty() && of.header_bytes as usize > bytes.len() {
return Err(invalid("a section's header is longer than its payload"));
}
Ok(bytes)
}
pub fn read(&self, part: usize, columns: &[usize]) -> Result<Chunk> {
self.read_impl(part, columns, true, None)
}
pub fn read_sparse(&self, part: usize, columns: &[usize]) -> Result<Chunk> {
self.read_impl(part, columns, false, None)
}
pub fn integer_tally(&self, part: usize, column: usize) -> Result<Option<Vec<(i64, u64)>>> {
let place = *self.places.get(part).ok_or_else(|| invalid("part index out of range"))?;
let field =
self.table.fields.get(column).ok_or_else(|| invalid("column index out of range"))?;
if !matches!(
field.ty,
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
) {
return Ok(None);
}
let stripe_index = place.stripe as usize;
let stripe = self
.table
.stripes
.get(stripe_index)
.ok_or_else(|| invalid("stripe index out of range"))?;
let page = stripe.pages.get(column).ok_or_else(|| invalid("stripe page is missing"))?;
let held = self.held(stripe_index, stripe, column, true)?;
let span = *held
.index
.get(place.part as usize)
.ok_or_else(|| invalid("part index out of range"))?;
let owned;
let bytes = match &held.page {
Some(page) => page.part(place.part as usize, span)?,
None => {
let offset = page
.offset
.checked_add(span.start as u64)
.ok_or_else(|| invalid("part range overflow"))?;
let mut bytes = vec![0; span.length];
read_at(&self.file, offset, &mut bytes)?;
verify_part(&bytes, span)?;
owned = bytes;
&owned
}
};
if bytes.first() != Some(&5) || bytes.get(1) != Some(&0) {
return Ok(None);
}
let (rows, counts) = integer::tally(&bytes[2..])?;
if rows != place.rows as usize {
return Err(invalid("encoded integer part holds the wrong number of rows"));
}
for &(value, _) in &counts {
let fits = match field.ty {
LogicalType::TinyInt => i8::try_from(value).is_ok(),
LogicalType::SmallInt => i16::try_from(value).is_ok(),
LogicalType::Integer => i32::try_from(value).is_ok(),
LogicalType::BigInt => true,
_ => false,
};
if !fits {
return Err(invalid("encoded integer value is outside its column type"));
}
}
Ok(Some(counts))
}
pub fn read_rows(
&self,
part: usize,
columns: &[usize],
positions: &[u32],
whole: bool,
) -> Result<Chunk> {
self.read_impl(part, columns, whole, Some(positions))
}
pub fn skips_codes(&self, part: usize, column: usize, candidates: &[u32]) -> Result<bool> {
if self.demoted(column) {
return Ok(false);
}
if candidates.is_empty() {
return Ok(true);
}
if candidates.windows(2).any(|pair| pair[0] >= pair[1]) {
return Err(Error::internal("native code candidates are not sorted and unique"));
}
let stripe = self.stripe_of(part)?;
let Some(page) = stripe.memberships.get(column) else {
return Ok(false);
};
let mut bytes = vec![0; page.length as usize];
read_at(&self.file, page.offset, &mut bytes)?;
if checksum(&bytes) != page.hash {
return Err(invalid("membership page checksum differs"));
}
let codes = decode_membership(&bytes)?;
let mut left = 0;
let mut right = 0;
while left < codes.len() && right < candidates.len() {
match codes[left].cmp(&candidates[right]) {
Ordering::Less => left += 1,
Ordering::Greater => right += 1,
Ordering::Equal => return Ok(false),
}
}
Ok(true)
}
fn stripe_of(&self, part: usize) -> Result<&Stripe> {
let place = self.places.get(part).ok_or_else(|| invalid("part index out of range"))?;
self.table
.stripes
.get(place.stripe as usize)
.ok_or_else(|| invalid("stripe index out of range"))
}
fn held(&self, at: usize, stripe: &Stripe, column: usize, whole: bool) -> Result<CachedColumn> {
let cache =
self.cache.columns.get(column).ok_or_else(|| invalid("column index out of range"))?;
let mut cached = cache.lock().map_err(|_| invalid("column page cache is poisoned"))?;
let known = cached.index.get(at).and_then(Clone::clone);
let page = cached.pages.get(at).and_then(Option::as_ref).map(|slot| {
slot.used.store(true, Atomic::Relaxed);
Arc::clone(&slot.page)
});
if let Some(index) = known.clone() {
if !whole || page.is_some() {
return Ok(CachedColumn { stripe: at, index, page });
}
}
if cached.loading.contains(&at) {
drop(cached);
if let Some(index) = known {
return Ok(CachedColumn { stripe: at, index, page: None });
}
let held = self.page_of(stripe, column, at, false, None)?;
let mut cached = cache.lock().map_err(|_| invalid("column page cache is poisoned"))?;
remember(&mut cached, &held);
return Ok(held);
}
cached.loading.push(at);
drop(cached);
let read = self.page_of(stripe, column, at, whole, known);
let mut cached = cache.lock().map_err(|_| invalid("column page cache is poisoned"))?;
if let Some(position) = cached.loading.iter().position(|loading| *loading == at) {
cached.loading.remove(position);
}
let held = read?;
let taken = remember(&mut cached, &held);
let first = taken.is_some()
&& cached.seen.get_mut(at).is_some_and(|seen| !std::mem::replace(seen, true));
if first {
let floor = self.cache.kept.load(Atomic::Relaxed).max(1);
cached.passing.push_back(at);
while cached.passing.len() > floor {
let Some(old) = cached.passing.pop_front() else { break };
if let Some(slot) = cached.pages.get_mut(old) {
*slot = None;
}
}
return Ok(held);
}
drop(cached);
if let Some((bytes, used)) = taken {
self.cache.held[column].fetch_add(1, Atomic::Relaxed);
self.pool.admit(Held {
shelf: Arc::downgrade(&self.cache),
column,
stripe: at,
bytes,
used,
});
}
Ok(held)
}
fn page_of(
&self,
stripe: &Stripe,
column: usize,
at: usize,
whole: bool,
known: Option<Arc<Vec<PartSpan>>>,
) -> Result<CachedColumn> {
let index = match known {
Some(index) => index,
None => {
self.indexes.fetch_add(1, Atomic::Relaxed);
Arc::new(read_index(&self.file, stripe, column)?)
}
};
let page = if whole {
self.pages.fetch_add(1, Atomic::Relaxed);
let span = stripe.pages.get(column).ok_or_else(|| invalid("stripe page is missing"))?;
let mut bytes = vec![0; span.length as usize];
read_at(&self.file, span.offset, &mut bytes)?;
let checked = index.iter().map(|_| AtomicBool::new(false)).collect();
Some(Arc::new(HeldPage { bytes, checked }))
} else {
None
};
Ok(CachedColumn { stripe: at, index, page })
}
fn read_impl(
&self,
at: usize,
columns: &[usize],
whole: bool,
positions: Option<&[u32]>,
) -> Result<Chunk> {
let place = *self.places.get(at).ok_or_else(|| invalid("part index out of range"))?;
let index = place.stripe as usize;
let stripe =
self.table.stripes.get(index).ok_or_else(|| invalid("stripe index out of range"))?;
let rows = place.rows as usize;
let mut picked = Vec::with_capacity(columns.len());
for &column in columns {
let field = self
.table
.fields
.get(column)
.ok_or_else(|| invalid("column index out of range"))?;
let page = stripe.pages.get(column).ok_or_else(|| invalid("stripe page is missing"))?;
let held = self.held(index, stripe, column, whole)?;
let span = *held
.index
.get(place.part as usize)
.ok_or_else(|| invalid("part index out of range"))?;
let owned;
let bytes = match &held.page {
Some(held) => held.part(place.part as usize, span),
None => {
let offset = page
.offset
.checked_add(span.start as u64)
.ok_or_else(|| invalid("part range overflow"))?;
let mut bytes = vec![0; span.length];
read_at(&self.file, offset, &mut bytes)?;
owned = bytes;
verify_part(&owned, span).map(|()| owned.as_slice())
}
}
.map_err(|error| {
invalid(&format!(
"{}, column {column} part {} of the page at {}",
error.message(),
place.part,
page.offset,
))
})?;
let dictionary = self.dictionary(column)?;
let mut vector = match positions {
None => decode(&field.ty, rows, bytes, dictionary)?,
Some(positions) => decode_at(&field.ty, rows, bytes, dictionary, positions)?,
};
if self.demoted(column) && vector.stable_dictionary_parts().is_some() {
vector = vector.flatten()?;
}
picked.push(vector.into_pages());
}
Chunk::with_rows(picked, positions.map_or(rows, <[u32]>::len))
}
#[must_use]
pub fn skips(&self, part: usize, probes: &[Probe]) -> bool {
let Some(place) = self.places.get(part).copied() else { return false };
let Some(stripe) = self.table.stripes.get(place.stripe as usize) else { return false };
if stripe.zone.skips(probes) {
return true;
}
probes.iter().any(|probe| self.outside(place, probe) || self.sifted(place, probe))
}
fn outside(&self, place: Place, probe: &Probe) -> bool {
match self.stripe_part_ranges(place.stripe as usize, probe.column) {
Some(ranges) => ranges
.get(place.part as usize)
.is_some_and(|range| range.excludes(probe.op, &probe.value)),
None => false,
}
}
fn stripe_part_ranges(&self, stripe: usize, column: usize) -> Option<&[Range]> {
let slot = self.part_ranges.get(column)?.get(stripe)?;
if let Some(held) = slot.get() {
return Some(held);
}
let page = self.table.stripes.get(stripe)?.part_ranges.get(column)?;
let mut bytes = vec![0; page.length as usize];
read_at(&self.file, page.offset, &mut bytes).ok()?;
if checksum(&bytes) != page.hash {
return None;
}
let ranges = Arc::new(decode_part_ranges(&bytes).ok()?);
let _ = slot.set(ranges);
slot.get().map(|held| held.as_slice())
}
#[must_use]
pub fn certain(&self, part: usize, probes: &[Probe]) -> bool {
let Some(place) = self.places.get(part).copied() else { return false };
let Some(stripe) = self.table.stripes.get(place.stripe as usize) else { return false };
if stripe.zone.certain(probes) {
return true;
}
probes
.iter()
.all(|probe| stripe.zone.certain(slice::from_ref(probe)) || self.inside(place, probe))
}
fn inside(&self, place: Place, probe: &Probe) -> bool {
match self.stripe_part_ranges(place.stripe as usize, probe.column) {
Some(ranges) => ranges
.get(place.part as usize)
.is_some_and(|range| range.certain(probe.op, &probe.value)),
None => false,
}
}
#[must_use]
pub fn stripe_skips(&self, stripe: usize, probes: &[Probe]) -> bool {
self.table.stripes.get(stripe).is_some_and(|held| held.zone.skips(probes))
}
fn sifted(&self, place: Place, probe: &Probe) -> bool {
if probe.op != Op::Equal {
return false;
}
match self.stripe_sieves(place.stripe as usize, probe.column) {
Some(sieves) => sieves
.get(place.part as usize)
.and_then(Option::as_ref)
.is_some_and(|sieve| sieve.excludes(&probe.value)),
None => false,
}
}
fn stripe_sieves(&self, stripe: usize, column: usize) -> Option<&[Option<Sieve>]> {
let slot = self.sieves.get(column)?.get(stripe)?;
if let Some(held) = slot.get() {
return Some(held);
}
let page = self.table.stripes.get(stripe)?.sieves.get(column)?;
let mut bytes = vec![0; page.length as usize];
read_at(&self.file, page.offset, &mut bytes).ok()?;
if checksum(&bytes) != page.hash {
return None;
}
let sieves = Arc::new(decode_sieves(&bytes).ok()?);
let _ = slot.set(sieves);
slot.get().map(|held| held.as_slice())
}
}
fn text_at_rank(dictionary: &Vector, rank: usize) -> Result<Value> {
let code = dictionary.code_at_rank(rank)? as usize;
if dictionary.logical_type() == &LogicalType::Blob {
let bytes = dictionary
.try_bytes_at(code)?
.ok_or_else(|| invalid("global dictionary order names a code it does not have"))?;
return Ok(Value::Blob(bytes.to_vec()));
}
let text = dictionary
.try_text_at(code)?
.ok_or_else(|| invalid("global dictionary order names a code it does not have"))?;
Ok(Value::Varchar(text.into()))
}
fn read_at<F: Positional + ?Sized>(file: &F, offset: u64, bytes: &mut [u8]) -> Result<()> {
file.fill_at(offset, bytes)
}
trait Positional {
fn fill_at(&self, offset: u64, bytes: &mut [u8]) -> Result<()>;
}
impl<T: Positional + ?Sized> Positional for &T {
fn fill_at(&self, offset: u64, bytes: &mut [u8]) -> Result<()> {
(**self).fill_at(offset, bytes)
}
}
impl<T: Positional + ?Sized> Positional for Arc<T> {
fn fill_at(&self, offset: u64, bytes: &mut [u8]) -> Result<()> {
(**self).fill_at(offset, bytes)
}
}
impl<T: Positional + ?Sized> Positional for Box<T> {
fn fill_at(&self, offset: u64, bytes: &mut [u8]) -> Result<()> {
(**self).fill_at(offset, bytes)
}
}
impl Positional for dyn rudb_io::File + '_ {
fn fill_at(&self, mut offset: u64, mut bytes: &mut [u8]) -> Result<()> {
while !bytes.is_empty() {
let read = self.read_at(offset, bytes)?;
if read == 0 {
return Err(invalid("column page ends before its declared length"));
}
offset += read as u64;
bytes = &mut bytes[read..];
}
Ok(())
}
}
impl Positional for File {
#[cfg(unix)]
fn fill_at(&self, mut offset: u64, mut bytes: &mut [u8]) -> Result<()> {
use std::os::unix::fs::FileExt;
while !bytes.is_empty() {
let read = self.read_at(bytes, offset).map_err(io)?;
if read == 0 {
return Err(invalid("column page ends before its declared length"));
}
offset += read as u64;
bytes = &mut bytes[read..];
}
Ok(())
}
#[cfg(windows)]
fn fill_at(&self, mut offset: u64, mut bytes: &mut [u8]) -> Result<()> {
use std::os::windows::fs::FileExt;
while !bytes.is_empty() {
let read = self.seek_read(bytes, offset).map_err(io)?;
if read == 0 {
return Err(invalid("column page ends before its declared length"));
}
offset += read as u64;
bytes = &mut bytes[read..];
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
fn fill_at(&self, offset: u64, bytes: &mut [u8]) -> Result<()> {
use std::io::{Read, Seek, SeekFrom};
let mut file = self.try_clone().map_err(io)?;
file.seek(SeekFrom::Start(offset)).map_err(io)?;
file.read_exact(bytes).map_err(io)
}
}
#[cfg(test)]
fn write_at(file: &File, offset: u64, bytes: &[u8]) -> Result<()> {
use std::io::{Seek, SeekFrom, Write};
let mut file = file;
file.seek(SeekFrom::Start(offset)).map_err(io)?;
file.write_all(bytes).map_err(io)
}
fn type_tag(ty: &LogicalType) -> Result<u8> {
match ty {
LogicalType::SmallInt => Ok(1),
LogicalType::Integer => Ok(2),
LogicalType::BigInt => Ok(3),
LogicalType::Varchar => Ok(4),
LogicalType::Date => Ok(5),
LogicalType::Timestamp => Ok(6),
LogicalType::Boolean => Ok(7),
LogicalType::TinyInt => Ok(8),
LogicalType::UTinyInt => Ok(9),
LogicalType::USmallInt => Ok(10),
LogicalType::UInteger => Ok(11),
LogicalType::UBigInt => Ok(12),
LogicalType::Decimal { .. } => Ok(13),
LogicalType::Float => Ok(14),
LogicalType::Double => Ok(15),
LogicalType::HugeInt => Ok(16),
LogicalType::UHugeInt => Ok(17),
LogicalType::Time => Ok(18),
LogicalType::TimeTz => Ok(19),
LogicalType::TimestampTz => Ok(20),
LogicalType::Interval => Ok(21),
LogicalType::Uuid => Ok(22),
LogicalType::Blob => Ok(23),
LogicalType::Bit => Ok(24),
LogicalType::TimestampS => Ok(25),
LogicalType::TimestampMs => Ok(26),
LogicalType::TimestampNs => Ok(27),
_ => Err(Error::not_implemented(format!("native storage for {ty}"))),
}
}
fn put_type(out: &mut Vec<u8>, ty: &LogicalType) -> Result<()> {
out.push(type_tag(ty)?);
if let LogicalType::Decimal { width, scale } = ty {
out.push(*width);
out.push(*scale);
}
Ok(())
}
fn read_type(cur: &mut Cursor<'_>) -> Result<LogicalType> {
let tag = cur.u8()?;
if tag == 13 {
let width = cur.u8()?;
let scale = cur.u8()?;
return LogicalType::decimal(width, scale)
.map_err(|_| invalid("decimal column width and scale are not a decimal"));
}
tag_type(tag)
}
fn tag_type(tag: u8) -> Result<LogicalType> {
match tag {
1 => Ok(LogicalType::SmallInt),
2 => Ok(LogicalType::Integer),
3 => Ok(LogicalType::BigInt),
4 => Ok(LogicalType::Varchar),
5 => Ok(LogicalType::Date),
6 => Ok(LogicalType::Timestamp),
7 => Ok(LogicalType::Boolean),
8 => Ok(LogicalType::TinyInt),
9 => Ok(LogicalType::UTinyInt),
10 => Ok(LogicalType::USmallInt),
11 => Ok(LogicalType::UInteger),
12 => Ok(LogicalType::UBigInt),
14 => Ok(LogicalType::Float),
15 => Ok(LogicalType::Double),
16 => Ok(LogicalType::HugeInt),
17 => Ok(LogicalType::UHugeInt),
18 => Ok(LogicalType::Time),
19 => Ok(LogicalType::TimeTz),
20 => Ok(LogicalType::TimestampTz),
21 => Ok(LogicalType::Interval),
22 => Ok(LogicalType::Uuid),
23 => Ok(LogicalType::Blob),
24 => Ok(LogicalType::Bit),
25 => Ok(LogicalType::TimestampS),
26 => Ok(LogicalType::TimestampMs),
27 => Ok(LogicalType::TimestampNs),
_ => Err(invalid("column type tag is unknown")),
}
}
fn put_u16(out: &mut Vec<u8>, value: u16) {
out.extend_from_slice(&value.to_le_bytes());
}
fn put_u32(out: &mut Vec<u8>, value: u32) {
out.extend_from_slice(&value.to_le_bytes());
}
fn put_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
fn put_var_u64(out: &mut Vec<u8>, mut value: u64) {
while value >= 0x80 {
out.push((value as u8 & 0x7f) | 0x80);
value >>= 7;
}
out.push(value as u8);
}
fn frequency_order(left: FrequencyValue, right: FrequencyValue) -> Ordering {
match (left, right) {
(FrequencyValue::Null, FrequencyValue::Null) => Ordering::Equal,
(FrequencyValue::Null, _) => Ordering::Less,
(_, FrequencyValue::Null) => Ordering::Greater,
(FrequencyValue::Integer(left), FrequencyValue::Integer(right)) => left.cmp(&right),
(FrequencyValue::Code(left), FrequencyValue::Code(right)) => left.cmp(&right),
(FrequencyValue::Integer(_), FrequencyValue::Code(_)) => Ordering::Less,
(FrequencyValue::Code(_), FrequencyValue::Integer(_)) => Ordering::Greater,
}
}
fn keep_most_frequent(entries: &mut Vec<FrequencyEntry>) -> u64 {
let order = |left: &FrequencyEntry, right: &FrequencyEntry| {
right.count.cmp(&left.count).then_with(|| frequency_order(left.value, right.value))
};
let omitted_max = if entries.len() > FREQUENCY_ENTRIES {
let (_, next, _) = entries.select_nth_unstable_by(FREQUENCY_ENTRIES, order);
let omitted_max = next.count;
entries.truncate(FREQUENCY_ENTRIES);
omitted_max
} else {
0
};
entries.sort_unstable_by(order);
omitted_max
}
fn code_frequency(
dictionary: &GlobalDictionary,
flat: &[u8],
bases: &[u64],
) -> Result<(FrequencySummary, Vec<Option<Vec<u8>>>)> {
let mut entries = dictionary
.counts
.iter()
.enumerate()
.filter(|(_, count)| **count != 0)
.map(|(code, &count)| FrequencyEntry { value: FrequencyValue::Code(code as u32), count })
.collect::<Vec<_>>();
if dictionary.nulls != 0 {
entries.push(FrequencyEntry { value: FrequencyValue::Null, count: dictionary.nulls });
}
let omitted_max = keep_most_frequent(&mut entries);
let mut spans = Vec::with_capacity(entries.len());
let mut text_bytes = 0_usize;
for entry in &entries {
let span = match entry.value {
FrequencyValue::Code(code) => {
let span = GlobalDictionary::value_span(&dictionary.ends, bases, code as usize);
let bytes = flat
.get(span.0..span.1)
.ok_or_else(|| invalid("a frequency code is outside its dictionary"))?;
text_bytes = text_bytes.saturating_add(bytes.len());
Some(span)
}
FrequencyValue::Null | FrequencyValue::Integer(_) => None,
};
spans.push(span);
}
let texts = if text_bytes > FREQUENCY_TEXT_BUDGET {
Vec::new()
} else {
spans.into_iter().map(|span| span.map(|(from, to)| flat[from..to].to_vec())).collect()
};
Ok((
FrequencySummary {
entries,
omitted_max,
ordinals: Vec::new(),
ordinal_entries: Vec::new(),
},
texts,
))
}
fn encode_directory(table: &Table) -> Result<Vec<u8>> {
let mut out = DIRECTORY.to_vec();
let name = table.name.as_bytes();
put_u16(&mut out, u16::try_from(name.len()).map_err(|_| invalid("table name too long"))?);
out.extend_from_slice(name);
put_u16(&mut out, u16::try_from(table.fields.len()).map_err(|_| invalid("too many columns"))?);
for field in &table.fields {
let name = field.name.as_bytes();
put_u16(&mut out, u16::try_from(name.len()).map_err(|_| invalid("column name too long"))?);
out.extend_from_slice(name);
put_type(&mut out, &field.ty)?;
out.push(u8::from(field.not_null));
}
for (field, dictionary) in table.fields.iter().zip(&table.dictionaries) {
match dictionary {
None => out.push(0),
Some(page) => {
out.push(dictionary_tag(&field.ty));
put_u64(&mut out, page.offset);
put_u32(&mut out, page.length);
put_u64(&mut out, page.hash);
}
}
}
for distinct in &table.distincts {
match distinct {
None => out.push(0),
Some(count) => {
out.push(1);
put_u64(&mut out, *count);
}
}
}
put_u64(&mut out, u64::try_from(table.rows).map_err(|_| invalid("row count overflow"))?);
put_u32(&mut out, u32::try_from(table.stripes.len()).map_err(|_| invalid("too many stripes"))?);
for stripe in &table.stripes {
put_u32(
&mut out,
u32::try_from(stripe.parts.len()).map_err(|_| invalid("too many parts in a stripe"))?,
);
for &rows in &stripe.parts {
put_u32(&mut out, rows);
}
put_u64(&mut out, stripe.index.offset);
put_u32(&mut out, stripe.index.length);
for page in &stripe.pages {
put_u64(&mut out, page.offset);
put_u32(&mut out, page.length);
}
for (column, ((field, dictionary), membership)) in
table.fields.iter().zip(&table.dictionaries).zip(stripe.memberships.slots()).enumerate()
{
if !coded_type(&field.ty) || dictionary.is_none() {
continue;
}
let page = match membership {
Some(page) => page,
None if table.demoted.get(column).copied().unwrap_or(false) => {
Page { offset: HEADER, length: 0, hash: 0 }
}
None => return Err(invalid("string page has no code membership index")),
};
put_u64(&mut out, page.offset);
put_u32(&mut out, page.length);
put_u64(&mut out, page.hash);
}
for sieve in stripe.sieves.slots() {
match sieve {
None => out.push(0),
Some(page) => {
out.push(1);
put_u64(&mut out, page.offset);
put_u32(&mut out, page.length);
put_u64(&mut out, page.hash);
}
}
}
for held in stripe.part_ranges.slots() {
match held {
None => out.push(0),
Some(page) => {
out.push(1);
put_u64(&mut out, page.offset);
put_u32(&mut out, page.length);
put_u64(&mut out, page.hash);
}
}
}
for range in stripe.zone.columns() {
put_bound(&mut out, range.low.as_ref())?;
put_bound(&mut out, range.high.as_ref())?;
put_u32(
&mut out,
u32::try_from(range.nulls).map_err(|_| invalid("null count overflow"))?,
);
out.push(u8::from(range.exact));
match range.sum {
None => out.push(0),
Some(total) => {
out.push(1);
out.extend_from_slice(&total.to_le_bytes());
}
}
}
}
out.extend_from_slice(FREQUENCIES);
put_u16(
&mut out,
u16::try_from(table.frequencies.len())
.map_err(|_| invalid("too many frequency columns"))?,
);
for summary in &table.frequencies {
let summary = match summary {
None => {
out.push(0);
continue;
}
Some(Frequencies::Held(summary)) => summary,
Some(Frequencies::Stored { .. }) => {
return Err(invalid("a synopsis left in the file cannot be written back"));
}
};
out.push(1);
put_u64(&mut out, summary.omitted_max);
put_u32(
&mut out,
u32::try_from(summary.entries.len())
.map_err(|_| invalid("too many frequency entries"))?,
);
for entry in &summary.entries {
match entry.value {
FrequencyValue::Null => out.push(0),
FrequencyValue::Integer(value) => {
out.push(1);
out.extend_from_slice(&value.to_le_bytes());
}
FrequencyValue::Code(value) => {
out.push(2);
put_u32(&mut out, value);
}
}
put_u64(&mut out, entry.count);
}
put_u32(
&mut out,
u32::try_from(summary.ordinals.len())
.map_err(|_| invalid("too many frequency ordinals"))?,
);
let mut previous = 0_u64;
for (at, &ordinal) in summary.ordinals.iter().enumerate() {
let delta = if at == 0 {
ordinal
} else {
ordinal
.checked_sub(previous)
.ok_or_else(|| invalid("frequency ordinals are not ordered"))?
};
if at != 0 && delta == 0 {
return Err(invalid("frequency ordinals are not unique"));
}
put_var_u64(&mut out, delta);
previous = ordinal;
}
if summary.ordinal_entries.len() != summary.ordinals.len() {
return Err(invalid("frequency ordinal values have a different length"));
}
for &entry in &summary.ordinal_entries {
if entry as usize >= summary.entries.len() {
return Err(invalid("frequency ordinal value is outside its entries"));
}
put_u16(&mut out, entry);
}
}
if !table.pair_frequencies.is_empty() {
out.extend_from_slice(PAIR_FREQUENCIES);
put_u16(
&mut out,
u16::try_from(table.pair_frequencies.len())
.map_err(|_| invalid("too many pair frequency summaries"))?,
);
for summary in &table.pair_frequencies {
put_u16(&mut out, summary.first);
put_u16(&mut out, summary.second);
put_u64(&mut out, summary.omitted_max);
put_u16(
&mut out,
u16::try_from(summary.entries.len())
.map_err(|_| invalid("too many pair frequency entries"))?,
);
for entry in &summary.entries {
put_u16(&mut out, entry.first_entry);
match entry.second {
None => out.push(0),
Some(code) => {
out.push(1);
put_u32(&mut out, code);
}
}
put_u64(&mut out, entry.count);
}
}
}
let text_columns = table.frequency_texts.iter().filter(|texts| !texts.is_empty()).count();
if text_columns != 0 {
out.extend_from_slice(FREQUENCY_TEXTS);
put_u16(
&mut out,
u16::try_from(text_columns)
.map_err(|_| invalid("too many string frequency columns"))?,
);
for (column, texts) in table.frequency_texts.iter().enumerate() {
if texts.is_empty() {
continue;
}
put_u16(
&mut out,
u16::try_from(column).map_err(|_| invalid("frequency text column overflows"))?,
);
put_u16(
&mut out,
u16::try_from(texts.len())
.map_err(|_| invalid("too many frequency text entries"))?,
);
for text in texts {
match text {
None => out.push(0),
Some(text) => {
out.push(1);
put_u32(
&mut out,
u32::try_from(text.len())
.map_err(|_| invalid("frequency text is too long"))?,
);
out.extend_from_slice(text);
}
}
}
}
}
if let Some(summary) = &table.host_groups {
out.extend_from_slice(HOST_GROUPS);
put_u16(
&mut out,
u16::try_from(summary.column).map_err(|_| invalid("host column overflows"))?,
);
put_u64(&mut out, summary.omitted_max);
put_u16(
&mut out,
u16::try_from(summary.entries.len()).map_err(|_| invalid("too many host groups"))?,
);
for entry in &summary.entries {
put_u32(
&mut out,
u32::try_from(entry.host.len()).map_err(|_| invalid("host name is too long"))?,
);
out.extend_from_slice(entry.host.as_bytes());
put_u64(&mut out, entry.count);
out.extend_from_slice(&entry.bytes_sum.to_le_bytes());
put_u32(
&mut out,
u32::try_from(entry.minimum.len())
.map_err(|_| invalid("host minimum is too long"))?,
);
out.extend_from_slice(entry.minimum.as_bytes());
}
}
if let Some(clustering) = &table.clustering {
out.extend_from_slice(CLUSTERING);
out.push(clustering.width().tag());
put_u16(
&mut out,
u16::try_from(clustering.columns().len())
.map_err(|_| invalid("too many clustering columns"))?,
);
for &column in clustering.columns() {
put_u16(
&mut out,
u16::try_from(column).map_err(|_| invalid("clustering column index overflow"))?,
);
}
}
let demoted = (0..table.fields.len())
.filter(|&column| table.demoted.get(column).copied().unwrap_or(false))
.collect::<Vec<_>>();
if !demoted.is_empty() {
out.extend_from_slice(DEMOTED);
put_u16(
&mut out,
u16::try_from(demoted.len()).map_err(|_| invalid("too many demoted columns"))?,
);
for column in demoted {
put_u16(
&mut out,
u16::try_from(column).map_err(|_| invalid("demoted column index overflow"))?,
);
}
}
out.extend_from_slice(SECTIONS);
put_u64(&mut out, table.generation);
put_u16(
&mut out,
u16::try_from(table.sections.len()).map_err(|_| invalid("too many sections"))?,
);
for held in &table.sections {
held.encode(&mut out)?;
}
if table.dictionary_payloads.iter().any(|&bytes| bytes != 0) {
out.extend_from_slice(DICTIONARY_PAYLOADS);
put_u16(
&mut out,
u16::try_from(table.fields.len()).map_err(|_| invalid("too many columns"))?,
);
for at in 0..table.fields.len() {
put_u64(&mut out, table.dictionary_payloads.get(at).copied().unwrap_or(0));
}
}
Ok(out)
}
fn signed_integer(ty: &LogicalType) -> bool {
matches!(
ty,
LogicalType::TinyInt | LogicalType::SmallInt | LogicalType::Integer | LogicalType::BigInt
)
}
fn integer_or_date(ty: &LogicalType) -> bool {
matches!(
ty,
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
| LogicalType::Date
)
}
fn table_integer_extremes(table: &Table) -> Vec<StoredIntegerExtremes> {
table
.fields
.iter()
.enumerate()
.map(|(column, field)| {
if !integer_or_date(&field.ty) {
return None;
}
let mut low: Option<i128> = None;
let mut high: Option<i128> = None;
for stripe in &table.stripes {
let range = stripe.zone.column(column)?;
if !range.exact {
return None;
}
match (range.low.as_ref(), range.high.as_ref()) {
(Some(Bound::Int(small)), Some(Bound::Int(large))) => {
low = Some(low.map_or(*small, |held| held.min(*small)));
high = Some(high.map_or(*large, |held| held.max(*large)));
}
(None, None) if stripe.rows == range.nulls => {}
_ => return None,
}
}
Some(low.zip(high))
})
.collect()
}
fn reader_integer_extremes(reader: &Reader) -> Result<Vec<StoredIntegerExtremes>> {
reader
.table
.fields
.iter()
.enumerate()
.map(|(column, field)| {
if !integer_or_date(&field.ty) {
return Ok(None);
}
match reader.exact_extremes(column)? {
Some((Bound::Int(low), Bound::Int(high))) => Ok(Some(Some((low, high)))),
None if reader.null_count(column)? == reader.table.rows as u64 => Ok(Some(None)),
_ => Ok(None),
}
})
.collect()
}
fn table_complete_numeric_frequencies(table: &Table) -> Vec<StoredNumericFrequencies> {
table
.fields
.iter()
.enumerate()
.map(|(column, field)| {
if !integer_or_date(&field.ty) {
return None;
}
let Some(Frequencies::Held(summary)) = table.frequencies.get(column)?.as_ref() else {
return None;
};
if summary.omitted_max != 0 || summary.entries.len() > MAX_CATALOG_FREQUENCIES {
return None;
}
let entries = summary
.entries
.iter()
.map(|entry| {
let value = match entry.value {
FrequencyValue::Null => None,
FrequencyValue::Integer(value) => Some(value),
FrequencyValue::Code(_) => return None,
};
Some((value, entry.count))
})
.collect::<Option<Vec<_>>>()?;
let rows = entries.iter().try_fold(0_u64, |sum, (_, count)| sum.checked_add(*count))?;
(rows == table.rows as u64).then_some(entries)
})
.collect()
}
fn integer_value(bits: u64, signed: bool) -> FrequencyValue {
if signed {
FrequencyValue::Integer(i128::from(bits as i64))
} else {
FrequencyValue::Integer(i128::from(bits))
}
}
fn frequency_bits(value: &Value) -> Option<u64> {
Some(match value {
Value::TinyInt(value) => i64::from(*value) as u64,
Value::SmallInt(value) => i64::from(*value) as u64,
Value::Integer(value) | Value::Date(value) => i64::from(*value) as u64,
Value::BigInt(value) | Value::Timestamp(value) => *value as u64,
Value::UTinyInt(value) => u64::from(*value),
Value::USmallInt(value) => u64::from(*value),
Value::UInteger(value) => u64::from(*value),
Value::UBigInt(value) => *value,
_ => return None,
})
}
fn numeric_frequency_value(value: &Value) -> Option<Option<i128>> {
Some(match value {
Value::Null => None,
Value::TinyInt(value) => Some(i128::from(*value)),
Value::SmallInt(value) => Some(i128::from(*value)),
Value::Integer(value) | Value::Date(value) => Some(i128::from(*value)),
Value::BigInt(value) => Some(i128::from(*value)),
Value::UTinyInt(value) => Some(i128::from(*value)),
Value::USmallInt(value) => Some(i128::from(*value)),
Value::UInteger(value) => Some(i128::from(*value)),
Value::UBigInt(value) => Some(i128::from(*value)),
_ => return None,
})
}
fn reader_complete_numeric_frequencies(reader: &Reader) -> Result<Vec<StoredNumericFrequencies>> {
reader
.table
.fields
.iter()
.enumerate()
.map(|(column, field)| {
if !integer_or_date(&field.ty) {
return Ok(None);
}
let Some(summary) = reader.frequency_summary(column)? else { return Ok(None) };
if summary.omitted_max != 0 || summary.entries.len() > MAX_CATALOG_FREQUENCIES {
return Ok(None);
}
let entries = reader.decode_frequencies(column, &field.ty, &summary.entries)?;
let Some(entries) = entries
.iter()
.map(|(value, count)| Some((numeric_frequency_value(value)?, *count)))
.collect::<Option<Vec<_>>>()
else {
return Ok(None);
};
let rows = entries.iter().try_fold(0_u64, |sum, (_, count)| sum.checked_add(*count));
Ok((rows == Some(reader.table.rows as u64)).then_some(entries))
})
.collect()
}
fn table_exact_sum(table: &Table, column: usize) -> Option<(i128, u64)> {
table.stripes.iter().try_fold((0_i128, 0_u64), |(sum, count), stripe| {
let range = stripe.zone.column(column)?;
let sum = sum.checked_add(range.sum?)?;
let nonnull = (stripe.rows as u64).checked_sub(range.nulls as u64)?;
Some((sum, count.checked_add(nonnull)?))
})
}
fn table_aggregate_sums(table: &Table) -> Vec<Option<(i128, u64)>> {
table
.fields
.iter()
.enumerate()
.map(|(column, field)| {
signed_integer(&field.ty).then(|| table_exact_sum(table, column)).flatten()
})
.collect()
}
fn reader_aggregate_sums(reader: &Reader) -> Result<Vec<Option<(i128, u64)>>> {
reader
.table
.fields
.iter()
.enumerate()
.map(
|(column, field)| {
if signed_integer(&field.ty) { reader.exact_sum(column) } else { Ok(None) }
},
)
.collect()
}
fn encode_catalog(entries: &[Entry], views: &[ViewEntry]) -> Result<Vec<u8>> {
let mut out = CATALOG.to_vec();
put_u32(&mut out, u32::try_from(entries.len()).map_err(|_| invalid("too many tables"))?);
for entry in entries {
let name = entry.name.as_bytes();
put_u16(&mut out, u16::try_from(name.len()).map_err(|_| invalid("table name too long"))?);
out.extend_from_slice(name);
put_u64(&mut out, u64::try_from(entry.rows).map_err(|_| invalid("row count overflow"))?);
put_u16(
&mut out,
u16::try_from(entry.fields.len()).map_err(|_| invalid("too many columns"))?,
);
for field in &entry.fields {
let name = field.name.as_bytes();
put_u16(
&mut out,
u16::try_from(name.len()).map_err(|_| invalid("column name too long"))?,
);
out.extend_from_slice(name);
put_type(&mut out, &field.ty)?;
out.push(u8::from(field.not_null));
}
put_u64(&mut out, entry.directory.offset);
put_u32(&mut out, entry.directory.length);
put_u64(&mut out, entry.directory.hash);
}
put_u32(&mut out, u32::try_from(views.len()).map_err(|_| invalid("too many views"))?);
for view in views {
let name = view.name.as_bytes();
put_u16(&mut out, u16::try_from(name.len()).map_err(|_| invalid("view name too long"))?);
out.extend_from_slice(name);
put_long_text(&mut out, &view.sql, "view body")?;
put_long_text(&mut out, &view.statement, "view statement")?;
put_u16(
&mut out,
u16::try_from(view.aliases.len()).map_err(|_| invalid("too many aliases"))?,
);
for alias in &view.aliases {
let alias = alias.as_bytes();
put_u16(
&mut out,
u16::try_from(alias.len()).map_err(|_| invalid("alias name too long"))?,
);
out.extend_from_slice(alias);
}
put_u16(
&mut out,
u16::try_from(view.columns.len()).map_err(|_| invalid("too many columns"))?,
);
for field in &view.columns {
let name = field.name.as_bytes();
put_u16(
&mut out,
u16::try_from(name.len()).map_err(|_| invalid("column name too long"))?,
);
out.extend_from_slice(name);
put_type(&mut out, &field.ty)?;
out.push(u8::from(field.not_null));
}
}
out.extend_from_slice(NONZERO_COUNTS);
for entry in entries {
if entry.nonzero.len() != entry.fields.len() {
return Err(invalid("nonzero count width differs from schema"));
}
for count in &entry.nonzero {
match count {
None => out.push(0),
Some(count) => {
out.push(1);
put_u64(&mut out, *count);
}
}
}
}
out.extend_from_slice(AGGREGATE_SUMS);
for entry in entries {
if entry.aggregates.len() != entry.fields.len() {
return Err(invalid("aggregate sum width differs from schema"));
}
for summary in &entry.aggregates {
match summary {
None => out.push(0),
Some((sum, count)) => {
out.push(1);
out.extend_from_slice(&sum.to_le_bytes());
put_u64(&mut out, *count);
}
}
}
}
out.extend_from_slice(DISTINCT_COUNTS);
for entry in entries {
if entry.distincts.len() != entry.fields.len() {
return Err(invalid("distinct count width differs from schema"));
}
for count in &entry.distincts {
match count {
None => out.push(0),
Some(count) => {
if *count > entry.rows as u64 {
return Err(invalid("distinct count exceeds table rows"));
}
out.push(1);
put_u64(&mut out, *count);
}
}
}
}
out.extend_from_slice(INTEGER_EXTREMES);
for entry in entries {
if entry.extremes.len() != entry.fields.len() {
return Err(invalid("integer extremes width differs from schema"));
}
for (field, extremes) in entry.fields.iter().zip(&entry.extremes) {
match extremes {
None => out.push(0),
Some(None) if integer_or_date(&field.ty) => out.push(1),
Some(Some((low, high))) if integer_or_date(&field.ty) && low <= high => {
out.push(2);
out.extend_from_slice(&low.to_le_bytes());
out.extend_from_slice(&high.to_le_bytes());
}
_ => return Err(invalid("integer extremes type or range differs")),
}
}
}
out.extend_from_slice(COMPLETE_FREQUENCIES);
for entry in entries {
if entry.frequencies.len() != entry.fields.len() {
return Err(invalid("numeric frequency width differs from schema"));
}
for (field, frequencies) in entry.fields.iter().zip(&entry.frequencies) {
match frequencies {
None => out.push(0),
Some(entries)
if integer_or_date(&field.ty) && entries.len() <= MAX_CATALOG_FREQUENCIES =>
{
let mut total = 0_u64;
for (at, (value, count)) in entries.iter().enumerate() {
if entries[..at].iter().any(|(held, _)| held == value) {
return Err(invalid("numeric frequency value repeats"));
}
total = total
.checked_add(*count)
.ok_or_else(|| invalid("numeric frequency count overflows"))?;
}
if total != entry.rows as u64 {
return Err(invalid("numeric frequencies do not cover table rows"));
}
out.push(1);
out.push(entries.len() as u8);
for (value, count) in entries {
match value {
None => out.push(0),
Some(value) => {
out.push(1);
out.extend_from_slice(&value.to_le_bytes());
}
}
put_u64(&mut out, *count);
}
}
_ => return Err(invalid("numeric frequency type or width differs")),
}
}
}
Ok(out)
}
fn put_long_text(out: &mut Vec<u8>, text: &str, what: &str) -> Result<()> {
let bytes = text.as_bytes();
put_u32(out, u32::try_from(bytes.len()).map_err(|_| invalid(&format!("{what} too long")))?);
out.extend_from_slice(bytes);
Ok(())
}
fn decode_catalog(bytes: &[u8], size: u64) -> Result<(Vec<Entry>, Vec<ViewEntry>)> {
let mut cur = Cursor::new(bytes);
if cur.take(8)? != CATALOG {
return Err(invalid("catalog magic differs"));
}
let count = cur.u32()? as usize;
let mut entries: Vec<Entry> = Vec::with_capacity(count.min(1024));
for _ in 0..count {
let name = cur.text()?;
let rows = usize::try_from(cur.u64()?).map_err(|_| invalid("row count does not fit"))?;
let width = cur.u16()? as usize;
let mut fields = Vec::with_capacity(width);
for _ in 0..width {
let name = cur.text()?;
let ty = read_type(&mut cur)?;
let not_null = match cur.u8()? {
0 => false,
1 => true,
_ => return Err(invalid("nullability flag differs")),
};
fields.push(Field { name, ty, not_null });
}
let directory = Page { offset: cur.u64()?, length: cur.u32()?, hash: cur.u64()? };
let end = directory
.offset
.checked_add(u64::from(directory.length))
.ok_or_else(|| invalid("table directory offset overflow"))?;
if directory.offset < HEADER
|| end > size
|| directory.length as usize > MAX_DIRECTORY
|| directory.length == 0
{
return Err(invalid("table directory range is outside the file"));
}
if entries.iter().any(|held| held.name == name) {
return Err(invalid("two tables in the catalog have the same name"));
}
let nonzero = vec![None; fields.len()];
let aggregates = vec![None; fields.len()];
let distincts = vec![None; fields.len()];
let extremes = vec![None; fields.len()];
let frequencies = vec![None; fields.len()];
entries.push(Entry {
name,
fields,
rows,
directory,
nonzero,
aggregates,
distincts,
extremes,
frequencies,
});
}
let count = if cur.done() { 0 } else { cur.u32()? as usize };
let mut views: Vec<ViewEntry> = Vec::with_capacity(count.min(1024));
for _ in 0..count {
let name = cur.text()?;
let sql = cur.long_text()?;
let statement = cur.long_text()?;
let width = cur.u16()? as usize;
let mut aliases = Vec::with_capacity(width);
for _ in 0..width {
aliases.push(cur.text()?);
}
let width = cur.u16()? as usize;
let mut columns = Vec::with_capacity(width);
for _ in 0..width {
let name = cur.text()?;
let ty = read_type(&mut cur)?;
let not_null = match cur.u8()? {
0 => false,
1 => true,
_ => return Err(invalid("nullability flag differs")),
};
columns.push(Field { name, ty, not_null });
}
if views.iter().any(|held| held.name == name) {
return Err(invalid("two views in the catalog have the same name"));
}
if entries.iter().any(|held| held.name == name) {
return Err(invalid("a table and a view in the catalog have the same name"));
}
views.push(ViewEntry { name, sql, statement, aliases, columns });
}
if !cur.done() {
if cur.take(8)? != NONZERO_COUNTS {
return Err(invalid("catalog extension magic differs"));
}
for entry in &mut entries {
for (field, count) in entry.fields.iter().zip(&mut entry.nonzero) {
*count = match cur.u8()? {
0 => None,
1 if matches!(
field.ty,
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
) =>
{
let value = cur.u64()?;
if value > entry.rows as u64 {
return Err(invalid("nonzero count exceeds rows"));
}
Some(value)
}
_ => return Err(invalid("nonzero count tag or column type differs")),
};
}
}
}
if !cur.done() {
if cur.take(8)? != AGGREGATE_SUMS {
return Err(invalid("aggregate catalog extension magic differs"));
}
for entry in &mut entries {
for (field, summary) in entry.fields.iter().zip(&mut entry.aggregates) {
*summary = match cur.u8()? {
0 => None,
1 if signed_integer(&field.ty) => {
let sum = i128::from_le_bytes(
cur.take(16)?
.try_into()
.map_err(|_| invalid("aggregate sum is truncated"))?,
);
let count = cur.u64()?;
if count > entry.rows as u64 {
return Err(invalid("aggregate count exceeds table rows"));
}
Some((sum, count))
}
_ => return Err(invalid("aggregate sum tag or column type differs")),
};
}
}
}
if !cur.done() {
if cur.take(8)? != DISTINCT_COUNTS {
return Err(invalid("distinct catalog extension magic differs"));
}
for entry in &mut entries {
for count in &mut entry.distincts {
*count = match cur.u8()? {
0 => None,
1 => {
let value = cur.u64()?;
if value > entry.rows as u64 {
return Err(invalid("distinct count exceeds table rows"));
}
Some(value)
}
_ => return Err(invalid("distinct count tag differs")),
};
}
}
}
if !cur.done() {
if cur.take(8)? != INTEGER_EXTREMES {
return Err(invalid("integer extremes catalog extension magic differs"));
}
for entry in &mut entries {
for (field, extremes) in entry.fields.iter().zip(&mut entry.extremes) {
*extremes = match cur.u8()? {
0 => None,
1 if integer_or_date(&field.ty) => Some(None),
2 if integer_or_date(&field.ty) => {
let low = i128::from_le_bytes(
cur.take(16)?
.try_into()
.map_err(|_| invalid("minimum is truncated"))?,
);
let high = i128::from_le_bytes(
cur.take(16)?
.try_into()
.map_err(|_| invalid("maximum is truncated"))?,
);
if low > high {
return Err(invalid("integer extremes are reversed"));
}
Some(Some((low, high)))
}
_ => return Err(invalid("integer extremes tag or type differs")),
};
}
}
}
if !cur.done() {
if cur.take(8)? != COMPLETE_FREQUENCIES {
return Err(invalid("numeric frequency catalog extension magic differs"));
}
for entry in &mut entries {
for (field, frequencies) in entry.fields.iter().zip(&mut entry.frequencies) {
*frequencies = match cur.u8()? {
0 => None,
1 if integer_or_date(&field.ty) => {
let len = cur.u8()? as usize;
if len > MAX_CATALOG_FREQUENCIES {
return Err(invalid("too many catalog numeric frequencies"));
}
let mut values = Vec::with_capacity(len);
let mut total = 0_u64;
for _ in 0..len {
let value = match cur.u8()? {
0 => None,
1 => Some(i128::from_le_bytes(cur.take(16)?.try_into().map_err(
|_| invalid("numeric frequency value is truncated"),
)?)),
_ => return Err(invalid("numeric frequency value tag differs")),
};
if values.iter().any(|(held, _)| *held == value) {
return Err(invalid("numeric frequency value repeats"));
}
let count = cur.u64()?;
total = total
.checked_add(count)
.ok_or_else(|| invalid("numeric frequency count overflows"))?;
values.push((value, count));
}
if total != entry.rows as u64 {
return Err(invalid("numeric frequencies do not cover table rows"));
}
Some(values)
}
_ => return Err(invalid("numeric frequency tag or type differs")),
};
}
}
}
if !cur.done() {
return Err(invalid("catalog has trailing bytes"));
}
Ok((entries, views))
}
struct Cursor<'a> {
bytes: &'a [u8],
at: usize,
window: Option<Window<'a>>,
}
struct Window<'a> {
file: &'a File,
offset: u64,
length: usize,
start: usize,
held: Vec<u8>,
size: usize,
}
const DIRECTORY_WINDOW: usize = 64 << 10;
impl<'a> Cursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, at: 0, window: None }
}
fn over(file: &'a File, offset: u64, length: usize) -> Self {
let window =
Window { file, offset, length, start: 0, held: Vec::new(), size: DIRECTORY_WINDOW };
Self { bytes: &[], at: 0, window: Some(window) }
}
fn len(&self) -> usize {
self.window.as_ref().map_or(self.bytes.len(), |window| window.length)
}
fn ensure(&mut self, len: usize) -> Result<()> {
let end = self.at.checked_add(len).ok_or_else(|| invalid("directory offset overflow"))?;
if end > self.len() {
return Err(invalid("directory is truncated"));
}
let Some(window) = &mut self.window else { return Ok(()) };
if self.at < window.start || end > window.start + window.held.len() {
let want = len.max(window.size).min(window.length - self.at);
window.start = self.at;
window.held.resize(want, 0);
read_at(window.file, window.offset + self.at as u64, &mut window.held)?;
}
Ok(())
}
fn held(&self, at: usize, len: usize) -> &[u8] {
match &self.window {
Some(window) => &window.held[at - window.start..at - window.start + len],
None => &self.bytes[at..at + len],
}
}
#[inline]
fn peek(&mut self, len: usize) -> Result<&[u8]> {
if self.window.is_none() {
let bytes = self.bytes;
return Ok(&bytes[self.at..self.end(len)?]);
}
self.ensure(len)?;
Ok(self.held(self.at, len))
}
#[inline]
fn take(&mut self, len: usize) -> Result<&[u8]> {
if self.window.is_none() {
let bytes = self.bytes;
let (at, end) = (self.at, self.end(len)?);
self.at = end;
return Ok(&bytes[at..end]);
}
self.take_windowed(len)
}
fn skip(&mut self, len: usize) -> Result<()> {
let end = self.at.checked_add(len).ok_or_else(|| invalid("directory offset overflow"))?;
if end > self.len() {
return Err(invalid("directory is truncated"));
}
self.at = end;
Ok(())
}
fn skip_bound(&mut self) -> Result<()> {
match self.u8()? {
0 => Ok(()),
1 => self.skip(16),
2 => self.skip(8),
3 => {
let length = self.u32()? as usize;
self.skip(length)
}
4 => self.skip(17),
_ => Err(invalid("a stored bound has an unknown tag")),
}
}
#[inline]
fn end(&self, len: usize) -> Result<usize> {
let end = self.at.checked_add(len).ok_or_else(|| invalid("directory offset overflow"))?;
if end > self.bytes.len() {
return Err(invalid("directory is truncated"));
}
Ok(end)
}
#[inline(never)]
fn take_windowed(&mut self, len: usize) -> Result<&[u8]> {
self.ensure(len)?;
self.at += len;
Ok(self.held(self.at - len, len))
}
#[inline]
fn u8(&mut self) -> Result<u8> {
Ok(self.take(1)?[0])
}
#[inline]
fn u16(&mut self) -> Result<u16> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().expect("two bytes")))
}
#[inline]
fn u32(&mut self) -> Result<u32> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().expect("four bytes")))
}
#[inline]
fn u64(&mut self) -> Result<u64> {
Ok(u64::from_le_bytes(self.take(8)?.try_into().expect("eight bytes")))
}
fn var_u64(&mut self) -> Result<u64> {
let mut value = 0_u64;
for shift in (0..=63).step_by(7) {
let byte = self.u8()?;
let part = u64::from(byte & 0x7f);
if shift == 63 && part > 1 {
return Err(invalid("frequency ordinal varint overflows"));
}
value |= part << shift;
if byte & 0x80 == 0 {
return Ok(value);
}
}
Err(invalid("frequency ordinal varint is too long"))
}
fn bound(&mut self) -> Result<Option<Bound>> {
let rest = self.len().saturating_sub(self.at);
let mut want = 32;
loop {
let offered = self.peek(want.min(rest))?;
let mut used = 0;
match bounds::get(offered, &mut used) {
Ok(bound) => {
self.at += used;
return Ok(bound);
}
Err(_) if want < rest => want *= 2,
Err(error) => return Err(error),
}
}
}
fn text(&mut self) -> Result<String> {
let len = self.u16()? as usize;
String::from_utf8(self.take(len)?.to_vec()).map_err(|_| invalid("name is not UTF-8"))
}
fn done(&self) -> bool {
self.at >= self.len()
}
fn long_text(&mut self) -> Result<String> {
let len = self.u32()? as usize;
String::from_utf8(self.take(len)?.to_vec()).map_err(|_| invalid("text is not UTF-8"))
}
}
fn decode_summary(
cur: &mut Cursor<'_>,
field: &Field,
rows: usize,
values: bool,
) -> Result<Option<FrequencySummary>> {
Ok(match cur.u8()? {
0 => None,
1 => {
let omitted_max = cur.u64()?;
let count = cur.u32()? as usize;
if count > FREQUENCY_ENTRIES {
return Err(invalid("frequency entry count exceeds its bound"));
}
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
let value = match cur.u8()? {
0 => FrequencyValue::Null,
1 => FrequencyValue::Integer(i128::from_le_bytes(
cur.take(16)?.try_into().expect("sixteen bytes"),
)),
2 => FrequencyValue::Code(cur.u32()?),
_ => return Err(invalid("frequency value tag differs")),
};
let valid = matches!(
(&field.ty, value),
(_, FrequencyValue::Null)
| (LogicalType::Varchar | LogicalType::Blob, FrequencyValue::Code(_))
| (
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
| LogicalType::Date
| LogicalType::Timestamp,
FrequencyValue::Integer(_),
)
);
if !valid {
return Err(invalid("frequency value does not match its column"));
}
let count = cur.u64()?;
if count == 0 || count > rows as u64 {
return Err(invalid("frequency count is outside the table"));
}
entries.push(FrequencyEntry { value, count });
}
if entries.windows(2).any(|pair| pair[0].count < pair[1].count) {
return Err(invalid("frequency entries are not descending"));
}
let ordinals = {
let ordinal_count = cur.u32()? as usize;
if ordinal_count > FREQUENCY_ORDINALS || ordinal_count > rows {
return Err(invalid("frequency ordinal count exceeds its bound"));
}
let mut ordinals = Vec::with_capacity(ordinal_count);
let mut previous = 0_u64;
for at in 0..ordinal_count {
let delta = cur.var_u64()?;
if at != 0 && delta == 0 {
return Err(invalid("frequency ordinals are not increasing"));
}
let ordinal = if at == 0 {
delta
} else {
previous
.checked_add(delta)
.ok_or_else(|| invalid("frequency ordinal overflows"))?
};
if ordinal >= rows as u64 {
return Err(invalid("frequency ordinal is outside the table"));
}
ordinals.push(ordinal);
previous = ordinal;
}
ordinals
};
let ordinal_entries = if values {
let mut ordinal_entries = Vec::with_capacity(ordinals.len());
for _ in 0..ordinals.len() {
let entry = cur.u16()?;
if entry as usize >= entries.len() {
return Err(invalid("frequency ordinal value is outside its entries"));
}
ordinal_entries.push(entry);
}
ordinal_entries
} else {
Vec::new()
};
Some(FrequencySummary { entries, omitted_max, ordinals, ordinal_entries })
}
_ => return Err(invalid("frequency summary tag differs")),
})
}
fn skip_summary(cur: &mut Cursor<'_>, values: bool, rows: usize) -> Result<()> {
match cur.u8()? {
0 => Ok(()),
1 => {
cur.skip(8)?;
let entries = cur.u32()? as usize;
if entries > FREQUENCY_ENTRIES {
return Err(invalid("frequency entry count exceeds its bound"));
}
for _ in 0..entries {
match cur.u8()? {
0 => {}
1 => cur.skip(16)?,
2 => cur.skip(4)?,
_ => return Err(invalid("frequency value tag differs")),
}
cur.skip(8)?;
}
let ordinals = cur.u32()? as usize;
if ordinals > FREQUENCY_ORDINALS || ordinals > rows {
return Err(invalid("frequency ordinal count exceeds its bound"));
}
for _ in 0..ordinals {
cur.var_u64()?;
}
if values {
cur.skip(ordinals * 2)?;
}
Ok(())
}
_ => Err(invalid("frequency summary tag differs")),
}
}
fn quick_nonzero(
mut cur: Cursor<'_>,
name: &str,
fields: &[Field],
rows: usize,
wanted: usize,
) -> Result<Option<u64>> {
if cur.take(8)? != DIRECTORY || cur.text()? != name {
return Err(invalid("table directory differs from the catalog"));
}
let width = cur.u16()? as usize;
if width != fields.len() {
return Err(invalid("table directory width differs from the catalog"));
}
for field in fields {
let stored =
Field { name: cur.text()?, ty: read_type(&mut cur)?, not_null: cur.u8()? != 0 };
if &stored != field {
return Err(invalid("table directory schema differs from the catalog"));
}
}
let mut dictionaries = Vec::with_capacity(width);
for field in fields {
let held = match cur.u8()? {
0 => false,
tag if coded_type(&field.ty) && tag == dictionary_tag(&field.ty) => {
cur.skip(20)?;
true
}
_ => return Err(invalid("dictionary page tag differs")),
};
dictionaries.push(held);
}
for _ in 0..width {
match cur.u8()? {
0 => {}
1 => cur.skip(8)?,
_ => return Err(invalid("distinct count tag differs")),
}
}
if cur.u64()? != rows as u64 {
return Err(invalid("table row count differs from the catalog"));
}
let stripes = cur.u32()? as usize;
let mut total = 0_usize;
let mut nulls = 0_u64;
for _ in 0..stripes {
let parts = cur.u32()? as usize;
if parts == 0 || parts > STRIPE_PARTS {
return Err(invalid("stripe part count is outside its bound"));
}
let mut stripe_rows = 0_usize;
for _ in 0..parts {
stripe_rows = stripe_rows
.checked_add(cur.u32()? as usize)
.ok_or_else(|| invalid("stripe row count overflow"))?;
}
total =
total.checked_add(stripe_rows).ok_or_else(|| invalid("stripe row count overflow"))?;
cur.skip(12 + width * 12)?;
for (field, held) in fields.iter().zip(&dictionaries) {
if coded_type(&field.ty) && *held {
cur.skip(20)?;
}
}
for _ in 0..width * 2 {
match cur.u8()? {
0 => {}
1 => cur.skip(20)?,
_ => return Err(invalid("stripe page tag differs")),
}
}
for column in 0..width {
cur.skip_bound()?;
cur.skip_bound()?;
let count = cur.u32()? as u64;
if count > stripe_rows as u64 {
return Err(invalid("null count exceeds stripe rows"));
}
if column == wanted {
nulls = nulls.checked_add(count).ok_or_else(|| invalid("null count overflow"))?;
}
cur.skip(1)?;
match cur.u8()? {
0 => {}
1 => cur.skip(16)?,
_ => return Err(invalid("a stripe sum has an unknown tag")),
}
}
}
if total != rows {
return Err(invalid("table row count differs from stripes"));
}
if cur.done() {
return Ok(None);
}
let magic = cur.take(8)?;
let values = magic == FREQUENCIES;
if !values && magic != FREQUENCIES_V2 {
return Err(invalid("directory extension magic differs"));
}
if cur.u16()? as usize != width {
return Err(invalid("frequency column count differs"));
}
for _ in 0..wanted {
skip_summary(&mut cur, values, rows)?;
}
let Some(summary) = decode_summary(&mut cur, &fields[wanted], rows, values)? else {
return Ok(None);
};
let zero = summary
.entries
.iter()
.find(|entry| entry.value == FrequencyValue::Integer(0))
.map(|entry| entry.count)
.or_else(|| (summary.omitted_max == 0).then_some(0));
Ok(zero.and_then(|zero| (rows as u64).checked_sub(nulls)?.checked_sub(zero)))
}
fn quick_integer_fold(
file: &File,
mut cur: Cursor<'_>,
entry: &Entry,
size: u64,
wanted: usize,
emit: &mut impl FnMut(i64, u64) -> Result<()>,
) -> Result<()> {
let name = &entry.name;
let fields = &entry.fields;
let rows = entry.rows;
if cur.take(8)? != DIRECTORY || cur.text()? != name.as_str() {
return Err(invalid("table directory differs from the catalog"));
}
let width = cur.u16()? as usize;
if width != fields.len() {
return Err(invalid("table directory width differs from the catalog"));
}
for field in fields {
let stored =
Field { name: cur.text()?, ty: read_type(&mut cur)?, not_null: cur.u8()? != 0 };
if &stored != field {
return Err(invalid("table directory schema differs from the catalog"));
}
}
let mut dictionaries = Vec::with_capacity(width);
for field in fields {
dictionaries.push(match cur.u8()? {
0 => false,
tag if coded_type(&field.ty) && tag == dictionary_tag(&field.ty) => {
cur.skip(20)?;
true
}
_ => return Err(invalid("dictionary page tag differs")),
});
}
for _ in 0..width {
match cur.u8()? {
0 => {}
1 => cur.skip(8)?,
_ => return Err(invalid("distinct count tag differs")),
}
}
if cur.u64()? != rows as u64 {
return Err(invalid("table row count differs from the catalog"));
}
let stripes = cur.u32()? as usize;
let mut total = 0_usize;
let mut bytes = Vec::new();
for _ in 0..stripes {
let parts = cur.u32()? as usize;
if parts == 0 || parts > STRIPE_PARTS {
return Err(invalid("stripe part count is outside its bound"));
}
let mut part_rows = Vec::with_capacity(parts);
for _ in 0..parts {
let count = cur.u32()? as usize;
if count == 0 {
return Err(invalid("empty part"));
}
total = total.checked_add(count).ok_or_else(|| invalid("stripe row count overflow"))?;
part_rows.push(count);
}
let index = Span { offset: cur.u64()?, length: cur.u32()? };
let section = index_section(parts)?;
let index_length =
section.checked_mul(width).ok_or_else(|| invalid("index page length overflow"))?;
if index.offset < HEADER
|| index.offset.checked_add(u64::from(index.length)).is_none_or(|end| end > size)
|| index.length as usize != index_length
{
return Err(invalid("index page range is outside the file"));
}
cur.skip(wanted * 12)?;
let page = Span { offset: cur.u64()?, length: cur.u32()? };
if page.offset < HEADER
|| page.offset.checked_add(u64::from(page.length)).is_none_or(|end| end > size)
|| page.length as usize > MAX_PAGE
{
return Err(invalid("column page range is outside the file"));
}
cur.skip((width - wanted - 1) * 12)?;
for (field, held) in fields.iter().zip(&dictionaries) {
if coded_type(&field.ty) && *held {
cur.skip(20)?;
}
}
for _ in 0..width * 2 {
match cur.u8()? {
0 => {}
1 => cur.skip(20)?,
_ => return Err(invalid("stripe page tag differs")),
}
}
for _ in 0..width {
cur.skip_bound()?;
cur.skip_bound()?;
cur.skip(5)?;
match cur.u8()? {
0 => {}
1 => cur.skip(16)?,
_ => return Err(invalid("a stripe sum has an unknown tag")),
}
}
let spans = read_index_span(file, index, page, parts, wanted)?;
for (span, expected_rows) in spans.into_iter().zip(part_rows) {
bytes.resize(span.length, 0);
let at = page
.offset
.checked_add(span.start as u64)
.ok_or_else(|| invalid("part range overflow"))?;
read_at(file, at, &mut bytes)?;
if checksum(&bytes) != span.hash {
return Err(invalid("integer part checksum differs"));
}
if bytes.first() == Some(&5) && bytes.get(1) == Some(&0) {
let decoded_rows = integer::fold(&bytes[2..], |value, count| {
check_integer_tally_value(value, &fields[wanted].ty)?;
emit(value, count)
})?;
if decoded_rows != expected_rows {
return Err(invalid("encoded integer part holds the wrong number of rows"));
}
} else {
let column = decode(&fields[wanted].ty, expected_rows, &bytes, None)?;
if let Some(packed) = column.packed_parts() {
let validity = column.validity();
let all_valid = column.none_null();
let base = packed.base();
let mut codes = [0_u64; 64];
for from in (0..expected_rows).step_by(codes.len()) {
let count = (expected_rows - from).min(codes.len());
packed.unpack(from, &mut codes[..count]);
for (offset, &code) in codes[..count].iter().enumerate() {
if all_valid || validity.is_valid(from + offset) {
emit((base + i128::from(code)) as i64, 1)?;
}
}
}
continue;
}
let column = column.into_flat()?;
let validity = column.validity();
macro_rules! count_decoded {
($values:expr) => {
for (row, &value) in $values.as_slice().iter().enumerate() {
if validity.is_valid(row) {
emit(i64::from(value), 1)?;
}
}
};
}
match column.data() {
Some(Data::Int8(values)) => count_decoded!(values),
Some(Data::Int16(values)) => count_decoded!(values),
Some(Data::Int32(values)) => count_decoded!(values),
Some(Data::Int64(values)) => count_decoded!(values),
_ => return Err(invalid("decoded integer part has the wrong type")),
}
}
}
}
if total != rows {
return Err(invalid("table row count differs from stripes"));
}
Ok(())
}
fn check_integer_tally_value(value: i64, ty: &LogicalType) -> Result<()> {
let fits = match ty {
LogicalType::TinyInt => i8::try_from(value).is_ok(),
LogicalType::SmallInt => i16::try_from(value).is_ok(),
LogicalType::Integer => i32::try_from(value).is_ok(),
LogicalType::BigInt => true,
_ => false,
};
if fits { Ok(()) } else { Err(invalid("encoded integer value is outside its column type")) }
}
fn decode_directory(bytes: &[u8], size: u64) -> Result<Table> {
read_directory(Cursor::new(bytes), size, None)
}
fn read_directory(mut cur: Cursor<'_>, size: u64, stored_at: Option<u64>) -> Result<Table> {
if cur.take(8)? != DIRECTORY {
return Err(invalid("directory magic differs"));
}
let name = cur.text()?;
let width = cur.u16()? as usize;
let mut fields = Vec::with_capacity(width);
for _ in 0..width {
let name = cur.text()?;
let ty = read_type(&mut cur)?;
let not_null = match cur.u8()? {
0 => false,
1 => true,
_ => return Err(invalid("nullability flag differs")),
};
fields.push(Field { name, ty, not_null });
}
let mut dictionaries = Vec::with_capacity(width);
for field in &fields {
dictionaries.push(match cur.u8()? {
0 => None,
tag if tag == dictionary_tag(&field.ty) => {
let page = Page { offset: cur.u64()?, length: cur.u32()?, hash: cur.u64()? };
let end = page
.offset
.checked_add(u64::from(page.length))
.ok_or_else(|| invalid("dictionary page offset overflow"))?;
if page.offset < HEADER || end > size {
return Err(invalid("dictionary page range is outside the file"));
}
Some(page)
}
_ => return Err(invalid("dictionary page tag differs")),
});
}
let mut distincts = Vec::with_capacity(width);
for _ in 0..width {
distincts.push(match cur.u8()? {
0 => None,
1 => Some(cur.u64()?),
_ => return Err(invalid("distinct count tag differs")),
});
}
let rows = usize::try_from(cur.u64()?).map_err(|_| invalid("row count does not fit"))?;
let count = cur.u32()? as usize;
let mut stripes = Vec::with_capacity(count);
let mut total = 0_usize;
for _ in 0..count {
let count = cur.u32()? as usize;
if count == 0 || count > STRIPE_PARTS {
return Err(invalid("stripe part count is outside its bound"));
}
let mut parts = Vec::with_capacity(count);
let mut stripe_rows = 0_usize;
for _ in 0..count {
let rows = cur.u32()?;
if rows == 0 {
return Err(invalid("empty part"));
}
parts.push(rows);
stripe_rows = stripe_rows
.checked_add(rows as usize)
.ok_or_else(|| invalid("stripe row count overflow"))?;
}
total =
total.checked_add(stripe_rows).ok_or_else(|| invalid("stripe row count overflow"))?;
let index = Span { offset: cur.u64()?, length: cur.u32()? };
let section = index_section(count)?;
let wanted = section
.checked_mul(width)
.and_then(|bytes| u32::try_from(bytes).ok())
.ok_or_else(|| invalid("index page length overflow"))?;
let end = index
.offset
.checked_add(u64::from(index.length))
.ok_or_else(|| invalid("index page offset overflow"))?;
if index.offset < HEADER || end > size || index.length != wanted {
return Err(invalid("index page range is outside the file"));
}
let mut pages = Vec::with_capacity(width);
for _ in 0..width {
let offset = cur.u64()?;
let length = cur.u32()?;
let end = offset
.checked_add(u64::from(length))
.ok_or_else(|| invalid("page offset overflow"))?;
if offset < HEADER || end > size || length as usize > MAX_PAGE {
return Err(invalid("page range is outside the file"));
}
pages.push(Span { offset, length });
}
let mut memberships = vec![None; width];
for (column, field) in fields.iter().enumerate() {
if !coded_type(&field.ty) || dictionaries[column].is_none() {
continue;
}
let page = Page { offset: cur.u64()?, length: cur.u32()?, hash: cur.u64()? };
let end = page
.offset
.checked_add(u64::from(page.length))
.ok_or_else(|| invalid("membership page offset overflow"))?;
if page.offset < HEADER || end > size || page.length as usize > MAX_PAGE {
return Err(invalid("membership page range is outside the file"));
}
if page.length != 0 {
memberships[column] = Some(page);
}
}
let mut sieves = vec![None; width];
for sieve in sieves.iter_mut().take(width) {
match cur.u8()? {
0 => continue,
1 => {}
_ => return Err(invalid("a sieve page has an unknown tag")),
}
let page = Page { offset: cur.u64()?, length: cur.u32()?, hash: cur.u64()? };
let end = page
.offset
.checked_add(u64::from(page.length))
.ok_or_else(|| invalid("sieve page offset overflow"))?;
if page.offset < HEADER || end > size || page.length as usize > MAX_PAGE {
return Err(invalid("sieve page range is outside the file"));
}
*sieve = Some(page);
}
let mut part_ranges = vec![None; width];
for held in part_ranges.iter_mut().take(width) {
match cur.u8()? {
0 => continue,
1 => {}
_ => return Err(invalid("a part range page has an unknown tag")),
}
let page = Page { offset: cur.u64()?, length: cur.u32()?, hash: cur.u64()? };
let end = page
.offset
.checked_add(u64::from(page.length))
.ok_or_else(|| invalid("part range page offset overflow"))?;
if page.offset < HEADER || end > size || page.length as usize > MAX_PAGE {
return Err(invalid("part range page range is outside the file"));
}
*held = Some(page);
}
let mut ranges = Vec::with_capacity(width);
for column in 0..width {
let low = cur.bound()?;
let high = cur.bound()?;
let nulls = cur.u32()? as usize;
if nulls > stripe_rows {
return Err(invalid("null count exceeds stripe rows"));
}
let exact = cur.u8()? != 0;
let sum = match cur.u8()? {
0 => None,
1 => Some(i128::from_le_bytes(
cur.take(16)?.try_into().map_err(|_| invalid("a stripe sum is truncated"))?,
)),
_ => return Err(invalid("a stripe sum has an unknown tag")),
};
let ty = &fields.get(column).ok_or_else(|| invalid("a stripe range has no column"))?.ty;
let low = low.map(|bound| scaled_as(bound, ty));
let high = high.map(|bound| scaled_as(bound, ty));
ranges.push(Range { low, high, nulls, exact, sum });
}
stripes.push(Stripe {
rows: stripe_rows,
parts,
index,
pages,
memberships: Pages::from_slots(memberships)?,
sieves: Pages::from_slots(sieves)?,
part_ranges: Pages::from_slots(part_ranges)?,
zone: Zone::from_ranges(ranges),
});
}
if total != rows {
return Err(invalid("table row count differs from stripes"));
}
let mut entry_counts = vec![0; width];
let frequencies = if cur.done() {
vec![None; width]
} else {
let frequency_magic = cur.take(8)?;
let frequency_values = frequency_magic == FREQUENCIES;
if !frequency_values && frequency_magic != FREQUENCIES_V2 {
return Err(invalid("directory extension magic differs"));
}
if cur.u16()? as usize != width {
return Err(invalid("frequency column count differs"));
}
let mut frequencies = Vec::with_capacity(width);
for (field, entry_count) in fields.iter().zip(&mut entry_counts) {
let start = cur.at;
let summary = decode_summary(&mut cur, field, rows, frequency_values)?;
*entry_count = summary.as_ref().map_or(0, |summary| summary.entries.len());
frequencies.push(match (summary, stored_at) {
(None, _) => None,
(Some(summary), None) => Some(Frequencies::Held(summary)),
(Some(_), Some(offset)) => Some(Frequencies::Stored {
span: Span {
offset: offset + start as u64,
length: u32::try_from(cur.at - start)
.map_err(|_| invalid("a frequency synopsis is too long"))?,
},
values: frequency_values,
}),
});
}
frequencies
};
let mut clustering = None;
let mut sections = Vec::new();
let mut pair_frequencies = Vec::new();
let mut seen_pair_frequencies = false;
let mut frequency_texts = vec![Vec::new(); width];
let mut seen_frequency_texts = false;
let mut host_groups = None;
let mut demoted = Vec::new();
let mut seen_sections = false;
let mut dictionary_payloads = Vec::new();
let mut seen_payloads = false;
let mut generation = 0;
while !cur.done() {
let mut tag = [0u8; 8];
tag.copy_from_slice(cur.take(8)?);
if &tag == PAIR_FREQUENCIES {
if seen_pair_frequencies {
return Err(invalid("directory names two pair frequency blocks"));
}
seen_pair_frequencies = true;
let count = cur.u16()? as usize;
if count > MAX_PAIR_FREQUENCIES {
return Err(invalid("pair frequency count exceeds its bound"));
}
pair_frequencies = Vec::with_capacity(count);
for _ in 0..count {
let first = cur.u16()?;
let second = cur.u16()?;
let first_at = first as usize;
let second_at = second as usize;
if frequencies.get(first_at).and_then(Option::as_ref).is_none() {
return Err(invalid("pair frequency first column has no synopsis"));
}
let first_entries = entry_counts[first_at];
if !matches!(fields.get(second_at), Some(field) if field.ty == LogicalType::Varchar)
|| dictionaries.get(second_at).copied().flatten().is_none()
{
return Err(invalid("pair frequency second column has no stable dictionary"));
}
if pair_frequencies
.iter()
.any(|held: &PairFrequencySummary| held.first == first && held.second == second)
{
return Err(invalid("directory repeats a pair frequency summary"));
}
let omitted_max = cur.u64()?;
if omitted_max > rows as u64 {
return Err(invalid("pair frequency omitted count exceeds the table"));
}
let entries_count = cur.u16()? as usize;
if entries_count > FREQUENCY_ENTRIES {
return Err(invalid("pair frequency entry count exceeds its bound"));
}
let mut entries = Vec::with_capacity(entries_count);
for _ in 0..entries_count {
let first_entry = cur.u16()?;
if first_entry as usize >= first_entries {
return Err(invalid("pair frequency anchor is outside its synopsis"));
}
let second = match cur.u8()? {
0 => None,
1 => Some(cur.u32()?),
_ => return Err(invalid("pair frequency string tag differs")),
};
let count = cur.u64()?;
if count == 0 || count > rows as u64 {
return Err(invalid("pair frequency count is outside the table"));
}
entries.push(PairFrequencyEntry { first_entry, second, count });
}
if entries.windows(2).any(|pair| pair[0].count < pair[1].count) {
return Err(invalid("pair frequency entries are not descending"));
}
pair_frequencies.push(PairFrequencySummary { first, second, entries, omitted_max });
}
} else if &tag == FREQUENCY_TEXTS {
if seen_frequency_texts {
return Err(invalid("directory names two frequency text blocks"));
}
seen_frequency_texts = true;
let columns = cur.u16()? as usize;
if columns > width {
return Err(invalid("frequency text column count exceeds the schema"));
}
for _ in 0..columns {
let column = cur.u16()? as usize;
if !frequency_texts.get(column).is_some_and(Vec::is_empty) {
return Err(invalid("frequency text column is repeated or out of range"));
}
if !matches!(fields.get(column), Some(field) if coded_type(&field.ty))
|| dictionaries.get(column).copied().flatten().is_none()
|| frequencies.get(column).and_then(Option::as_ref).is_none()
{
return Err(invalid("frequency texts belong to a non-string synopsis"));
}
let count = cur.u16()? as usize;
if count == 0 || count != entry_counts[column] {
return Err(invalid("frequency text count differs from its synopsis"));
}
let mut texts = Vec::with_capacity(count);
for _ in 0..count {
texts.push(match cur.u8()? {
0 => None,
1 => {
let length = cur.u32()? as usize;
let bytes = cur.take(length)?.to_vec();
if fields[column].ty == LogicalType::Varchar {
std::str::from_utf8(&bytes)
.map_err(|_| invalid("frequency text is not UTF-8"))?;
}
Some(bytes)
}
_ => return Err(invalid("frequency text tag differs")),
});
}
frequency_texts[column] = texts;
}
} else if &tag == HOST_GROUPS {
if host_groups.is_some() {
return Err(invalid("directory names two host group blocks"));
}
let column = cur.u16()? as usize;
if !matches!(fields.get(column), Some(field) if field.ty == LogicalType::Varchar)
|| dictionaries.get(column).copied().flatten().is_none()
{
return Err(invalid("host groups belong to a non-string dictionary"));
}
let omitted_max = cur.u64()?;
if omitted_max > rows as u64 {
return Err(invalid("host group bound exceeds the table"));
}
let count = cur.u16()? as usize;
if count > host::CAPACITY {
return Err(invalid("host group count exceeds its bound"));
}
let mut entries = Vec::with_capacity(count);
let mut bytes = 0_usize;
for _ in 0..count {
let host_len = cur.u32()? as usize;
bytes =
bytes.checked_add(host_len).ok_or_else(|| invalid("host bytes overflow"))?;
if bytes > host::BYTE_BUDGET {
return Err(invalid("host groups exceed their byte budget"));
}
let host = std::str::from_utf8(cur.take(host_len)?)
.map_err(|_| invalid("host is not UTF-8"))?
.to_owned();
let count = cur.u64()?;
if count == 0 || count > rows as u64 {
return Err(invalid("host group count exceeds the table"));
}
let bytes_sum = i128::from_le_bytes(
cur.take(16)?
.try_into()
.map_err(|_| invalid("host length sum is truncated"))?,
);
if bytes_sum < 0 {
return Err(invalid("host length sum is negative"));
}
let minimum_len = cur.u32()? as usize;
bytes =
bytes.checked_add(minimum_len).ok_or_else(|| invalid("host bytes overflow"))?;
if bytes > host::BYTE_BUDGET {
return Err(invalid("host groups exceed their byte budget"));
}
let minimum = std::str::from_utf8(cur.take(minimum_len)?)
.map_err(|_| invalid("host minimum is not UTF-8"))?
.to_owned();
entries.push(host::HostEntry { host, count, bytes_sum, minimum });
}
if entries.windows(2).any(|pair| pair[0].count < pair[1].count)
|| entries.iter().any(|entry| entry.host.is_empty() || entry.minimum.is_empty())
{
return Err(invalid("host groups are not in certified order"));
}
host_groups = Some(host::HostSummary { column, omitted_max, entries });
} else if &tag == CLUSTERING {
if clustering.is_some() {
return Err(invalid("directory names two clustering declarations"));
}
let bucket = Width::from_tag(cur.u8()?)
.ok_or_else(|| invalid("clustering width tag differs"))?;
let count = cur.u16()? as usize;
let mut columns = Vec::with_capacity(count.min(fields.len()));
for _ in 0..count {
columns.push(u32::from(cur.u16()?));
}
clustering = Some(Clustering::new(columns, bucket, &fields).map_err(|_| {
invalid("stored clustering declaration does not match the table it is on")
})?);
} else if &tag == DEMOTED {
if !demoted.is_empty() {
return Err(invalid("directory names two demoted column blocks"));
}
let count = cur.u16()? as usize;
if count == 0 || count > width {
return Err(invalid("demoted column count is outside the schema"));
}
demoted = vec![false; width];
for _ in 0..count {
let column = cur.u16()? as usize;
if dictionaries.get(column).copied().flatten().is_none() || demoted[column] {
return Err(invalid("a demoted column is repeated or has no dictionary"));
}
demoted[column] = true;
}
} else if &tag == SECTIONS {
if seen_sections {
return Err(invalid("directory names two section tables"));
}
seen_sections = true;
generation = cur.u64()?;
let count = cur.u16()? as usize;
if count > MAX_SECTIONS {
return Err(invalid("section count exceeds its bound"));
}
sections = Vec::with_capacity(count);
for _ in 0..count {
sections.push(Section::decode(cur.take(section::ENTRY_BYTES)?)?);
}
for held in §ions {
let Some(end) = held.extent_page.checked_add(u64::from(held.extent_bytes)) else {
return Err(invalid("a section's extent table overflows the file"));
};
if held.extent_bytes != 0 && (held.extent_page < HEADER || end > size) {
return Err(invalid("a section's extent table is outside the file"));
}
if held.extents == 0 && held.extent_bytes != 0 {
return Err(invalid("a section with no extents names an extent table"));
}
}
} else if &tag == DICTIONARY_PAYLOADS {
if seen_payloads {
return Err(invalid("directory names two dictionary payload blocks"));
}
seen_payloads = true;
let count = cur.u16()? as usize;
if count != fields.len() {
return Err(invalid("dictionary payload block does not match the table's columns"));
}
dictionary_payloads = Vec::with_capacity(count);
for _ in 0..count {
let bytes = cur.u64()?;
if bytes > size {
return Err(invalid("a dictionary payload is larger than the file"));
}
dictionary_payloads.push(bytes);
}
} else {
return Err(invalid("directory extension magic differs"));
}
}
if !cur.done() {
return Err(invalid("directory has trailing bytes"));
}
for stripe in &stripes {
for (column, field) in fields.iter().enumerate() {
if coded_type(&field.ty)
&& dictionaries[column].is_some()
&& stripe.memberships.get(column).is_none()
&& !demoted.get(column).copied().unwrap_or(false)
{
return Err(invalid("string page has no code membership index"));
}
}
}
Ok(Table {
name,
fields,
stripes,
rows,
dictionaries,
dictionary_payloads,
demoted,
distincts,
frequencies,
pair_frequencies,
frequency_texts,
host_groups,
clustering,
generation,
sections,
})
}
fn put_bound(out: &mut Vec<u8>, bound: Option<&Bound>) -> Result<()> {
bounds::put(out, bound)
}
#[derive(Debug)]
struct Codes;
impl chooser::Chooser for Codes {
fn name(&self) -> &'static str {
"codes"
}
fn narrow_strings(
&self,
_values: &[&[u8]],
offered: &[string::Kind],
_depth: u8,
) -> Vec<string::Kind> {
offered.to_vec()
}
fn narrow_integers(
&self,
_values: &[i64],
offered: &[integer::Kind],
depth: u8,
) -> Vec<integer::Kind> {
narrowed_to(Codes::keep(depth), offered)
}
fn considers_integer(&self, kind: integer::Kind, depth: u8) -> bool {
Codes::keep(depth).contains(&kind)
}
}
impl Codes {
fn keep(depth: u8) -> &'static [integer::Kind] {
if depth == 0 {
&[integer::Kind::Constant, integer::Kind::Packed, integer::Kind::Rle]
} else {
&[integer::Kind::Constant, integer::Kind::Packed]
}
}
}
fn narrowed_to(keep: &[integer::Kind], offered: &[integer::Kind]) -> Vec<integer::Kind> {
let narrowed: Vec<integer::Kind> =
offered.iter().copied().filter(|kind| keep.contains(kind)).collect();
if narrowed.is_empty() { offered.to_vec() } else { narrowed }
}
#[derive(Debug)]
struct Fixed;
impl chooser::Chooser for Fixed {
fn name(&self) -> &'static str {
"fixed"
}
fn narrow_strings(
&self,
_values: &[&[u8]],
offered: &[string::Kind],
_depth: u8,
) -> Vec<string::Kind> {
offered.to_vec()
}
fn narrow_integers(
&self,
_values: &[i64],
offered: &[integer::Kind],
depth: u8,
) -> Vec<integer::Kind> {
narrowed_to(Fixed::keep(depth), offered)
}
fn considers_integer(&self, kind: integer::Kind, depth: u8) -> bool {
Fixed::keep(depth).contains(&kind)
}
}
impl Fixed {
fn keep(depth: u8) -> &'static [integer::Kind] {
if depth == 0 {
&[
integer::Kind::Constant,
integer::Kind::Packed,
integer::Kind::Delta,
integer::Kind::Rle,
integer::Kind::Sparse,
integer::Kind::Strided,
]
} else {
&[integer::Kind::Constant, integer::Kind::Packed, integer::Kind::Delta]
}
}
}
fn widened(data: &Data) -> Option<Vec<i64>> {
match data {
Data::Int8(values) => Some(values.iter().map(|value| i64::from(*value)).collect()),
Data::UInt8(values) => Some(values.iter().map(|value| i64::from(*value)).collect()),
Data::Int16(values) => Some(values.iter().map(|value| i64::from(*value)).collect()),
Data::UInt16(values) => Some(values.iter().map(|value| i64::from(*value)).collect()),
Data::Int32(values) => Some(values.iter().map(|value| i64::from(*value)).collect()),
Data::UInt32(values) => Some(values.iter().map(|value| i64::from(*value)).collect()),
Data::Int64(values) => Some(values.to_vec()),
_ => None,
}
}
trait Narrow: Copy {
const BIASED: (u32, u64);
fn narrow(value: i64) -> Self;
}
#[allow(clippy::cast_sign_loss, reason = "a residue is a bit pattern and not a number")]
fn residue<T: Narrow>(value: i64) -> u64 {
let (bits, bias) = T::BIASED;
(value as u64).wrapping_add(bias) >> bits
}
macro_rules! narrows {
($($ty:ty => $bias:expr),* $(,)?) => {$(
impl Narrow for $ty {
const BIASED: (u32, u64) = (<$ty>::BITS, $bias);
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the caller has checked the bits this truncates away"
)]
fn narrow(value: i64) -> Self {
value as Self
}
}
)*};
}
narrows! {
i8 => 1 << 7,
u8 => 0,
i16 => 1 << 15,
u16 => 0,
i32 => 1 << 31,
u32 => 0,
}
fn fit<T: Narrow>(values: &[i64]) -> Result<Vec<T>> {
let mut spilled = 0u64;
for value in values {
spilled |= residue::<T>(*value);
}
if spilled != 0 {
return Err(invalid("page value is not of its type"));
}
Ok(values.iter().map(|value| T::narrow(*value)).collect())
}
fn narrowed(ty: &LogicalType, values: Vec<i64>) -> Result<Data> {
Ok(match ty {
LogicalType::TinyInt => Data::Int8(fit::<i8>(&values)?.into()),
LogicalType::UTinyInt => Data::UInt8(fit::<u8>(&values)?.into()),
LogicalType::SmallInt => Data::Int16(fit::<i16>(&values)?.into()),
LogicalType::USmallInt => Data::UInt16(fit::<u16>(&values)?.into()),
LogicalType::Integer | LogicalType::Date => Data::Int32(fit::<i32>(&values)?.into()),
LogicalType::UInteger => Data::UInt32(fit::<u32>(&values)?.into()),
LogicalType::BigInt
| LogicalType::Timestamp
| LogicalType::Time
| LogicalType::TimeTz
| LogicalType::TimestampTz
| LogicalType::TimestampS
| LogicalType::TimestampMs
| LogicalType::TimestampNs => Data::Int64(values.into()),
LogicalType::Decimal { .. } => match ty.physical() {
PhysicalType::Int16 => Data::Int16(fit::<i16>(&values)?.into()),
PhysicalType::Int32 => Data::Int32(fit::<i32>(&values)?.into()),
PhysicalType::Int64 => Data::Int64(values.into()),
_ => return Err(invalid("cascade codec belongs to a decimal that is not an integer")),
},
_ => return Err(invalid("cascade codec belongs to a page that is not integers")),
})
}
fn plain_width(ty: &LogicalType) -> Option<usize> {
Some(match ty {
LogicalType::TinyInt | LogicalType::UTinyInt => 1,
LogicalType::SmallInt | LogicalType::USmallInt => 2,
LogicalType::Integer | LogicalType::UInteger | LogicalType::Date => 4,
LogicalType::BigInt
| LogicalType::Timestamp
| LogicalType::Time
| LogicalType::TimeTz
| LogicalType::TimestampTz
| LogicalType::TimestampS
| LogicalType::TimestampMs
| LogicalType::TimestampNs => 8,
LogicalType::Decimal { .. } => match ty.physical() {
PhysicalType::Int16 => 2,
PhysicalType::Int32 => 4,
PhysicalType::Int64 => 8,
_ => return None,
},
_ => return None,
})
}
fn cascaded(
flat: &Vector,
ty: &LogicalType,
packed: Option<&Packed<'_>>,
settling: &mut Settling,
) -> Result<Option<Vec<u8>>> {
let (Some(width), Some(data)) = (plain_width(ty), flat.data()) else { return Ok(None) };
let Some(values) = widened(data) else { return Ok(None) };
let plain = values.len().saturating_mul(width);
let best = match packed {
Some(packed) => plain.min(21 + size_of_val(packed.words())),
None => plain,
};
let out = settling.encode(&values)?;
Ok((out.len() < best).then_some(out))
}
const SEARCH_EVERY: usize = 16;
#[derive(Debug, Default)]
struct Settling {
shape: Option<Shape>,
since: usize,
}
impl Settling {
fn encode(&mut self, values: &[i64]) -> Result<Vec<u8>> {
if let Some(shape) = self.shape.as_ref().filter(|_| self.since < SEARCH_EVERY) {
let replay = chooser::Replay::new(&shape.kinds, &Fixed).expecting(&shape.offered);
let out = integer::encode_with(values, &replay)?;
if !replay.held() {
self.settle(&out, values.len(), replay.first_offered())?;
return Ok(out);
}
let grown = (out.len() as u128) * (shape.rows as u128) * 4;
if grown <= (shape.len as u128) * (values.len() as u128) * 5 {
self.since += 1;
return Ok(out);
}
}
let search = chooser::Replay::new(&[], &Fixed);
let out = integer::encode_with(values, &search)?;
self.settle(&out, values.len(), search.first_offered())?;
Ok(out)
}
fn settle(&mut self, out: &[u8], rows: usize, offered: Vec<integer::Kind>) -> Result<()> {
let kinds = integer::shape(out)?;
self.shape = Some(Shape { kinds, offered, len: out.len().max(1), rows: rows.max(1) });
self.since = 0;
Ok(())
}
}
#[derive(Debug)]
struct Shape {
kinds: Vec<integer::Kind>,
offered: Vec<integer::Kind>,
len: usize,
rows: usize,
}
fn text_compressed(flat: &Vector) -> Result<Option<Vec<u8>>> {
let mut values: Vec<&[u8]> = Vec::with_capacity(flat.len());
let mut payload = 0_usize;
for row in 0..flat.len() {
let text = flat.bytes_at(row).unwrap_or(b"");
payload = payload.saturating_add(text.len());
values.push(text);
}
let plain = (flat.len() + 1).saturating_mul(4).saturating_add(payload);
let Some(out) = string::encode_only(string::Kind::Fsst, &values)? else {
return Ok(None);
};
Ok((out.len() < plain).then_some(out))
}
fn encoded_codes(codes: &[u32]) -> Result<Option<Vec<u8>>> {
let wide: Vec<i64> = codes.iter().map(|code| i64::from(*code)).collect();
let coded = integer::encode_with(&wide, &Codes)?;
let plain = codes.len().saturating_mul(size_of::<u32>());
Ok((coded.len() < plain).then_some(coded))
}
fn push_validity(out: &mut Vec<u8>, flat: &Vector) {
let flag = match flat.validity() {
Validity::AllValid => 0,
Validity::AllInvalid => 1,
Validity::Mask(_) => 2,
};
out.push(flag);
if flag == 2 {
for group in (0..flat.len()).step_by(8) {
let mut bits = 0_u8;
for bit in 0..8 {
if group + bit < flat.len() && !flat.is_null_at(group + bit) {
bits |= 1 << bit;
}
}
out.push(bits);
}
}
}
fn coded_page(codes: &[u32], validity: &[u8]) -> Result<Vec<u8>> {
let coded = encoded_codes(codes)?;
let mut out = Vec::with_capacity(
1 + validity.len() + coded.as_ref().map_or(size_of_val(codes), Vec::len),
);
out.push(if coded.is_some() { 4 } else { 3 });
out.extend_from_slice(validity);
match coded {
Some(coded) => out.extend_from_slice(&coded),
None => {
for &code in codes {
put_u32(&mut out, code);
}
}
}
Ok(out)
}
fn encode(vector: &Vector, settling: &mut Settling) -> Result<Vec<u8>> {
let ty = vector.logical_type();
let flat = vector.flatten()?;
let mut out = Vec::new();
let dictionary = if coded_type(ty) { string_dictionary(&flat)? } else { None };
let compressed_text =
if dictionary.is_none() && coded_type(ty) { text_compressed(&flat)? } else { None };
let packed_vector = if dictionary.is_none() { Some(flat.bit_packed()?) } else { None };
let packed = packed_vector.as_ref().and_then(Vector::packed_parts);
let cascade =
if dictionary.is_none() { cascaded(&flat, ty, packed.as_ref(), settling)? } else { None };
out.push(if cascade.is_some() {
5
} else if dictionary.is_some() {
1
} else if compressed_text.is_some() {
6
} else if packed.is_some() {
2
} else {
0
});
push_validity(&mut out, &flat);
if let Some(cascade) = cascade {
out.extend_from_slice(&cascade);
return Ok(out);
}
if let Some(dictionary) = dictionary {
out.extend_from_slice(&dictionary);
return Ok(out);
}
if let Some(compressed_text) = compressed_text {
out.extend_from_slice(&compressed_text);
return Ok(out);
}
if let Some(packed) = packed {
if packed.offset() != 0 {
return Err(invalid("writer received a sliced packed vector"));
}
out.push(u8::try_from(packed.width()).map_err(|_| invalid("packed width overflow"))?);
out.extend_from_slice(&packed.base().to_le_bytes());
put_u32(
&mut out,
u32::try_from(packed.words().len()).map_err(|_| invalid("too many packed words"))?,
);
for word in packed.words() {
put_u64(&mut out, *word);
}
return Ok(out);
}
let data = flat.data().ok_or_else(|| invalid("scalar column did not flatten"))?;
match (ty, data) {
(LogicalType::TinyInt, Data::Int8(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::UTinyInt, Data::UInt8(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::SmallInt, Data::Int16(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::USmallInt, Data::UInt16(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::UInteger, Data::UInt32(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::UBigInt, Data::UInt64(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Integer | LogicalType::Date, Data::Int32(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(
LogicalType::BigInt
| LogicalType::Timestamp
| LogicalType::Time
| LogicalType::TimeTz
| LogicalType::TimestampTz
| LogicalType::TimestampS
| LogicalType::TimestampMs
| LogicalType::TimestampNs,
Data::Int64(values),
) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::HugeInt | LogicalType::Uuid, Data::Int128(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::UHugeInt, Data::UInt128(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Float, Data::Float32(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Double, Data::Float64(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Interval, Data::Interval(values)) => {
for (months, days, micros) in &**values {
out.extend_from_slice(&months.to_le_bytes());
out.extend_from_slice(&days.to_le_bytes());
out.extend_from_slice(µs.to_le_bytes());
}
}
(LogicalType::Boolean, Data::Bool(values)) => {
for value in &**values {
out.push(u8::from(*value));
}
}
(LogicalType::Decimal { .. }, Data::Int16(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Decimal { .. }, Data::Int32(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Decimal { .. }, Data::Int64(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Decimal { .. }, Data::Int128(values)) => {
for value in &**values {
out.extend_from_slice(&value.to_le_bytes());
}
}
(LogicalType::Varchar | LogicalType::Blob | LogicalType::Bit, Data::Varlen(values)) => {
let mut bytes = Vec::new();
put_u32(&mut out, 0);
for row in 0..vector.len() {
let value = values.bytes(row).ok_or_else(|| invalid("string view is invalid"))?;
bytes.extend_from_slice(value);
put_u32(
&mut out,
u32::try_from(bytes.len())
.map_err(|_| invalid("string payload exceeds 4GiB"))?,
);
}
out.extend_from_slice(&bytes);
}
_ => return Err(Error::not_implemented(format!("native page for {ty}"))),
}
Ok(out)
}
fn put_varint(out: &mut Vec<u8>, mut value: u32) {
while value >= 0x80 {
out.push((value as u8 & 0x7f) | 0x80);
value >>= 7;
}
out.push(value as u8);
}
fn unique_codes(codes: &[u32]) -> Vec<u32> {
let mut unique = codes.to_vec();
unique.sort_unstable();
unique.dedup();
unique
}
fn merged_codes(lists: Vec<Vec<u32>>) -> Vec<u32> {
let mut lists = lists;
while lists.len() > 1 {
let mut next = Vec::with_capacity(lists.len().div_ceil(2));
for pair in lists.chunks(2) {
match pair {
[left, right] => next.push(merged_pair(left, right)),
[only] => next.push(only.clone()),
_ => {}
}
}
lists = next;
}
lists.pop().unwrap_or_default()
}
fn merged_pair(left: &[u32], right: &[u32]) -> Vec<u32> {
let mut out = Vec::with_capacity(left.len().saturating_add(right.len()));
let mut at = 0;
let mut to = 0;
while at < left.len() && to < right.len() {
match left[at].cmp(&right[to]) {
Ordering::Less => {
out.push(left[at]);
at += 1;
}
Ordering::Greater => {
out.push(right[to]);
to += 1;
}
Ordering::Equal => {
out.push(left[at]);
at += 1;
to += 1;
}
}
}
out.extend_from_slice(&left[at..]);
out.extend_from_slice(&right[to..]);
out
}
fn merged_range(ranges: impl Iterator<Item = Range>) -> Range {
let mut merged = Range::default();
let mut first = true;
for range in ranges {
merged.nulls = merged.nulls.saturating_add(range.nulls);
merged.sum = match (merged.sum.take(), range.sum) {
(Some(held), Some(next)) if !first => held.checked_add(next),
(_, next) if first => next,
_ => None,
};
merged.exact = if first { range.exact } else { merged.exact && range.exact };
if first {
merged.low = range.low;
merged.high = range.high;
first = false;
continue;
}
merged.low = match (merged.low.take(), range.low) {
(Some(held), Some(next)) => Some(held.smaller(next)),
_ => None,
};
merged.high = match (merged.high.take(), range.high) {
(Some(held), Some(next)) => Some(held.larger(next)),
_ => None,
};
}
merged
}
fn shortened(bound: Option<Bound>, high: bool) -> Option<Bound> {
match bound {
Some(Bound::Bytes(mut value)) if value.len() > PART_BOUND_BYTES => {
value.truncate(PART_BOUND_BYTES);
if !high {
return Some(Bound::Bytes(value));
}
while let Some(last) = value.pop() {
if last < u8::MAX {
value.push(last + 1);
return Some(Bound::Bytes(value));
}
}
None
}
other => other,
}
}
fn encode_part_ranges(ranges: &[Range]) -> Result<Vec<u8>> {
let mut out = Vec::new();
put_u32(
&mut out,
u32::try_from(ranges.len()).map_err(|_| invalid("too many parts in a stripe"))?,
);
for range in ranges {
put_bound(&mut out, shortened(range.low.clone(), false).as_ref())?;
put_bound(&mut out, shortened(range.high.clone(), true).as_ref())?;
put_u32(&mut out, u32::try_from(range.nulls).map_err(|_| invalid("null count overflow"))?);
}
Ok(out)
}
fn decode_part_ranges(bytes: &[u8]) -> Result<Vec<Range>> {
let mut cur = Cursor::new(bytes);
let parts = cur.u32()? as usize;
let mut out = Vec::new();
for _ in 0..parts {
let low = cur.bound()?;
let high = cur.bound()?;
let nulls = cur.u32()? as usize;
out.push(Range { low, high, nulls, exact: false, sum: None });
}
Ok(out)
}
fn encode_sieves<'a>(sieves: impl Iterator<Item = &'a Option<Sieve>>) -> Result<Vec<u8>> {
let held: Vec<&Option<Sieve>> = sieves.collect();
let mut out = Vec::new();
put_u32(
&mut out,
u32::try_from(held.len()).map_err(|_| invalid("too many parts in a stripe"))?,
);
for sieve in &held {
let length = sieve.as_ref().map_or(0, Sieve::len);
put_u32(&mut out, u32::try_from(length).map_err(|_| invalid("sieve length overflow"))?);
}
for sieve in held.into_iter().flatten() {
out.extend_from_slice(&sieve.to_bytes());
}
Ok(out)
}
fn decode_sieves(bytes: &[u8]) -> Result<Vec<Option<Sieve>>> {
let parts = u32::from_le_bytes(
bytes
.get(..4)
.ok_or_else(|| invalid("sieve page is truncated"))?
.try_into()
.map_err(|_| invalid("sieve page is truncated"))?,
) as usize;
let mut lengths = Vec::with_capacity(parts);
for part in 0..parts {
let at = 4 + part * 4;
let field = bytes.get(at..at + 4).ok_or_else(|| invalid("sieve page is truncated"))?;
lengths.push(u32::from_le_bytes(
field.try_into().map_err(|_| invalid("sieve page is truncated"))?,
) as usize);
}
let mut at = 4 + parts * 4;
let mut out = Vec::with_capacity(parts);
for length in lengths {
if length == 0 {
out.push(None);
continue;
}
let end = at.checked_add(length).ok_or_else(|| invalid("sieve page is truncated"))?;
let field = bytes.get(at..end).ok_or_else(|| invalid("sieve page is truncated"))?;
out.push(Sieve::from_bytes(field));
at = end;
}
if at != bytes.len() {
return Err(invalid("sieve page has trailing bytes"));
}
Ok(out)
}
fn encode_membership(unique: &[u32]) -> Vec<u8> {
let mut out = Vec::with_capacity(unique.len().saturating_mul(2).saturating_add(5));
put_varint(&mut out, u32::try_from(unique.len()).unwrap_or(u32::MAX));
let mut previous = 0;
for (at, &code) in unique.iter().enumerate() {
put_varint(&mut out, if at == 0 { code } else { code - previous });
previous = code;
}
out
}
fn take_varint(bytes: &[u8], at: &mut usize) -> Result<u32> {
let mut value = 0_u32;
for shift in (0..35).step_by(7) {
let byte = *bytes.get(*at).ok_or_else(|| invalid("membership varint is truncated"))?;
*at += 1;
let part = u32::from(byte & 0x7f);
if shift == 28 && part > 0x0f {
return Err(invalid("membership varint overflow"));
}
value = value
.checked_add(
part.checked_shl(shift).ok_or_else(|| invalid("membership varint overflow"))?,
)
.ok_or_else(|| invalid("membership varint overflow"))?;
if byte & 0x80 == 0 {
return Ok(value);
}
}
Err(invalid("membership varint is too long"))
}
fn decode_membership(bytes: &[u8]) -> Result<Vec<u32>> {
let mut at = 0;
let count = take_varint(bytes, &mut at)? as usize;
let mut codes = Vec::with_capacity(count);
let mut previous = 0_u32;
for index in 0..count {
let delta = take_varint(bytes, &mut at)?;
let code = if index == 0 {
delta
} else {
previous.checked_add(delta).ok_or_else(|| invalid("membership code overflow"))?
};
if index > 0 && code <= previous {
return Err(invalid("membership codes are not increasing"));
}
codes.push(code);
previous = code;
}
if at != bytes.len() {
return Err(invalid("membership page has trailing bytes"));
}
Ok(codes)
}
fn string_dictionary(vector: &Vector) -> Result<Option<Vec<u8>>> {
let mut by_text = HashMap::new();
let mut values = Vec::new();
let mut codes = Vec::with_capacity(vector.len());
let mut plain_bytes = 0_usize;
for row in 0..vector.len() {
let text = vector.bytes_at(row).unwrap_or(b"");
plain_bytes = plain_bytes.saturating_add(text.len());
let code = match by_text.get(text) {
Some(&code) => code,
None => {
let code = u32::try_from(values.len())
.map_err(|_| invalid("too many dictionary values"))?;
by_text.insert(text, code);
values.push(text);
code
}
};
codes.push(code);
}
let dictionary_bytes = values.iter().map(|value| value.len()).sum::<usize>();
let encoded = 8_usize
.saturating_add((values.len() + 1).saturating_mul(4))
.saturating_add(dictionary_bytes)
.saturating_add(codes.len().saturating_mul(4));
let plain = (vector.len() + 1).saturating_mul(4).saturating_add(plain_bytes);
if encoded >= plain {
return Ok(None);
}
let mut out = Vec::with_capacity(encoded);
put_u32(
&mut out,
u32::try_from(values.len()).map_err(|_| invalid("too many dictionary values"))?,
);
put_u32(
&mut out,
u32::try_from(dictionary_bytes).map_err(|_| invalid("dictionary payload exceeds 4GiB"))?,
);
let mut offset = 0_u32;
put_u32(&mut out, offset);
for value in &values {
offset = offset
.checked_add(
u32::try_from(value.len()).map_err(|_| invalid("dictionary value is too long"))?,
)
.ok_or_else(|| invalid("dictionary payload exceeds 4GiB"))?;
put_u32(&mut out, offset);
}
for value in values {
out.extend_from_slice(value);
}
for code in codes {
put_u32(&mut out, code);
}
Ok(Some(out))
}
struct Room<'a, T> {
state: &'a Mutex<(T, usize)>,
finished: &'a Condvar,
bytes: usize,
}
impl<T> Drop for Room<'_, T> {
fn drop(&mut self) {
let mut held = self.state.lock().unwrap_or_else(PoisonError::into_inner);
held.1 -= self.bytes;
drop(held);
self.finished.notify_all();
}
}
enum Closing<'a> {
Numeric {
column: usize,
counted: bool,
},
Dictionary {
index: usize,
dictionary: &'a GlobalDictionary,
},
}
enum Closed {
Numeric(usize, (Option<FrequencySummary>, Option<u64>)),
Dictionary(usize, ClosedDictionary),
}
struct ClosedDictionary {
distinct: Option<u64>,
frequencies: Option<FrequencySummary>,
texts: Vec<Option<Vec<u8>>>,
hosts: Option<host::HostSummary>,
encoded: EncodedDictionary,
payload: u64,
}
struct EncodedDictionary {
index: Vec<u8>,
ranks: Vec<u8>,
grams: Vec<u8>,
}
fn sort_by_value<'a>(codes: &mut [u32], values: impl Fn(u32) -> &'a [u8]) {
let mut work = vec![(0, codes.len(), 0)];
let mut keyed: Vec<(u64, u8, u32)> = Vec::new();
while let Some((from, to, depth)) = work.pop() {
let part = &mut codes[from..to];
keyed.clear();
keyed.extend(part.iter().map(|&code| {
let value = values(code);
let rest = value.get(depth..).unwrap_or_default();
(head(rest), rest.len().min(8) as u8, code)
}));
keyed.sort_unstable();
for (slot, entry) in part.iter_mut().zip(keyed.iter()) {
*slot = entry.2;
}
let mut start = 0;
while start < keyed.len() {
let (key, taken, _) = keyed[start];
let mut end = start + 1;
while end < keyed.len() && keyed[end].0 == key && keyed[end].1 == taken {
end += 1;
}
if taken == 8 && end - start > 1 {
work.push((from + start, from + end, depth + 8));
}
start = end;
}
}
}
const PARALLEL_SORT_MIN: usize = 1 << 16;
const BUCKETS_PER_WORKER: usize = 4;
const SAMPLES_PER_BUCKET: usize = 32;
fn sort_by_value_across<'a>(
codes: &mut [u32],
values: impl Fn(u32) -> &'a [u8] + Sync,
workers: usize,
) {
if workers <= 1 || codes.len() < PARALLEL_SORT_MIN {
sort_by_value(codes, values);
return;
}
let buckets = workers * BUCKETS_PER_WORKER;
let wanted = buckets * SAMPLES_PER_BUCKET;
let mut sample = (0..wanted).map(|at| codes[at * codes.len() / wanted]).collect::<Vec<_>>();
sort_by_value(&mut sample, &values);
let splitters =
(1..buckets).map(|cut| values(sample[cut * sample.len() / buckets])).collect::<Vec<_>>();
let values = &values;
let splitters = &splitters;
let per = codes.len().div_ceil(workers);
let places = std::thread::scope(|scope| {
codes
.chunks(per)
.map(|run| {
scope.spawn(move || {
run.iter()
.map(|&code| {
let value = values(code);
splitters.partition_point(|splitter| *splitter <= value) as u32
})
.collect::<Vec<_>>()
})
})
.collect::<Vec<_>>()
.into_iter()
.flat_map(|handle| {
handle.join().unwrap_or_else(|panic| std::panic::resume_unwind(panic))
})
.collect::<Vec<_>>()
});
let mut starts = vec![0_usize; buckets + 1];
for &place in &places {
starts[place as usize + 1] += 1;
}
for bucket in 0..buckets {
starts[bucket + 1] += starts[bucket];
}
let mut laid = vec![0_u32; codes.len()];
let mut next = starts.clone();
for (&code, &place) in codes.iter().zip(&places) {
laid[next[place as usize]] = code;
next[place as usize] += 1;
}
drop(places);
let mut runs = Vec::with_capacity(buckets);
let mut rest = laid.as_mut_slice();
for bucket in 0..buckets {
let (run, after) = rest.split_at_mut(starts[bucket + 1] - starts[bucket]);
runs.push(run);
rest = after;
}
runs.sort_by_key(|run| run.len());
let queue = Mutex::new(runs);
std::thread::scope(|scope| {
for _ in 0..workers {
scope.spawn(|| {
loop {
let taken = queue.lock().unwrap_or_else(PoisonError::into_inner).pop();
let Some(run) = taken else { break };
sort_by_value(run, values);
}
});
}
});
codes.copy_from_slice(&laid);
}
fn head(bytes: &[u8]) -> u64 {
let mut word = [0; 8];
let take = bytes.len().min(8);
word[..take].copy_from_slice(&bytes[..take]);
u64::from_be_bytes(word)
}
fn encode_global_dictionary(
dictionary: &GlobalDictionary,
order: &[(u64, u32)],
places: &[Placed],
scattered: bool,
) -> Result<EncodedDictionary> {
let values = dictionary.values();
if order.len() != values {
return Err(invalid("global dictionary order does not cover its values"));
}
let blocks = values.div_ceil(TEXT_PAYLOAD_VALUES);
if places.len() != blocks {
return Err(invalid("global dictionary payload is not the blocks it says it is"));
}
if dictionary.grams.len() != blocks {
return Err(invalid("global dictionary signatures do not cover its blocks"));
}
let (ranks, rank_ends) = encode_ranks(order, code_width(values))?;
let rank_blocks = values.div_ceil(TEXT_RANK_BLOCK);
let offset_bits = offset_width(&dictionary.ends);
let payload_words = if scattered { 3 } else { 2 };
let index_len = DICTIONARY_HEADER
.checked_add(offset_bytes(values, offset_bits))
.and_then(|len| len.checked_add(blocks.checked_mul(payload_words * 8)?))
.and_then(|len| len.checked_add(rank_blocks.checked_mul(16)?))
.and_then(|len| len.checked_add(8))
.ok_or_else(|| invalid("global dictionary index length overflow"))?;
let mut index = Vec::with_capacity(index_len);
put_u32(
&mut index,
u32::try_from(values).map_err(|_| invalid("global dictionary has too many values"))?,
);
put_u32(&mut index, TEXT_PAYLOAD_VALUES as u32);
put_u32(
&mut index,
u32::try_from(blocks).map_err(|_| invalid("global dictionary has too many blocks"))?,
);
let flag = (if scattered { DICTIONARY_SCATTERED } else { 0 })
| DICTIONARY_GRAMS
| DICTIONARY_WIDE_GRAMS;
put_u32(&mut index, offset_bits as u32 | flag);
encode_offsets(&dictionary.ends, offset_bits, &mut index)?;
let mut end = 0_u64;
for place in places {
if scattered {
put_u64(&mut index, place.start);
put_u64(&mut index, place.length);
} else {
end = end
.checked_add(place.length)
.ok_or_else(|| invalid("global dictionary payload overflow"))?;
put_u64(&mut index, end);
}
}
for place in places {
put_u64(&mut index, place.hash);
}
if rank_ends.len() != rank_blocks {
return Err(invalid("global dictionary order is not the blocks it says it is"));
}
for end in &rank_ends {
put_u64(&mut index, *end);
}
let mut at = 0_usize;
for end in &rank_ends {
let end = usize::try_from(*end).map_err(|_| invalid("global dictionary order overflow"))?;
put_u64(&mut index, checksum(&ranks[at..end]));
at = end;
}
let gram_len = blocks
.checked_mul(TEXT_GRAM_BYTES)
.ok_or_else(|| invalid("global dictionary signature count overflow"))?;
let mut grams = Vec::with_capacity(gram_len);
for block in &dictionary.grams {
grams.extend_from_slice(block);
}
put_u64(&mut index, checksum(&grams));
if index.len() != index_len {
return Err(invalid("global dictionary index is not the length it was laid out for"));
}
Ok(EncodedDictionary { index, ranks, grams })
}
const PAYLOAD_SAMPLE_BLOCKS: usize = 8;
fn payload_shapes() -> Vec<chooser::Settled> {
let integers = vec![integer::Kind::Packed];
[
vec![string::Kind::Front, string::Kind::Lz],
vec![string::Kind::Lz, string::Kind::Fsst],
vec![string::Kind::Lz, string::Kind::Plain],
vec![string::Kind::Fsst],
vec![string::Kind::Plain],
]
.into_iter()
.map(|strings| chooser::Settled::new(strings, integers.clone()))
.collect()
}
fn synced(file: &dyn rudb_io::File, profile: Option<&LoadProfile>) -> Result<()> {
let started = profile.map(|_| std::time::Instant::now());
file.sync()?;
if let (Some(profile), Some(started)) = (profile, started) {
profile.waited(
Stage::Publish,
u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX),
);
}
Ok(())
}
#[derive(Debug)]
pub(crate) struct Unencoded {
column: usize,
at: usize,
ends: Vec<u32>,
bytes: Vec<u8>,
shape: chooser::Settled,
}
impl Unencoded {
pub(crate) fn encode(&self) -> Result<EncodedBlock> {
let values = block_values(&self.ends, &self.bytes);
Ok((string::encode_with(&values, &self.shape)?, block_grams(&values)))
}
pub(crate) fn place(&self) -> (usize, usize) {
(self.column, self.at)
}
}
pub(crate) type EncodedBlock = (Vec<u8>, Box<[u8; TEXT_GRAM_BYTES]>);
fn block_grams(values: &[&[u8]]) -> Box<[u8; TEXT_GRAM_BYTES]> {
let mut grams = Box::new([0_u8; TEXT_GRAM_BYTES]);
for value in values {
for gram in value.windows(4) {
for bit in gram_bits(gram, TEXT_GRAM_BYTES) {
grams[bit / 8] |= 1 << (bit % 8);
}
}
}
grams
}
fn block_values<'a>(ends: &[u32], bytes: &'a [u8]) -> Vec<&'a [u8]> {
let mut out = Vec::with_capacity(ends.len());
let mut from = 0;
for &to in ends {
out.push(&bytes[from..to as usize]);
from = to as usize;
}
out
}
fn finish_dictionaries(dictionaries: &mut [Option<GlobalDictionary>]) -> Result<()> {
for dictionary in dictionaries.iter_mut().flatten() {
if !dictionary.early.is_empty() {
return Err(Error::internal("a dictionary block handed out never came back"));
}
dictionary.seal_rest();
dictionary.settle_rest()?;
}
encode_waiting(dictionaries)?;
if dictionaries
.iter()
.flatten()
.any(|dictionary| dictionary.encoded() != dictionary.values().div_ceil(TEXT_PAYLOAD_VALUES))
{
return Err(Error::internal("a dictionary block handed out never came back"));
}
Ok(())
}
fn encode_waiting(dictionaries: &mut [Option<GlobalDictionary>]) -> Result<()> {
let jobs = dictionaries
.iter()
.enumerate()
.flat_map(|(column, held)| {
(0..held.as_ref().map_or(0, |held| held.waiting.len())).map(move |at| (column, at))
})
.collect::<Vec<_>>();
if jobs.is_empty() {
return Ok(());
}
let one = |column: usize, at: usize| -> Result<(usize, usize, EncodedBlock)> {
let held = dictionaries[column].as_ref().ok_or_else(|| Error::internal("no dictionary"))?;
Ok((column, at, held.encode_waiting(at)?))
};
let workers = std::thread::available_parallelism()
.map_or(1, usize::from)
.min(MAX_FREQUENCY_WORKERS)
.min(jobs.len());
let made = if workers <= 1 {
jobs.iter().map(|&(column, at)| one(column, at)).collect::<Result<Vec<_>>>()?
} else {
let next = AtomicUsize::new(0);
let jobs = &jobs;
let pieces = std::thread::scope(|scope| {
(0..workers)
.map(|_| {
scope.spawn(|| {
let mut mine = Vec::new();
loop {
let job = next.fetch_add(1, Atomic::Relaxed);
let Some(&(column, at)) = jobs.get(job) else { break };
mine.push(one(column, at)?);
}
Ok(mine)
})
})
.collect::<Vec<_>>()
.into_iter()
.map(|handle| {
handle
.join()
.map_err(|_| Error::internal("a dictionary encode worker panicked"))?
})
.collect::<Result<Vec<_>>>()
})?;
pieces.into_iter().flatten().collect()
};
let mut done: Vec<Vec<(usize, EncodedBlock)>> =
(0..dictionaries.len()).map(|_| Vec::new()).collect();
for (column, at, bytes) in made {
done[column].push((at, bytes));
}
for (column, mut made) in done.into_iter().enumerate() {
if made.is_empty() {
continue;
}
let Some(held) = dictionaries[column].as_mut() else { continue };
made.sort_by_key(|(at, _)| *at);
let waiting = std::mem::take(&mut held.waiting);
for ((at, _), (_, block)) in waiting.into_iter().zip(made) {
if held.encoded() != at {
return Err(Error::internal("a dictionary block was encoded out of order"));
}
held.push_block(block);
}
}
Ok(())
}
fn settle_shape(sample: &[Vec<&[u8]>]) -> Result<chooser::Settled> {
let mut best: Option<(chooser::Settled, usize)> = None;
for shape in payload_shapes() {
let mut size = 0;
for block in sample {
size += string::encode_with(block, &shape)?.len();
}
if best.as_ref().is_none_or(|(_, smallest)| size < *smallest) {
best = Some((shape, size));
}
}
best.map(|(shape, _)| shape)
.ok_or_else(|| invalid("no shape applies to a global dictionary payload"))
}
fn encode_ranks(order: &[(u64, u32)], code_bits: usize) -> Result<(Vec<u8>, Vec<u64>)> {
let mut out = Vec::with_capacity(order.len() * 4);
let mut ends = Vec::with_capacity(order.len().div_ceil(TEXT_RANK_BLOCK));
let mut heads = Vec::with_capacity(TEXT_RANK_BLOCK);
let mut codes = Vec::with_capacity(TEXT_RANK_BLOCK);
for block in order.chunks(TEXT_RANK_BLOCK) {
let base = block.first().map_or(0, |&(head, _)| head);
let span = block.last().map_or(0, |&(head, _)| head.wrapping_sub(base));
let width = (u64::BITS - span.leading_zeros()) as usize;
heads.clear();
codes.clear();
for &(head, code) in block {
heads.push(head.wrapping_sub(base));
codes.push(u64::from(code));
}
put_u64(&mut out, base);
out.push(width as u8);
bitpack::pack_tail(&heads, width, &mut out)
.map_err(|_| invalid("global dictionary heads do not pack"))?;
bitpack::pack_tail(&codes, code_bits, &mut out)
.map_err(|_| invalid("global dictionary codes do not pack"))?;
ends.push(out.len() as u64);
}
Ok((out, ends))
}
fn open_global_dictionary(
file: Arc<File>,
page: Page,
ty: &LogicalType,
keep_budget: usize,
) -> Result<Vector> {
if !coded_type(ty) {
return Err(invalid("global dictionary belongs to a non-string column"));
}
let mut header = [0; DICTIONARY_HEADER];
read_at(&file, page.offset, &mut header)?;
let count = u32::from_le_bytes(header[0..4].try_into().expect("four bytes")) as usize;
let per_block = u32::from_le_bytes(header[4..8].try_into().expect("four bytes")) as usize;
let blocks = u32::from_le_bytes(header[8..12].try_into().expect("four bytes")) as usize;
let width = u32::from_le_bytes(header[12..16].try_into().expect("four bytes"));
let scattered = width & DICTIONARY_SCATTERED != 0;
let has_grams = width & DICTIONARY_GRAMS != 0;
let gram_width =
if width & DICTIONARY_WIDE_GRAMS != 0 { TEXT_GRAM_BYTES } else { NARROW_GRAM_BYTES };
let offset_bits = (width & !DICTIONARY_FLAGS) as usize;
if per_block != TEXT_PAYLOAD_VALUES {
return Err(invalid("global dictionary block width differs"));
}
if blocks != count.div_ceil(TEXT_PAYLOAD_VALUES) {
return Err(invalid("global dictionary block count differs from its value count"));
}
if offset_bits > u32::BITS as usize {
return Err(invalid("global dictionary packs offsets past a payload"));
}
let offset_len = offset_bytes(count, offset_bits);
let ranks = count;
let rank_blocks = ranks.div_ceil(TEXT_RANK_BLOCK);
let payload_words = if scattered { 3 } else { 2 };
let hash_len = blocks
.checked_mul(payload_words * 8)
.and_then(|len| len.checked_add(rank_blocks.checked_mul(16)?))
.and_then(|len| len.checked_add(usize::from(has_grams) * 8))
.ok_or_else(|| invalid("global dictionary block count overflow"))?;
let gram_len = if has_grams {
blocks
.checked_mul(gram_width)
.ok_or_else(|| invalid("global dictionary signature count overflow"))?
} else {
0
};
let index_len = DICTIONARY_HEADER
.checked_add(offset_len)
.and_then(|len| len.checked_add(hash_len))
.ok_or_else(|| invalid("global dictionary header overflow"))?;
if index_len > page.length as usize {
return Err(invalid("global dictionary offset index exceeds its page"));
}
let mut index = vec![0; index_len];
index[..DICTIONARY_HEADER].copy_from_slice(&header);
read_at(&file, page.offset + DICTIONARY_HEADER as u64, &mut index[DICTIONARY_HEADER..])?;
if checksum(&index) != page.hash {
return Err(invalid("global dictionary index checksum differs"));
}
let word_end = index_len - usize::from(has_grams) * 8;
let gram_hash = has_grams
.then(|| u64::from_le_bytes(index[word_end..index_len].try_into().expect("eight bytes")));
let mut words = index[DICTIONARY_HEADER + offset_len..word_end]
.chunks_exact(8)
.map(|part| u64::from_le_bytes(part.try_into().expect("eight bytes")))
.collect::<Vec<_>>();
let mut rest = words.split_off(blocks * payload_words);
let rank_hashes = rest.split_off(rank_blocks);
let rank_ends = rest;
if rank_ends.windows(2).any(|pair| pair[0] >= pair[1]) {
return Err(invalid("global dictionary order blocks do not rise"));
}
let rank_len = usize::try_from(rank_ends.last().copied().unwrap_or_default())
.map_err(|_| invalid("global dictionary rank overflow"))?;
let body_len = index_len
.checked_add(rank_len)
.ok_or_else(|| invalid("global dictionary header overflow"))?;
if body_len > page.length as usize {
return Err(invalid("global dictionary order exceeds its page"));
}
let gram_end = body_len
.checked_add(gram_len)
.ok_or_else(|| invalid("global dictionary signature length overflow"))?;
if gram_end > page.length as usize {
return Err(invalid("global dictionary signatures exceed their page"));
}
let grams = gram_hash.map(|hash| NativeGrams {
start: page.offset + body_len as u64,
length: gram_len,
width: gram_width,
hash,
verdicts: Mutex::new(Vec::new()),
});
let mut offsets = index;
offsets.truncate(DICTIONARY_HEADER + offset_len);
let hashes = words.split_off(blocks * (payload_words - 1));
let (starts, lengths) = if scattered {
let mut starts = Vec::with_capacity(blocks);
let mut lengths = Vec::with_capacity(blocks);
for pair in words.chunks_exact(2) {
starts.push(pair[0]);
lengths.push(pair[1]);
}
(starts, lengths)
} else {
let base = page.offset + gram_end as u64;
let mut starts = Vec::with_capacity(blocks);
let mut lengths = Vec::with_capacity(blocks);
let mut at = 0_u64;
for &end in &words {
let len = end
.checked_sub(at)
.ok_or_else(|| invalid("global dictionary block ends before it starts"))?;
starts.push(base + at);
lengths.push(len);
at = end;
}
(starts, lengths)
};
let stored_len = page.length as u64 - gram_end as u64;
if scattered && stored_len == 0 {
let size = file.metadata().map_err(io)?.len();
let inside = starts.iter().zip(&lengths).all(|(&start, &len)| {
start >= HEADER && start.checked_add(len).is_some_and(|end| end <= size)
});
if !inside {
return Err(invalid("global dictionary block lies outside the file"));
}
} else if lengths.iter().try_fold(0_u64, |sum, len| sum.checked_add(*len)) != Some(stored_len) {
return Err(invalid("global dictionary blocks do not bound the payload"));
}
Vector::external_text(
ty.clone(),
Arc::new(NativeText {
file,
values: count,
offsets,
offset_bits,
value_ends: OnceLock::new(),
value_lens: OnceLock::new(),
ends_asked: AtomicUsize::new(0),
ranks,
rank_at: page.offset + index_len as u64,
rank_ends,
rank_hashes,
rank_blocks: (0..rank_blocks).map(|_| OnceLock::new()).collect(),
code_bits: code_width(count),
code_ranks: OnceLock::new(),
starts,
lengths,
hashes,
grams,
blocks: (0..blocks).map(|_| OnceLock::new()).collect(),
char_lens: (0..blocks).map(|_| OnceLock::new()).collect(),
keep_budget,
payload_kept: AtomicUsize::new(0),
swept: (0..blocks).map(|_| AtomicBool::new(false)).collect(),
visit_dropped: AtomicUsize::new(0),
searched: Mutex::new(HashMap::new()),
}),
)
}
fn page_encoding(ty: &LogicalType, rows: usize, bytes: &[u8]) -> String {
fn cascade_at(rows: usize, bytes: &[u8]) -> Result<(u8, usize)> {
let mut cur = Cursor::new(bytes);
let codec = cur.u8()?;
if cur.u8()? == 2 {
cur.take(rows.div_ceil(8))?;
}
Ok((codec, cur.at))
}
let Ok((codec, at)) = cascade_at(rows, bytes) else {
return "UNREADABLE".to_string();
};
let tail = &bytes[at..];
let described = |described: Result<String>| described.unwrap_or_else(|_| "UNREADABLE".into());
match codec {
0 => match ty {
LogicalType::Varchar | LogicalType::Blob => "PLAIN".to_string(),
_ => "FIXED".to_string(),
},
1 => "DICT(PLAIN)".to_string(),
2 => "FOR+BITPACK".to_string(),
3 => "TABLE DICT".to_string(),
4 => format!("TABLE DICT({})", described(integer::describe(tail))),
5 => described(integer::describe(tail)),
6 => described(string::describe(tail)),
other => format!("CODEC {other}"),
}
}
fn decode_selected_stable_codes(
rows: usize,
bytes: &[u8],
positions: &[usize],
out: &mut Vec<Option<u32>>,
) -> Result<bool> {
if positions.windows(2).any(|pair| pair[0] >= pair[1])
|| positions.last().is_some_and(|&position| position >= rows)
{
return Err(invalid("selected code positions are not sorted and in range"));
}
let mut cur = Cursor::new(bytes);
let codec = cur.u8()?;
if codec != 3 && codec != 4 {
return Ok(false);
}
let flag = cur.u8()?;
let mask = match flag {
0 | 1 => None,
2 => {
let at = cur.at;
let len = rows.div_ceil(8);
cur.take(len)?;
Some((at, len))
}
_ => return Err(invalid("page validity tag differs")),
};
let valid = |row: usize| match flag {
0 => true,
1 => false,
2 => mask.is_some_and(|(at, _)| bytes[at + row / 8] >> (row % 8) & 1 == 1),
_ => unreachable!("the validity tag was checked"),
};
if codec == 4 {
let wide = integer::decode_selected(&bytes[cur.at..], positions)?;
for (&row, code) in positions.iter().zip(wide) {
let code = u32::try_from(code).map_err(|_| invalid("code is not a code"))?;
out.push(valid(row).then_some(code));
}
return Ok(true);
}
let codes_at = cur.at;
let codes_len = rows.checked_mul(4).ok_or_else(|| invalid("page size overflow"))?;
cur.take(codes_len)?;
if cur.at != bytes.len() {
return Err(invalid("global code page has trailing bytes"));
}
let codes = &bytes[codes_at..codes_at + codes_len];
for &row in positions {
let at = row.checked_mul(4).ok_or_else(|| invalid("dictionary code offset overflow"))?;
let code = u32::from_le_bytes(
codes[at..at + 4].try_into().map_err(|_| invalid("dictionary code is truncated"))?,
);
out.push(valid(row).then_some(code));
}
Ok(true)
}
fn decode_at(
ty: &LogicalType,
rows: usize,
bytes: &[u8],
global: Option<Arc<Vector>>,
positions: &[u32],
) -> Result<Vector> {
if positions.last().is_some_and(|&last| last as usize >= rows) {
return Err(invalid("a position is past the end of the part"));
}
if bytes.first() != Some(&6) {
return decode(ty, rows, bytes, global)?.gather(positions);
}
if !coded_type(ty) {
return Err(invalid("compressed text codec belongs to a non-string page"));
}
let mut cur = Cursor::new(bytes);
cur.u8()?;
let validity = match cur.u8()? {
0 => Validity::AllValid,
1 => Validity::AllInvalid,
2 => {
let mask = cur.take(rows.div_ceil(8))?;
Validity::from_iter(positions.len(), |at| {
let row = positions[at] as usize;
mask[row / 8] >> (row % 8) & 1 == 1
})
}
_ => return Err(invalid("page validity tag differs")),
};
let (payload, ends) = string::decode_flat_at(&bytes[cur.at..], positions)?.into_parts();
let mut values = StringColumn::over(Buffer::from_vec(payload).into_page());
push_values(&mut values, ty, &ends)?;
Ok(Vector::flat(ty.clone(), Data::Varlen(values))?.with_validity(validity))
}
fn push_values(values: &mut StringColumn, ty: &LogicalType, ends: &[usize]) -> Result<()> {
if ty == &LogicalType::Varchar {
return values.push_run_in_place(0, ends);
}
let mut start = 0;
for &end in ends {
let len = end
.checked_sub(start)
.ok_or_else(|| invalid("a string value ends before it starts"))?;
values.push_bytes_in_place(start, len)?;
start = end;
}
Ok(())
}
fn decode(
ty: &LogicalType,
rows: usize,
bytes: &[u8],
global: Option<Arc<Vector>>,
) -> Result<Vector> {
let mut cur = Cursor::new(bytes);
let codec = cur.u8()?;
let flag = cur.u8()?;
let validity = match flag {
0 => Validity::AllValid,
1 => Validity::AllInvalid,
2 => {
let mask = cur.take(rows.div_ceil(8))?;
Validity::from_iter(rows, |row| mask[row / 8] >> (row % 8) & 1 == 1)
}
_ => return Err(invalid("page validity tag differs")),
};
if codec == 1 {
if !coded_type(ty) {
return Err(invalid("dictionary codec belongs to a non-string page"));
}
let count = cur.u32()? as usize;
let payload_len = cur.u32()? as usize;
let offset_bytes = cur.take(
(count + 1)
.checked_mul(4)
.ok_or_else(|| invalid("dictionary offset count overflow"))?,
)?;
let offsets = offset_bytes
.chunks_exact(4)
.map(|part| u32::from_le_bytes(part.try_into().expect("four bytes")))
.collect::<Vec<_>>();
let payload = cur.take(payload_len)?.to_vec();
if offsets.first() != Some(&0)
|| offsets.last().copied().map(|last| last as usize) != Some(payload.len())
|| offsets.windows(2).any(|pair| pair[0] > pair[1])
{
return Err(invalid("dictionary offsets do not bound the payload"));
}
let mut strings = StringColumn::over(Buffer::from_vec(payload).into_page());
let ends: Vec<usize> = offsets[1..].iter().map(|&end| end as usize).collect();
push_values(&mut strings, ty, &ends)?;
let mut codes = Vec::with_capacity(rows);
for _ in 0..rows {
codes.push(cur.u32()?);
}
if codes.iter().any(|code| *code as usize >= count) {
return Err(invalid("dictionary code is out of range"));
}
if cur.at != bytes.len() {
return Err(invalid("dictionary page has trailing bytes"));
}
let dictionary = Vector::flat(ty.clone(), Data::Varlen(strings))?;
return Ok(Vector::dictionary(codes, dictionary)?.with_validity(validity));
}
if codec == 3 || codec == 4 {
let dictionary = global.ok_or_else(|| invalid("global code page has no dictionary"))?;
let codes = if codec == 4 {
let wide = integer::decode(&bytes[cur.at..])?;
if wide.len() != rows {
return Err(invalid("encoded code page holds the wrong number of rows"));
}
let seen = wide.iter().fold(0_i64, |seen, &code| seen | code);
if seen < 0 || seen > i64::from(u32::MAX) {
return Err(invalid("code is not a code"));
}
wide.iter().map(|&code| code as u32).collect()
} else {
let mut codes = Vec::with_capacity(rows);
for _ in 0..rows {
codes.push(cur.u32()?);
}
if cur.at != bytes.len() {
return Err(invalid("global code page has trailing bytes"));
}
codes
};
let highest = codes.iter().copied().max();
return Ok(Vector::stable_dictionary_validated(codes, dictionary, highest)?
.with_validity(validity));
}
if codec == 6 {
if !coded_type(ty) {
return Err(invalid("compressed text codec belongs to a non-string page"));
}
let (payload, ends) = string::decode_flat(&bytes[cur.at..])?.into_parts();
if ends.len() != rows {
return Err(invalid("compressed text page holds the wrong number of rows"));
}
let mut values = StringColumn::over(Buffer::from_vec(payload).into_page());
push_values(&mut values, ty, &ends)?;
return Ok(Vector::flat(ty.clone(), Data::Varlen(values))?.with_validity(validity));
}
if codec == 5 {
let values = integer::decode(&bytes[cur.at..])?;
if values.len() != rows {
return Err(invalid("cascade page holds the wrong number of rows"));
}
let data = narrowed(ty, values)?;
return Ok(Vector::flat(ty.clone(), data)?.with_validity(validity));
}
if codec == 2 {
let width = u32::from(cur.u8()?);
let base = i128::from_le_bytes(cur.take(16)?.try_into().expect("sixteen bytes"));
let count = cur.u32()? as usize;
let length = count.checked_mul(8).ok_or_else(|| invalid("packed page is too long"))?;
let words: Vec<u64> = cur
.take(length)?
.chunks_exact(8)
.map(|word| u64::from_le_bytes(word.try_into().expect("eight bytes")))
.collect();
if cur.at != bytes.len() {
return Err(invalid("packed page has trailing bytes"));
}
return Ok(Vector::packed(ty.clone(), words, width, base, rows)?.with_validity(validity));
}
if codec != 0 {
return Err(invalid("page codec is unknown"));
}
let data = match ty {
LogicalType::TinyInt => {
let values = cur.take(rows)?;
Data::Int8(values.iter().map(|item| *item as i8).collect::<Vec<_>>().into())
}
LogicalType::UTinyInt => Data::UInt8(cur.take(rows)?.to_vec().into()),
LogicalType::SmallInt => {
let values =
cur.take(rows.checked_mul(2).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int16(
values
.chunks_exact(2)
.map(|item| i16::from_le_bytes(item.try_into().expect("two bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::USmallInt => {
let values =
cur.take(rows.checked_mul(2).ok_or_else(|| invalid("page size overflow"))?)?;
Data::UInt16(
values
.chunks_exact(2)
.map(|item| u16::from_le_bytes(item.try_into().expect("two bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::UInteger => {
let values =
cur.take(rows.checked_mul(4).ok_or_else(|| invalid("page size overflow"))?)?;
Data::UInt32(
values
.chunks_exact(4)
.map(|item| u32::from_le_bytes(item.try_into().expect("four bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::UBigInt => {
let values =
cur.take(rows.checked_mul(8).ok_or_else(|| invalid("page size overflow"))?)?;
Data::UInt64(
values
.chunks_exact(8)
.map(|item| u64::from_le_bytes(item.try_into().expect("eight bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::Integer | LogicalType::Date => {
let values =
cur.take(rows.checked_mul(4).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int32(
values
.chunks_exact(4)
.map(|item| i32::from_le_bytes(item.try_into().expect("four bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::BigInt
| LogicalType::Timestamp
| LogicalType::Time
| LogicalType::TimeTz
| LogicalType::TimestampTz
| LogicalType::TimestampS
| LogicalType::TimestampMs
| LogicalType::TimestampNs => {
let values =
cur.take(rows.checked_mul(8).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int64(
values
.chunks_exact(8)
.map(|item| i64::from_le_bytes(item.try_into().expect("eight bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::HugeInt | LogicalType::Uuid => {
let values =
cur.take(rows.checked_mul(16).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int128(
values
.chunks_exact(16)
.map(|item| i128::from_le_bytes(item.try_into().expect("sixteen bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::UHugeInt => {
let values =
cur.take(rows.checked_mul(16).ok_or_else(|| invalid("page size overflow"))?)?;
Data::UInt128(
values
.chunks_exact(16)
.map(|item| u128::from_le_bytes(item.try_into().expect("sixteen bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::Float => {
let values =
cur.take(rows.checked_mul(4).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Float32(
values
.chunks_exact(4)
.map(|item| f32::from_le_bytes(item.try_into().expect("four bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::Double => {
let values =
cur.take(rows.checked_mul(8).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Float64(
values
.chunks_exact(8)
.map(|item| f64::from_le_bytes(item.try_into().expect("eight bytes")))
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::Interval => {
let values =
cur.take(rows.checked_mul(16).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Interval(
values
.chunks_exact(16)
.map(|item| {
(
i32::from_le_bytes(item[..4].try_into().expect("four bytes")),
i32::from_le_bytes(item[4..8].try_into().expect("four bytes")),
i64::from_le_bytes(item[8..].try_into().expect("eight bytes")),
)
})
.collect::<Vec<_>>()
.into(),
)
}
LogicalType::Boolean => {
let values = cur.take(rows)?;
if values.iter().any(|value| *value > 1) {
return Err(invalid("boolean page has another value"));
}
Data::Bool(values.iter().map(|value| *value == 1).collect::<Vec<_>>().into())
}
LogicalType::Decimal { .. } => match ty.physical() {
PhysicalType::Int16 => {
let values =
cur.take(rows.checked_mul(2).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int16(
values
.chunks_exact(2)
.map(|item| i16::from_le_bytes(item.try_into().expect("two bytes")))
.collect::<Vec<_>>()
.into(),
)
}
PhysicalType::Int32 => {
let values =
cur.take(rows.checked_mul(4).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int32(
values
.chunks_exact(4)
.map(|item| i32::from_le_bytes(item.try_into().expect("four bytes")))
.collect::<Vec<_>>()
.into(),
)
}
PhysicalType::Int64 => {
let values =
cur.take(rows.checked_mul(8).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int64(
values
.chunks_exact(8)
.map(|item| i64::from_le_bytes(item.try_into().expect("eight bytes")))
.collect::<Vec<_>>()
.into(),
)
}
_ => {
let values =
cur.take(rows.checked_mul(16).ok_or_else(|| invalid("page size overflow"))?)?;
Data::Int128(
values
.chunks_exact(16)
.map(|item| i128::from_le_bytes(item.try_into().expect("sixteen bytes")))
.collect::<Vec<_>>()
.into(),
)
}
},
LogicalType::Varchar | LogicalType::Blob | LogicalType::Bit => {
let offset_bytes = cur
.take((rows + 1).checked_mul(4).ok_or_else(|| invalid("offset count overflow"))?)?;
let offsets = offset_bytes
.chunks_exact(4)
.map(|part| u32::from_le_bytes(part.try_into().expect("four bytes")))
.collect::<Vec<_>>();
let payload = cur.take(bytes.len() - cur.at)?.to_vec();
if offsets.first() != Some(&0)
|| offsets.last().copied().map(|last| last as usize) != Some(payload.len())
|| offsets.windows(2).any(|pair| pair[0] > pair[1])
{
return Err(invalid("string offsets do not bound the payload"));
}
let mut values = StringColumn::over(Buffer::from_vec(payload).into_page());
let ends: Vec<usize> = offsets[1..].iter().map(|&end| end as usize).collect();
push_values(&mut values, ty, &ends)?;
Data::Varlen(values)
}
_ => return Err(Error::not_implemented(format!("native page for {ty}"))),
};
if cur.at != bytes.len() {
return Err(invalid("page has trailing bytes"));
}
Ok(Vector::flat(ty.clone(), data)?.with_validity(validity))
}
#[cfg(test)]
mod tests {
use std::fs::{self, OpenOptions};
use std::io::{Seek, SeekFrom, Write};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rudb_common::Stat;
use rudb_common::Value;
use rudb_common::bounds::{Frequencies, Op, Remainder, Zones};
use rudb_common::stat::Provenance;
use super::*;
#[test]
fn a_name_taken_in_pieces_is_the_name_of_the_pieces_joined() {
let bytes: Vec<u8> =
(0..300_u32).map(|at| (at.wrapping_mul(2_654_435_761) >> 13) as u8).collect();
for length in [0, 1, 7, 31, 32, 33, 63, 64, 65, 100, 300] {
let whole = content_name(&bytes[..length]);
for step in [1, 3, 8, 31, 32, 33, 64, 301] {
let mut namer = ContentNamer::default();
bytes[..length].chunks(step).for_each(|piece| namer.update(piece));
assert_eq!(namer.finish(), whole, "{length} bytes in pieces of {step}");
}
}
}
#[derive(Debug)]
struct TestsEverything<'a>(&'a dyn chooser::Chooser);
impl chooser::Chooser for TestsEverything<'_> {
fn name(&self) -> &'static str {
"tests everything"
}
fn narrow_strings(
&self,
values: &[&[u8]],
offered: &[string::Kind],
depth: u8,
) -> Vec<string::Kind> {
self.0.narrow_strings(values, offered, depth)
}
fn narrow_integers(
&self,
values: &[i64],
offered: &[integer::Kind],
depth: u8,
) -> Vec<integer::Kind> {
self.0.narrow_integers(values, offered, depth)
}
}
#[test]
fn ruling_kinds_out_before_testing_for_them_writes_the_same_bytes() {
let columns: Vec<Vec<i64>> = vec![
vec![],
vec![5; 1000],
(0..1000).collect(),
(0..1000).map(|row| 1_600_000_000_000_000 + row * 1_000_000).collect(),
(0..1000).map(|row| row / 50).collect(),
(0..1000).map(|row| if row % 97 == 0 { row } else { 0 }).collect(),
(0..1000).map(|row| (row * 7919) % 13).collect(),
(0..1000).map(|row| (row * 2_654_435_761) % 1_000_003).collect(),
(0..1000).map(|row| [3, 3, 3, 9, 9, 1][row as usize % 6]).collect(),
(0..1000).map(|row| i64::MIN + row % 3).collect(),
];
let choosers: [&dyn chooser::Chooser; 2] = [&Fixed, &Codes];
for column in &columns {
for chooser in choosers {
let quick = integer::encode_with(column, chooser).unwrap();
let full = integer::encode_with(column, &TestsEverything(chooser)).unwrap();
assert_eq!(
quick,
full,
"{} on {:?}",
chooser.name(),
&column[..column.len().min(8)]
);
}
}
}
#[test]
fn parts_that_look_alike_replay_to_the_bytes_a_search_writes() {
let mut settling = Settling::default();
for part in 0..STRIPE_PARTS as i64 {
let values: Vec<i64> = (0..2048)
.map(|row| 1_600_000_000_000_000 + (part * 2048 + row) * 1_000_000 + row % 7)
.collect();
let searched = integer::encode_with(&values, &Fixed).unwrap();
assert_eq!(settling.encode(&values).unwrap(), searched, "part {part}");
}
}
#[test]
fn a_column_that_changes_under_the_shape_is_searched_again() {
let mut state = 0x9e37_79b9_7f4a_7c15_u64;
let mut noise = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state % 1_000_000) as i64
};
let mut settling = Settling::default();
for part in 0..STRIPE_PARTS as i64 {
let values: Vec<i64> = match part / 16 {
0 => (0..2048).map(|row| (part * 2048 + row) / 300).collect(),
1 => (0..2048).map(|_| noise()).collect(),
2 => (0..2048).map(|row| if row % 97 == 0 { row } else { 42 }).collect(),
_ => (0..2048).map(|row| 5 + (part * 2048 + row) * 1_000_000).collect(),
};
let settled = settling.encode(&values).unwrap();
assert_eq!(integer::decode(&settled).unwrap(), values, "part {part}");
let searched = integer::encode_with(&values, &Fixed).unwrap();
assert!(
settled.len() * 4 <= searched.len() * 5,
"part {part}: {} settled against {} searched, {} against {}",
settled.len(),
searched.len(),
integer::describe(&settled).unwrap(),
integer::describe(&searched).unwrap(),
);
}
}
#[test]
fn checksum_matches_fixed_vectors() {
assert_eq!(checksum(b""), 0xef46_db37_51d8_e999);
assert_eq!(checksum(b"a"), 0xd24e_c4f1_a98c_6e5b);
assert_eq!(checksum(b"abc"), 0x44bc_2cf5_ad77_0999);
}
#[test]
fn sorting_across_threads_matches_sorting_on_one() {
let mut state = 0x9e37_79b9_7f4a_7c15_u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let mut values = Vec::new();
for at in 0..150_000_u64 {
let value = match next() % 6 {
0 => Vec::new(),
1 => format!("https://example.com/{}", next() % 5_000).into_bytes(),
2 => format!("https://example.com/path/{at}").into_bytes(),
3 => b"same".to_vec(),
4 => vec![0xff; (next() % 12) as usize],
_ => (0..next() % 20).map(|_| (next() % 3) as u8).collect(),
};
values.push(value);
}
let value = |code: u32| values[code as usize].as_slice();
for workers in [1, 2, 3, 8, 32] {
let mut one = (0..values.len() as u32).rev().collect::<Vec<_>>();
let mut across = one.clone();
sort_by_value(&mut one, value);
sort_by_value_across(&mut across, value, workers);
assert_eq!(one, across, "{workers} workers");
}
let mut sorted = (0..values.len() as u32).collect::<Vec<_>>();
sort_by_value_across(&mut sorted, value, 8);
assert!(sorted.windows(2).all(|pair| value(pair[0]) <= value(pair[1])));
}
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-native-{label}-{}-{stamp}.rdb", std::process::id()))
}
fn dictionary_values(dictionary: &GlobalDictionary) -> Vec<Vec<u8>> {
let (flat, bases) = dictionary.decoded(None).expect("the blocks decode");
(0..dictionary.values())
.map(|code| {
let (from, to) = GlobalDictionary::value_span(&dictionary.ends, &bases, code);
flat[from..to].to_vec()
})
.collect()
}
fn attached(table: &Table) -> Vec<&Section> {
table.sections().iter().filter(|held| !held.among(section::STATISTICS_KINDS)).collect()
}
#[test]
fn a_read_at_an_offset_ignores_where_another_thread_left_the_cursor() {
const SPANS: usize = 64;
const SPAN: usize = 512;
let path = path("positional");
let content: Vec<u8> =
(0..SPANS).flat_map(|span| std::iter::repeat_n(span as u8, SPAN)).collect();
fs::write(&path, &content).expect("the file is written");
let file = Arc::new(File::open(&path).expect("the file opens"));
std::thread::scope(|scope| {
for _ in 0..8 {
let file = Arc::clone(&file);
scope.spawn(move || {
for _ in 0..64 {
for span in 0..SPANS {
let mut bytes = [0_u8; SPAN];
read_at(&file, (span * SPAN) as u64, &mut bytes)
.expect("the span reads");
assert!(
bytes.iter().all(|byte| *byte == span as u8),
"span {span} came back as {}",
bytes[0],
);
}
}
});
}
});
let mut past = [0_u8; SPAN];
let end = (SPANS * SPAN) as u64;
let error = read_at(&file, end, &mut past).expect_err("a read past the end is refused");
assert!(error.message().contains("ends before its declared length"), "{error}");
drop(file);
let _ = fs::remove_file(&path);
}
#[test]
fn a_writer_puts_a_page_where_it_said_it_did_wherever_the_cursor_has_got_to() {
let path = path("cursor");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::Integer),
Field::new("text", LogicalType::Varchar),
],
)
.expect("new file");
writer.append(&sample()).expect("first part");
writer.append(&sample()).expect("second part");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert_eq!(reader.table().rows(), 6);
let ids = reader.read(0, &[0]).expect("the integer page reads back");
assert_eq!(ids.value_at(0, 0), Value::Integer(4));
assert_eq!(ids.value_at(2, 0), Value::Integer(-2));
let text = reader.read(1, &[1]).expect("the text page reads back");
assert_eq!(text.value_at(1, 0), Value::Null);
assert_eq!(text.value_at(2, 0), Value::Varchar("long text after a slash".into()));
let end = reader.table().stripes().iter().flat_map(|stripe| {
stripe
.pages
.iter()
.map(|page| page.offset + u64::from(page.length))
.chain(std::iter::once(stripe.index.offset + u64::from(stripe.index.length)))
});
let last = end.fold(HEADER, u64::max);
let directory = fs::metadata(&path).expect("the file is there").len();
assert!(last <= directory, "a page runs to {last} in a file of {directory} bytes");
fs::remove_file(path).expect("remove scratch file");
}
fn dictionary_index_len(header: &[u8; DICTIONARY_HEADER]) -> u64 {
let count = u64::from(u32::from_le_bytes(header[0..4].try_into().expect("four bytes")));
let blocks = u64::from(u32::from_le_bytes(header[8..12].try_into().expect("four bytes")));
let width = u32::from_le_bytes(header[12..16].try_into().expect("four bytes"));
let bits = (width & !DICTIONARY_FLAGS) as usize;
let payload_words = if width & DICTIONARY_SCATTERED == 0 { 2 } else { 3 };
let rank_blocks = count.div_ceil(TEXT_RANK_BLOCK as u64);
DICTIONARY_HEADER as u64
+ offset_bytes(count as usize, bits) as u64
+ blocks * payload_words * 8
+ rank_blocks * 16
+ if width & DICTIONARY_GRAMS == 0 { 0 } else { 8 }
}
fn sample() -> Chunk {
Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&[Value::Integer(4), Value::Integer(9), Value::Integer(-2)],
)
.expect("integers"),
Vector::from_values(
LogicalType::Varchar,
&[
Value::Varchar("alpha".into()),
Value::Null,
Value::Varchar("long text after a slash".into()),
],
)
.expect("strings"),
])
.expect("matching rows")
}
fn sample_ids() -> Chunk {
Chunk::new(vec![
Vector::flat(LogicalType::Integer, Data::Int32(vec![7, 8, 9].into()))
.expect("integers"),
])
.expect("one column")
}
#[test]
fn the_planner_gets_the_null_count_off_the_same_directory_the_bounds_are_in() {
let path = path("nulls_for_the_planner");
let mut writer =
Writer::create(&path, "items", vec![Field::new("a", LogicalType::Integer)])
.expect("new file");
let rows = Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&[
Value::Integer(4),
Value::Null,
Value::Integer(9),
Value::Null,
Value::Integer(1),
Value::Integer(2),
],
)
.expect("integers"),
])
.expect("one column");
writer.append(&rows).expect("the only part");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let stripes = Stripes::new(reader);
let column = stripes.column("a").expect("the file has that column");
assert_eq!(stripes.nulls(column), Stat::exact(2, Provenance::NullCount));
assert_eq!(stripes.nulls(column + 1), Stat::Unknown);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn the_planner_gets_a_leading_count_without_a_complete_numeric_synopsis() {
let path = path("frequencies_for_the_planner");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
let rows = Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&[
Value::Integer(4),
Value::Integer(4),
Value::Integer(4),
Value::Integer(9),
Value::Integer(9),
Value::Integer(1),
],
)
.expect("integers"),
])
.expect("one column");
writer.append(&rows).expect("the only part");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let common = Common::new(reader);
assert_eq!(common.rows(), 6);
let column = common.column("id").expect("the file has that column");
assert_eq!(common.column("nothing"), None);
assert_eq!(
common.rows_with(column, &Bound::Int(4)),
Stat::exact(3, Provenance::FrequencySynopsis)
);
assert_eq!(common.rows_with(column, &Bound::Int(7)), Stat::Unknown);
assert_eq!(common.rows_with(column, &Bound::Bytes(b"four".to_vec())), Stat::Unknown);
assert!(common.remainder(column).is_some());
fs::remove_file(&path).expect("clean up");
}
#[test]
fn string_frequency_estimates_do_not_open_the_global_dictionary() {
let path = path("string_frequencies_for_the_planner");
let mut writer =
Writer::create(&path, "items", vec![Field::required("text", LogicalType::Varchar)])
.expect("new file");
let rows = Chunk::new(vec![
Vector::from_values(
LogicalType::Varchar,
&[
Value::Varchar(String::new()),
Value::Varchar("alpha".into()),
Value::Varchar(String::new()),
Value::Varchar("beta".into()),
Value::Varchar(String::new()),
],
)
.expect("strings"),
])
.expect("one column");
writer.append(&rows).expect("the only part");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert_eq!(reader.reads().dictionaries, 0, "open reads only the directory");
let common = Common::new(reader.clone());
let column = common.column("text").expect("the file has that column");
assert_eq!(
common.rows_with(column, &Bound::Bytes(Vec::new())),
Stat::exact(3, Provenance::FrequencySynopsis)
);
assert_eq!(
common.rows_with(column, &Bound::Bytes(b"missing".to_vec())),
Stat::exact(0, Provenance::FrequencySynopsis)
);
assert_eq!(
reader.reads().dictionaries,
0,
"the bounded spellings answer without opening the dictionary index"
);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn host_groups_certify_omitted_hosts_and_keep_exact_aggregates() {
let path = path("certified_host_groups");
let mut writer =
Writer::create(&path, "hits", vec![Field::required("Referer", LogicalType::Varchar)])
.expect("new file");
let mut values = vec![Value::Varchar("http://www.example.com/a".into()); 150];
values.extend(vec![Value::Varchar("https://example.com/b".into()); 70]);
values.extend((0..550).map(|at| Value::Varchar(format!("https://site{at}.test/x"))));
values.push(Value::Varchar(String::new()));
for part in values.chunks(512) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("part written");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen");
assert!(reader.table.host_groups.is_none(), "no query-specific host result is stored");
fs::remove_file(&path).expect("clean up");
}
fn bare_table(sections: Vec<Section>) -> Table {
Table {
name: "linked".to_owned(),
fields: vec![Field::required("id", LogicalType::Integer)],
stripes: Vec::new(),
rows: 0,
dictionaries: vec![None],
dictionary_payloads: Vec::new(),
demoted: Vec::new(),
distincts: vec![None],
frequencies: vec![None],
pair_frequencies: Vec::new(),
frequency_texts: Vec::new(),
host_groups: None,
clustering: None,
generation: 1,
sections,
}
}
fn a_key_map_section() -> Section {
Section {
kind: *section::KEY_MAP,
id: 1,
generation: 3,
extents: 1,
extent_page: HEADER,
extent_bytes: section::EXTENT_BYTES as u32,
hash: 0x1234_5678_9abc_def0,
flags: 0,
header_bytes: 24,
}
}
#[test]
fn a_section_table_round_trips_through_a_directory() {
let mut later = a_key_map_section();
later.kind = *b"RUDBZZ9\0";
later.id = 2;
let table = bare_table(vec![a_key_map_section(), later]);
let directory = encode_directory(&table).expect("directory");
let decoded = decode_directory(&directory, 1 << 20).expect("reopen");
assert_eq!(decoded.sections(), &[a_key_map_section(), later]);
assert!(decoded.sections()[0].known());
assert!(!decoded.sections()[1].known());
}
#[test]
fn a_directory_written_before_the_section_table_reads_as_a_table_with_none() {
let directory = encode_directory(&bare_table(Vec::new())).expect("directory");
let block = SECTIONS.len() + size_of::<u64>() + size_of::<u16>();
let older = &directory[..directory.len() - block];
let decoded = decode_directory(older, 1 << 20).expect("a directory from before sections");
assert!(decoded.sections().is_empty());
assert_eq!(decoded.generation(), 0, "a format 22 table recorded no generation");
assert_eq!(decoded.name(), "linked");
assert_eq!(decoded.fields().len(), 1, "everything before the block still decodes");
}
#[test]
fn a_file_stamped_with_the_previous_format_still_opens_and_reads() {
let path = path("format_twenty_two");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
let rows = Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&[Value::Integer(1), Value::Integer(2), Value::Integer(3)],
)
.expect("integers"),
])
.expect("one column");
writer.append(&rows).expect("the only part");
writer.finish().expect("commit");
let file = OpenOptions::new().write(true).open(&path).expect("reopen to patch");
write_at(&file, 8, &22_u32.to_le_bytes()).expect("stamp the older format");
drop(file);
let reader = Reader::open(&path).expect("a format 22 file opens unchanged");
assert_eq!(reader.table().rows(), 3);
assert_eq!(reader.read(0, &[0]).expect("the part still reads").len(), 3);
let file = OpenOptions::new().write(true).open(&path).expect("reopen to patch");
write_at(&file, 8, &21_u32.to_le_bytes()).expect("stamp an unreadable format");
drop(file);
let error = Reader::open(&path).expect_err("format 21 is not readable");
assert!(error.to_string().contains("format 21"), "{error}");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_section_whose_extent_table_is_outside_the_file_is_refused() {
let mut past = a_key_map_section();
past.extent_page = 1 << 30;
let directory = encode_directory(&bare_table(vec![past])).expect("directory");
let error = decode_directory(&directory, 1 << 20).expect_err("refused");
assert!(error.to_string().contains("outside the file"), "{error}");
let mut inside_the_header = a_key_map_section();
inside_the_header.extent_page = 8;
let directory = encode_directory(&bare_table(vec![inside_the_header])).expect("directory");
assert!(
decode_directory(&directory, 1 << 20).is_err(),
"a section may not overlap a header"
);
}
#[test]
fn a_section_recorded_as_not_built_is_legal_and_names_no_bytes() {
let not_built = Section {
kind: *section::FORWARD_LINK,
id: 9,
generation: 3,
extents: 0,
extent_page: 0,
extent_bytes: 0,
hash: 0,
flags: 0,
header_bytes: 0,
};
let directory = encode_directory(&bare_table(vec![not_built])).expect("directory");
let decoded = decode_directory(&directory, 1 << 20).expect("reopen");
assert_eq!(decoded.sections(), &[not_built]);
let mut incoherent = not_built;
incoherent.extent_bytes = 28;
incoherent.extent_page = HEADER;
let directory = encode_directory(&bare_table(vec![incoherent])).expect("directory");
assert!(decode_directory(&directory, 1 << 20).is_err());
}
#[test]
fn a_directory_naming_more_sections_than_the_bound_is_refused() {
let directory = encode_directory(&bare_table(Vec::new())).expect("directory");
let mut torn = directory.clone();
let count_at = torn.len() - size_of::<u16>();
torn[count_at..].copy_from_slice(&u16::MAX.to_le_bytes());
assert!(decode_directory(&torn, 1 << 20).is_err());
}
fn linked_file(label: &str, rows: i32) -> PathBuf {
let path = path(label);
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
let values = (0..rows).map(Value::Integer).collect::<Vec<_>>();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::Integer, &values).expect("integers")])
.expect("one column");
writer.append(&chunk).expect("the only part");
writer.finish().expect("commit");
path
}
fn a_key_map_payload() -> Vec<u8> {
(0..512_u32).flat_map(u32::to_le_bytes).collect()
}
#[test]
fn a_section_attached_to_a_committed_file_reads_back_byte_for_byte() {
let path = linked_file("attach", 64);
let payload = a_key_map_payload();
let table = attach(
&path,
"items",
&[section::Attachment {
kind: *section::KEY_MAP,
id: 0,
flags: 2,
header_bytes: 40,
bytes: &payload,
}],
)
.expect("attach a key map");
assert_eq!(attached(&table).len(), 1);
let reader = Reader::open(&path).expect("reopen after the attach");
let held = attached(reader.table());
assert_eq!(held.len(), 1);
assert_eq!(held[0].kind, *section::KEY_MAP);
assert_eq!(held[0].flags, 2, "the form a reader must not have to guess");
assert_eq!(held[0].header_bytes, 40);
assert_eq!(held[0].generation, 1);
assert!(held[0].usable(reader.table().generation()));
assert_eq!(reader.payload(held[0]).expect("read the payload"), payload);
assert_eq!(reader.extents(held[0]).expect("extent table").len(), 1);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn attaching_a_section_answers_every_row_exactly_as_before() {
let path = linked_file("attach_changes_nothing", 300);
let before = Reader::open(&path).expect("open before");
let rows = before.table().rows();
let first = before.read(0, &[0]).expect("read before");
let values = (0..rows).map(|at| first.value_at(at, 0)).collect::<Vec<_>>();
let layout = before.layout().columns_total();
drop(before);
let payload = a_key_map_payload();
attach(
&path,
"items",
&[section::Attachment {
kind: *section::KEY_MAP,
id: 0,
flags: 0,
header_bytes: 0,
bytes: &payload,
}],
)
.expect("attach");
let after = Reader::open(&path).expect("open after");
assert_eq!(after.table().rows(), rows);
let read = after.read(0, &[0]).expect("read after");
for (at, value) in values.iter().enumerate() {
assert_eq!(&read.value_at(at, 0), value, "row {at} moved");
}
assert_eq!(
after.layout().columns_total(),
layout,
"an attach appends and does not rewrite a column page"
);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_rebuilt_section_replaces_the_one_it_supersedes() {
let path = linked_file("attach_twice", 32);
let one = a_key_map_payload();
let two = vec![7_u8; 1024];
let entry = |bytes| section::Attachment {
kind: *section::KEY_MAP,
id: 4,
flags: 1,
header_bytes: 0,
bytes,
};
attach(&path, "items", &[entry(&one)]).expect("first build");
attach(&path, "items", &[entry(&two)]).expect("rebuild");
let reader = Reader::open(&path).expect("reopen");
let held = attached(reader.table());
assert_eq!(held.len(), 1, "one map per column and not one per build");
assert_eq!(reader.payload(held[0]).expect("payload"), two);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn an_attach_carries_through_a_kind_it_does_not_know() {
let path = linked_file("attach_unknown", 16);
let payload = vec![3_u8; 96];
attach(
&path,
"items",
&[section::Attachment {
kind: *b"RUDBZZ9\0",
id: 1,
flags: 0,
header_bytes: 0,
bytes: &payload,
}],
)
.expect("a kind this build does not know still writes");
let key_map = a_key_map_payload();
attach(
&path,
"items",
&[section::Attachment {
kind: *section::KEY_MAP,
id: 0,
flags: 0,
header_bytes: 0,
bytes: &key_map,
}],
)
.expect("attach beside it");
let reader = Reader::open(&path).expect("reopen");
let held = attached(reader.table());
assert_eq!(held.len(), 2, "the unfamiliar entry survived a directory rewrite");
let unknown = held.iter().find(|one| !one.known()).expect("the unfamiliar one");
assert_eq!(reader.payload(unknown).expect("its bytes are still there"), payload);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_payload_of_nothing_is_a_relationship_recorded_as_not_built() {
let path = linked_file("attach_not_built", 8);
attach(
&path,
"items",
&[section::Attachment {
kind: *section::FORWARD_LINK,
id: 2,
flags: 0,
header_bytes: 0,
bytes: &[],
}],
)
.expect("record a link that did not fit the budget");
let reader = Reader::open(&path).expect("reopen");
let held = attached(reader.table());
assert_eq!(held.len(), 1);
assert_eq!(held[0].extents, 0);
assert_eq!(held[0].extent_page, 0, "an entry that names no bytes points at none");
assert!(reader.extents(held[0]).expect("no extent table").is_empty());
assert!(reader.payload(held[0]).expect("no payload").is_empty());
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_payload_past_one_extent_is_split_and_joined_back() {
let path = linked_file("attach_two_extents", 8);
let payload = vec![0x5a_u8; section::MAX_EXTENT as usize + 1];
attach(
&path,
"items",
&[section::Attachment {
kind: *section::KEY_MAP,
id: 0,
flags: 0,
header_bytes: 0,
bytes: &payload,
}],
)
.expect("attach a payload past the bound");
let reader = Reader::open(&path).expect("reopen");
let held = attached(reader.table());
let extents = reader.extents(held[0]).expect("extent table");
assert_eq!(extents.len(), 2, "one byte past the bound is two extents");
assert_eq!(extents[0].length, section::MAX_EXTENT);
assert_eq!(extents[1].length, 1);
assert_eq!(extents[1].first, u64::from(section::MAX_EXTENT));
assert_eq!(reader.extent(&extents[1]).expect("the last extent"), vec![0x5a]);
assert_eq!(reader.payload(held[0]).expect("the whole payload").len(), payload.len());
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_torn_extent_is_refused_rather_than_decoded() {
let path = linked_file("attach_torn", 8);
let payload = a_key_map_payload();
attach(
&path,
"items",
&[section::Attachment {
kind: *section::KEY_MAP,
id: 0,
flags: 0,
header_bytes: 0,
bytes: &payload,
}],
)
.expect("attach");
let reader = Reader::open(&path).expect("reopen");
let extent = reader.extents(&reader.table().sections()[0]).expect("extent table")[0];
let file = OpenOptions::new().write(true).open(&path).expect("reopen to corrupt");
write_at(&file, extent.offset + 7, &[0xff]).expect("flip a byte of the payload");
drop(file);
let reader = Reader::open(&path).expect("the table still opens");
let error = reader
.payload(&reader.table().sections()[0])
.expect_err("a corrupt payload is not handed out");
assert!(error.to_string().contains("checksum"), "{error}");
assert_eq!(reader.read(0, &[0]).expect("the column is untouched").width(), 1);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn attaching_to_a_file_of_the_previous_format_is_refused_rather_than_done() {
let path = linked_file("attach_old_format", 8);
let file = OpenOptions::new().write(true).open(&path).expect("reopen to patch");
write_at(&file, 8, &22_u32.to_le_bytes()).expect("stamp the older format");
drop(file);
let payload = a_key_map_payload();
let error = attach(
&path,
"items",
&[section::Attachment {
kind: *section::KEY_MAP,
id: 0,
flags: 0,
header_bytes: 0,
bytes: &payload,
}],
)
.expect_err("format 22 cannot gain a section");
assert!(error.to_string().contains("format 22"), "{error}");
assert!(Reader::open(&path).expect("and the file is untouched").table().rows() == 8);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_section_header_longer_than_its_payload_is_refused_at_the_write() {
let path = linked_file("attach_bad_header", 8);
let error = attach(
&path,
"items",
&[section::Attachment {
kind: *section::KEY_MAP,
id: 0,
flags: 0,
header_bytes: 40,
bytes: &[1, 2, 3],
}],
)
.expect_err("a writer's bug stops at the write");
assert!(error.to_string().contains("header is longer"), "{error}");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn attaching_to_a_name_the_file_does_not_hold_says_so() {
let path = linked_file("attach_wrong_name", 8);
let error = attach(&path, "orders", &[]).expect_err("no such table");
assert!(error.to_string().contains("orders"), "{error}");
fs::remove_file(&path).expect("clean up");
}
#[test]
fn the_planner_gets_an_exact_count_for_a_leading_value_of_an_incomplete_synopsis() {
let path = path("frequency_prefix_for_the_planner");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
let mut values = vec![Value::Integer(1); 10_000];
for _ in 0..10 {
values.extend((0..600).map(|tail| Value::Integer(1_000 + tail)));
}
for part in values.chunks(8_000) {
let rows = Chunk::new(vec![
Vector::from_values(LogicalType::Integer, part).expect("integers"),
])
.expect("one column");
writer.append(&rows).expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let prefix =
reader.frequency_prefix(0).expect("a readable synopsis").expect("the column has one");
assert_eq!(prefix.entries.len(), 512);
assert_eq!(prefix.omitted_max, 10);
let common = Common::new(reader);
assert_eq!(common.rows(), 16_000);
let column = common.column("id").expect("the file has that column");
assert_eq!(
common.rows_with(column, &Bound::Int(1)),
Stat::exact(10_000, Provenance::FrequencySynopsis)
);
assert_eq!(
common.rows_with(column, &Bound::Int(1_100)),
Stat::exact(10, Provenance::FrequencySynopsis)
);
assert_eq!(common.rows_with(column, &Bound::Int(1_550)), Stat::Unknown);
assert_eq!(common.rows_with(column, &Bound::Int(9_999)), Stat::Unknown);
let remainder = common.remainder(column).expect("the list is a prefix");
assert_eq!(remainder, Remainder { rows: 890, listed: 512, most: 10 });
assert_eq!(remainder.rows / (601 - remainder.listed), 10);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_file_holding_no_table_commits_and_opens_and_a_table_can_be_added_to_it() {
let path = path("empty");
Writer::empty(&path, &[]).expect("a file with nothing in it");
let catalog = Catalog::open(&path).expect("the empty file opens");
assert_eq!(catalog.len(), 0);
assert!(catalog.is_empty());
assert_eq!(catalog.names().count(), 0);
let mut writer =
Writer::open(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("a table goes into the empty file");
writer.append(&sample_ids()).expect("rows");
writer.finish().expect("commit");
let catalog = Catalog::open(&path).expect("the file opens again");
assert_eq!(catalog.names().collect::<Vec<_>>(), vec!["items"]);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_committed_empty_table_gives_up_its_name_and_one_with_rows_does_not() {
let path = path("empty-name");
let field = || vec![Field::required("id", LogicalType::Integer)];
Writer::create(&path, "items", field()).expect("new file").finish().expect("commit");
let catalog = Catalog::open(&path).expect("the file opens");
assert_eq!(catalog.rows().collect::<Vec<_>>(), vec![("items", 0)]);
let mut writer = Writer::open(&path, "items", field()).expect("the empty name is free");
writer.append(&sample_ids()).expect("rows");
writer.finish().expect("commit");
let catalog = Catalog::open(&path).expect("the file opens again");
assert_eq!(catalog.names().collect::<Vec<_>>(), vec!["items"]);
let held = catalog.rows().collect::<Vec<_>>();
assert_eq!(held.len(), 1);
assert!(held[0].1 > 0, "the rows that were appended are the ones the catalog counts");
let error = Writer::open(&path, "items", field()).expect_err("a name with rows is taken");
assert!(error.to_string().contains("same name"), "{error}");
fs::remove_file(&path).expect("clean up");
}
fn sample_view(name: &str) -> ViewEntry {
ViewEntry {
name: name.to_string(),
sql: "SELECT id FROM items WHERE id > 0".to_string(),
statement: format!("CREATE VIEW {name} AS SELECT id FROM items WHERE (id > 0);"),
aliases: vec!["n".to_string()],
columns: vec![Field::new("n", LogicalType::Integer)],
}
}
#[test]
fn a_view_written_into_the_catalog_comes_back_whole() {
let path = path("views");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
writer.append(&sample_ids()).expect("rows");
writer.with_views(vec![sample_view("v")]).finish().expect("commit");
let catalog = Catalog::open(&path).expect("reopen");
assert_eq!(catalog.views().cloned().collect::<Vec<_>>(), vec![sample_view("v")]);
assert_eq!(catalog.names().collect::<Vec<_>>(), vec!["items"]);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn appending_a_table_carries_the_views_forward() {
let path = path("viewscarry");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
writer.append(&sample_ids()).expect("rows");
writer.with_views(vec![sample_view("v")]).finish().expect("commit");
let mut writer =
Writer::open(&path, "other", vec![Field::required("id", LogicalType::Integer)])
.expect("a second table");
writer.append(&sample_ids()).expect("rows");
writer.finish().expect("commit");
let catalog = Catalog::open(&path).expect("reopen");
assert_eq!(catalog.views().count(), 1);
assert_eq!(catalog.names().collect::<Vec<_>>(), vec!["items", "other"]);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn restating_the_views_leaves_every_table_where_it_was() {
let path = path("restate");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
writer.append(&sample_ids()).expect("rows");
writer.finish().expect("commit");
let before = fs::metadata(&path).expect("the file is there").len();
Writer::restate(&path, &[sample_view("v"), sample_view("w")]).expect("two views");
let catalog = Catalog::open(&path).expect("reopen");
assert_eq!(catalog.views().count(), 2);
assert_eq!(catalog.names().collect::<Vec<_>>(), vec!["items"]);
let after = fs::metadata(&path).expect("the file is there").len();
assert!(after > before, "a generation was written");
assert!(after - before < before, "the table was not written again");
let reader = Catalog::open(&path).expect("reopen").table("items").expect("the table");
assert_eq!(reader.table().rows, 3);
Writer::restate(&path, &[]).expect("no views at all");
assert_eq!(Catalog::open(&path).expect("reopen").views().count(), 0);
fs::remove_file(&path).expect("clean up");
}
#[test]
fn a_view_named_after_a_table_is_refused_when_the_catalog_is_read() {
let bytes = encode_catalog(
&[Entry {
name: "items".to_string(),
fields: vec![Field::required("id", LogicalType::Integer)],
rows: 1,
directory: Page { offset: HEADER, length: 8, hash: 0 },
nonzero: vec![None],
aggregates: vec![None],
distincts: vec![None],
extremes: vec![None],
frequencies: vec![None],
}],
&[sample_view("items")],
)
.expect("it encodes, because encoding does not look");
let error = decode_catalog(&bytes, HEADER + 8).expect_err("and decoding does");
assert!(error.to_string().contains("same name"), "{error}");
}
#[test]
fn a_compressed_text_page_read_at_some_rows_is_those_rows_of_the_whole() {
let rows: usize = 300;
let text: Vec<String> =
(0..rows).map(|row| format!("a street named after number {}", row * 7)).collect();
let values: Vec<&[u8]> = text.iter().map(String::as_bytes).collect();
let mut page = vec![6, 2];
page.extend((0..rows.div_ceil(8)).map(|byte| {
(0..8).filter(|bit| (byte * 8 + bit) % 5 != 3).fold(0_u8, |mask, bit| mask | 1 << bit)
}));
let compressed = string::encode_only(string::Kind::Fsst, &values)
.expect("encoded")
.expect("text this repetitive compresses");
page.extend_from_slice(&compressed);
let whole = decode(&LogicalType::Varchar, rows, &page, None).expect("the whole page");
let positions = [0_u32, 3, 8, 13, 200, 299];
let some =
decode_at(&LogicalType::Varchar, rows, &page, None, &positions).expect("some rows");
assert_eq!(some.len(), positions.len());
for (at, &row) in positions.iter().enumerate() {
assert_eq!(some.value_at(at), whole.value_at(row as usize), "row {row}");
}
assert_eq!(some.value_at(1), Value::Null, "row 3 is null");
assert!(decode_at(&LogicalType::Varchar, rows, &page, None, &[300]).is_err());
assert!(decode_at(&LogicalType::Varchar, rows, &page, None, &[8, 3]).is_err());
}
#[test]
fn a_part_read_at_some_rows_is_the_part_read_whole_and_gathered() {
let path = path("rows");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::Integer),
Field::new("text", LogicalType::Varchar),
],
)
.expect("new file");
let rows = 2_000;
let chunk = Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&(0..rows).map(Value::Integer).collect::<Vec<_>>(),
)
.expect("integers"),
Vector::from_values(
LogicalType::Varchar,
&(0..rows)
.map(|row| {
if row % 7 == 2 {
Value::Null
} else {
Value::Varchar(format!("a comment about order {}", row * 13))
}
})
.collect::<Vec<_>>(),
)
.expect("strings"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let positions = [1_u32, 2, 9, 1_000, 1_999];
for whole in [true, false] {
let some = reader.read_rows(0, &[0, 1], &positions, whole).expect("some rows");
let all = reader.read(0, &[0, 1]).expect("the whole part");
assert_eq!(some.len(), positions.len());
for column in 0..2 {
for (at, &row) in positions.iter().enumerate() {
assert_eq!(some.value_at(at, column), all.value_at(row as usize, column));
}
}
}
assert!(reader.read_rows(0, &[1], &[2_000], true).is_err());
}
#[test]
fn committed_file_reopens_and_reads_only_requested_columns() {
let path = path("reopen");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::Integer),
Field::new("text", LogicalType::Varchar),
],
)
.expect("new file");
writer.append(&sample()).expect("first part");
writer.append(&sample()).expect("second part");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert_eq!(reader.table().rows(), 6);
assert_eq!(reader.table().stripes().len(), 1);
assert_eq!(reader.parts(), 2);
assert_eq!(reader.part_rows(0), 3);
assert_eq!(reader.part_rows(1), 3);
let text = reader.read(1, &[1]).expect("only text page");
assert_eq!(text.width(), 1);
assert_eq!(text.value_at(1, 0), Value::Null);
assert_eq!(text.value_at(2, 0), Value::Varchar("long text after a slash".into()));
let sparse = reader.read_sparse(1, &[1]).expect("one part without its whole page");
assert_eq!(sparse.width(), 1);
assert_eq!(sparse.value_at(1, 0), Value::Null);
assert_eq!(sparse.value_at(2, 0), Value::Varchar("long text after a slash".into()));
assert!(!reader.skips_codes(0, 1, &[0]).expect("alpha is in the stripe"));
assert!(!reader.skips_codes(0, 1, &[2]).expect("long text is in the stripe"));
assert!(reader.skips_codes(0, 1, &[3]).expect("unknown code is absent"));
let count = reader.read(0, &[]).expect("no page is needed for count");
assert_eq!(count.len(), 3);
assert!(reader.skips(0, &[Probe { column: 0, op: Op::Greater, value: Bound::Int(100) }]));
assert!(!reader.skips(0, &[Probe { column: 0, op: Op::Greater, value: Bound::Int(0) }]));
assert_eq!(reader.top_frequencies(0, 1).expect("valid integer synopsis"), None);
let integers = reader.frequency_prefix(0).expect("valid integer synopsis").expect("kept");
assert_eq!(integers.entries, vec![(Value::Integer(-2), 2), (Value::Integer(4), 2)]);
assert_eq!(integers.omitted_max, 2);
let strings = reader.top_frequencies(1, 1).expect("valid string synopsis").expect("kept");
assert_eq!(strings.len(), 3);
assert!(strings.contains(&(Value::Null, 2)));
assert!(strings.contains(&(Value::Varchar("alpha".into()), 2)));
assert!(strings.contains(&(Value::Varchar("long text after a slash".into()), 2)));
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn runs_handed_over_out_of_order_still_read_back_in_source_order() {
let path = path("interleaved-runs");
let mut writer =
Writer::create(&path, "interleaved", vec![Field::new("v", LogicalType::BigInt)])
.expect("new file");
for morsel in [2_u64, 0, 3, 1] {
let parts = (0..4_u64)
.map(|chunk| {
let first = i64::try_from(morsel * 32 + chunk * 8).expect("small");
let values =
(0..8_i64).map(|row| Value::BigInt(first + row)).collect::<Vec<_>>();
let column =
Vector::from_values(LogicalType::BigInt, &values).expect("a column");
((morsel, chunk), Chunk::new(vec![column]).expect("one column"))
})
.collect::<Vec<_>>();
writer.append_stripe(parts).expect("a stripe");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
assert_eq!(reader.table().stripes().len(), 4, "a run is a stripe of its own");
assert_eq!(reader.table().rows(), 128);
for part in 0..16_usize {
let read = reader.read(part, &[0]).expect("a part back");
for row in 0..8_usize {
let want = i64::try_from(part * 8 + row).expect("small");
assert_eq!(read.value_at(row, 0), Value::BigInt(want), "part {part} row {row}");
}
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn runs_that_overlap_each_other_are_refused_at_commit() {
let path = path("overlapping-runs");
let mut writer =
Writer::create(&path, "overlapping", vec![Field::new("v", LogicalType::BigInt)])
.expect("new file");
let one = |order: (u64, u64)| {
let column =
Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1)]).expect("a column");
(order, Chunk::new(vec![column]).expect("one column"))
};
writer.append_stripe(vec![one((0, 0)), one((0, 2))]).expect("a stripe");
writer.append_stripe(vec![one((0, 1))]).expect("a stripe");
let error = writer.finish().expect_err("the runs overlap");
assert!(error.message().contains("source order"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_run_longer_than_a_stripe_is_refused() {
let path = path("overlong-run");
let mut writer =
Writer::create(&path, "overlong", vec![Field::new("v", LogicalType::BigInt)])
.expect("new file");
let parts = (0..=STRIPE_PARTS)
.map(|at| {
let column = Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1)])
.expect("a column");
let chunk = Chunk::new(vec![column]).expect("one column");
((0, u64::try_from(at).expect("small")), chunk)
})
.collect::<Vec<_>>();
let error = writer.append_stripe(parts).expect_err("one part too many");
assert!(error.message().contains("more parts than it holds"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn parts_past_the_stripe_bound_start_a_new_stripe() {
let path = path("stripe-bound");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::Integer),
Field::new("text", LogicalType::Varchar),
],
)
.expect("new file");
let parts = STRIPE_PARTS * 2 + 3;
for part in 0..parts {
let id = part as i32;
let chunk = Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&[Value::Integer(id), Value::Integer(-id)],
)
.expect("integers"),
Vector::from_values(
LogicalType::Varchar,
&[Value::Varchar(format!("value {part}")), Value::Null],
)
.expect("strings"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert_eq!(reader.parts(), parts);
assert_eq!(reader.table().rows(), parts * 2);
assert_eq!(reader.table().stripes().len(), parts.div_ceil(STRIPE_PARTS));
assert_eq!(reader.table().stripes()[0].parts(), STRIPE_PARTS);
assert_eq!(reader.table().stripes()[0].rows(), STRIPE_PARTS * 2);
assert_eq!(reader.table().stripes()[2].parts(), 3);
for part in (0..parts).rev() {
let dense = reader.read(part, &[0, 1]).expect("a whole page read");
let sparse = reader.read_sparse(part, &[0, 1]).expect("one part read");
for chunk in [&dense, &sparse] {
assert_eq!(chunk.len(), 2, "part {part} has its own row count");
assert_eq!(chunk.value_at(0, 0), Value::Integer(part as i32));
assert_eq!(chunk.value_at(1, 0), Value::Integer(-(part as i32)));
assert_eq!(chunk.value_at(0, 1), Value::Varchar(format!("value {part}")));
assert_eq!(chunk.value_at(1, 1), Value::Null);
}
}
let above = [Probe { column: 0, op: Op::Greater, value: Bound::Int(100) }];
assert!(reader.skips(0, &above), "the first stripe stops at 63");
assert!(!reader.skips(STRIPE_PARTS * 2, &above), "the third stripe reaches 130");
fs::remove_file(path).expect("remove scratch file");
}
fn scattered(n: i64) -> i64 {
n.wrapping_mul(-7_046_029_254_386_353_131)
}
#[test]
fn a_part_is_skipped_when_its_sieve_does_not_hold_the_constant() {
let path = path("sieve-skip");
let mut writer =
Writer::create(&path, "hits", vec![Field::required("id", LogicalType::BigInt)])
.expect("new file");
let parts = STRIPE_PARTS + 3;
let per_part = 128;
for part in 0..parts {
let held: Vec<Value> = (0..per_part)
.map(|row| Value::BigInt(scattered((part * per_part + row) as i64)))
.collect();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("numbers")])
.expect("one column");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let probe = |value: i64| Probe {
column: 0,
op: Op::Equal,
value: Bound::Int(i128::from(scattered(value))),
};
for wanted in [0_i64, (per_part + 1) as i64, (parts * per_part - 1) as i64] {
let tests = [probe(wanted)];
let kept: Vec<usize> = (0..parts).filter(|&part| !reader.skips(part, &tests)).collect();
let home = wanted as usize / per_part;
assert!(kept.contains(&home), "the part holding {wanted} is read");
assert!(kept.len() <= 2, "{wanted} keeps {kept:?}, which is more than one stray part");
}
let absent = [probe((parts * per_part) as i64 + 1)];
let kept = (0..parts).filter(|&part| !reader.skips(part, &absent)).count();
assert!(kept <= 1, "{kept} parts of {parts} kept a value no part holds");
let tests = [probe(0)];
assert!(
reader.table().stripes().iter().all(|stripe| !stripe.zone.skips(&tests)),
"the bounds rule out no stripe at all"
);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_part_is_skipped_when_its_own_bounds_rule_out_a_comparison_the_stripe_keeps() {
let path = path("part-range-skip");
let mut writer =
Writer::create(&path, "hits", vec![Field::required("at", LogicalType::BigInt)])
.expect("new file");
let parts = STRIPE_PARTS + 3;
let per_part = 128;
for part in 0..parts {
let held: Vec<Value> = (0..per_part)
.map(|row| {
Value::BigInt((part * 1_000) as i64 + (scattered(row as i64).rem_euclid(900)))
})
.collect();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("numbers")])
.expect("one column");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let under = [Probe { column: 0, op: Op::Less, value: Bound::Int(3_000) }];
let kept: Vec<usize> = (0..parts).filter(|&part| !reader.skips(part, &under)).collect();
assert_eq!(kept, vec![0, 1, 2], "only the three parts that start under three thousand");
assert!(!reader.stripe_skips(0, &under), "the stripe reaches from zero and keeps itself");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_part_is_waved_through_when_its_own_bounds_pass_a_comparison_the_stripe_cannot() {
let path = path("part-range-certain");
let mut writer =
Writer::create(&path, "hits", vec![Field::required("at", LogicalType::BigInt)])
.expect("new file");
let parts = STRIPE_PARTS + 3;
let per_part = 128;
for part in 0..parts {
let held: Vec<Value> = (0..per_part)
.map(|row| {
Value::BigInt((part * 1_000) as i64 + (scattered(row as i64).rem_euclid(900)))
})
.collect();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("numbers")])
.expect("one column");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let under = [Probe { column: 0, op: Op::Less, value: Bound::Int(3_000) }];
let waved: Vec<usize> = (0..parts).filter(|&part| reader.certain(part, &under)).collect();
assert_eq!(waved, vec![0, 1, 2], "the three parts that end under three thousand");
assert!(!reader.stripe_skips(0, &under), "the stripe straddles the comparison");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_stripe_of_one_part_writes_no_range_page_and_a_stripe_of_many_does() {
for (parts, wanted) in [(1_usize, false), (STRIPE_PARTS, true)] {
let path = path("part-range-page");
let mut writer =
Writer::create(&path, "hits", vec![Field::required("at", LogicalType::BigInt)])
.expect("new file");
for part in 0..parts {
let held: Vec<Value> = (0..128)
.map(|row| {
Value::BigInt((part * 1_000) as i64 + scattered(row as i64).rem_euclid(900))
})
.collect();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &held).expect("numbers"),
])
.expect("one column");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let bytes = reader.layout().columns[0].part_ranges;
assert_eq!(bytes > 0, wanted, "{parts} parts wrote {bytes} bytes of ranges");
fs::remove_file(path).expect("remove scratch file");
}
}
#[test]
fn a_string_end_that_is_cut_down_still_covers_the_value_it_came_from() {
let long = vec![b'a'; PART_BOUND_BYTES * 2];
let low = shortened(Some(Bound::Bytes(long.clone())), false).expect("a low end");
let high = shortened(Some(Bound::Bytes(long.clone())), true).expect("a high end");
let Bound::Bytes(low) = low else { panic!("a string stays a string") };
let Bound::Bytes(high) = high else { panic!("a string stays a string") };
assert!(low.len() <= PART_BOUND_BYTES && high.len() <= PART_BOUND_BYTES);
assert!(low.as_slice() <= long.as_slice(), "the low end is at or under the value");
assert!(high.as_slice() >= long.as_slice(), "the high end is at or over the value");
}
#[test]
fn a_string_end_with_no_room_to_step_up_gives_up_the_bound() {
let long = vec![u8::MAX; PART_BOUND_BYTES * 2];
assert_eq!(shortened(Some(Bound::Bytes(long.clone())), true), None);
let low = shortened(Some(Bound::Bytes(long)), false).expect("a low end is still a prefix");
assert_eq!(low, Bound::Bytes(vec![u8::MAX; PART_BOUND_BYTES]));
}
#[test]
fn what_a_column_is_stored_as_follows_the_order_the_rows_were_written_in() {
let parts = 4;
let per_part = 1024;
let rows = parts * per_part;
let written = |name: &str, keys: &[i64]| {
let path = path(name);
let fields = vec![Field::required("key", LogicalType::BigInt)];
let mut writer = Writer::create(&path, "keys", fields).expect("new file");
for part in 0..parts {
let values: Vec<Value> = keys[part * per_part..(part + 1) * per_part]
.iter()
.map(|key| Value::BigInt(*key))
.collect();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &values).expect("numbers"),
])
.expect("one column");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
path
};
let climbing = |step: &dyn Fn(usize) -> i64| {
let mut key = 0;
(0..rows)
.map(|row| {
key += step(row);
key
})
.collect::<Vec<i64>>()
};
let ascending = climbing(&|row| (row % 3) as i64);
let sparse = climbing(&|row| ((row * 2_654_435_761) % 4096) as i64);
let near_path = written("stored-near", &ascending);
let far_path = written("stored-far", &sparse);
let one = Reader::open(&near_path).expect("reopen from disk");
let other = Reader::open(&far_path).expect("reopen from disk");
let near = one.stored(0).expect("the column is stored");
let far = other.stored(0).expect("the column is stored");
assert_eq!(near.len(), parts, "one row per part");
assert_eq!(far.len(), parts);
let total = |stored: &[StoredPart]| stored.iter().map(|part| part.bytes).sum::<u64>();
assert_eq!(total(&near), one.layout().columns[0].pages);
assert_eq!(total(&far), other.layout().columns[0].pages);
assert!(
total(&near) * 2 < total(&far),
"the sparse keys cost more, {} against {}",
total(&far),
total(&near)
);
for (at, part) in near.iter().enumerate() {
assert_eq!(part.part, at);
assert_eq!(part.row, at * per_part);
assert_eq!(part.rows, per_part);
let held = &ascending[at * per_part..(at + 1) * per_part];
assert_eq!(part.low, Some(Value::BigInt(held[0])));
assert_eq!(part.high, Some(Value::BigInt(held[per_part - 1])));
assert_eq!(part.nulls, Some(0));
}
assert!(near[0].encoding.contains("DELTA"), "{}", near[0].encoding);
assert!(far[0].encoding.contains("DELTA"), "{}", far[0].encoding);
assert_ne!(near[0].encoding, far[0].encoding);
fs::remove_file(near_path).expect("remove scratch file");
fs::remove_file(far_path).expect("remove scratch file");
}
#[test]
fn a_sieve_larger_than_the_part_it_indexes_is_not_written() {
let path = path("sieve-pays");
let fields = vec![
Field::required("spread", LogicalType::BigInt),
Field::required("repeated", LogicalType::BigInt),
];
let mut writer = Writer::create(&path, "hits", fields).expect("new file");
let parts = 3;
let per_part = 1024;
for part in 0..parts {
let base = (part * per_part) as i64;
let spread: Vec<Value> =
(0..per_part).map(|row| Value::BigInt(scattered(base + row as i64))).collect();
let repeated: Vec<Value> =
(0..per_part).map(|row| Value::BigInt(scattered((row / 256) as i64))).collect();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &spread).expect("numbers"),
Vector::from_values(LogicalType::BigInt, &repeated).expect("numbers"),
])
.expect("two columns");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let layout = reader.layout();
let spread = &layout.columns[0];
let repeated = &layout.columns[1];
assert!(spread.sieves > 0, "a column whose parts are worth a filter keeps one");
assert_eq!(
repeated.sieves, 0,
"a column whose filter costs more than its parts keeps none"
);
for column in &layout.columns {
assert!(
column.sieves < column.pages,
"{} spends {} on sieves over {} of data",
column.name,
column.sieves,
column.pages
);
}
let absent = [Probe {
column: 0,
op: Op::Equal,
value: Bound::Int(i128::from(scattered((parts * per_part) as i64 + 1))),
}];
assert!((0..parts).all(|part| reader.skips(part, &absent)), "no part holds it");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_damaged_sieve_page_is_read_through_rather_than_refused() {
let path = path("sieve-damaged");
let mut writer =
Writer::create(&path, "hits", vec![Field::required("id", LogicalType::BigInt)])
.expect("new file");
let rows = 128;
let held: Vec<Value> = (0..rows).map(|row| Value::BigInt(scattered(row))).collect();
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::BigInt, &held).expect("numbers")])
.expect("one column");
writer.append(&chunk).expect("one part");
writer.finish().expect("commit");
let page = Reader::open(&path).expect("reopen").table.stripes[0]
.sieves
.get(0)
.expect("a sieve page");
let mut file = OpenOptions::new().write(true).open(&path).expect("open the sieve page");
file.seek(SeekFrom::Start(page.offset + u64::from(page.length) - 1)).expect("seek");
file.write_all(&[0xff]).expect("damage one byte");
drop(file);
let reader = Reader::open(&path).expect("reopen the damaged file");
let absent =
[Probe { column: 0, op: Op::Equal, value: Bound::Int(i128::from(scattered(99))) }];
assert!(!reader.skips(0, &absent), "a sieve that cannot be read skips nothing");
assert_eq!(
reader.read(0, &[0]).expect("the rows are untouched").len(),
usize::try_from(rows).expect("a small count")
);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn workers_that_want_the_same_stripe_read_it_once() {
let path = path("single-flight");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
for part in 0..STRIPE_PARTS {
let id = part as i32;
let chunk = Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&[Value::Integer(id), Value::Integer(-id)],
)
.expect("integers"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert_eq!(reader.table().stripes().len(), 1, "one stripe is the point of the test");
let barrier = std::sync::Barrier::new(8);
std::thread::scope(|scope| {
for worker in 0..8 {
let reader = &reader;
let barrier = &barrier;
scope.spawn(move || {
barrier.wait();
for part in (worker..STRIPE_PARTS).step_by(8) {
let chunk = reader.read(part, &[0]).expect("a whole page read");
assert_eq!(chunk.value_at(0, 0), Value::Integer(part as i32));
assert_eq!(chunk.value_at(1, 0), Value::Integer(-(part as i32)));
}
});
}
});
assert_eq!(reader.pages.load(Atomic::Relaxed), 1, "one stripe, one page read, whoever won");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn opening_costs_the_same_over_a_thousand_times_the_rows() {
let opened = |label: &str, rows_per_part: i32| {
let path = path(label);
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
for part in 0..STRIPE_PARTS * 3 {
let values = (0..rows_per_part)
.map(|row| {
Value::Integer((part as i32 * rows_per_part + row).wrapping_mul(2_654_435))
})
.collect::<Vec<_>>();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Integer, &values).expect("integers"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let size = fs::metadata(&path).expect("the file is there").len();
let out = (reader.reads(), reader.table().stripes().len(), size);
fs::remove_file(path).expect("remove scratch file");
out
};
let (thin, thin_stripes, thin_size) = opened("open-thin", 1);
let (fat, fat_stripes, fat_size) = opened("open-fat", 1000);
assert_eq!(
thin_stripes, fat_stripes,
"the same stripe count is what makes this a fair ask"
);
assert!(
fat_size > thin_size * 50,
"the fat file has to actually be larger, and it is {fat_size} against {thin_size}"
);
assert_eq!(thin.opening.reads, fat.opening.reads, "the same reads either way");
assert_eq!(thin.pages, 0, "opening read a page");
assert_eq!(fat.pages, 0, "opening read a page");
assert_eq!(thin.indexes, 0, "opening read an index");
assert_eq!(fat.indexes, 0, "opening read an index");
assert!(
fat.opening.bytes < thin.opening.bytes * 2,
"opening the thin file read {} bytes and the fat one read {}",
thin.opening.bytes,
fat.opening.bytes
);
}
#[test]
fn two_opens_of_one_file_cost_the_same_and_the_second_is_not_cheaper() {
let path = path("open-twice");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
for part in 0..STRIPE_PARTS * 3 {
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Integer, &[Value::Integer(part as i32)])
.expect("integers"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let first = Reader::open(&path).expect("open");
for part in 0..first.parts() {
first.read(part, &[0]).expect("a part");
}
assert!(first.reads().pages > 0, "the scan has to have read something");
let second = Reader::open(&path).expect("open again");
assert_eq!(first.reads().opening, second.reads().opening);
assert_eq!(
second.reads().pages,
0,
"the second open read a page off the back of the first"
);
assert_eq!(second.reads().indexes, 0, "the second open read an index it inherited");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn an_index_is_read_once_per_stripe_however_often_the_page_is_evicted() {
let path = path("index-cache");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
let parts = STRIPE_PARTS * (CACHED_STRIPES_PER_COLUMN + 2);
for part in 0..parts {
let id = part as i32;
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Integer, &[Value::Integer(id)]).expect("integers"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let stripes = reader.table().stripes().len();
assert!(stripes > CACHED_STRIPES_PER_COLUMN, "the page cache has to be too small for this");
for _ in 0..2 {
for part in 0..parts {
let chunk = reader.read(part, &[0]).expect("a part");
assert_eq!(chunk.value_at(0, 0), Value::Integer(part as i32));
}
}
assert_eq!(reader.indexes.load(Atomic::Relaxed), stripes, "one index read per stripe");
assert!(
reader.pages.load(Atomic::Relaxed) > stripes,
"the pages are the ones that get read again, which is what makes the index count mean \
something"
);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_pool_keeps_pages_between_scans_and_gives_them_to_the_table_being_read() {
let path = path("page-pool");
let parts = STRIPE_PARTS * (CACHED_STRIPES_PER_COLUMN * 2 + 2);
let fields = || vec![Field::required("id", LogicalType::Integer)];
let mut writer = Writer::create(&path, "a", fields()).expect("new file");
for table in ["a", "b"] {
if table == "b" {
writer = writer.next("b".to_string(), fields()).expect("a second table");
}
for part in 0..parts {
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Integer, &[Value::Integer(part as i32)])
.expect("integers"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
}
}
writer.finish().expect("commit");
let pool = PagePool::new(usize::MAX);
let catalog = Catalog::open_in(&path, &pool).expect("the file opens");
let (a, b) = (catalog.table("a").expect("a"), catalog.table("b").expect("b"));
let stripes = a.table().stripes().len();
assert!(
stripes > CACHED_STRIPES_PER_COLUMN * 2,
"the floor has to be smaller than a table"
);
let scan = |reader: &Reader| {
for part in 0..parts {
let chunk = reader.read(part, &[0]).expect("a part");
assert_eq!(chunk.value_at(0, 0), Value::Integer(part as i32));
}
};
scan(&a);
assert_eq!(pool.bytes(), 0, "a page read once is not the pool's");
scan(&a);
let twice = stripes * 2 - CACHED_STRIPES_PER_COLUMN;
assert_eq!(a.pages.load(Atomic::Relaxed), twice, "the second scan reads the rest again");
scan(&a);
assert_eq!(a.pages.load(Atomic::Relaxed), twice, "the third scan reads nothing");
let one = pool.bytes();
assert!(one > 0, "the pool counts what the reader holds");
pool.budget.store(one, Atomic::Relaxed);
scan(&b);
scan(&b);
assert_eq!(b.pages.load(Atomic::Relaxed), twice, "a page is never let go while in use");
assert_eq!(a.cache.held[0].load(Atomic::Relaxed), CACHED_STRIPES_PER_COLUMN);
let column = a.cache.columns[0].lock().expect("the column");
let held = column.pages.iter().flatten().count();
assert_eq!(
held,
CACHED_STRIPES_PER_COLUMN + column.passing.len(),
"the count and the slots agree"
);
drop(column);
drop((a, b, catalog));
let c = Catalog::open_in(&path, &pool).expect("again").table("a").expect("a");
scan(&c);
scan(&c);
assert!(pool.bytes() <= one, "only what the live reader holds is counted");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_worker_per_stripe_reads_its_page_once_when_the_cache_was_told_to_expect_it() {
let workers = CACHED_STRIPES_PER_COLUMN + 4;
let path = path("stripe-per-worker");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
for part in 0..STRIPE_PARTS * workers {
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Integer, &[Value::Integer(part as i32)])
.expect("integers"),
])
.expect("matching rows");
writer.append(&chunk).expect("one part");
}
writer.finish().expect("commit");
let read = |told: bool| {
let reader = Reader::open(&path).expect("reopen from disk");
assert_eq!(reader.table().stripes().len(), workers, "a stripe per worker");
if told {
reader.keep_stripes(workers);
}
let barrier = std::sync::Barrier::new(workers);
std::thread::scope(|scope| {
for (worker, run) in reader.stripe_parts().into_iter().enumerate() {
let reader = &reader;
let barrier = &barrier;
scope.spawn(move || {
for part in run {
barrier.wait();
let chunk = reader.read(part, &[0]).expect("a part of my own stripe");
assert_eq!(chunk.value_at(0, 0), Value::Integer(part as i32));
}
assert!(worker < workers);
});
}
});
reader.pages.load(Atomic::Relaxed)
};
assert_eq!(read(true), workers, "one page read per stripe and no more");
assert!(read(false) > workers, "a cache that small is read again on every part");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_damaged_index_page_is_an_error() {
let path = path("damaged-index");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::Integer)])
.expect("new file");
writer.append(&sample_ids()).expect("first part");
writer.append(&sample_ids()).expect("second part");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let index = reader.table.stripes[0].index;
let mut byte = [0; 1];
read_at(&reader.file, index.offset, &mut byte).expect("the first part length");
let mut file = OpenOptions::new().write(true).open(&path).expect("open index page");
file.seek(SeekFrom::Start(index.offset)).expect("index start");
file.write_all(&[!byte[0]]).expect("damage the first part length");
let error = reader.read(1, &[0]).expect_err("a damaged index must not be used");
assert!(error.message().contains("index page section checksum differs"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn every_integer_width_round_trips_through_a_page() {
let path = path("integer-widths");
let columns = [
(LogicalType::TinyInt, vec![Value::TinyInt(i8::MIN), Value::TinyInt(i8::MAX)]),
(LogicalType::UTinyInt, vec![Value::UTinyInt(0), Value::UTinyInt(u8::MAX)]),
(LogicalType::SmallInt, vec![Value::SmallInt(i16::MIN), Value::SmallInt(i16::MAX)]),
(LogicalType::USmallInt, vec![Value::USmallInt(0), Value::USmallInt(u16::MAX)]),
(LogicalType::Integer, vec![Value::Integer(i32::MIN), Value::Integer(i32::MAX)]),
(LogicalType::UInteger, vec![Value::UInteger(0), Value::UInteger(u32::MAX)]),
(LogicalType::BigInt, vec![Value::BigInt(i64::MIN), Value::BigInt(i64::MAX)]),
(LogicalType::UBigInt, vec![Value::UBigInt(0), Value::UBigInt(u64::MAX)]),
];
let fields = columns
.iter()
.enumerate()
.map(|(at, (ty, _))| Field::required(format!("c{at}"), ty.clone()))
.collect::<Vec<_>>();
let vectors = columns
.iter()
.map(|(ty, values)| Vector::from_values(ty.clone(), values).expect("a vector"))
.collect::<Vec<_>>();
let mut writer = Writer::create(&path, "widths", fields).expect("new file");
writer.append(&Chunk::new(vectors).expect("matching rows")).expect("one stripe");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let wanted = (0..columns.len()).collect::<Vec<_>>();
let read = reader.read(0, &wanted).expect("every column");
assert_eq!(read.len(), 2);
for (at, (ty, values)) in columns.iter().enumerate() {
assert_eq!(read.value_at(0, at), values[0], "the low end of {ty}");
assert_eq!(read.value_at(1, at), values[1], "the high end of {ty}");
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn every_other_type_the_format_knows_round_trips_through_a_page() {
let path = path("other-types");
let columns = [
(LogicalType::Float, vec![Value::Float(f32::MIN), Value::Float(-0.0)]),
(LogicalType::Double, vec![Value::Double(f64::MIN), Value::Double(f64::MAX)]),
(LogicalType::HugeInt, vec![Value::HugeInt(i128::MIN), Value::HugeInt(i128::MAX)]),
(LogicalType::UHugeInt, vec![Value::UHugeInt(0), Value::UHugeInt(u128::MAX)]),
(LogicalType::Time, vec![Value::Time(0), Value::Time(86_399_999_999)]),
(LogicalType::TimeTz, vec![Value::TimeTz(-50_400_000_000), Value::TimeTz(0)]),
(
LogicalType::TimestampTz,
vec![Value::TimestampTz(i64::MIN + 1), Value::TimestampTz(i64::MAX)],
),
(
LogicalType::Interval,
vec![
Value::Interval { months: i32::MIN, days: i32::MAX, micros: i64::MIN },
Value::Interval { months: 13, days: -1, micros: 1 },
],
),
(
LogicalType::Blob,
vec![Value::Blob(vec![0, 0xff, 0x80, 0xfe]), Value::Blob(Vec::new())],
),
];
let fields = columns
.iter()
.enumerate()
.map(|(at, (ty, _))| Field::required(format!("c{at}"), ty.clone()))
.collect::<Vec<_>>();
let vectors = columns
.iter()
.map(|(ty, values)| Vector::from_values(ty.clone(), values).expect("a vector"))
.collect::<Vec<_>>();
let mut writer = Writer::create(&path, "others", fields).expect("new file");
writer.append(&Chunk::new(vectors).expect("matching rows")).expect("one stripe");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let wanted = (0..columns.len()).collect::<Vec<_>>();
let read = reader.read(0, &wanted).expect("every column");
assert_eq!(read.len(), 2);
for (at, (ty, values)) in columns.iter().enumerate() {
assert_eq!(read.value_at(0, at), values[0], "the low end of {ty}");
assert_eq!(read.value_at(1, at), values[1], "the high end of {ty}");
}
let Value::Float(zero) = read.value_at(1, 0) else { panic!("a float stays a float") };
assert!(zero.is_sign_negative(), "a negative zero came back as {zero}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_nan_survives_being_written_down() {
let path = path("nan");
let nan = Vector::from_values(LogicalType::Double, &[Value::Double(f64::NAN)])
.expect("a NaN vector");
let mut writer =
Writer::create(&path, "nan", vec![Field::required("d", LogicalType::Double)])
.expect("new file");
writer.append(&Chunk::new(vec![nan]).expect("one column")).expect("one stripe");
writer.finish().expect("commit");
let read = Reader::open(&path).expect("reopen").read(0, &[0]).expect("the column");
let Value::Double(back) = read.value_at(0, 0) else { panic!("a double stays a double") };
assert!(back.is_nan(), "a NaN came back as {back}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_uuid_and_a_bit_string_come_back_as_the_bits_that_went_in() {
let path = path("uuid-and-bit");
let uuids = vec![0_i128, i128::MIN, -1];
let mut bits = StringColumn::new();
for value in [&b"\x02\xff"[..], &b""[..], &b"\x00\x01\x02\x03\x04\x05"[..]] {
bits.push_bytes(value);
}
let expected = bits.clone();
let fields =
vec![Field::required("u", LogicalType::Uuid), Field::required("b", LogicalType::Bit)];
let vectors = vec![
Vector::flat(LogicalType::Uuid, Data::Int128(uuids.clone().into())).expect("uuids"),
Vector::flat(LogicalType::Bit, Data::Varlen(bits)).expect("bit strings"),
];
let mut writer = Writer::create(&path, "ids", fields).expect("new file");
writer.append(&Chunk::new(vectors).expect("matching rows")).expect("one stripe");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let read = reader.read(0, &[0, 1]).expect("both columns").flatten().expect("flat");
let Some(Data::Int128(back)) = read.column(0).expect("the uuids").data() else {
panic!("a uuid column is the 128 bit lane")
};
assert_eq!(back.as_slice(), uuids.as_slice());
let Some(Data::Varlen(back)) = read.column(1).expect("the bits").data() else {
panic!("a bit column is bytes")
};
for row in 0..expected.len() {
assert_eq!(back.bytes(row), expected.bytes(row), "row {row} of the bit column");
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_run_counted_at_once_leaves_the_candidates_a_row_at_a_time_would() {
let mut rows: Vec<Option<u64>> = Vec::new();
let mut state = 0x2545_f491_4f6c_dd1d_u64;
for index in 0..400_000_u64 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let times = 1 + (state % 7) as usize;
let bits = match state % 11 {
0 => None,
1..=3 => Some(state % 16),
_ => Some(index.wrapping_mul(0x9e37_79b9_7f4a_7c15)),
};
rows.extend(std::iter::repeat_n(bits, times));
}
let mut by_row = Candidates::default();
for &bits in &rows {
by_row.add(bits, 1);
}
let mut by_run = Candidates::default();
let mut run = Run::default();
let mut runs = 0_usize;
for &bits in &rows {
if let Some((bits, times)) = run.push(bits) {
by_run.add(bits, times);
runs += 1;
}
}
if let Some((bits, times)) = run.take() {
by_run.add(bits, times);
}
assert!(runs < rows.len() / 2, "the rows came in runs");
assert!(by_row.decrements > 0, "the table filled and turned values away");
assert_eq!(sorted_candidates(&by_run), sorted_candidates(&by_row));
assert_eq!(by_run.nulls, by_row.nulls);
assert_eq!(by_run.decrements, by_row.decrements);
}
fn sorted_candidates(candidates: &Candidates) -> Vec<(u64, u32)> {
let mut pairs = candidates.pairs().collect::<Vec<_>>();
pairs.sort_unstable();
assert_eq!(pairs.len(), candidates.held, "the count of held slots drifted");
pairs
}
#[derive(Default)]
struct MapCandidates {
counts: HashMap<u64, u32>,
nulls: u32,
decrements: u64,
}
impl MapCandidates {
fn add(&mut self, bits: Option<u64>, mut times: u32) {
while times > 0 {
let held = match bits {
Some(bits) => self.counts.get_mut(&bits),
None if self.nulls != 0 => Some(&mut self.nulls),
None => None,
};
if let Some(count) = held {
*count = count.saturating_add(times);
return;
}
if self.counts.len() + usize::from(self.nulls != 0) < FREQUENCY_CANDIDATES {
match bits {
Some(bits) => {
self.counts.insert(bits, times);
}
None => self.nulls = times,
}
return;
}
self.counts.retain(|_, count| {
*count -= 1;
*count != 0
});
self.nulls = self.nulls.saturating_sub(1);
self.decrements = self.decrements.saturating_add(1);
times -= 1;
}
}
}
#[test]
fn the_open_addressed_candidates_agree_with_the_map_they_replaced() {
for seed in [0x2545_f491_4f6c_dd1d_u64, 0x9e37_79b9_7f4a_7c15, 7] {
let mut table = Candidates::default();
let mut oracle = MapCandidates::default();
let mut state = seed;
for index in 0..300_000_u64 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let bits = match state % 13 {
0 => None,
1..=4 => Some(state % 40),
5 => Some((index % 1000) * 1_000_000),
_ => Some(state),
};
let times = 1 + (state >> 60) as u32 % 3;
table.add(bits, times);
oracle.add(bits, times);
if index % 50_000 == 0 {
let mut expected =
oracle.counts.iter().map(|(&b, &c)| (b, c)).collect::<Vec<_>>();
expected.sort_unstable();
assert_eq!(sorted_candidates(&table), expected, "seed {seed} row {index}");
}
}
let mut expected = oracle.counts.iter().map(|(&b, &c)| (b, c)).collect::<Vec<_>>();
expected.sort_unstable();
assert_eq!(sorted_candidates(&table), expected, "seed {seed}");
assert_eq!(table.nulls, oracle.nulls, "seed {seed}");
assert_eq!(table.decrements, oracle.decrements, "seed {seed}");
assert!(table.decrements > 0, "seed {seed} never filled the table");
for &(bits, _) in &expected {
assert!(table.position(bits).is_some(), "seed {seed} lost {bits}");
}
}
}
#[test]
fn numeric_frequency_candidates_keep_bounded_row_ordinals() {
let path = path("frequency-ordinals");
let mut writer =
Writer::create(&path, "items", vec![Field::required("id", LogicalType::BigInt)])
.expect("new file");
let mut values = Vec::new();
for leader in 0..10_i64 {
values.extend(std::iter::repeat_n(leader, 100));
}
values.extend(1_000_i64..41_000);
for part in values.chunks(1_024) {
let vector = Vector::flat(LogicalType::BigInt, Data::Int64(part.to_vec().into()))
.expect("big integers");
writer.append(&Chunk::new(vec![vector]).expect("one column")).expect("one stripe");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let occurrences =
reader.frequency_occurrences(0).expect("valid metadata").expect("bounded ordinals");
assert!(occurrences.omitted_max < 100);
assert!(occurrences.ordinals.len() <= FREQUENCY_ORDINALS);
assert_eq!(occurrences.anchor_indices.len(), occurrences.ordinals.len());
assert!(occurrences.ordinals.windows(2).all(|pair| pair[0] < pair[1]));
assert_eq!(&occurrences.ordinals[..1_000], &(0_u64..1_000).collect::<Vec<_>>());
assert_eq!(
&occurrences.anchor_indices[..1_000]
.iter()
.map(|&entry| occurrences.anchors[entry as usize].clone())
.collect::<Vec<_>>(),
&(0_i64..10)
.flat_map(|leader| std::iter::repeat_n(Value::BigInt(leader), 100))
.collect::<Vec<_>>()
);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn numeric_frequencies_count_nulls_and_values_past_the_top_of_bigint() {
let path = path("frequency-bits");
let mut writer = Writer::create(
&path,
"items",
vec![Field::new("u", LogicalType::UBigInt), Field::new("s", LogicalType::BigInt)],
)
.expect("new file");
let mut rows = Vec::new();
let mut leaders = Vec::new();
for leader in 0..10_u64 {
let count = 300 - leader * 10;
let (unsigned, signed) = if leader == 0 {
(Value::Null, Value::Null)
} else {
(Value::UBigInt(u64::MAX - leader), Value::BigInt(-(leader as i64)))
};
rows.extend(std::iter::repeat_n((unsigned.clone(), signed.clone()), count as usize));
leaders.push(((unsigned, count), (signed, count)));
}
rows.extend((1_000..41_000_u64).map(|id| (Value::UBigInt(id), Value::BigInt(id as i64))));
for part in rows.chunks(1_024) {
let unsigned = part.iter().map(|(value, _)| value.clone()).collect::<Vec<_>>();
let signed = part.iter().map(|(_, value)| value.clone()).collect::<Vec<_>>();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::UBigInt, &unsigned).expect("unsigned"),
Vector::from_values(LogicalType::BigInt, &signed).expect("signed"),
])
.expect("matching columns");
writer.append(&chunk).expect("rows");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
for column in 0..2 {
let prefix =
reader.frequency_prefix(column).expect("valid metadata").expect("a synopsis");
let wanted = leaders
.iter()
.map(|(unsigned, signed)| if column == 0 { unsigned } else { signed })
.cloned()
.collect::<Vec<_>>();
assert_eq!(&prefix.entries[..10], &wanted[..], "column {column}");
assert!(prefix.omitted_max < 210, "column {column}");
assert_eq!(
reader.distinct_values(column).expect("valid metadata"),
Some(9 + 40_000),
"column {column}"
);
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_narrow_column_takes_its_frequencies_from_the_tally_and_they_match_the_rows() {
let path = path("frequency-tally");
let types = [
LogicalType::TinyInt,
LogicalType::UInteger,
LogicalType::Date,
LogicalType::Timestamp,
];
let value = |ty: &LogicalType, at: i64| match ty {
LogicalType::TinyInt => Value::TinyInt((at % 250 - 125) as i8),
LogicalType::UInteger => Value::UInteger(u32::MAX - at as u32),
LogicalType::Date => Value::Date(19_000 - at as i32),
_ => Value::Timestamp(1_700_000_000_000_000 - at * 1_000_003),
};
let fields = types
.iter()
.enumerate()
.map(|(at, ty)| Field::new(format!("c{at}"), ty.clone()))
.collect::<Vec<_>>();
let mut writer = Writer::create(&path, "items", fields).expect("new file");
let mut rows = Vec::new();
for at in 0..250_i64 {
for _ in 0..=(at % 37) {
rows.push(if rows.len() % 13 == 0 { None } else { Some(at) });
}
}
for part in rows.chunks(1_000) {
let columns = types
.iter()
.map(|ty| {
let values = part
.iter()
.map(|row| row.map_or(Value::Null, |at| value(ty, at)))
.collect::<Vec<_>>();
Vector::from_values(ty.clone(), &values).expect("a column")
})
.collect();
writer.append(&Chunk::new(columns).expect("matching columns")).expect("rows");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
for (column, ty) in types.iter().enumerate() {
let mut counts = HashMap::<Option<i64>, u64>::new();
for row in &rows {
*counts.entry(*row).or_default() += 1;
}
let wanted = counts
.into_iter()
.map(|(row, count)| (row.map_or(Value::Null, |at| value(ty, at)), count))
.collect::<Vec<_>>();
let prefix =
reader.frequency_prefix(column).expect("valid metadata").expect("a synopsis");
assert_eq!(prefix.entries.len(), 2, "column {column}");
assert!(prefix.omitted_max > 0, "column {column}");
for (value, count) in &prefix.entries {
let held =
wanted.iter().find(|(wanted, _)| wanted == value).map(|(_, count)| count);
assert_eq!(held, Some(count), "column {column} value {value:?}");
}
assert!(prefix.entries.windows(2).all(|pair| pair[0].1 >= pair[1].1));
assert_eq!(
reader.distinct_values(column).expect("valid metadata"),
Some(wanted.len() as u64 - 1),
"column {column}"
);
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn distinct_counts_are_exact_either_side_of_a_full_candidate_table() {
let edge = FREQUENCY_CANDIDATES as i64;
for distinct in [0, 1, 7, edge - 2, edge - 1, edge, edge + 1, edge + 2, 3 * edge] {
for with_null in [false, true] {
let path = path("distinct-edge");
let mut writer =
Writer::create(&path, "items", vec![Field::new("id", LogicalType::BigInt)])
.expect("new file");
let mut values = Vec::new();
for round in 0..2 {
for value in 0..distinct {
let repeat = if round == 0 { 1 + (value % 3) as usize } else { 1 };
values.extend(std::iter::repeat_n(
Value::BigInt(value * 7_919 % distinct),
repeat,
));
if with_null && value % 1_000 == 0 {
values.push(Value::Null);
}
}
}
if with_null {
values.push(Value::Null);
}
for part in values.chunks(1_024) {
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, part).expect("ids"),
])
.expect("one column");
writer.append(&chunk).expect("rows");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert_eq!(
reader.distinct_values(0).expect("valid metadata"),
Some(distinct as u64),
"{distinct} values, null {with_null}"
);
fs::remove_file(path).expect("remove scratch file");
}
}
}
#[test]
fn narrow_nonzero_count_matches_the_full_reader_across_stripes() {
let path = path("quick-nonzero");
let mut writer = Writer::create(
&path,
"items",
vec![Field::new("label", LogicalType::Varchar), Field::new("id", LogicalType::Integer)],
)
.expect("create");
for ids in [
&[Value::Integer(0), Value::Null, Value::Integer(3)][..],
&[Value::Integer(0), Value::Integer(7), Value::Null][..],
] {
let labels = vec![Value::Varchar("same".into()); ids.len()];
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, &labels).expect("labels"),
Vector::from_values(LogicalType::Integer, ids).expect("ids"),
])
.expect("chunk"),
)
.expect("append");
}
writer.finish().expect("finish");
let catalog = Catalog::open(&path).expect("catalog");
assert_eq!(catalog.entries[0].nonzero, vec![None, None]);
assert_eq!(catalog.entries[0].aggregates, vec![None, Some((10, 4))]);
assert_eq!(catalog.entries[0].distincts, vec![Some(1), Some(3)]);
assert_eq!(catalog.exact_numeric_frequencies("items", 1).expect("frequencies"), None);
let prefix = catalog
.table("items")
.expect("reader")
.frequency_prefix(1)
.expect("valid metadata")
.expect("partial frequencies");
assert_eq!(prefix.entries, vec![(Value::Null, 2), (Value::Integer(0), 2)]);
assert_eq!(prefix.omitted_max, 1);
assert_eq!(catalog.distinct_count("items", 1).expect("distinct count"), Some(3));
assert_eq!(
catalog.integer_extremes("items", 1).expect("extremes"),
Some(IntegerExtremes::Values { low: 0, high: 7 })
);
assert_eq!(
catalog.aggregate_sums("items", &[1]).expect("catalog sums"),
Some(CertifiedSums { columns: vec![(10, 4)], rows: 6 })
);
assert_eq!(catalog.nonzero_count("items", 1).expect("quick count"), Some(2));
let mut legacy = catalog.clone();
Arc::make_mut(&mut legacy.entries)[0].nonzero[1] = Some(999);
assert_eq!(legacy.nonzero_count("items", 1).expect("ignore legacy count"), Some(2));
Arc::make_mut(&mut legacy.entries)[0].frequencies[1] = None;
assert_eq!(legacy.nonzero_count("items", 1).expect("directory fallback"), Some(2));
Writer::certify_counts(&path).expect("recertify");
assert_eq!(
Catalog::open(&path).expect("reopen").nonzero_count("items", 1).expect("count"),
Some(2)
);
assert_eq!(
Catalog::open(&path).expect("reopen").aggregate_sums("items", &[1]).expect("sums"),
Some(CertifiedSums { columns: vec![(10, 4)], rows: 6 })
);
assert_eq!(
Catalog::open(&path).expect("reopen").distinct_count("items", 1).expect("distinct"),
Some(3)
);
assert_eq!(
Catalog::open(&path).expect("reopen").integer_extremes("items", 1).expect("ends"),
Some(IntegerExtremes::Values { low: 0, high: 7 })
);
assert_eq!(
Catalog::open(&path)
.expect("reopen")
.exact_numeric_frequencies("items", 1)
.expect("frequencies"),
None
);
assert_eq!(catalog.table("items").expect("reader").null_count(1).expect("nulls"), 2);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn numeric_string_pair_leaders_are_certified_in_the_directory() {
let path = path("pair-frequencies");
let mut pairs = Vec::new();
pairs.extend(std::iter::repeat_n((1_i64, "alpha".to_string()), 100));
pairs.extend(std::iter::repeat_n((1_i64, "beta".to_string()), 50));
pairs.extend(std::iter::repeat_n((2_i64, "gamma".to_string()), 40));
pairs.extend((1_000_i64..1_600).map(|id| (id, format!("tail {id}"))));
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::BigInt),
Field::required("phrase", LogicalType::Varchar),
],
)
.expect("new file");
for part in pairs.chunks(1_024) {
let ids = part.iter().map(|(id, _)| Value::BigInt(*id)).collect::<Vec<_>>();
let phrases =
part.iter().map(|(_, phrase)| Value::Varchar(phrase.clone())).collect::<Vec<_>>();
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &ids).expect("ids"),
Vector::from_values(LogicalType::Varchar, &phrases).expect("phrases"),
])
.expect("matching columns"),
)
.expect("rows");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert!(
reader.table.pair_frequencies.is_empty(),
"no query-specific pair result is stored"
);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn legacy_group_answers_are_ignored() {
let path = path("legacy-group-answers");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::BigInt),
Field::required("text", LogicalType::Varchar),
],
)
.expect("new file");
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1)]).expect("id"),
Vector::from_values(LogicalType::Varchar, &[Value::Varchar("x".into())])
.expect("text"),
])
.expect("row"),
)
.expect("append");
writer.finish().expect("commit");
let mut reader = Reader::open(&path).expect("reopen");
let table = Arc::make_mut(&mut reader.table);
table.pair_frequencies.push(PairFrequencySummary {
first: 0,
second: 1,
entries: vec![PairFrequencyEntry { first_entry: 0, second: Some(0), count: 999 }],
omitted_max: 0,
});
table.host_groups = Some(host::HostSummary {
column: 1,
omitted_max: 0,
entries: vec![host::HostEntry {
host: "fake.test".into(),
count: 999,
bytes_sum: 999,
minimum: "x".into(),
}],
});
assert_eq!(reader.top_pair_frequencies(0, 1, 1).expect("legacy pair"), None);
assert_eq!(reader.host_groups(1, 1).expect("legacy host"), None);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_file_from_another_format_says_which_format_it_is() {
let older = path("older-format");
let mut writer =
Writer::create(&older, "items", vec![Field::new("id", LogicalType::Integer)])
.expect("new file");
let chunk = Chunk::new(vec![
Vector::flat(LogicalType::Integer, Data::Int32(vec![1, 2, 3].into()))
.expect("integers"),
])
.expect("chunk");
writer.append(&chunk).expect("page written");
writer.finish().expect("commit");
let unreadable =
READABLE.iter().copied().min().expect("at least one format is readable") - 1;
let mut file = OpenOptions::new().write(true).open(&older).expect("open for the header");
file.seek(SeekFrom::Start(8)).expect("the version follows the magic");
file.write_all(&unreadable.to_le_bytes()).expect("write an older version");
drop(file);
let complaint = Reader::open(&older).expect_err("an older format is refused").to_string();
assert!(complaint.contains(&format!("format {unreadable}")), "{complaint}");
assert!(complaint.contains(&format!("format {FORMAT}")), "{complaint}");
let mut file = OpenOptions::new().write(true).open(&older).expect("open for the header");
file.seek(SeekFrom::Start(0)).expect("the magic is first");
file.write_all(b"NOTRUDB!").expect("write another engine's magic");
drop(file);
let complaint = Reader::open(&older).expect_err("a foreign file is refused").to_string();
assert!(complaint.contains("magic"), "{complaint}");
assert!(!complaint.contains("format"), "a version has nothing to do with it: {complaint}");
fs::remove_file(older).expect("remove scratch file");
}
#[test]
fn an_unfinished_or_damaged_file_does_not_answer_with_partial_rows() {
let unfinished = path("unfinished");
let mut writer =
Writer::create(&unfinished, "items", vec![Field::new("id", LogicalType::Integer)])
.expect("new file");
let chunk = Chunk::new(vec![
Vector::flat(LogicalType::Integer, Data::Int32(vec![1, 2, 3].into()))
.expect("integers"),
])
.expect("chunk");
writer.append(&chunk).expect("page written");
drop(writer);
assert!(Reader::open(&unfinished).is_err(), "no directory was committed");
fs::remove_file(unfinished).expect("remove scratch file");
let damaged = path("damaged");
let mut writer =
Writer::create(&damaged, "items", vec![Field::new("id", LogicalType::Integer)])
.expect("new file");
writer.append(&chunk).expect("page written");
writer.finish().expect("commit");
let reader = Reader::open(&damaged).expect("valid directory");
let mut file =
OpenOptions::new().write(true).open(&damaged).expect("open for a damaged page");
file.seek(SeekFrom::Start(HEADER + 1)).expect("inside first page");
file.write_all(&[255]).expect("damage one byte");
assert!(reader.read(0, &[0]).is_err(), "page checksum rejects corruption");
fs::remove_file(damaged).expect("remove scratch file");
}
#[test]
fn damaged_lazy_dictionary_payload_is_an_error() {
let path = path("damaged-dictionary");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::Integer),
Field::new("text", LogicalType::Varchar),
],
)
.expect("new file");
writer.append(&sample()).expect("stripe written");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let dictionary = reader.table.dictionaries[1].expect("string dictionary page");
let mut header = [0; DICTIONARY_HEADER];
read_at(&reader.file, dictionary.offset, &mut header).expect("dictionary header");
let count = u32::from_le_bytes(header[0..4].try_into().expect("four bytes")) as usize;
let width = u32::from_le_bytes(header[12..16].try_into().expect("four bytes"));
assert_ne!(width & DICTIONARY_SCATTERED, 0, "the blocks say where they are");
let bits = (width & !DICTIONARY_FLAGS) as usize;
let mut start = [0; 8];
let at = dictionary.offset + (DICTIONARY_HEADER + offset_bytes(count, bits)) as u64;
read_at(&reader.file, at, &mut start).expect("the first block's start");
let mut file = OpenOptions::new().write(true).open(&path).expect("open dictionary page");
file.seek(SeekFrom::Start(u64::from_le_bytes(start))).expect("inside dictionary payload");
file.write_all(&[255]).expect("damage dictionary payload");
let chunk = reader.read(0, &[1]).expect("code page and dictionary index remain valid");
let error =
chunk.validate_external().expect_err("payload corruption must reach the caller");
assert!(error.message().contains("payload checksum differs"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_column_of_all_different_values_is_written_without_a_dictionary() {
let path = path("dictionary-decide");
let rows = 20_000;
let unique =
|row: usize| format!("{row:09} a value that appears exactly once in the table");
let repeated = |row: usize| unique(row / 40);
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("unique", LogicalType::Varchar),
Field::required("repeated", LogicalType::Varchar),
],
)
.expect("new file");
for part in (0..rows).step_by(1_000) {
let span = part..(part + 1_000).min(rows);
let left = span.clone().map(|row| Value::Varchar(unique(row))).collect::<Vec<_>>();
let right = span.map(|row| Value::Varchar(repeated(row))).collect::<Vec<_>>();
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, &left).expect("strings"),
Vector::from_values(LogicalType::Varchar, &right).expect("strings"),
])
.expect("two columns"),
)
.expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert!(
reader.table.dictionaries[0].is_none(),
"a column with no repeats has nothing to say twice"
);
assert!(
reader.table.dictionaries[1].is_some(),
"a column whose values come round again keeps its dictionary"
);
let mut first = 0;
for part in 0..reader.parts() {
let chunk = reader.read(part, &[0, 1]).expect("a part");
for row in 0..chunk.len() {
assert_eq!(chunk.value_at(row, 0), Value::Varchar(unique(first + row)));
assert_eq!(chunk.value_at(row, 1), Value::Varchar(repeated(first + row)));
}
first += chunk.len();
}
assert_eq!(first, rows, "every row was read back");
let raw = (0..rows).map(|row| unique(row).len()).sum::<usize>();
let size = fs::metadata(&path).expect("the file is there").len() as usize;
assert!(size < raw, "a column without a dictionary is still encoded: {size} against {raw}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_dictionary_over_many_blocks_checks_every_block_of_it() {
let path = path("dictionary-blocks");
let value = |row: usize| {
let row = row.saturating_sub(8_000);
format!("{row:07} a value long enough to be worth a payload block")
};
let parts = 40;
let per_part = 1000;
let mut writer =
Writer::create(&path, "items", vec![Field::required("text", LogicalType::Varchar)])
.expect("new file");
for part in 0..parts {
let values = (0..per_part)
.map(|row| Value::Varchar(value(part * per_part + row)))
.collect::<Vec<_>>();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, &values).expect("strings"),
])
.expect("matching rows");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let dictionary = reader.table.dictionaries[0].expect("string dictionary page");
assert!(
parts * per_part > TEXT_PAYLOAD_VALUES * 4,
"the dictionary has to be several blocks for this to be testing anything"
);
for part in [0, parts - 1] {
let chunk = reader.read(part, &[0]).expect("a part");
chunk.validate_external().expect("every payload block checks out");
assert_eq!(chunk.value_at(0, 0), Value::Varchar(value(part * per_part)));
}
let mut header = [0; DICTIONARY_HEADER];
read_at(&reader.file, dictionary.offset, &mut header).expect("dictionary header");
let count = u32::from_le_bytes(header[0..4].try_into().expect("four bytes")) as usize;
let blocks = u32::from_le_bytes(header[8..12].try_into().expect("four bytes")) as usize;
let width = u32::from_le_bytes(header[12..16].try_into().expect("four bytes"));
let bits = (width & !DICTIONARY_FLAGS) as usize;
let mut place = [0; 16];
let at = DICTIONARY_HEADER + offset_bytes(count, bits) + (blocks - 1) * 16;
read_at(&reader.file, dictionary.offset + at as u64, &mut place).expect("its place");
let start = u64::from_le_bytes(place[..8].try_into().expect("eight bytes"));
let length = u64::from_le_bytes(place[8..].try_into().expect("eight bytes"));
let mut file = OpenOptions::new().write(true).open(&path).expect("open dictionary page");
file.seek(SeekFrom::Start(start + length - 4)).expect("the last bytes of the last block");
file.write_all(&[255]).expect("damage the last payload block");
let reader = Reader::open(&path).expect("the directory and the index are untouched");
let chunk = reader.read(parts - 1, &[0]).expect("the code page remains valid");
let error = chunk.validate_external().expect_err("the damage must reach the caller");
assert!(error.message().contains("payload checksum differs"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn values_of_different_lengths_read_back_out_of_packed_offsets() {
let path = path("dictionary-offsets");
let value = |row: usize| {
let row = row % 5_000;
if row % 511 == 3 { String::new() } else { "x".repeat(row % 97) + &format!("{row:05}") }
};
let rows = 6_000;
let mut writer =
Writer::create(&path, "items", vec![Field::required("text", LogicalType::Varchar)])
.expect("new file");
let values = (0..rows).map(|row| Value::Varchar(value(row))).collect::<Vec<_>>();
for part in values.chunks(1_000) {
let chunk =
Chunk::new(vec![Vector::from_values(LogicalType::Varchar, part).expect("strings")])
.expect("matching rows");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert!(
rows > TEXT_PAYLOAD_VALUES * 4,
"the dictionary has to be several blocks for this to be testing anything"
);
for part in 0..rows / 1_000 {
let chunk = reader.read(part, &[0]).expect("a part");
for row in 0..1_000 {
let row = part * 1_000 + row;
assert_eq!(
chunk.value_at(row % 1_000, 0),
Value::Varchar(value(row)),
"value {row}"
);
}
}
for _ in 0..2 {
for part in 0..rows / 1_000 {
let chunk = reader.read(part, &[0]).expect("a part");
let mut lens = vec![0_i64; 1_000];
let column = chunk.column(0).expect("one column");
assert!(column.try_bytes_lens(&mut lens).expect("lengths"), "a stored column");
for (row, &len) in lens.iter().enumerate() {
let row = part * 1_000 + row;
assert_eq!(len as usize, value(row).len(), "the length of value {row}");
}
}
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn lengths_restart_at_each_block_and_refuse_ends_that_go_backwards() {
let mut ends: Vec<u32> = (1..=TEXT_PAYLOAD_VALUES as u32).map(|at| at * 2).collect();
ends.extend([3, 3, 10]);
let Some(Lengths::Narrow(lens)) = lengths_of(&ends) else { panic!("short ordered ends") };
assert!(lens[..TEXT_PAYLOAD_VALUES].iter().all(|&len| len == 2));
assert_eq!(&lens[TEXT_PAYLOAD_VALUES..], &[3, 0, 7]);
let long = [5, 70_005, 70_006];
let Some(Lengths::Wide(lens)) = lengths_of(&long) else { panic!("long ordered ends") };
assert_eq!(lens, [5, 70_000, 1]);
let mut read = Vec::new();
Lengths::Wide(lens).extend_at(&[1, 9, 0], &mut read);
assert_eq!(read, [70_000, 0, 5], "a position past the end is no length");
ends.push(9);
assert!(lengths_of(&ends).is_none());
}
#[test]
fn a_global_dictionary_is_opened_once_however_many_workers_ask_at_once() {
let path = path("dictionary-once");
let parts = 8;
let per_part = 500;
let value =
|row: usize| format!("{row:07} a value long enough to be worth a payload block");
let mut writer =
Writer::create(&path, "items", vec![Field::required("text", LogicalType::Varchar)])
.expect("new file");
for part in 0..parts {
let values = (0..per_part)
.map(|row| Value::Varchar(value(part * per_part + row)))
.collect::<Vec<_>>();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, &values).expect("strings"),
])
.expect("matching rows");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
assert!(reader.table.dictionaries[0].is_some(), "the column has to have one to share");
assert_eq!(reader.reads().dictionaries, 0, "opening the file does not open a dictionary");
let workers = 16;
let gate = std::sync::Barrier::new(workers);
std::thread::scope(|scope| {
for worker in 0..workers {
let reader = reader.clone();
let gate = &gate;
scope.spawn(move || {
gate.wait();
let chunk = reader.read(worker % parts, &[0]).expect("a part");
assert_eq!(
chunk.value_at(0, 0),
Value::Varchar(value((worker % parts) * per_part))
);
});
}
});
assert_eq!(reader.reads().dictionaries, 1, "sixteen workers, one dictionary, one open");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_damaged_sorted_order_is_an_error() {
let path = path("damaged-order");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::Integer),
Field::new("text", LogicalType::Varchar),
],
)
.expect("new file");
writer.append(&sample()).expect("stripe written");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let page = reader.table.dictionaries[1].expect("string dictionary page");
let mut header = [0; DICTIONARY_HEADER];
read_at(&reader.file, page.offset, &mut header).expect("dictionary header");
let index_len = dictionary_index_len(&header);
let mut file = OpenOptions::new().write(true).open(&path).expect("open dictionary page");
file.seek(SeekFrom::Start(page.offset + index_len)).expect("the first head");
file.write_all(&[255]).expect("damage the order");
let dictionary = reader.dictionary(1).expect("read").expect("a string column has one");
let error = dictionary.compare_rank(0, b"anything").expect_err("a damaged order is caught");
assert!(error.message().contains("rank checksum differs"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_global_dictionary_carries_the_sorted_order_of_its_values() {
let spellings = ["overlong1z", "b", "", "overlong1a", "overlong", "ab", "a", "overlong1"];
let path = path("dictionary-order");
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
writer
.append(
&Chunk::new(vec![
Vector::from_values(
LogicalType::Varchar,
&spellings.map(|text| Value::Varchar(text.into())),
)
.expect("strings"),
])
.expect("one column"),
)
.expect("stripe written");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
let count = dictionary.ranks().expect("a v10 file stores one");
assert_eq!(count, spellings.len(), "every distinct value has a rank");
let order = (0..count)
.map(|rank| dictionary.code_at_rank(rank).expect("a code"))
.collect::<Vec<_>>();
let mut seen = order.clone();
seen.sort_unstable();
assert_eq!(seen, (0..spellings.len() as u32).collect::<Vec<_>>(), "a permutation of codes");
let ranked = order
.iter()
.map(|&code| {
dictionary.try_bytes_at(code as usize).expect("read").expect("a value").to_vec()
})
.collect::<Vec<_>>();
let mut expected = spellings.map(|text| text.as_bytes().to_vec()).to_vec();
expected.sort();
assert_eq!(ranked, expected, "rank order is value order");
for (rank, value) in expected.iter().enumerate() {
assert_eq!(
dictionary.compare_rank(rank, value).expect("compare"),
Ordering::Equal,
"rank {rank} is its own value"
);
if rank > 0 {
assert_eq!(
dictionary.compare_rank(rank - 1, value).expect("compare"),
Ordering::Less,
"rank {rank} follows the one before it"
);
}
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn text_columns_closed_at_once_each_keep_their_own_dictionary() {
let sizes = [300_usize, 5_000, 40, 2_000, 1_200];
let path = path("dictionaries-at-once");
let fields = (0..sizes.len())
.map(|column| Field::new(format!("text{column}"), LogicalType::Varchar))
.collect::<Vec<_>>();
let mut writer = Writer::create(&path, "items", fields).expect("new file");
let rows = 10_000_usize;
for start in (0..rows).step_by(1_024) {
let columns = sizes
.iter()
.enumerate()
.map(|(column, &size)| {
let values = (start..(start + 1_024).min(rows))
.map(|row| Value::Varchar(format!("c{column}-{:05}", (row * 7919) % size)))
.collect::<Vec<_>>();
Vector::from_values(LogicalType::Varchar, &values).expect("strings")
})
.collect::<Vec<_>>();
writer.append(&Chunk::new(columns).expect("five columns")).expect("stripe written");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
for (column, &size) in sizes.iter().enumerate() {
let dictionary =
reader.dictionary(column).expect("read").expect("a string column has one");
let count = dictionary.ranks().expect("a v10 file stores one");
assert_eq!(count, size, "column {column} has its own distinct count");
let ranked = (0..count)
.map(|rank| {
let code = dictionary.code_at_rank(rank).expect("a code");
dictionary.try_bytes_at(code as usize).expect("read").expect("a value").to_vec()
})
.collect::<Vec<_>>();
let expected = (0..size)
.map(|value| format!("c{column}-{value:05}").into_bytes())
.collect::<Vec<_>>();
assert_eq!(ranked, expected, "column {column} ranks its own values in order");
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_large_dictionary_ranks_in_value_order() {
let path = path("dictionary-large-rank");
let value = |row: u64| {
let mixed = row.wrapping_mul(0x9e37_79b9_7f4a_7c15) >> 40;
match row % 3 {
0 => format!("https://example.com/a/long/shared/path/{mixed:08}"),
1 => format!("{mixed}"),
_ => format!("x{}", row % 1000).repeat(1 + (row % 4) as usize) + &row.to_string(),
}
};
let distinct = 70_000;
let parts = 4 * distinct / 1000;
let per_part = 1000;
let mut writer =
Writer::create(&path, "items", vec![Field::required("text", LogicalType::Varchar)])
.expect("new file");
for part in 0..parts {
let values = (0..per_part)
.map(|row| Value::Varchar(value((part * per_part + row) / 4)))
.collect::<Vec<_>>();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, &values).expect("strings"),
])
.expect("matching rows");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
let count = dictionary.ranks().expect("a ranked dictionary");
assert_eq!(count, distinct as usize, "every distinct value has a rank");
assert!(count >= PARALLEL_SORT_MIN, "too few values to be sorted on more than one thread");
let ranked = (0..count)
.map(|rank| {
let code = dictionary.code_at_rank(rank).expect("a code");
dictionary.try_bytes_at(code as usize).expect("read").expect("a value").to_vec()
})
.collect::<Vec<_>>();
let mut expected = (0..distinct).map(|row| value(row).into_bytes()).collect::<Vec<_>>();
expected.sort();
assert_eq!(ranked, expected, "rank order is value order");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_directory_read_a_window_at_a_time_is_the_directory_read_whole() {
let path = path("windowed-directory");
let fields = vec![
Field::required("id", LogicalType::BigInt),
Field::required("word", LogicalType::Varchar),
Field::new("score", LogicalType::Double),
];
let mut writer = Writer::create(&path, "items", fields).expect("new file");
for part in 0..70_i64 {
let ids = (0..100).map(|row| Value::BigInt(part * 100 + row % 7)).collect::<Vec<_>>();
let words = (0..100)
.map(|row| Value::Varchar(format!("word {}", row % 13)))
.collect::<Vec<_>>();
let scores = (0..100)
.map(|row| if row % 4 == 0 { Value::Null } else { Value::Double(row as f64) })
.collect::<Vec<_>>();
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &ids).expect("integers"),
Vector::from_values(LogicalType::Varchar, &words).expect("strings"),
Vector::from_values(LogicalType::Double, &scores).expect("doubles"),
])
.expect("three columns");
writer.append(&chunk).expect("a part");
}
writer.finish().expect("commit");
let catalog = Catalog::open(&path).expect("reopen");
let entry = catalog.entries.first().expect("one table").directory;
let (offset, length) = (entry.offset, entry.length as usize);
let mut bytes = vec![0; length];
read_at(&catalog.file, offset, &mut bytes).expect("the directory");
assert_eq!(file_checksum(&catalog.file, offset, length).expect("checksum"), entry.hash);
let whole = decode_directory(&bytes, catalog.size).expect("whole");
assert!(whole.stripes.len() > 1, "the table should span stripes");
for size in [1, 7, 33, 4_096] {
let mut cursor = Cursor::over(&catalog.file, offset, length);
cursor.window.as_mut().expect("a window").size = size;
let windowed = read_directory(cursor, catalog.size, Some(offset)).expect("windowed");
assert_eq!(format!("{:?}", windowed.stripes), format!("{:?}", whole.stripes));
assert_eq!(format!("{:?}", windowed.fields), format!("{:?}", whole.fields));
let mut stored = 0;
for (column, (left, held)) in
windowed.frequencies.iter().zip(&whole.frequencies).enumerate()
{
match (left, held) {
(None, None) => {}
(
Some(super::Frequencies::Stored { span, values }),
Some(super::Frequencies::Held(summary)),
) => {
let mut one = vec![0; span.length as usize];
read_at(&catalog.file, span.offset, &mut one).expect("a synopsis");
let read = decode_summary(
&mut Cursor::new(&one),
&whole.fields[column],
whole.rows,
*values,
)
.expect("a valid synopsis")
.expect("one is there");
assert_eq!(format!("{read:?}"), format!("{summary:?}"));
stored += 1;
}
other => panic!("column {column} came back as {other:?}"),
}
}
assert!(stored >= 2, "only {stored} synopses were left in the file");
}
let reader = catalog.table("items").expect("the table");
assert!(reader.frequency_summaries[1].get().is_none());
assert!(reader.top_frequencies(1, 1).expect("a readable synopsis").is_some());
let first = reader.frequency_summaries[1].get().expect("decoded synopsis");
let clone = reader.clone();
assert!(clone.top_frequencies(1, 1).expect("cached synopsis").is_some());
assert!(Arc::ptr_eq(first, clone.frequency_summaries[1].get().expect("same synopsis")));
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_checksum_carried_across_reads_is_the_checksum_of_the_whole() {
let path = path("file-checksum");
let bytes = (0..200_000_u32)
.map(|at| (at.wrapping_mul(2_654_435_761) >> 13) as u8)
.collect::<Vec<_>>();
fs::write(&path, &bytes).expect("scratch file");
let file = File::open(&path).expect("open");
for (offset, length) in [
(0, 0),
(3, 1),
(5, 31),
(0, 32),
(9, 33),
(1, 65_536),
(7, 65_567),
(0, 200_000),
(11, 131_101),
] {
let whole = checksum(&bytes[offset..offset + length]);
assert_eq!(
file_checksum(&file, offset as u64, length).expect("read"),
whole,
"{offset} {length}"
);
}
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_string_synopsis_is_read_without_keeping_the_dictionary_blocks() {
let path = path("synopsis-keeps-no-block");
let spelled = |index: usize| Value::Varchar(format!("phrase {index:05}"));
let mut values = (0..3_000).map(spelled).collect::<Vec<_>>();
for _ in 0..3 {
values.extend((0..3_000).step_by(5).map(spelled));
}
let mut writer =
Writer::create(&path, "items", vec![Field::required("text", LogicalType::Varchar)])
.expect("new file");
for part in values.chunks(1_024) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
let resting = dictionary.footprint();
let prefix = reader.frequency_prefix(0).expect("a readable synopsis").expect("one");
assert_eq!(prefix.entries.len(), 512);
for (value, count) in &prefix.entries {
let Value::Varchar(text) = value else { panic!("a string column gave {value:?}") };
let index = text["phrase ".len()..].parse::<usize>().expect("a spelled number");
assert_eq!((index % 5, *count), (0, 4), "{text} came back with {count}");
}
assert_eq!(dictionary.footprint(), resting, "reading the synopsis kept a decoded block");
let again = reader.frequency_prefix(0).expect("a readable synopsis").expect("one");
assert_eq!(again.entries, prefix.entries);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn character_lengths_are_counted_without_keeping_the_dictionary_blocks() {
let path = path("character-lengths");
let spellings = (0..2_500)
.map(|index| Value::Varchar(format!("héllo {index:05} {}", "ü".repeat(index % 30))))
.collect::<Vec<_>>();
let mut writer =
Writer::create(&path, "items", vec![Field::required("text", LogicalType::Varchar)])
.expect("new file");
for part in spellings.chunks(1_024) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
let resting = dictionary.footprint();
let mut lens = Vec::new();
assert!(dictionary.try_chars_lens(&mut lens).expect("counted"), "a stored source counts");
let counted = dictionary.footprint() - resting;
let blocks = dictionary.len().div_ceil(TEXT_PAYLOAD_VALUES);
assert!(
counted <= blocks * TEXT_PAYLOAD_VALUES * size_of::<u32>(),
"counting kept {counted} bytes, more than a count a value"
);
let expected = (0..dictionary.len())
.map(|code| {
let bytes = dictionary.try_bytes_at(code).expect("read").expect("a value");
i64::try_from(std::str::from_utf8(bytes).expect("utf-8").chars().count())
.expect("small")
})
.collect::<Vec<_>>();
assert_eq!(lens, expected, "a count is the number of characters, not of bytes");
let mut again = Vec::new();
assert!(dictionary.try_chars_lens(&mut again).expect("counted"));
assert_eq!(again, lens, "the kept counts answer the second time");
fs::remove_file(path).expect("remove scratch file");
}
fn stored_spellings(label: &str, spellings: &[String]) -> (PathBuf, Reader) {
let path = path(label);
let values = spellings.iter().map(|text| Value::Varchar(text.clone())).collect::<Vec<_>>();
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
for part in values.chunks(1_024) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("reopen from disk");
(path, reader)
}
fn scattered_rows(len: usize) -> (Vec<u32>, Vec<bool>) {
let codes = (0..len)
.map(|row| u32::try_from(row * 7_919 % len).expect("a small dictionary"))
.collect::<Vec<_>>();
let valid = (0..len).map(|row| row % 7 != 3).collect::<Vec<_>>();
(codes, valid)
}
#[test]
fn character_lengths_with_nulls_are_counted_without_keeping_the_dictionary_blocks() {
let spellings = (0..2_500)
.map(|index| format!("héllo {index:05} {}", "ü".repeat(index % 30)))
.collect::<Vec<_>>();
let (path, reader) = stored_spellings("character-lengths-nulls", &spellings);
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
let (codes, valid) = scattered_rows(spellings.len());
let rows = Vector::dictionary_over(codes.clone(), Arc::clone(&dictionary))
.expect("every code is inside")
.with_validity(Validity::from_run(&valid));
let resting = dictionary.footprint();
let lens = rudb_kernels::call("length", &[&rows], &LogicalType::BigInt, None)
.expect("length reads");
let counted = dictionary.footprint() - resting;
let blocks = dictionary.len().div_ceil(TEXT_PAYLOAD_VALUES);
assert!(
counted <= blocks * TEXT_PAYLOAD_VALUES * size_of::<u32>(),
"length over a vector with nulls kept {counted} bytes, more than a count a value"
);
let expected = (0..rows.len())
.map(|row| match valid[row] {
true => Value::BigInt(
i64::try_from(spellings[codes[row] as usize].chars().count()).expect("small"),
),
false => Value::Null,
})
.collect::<Vec<_>>();
let answers = (0..lens.len()).map(|row| lens.value_at(row)).collect::<Vec<_>>();
assert_eq!(answers, expected, "a count of characters where a row has one, null elsewhere");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn string_kernels_read_a_stored_dictionary_without_keeping_its_blocks() {
let spellings = (0..2_500)
.map(|index| format!("HéLLo {index:05} {}", "Üß".repeat(index % 30)))
.collect::<Vec<_>>();
let (path, reader) = stored_spellings("string-kernels", &spellings);
let page = reader.table.dictionaries[0].expect("a string column has one");
let starved =
open_global_dictionary(Arc::clone(&reader.file), page, &LogicalType::Varchar, 0)
.expect("a dictionary opens whatever it may keep");
let starved = Arc::new(starved);
let (codes, valid) = scattered_rows(spellings.len());
let rows = Vector::dictionary_over(codes.clone(), Arc::clone(&starved))
.expect("every code is inside")
.with_validity(Validity::from_run(&valid));
let expected = |each: &dyn Fn(&str) -> String| {
(0..rows.len())
.map(|row| match valid[row] {
true => Value::Varchar(each(&spellings[codes[row] as usize])),
false => Value::Null,
})
.collect::<Vec<_>>()
};
let answers =
|vector: &Vector| (0..vector.len()).map(|row| vector.value_at(row)).collect::<Vec<_>>();
let resting = starved.footprint();
let ends = spellings.len() * size_of::<u32>();
let lowered = rudb_kernels::call("lower", &[&rows], &LogicalType::Varchar, None)
.expect("lower reads");
assert_eq!(answers(&lowered), expected(&|text| text.to_lowercase()), "lower");
assert!(starved.footprint() <= resting + ends, "lower kept a block it read");
let start = Vector::constant(LogicalType::BigInt, Value::BigInt(3), rows.len());
let length = Vector::constant(LogicalType::BigInt, Value::BigInt(9), rows.len());
let cut =
rudb_kernels::call("substring", &[&rows, &start, &length], &LogicalType::Varchar, None)
.expect("substring reads");
let cut_of = |text: &str| text.chars().skip(2).take(9).collect::<String>();
assert_eq!(answers(&cut), expected(&cut_of), "substring");
assert!(starved.footprint() <= resting + ends, "substring kept a block it read");
let raised = rudb_kernels::call("upper", &[&rows], &LogicalType::Varchar, None)
.expect("upper reads");
assert_eq!(answers(&raised), expected(&|text| text.to_uppercase()), "upper");
let payload = spellings.iter().map(String::len).sum::<usize>();
assert!(
starved.footprint() >= resting + payload,
"a visit that has dropped a column's worth of blocks keeps what it reads"
);
let again = rudb_kernels::call("upper", &[&rows], &LogicalType::Varchar, None)
.expect("upper reads kept blocks");
assert_eq!(answers(&again), answers(&raised), "the kept blocks answer the same");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_dictionary_sweep_reads_every_value_and_keeps_it_under_the_budget() {
let path = path("dictionary-sweep");
let spellings = (0..2_500)
.map(|index| Value::Varchar(format!("value {index:08} {}", "x".repeat(index % 40))))
.collect::<Vec<_>>();
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
for part in spellings.chunks(1_024) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("stripe written");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
assert_eq!(dictionary.len(), spellings.len(), "every value is distinct");
for first in [0, TEXT_PAYLOAD_VALUES, TEXT_PAYLOAD_VALUES * 2] {
assert!(dictionary.text_block_might_contain(first, b"value").expect("signature"));
assert!(!dictionary.text_block_might_contain(first, b"google").expect("signature"));
}
let resting = dictionary.footprint();
let sweep = || {
let mut swept: Vec<Vec<u8>> = Vec::new();
let mut at = 0;
let mut calls = 0;
while at < dictionary.len() {
let stopped = dictionary
.sweep_text(at, dictionary.len(), &mut |index: usize, text: &[u8]| {
assert_eq!(index, swept.len(), "a sweep hands its values over in order");
swept.push(text.to_vec());
Ok(())
})
.expect("a sweep reads");
assert!(stopped > at, "a sweep moves");
at = stopped;
calls += 1;
}
assert_eq!(calls, 3, "a sweep hands over one block at a time");
swept
};
let swept = sweep();
assert_eq!(dictionary.footprint(), resting, "a first sweep keeps nothing it decoded");
assert_eq!(sweep(), swept, "a second sweep reads what the first did");
let after = dictionary.footprint();
assert!(after > resting, "a second sweep under the budget keeps what it decoded");
let read = (0..dictionary.len())
.map(|code| dictionary.try_bytes_at(code).expect("read").expect("a value").to_vec())
.collect::<Vec<_>>();
assert_eq!(swept, read, "a sweep answers what a point read answers");
let grown = dictionary.footprint() - after;
assert!(
grown == 0 || grown == dictionary.len() * size_of::<u32>(),
"a point read of a kept block decodes nothing, and {grown} bytes grew"
);
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_narrow_signature_of_an_older_file_answers_by_its_own_width() {
let path = path("narrow-substring-signature");
let blocks = [&b"https://google.com/"[..], b"https://example.org/", b"mail.google.com"];
let mut grams = Vec::new();
for text in blocks {
let mut bits = vec![0_u8; NARROW_GRAM_BYTES];
for gram in text.windows(4) {
for bit in gram_bits(gram, NARROW_GRAM_BYTES) {
bits[bit / 8] |= 1 << (bit % 8);
}
}
grams.extend(bits);
}
fs::write(&path, &grams).expect("scratch file");
let file = File::open(&path).expect("open scratch file");
let signatures = NativeGrams {
start: 0,
length: grams.len(),
width: NARROW_GRAM_BYTES,
hash: checksum(&grams),
verdicts: Mutex::new(Vec::new()),
};
let verdict = signatures.verdicts(&file, b"google").expect("signatures read");
assert_eq!(&verdict[..], &[true, false, true], "one verdict a block, at the narrow width");
assert!(signatures.footprint() > 0, "a verdict is remembered");
let again = signatures.verdicts(&file, b"google").expect("remembered");
assert!(Arc::ptr_eq(&verdict, &again), "a second question about a literal reads nothing");
let damaged = NativeGrams {
hash: signatures.hash ^ 1,
verdicts: Mutex::new(Vec::new()),
..signatures
};
let error = damaged.verdicts(&file, b"google").expect_err("a damaged region is refused");
assert!(error.to_string().contains("substring signatures checksum differs"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_damaged_substring_signature_is_checked_only_when_used() {
let path = path("damaged-substring-signature");
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
let rows = [Value::Varchar("google".into()), Value::Varchar("example".into())];
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, &rows).expect("strings"),
])
.expect("one column"),
)
.expect("stripe written");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let page = reader.table.dictionaries[0].expect("string dictionary page");
let mut file = OpenOptions::new().write(true).open(&path).expect("open dictionary page");
file.seek(SeekFrom::Start(page.offset + u64::from(page.length) - 1))
.expect("last signature byte");
file.write_all(&[255]).expect("damage signature");
let reader = Reader::open(&path).expect("the directory is still valid");
let dictionary = reader.dictionary(0).expect("index is still valid").expect("dictionary");
let error = dictionary
.text_block_might_contain(0, b"goog")
.expect_err("a used signature checks its own checksum");
assert!(error.message().contains("substring signatures checksum differs"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_sweep_over_a_block_with_a_short_second_run_reads_what_a_point_read_reads() {
let path = path("dictionary-sweep-short-run");
let spellings = (0..2_800)
.map(|index| Value::Varchar(format!("value {index:08} {}", "x".repeat(index % 40))))
.collect::<Vec<_>>();
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
for part in spellings.chunks(1_024) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("stripe written");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
assert_eq!(dictionary.len(), spellings.len(), "every value is distinct");
let last = dictionary.len() % TEXT_PAYLOAD_VALUES;
assert!(last > TEXT_OFFSET_RUN, "the last block has to reach into a second run of offsets");
assert!(last < TEXT_PAYLOAD_VALUES, "and that second run has to be short of a whole one");
let mut swept: Vec<Vec<u8>> = Vec::new();
let mut at = 0;
while at < dictionary.len() {
let stopped = dictionary
.sweep_text(at, dictionary.len(), &mut |index: usize, text: &[u8]| {
assert_eq!(index, swept.len(), "a sweep hands its values over in order");
swept.push(text.to_vec());
Ok(())
})
.expect("a sweep reads");
assert!(stopped > at, "a sweep moves");
at = stopped;
}
let read = (0..dictionary.len())
.map(|code| dictionary.try_bytes_at(code).expect("read").expect("a value").to_vec())
.collect::<Vec<_>>();
assert_eq!(swept, read, "a sweep answers what a point read answers");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn the_unpacked_ends_answer_what_the_packed_ends_answer() {
let path = path("dictionary-unpacked-ends");
let spellings = (0..2_800)
.map(|index| Value::Varchar(format!("value {index:08} {}", "x".repeat(index % 40))))
.collect::<Vec<_>>();
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
for part in spellings.chunks(1_024) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("stripe written");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let dictionary = reader.dictionary(0).expect("read").expect("a string column has one");
assert_eq!(dictionary.len(), spellings.len(), "every value is distinct");
let wanted = (0..spellings.len())
.map(|index| format!("value {index:08} {}", "x".repeat(index % 40)).into_bytes())
.collect::<Vec<_>>();
let pass = |what: &str| {
for (index, value) in wanted.iter().enumerate() {
let len = dictionary.try_bytes_len_at(index).expect("read").expect("a value");
assert_eq!(len, value.len(), "{what} has the wrong length at {index}");
let bytes = dictionary.try_bytes_at(index).expect("read").expect("a value");
assert_eq!(bytes, value.as_slice(), "{what} has the wrong value at {index}");
}
};
pass("the first pass");
pass("the second pass");
let lens = wanted.iter().map(|value| value.len() as i64).collect::<Vec<_>>();
let mut whole = vec![0i64; wanted.len()];
assert!(dictionary.try_bytes_lens(&mut whole).expect("read"), "the text answers whole");
assert_eq!(whole, lens, "a vector of lengths answers what a length at a time answers");
let codes = (0..4_000_u32).map(|row| (7 * (4_000 - row)) % 2_800).collect::<Vec<_>>();
let coded = Vector::dictionary_over(codes.clone(), dictionary).expect("codes in range");
let mut through = vec![0i64; codes.len()];
assert!(coded.try_bytes_lens(&mut through).expect("read"), "the codes answer whole");
for (row, &code) in codes.iter().enumerate() {
assert_eq!(through[row], lens[code as usize], "row {row} reads code {code}");
let one = coded.try_bytes_len_at(row).expect("read").expect("a value");
assert_eq!(through[row], one as i64, "row {row} a row at a time");
}
let fresh = Reader::open(&path).expect("valid directory");
let untouched = fresh.dictionary(0).expect("read").expect("a string column has one");
let few = vec![2_799_u32, 0, 1_024, 1_023, 511, 512];
let coded = Vector::dictionary_over(few.clone(), untouched).expect("in range");
let mut short = vec![0i64; few.len()];
assert!(coded.try_bytes_lens(&mut short).expect("read"), "the codes answer whole");
let expected = few.iter().map(|&code| lens[code as usize]).collect::<Vec<_>>();
assert_eq!(short, expected, "the packed ends answer what the table answers");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn narrowing_a_page_takes_what_fits_and_refuses_what_does_not() {
assert_eq!(fit::<i8>(&[]).expect("an empty page fits anything"), Vec::<i8>::new());
assert_eq!(fit::<i8>(&[-128, 0, 127]).expect("the edges fit"), vec![-128_i8, 0, 127]);
fit::<i8>(&[128]).expect_err("one past the top does not fit");
fit::<i8>(&[-129]).expect_err("one past the bottom does not fit");
assert_eq!(fit::<u8>(&[0, 255]).expect("the edges fit"), vec![0_u8, 255]);
fit::<u8>(&[256]).expect_err("one past the top does not fit");
fit::<u8>(&[-1]).expect_err("a negative does not fit an unsigned page");
assert_eq!(
fit::<i16>(&[-32_768, 0, 32_767]).expect("the edges fit"),
vec![-32_768_i16, 0, 32_767]
);
fit::<i16>(&[32_768]).expect_err("one past the top does not fit");
fit::<i16>(&[-32_769]).expect_err("one past the bottom does not fit");
assert_eq!(fit::<u16>(&[0, 65_535]).expect("the edges fit"), vec![0_u16, 65_535]);
fit::<u16>(&[65_536]).expect_err("one past the top does not fit");
fit::<u16>(&[-1]).expect_err("a negative does not fit an unsigned page");
assert_eq!(
fit::<i32>(&[i64::from(i32::MIN), 0, i64::from(i32::MAX)]).expect("the edges fit"),
vec![i32::MIN, 0, i32::MAX]
);
fit::<i32>(&[i64::from(i32::MAX) + 1]).expect_err("one past the top does not fit");
fit::<i32>(&[i64::from(i32::MIN) - 1]).expect_err("one past the bottom does not fit");
assert_eq!(
fit::<u32>(&[0, 4_294_967_295]).expect("the edges fit"),
vec![0_u32, 4_294_967_295]
);
fit::<u32>(&[4_294_967_296]).expect_err("one past the top does not fit");
fit::<u32>(&[-1]).expect_err("a negative does not fit an unsigned page");
fit::<i8>(&[0, 1, 2, 128, 3]).expect_err("one bad value spoils the page");
}
#[test]
fn the_residue_agrees_with_a_checked_conversion_everywhere() {
for value in -70_000_i64..70_000 {
assert_eq!(fit::<i8>(&[value]).is_ok(), i8::try_from(value).is_ok(), "{value} as i8");
assert_eq!(fit::<u8>(&[value]).is_ok(), u8::try_from(value).is_ok(), "{value} as u8");
assert_eq!(fit::<i16>(&[value]).is_ok(), i16::try_from(value).is_ok(), "{value} i16");
assert_eq!(fit::<u16>(&[value]).is_ok(), u16::try_from(value).is_ok(), "{value} u16");
}
let wide = [i64::MIN, i64::MIN + 1, i64::from(i32::MIN), 0, i64::from(u32::MAX), i64::MAX];
for edge in wide {
for step in -2_i64..=2 {
let value = edge.saturating_add(step);
assert_eq!(
fit::<i32>(&[value]).is_ok(),
i32::try_from(value).is_ok(),
"{value} as i32"
);
assert_eq!(
fit::<u32>(&[value]).is_ok(),
u32::try_from(value).is_ok(),
"{value} as u32"
);
}
}
}
#[test]
fn a_dictionary_reads_the_same_whether_its_blocks_say_where_they_are() {
let spellings = (0..3_000)
.map(|index| format!("value {index:08} {}", "y".repeat(index % 40)))
.collect::<Vec<_>>();
let mut read = Vec::new();
for layout in ["outside", "inside", "behind"] {
let mut dictionary = GlobalDictionary::new();
for text in &spellings {
dictionary.code(text).expect("a code for every spelling");
}
dictionary.finish_blocks().expect("the last block encodes");
let order = dictionary.ranked(None).expect("a sorted order");
let laid = |from: u64| {
let mut at = from;
dictionary
.blocks
.iter()
.map(|block| {
let place =
Placed { start: at, length: block.len() as u64, hash: checksum(block) };
at += block.len() as u64;
place
})
.collect::<Vec<_>>()
};
let payload = dictionary.blocks.concat();
let scattered = layout != "behind";
let (bytes, encoded, offset, length) = if layout == "outside" {
let mut bytes = vec![0; HEADER as usize];
bytes.extend_from_slice(&payload);
let encoded = encode_global_dictionary(&dictionary, &order, &laid(HEADER), true)
.expect("an encoding");
let offset = bytes.len() as u64;
bytes.extend_from_slice(&encoded.index);
bytes.extend_from_slice(&encoded.ranks);
bytes.extend_from_slice(&encoded.grams);
let length = encoded.index.len() + encoded.ranks.len() + encoded.grams.len();
(bytes, encoded, offset, length)
} else {
let first = encode_global_dictionary(&dictionary, &order, &laid(0), scattered)
.expect("an encoding");
let body = (first.index.len() + first.ranks.len() + first.grams.len()) as u64;
let encoded = encode_global_dictionary(&dictionary, &order, &laid(body), scattered)
.expect("an encoding");
let mut bytes = encoded.index.clone();
bytes.extend_from_slice(&encoded.ranks);
bytes.extend_from_slice(&encoded.grams);
bytes.extend_from_slice(&payload);
let length = bytes.len();
(bytes, encoded, 0, length)
};
let path = path(&format!("blocks-{layout}"));
fs::write(&path, &bytes).expect("the dictionary is written on its own");
let file = Arc::new(File::open(&path).expect("it opens again"));
let page = Page {
offset,
length: u32::try_from(length).expect("a test dictionary is small"),
hash: checksum(&encoded.index),
};
let opened =
open_global_dictionary(file, page, &LogicalType::Varchar, TEXT_KEEP_BUDGET)
.expect("a dictionary laid out either way opens");
let mut swept: Vec<Vec<u8>> = Vec::new();
let mut at = 0;
while at < opened.len() {
at = opened
.sweep_text(at, opened.len(), &mut |_index: usize, text: &[u8]| {
swept.push(text.to_vec());
Ok(())
})
.expect("a sweep reads");
}
fs::remove_file(&path).expect("clean up");
read.push(swept);
}
let wanted =
spellings.iter().map(|text| text.as_bytes().to_vec()).collect::<Vec<Vec<u8>>>();
assert_eq!(read[0], wanted, "the blocks outside the page hold the values");
assert_eq!(read[1], read[0], "the blocks inside the page hold the same values");
assert_eq!(read[2], read[0], "the blocks behind one another hold the same values");
}
#[test]
fn a_dictionary_at_its_budget_sweeps_without_keeping() {
let path = path("dictionary-budget");
let spellings = (0..2_500)
.map(|index| Value::Varchar(format!("value {index:08} {}", "y".repeat(index % 40))))
.collect::<Vec<_>>();
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
for part in spellings.chunks(1_024) {
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::Varchar, part).expect("strings"),
])
.expect("one column"),
)
.expect("stripe written");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let page = reader.table.dictionaries[0].expect("a string column has one");
let file = Arc::clone(&reader.file);
let starved = open_global_dictionary(file, page, &LogicalType::Varchar, 0)
.expect("a dictionary opens whatever it may keep");
let resting = starved.footprint();
let mut swept: Vec<Vec<u8>> = Vec::new();
let mut at = 0;
while at < starved.len() {
at = starved
.sweep_text(at, starved.len(), &mut |_index: usize, text: &[u8]| {
swept.push(text.to_vec());
Ok(())
})
.expect("a sweep reads");
}
assert_eq!(swept.len(), spellings.len(), "a starved sweep still reads every value");
assert_eq!(starved.footprint(), resting, "and keeps no block it decoded");
let generous = reader.dictionary(0).expect("read").expect("a string column has one");
let read = (0..generous.len())
.map(|code| generous.try_bytes_at(code).expect("read").expect("a value").to_vec())
.collect::<Vec<_>>();
assert_eq!(swept, read, "a starved sweep answers what a point read answers");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn damaged_membership_cannot_skip_a_string_page() {
let path = path("damaged-membership");
let mut writer = Writer::create(
&path,
"items",
vec![
Field::required("id", LogicalType::Integer),
Field::new("text", LogicalType::Varchar),
],
)
.expect("new file");
writer.append(&sample()).expect("stripe written");
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let membership = reader.table.stripes[0].memberships.get(1).expect("string membership");
let mut file = OpenOptions::new().write(true).open(&path).expect("open membership page");
file.seek(SeekFrom::Start(membership.offset)).expect("membership start");
file.write_all(&[255]).expect("damage membership");
let error = reader.skips_codes(0, 1, &[3]).expect_err("corruption must not skip rows");
assert!(error.message().contains("membership page checksum differs"), "{error}");
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn membership_delta_stream_is_sorted_exact_and_bounded() {
let unique = unique_codes(&[900, 4, 4, 72, 9, u32::MAX]);
assert_eq!(unique, [4, 9, 72, 900, u32::MAX]);
let encoded = encode_membership(&unique);
assert_eq!(
decode_membership(&encoded).expect("valid membership"),
[4, 9, 72, 900, u32::MAX]
);
let merged = merged_codes(vec![vec![4, 900], vec![9, 900, u32::MAX], vec![72]]);
assert_eq!(merged, [4, 9, 72, 900, u32::MAX]);
assert_eq!(
decode_membership(&encode_membership(&merged)).expect("valid membership"),
unique
);
assert!(decode_membership(&[1, 0x80]).is_err(), "a truncated varint is invalid");
assert!(
decode_membership(&[1, 0xff, 0xff, 0xff, 0xff, 0x10]).is_err(),
"a value past u32 is invalid"
);
}
#[test]
fn a_global_dictionary_may_be_larger_than_one_column_page() {
let dictionary = Page {
offset: HEADER,
length: u32::try_from(MAX_PAGE + 1).expect("the page bound fits on disk"),
hash: 0,
};
let table = Table {
name: "items".to_owned(),
fields: vec![Field::new("text", LogicalType::Varchar)],
stripes: Vec::new(),
rows: 0,
dictionaries: vec![Some(dictionary)],
dictionary_payloads: Vec::new(),
demoted: Vec::new(),
distincts: vec![None],
frequencies: vec![None],
pair_frequencies: Vec::new(),
frequency_texts: Vec::new(),
host_groups: None,
clustering: None,
generation: 1,
sections: Vec::new(),
};
let directory = encode_directory(&table).expect("directory");
let file_size = dictionary.offset + u64::from(dictionary.length) + 1;
let decoded = decode_directory(&directory, file_size).expect("large lazy dictionary");
assert_eq!(decoded.dictionaries[0].expect("dictionary").length, dictionary.length);
}
#[test]
fn a_column_with_one_value_everywhere_costs_almost_nothing_a_row() {
let path = path("constant-codes");
let mut writer =
Writer::create(&path, "items", vec![Field::new("text", LogicalType::Varchar)])
.expect("new file");
let empty = vec![Value::Varchar(String::new()); 1024];
for _ in 0..4 {
let column = Vector::from_values(LogicalType::Varchar, &empty).expect("strings");
writer.append(&Chunk::new(vec![column]).expect("one column")).expect("a part");
}
writer.finish().expect("commit");
let reader = Reader::open(&path).expect("valid directory");
let pages = reader.layout().columns.first().expect("one column").pages;
assert!(pages < 256, "{pages} bytes of pages for 4,096 rows of one value");
let read = reader.read(3, &[0]).expect("the last part back");
assert_eq!(read.value_at(0, 0), Value::Varchar(String::new()));
assert_eq!(read.value_at(1023, 0), Value::Varchar(String::new()));
fs::remove_file(path).expect("remove scratch file");
}
#[test]
fn a_cascade_value_too_wide_for_its_column_is_refused_rather_than_cut() {
let over = vec![i64::from(i32::MAX) + 1];
let error = narrowed(&LogicalType::Integer, over).expect_err("a page that disagrees");
assert!(format!("{error}").contains("not of its type"), "{error}");
assert!(narrowed(&LogicalType::BigInt, vec![i64::MIN]).is_ok(), "bigint holds all of i64");
assert!(narrowed(&LogicalType::Varchar, vec![0]).is_err(), "strings are not integers");
}
#[test]
fn a_code_stream_the_cascade_cannot_shrink_is_left_alone() {
let mut state: u32 = 0x9e37_79b9;
let spread: Vec<u32> = (0..1024)
.map(|_| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state
})
.collect();
assert_eq!(encoded_codes(&spread).expect("no failure"), None);
let near: Vec<u32> = (0..1024).collect();
let coded = encoded_codes(&near).expect("no failure").expect("counting up is packable");
assert!(coded.len() < near.len() * 4, "{} bytes for a run of 1,024", coded.len());
}
#[test]
fn two_writes_of_the_same_rows_give_the_same_bytes() {
fn written(path: &PathBuf) {
let fields = (0..40)
.map(|column| {
let ty =
if column % 4 == 0 { LogicalType::Varchar } else { LogicalType::BigInt };
Field::new(format!("c{column}"), ty)
})
.collect::<Vec<_>>();
let mut writer = Writer::create(path, "wide", fields).expect("new file");
for part in 0..70_u64 {
let columns = (0..40)
.map(|column| {
let values = (0..64_u64)
.map(|row| {
let seed = part.wrapping_mul(31).wrapping_add(row);
if column % 4 == 0 {
Value::Varchar(format!("v{}", seed % 17))
} else {
Value::BigInt(i64::try_from(seed % 97).expect("small"))
}
})
.collect::<Vec<_>>();
let ty = if column % 4 == 0 {
LogicalType::Varchar
} else {
LogicalType::BigInt
};
Vector::from_values(ty, &values).expect("a column")
})
.collect::<Vec<_>>();
writer.append(&Chunk::new(columns).expect("forty columns")).expect("a part");
}
writer.finish().expect("commit");
}
let first = path("repeatable-one");
let second = path("repeatable-two");
written(&first);
written(&second);
let left = fs::read(&first).expect("the first file");
let right = fs::read(&second).expect("the second file");
assert_eq!(left.len(), right.len(), "two writes of the same rows differ in length");
assert!(left == right, "two writes of the same rows differ in their bytes");
let reader = Reader::open(&first).expect("valid directory");
assert_eq!(reader.table().rows(), 70 * 64);
let read = reader.read(0, &[0, 1]).expect("the first part back");
assert_eq!(read.value_at(0, 0), Value::Varchar("v0".to_owned()));
assert_eq!(read.value_at(0, 1), Value::BigInt(0));
fs::remove_file(first).expect("remove scratch file");
fs::remove_file(second).expect("remove scratch file");
}
fn three_tables(path: &PathBuf) {
let writer = Writer::create(
path,
"region",
vec![
Field::new("r_key", LogicalType::Integer),
Field::new("r_name", LogicalType::Varchar),
],
)
.expect("new file");
let mut writer = writer;
writer
.append(
&Chunk::new(vec![
Vector::from_values(
LogicalType::Integer,
&[Value::Integer(0), Value::Integer(1)],
)
.expect("keys"),
Vector::from_values(
LogicalType::Varchar,
&[Value::Varchar("AFRICA".to_owned()), Value::Varchar("ASIA".to_owned())],
)
.expect("names"),
])
.expect("two columns"),
)
.expect("a part");
let mut writer = writer
.next("empty", vec![Field::new("nothing", LogicalType::BigInt)])
.expect("a second table");
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &[Value::BigInt(7)]).expect("a row"),
])
.expect("one column"),
)
.expect("a part");
let mut writer =
writer.next("wide", vec![Field::new("n", LogicalType::BigInt)]).expect("a third table");
for part in 0..70_i64 {
let values = (0..64).map(|row| Value::BigInt(part * 64 + row)).collect::<Vec<_>>();
writer
.append(
&Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &values).expect("a column"),
])
.expect("one column"),
)
.expect("a part");
}
writer.finish().expect("commit");
}
#[test]
fn three_tables_in_one_file_read_back_by_name() {
let file = path("three-tables");
three_tables(&file);
let catalog = Catalog::open(&file).expect("a committed catalog");
assert_eq!(catalog.names().collect::<Vec<_>>(), ["region", "empty", "wide"]);
let region = catalog.table("region").expect("the first table");
assert_eq!(region.table().rows(), 2);
assert_eq!(
region.read(0, &[1]).expect("names").value_at(1, 0),
Value::Varchar("ASIA".to_owned())
);
let wide = catalog.table("wide").expect("the third table");
assert_eq!(wide.table().rows(), 70 * 64);
assert_eq!(wide.read(0, &[0]).expect("the first part").value_at(0, 0), Value::BigInt(0));
let empty = catalog.table("empty").expect("the second table");
assert_eq!(empty.table().rows(), 1);
assert_eq!(empty.read(0, &[0]).expect("the row").value_at(0, 0), Value::BigInt(7));
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn a_name_the_file_does_not_hold_is_an_error_rather_than_the_first_table() {
let file = path("three-tables-missing");
three_tables(&file);
let catalog = Catalog::open(&file).expect("a committed catalog");
let error = catalog.table("nation").expect_err("no such table");
assert!(error.message().contains("nation"), "{}", error.message());
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn a_file_of_three_tables_will_not_open_as_one() {
let file = path("three-tables-unnamed");
three_tables(&file);
let error = Reader::open(&file).expect_err("more than one table");
assert!(error.message().contains("more than one table"), "{}", error.message());
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn decimals_of_every_storage_width_round_trip() {
let file = path("decimals");
let widths = [(4_u8, 2_u8), (9, 2), (18, 4), (38, 6)];
let fields = widths
.iter()
.enumerate()
.map(|(index, (width, scale))| {
Field::new(
format!("d{index}"),
LogicalType::decimal(*width, *scale).expect("a decimal type"),
)
})
.collect::<Vec<_>>();
let mut writer = Writer::create(&file, "money", fields).expect("new file");
let rows: [i128; 3] = [-1234, 0, 999];
let columns = widths
.iter()
.map(|(width, scale)| {
let values = rows
.iter()
.map(|unscaled| Value::Decimal {
unscaled: *unscaled,
width: *width,
scale: *scale,
})
.collect::<Vec<_>>();
Vector::from_values(
LogicalType::decimal(*width, *scale).expect("a decimal type"),
&values,
)
.expect("a decimal column")
})
.collect::<Vec<_>>();
writer.append(&Chunk::new(columns).expect("four columns")).expect("a part");
writer.finish().expect("commit");
let reader = Reader::open(&file).expect("a committed file");
for (index, (width, scale)) in widths.iter().enumerate() {
assert_eq!(
reader.table().fields()[index].ty,
LogicalType::decimal(*width, *scale).expect("a decimal type"),
"column {index} came back as another type"
);
let column = reader.read(0, &[index]).expect("the column");
for (row, unscaled) in rows.iter().enumerate() {
assert_eq!(
column.value_at(row, 0),
Value::Decimal { unscaled: *unscaled, width: *width, scale: *scale },
"column {index} row {row}"
);
}
}
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn two_tables_of_one_name_are_refused_before_anything_is_committed() {
let file = path("two-of-a-name");
let writer = Writer::create(&file, "t", vec![Field::new("a", LogicalType::BigInt)])
.expect("new file");
let error = writer
.next("t", vec![Field::new("a", LogicalType::BigInt)])
.expect_err("the same name twice");
assert!(error.message().contains("same name"), "{}", error.message());
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn integer_tally_counts_encoded_rows_and_declines_null_parts() {
let file = path("integer-tally");
let mut writer =
Writer::create(&file, "events", vec![Field::new("source", LogicalType::SmallInt)])
.expect("new file");
let mut values = vec![Value::SmallInt(0); 1024];
values[7] = Value::SmallInt(3);
values[99] = Value::SmallInt(-2);
values[1001] = Value::SmallInt(3);
let column = Vector::from_values(LogicalType::SmallInt, &values).expect("integer values");
writer.append(&Chunk::new(vec![column]).expect("one column")).expect("first part");
values[0] = Value::Null;
let column = Vector::from_values(LogicalType::SmallInt, &values).expect("nullable values");
writer.append(&Chunk::new(vec![column]).expect("one column")).expect("second part");
writer.finish().expect("commit");
let reader = Reader::open(&file).expect("read file");
assert_eq!(
reader.integer_tally(0, 0).expect("valid part"),
Some(vec![(-2, 1), (0, 1021), (3, 2)])
);
assert!(reader.integer_tally(1, 0).expect("valid null part").is_none());
let catalog = Catalog::open(&file).expect("catalog");
assert_eq!(
catalog.integer_tally("events", 0).expect("nullable column"),
Some(vec![(-2, 2), (0, 2041), (3, 4)])
);
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn catalog_tallies_one_integer_column_without_opening_the_whole_table() {
let file = path("catalog-integer-tally");
let mut writer = Writer::create(
&file,
"events",
vec![
Field::new("noise", LogicalType::SmallInt),
Field::new("source", LogicalType::SmallInt),
],
)
.expect("new file");
let noise = vec![Value::SmallInt(9); 1024];
let mut source = vec![Value::SmallInt(0); 1024];
source[7] = Value::SmallInt(3);
source[99] = Value::SmallInt(-2);
let chunk = Chunk::new(vec![
Vector::from_values(LogicalType::SmallInt, &noise).expect("noise"),
Vector::from_values(LogicalType::SmallInt, &source).expect("source"),
])
.expect("two columns");
writer.append(&chunk).expect("append");
writer.finish().expect("commit");
let catalog = Catalog::open(&file).expect("catalog");
assert_eq!(
catalog.integer_tally("events", 1).expect("selected column"),
Some(vec![(-2, 1), (0, 1022), (3, 1)])
);
assert_eq!(
catalog.integer_tally("events", 0).expect("other column"),
Some(vec![(9, 1024)])
);
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn opening_the_catalog_reads_no_table_directory() {
let file = path("catalog-only");
three_tables(&file);
let catalog = Catalog::open(&file).expect("a committed catalog");
assert_eq!(catalog.opening.reads, 2, "opening the catalog read more than the slot");
assert_eq!(catalog.names().len(), 3);
fs::remove_file(file).expect("remove scratch file");
}
#[test]
fn the_checksum_answers_what_it_has_always_answered() {
let bytes: Vec<u8> =
(0..1000_u32).map(|at| (at.wrapping_mul(31).wrapping_add(7) % 251) as u8).collect();
for (length, expected) in [
(0, 0xef46_db37_51d8_e999),
(1, 0xa96c_7f0c_e858_bbb7),
(3, 0x56e6_9576_32a4_87f9),
(4, 0xc60d_15b1_e3ff_8f04),
(5, 0x8088_1585_8624_dd4e),
(7, 0xafbe_fc3d_6c6f_9a8e),
(8, 0x3da5_c7aa_2696_83e0),
(9, 0x465e_c429_b13c_3892),
(15, 0xdee8_9d8a_065a_6233),
(16, 0x1330_489a_7767_9c80),
(31, 0x3391_303d_485e_846e),
(32, 0x40b7_aff7_5d45_bbc8),
(33, 0x4997_cae4_951c_17a5),
(39, 0x5807_28fd_5c14_5739),
(40, 0xf95c_f6f5_c08a_3d3b),
(63, 0x2944_b4da_fc69_b206),
(64, 0xbb76_f6ef_19bd_5a1b),
(65, 0x814e_0c65_4a9f_d640),
(127, 0x00de_aab1_31cf_f89b),
(1000, 0x9e33_00c1_cde3_c58d),
] {
assert_eq!(checksum(&bytes[..length]), expected, "the checksum of {length} bytes");
}
assert_eq!(checksum(b"the quick brown fox jumps over the lazy dog"), 0xed71_4233_c5a9_a792);
}
#[test]
fn a_declared_order_comes_back_out_of_the_file() {
let path = path("clustered");
let shipped = vec![
Field::new("key", LogicalType::BigInt),
Field::new("line", LogicalType::Integer),
Field::new("shipdate", LogicalType::Date),
];
let plain = vec![Field::new("a", LogicalType::Integer)];
let stage_zero = Clustering::new(vec![2, 0, 1], Width::Month, &shipped).expect("valid");
let mut writer = Writer::create(&path, "lineitem", shipped)
.expect("new file")
.declare(stage_zero.clone())
.expect("the columns are the table's");
let column = |ty: LogicalType, values: &[Value]| {
Vector::from_values(ty, values).expect("the values match the type")
};
writer
.append(
&Chunk::new(vec![
column(
LogicalType::BigInt,
&[Value::BigInt(0), Value::BigInt(1), Value::BigInt(2), Value::BigInt(3)],
),
column(
LogicalType::Integer,
&[
Value::Integer(1),
Value::Integer(1),
Value::Integer(1),
Value::Integer(1),
],
),
column(
LogicalType::Date,
&[Value::Date(0), Value::Date(1), Value::Date(2), Value::Date(3)],
),
])
.expect("three columns"),
)
.expect("four rows");
let mut writer = writer.next("nation", plain).expect("a second table");
writer
.append(
&Chunk::new(vec![column(LogicalType::Integer, &[Value::Integer(7)])])
.expect("one column"),
)
.expect("one row");
writer.finish().expect("commit");
let catalog = Catalog::open(&path).expect("reopen");
let lineitem = catalog.table("lineitem").expect("the clustered table");
assert_eq!(lineitem.table().clustering(), Some(&stage_zero));
let nation = catalog.table("nation").expect("the plain table");
assert_eq!(nation.table().clustering(), None, "nobody declared one here");
assert_eq!(lineitem.table().rows(), 4);
assert_eq!(nation.table().rows(), 1);
fs::remove_file(&path).ok();
}
#[test]
fn a_declaration_off_the_end_of_the_table_never_reaches_the_file() {
let path = path("clustered-bad");
let writer = Writer::create(&path, "items", vec![Field::new("a", LogicalType::Integer)])
.expect("new file");
let four =
(0..4).map(|at| Field::new(format!("c{at}"), LogicalType::Integer)).collect::<Vec<_>>();
let wrong = Clustering::new(vec![3], Width::Exact, &four).expect("valid against four");
assert!(writer.declare(wrong).is_err(), "the table has one column, not four");
fs::remove_file(&path).ok();
}
#[test]
fn blocks_handed_out_and_given_back_out_of_order_are_the_blocks_encoded_in_place() {
let values = (0..PAYLOAD_SAMPLE_BLOCKS * TEXT_PAYLOAD_VALUES * 2 + 100)
.map(|at| format!("http://example{}.test/page/{at:06}", at % 7))
.collect::<Vec<_>>();
let filled = || {
let mut dictionary = GlobalDictionary::new();
for value in &values {
dictionary.code(value).expect("a code for every value");
}
dictionary.settle().expect("a shape");
dictionary
};
let mut in_place = filled();
in_place.finish_blocks().expect("every block encodes");
let mut handed = filled();
let out = handed.hand_out(3);
assert_eq!(out.len(), PAYLOAD_SAMPLE_BLOCKS * 2, "every sealed block goes out");
assert!(handed.waiting.is_empty(), "and none is left to be encoded under the lock");
for job in out.iter().rev() {
assert_eq!(job.place().0, 3, "a block goes back to the column it came from");
handed.take_back(job.place().1, job.encode().expect("encodes")).expect("taken back");
}
assert!(handed.early.is_empty(), "nothing is waiting on a gap");
handed.finish_blocks().expect("the last block encodes");
assert_eq!(handed.blocks, in_place.blocks, "the same blocks in the same order");
assert_eq!(handed.grams, in_place.grams, "with the same signatures");
}
#[test]
fn a_block_given_back_twice_is_refused() {
let mut dictionary = GlobalDictionary::new();
for at in 0..PAYLOAD_SAMPLE_BLOCKS * TEXT_PAYLOAD_VALUES {
dictionary.code(&format!("value {at}")).expect("a code");
}
dictionary.settle().expect("a shape");
let out = dictionary.hand_out(0);
let last = out.last().expect("blocks went out");
let at = last.place().1;
dictionary.take_back(at, last.encode().expect("encodes")).expect("taken back once");
assert!(dictionary.take_back(at, last.encode().expect("encodes")).is_err());
}
#[test]
fn the_dictionary_order_is_the_byte_order_however_deep_the_values_agree() {
let mut values = vec![String::new(), "http://".to_owned()];
for host in 0..7 {
for path in 0..30 {
values.push(format!("http://example{host}.test/page/{path:04}/index.html"));
values.push(format!("http://example{host}.test/page/{path:04}"));
}
}
values.push("http://example0.test/page/0000/index.htmlx".to_owned());
let mut dictionary = GlobalDictionary::new();
for value in &values {
dictionary.code(value).expect("a code for every value");
}
dictionary.finish_blocks().expect("the last block encodes");
let ranked = dictionary.ranked(None).expect("a sorted order");
assert_eq!(ranked.len(), values.len(), "one entry a distinct value");
let spellings = dictionary_values(&dictionary);
let seen = ranked
.iter()
.map(|&(_, code)| {
String::from_utf8(spellings[code as usize].clone()).expect("text in, text out")
})
.collect::<Vec<_>>();
let mut wanted = values.clone();
wanted.sort_unstable();
assert_eq!(seen, wanted, "the order is the order the bytes give");
for &(carried, code) in &ranked {
let value = &spellings[code as usize];
assert_eq!(carried, head(value), "the head belongs to the value it is filed with");
}
}
#[test]
fn the_commonest_entries_are_the_ones_a_full_sort_would_have_kept() {
let entry =
|value: u32, count: u64| FrequencyEntry { value: FrequencyValue::Code(value), count };
let mut all = (0..FREQUENCY_ENTRIES as u32 * 3)
.map(|code| entry(code, u64::from(code % 7) + 1))
.collect::<Vec<_>>();
all.push(FrequencyEntry { value: FrequencyValue::Null, count: 4 });
let mut sorted = all.clone();
sorted.sort_unstable_by(|left, right| {
right.count.cmp(&left.count).then_with(|| frequency_order(left.value, right.value))
});
let wanted_omitted = sorted[FREQUENCY_ENTRIES].count;
sorted.truncate(FREQUENCY_ENTRIES);
let mut picked = all.clone();
let omitted = keep_most_frequent(&mut picked);
assert_eq!(omitted, wanted_omitted, "the largest count that did not make the cut");
assert_eq!(picked.len(), FREQUENCY_ENTRIES, "the cut is where it says it is");
assert!(
picked
.iter()
.zip(&sorted)
.all(|(one, two)| one.value == two.value && one.count == two.count),
"the same entries in the same order"
);
let mut short = all[..FREQUENCY_ENTRIES - 1].to_vec();
let omitted = keep_most_frequent(&mut short);
assert_eq!(omitted, 0, "nothing is omitted when everything fits");
assert!(short.windows(2).all(|pair| pair[0].count >= pair[1].count), "still in order");
}
#[test]
fn a_short_dictionary_sorts_without_a_bucketing_pass() {
let empty = GlobalDictionary::new();
assert!(empty.ranked(None).expect("an empty order").is_empty(), "nothing in, nothing out");
let mut dictionary = GlobalDictionary::new();
for value in ["pear", "apple", "", "apples", "app"] {
dictionary.code(value).expect("a code for every value");
}
dictionary.finish_blocks().expect("the one block encodes");
let spellings = dictionary_values(&dictionary);
let seen = dictionary
.ranked(None)
.expect("a sorted order")
.iter()
.map(|&(_, code)| spellings[code as usize].clone())
.collect::<Vec<_>>();
let wanted: Vec<Vec<u8>> =
[&b""[..], b"app", b"apple", b"apples", b"pear"].iter().map(|v| v.to_vec()).collect();
assert_eq!(seen, wanted, "shorter first where one runs out inside another");
}
#[test]
fn a_demoted_dictionary_holds_less_and_takes_no_more_values() {
let profile = LoadProfile::begin("demoted");
let mut dictionary = GlobalDictionary::new();
for value in 0..50_000 {
dictionary.code(&format!("https://example.com/page/{value}")).expect("a code");
}
let (_, grown) = dictionary.recharge(Some(&profile));
assert_eq!(profile.held(), grown, "the profile holds what the dictionary does");
dictionary.demote();
let (before, after) = dictionary.recharge(Some(&profile));
assert_eq!(before, grown);
assert!(after < grown - grown / 4, "the lookup is let go of: {after} of {grown}");
assert_eq!(profile.held(), after, "the profile was told about the drop");
assert!(dictionary.code("one more").is_err(), "a demoted dictionary takes no values");
dictionary.demote();
assert_eq!(
dictionary.recharge(Some(&profile)),
(after, after),
"demoting twice is a no-op"
);
assert_eq!(dictionary.values(), 50_000, "the values coded before stay");
}
}