use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
use bytes::Bytes;
use crate::{
catalog::CatalogSnapshot,
store::{
inline::InlineChunkLocator,
key::{CurrentKey, EntityKey, InlineKey, InlineOperation, Key, SysKey},
proto::{
HeadValue, InlineChunkRangeValue, SnapshotValue, TableColumnStatsValue, TableStatsValue,
},
read::{EntityRecord, RecordSet, decode_current_record, decode_history_record},
value,
},
transaction::commit::StagedWrite,
};
#[derive(Default)]
struct InlineFold {
writes: Vec<(u64, InlineOperation, Option<InlineChunkLocator>)>,
chunks: BTreeSet<(u64, InlineOperation)>,
unfollowable: BTreeSet<u64>,
}
impl InlineFold {
fn observe(&mut self, key: &Key, bytes: Option<&[u8]>) {
match key {
Key::Inline(InlineKey::ChunkLocator {
table_id,
row_id_end,
schema_version,
begin_snapshot,
chunk_seq,
}) => {
let operation = InlineOperation::Insert {
table_id: *table_id,
schema_version: *schema_version,
begin_snapshot: *begin_snapshot,
chunk_seq: *chunk_seq,
};
let located = bytes
.map(|bytes| {
value::decode_owned::<InlineChunkRangeValue>(Bytes::copy_from_slice(bytes))
.and_then(|range| {
InlineChunkLocator::from_directory(
operation,
range.row_id_start,
*row_id_end,
)
})
})
.transpose();
match located {
Ok(locator) => self.writes.push((*table_id, operation, locator)),
Err(_) => {
self.unfollowable.insert(*table_id);
}
}
}
Key::Inline(InlineKey::Live(operation) | InlineKey::Arch(operation)) => {
self.chunks.insert((operation.table_id(), *operation));
}
_ => {}
}
}
fn apply(
mut self,
sets: &mut BTreeMap<u64, imbl::OrdMap<InlineOperation, InlineChunkLocator>>,
) {
let located: BTreeSet<(u64, InlineOperation)> = self
.writes
.iter()
.map(|(table_id, operation, _)| (*table_id, *operation))
.collect();
for (table_id, _) in self.chunks.difference(&located) {
self.unfollowable.insert(*table_id);
}
for table_id in &self.unfollowable {
sets.remove(table_id);
}
for (table_id, operation, locator) in self.writes {
if self.unfollowable.contains(&table_id) {
continue;
}
let Some(set) = sets.get_mut(&table_id) else {
continue;
};
match locator {
Some(locator) => set.insert(operation, locator),
None => set.remove(&operation),
};
}
}
}
struct Maintained<K: Ord, V> {
head: Option<HeadValue>,
rows: Arc<BTreeMap<K, V>>,
}
fn head_stamp(writes: &[StagedWrite]) -> Option<HeadValue> {
let head_key = Key::Sys(SysKey::Head).encode();
writes.iter().rev().find_map(|(key, bytes)| {
(key == &head_key)
.then_some(bytes.as_ref())
.flatten()
.and_then(|bytes| value::decode_value::<HeadValue>(bytes).ok())
})
}
pub(crate) fn materialize<K: Ord, V: Clone>(rows: &BTreeMap<K, V>) -> Vec<V> {
rows.values().cloned().collect()
}
type RecordHalf = Option<(HeadValue, RecordSet)>;
#[derive(Clone, Copy)]
enum Half {
Current,
History,
}
fn fold_record(
half: &mut RecordHalf,
encoded_key: &[u8],
bytes: Option<&[u8]>,
decode: impl FnOnce(&[u8]) -> crate::error::Result<EntityRecord>,
) {
let Some((_, records)) = half else {
return;
};
match bytes {
None => {
records.remove(encoded_key);
}
Some(bytes) => match decode(bytes) {
Ok(record) => {
records.insert(Bytes::copy_from_slice(encoded_key), Arc::new(record));
}
Err(_) => *half = None,
},
}
}
fn fold_current_write(half: &mut RecordHalf, key: &Key, encoded_key: &[u8], bytes: Option<&[u8]>) {
if let Key::Current(_) = key {
fold_record(half, encoded_key, bytes, |bytes| {
decode_current_record(key.clone(), bytes)
});
}
}
fn same_head(a: &HeadValue, b: &HeadValue) -> bool {
a.snapshot_id == b.snapshot_id && a.batch_seq == b.batch_seq
}
pub(crate) fn view_head(view: &CatalogSnapshot) -> HeadValue {
HeadValue {
snapshot_id: view.snapshot.snapshot_id,
batch_seq: view.batch_seq,
}
}
impl<K: Ord, V> Maintained<K, V> {
fn empty() -> Self {
Self {
head: None,
rows: Arc::new(BTreeMap::new()),
}
}
fn install(&mut self, head: HeadValue, rows: BTreeMap<K, V>) {
self.head = Some(head);
self.rows = Arc::new(rows);
}
fn clear(&mut self) {
self.head = None;
self.rows = Arc::new(BTreeMap::new());
}
fn estimated_bytes(&self) -> u64
where
V: prost::Message,
{
self.rows
.values()
.map(|value| {
u64::try_from(value.encoded_len()).unwrap_or(u64::MAX)
+ u64::try_from(std::mem::size_of::<K>()).unwrap_or(0)
})
.sum()
}
fn advance(&mut self, new_head: HeadValue) {
if self.head.is_some() {
self.head = Some(new_head);
}
}
fn clear_unless_at(&mut self, expected: HeadValue) {
if self
.head
.as_ref()
.is_some_and(|head| !same_head(head, &expected))
{
self.clear();
}
}
fn serve(&self, expected: &HeadValue) -> Option<Arc<BTreeMap<K, V>>> {
self.head
.as_ref()
.is_some_and(|head| same_head(head, expected))
.then(|| Arc::clone(&self.rows))
}
fn fold(&mut self, key: K, bytes: Option<&[u8]>)
where
K: Clone,
V: prost::Message + Default + Clone,
{
if self.head.is_none() {
return;
}
let decoded = match bytes {
None => None,
Some(bytes) => {
let Ok(decoded) = value::decode_value(bytes) else {
self.clear();
return;
};
Some(decoded)
}
};
let rows = Arc::make_mut(&mut self.rows);
match decoded {
None => {
rows.remove(&key);
}
Some(decoded) => {
rows.insert(key, decoded);
}
}
}
}
pub(crate) fn fold_committed_batch(
cache: &std::sync::RwLock<ProjectionCache>,
writes: &[StagedWrite],
new_head: u64,
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.apply_batch(writes, new_head);
}
pub(crate) fn cached_head_view(
cache: &std::sync::RwLock<ProjectionCache>,
expected: &HeadValue,
) -> Option<Arc<CatalogSnapshot>> {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.head_view(expected)
}
pub(crate) fn held_head_view(
cache: &std::sync::RwLock<ProjectionCache>,
) -> Option<Arc<CatalogSnapshot>> {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.head_view
.clone()
}
pub(crate) fn install_head_view(
cache: &std::sync::RwLock<ProjectionCache>,
view: Arc<CatalogSnapshot>,
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.set_head_view(view);
}
pub(crate) fn cache_epoch(cache: &std::sync::RwLock<ProjectionCache>) -> u64 {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.epoch()
}
pub(crate) fn install_head_view_at(
cache: &std::sync::RwLock<ProjectionCache>,
epoch: u64,
view: Arc<CatalogSnapshot>,
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.set_head_view_at(epoch, view);
}
pub(crate) fn format_floor(cache: &std::sync::RwLock<ProjectionCache>) -> u64 {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.format_floor()
}
pub(crate) fn raise_format_floor(cache: &std::sync::RwLock<ProjectionCache>, observed: u64) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.raise_format_floor(observed);
}
pub(crate) fn migration_clear_at(
cache: &std::sync::RwLock<ProjectionCache>,
head: &HeadValue,
) -> bool {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.migration_clear_at(head)
}
pub(crate) fn note_migration_clear(cache: &std::sync::RwLock<ProjectionCache>, head: HeadValue) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.note_migration_clear(head);
}
pub(crate) fn folded_inline_locators(
cache: &std::sync::RwLock<ProjectionCache>,
table_id: u64,
head: &HeadValue,
) -> Option<imbl::OrdMap<InlineOperation, InlineChunkLocator>> {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.folded_inline_locators(table_id, head)
}
pub(crate) fn seed_inline_locators(
cache: &std::sync::RwLock<ProjectionCache>,
table_id: u64,
head: &HeadValue,
locators: imbl::OrdMap<InlineOperation, InlineChunkLocator>,
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.seed_inline_locators(table_id, head, locators);
}
pub(crate) fn inline_directory_complete(
cache: &std::sync::RwLock<ProjectionCache>,
table_id: u64,
) -> bool {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.inline_directory_complete(table_id)
}
pub(crate) fn inline_chunk_width(
cache: &std::sync::RwLock<ProjectionCache>,
table_id: u64,
) -> Option<u64> {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.inline_chunk_width(table_id)
}
pub(crate) fn note_inline_chunk_width(
cache: &std::sync::RwLock<ProjectionCache>,
table_id: u64,
width: u64,
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.note_inline_chunk_width(table_id, width);
}
pub(crate) fn note_inline_directory_complete(
cache: &std::sync::RwLock<ProjectionCache>,
table_id: u64,
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.note_inline_directory_complete(table_id);
}
pub(crate) fn invalidate_head_view(cache: &std::sync::RwLock<ProjectionCache>) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear_head_view();
}
pub(crate) fn invalidate_current_state(cache: &std::sync::RwLock<ProjectionCache>) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear_current_state();
}
pub(crate) fn shared_current_entities(
cache: &std::sync::RwLock<ProjectionCache>,
head: &HeadValue,
) -> Option<Arc<RecordSet>> {
cache
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.current_entities_at(head)
}
pub(crate) fn install_shared_current_entities(
cache: &std::sync::RwLock<ProjectionCache>,
head: HeadValue,
records: Arc<RecordSet>,
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.install_current_entities(head, records);
}
pub(crate) fn fold_replayed_writes(
cache: &std::sync::RwLock<ProjectionCache>,
base: &HeadValue,
head: HeadValue,
writes: &[StagedWrite],
) {
cache
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.fold_current_across_gap(base, head, writes);
}
pub(crate) struct ProjectionCache {
snapshots: Maintained<u64, SnapshotValue>,
table_stats: Maintained<u64, TableStatsValue>,
table_column_stats: Maintained<(u64, u64), TableColumnStatsValue>,
current_entities: Option<(HeadValue, Arc<RecordSet>)>,
history_entities: Option<(HeadValue, Arc<RecordSet>)>,
head_view: Option<Arc<CatalogSnapshot>>,
folded_head: Option<HeadValue>,
format_floor: u64,
migration_clear: Option<HeadValue>,
inline_directory_complete: BTreeSet<u64>,
inline_locators: BTreeMap<u64, imbl::OrdMap<InlineOperation, InlineChunkLocator>>,
inline_chunk_width: BTreeMap<u64, u64>,
epoch: u64,
}
impl ProjectionCache {
pub(crate) fn estimated_bytes(&self) -> u64 {
let records = |half: &Option<(HeadValue, Arc<RecordSet>)>| {
half.as_ref().map_or(0, |(_, records)| {
records
.values()
.map(|record| record.estimated_bytes())
.sum()
})
};
records(&self.current_entities)
.saturating_add(records(&self.history_entities))
.saturating_add(self.snapshots.estimated_bytes())
.saturating_add(self.table_stats.estimated_bytes())
.saturating_add(self.table_column_stats.estimated_bytes())
.saturating_add(
self.head_view
.as_ref()
.map_or(0, |view| view.estimated_bytes()),
)
}
pub(crate) fn empty() -> Self {
Self {
snapshots: Maintained::empty(),
table_stats: Maintained::empty(),
table_column_stats: Maintained::empty(),
current_entities: None,
history_entities: None,
head_view: None,
folded_head: None,
format_floor: 0,
migration_clear: None,
inline_directory_complete: BTreeSet::new(),
inline_locators: BTreeMap::new(),
inline_chunk_width: BTreeMap::new(),
epoch: 0,
}
}
fn folded_inline_locators(
&self,
table_id: u64,
head: &HeadValue,
) -> Option<imbl::OrdMap<InlineOperation, InlineChunkLocator>> {
self.folded_head
.as_ref()
.is_some_and(|folded| same_head(folded, head))
.then(|| self.inline_locators.get(&table_id).cloned())
.flatten()
}
pub(crate) fn seed_inline_locators(
&mut self,
table_id: u64,
head: &HeadValue,
locators: imbl::OrdMap<InlineOperation, InlineChunkLocator>,
) {
if self
.folded_head
.as_ref()
.is_some_and(|folded| same_head(folded, head))
{
self.inline_locators.insert(table_id, locators);
}
}
pub(crate) fn inline_directory_complete(&self, table_id: u64) -> bool {
self.inline_directory_complete.contains(&table_id)
}
pub(crate) fn note_inline_directory_complete(&mut self, table_id: u64) {
self.inline_directory_complete.insert(table_id);
}
pub(crate) fn inline_chunk_width(&self, table_id: u64) -> Option<u64> {
self.inline_chunk_width.get(&table_id).copied()
}
pub(crate) fn note_inline_chunk_width(&mut self, table_id: u64, width: u64) {
let slot = self.inline_chunk_width.entry(table_id).or_default();
*slot = (*slot).max(width);
}
pub(crate) fn format_floor(&self) -> u64 {
self.format_floor
}
pub(crate) fn raise_format_floor(&mut self, observed: u64) {
self.format_floor = self.format_floor.max(observed);
}
pub(crate) fn migration_clear_at(&self, head: &HeadValue) -> bool {
self.migration_clear.as_ref() == Some(head)
}
pub(crate) fn note_migration_clear(&mut self, head: HeadValue) {
self.migration_clear = Some(head);
}
pub(crate) fn head_view(&self, expected: &HeadValue) -> Option<Arc<CatalogSnapshot>> {
self.head_view
.as_ref()
.filter(|view| {
view.snapshot.snapshot_id == expected.snapshot_id
&& view.batch_seq == expected.batch_seq
})
.cloned()
}
pub(crate) fn epoch(&self) -> u64 {
self.epoch
}
pub(crate) fn set_head_view(&mut self, view: Arc<CatalogSnapshot>) {
self.head_view = Some(view);
self.epoch = self.epoch.wrapping_add(1);
}
pub(crate) fn set_head_view_at(&mut self, epoch: u64, view: Arc<CatalogSnapshot>) {
if self.epoch == epoch {
self.head_view = Some(view);
}
}
pub(crate) fn clear_head_view(&mut self) {
self.head_view = None;
self.epoch = self.epoch.wrapping_add(1);
}
pub(crate) fn clear_current_state(&mut self) {
self.head_view = None;
self.current_entities = None;
self.epoch = self.epoch.wrapping_add(1);
}
pub(crate) fn install_current_entities(&mut self, head: HeadValue, records: Arc<RecordSet>) {
self.current_entities = Some((head, records));
}
pub(crate) fn install_history_entities(&mut self, head: HeadValue, records: Arc<RecordSet>) {
self.history_entities = Some((head, records));
}
pub(crate) fn current_entities_at(&self, expected: &HeadValue) -> Option<Arc<RecordSet>> {
self.current_entities
.as_ref()
.and_then(|(head, records)| same_head(head, expected).then(|| Arc::clone(records)))
}
pub(crate) fn history_entities_at(&self, expected: &HeadValue) -> Option<Arc<RecordSet>> {
self.history_entities
.as_ref()
.and_then(|(head, records)| same_head(head, expected).then(|| Arc::clone(records)))
}
pub(crate) fn install_snapshots(&mut self, head: HeadValue, rows: Vec<SnapshotValue>) {
self.snapshots
.install(head, rows.into_iter().map(|r| (r.snapshot_id, r)).collect());
}
pub(crate) fn install_table_stats(&mut self, head: HeadValue, rows: Vec<TableStatsValue>) {
self.table_stats
.install(head, rows.into_iter().map(|r| (r.table_id, r)).collect());
}
pub(crate) fn install_table_column_stats(
&mut self,
head: HeadValue,
rows: Vec<TableColumnStatsValue>,
) {
self.table_column_stats.install(
head,
rows.into_iter()
.map(|r| ((r.table_id, r.column_id), r))
.collect(),
);
}
pub(crate) fn snapshots_at(
&self,
expected: &HeadValue,
) -> Option<Arc<BTreeMap<u64, SnapshotValue>>> {
self.snapshots.serve(expected)
}
pub(crate) fn table_stats_at(
&self,
expected: &HeadValue,
) -> Option<Arc<BTreeMap<u64, TableStatsValue>>> {
self.table_stats.serve(expected)
}
pub(crate) fn table_column_stats_at(
&self,
expected: &HeadValue,
) -> Option<Arc<BTreeMap<(u64, u64), TableColumnStatsValue>>> {
self.table_column_stats.serve(expected)
}
pub(crate) fn apply_batch(&mut self, writes: &[StagedWrite], new_head: u64) {
self.drop_what_lags_behind();
let mut current = self.take_half(Half::Current);
let mut history = self.take_half(Half::History);
let mut inline = InlineFold::default();
for (encoded_key, write) in writes {
let bytes = write.as_deref();
let Ok(key) = Key::decode(encoded_key) else {
self.clear_folded();
return;
};
inline.observe(&key, bytes);
match key {
Key::Snapshot { snapshot_id } => self.snapshots.fold(snapshot_id, bytes),
Key::Current(CurrentKey::Entity(EntityKey::TableStats { table_id })) => {
self.table_stats.fold(table_id, bytes);
}
Key::Current(CurrentKey::Entity(EntityKey::TableColumnStats {
table_id,
column_id,
})) => self.table_column_stats.fold((table_id, column_id), bytes),
_ => {}
}
match &key {
Key::Current(_) => fold_current_write(&mut current, &key, encoded_key, bytes),
Key::History(_) => fold_record(&mut history, encoded_key, bytes, |bytes| {
decode_history_record(key.clone(), bytes)
}),
_ => {}
}
}
let Some(stamp) = head_stamp(writes).filter(|stamp| stamp.snapshot_id == new_head) else {
self.clear_folded();
return;
};
self.snapshots.advance(stamp);
self.table_stats.advance(stamp);
self.table_column_stats.advance(stamp);
self.current_entities = current.map(|(_, records)| (stamp, Arc::new(records)));
self.history_entities = history.map(|(_, records)| (stamp, Arc::new(records)));
inline.apply(&mut self.inline_locators);
self.folded_head = Some(stamp);
}
pub(crate) fn fold_current_across_gap(
&mut self,
base: &HeadValue,
head: HeadValue,
writes: &[StagedWrite],
) {
self.history_entities = None;
let mut current = self.take_half(Half::Current);
if current
.as_ref()
.is_none_or(|(stamp, _)| !same_head(stamp, base))
{
return;
}
for (encoded_key, write) in writes {
match Key::decode(encoded_key) {
Ok(key) => fold_current_write(&mut current, &key, encoded_key, write.as_deref()),
Err(_) => return,
}
}
self.current_entities = current.map(|(_, records)| (head, Arc::new(records)));
}
fn take_half(&mut self, half: Half) -> RecordHalf {
let slot = match half {
Half::Current => &mut self.current_entities,
Half::History => &mut self.history_entities,
};
slot.take()
.map(|(head, records)| (head, RecordSet::clone(&records)))
}
fn clear_folded(&mut self) {
self.current_entities = None;
self.history_entities = None;
self.inline_locators.clear();
self.snapshots.clear();
self.table_stats.clear();
self.table_column_stats.clear();
}
fn drop_what_lags_behind(&mut self) {
let Some(folded_head) = self.folded_head else {
return;
};
let lags = |half: &Option<(HeadValue, Arc<RecordSet>)>| {
half.as_ref()
.is_some_and(|(head, _)| !same_head(head, &folded_head))
};
if lags(&self.current_entities) {
self.current_entities = None;
}
if lags(&self.history_entities) {
self.history_entities = None;
}
self.snapshots.clear_unless_at(folded_head);
self.table_stats.clear_unless_at(folded_head);
self.table_column_stats.clear_unless_at(folded_head);
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use object_store::memory::InMemory;
use super::*;
use crate::{
Catalog, CatalogOptions, ColumnDef,
ffi_support::{dump_snapshots, dump_table_column_stats, dump_table_stats},
store::{
key::{EntityKey, Key},
proto::{SchemaValue, SnapshotValue, TableColumnStatsValue, TableStatsValue},
value::encode_value,
},
};
fn view_at(snapshot_id: u64) -> Arc<CatalogSnapshot> {
Arc::new(CatalogSnapshot {
snapshot: SnapshotValue {
snapshot_id,
..SnapshotValue::default()
},
..CatalogSnapshot::default()
})
}
fn head_at(snapshot_id: u64) -> HeadValue {
HeadValue {
snapshot_id,
batch_seq: 0,
}
}
#[test]
fn an_interleaved_invalidation_voids_an_install() {
let cache = std::sync::RwLock::new(ProjectionCache::empty());
let epoch = cache_epoch(&cache);
invalidate_head_view(&cache);
install_head_view_at(&cache, epoch, view_at(7));
assert!(cached_head_view(&cache, &head_at(7)).is_none());
}
#[test]
fn an_uncontended_install_lands() {
let cache = std::sync::RwLock::new(ProjectionCache::empty());
let epoch = cache_epoch(&cache);
install_head_view_at(&cache, epoch, view_at(7));
assert!(cached_head_view(&cache, &head_at(7)).is_some());
}
async fn current_head(catalog: &Catalog) -> HeadValue {
let session = catalog.begin_read().await.unwrap();
let head = crate::store::read::read_head(session.handle())
.await
.unwrap()
.expect("an initialized store has a head");
session.finish();
head
}
async fn scanned_snapshots(catalog: &Catalog) -> Vec<SnapshotValue> {
let session = catalog.begin_read().await.unwrap();
let rows = crate::store::read::scan_snapshots(session.handle())
.await
.unwrap();
session.finish();
rows
}
#[tokio::test]
async fn dumps_after_commits_match_fresh_scans() {
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap();
for round in 0..8u64 {
let _ = dump_snapshots(&catalog).await.unwrap();
catalog
.commit(|tx| {
let main = tx.schemas()[0].id;
let table = tx.create_table(
main,
&format!("t{round}"),
&[ColumnDef {
name: "id".into(),
column_type: "BIGINT".into(),
nulls_allowed: false,
default_value: None,
children: Vec::new(),
}],
)?;
if round % 2 == 0 {
tx.rename_table(table, &format!("t{round}_renamed"))?;
}
Ok(())
})
.await
.unwrap();
let served = dump_snapshots(&catalog).await.unwrap();
assert_eq!(served, scanned_snapshots(&catalog).await, "round {round}");
let head = current_head(&catalog).await;
let cache_current = {
let guard = catalog
.projections()
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.snapshots_at(&head).is_some()
};
assert!(
cache_current,
"cache must be current at head {head:?} after serving"
);
let _ = dump_table_stats(&catalog).await.unwrap();
let _ = dump_table_column_stats(&catalog).await.unwrap();
}
}
async fn seed_one_table(catalog: &Catalog, name: &str) {
catalog
.commit(|tx| {
let main = tx.schemas()[0].id;
tx.create_table(
main,
name,
&[ColumnDef {
name: "id".into(),
column_type: "BIGINT".into(),
nulls_allowed: false,
default_value: None,
children: Vec::new(),
}],
)?;
Ok(())
})
.await
.unwrap();
}
async fn read_every_seam(catalog: &Catalog) {
let _ = catalog.snapshot().await.unwrap();
let _ = dump_snapshots(catalog).await.unwrap();
let _ = dump_table_stats(catalog).await.unwrap();
let _ = dump_table_column_stats(catalog).await.unwrap();
}
#[tokio::test]
async fn a_warm_writer_resolves_head_without_reading_the_store() {
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap();
seed_one_table(&catalog, "t").await;
read_every_seam(&catalog).await;
let warmed = catalog.head_reads();
for _ in 0..8 {
read_every_seam(&catalog).await;
}
assert_eq!(
catalog.head_reads(),
warmed,
"a warm read-write handle read `sys/head`"
);
}
#[tokio::test]
async fn a_warm_writer_still_sees_its_own_later_commits() {
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap();
seed_one_table(&catalog, "first").await;
read_every_seam(&catalog).await;
let warmed = catalog.head_reads();
seed_one_table(&catalog, "second").await;
let view = catalog.snapshot().await.unwrap();
let main = view.schemas()[0].id;
assert!(
view.table_by_name(main, "second").is_some(),
"a warm read missed the commit that preceded it"
);
assert_eq!(
dump_snapshots(&catalog).await.unwrap(),
scanned_snapshots(&catalog).await,
"the warm dump disagreed with the store"
);
assert_eq!(
catalog.head_reads(),
warmed,
"seeing the commit cost a head read"
);
}
#[tokio::test]
async fn a_read_only_handle_resolves_head_from_the_store_every_read() {
let object_store: Arc<dyn object_store::ObjectStore> = Arc::new(InMemory::new());
let writer = Catalog::open(Arc::clone(&object_store), CatalogOptions::default())
.await
.unwrap();
seed_one_table(&writer, "t").await;
writer.close().await.unwrap();
let reader = Catalog::open_read_only(object_store, CatalogOptions::default())
.await
.unwrap();
let _ = reader.snapshot().await.unwrap();
let after_first = reader.head_reads();
for _ in 0..4 {
let _ = reader.snapshot().await.unwrap();
}
assert_eq!(
reader.head_reads(),
after_first + 4,
"a read-only handle served a read without resolving the head"
);
}
#[tokio::test]
async fn an_invalidated_view_sends_the_writer_back_to_the_store() {
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap();
seed_one_table(&catalog, "t").await;
let _ = catalog.snapshot().await.unwrap();
assert!(catalog.writer_head_view().is_some());
invalidate_head_view(catalog.projections());
assert!(
catalog.writer_head_view().is_none(),
"an invalidated cache still served a warm read"
);
}
#[test]
fn a_commit_install_refuses_an_older_readers_install() {
let cache = std::sync::RwLock::new(ProjectionCache::empty());
let epoch = cache_epoch(&cache);
install_head_view(&cache, view_at(9));
install_head_view_at(&cache, epoch, view_at(7));
assert!(cached_head_view(&cache, &head_at(9)).is_some());
assert!(cached_head_view(&cache, &head_at(7)).is_none());
}
fn snapshot_value(id: u64) -> SnapshotValue {
SnapshotValue {
snapshot_id: id,
snapshot_time_micros: 1,
schema_version: 0,
next_catalog_id: 1,
next_file_id: 0,
changes_made: String::new(),
author: None,
commit_message: None,
commit_extra_info: None,
schema_changed_table_ids: Vec::new(),
deleted_data_file_ids: Vec::new(),
}
}
fn stats_value(table_id: u64, record_count: u64) -> TableStatsValue {
TableStatsValue {
table_id,
record_count,
next_row_id: record_count,
file_size_bytes: 100,
}
}
fn column_stats_value(table_id: u64, column_id: u64) -> TableColumnStatsValue {
TableColumnStatsValue {
table_id,
column_id,
contains_null: Some(false),
contains_nan: None,
min_value: Some("1".into()),
max_value: Some("9".into()),
extra_stats: None,
}
}
fn stamp(snapshot_id: u64, batch_seq: u64) -> HeadValue {
HeadValue {
snapshot_id,
batch_seq,
}
}
fn head_write(snapshot_id: u64, batch_seq: u64) -> StagedWrite {
(
Key::Sys(SysKey::Head).encode(),
Some(encode_value(&stamp(snapshot_id, batch_seq))),
)
}
fn schema_value(schema_id: u64, end_snapshot: Option<u64>) -> SchemaValue {
SchemaValue {
schema_id,
schema_name: format!("schema-{schema_id}"),
begin_snapshot: 1,
end_snapshot,
..SchemaValue::default()
}
}
fn half_of(records: Vec<(Key, EntityRecord)>) -> Arc<RecordSet> {
Arc::new(
records
.into_iter()
.map(|(key, record)| (Bytes::from(key.encode()), Arc::new(record)))
.collect(),
)
}
fn schema_ids(records: &RecordSet) -> Vec<u64> {
records
.values()
.filter_map(|record| match record.as_ref() {
EntityRecord::Schema(schema) => Some(schema.schema_id),
_ => None,
})
.collect()
}
fn installed_at_three() -> ProjectionCache {
let mut cache = ProjectionCache::empty();
cache.install_snapshots(stamp(3, 3), (0..=3).map(snapshot_value).collect());
cache.install_table_stats(stamp(3, 3), vec![stats_value(7, 10)]);
cache.install_table_column_stats(stamp(3, 3), vec![column_stats_value(7, 1)]);
cache
}
#[test]
fn fold_inserts_snapshot_and_updates_stats() {
let mut cache = installed_at_three();
let writes = vec![
(
Key::Snapshot { snapshot_id: 4 }.encode(),
Some(encode_value(&snapshot_value(4))),
),
(
Key::current(EntityKey::TableStats { table_id: 7 }).encode(),
Some(encode_value(&stats_value(7, 11))),
),
head_write(4, 4),
];
cache.apply_batch(&writes, 4);
let at = stamp(4, 4);
let snapshots = materialize(&cache.snapshots_at(&at).unwrap());
assert_eq!(snapshots.len(), 5);
assert_eq!(snapshots.last().unwrap().snapshot_id, 4);
let stats = materialize(&cache.table_stats_at(&at).unwrap());
assert_eq!(stats.len(), 1);
assert_eq!(stats[0].record_count, 11);
assert_eq!(cache.table_column_stats_at(&at).unwrap().len(), 1);
}
#[test]
fn fold_deletes_remove_rows() {
let mut cache = installed_at_three();
let writes = vec![
(Key::Snapshot { snapshot_id: 2 }.encode(), None),
(
Key::current(EntityKey::TableColumnStats {
table_id: 7,
column_id: 1,
})
.encode(),
None,
),
head_write(3, 4),
];
cache.apply_batch(&writes, 3);
let at = stamp(3, 4);
let snapshots = materialize(&cache.snapshots_at(&at).unwrap());
assert_eq!(snapshots.len(), 3);
assert!(snapshots.iter().all(|s| s.snapshot_id != 2));
assert!(cache.table_column_stats_at(&at).unwrap().is_empty());
assert!(cache.snapshots_at(&stamp(3, 3)).is_none());
}
#[test]
fn serve_refuses_a_mismatched_head() {
let cache = installed_at_three();
assert!(cache.snapshots_at(&stamp(4, 4)).is_none());
assert!(cache.table_stats_at(&stamp(2, 2)).is_none());
assert!(cache.snapshots_at(&stamp(3, 4)).is_none());
}
#[test]
fn fold_on_an_empty_cache_is_a_noop() {
let mut cache = ProjectionCache::empty();
cache.apply_batch(
&[
(
Key::Snapshot { snapshot_id: 1 }.encode(),
Some(encode_value(&snapshot_value(1))),
),
head_write(1, 1),
],
1,
);
assert!(cache.snapshots_at(&stamp(1, 1)).is_none());
}
#[test]
fn an_undecodable_value_clears_only_the_touched_projection() {
let mut cache = installed_at_three();
cache.apply_batch(
&[
(
Key::Snapshot { snapshot_id: 4 }.encode(),
Some(vec![0xff, 0xff, 0xff, 0xff]),
),
head_write(4, 4),
],
4,
);
assert!(cache.snapshots_at(&stamp(4, 4)).is_none());
assert!(cache.table_stats_at(&stamp(4, 4)).is_some());
}
#[test]
fn an_undecodable_key_clears_everything() {
let mut cache = installed_at_three();
cache.apply_batch(&[(vec![0xff, 0xee], Some(vec![])), head_write(4, 4)], 4);
assert!(cache.snapshots_at(&stamp(4, 4)).is_none());
assert!(cache.table_stats_at(&stamp(4, 4)).is_none());
assert!(cache.table_column_stats_at(&stamp(4, 4)).is_none());
}
#[test]
fn rows_installed_behind_the_cache_are_dropped_rather_than_carried() {
let mut cache = ProjectionCache::empty();
cache.install_snapshots(stamp(1, 1), vec![snapshot_value(0), snapshot_value(1)]);
cache.install_current_entities(
stamp(1, 1),
half_of(vec![(
Key::current(EntityKey::TableStats { table_id: 7 }),
EntityRecord::TableStats(stats_value(7, 10)),
)]),
);
cache.apply_batch(
&[
(
Key::Snapshot { snapshot_id: 2 }.encode(),
Some(encode_value(&snapshot_value(2))),
),
head_write(2, 2),
],
2,
);
assert_eq!(
materialize(&cache.snapshots_at(&stamp(2, 2)).unwrap()).len(),
3
);
cache.install_snapshots(stamp(1, 1), vec![snapshot_value(0), snapshot_value(1)]);
cache.install_current_entities(
stamp(1, 1),
half_of(vec![(
Key::current(EntityKey::TableStats { table_id: 7 }),
EntityRecord::TableStats(stats_value(7, 10)),
)]),
);
cache.apply_batch(
&[
(
Key::Snapshot { snapshot_id: 3 }.encode(),
Some(encode_value(&snapshot_value(3))),
),
head_write(3, 3),
],
3,
);
assert!(cache.snapshots_at(&stamp(3, 3)).is_none());
assert!(cache.current_entities_at(&stamp(3, 3)).is_none());
}
#[test]
fn untouched_record_halves_ride_the_stamp_forward() {
let half = || {
half_of(vec![(
Key::current(EntityKey::TableStats { table_id: 7 }),
EntityRecord::TableStats(stats_value(7, 10)),
)])
};
let mut cache = installed_at_three();
cache.install_current_entities(stamp(3, 3), half());
cache.install_history_entities(stamp(3, 3), half());
cache.apply_batch(
&[
(
Key::Inline(crate::store::key::InlineKey::Live(
crate::store::key::InlineOperation::Insert {
table_id: 7,
schema_version: 1,
begin_snapshot: 4,
chunk_seq: 0,
},
))
.encode(),
Some(vec![1, 2, 3]),
),
head_write(4, 4),
],
4,
);
assert!(cache.current_entities_at(&stamp(4, 4)).is_some());
assert!(cache.history_entities_at(&stamp(4, 4)).is_some());
assert!(cache.current_entities_at(&stamp(3, 3)).is_none());
cache.apply_batch(
&[
(
Key::current(EntityKey::Schema { schema_id: 9 }).encode(),
Some(vec![1, 2, 3]),
),
head_write(5, 5),
],
5,
);
assert!(cache.current_entities_at(&stamp(5, 5)).is_none());
assert!(cache.history_entities_at(&stamp(5, 5)).is_some());
}
#[test]
fn current_writes_fold_into_the_current_half() {
let mut cache = installed_at_three();
cache.install_current_entities(
stamp(3, 3),
half_of(vec![
(
Key::current(EntityKey::Schema { schema_id: 1 }),
EntityRecord::Schema(schema_value(1, None)),
),
(
Key::current(EntityKey::Schema { schema_id: 2 }),
EntityRecord::Schema(schema_value(2, None)),
),
]),
);
cache.apply_batch(
&[
(
Key::current(EntityKey::Schema { schema_id: 1 }).encode(),
None,
),
(
Key::current(EntityKey::Schema { schema_id: 9 }).encode(),
Some(encode_value(&schema_value(9, None))),
),
head_write(4, 4),
],
4,
);
let current = cache.current_entities_at(&stamp(4, 4)).unwrap();
assert_eq!(schema_ids(¤t), vec![2, 9]);
}
#[test]
fn history_writes_fold_into_the_history_half() {
let mut cache = installed_at_three();
cache.install_history_entities(stamp(3, 3), Arc::default());
cache.apply_batch(
&[
(
Key::current(EntityKey::Schema { schema_id: 1 }).encode(),
None,
),
(
Key::history(EntityKey::Schema { schema_id: 1 }, 4).encode(),
Some(encode_value(&schema_value(1, Some(4)))),
),
head_write(4, 4),
],
4,
);
let history = cache.history_entities_at(&stamp(4, 4)).unwrap();
assert_eq!(schema_ids(&history), vec![1]);
assert!(cache.current_entities_at(&stamp(4, 4)).is_none());
}
#[test]
fn a_replayed_gap_folds_the_current_half_and_drops_history() {
let mut cache = ProjectionCache::empty();
cache.install_current_entities(
stamp(3, 3),
half_of(vec![(
Key::current(EntityKey::Schema { schema_id: 1 }),
EntityRecord::Schema(schema_value(1, None)),
)]),
);
cache.install_history_entities(stamp(3, 3), Arc::default());
cache.fold_current_across_gap(
&stamp(3, 3),
stamp(5, 5),
&[(
Key::current(EntityKey::Schema { schema_id: 2 }).encode(),
Some(encode_value(&schema_value(2, None))),
)],
);
let current = cache.current_entities_at(&stamp(5, 5)).unwrap();
assert_eq!(schema_ids(¤t), vec![1, 2]);
assert!(cache.history_entities_at(&stamp(5, 5)).is_none());
cache.fold_current_across_gap(&stamp(4, 4), stamp(6, 6), &[]);
assert!(cache.current_entities_at(&stamp(6, 6)).is_none());
}
#[test]
fn irrelevant_keys_still_advance_the_head() {
let mut cache = installed_at_three();
cache.apply_batch(
&[
(
Key::current(EntityKey::Schema { schema_id: 9 }).encode(),
Some(vec![1, 2, 3]),
),
head_write(4, 4),
],
4,
);
assert_eq!(cache.snapshots_at(&stamp(4, 4)).unwrap().len(), 4);
assert!(cache.snapshots_at(&stamp(3, 3)).is_none());
}
#[test]
fn a_batch_without_a_head_write_clears_everything() {
let mut cache = installed_at_three();
cache.apply_batch(
&[(
Key::Snapshot { snapshot_id: 4 }.encode(),
Some(encode_value(&snapshot_value(4))),
)],
4,
);
assert!(cache.snapshots_at(&stamp(4, 4)).is_none());
assert!(cache.snapshots_at(&stamp(3, 3)).is_none());
assert!(cache.table_stats_at(&stamp(3, 3)).is_none());
}
}