use std::sync::Arc;
use moraine::{Catalog, CatalogOptions, SnapshotId};
use object_store::memory::InMemory;
use crate::{
counting_store::CountingStore,
fixtures::{col, datafile, seeded},
};
#[tokio::test]
async fn a_read_only_handle_materializes_once() {
let object_store = Arc::new(InMemory::new());
let writer = Catalog::open(
Arc::clone(&object_store) as Arc<dyn object_store::ObjectStore>,
CatalogOptions::default(),
)
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
let table = tx.create_table(schema, "t", &[col("a")])?;
for _ in 0..64 {
tx.register_data_file(table, datafile(100), &[])?;
}
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let counting = Arc::new(CountingStore::new(object_store));
let reader = Catalog::open_read_only(
Arc::clone(&counting) as Arc<dyn object_store::ObjectStore>,
CatalogOptions::default(),
)
.await
.unwrap();
let first = reader.snapshot().await.unwrap();
let cold = counting.take_reads();
assert!(cold > 0, "a cold view read nothing");
for _ in 0..8 {
let view = reader.snapshot().await.unwrap();
assert_eq!(view.schemas().len(), first.schemas().len());
}
let warm = counting.take_reads();
assert!(
warm < cold,
"warm reads cost as much as the cold one ({warm} vs {cold}): the handle is \
rebuilding the catalog per read"
);
}
#[tokio::test]
async fn a_materialization_reads_ahead_rather_than_block_by_block() {
let object_store = Arc::new(InMemory::new());
let writer = Catalog::open(
Arc::clone(&object_store) as Arc<dyn object_store::ObjectStore>,
CatalogOptions::default(),
)
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
let table = tx.create_table(schema, "t", &[col("a")])?;
for _ in 0..4_000 {
tx.register_data_file(table, datafile(100), &[])?;
}
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let counting = Arc::new(CountingStore::new(object_store));
let reader = Catalog::open_read_only(
Arc::clone(&counting) as Arc<dyn object_store::ObjectStore>,
CatalogOptions::default(),
)
.await
.unwrap();
counting.take_reads();
let view = reader.snapshot().await.unwrap();
let reads = counting.take_reads();
let files = view
.tables_in(view.schemas()[0].id)
.first()
.map(|table| view.data_files_of(table.id).len())
.unwrap_or_default();
assert!(files >= 4_000, "seed did not land: {files} files");
assert!(
reads < 20,
"materialization issued {reads} reads for {files} files — scanning block by block"
);
}
#[tokio::test]
async fn a_read_only_handle_scans_once_for_many_dumps() {
let object_store = Arc::new(InMemory::new());
let writer = Catalog::open(
Arc::clone(&object_store) as Arc<dyn object_store::ObjectStore>,
CatalogOptions::default(),
)
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
let table = tx.create_table(schema, "t", &[col("a")])?;
for _ in 0..2_000 {
tx.register_data_file(table, datafile(100), &[])?;
}
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let counting = Arc::new(CountingStore::new(object_store));
let reader = Catalog::open_read_only(
Arc::clone(&counting) as Arc<dyn object_store::ObjectStore>,
CatalogOptions::default(),
)
.await
.unwrap();
let first = moraine::ffi_support::dump_data_files(&reader)
.await
.unwrap();
let cold = counting.take_reads();
assert!(!first.is_empty(), "seed did not land");
assert!(cold > 0, "a cold dump read nothing");
for _ in 0..12 {
let again = moraine::ffi_support::dump_data_files(&reader)
.await
.unwrap();
assert_eq!(again.len(), first.len());
}
let warm = counting.take_reads();
assert!(
warm < cold,
"twelve further dumps cost {warm} reads against {cold} for one: the reader is \
rescanning per dump"
);
}
#[tokio::test]
async fn a_read_after_a_commit_sees_the_commit() {
let (catalog, schema, table_a, _) = seeded().await;
let first = catalog.snapshot().await.unwrap();
assert!(first.table_by_name(schema, "late").is_none());
catalog
.commit(|tx| {
tx.create_table(schema, "late", &[col("x")])?;
tx.register_data_file(table_a, datafile(5), &[])?;
Ok(())
})
.await
.unwrap();
let second = catalog.snapshot().await.unwrap();
let late = second
.table_by_name(schema, "late")
.expect("a cached read must see the new table");
assert_eq!(second.columns_of(late.id).len(), 1);
assert_eq!(second.data_files_of(table_a).len(), 1);
catalog.close().await.unwrap();
}
#[tokio::test]
async fn repeated_reads_agree() {
let (catalog, schema, table_a, _) = seeded().await;
catalog
.commit(|tx| {
tx.register_data_file(table_a, datafile(3), &[])?;
Ok(())
})
.await
.unwrap();
let first = catalog.snapshot().await.unwrap();
let second = catalog.snapshot().await.unwrap();
assert_eq!(
first.current_snapshot().id.get(),
second.current_snapshot().id.get()
);
assert_eq!(
first.tables_in(schema).len(),
second.tables_in(schema).len()
);
assert_eq!(
first.data_files_of(table_a).len(),
second.data_files_of(table_a).len()
);
catalog.close().await.unwrap();
}
#[tokio::test]
async fn a_cached_read_matches_a_cold_reopen() {
let (catalog, schema, table_a, table_b) = seeded().await;
let warm = catalog.snapshot().await.unwrap();
assert_eq!(warm.tables_in(schema).len(), 2);
let wide = std::cell::Cell::new(None);
catalog
.commit(|tx| {
tx.register_data_file(table_a, datafile(7), &[])?;
tx.rename_table(table_b, "renamed")?;
tx.create_view(schema, "v", "duckdb", "SELECT 1")?;
wide.set(Some(tx.create_table(
schema,
"wide",
&[col("x"), col("y")],
)?));
Ok(())
})
.await
.unwrap();
let wide = wide.get().unwrap();
catalog
.commit(|tx| {
let second = tx.columns_of(wide)[1].id;
tx.drop_column(wide, second)
})
.await
.unwrap();
let cached = catalog.snapshot().await.unwrap();
let head = cached.current_snapshot().id;
let cold = catalog
.snapshot_at(SnapshotId::new(head.get()))
.await
.unwrap();
assert_eq!(cached.tables_in(schema).len(), cold.tables_in(schema).len());
assert!(cached.table_by_name(schema, "renamed").is_some());
assert!(cached.view_by_name(schema, "v").is_some());
assert_eq!(
cached.data_files_of(table_a).len(),
cold.data_files_of(table_a).len()
);
assert_eq!(cached.columns_of(wide).len(), cold.columns_of(wide).len());
assert_eq!(cached.columns_of(wide).len(), 1);
catalog.close().await.unwrap();
}
#[tokio::test]
async fn a_held_view_is_unmoved_by_later_commits() {
let (catalog, schema, table_a, table_b) = seeded().await;
let held = catalog.snapshot().await.unwrap();
let at = held.current_snapshot().id.get();
let tables_before = held.tables_in(schema).len();
let files_before = held.data_files_of(table_a).len();
for round in 0..4u64 {
catalog
.commit(|tx| {
tx.register_data_file(table_a, datafile(round), &[])?;
tx.create_table(schema, &format!("later{round}"), &[col("x")])?;
Ok(())
})
.await
.unwrap();
}
catalog.commit(|tx| tx.drop_table(table_b)).await.unwrap();
assert_eq!(held.current_snapshot().id.get(), at);
assert_eq!(held.tables_in(schema).len(), tables_before);
assert_eq!(held.data_files_of(table_a).len(), files_before);
assert!(held.table_by_name(schema, "later0").is_none());
assert!(held.table_by_name(schema, "b").is_some());
let travelled = catalog.snapshot_at(SnapshotId::new(at)).await.unwrap();
assert_eq!(travelled.tables_in(schema).len(), tables_before);
assert_eq!(travelled.data_files_of(table_a).len(), files_before);
catalog.close().await.unwrap();
}
#[tokio::test]
async fn a_read_only_catalog_serves_a_cached_view_that_matches_the_store() {
let object_store: Arc<InMemory> = Arc::new(InMemory::new());
let writer = Catalog::open(object_store.clone(), CatalogOptions::default())
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.create_schema("s")?;
tx.create_table(schema, "t", &[col("x")])?;
Ok(())
})
.await
.unwrap();
let reader = Catalog::open_read_only(object_store, CatalogOptions::default())
.await
.unwrap();
let first = reader.snapshot().await.unwrap();
let schema = first.schema_by_name("s").unwrap().id;
assert!(first.table_by_name(schema, "t").is_some());
let second = reader.snapshot().await.unwrap();
assert_eq!(
first.current_snapshot().id.get(),
second.current_snapshot().id.get()
);
assert_eq!(
second.tables_in(schema).len(),
first.tables_in(schema).len()
);
assert!(second.table_by_name(schema, "t").is_some());
let scanned = reader
.snapshot_at(SnapshotId::new(first.current_snapshot().id.get()))
.await
.unwrap();
assert_eq!(
scanned.tables_in(schema).len(),
first.tables_in(schema).len()
);
writer.close().await.unwrap();
}
#[tokio::test]
async fn a_bounded_disk_cache_serves_a_writer_and_a_reader() {
let object_store = Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>;
let cache = std::env::temp_dir().join(format!(
"moraine-bounded-cache-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&cache);
let mut options = CatalogOptions::default();
options.cache_dir = Some(cache.clone());
options.cache_size = Some(64 * 1024 * 1024);
let writer = Catalog::open(Arc::clone(&object_store), options.clone())
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
tx.create_table(schema, "t", &[col("a")])?;
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let reader = Catalog::open_read_only(object_store, options)
.await
.unwrap();
let view = reader.snapshot().await.unwrap();
let schema = view.schema_by_name("main").expect("bootstrap").id;
assert!(view.table_by_name(schema, "t").is_some());
reader.close().await.unwrap();
let _ = std::fs::remove_dir_all(&cache);
}
#[tokio::test]
async fn the_cache_reports_what_it_served() {
let object_store = Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>;
let before = moraine::cache_tally();
let writer = Catalog::open(Arc::clone(&object_store), CatalogOptions::default())
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
tx.create_table(schema, "t", &[col("a")])?;
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let reader = Catalog::open_read_only(object_store, CatalogOptions::default())
.await
.unwrap();
let view = reader.snapshot().await.unwrap();
assert!(
view.table_by_name(view.schema_by_name("main").expect("bootstrap").id, "t")
.is_some()
);
reader.close().await.unwrap();
let after = moraine::cache_tally();
let metadata_lookups = (after.metadata_hits + after.metadata_misses)
- (before.metadata_hits + before.metadata_misses);
assert!(
metadata_lookups > 0,
"reads did not reach the cache: {before:?} then {after:?}"
);
assert!(
after.metadata_hit_rate().is_some(),
"a cache that has served has a rate to report"
);
}
#[tokio::test]
async fn each_catalog_tallies_its_own_reads() {
let quiet = Catalog::open(
Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>,
CatalogOptions::default(),
)
.await
.unwrap();
let busy_store = Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>;
let writer = Catalog::open(Arc::clone(&busy_store), CatalogOptions::default())
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
tx.create_table(schema, "t", &[col("a")])?;
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let quiet_before = quiet.cache_tally();
let process_before = moraine::cache_tally();
let busy = Catalog::open_read_only(busy_store, CatalogOptions::default())
.await
.unwrap();
let view = busy.snapshot().await.unwrap();
assert!(
view.table_by_name(view.schema_by_name("main").expect("bootstrap").id, "t")
.is_some()
);
let busy_tally = busy.cache_tally();
let busy_lookups = busy_tally.metadata_hits + busy_tally.metadata_misses;
assert!(
busy_lookups > 0,
"the reading catalog's own tally must move: {busy_tally:?}"
);
assert_eq!(
quiet.cache_tally(),
quiet_before,
"an idle catalog must not be charged for another's reads"
);
let process_after = moraine::cache_tally();
assert!(
(process_after.metadata_hits + process_after.metadata_misses)
- (process_before.metadata_hits + process_before.metadata_misses)
>= busy_lookups,
"the process's counts still cover every attach's reads"
);
busy.close().await.unwrap();
quiet.close().await.unwrap();
}
#[tokio::test]
async fn a_scan_does_not_evict_what_probes_need() {
let object_store = Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>;
let writer = Catalog::open(Arc::clone(&object_store), CatalogOptions::default())
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
for n in 0..64 {
tx.create_table(schema, &format!("t{n}"), &[col("a")])?;
}
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let reader = Catalog::open_read_only(object_store, CatalogOptions::default())
.await
.unwrap();
let _ = reader.snapshot().await.unwrap();
let before_repeat = moraine::cache_tally();
let _ = reader.snapshot().await.unwrap();
let warm_misses = moraine::cache_tally().metadata_misses - before_repeat.metadata_misses;
let census = reader
.store_census({
let mut request = moraine::CensusRequest::default();
request.count_live_entries = true;
request
})
.await
.unwrap();
assert!(!census.subspaces.is_empty());
let before_after_scan = moraine::cache_tally();
let _ = reader.snapshot().await.unwrap();
let after_scan_misses =
moraine::cache_tally().metadata_misses - before_after_scan.metadata_misses;
reader.close().await.unwrap();
assert!(
after_scan_misses <= warm_misses,
"a scan cost the probe path its residency: {warm_misses} misses warm, \
{after_scan_misses} after a scan"
);
}
#[tokio::test]
async fn attached_catalogs_share_one_cache() {
let first_store = Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>;
let second_store = Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>;
for store in [&first_store, &second_store] {
let writer = Catalog::open(Arc::clone(store), CatalogOptions::default())
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
tx.create_table(schema, "t", &[col("a")])?;
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
}
let first = Catalog::open_read_only(first_store, CatalogOptions::default())
.await
.unwrap();
let _ = first.snapshot().await.unwrap();
let after_first = moraine::cache_tally();
let second = Catalog::open_read_only(second_store, CatalogOptions::default())
.await
.unwrap();
let _ = second.snapshot().await.unwrap();
let after_second = moraine::cache_tally();
first.close().await.unwrap();
second.close().await.unwrap();
let first_lookups = after_first.metadata_hits + after_first.metadata_misses;
let second_lookups = after_second.metadata_hits + after_second.metadata_misses;
assert!(
second_lookups > first_lookups,
"the second catalog's reads did not reach the shared cache: \
{first_lookups} then {second_lookups}"
);
}
#[tokio::test]
async fn a_preload_warms_before_the_first_read() {
let object_store = Arc::new(InMemory::new()) as Arc<dyn object_store::ObjectStore>;
let writer = Catalog::open(Arc::clone(&object_store), CatalogOptions::default())
.await
.unwrap();
writer
.commit(|tx| {
let schema = tx.schema_by_name("main").expect("bootstrap").id;
for n in 0..32 {
tx.create_table(schema, &format!("t{n}"), &[col("a")])?;
}
Ok(())
})
.await
.unwrap();
writer.close().await.unwrap();
let mut options = CatalogOptions::default();
options.cache_preload = Some(moraine::CachePreload::All);
let before = moraine::cache_tally();
let reader = Catalog::open_read_only(object_store, options)
.await
.unwrap();
let after_open = moraine::cache_tally();
let warm_lookups = (after_open.metadata_hits + after_open.metadata_misses)
- (before.metadata_hits + before.metadata_misses);
assert!(
warm_lookups > 0,
"a preload consulted the cache not at all: {before:?} then {after_open:?}"
);
let view = reader.snapshot().await.unwrap();
assert!(view.schema_by_name("main").is_some());
reader.close().await.unwrap();
}