use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use arrow_array::RecordBatch;
use futures::TryStreamExt;
use lance_core::Result;
use uuid::Uuid;
use super::data_source::{LsmDataSource, LsmGeneration};
use super::exec::{compute_pk_hash, resolve_pk_indices};
use super::flushed_cache::{FlushedMemTableCache, open_flushed_dataset};
use crate::dataset::Dataset;
use crate::dataset::mem_wal::write::BatchStore;
use crate::session::Session;
pub type SourceBlockLists = HashMap<(Option<Uuid>, LsmGeneration), Vec<Arc<HashSet<u64>>>>;
type ShardGenSets = HashMap<Uuid, Vec<(LsmGeneration, Arc<HashSet<u64>>)>>;
pub async fn compute_source_block_lists(
sources: &[LsmDataSource],
pk_columns: &[String],
session: Option<&Arc<Session>>,
flushed_cache: Option<&Arc<FlushedMemTableCache>>,
) -> Result<SourceBlockLists> {
let mut by_shard: ShardGenSets = HashMap::new();
let mut has_base = false;
for source in sources {
match source {
LsmDataSource::BaseTable { .. } => has_base = true,
LsmDataSource::ActiveMemTable {
batch_store,
shard_id,
generation,
..
} => {
let hashes = Arc::new(pk_hashes_from_batch_store(batch_store, pk_columns)?);
by_shard
.entry(*shard_id)
.or_default()
.push((*generation, hashes));
}
LsmDataSource::FlushedMemTable {
path,
shard_id,
generation,
..
} => {
let hashes = flushed_pk_hashes(path, pk_columns, session, flushed_cache).await?;
by_shard
.entry(*shard_id)
.or_default()
.push((*generation, hashes));
}
}
}
let mut blocked: SourceBlockLists = HashMap::new();
let mut base_blocked: Vec<Arc<HashSet<u64>>> = Vec::new();
for (shard, mut gens) in by_shard {
gens.sort_by_key(|(generation, _)| std::cmp::Reverse(*generation));
let mut newer: Vec<Arc<HashSet<u64>>> = Vec::new();
for (generation, hashes) in gens {
if !newer.is_empty() {
blocked.insert((Some(shard), generation), newer.clone());
}
if !hashes.is_empty() {
base_blocked.push(hashes.clone());
newer.push(hashes);
}
}
}
if has_base && !base_blocked.is_empty() {
blocked.insert((None, LsmGeneration::BASE_TABLE), base_blocked);
}
Ok(blocked)
}
pub async fn fresh_tier_block_list(
sources: &[LsmDataSource],
pk_columns: &[String],
session: Option<&Arc<Session>>,
flushed_cache: Option<&Arc<FlushedMemTableCache>>,
) -> Result<Vec<Arc<HashSet<u64>>>> {
let mut sets = Vec::new();
for source in sources {
let set = match source {
LsmDataSource::BaseTable { .. } => continue,
LsmDataSource::ActiveMemTable { batch_store, .. } => {
Arc::new(pk_hashes_from_batch_store(batch_store, pk_columns)?)
}
LsmDataSource::FlushedMemTable { path, .. } => {
flushed_pk_hashes(path, pk_columns, session, flushed_cache).await?
}
};
if !set.is_empty() {
sets.push(set);
}
}
Ok(sets)
}
pub fn pk_hashes_from_batch_store(
store: &BatchStore,
pk_columns: &[String],
) -> Result<HashSet<u64>> {
let mut batches: Vec<RecordBatch> = Vec::with_capacity(store.len());
for i in 0..store.len() {
if let Some(stored) = store.get(i) {
batches.push(stored.data.clone());
}
}
pk_hashes_from_batches(&batches, pk_columns)
}
fn pk_hashes_from_batches(batches: &[RecordBatch], pk_columns: &[String]) -> Result<HashSet<u64>> {
let mut pk_hashes = HashSet::new();
for batch in batches {
if batch.num_rows() == 0 {
continue;
}
let pk_indices = resolve_pk_indices(batch, pk_columns)
.map_err(|e| lance_core::Error::invalid_input(e.to_string()))?;
for row_idx in 0..batch.num_rows() {
pk_hashes.insert(compute_pk_hash(batch, &pk_indices, row_idx));
}
}
Ok(pk_hashes)
}
async fn flushed_pk_hashes(
path: &str,
pk_columns: &[String],
session: Option<&Arc<Session>>,
flushed_cache: Option<&Arc<FlushedMemTableCache>>,
) -> Result<Arc<HashSet<u64>>> {
match flushed_cache {
Some(cache) => {
let build_cache = cache.clone();
let build_path = path.to_string();
let build_session = session.cloned();
let build_pk = pk_columns.to_vec();
cache
.get_or_build_pk_hashes(
path,
Box::pin(async move {
let dataset = open_flushed_dataset(
&build_path,
build_session.as_ref(),
Some(&build_cache),
)
.await?;
scan_pk_hashes(&dataset, &build_pk).await
}),
)
.await
}
None => {
let dataset = open_flushed_dataset(path, session, None).await?;
Ok(Arc::new(scan_pk_hashes(&dataset, pk_columns).await?))
}
}
}
async fn scan_pk_hashes(dataset: &Dataset, pk_columns: &[String]) -> Result<HashSet<u64>> {
let pk_refs: Vec<&str> = pk_columns.iter().map(String::as_str).collect();
let mut scanner = dataset.scan();
scanner.project(&pk_refs)?;
let mut stream = scanner.try_into_stream().await?;
let mut hashes = HashSet::new();
while let Some(batch) = stream.try_next().await? {
if batch.num_rows() == 0 {
continue;
}
let pk_indices = resolve_pk_indices(&batch, pk_columns)
.map_err(|e| lance_core::Error::invalid_input(e.to_string()))?;
for row in 0..batch.num_rows() {
hashes.insert(compute_pk_hash(&batch, &pk_indices, row));
}
}
Ok(hashes)
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_array::Int32Array;
use arrow_schema::{DataType, Field, Schema};
use std::sync::Arc;
fn id_batch(ids: &[i32]) -> RecordBatch {
let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(ids.to_vec()))]).unwrap()
}
fn hash_id(id: i32) -> u64 {
let batch = id_batch(&[id]);
let pk_indices = resolve_pk_indices(&batch, &["id".to_string()]).unwrap();
compute_pk_hash(&batch, &pk_indices, 0)
}
fn blocks(sets: &[Arc<HashSet<u64>>], id: i32) -> bool {
sets.iter().any(|s| s.contains(&hash_id(id)))
}
#[test]
fn pk_hashes_collapse_within_gen_duplicates() {
let hashes = pk_hashes_from_batches(&[id_batch(&[1, 2, 1])], &["id".to_string()]).unwrap();
assert_eq!(hashes.len(), 2); }
#[test]
fn empty_batches_yield_empty_membership() {
let hashes = pk_hashes_from_batches(&[id_batch(&[])], &["id".to_string()]).unwrap();
assert!(hashes.is_empty());
}
#[test]
fn batch_store_membership_collapses_within_gen_dups() {
let store = BatchStore::with_capacity(8);
store.append(id_batch(&[1])).unwrap();
store.append(id_batch(&[1])).unwrap();
store.append(id_batch(&[2, 3])).unwrap();
let hashes = pk_hashes_from_batch_store(&store, &["id".to_string()]).unwrap();
assert_eq!(hashes.len(), 3); }
#[tokio::test]
async fn fresh_tier_block_list_one_set_per_in_memory_gen() {
use crate::dataset::mem_wal::scanner::data_source::{LsmDataSource, LsmGeneration};
use crate::dataset::mem_wal::write::IndexStore;
use uuid::Uuid;
let shard = Uuid::new_v4();
let mk = |ids: &[i32], generation: u64| {
let store = BatchStore::with_capacity(8);
store.append(id_batch(ids)).unwrap();
LsmDataSource::ActiveMemTable {
batch_store: Arc::new(store),
index_store: Arc::new(IndexStore::new()),
schema: id_batch(&[1]).schema(),
shard_id: shard,
generation: LsmGeneration::memtable(generation),
}
};
let sources = vec![mk(&[1, 2], 2), mk(&[3], 1)];
let sets = fresh_tier_block_list(&sources, &["id".to_string()], None, None)
.await
.unwrap();
assert_eq!(sets.len(), 2);
for id in [1, 2, 3] {
assert!(blocks(&sets, id));
}
assert!(!blocks(&sets, 4));
}
#[tokio::test]
async fn block_lists_suppress_stale_across_in_memory_gens() {
use crate::dataset::mem_wal::scanner::data_source::{LsmDataSource, LsmGeneration};
use crate::dataset::mem_wal::write::IndexStore;
use uuid::Uuid;
let shard = Uuid::new_v4();
let mk = |batches: &[&[i32]], generation: u64| {
let store = BatchStore::with_capacity(8);
for ids in batches {
store.append(id_batch(ids)).unwrap();
}
LsmDataSource::ActiveMemTable {
batch_store: Arc::new(store),
index_store: Arc::new(IndexStore::new()),
schema: id_batch(&[1]).schema(),
shard_id: shard,
generation: LsmGeneration::memtable(generation),
}
};
let sources = vec![mk(&[&[1]], 1), mk(&[&[1], &[2]], 2)];
let blocked = Box::pin(compute_source_block_lists(
&sources,
&["id".to_string()],
None,
None,
))
.await
.unwrap();
let g1 = LsmGeneration::memtable(1);
let g2 = LsmGeneration::memtable(2);
assert!(blocks(&blocked[&(Some(shard), g1)], 1));
assert!(!blocked.contains_key(&(Some(shard), g2)));
}
#[tokio::test]
async fn block_lists_suppress_stale_base_row() {
use crate::dataset::mem_wal::scanner::data_source::{LsmDataSource, LsmGeneration};
use crate::dataset::mem_wal::write::IndexStore;
use crate::dataset::{Dataset, WriteParams};
use arrow_array::RecordBatchIterator;
use uuid::Uuid;
let base_batch = id_batch(&[1, 3]);
let schema = base_batch.schema();
let tmp = tempfile::tempdir().unwrap();
let uri = format!("{}/base", tmp.path().to_str().unwrap());
let reader = RecordBatchIterator::new(vec![Ok(base_batch)], schema.clone());
let base = Arc::new(
Dataset::write(reader, &uri, Some(WriteParams::default()))
.await
.unwrap(),
);
let store = BatchStore::with_capacity(8);
store.append(id_batch(&[1])).unwrap();
store.append(id_batch(&[2])).unwrap();
let sources = vec![
LsmDataSource::BaseTable { dataset: base },
LsmDataSource::ActiveMemTable {
batch_store: Arc::new(store),
index_store: Arc::new(IndexStore::new()),
schema,
shard_id: Uuid::new_v4(),
generation: LsmGeneration::memtable(1),
},
];
let blocked = Box::pin(compute_source_block_lists(
&sources,
&["id".to_string()],
None,
None,
))
.await
.unwrap();
let base_blocked = blocked
.get(&(None, LsmGeneration::BASE_TABLE))
.expect("base has a blocked set");
assert!(blocks(base_blocked, 1));
assert!(!blocks(base_blocked, 3));
}
#[tokio::test]
async fn block_lists_are_keyed_per_shard() {
use crate::dataset::mem_wal::scanner::data_source::{LsmDataSource, LsmGeneration};
use crate::dataset::mem_wal::write::IndexStore;
use uuid::Uuid;
let mk = |shard: Uuid, ids: &[i32], generation: u64| {
let store = BatchStore::with_capacity(8);
store.append(id_batch(ids)).unwrap();
LsmDataSource::ActiveMemTable {
batch_store: Arc::new(store),
index_store: Arc::new(IndexStore::new()),
schema: id_batch(&[1]).schema(),
shard_id: shard,
generation: LsmGeneration::memtable(generation),
}
};
let a = Uuid::new_v4();
let b = Uuid::new_v4();
let sources = vec![
mk(a, &[1], 1),
mk(a, &[1], 2),
mk(b, &[2], 1),
mk(b, &[2], 2),
];
let blocked = Box::pin(compute_source_block_lists(
&sources,
&["id".to_string()],
None,
None,
))
.await
.unwrap();
let g1 = LsmGeneration::memtable(1);
let g2 = LsmGeneration::memtable(2);
assert!(blocks(&blocked[&(Some(a), g1)], 1));
assert!(!blocks(&blocked[&(Some(a), g1)], 2));
assert!(blocks(&blocked[&(Some(b), g1)], 2));
assert!(!blocks(&blocked[&(Some(b), g1)], 1));
assert!(!blocked.contains_key(&(Some(a), g2)));
assert!(!blocked.contains_key(&(Some(b), g2)));
}
}