use crate::Dataset;
use crate::index::frag_reuse_reader::SegmentPlanParts;
use crate::index::{DatasetIndexExt, DatasetIndexInternalExt};
use lance_core::Error;
use lance_core::cache::{CacheKey, CacheKeySchema, KeyBuilder};
use lance_core::deepsize::DeepSizeOf;
use lance_index::frag_reuse::{
CompactFragReuseIndex, CompactFragReuseIndexHandle, FRAG_REUSE_DETAILS_FILE_NAME,
FRAG_REUSE_INDEX_NAME, FragReuseGroup, FragReuseIndexDetails, FragReuseVersion,
};
use lance_index::scalar::{BatchRowIdRemapper, MetricsCollector, RowIdRemapper};
use lance_table::format::pb::fragment_reuse_index_details::{
self as pb_fri, Content, InlineContent,
};
use lance_table::format::pb::{ExternalFile, FragmentReuseIndexDetails};
use lance_table::format::{Fragment, IndexMetadata};
use lance_table::transaction::RewriteGroup;
use prost::Message;
use roaring::RoaringBitmap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
#[derive(Clone)]
pub(crate) enum ResolvedRemapping {
V0(Arc<dyn RowIdRemapper>),
V1Identity,
V1Translate {
remapper: Arc<dyn BatchRowIdRemapper>,
fingerprint: [u8; 32],
},
}
impl std::fmt::Debug for ResolvedRemapping {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::V0(_) => f.debug_tuple("V0").finish_non_exhaustive(),
Self::V1Identity => f.debug_tuple("V1Identity").finish(),
Self::V1Translate { remapper, .. } => {
f.debug_tuple("V1Translate").field(remapper).finish()
}
}
}
}
pub(crate) fn fri_cache_id(resolved: &Option<(Uuid, ResolvedRemapping)>) -> Option<&Uuid> {
match resolved {
Some((uuid, ResolvedRemapping::V0(_))) => Some(uuid),
_ => None,
}
}
pub(crate) fn scoped_index_cache(
dataset: &Dataset,
resolved: &Option<(Uuid, ResolvedRemapping)>,
) -> crate::session::index_caches::DSIndexCache {
crate::session::index_caches::DSIndexCache(match resolved {
Some((_, ResolvedRemapping::V1Translate { fingerprint, .. })) => dataset
.index_cache
.with_key_prefix(&translated_namespace(fingerprint)),
_ => dataset.index_cache.0.clone(),
})
}
fn translated_namespace(fingerprint: &[u8; 32]) -> String {
use std::fmt::Write;
let mut prefix = String::with_capacity("fri-xlat/".len() + 2 * fingerprint.len());
prefix.push_str("fri-xlat/");
for byte in fingerprint {
write!(prefix, "{byte:02x}").expect("writing to a String cannot fail");
}
prefix
}
#[derive(Clone, Debug)]
pub(crate) enum SegmentRemappingPlan {
Identity,
Translate {
coverage: RoaringBitmap,
excluded_fragments: RoaringBitmap,
fingerprint: [u8; 32],
},
MissingCoverage,
}
#[derive(Clone, Debug)]
pub(crate) struct FriQueryPlan {
pub(crate) segments: HashMap<Uuid, SegmentRemappingPlan>,
}
impl DeepSizeOf for FriQueryPlan {
fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
self.segments
.values()
.map(|segment| match segment {
SegmentRemappingPlan::Translate {
coverage,
excluded_fragments,
fingerprint,
} => {
coverage.serialized_size()
+ excluded_fragments.serialized_size()
+ fingerprint.len()
}
_ => 0,
})
.sum::<usize>()
+ self.segments.len() * std::mem::size_of::<(Uuid, SegmentRemappingPlan)>()
}
}
#[derive(Clone)]
pub(crate) struct FriQueryPlanKey<'a> {
pub(crate) fri_uuid: &'a Uuid,
}
impl CacheKey for FriQueryPlanKey<'_> {
type ValueType = FriQueryPlan;
fn key(&self) -> std::borrow::Cow<'_, str> {
self.fri_uuid.to_string().into()
}
fn type_name() -> &'static str {
"FriQueryPlan"
}
fn schema() -> CacheKeySchema {
CacheKeySchema::new("lance.index.fri-query-plan", 1)
}
fn write_key(&self, builder: &mut KeyBuilder) {
builder.write_fixed_bytes(self.fri_uuid.as_bytes());
}
}
async fn fri_query_plan(
dataset: &Dataset,
fri: &IndexMetadata,
stored: &[IndexMetadata],
mapping: &Arc<super::frag_reuse_reader::FragmentReuseIndex>,
) -> lance_core::Result<Arc<FriQueryPlan>> {
dataset
.index_cache
.with_key_prefix(dataset.manifest_location.path.as_ref())
.get_or_insert_with_key(
FriQueryPlanKey {
fri_uuid: &fri.uuid,
},
|| async {
let indices = dataset.load_indices().await?;
let stored_by_uuid: HashMap<Uuid, &IndexMetadata> =
stored.iter().map(|entry| (entry.uuid, entry)).collect();
let mut filtered_by_uuid: HashMap<Uuid, &IndexMetadata> =
HashMap::with_capacity(indices.len());
let mut groups: HashMap<&str, Vec<Uuid>> = HashMap::new();
for entry in indices.iter() {
filtered_by_uuid.insert(entry.uuid, entry);
if entry.name != FRAG_REUSE_INDEX_NAME {
groups
.entry(entry.name.as_str())
.or_default()
.push(entry.uuid);
}
}
let mut parts_by_uuid: HashMap<Uuid, SegmentPlanParts> = HashMap::new();
for members in groups.into_values() {
let provenance: Vec<RoaringBitmap> = members
.iter()
.map(|uuid| {
stored_by_uuid
.get(uuid)
.and_then(|source| source.fragment_bitmap.clone())
.unwrap_or_default()
})
.collect();
for (uuid, parts) in members.iter().zip(mapping.segment_plans(&provenance)) {
parts_by_uuid.insert(*uuid, parts);
}
}
let mut segments = HashMap::with_capacity(stored.len());
for source in stored.iter() {
let plan = if !mapping.may_need_translation(source.fragment_bitmap.as_ref()) {
SegmentRemappingPlan::Identity
} else if let Some(entry) = filtered_by_uuid.get(&source.uuid)
&& let Some(bitmap) = &entry.fragment_bitmap
{
let coverage = bitmap & dataset.fragment_bitmap.as_ref();
let parts = parts_by_uuid.remove(&source.uuid).unwrap_or_default();
let fingerprint = mapping.translation_fingerprint(
&coverage,
&parts.excluded,
&parts.path,
);
SegmentRemappingPlan::Translate {
coverage,
excluded_fragments: parts.excluded,
fingerprint,
}
} else {
SegmentRemappingPlan::MissingCoverage
};
segments.insert(source.uuid, plan);
}
Ok(FriQueryPlan { segments })
},
)
.await
}
pub(super) async fn open_row_id_remapping(
dataset: &Dataset,
index: &IndexMetadata,
metrics: &dyn MetricsCollector,
) -> lance_core::Result<Option<(Uuid, ResolvedRemapping)>> {
open_row_id_remapping_with_plan(dataset, index, None, metrics).await
}
pub(super) async fn open_row_id_remapping_with_plan(
dataset: &Dataset,
index: &IndexMetadata,
staged: Option<&SegmentRemappingPlan>,
metrics: &dyn MetricsCollector,
) -> lance_core::Result<Option<(Uuid, ResolvedRemapping)>> {
let stored = super::load_all_indices(dataset).await?;
let Some(fri) = stored
.iter()
.find(|entry| entry.name == FRAG_REUSE_INDEX_NAME)
else {
return Ok(None);
};
super::frag_reuse_with_stable_row_ids::ensure_frag_reuse_applies(
&dataset.manifest,
&stored,
index,
)?;
if fri.index_version == 0 {
return Ok(dataset.open_frag_reuse_index(metrics).await?.map(|legacy| {
(
legacy.uuid,
ResolvedRemapping::V0(Arc::new(CompactFragReuseIndexHandle(legacy))),
)
}));
}
if fri.index_version != 1 {
return Err(Error::not_supported(format!(
"FRI index_version {} is unsupported. Please upgrade to a newer version",
fri.index_version
)));
}
lance_index::scalar::check_batch_remapping_entry()?;
let mapping = super::frag_reuse_reader::FragmentReuseIndex::open(dataset, fri).await?;
let snapshot_plan;
let plan = match staged {
Some(plan) => plan,
None => {
snapshot_plan = fri_query_plan(dataset, fri, &stored, &mapping).await?;
match snapshot_plan.segments.get(&index.uuid) {
Some(plan) => plan,
None => {
return Err(Error::not_supported(format!(
"FRI remapping requires committed segment metadata for {}; a staged \
segment must be opened with its own plan",
index.uuid
)));
}
}
}
};
match plan {
SegmentRemappingPlan::Identity => Ok(Some((fri.uuid, ResolvedRemapping::V1Identity))),
SegmentRemappingPlan::MissingCoverage => Err(Error::not_supported(format!(
"FRI query coverage is unavailable for segment {}",
index.uuid
))),
SegmentRemappingPlan::Translate {
coverage,
excluded_fragments,
fingerprint,
} => Ok(Some((
fri.uuid,
ResolvedRemapping::V1Translate {
remapper: Arc::new(super::frag_reuse_remapping::QueryRowIdRemapper::new(
mapping,
coverage.clone(),
excluded_fragments.clone(),
)),
fingerprint: *fingerprint,
},
))),
}
}
pub async fn load_frag_reuse_index_details(
dataset: &Dataset,
index: &IndexMetadata,
) -> lance_core::Result<Arc<FragReuseIndexDetails>> {
if index.index_version != 0 {
return Err(Error::not_supported(format!(
"This operation requires interpreting FRI index_version {}; tagged FRI maintenance is not supported by this client. Upgrade to a client supporting this operation",
index.index_version
)));
}
let details_any = index.index_details.clone();
if details_any.is_none()
|| !details_any
.as_ref()
.unwrap()
.type_url
.ends_with("FragmentReuseIndexDetails")
{
return Err(Error::index(
"Index details is not for the fragment reuse index",
));
}
let proto = details_any.unwrap().to_msg::<FragmentReuseIndexDetails>()?;
match &proto.content {
None => Err(Error::index("Index details content is not found")),
Some(Content::Inline(content)) => {
Ok(Arc::new(FragReuseIndexDetails::try_from(content.clone())?))
}
Some(Content::External(external_file)) => {
let data = read_fri_external_file(dataset, index, external_file).await?;
let pb_sequence = InlineContent::decode(data)?;
Ok(Arc::new(FragReuseIndexDetails::try_from(pb_sequence)?))
}
}
}
async fn read_fri_external_file(
dataset: &Dataset,
index: &IndexMetadata,
file: &ExternalFile,
) -> lance_core::Result<bytes::Bytes> {
let end = file
.offset
.checked_add(file.size)
.and_then(|n| usize::try_from(n).ok())
.ok_or_else(|| Error::corrupt_file_named("FRI details", "external FRI range overflow"))?;
let path = dataset
.indice_files_dir(index)?
.join(index.uuid.to_string())
.join(file.path.as_str());
dataset
.object_store_for_index(index)
.await?
.open(&path)
.await?
.get_range(file.offset as usize..end)
.await
.map_err(Error::from)
}
pub(crate) async fn open_frag_reuse_index(
uuid: Uuid,
details: &FragReuseIndexDetails,
) -> lance_core::Result<CompactFragReuseIndex> {
CompactFragReuseIndex::try_new(uuid, details.clone())
}
pub(crate) async fn build_new_frag_reuse_index(
dataset: &mut Dataset,
frag_reuse_groups: Vec<FragReuseGroup>,
new_fragment_bitmap: RoaringBitmap,
) -> lance_core::Result<IndexMetadata> {
let new_version = FragReuseVersion {
dataset_version: dataset.manifest.version,
groups: frag_reuse_groups,
};
let index_meta = dataset.load_indices().await.map(|indices| {
indices
.iter()
.find(|idx| idx.name == FRAG_REUSE_INDEX_NAME)
.cloned()
})?;
let new_index_details = match &index_meta {
None => FragReuseIndexDetails {
versions: Vec::from([new_version]),
},
Some(index_meta) => {
let current_details = load_frag_reuse_index_details(dataset, index_meta).await?;
let mut versions = current_details.versions.clone();
versions.push(new_version);
FragReuseIndexDetails { versions }
}
};
build_frag_reuse_index_metadata(
dataset,
index_meta.as_ref(),
new_index_details,
new_fragment_bitmap,
)
.await
}
pub(crate) async fn build_frag_reuse_index_metadata(
dataset: &Dataset,
index_meta: Option<&IndexMetadata>,
new_index_details: FragReuseIndexDetails,
new_fragment_bitmap: RoaringBitmap,
) -> lance_core::Result<IndexMetadata> {
let index_id = uuid::Uuid::new_v4();
let new_index_details_proto = InlineContent::from(&new_index_details);
let proto = if new_index_details_proto.encoded_len() > 204800 {
let file_path = dataset
.indices_dir()
.join(index_id.to_string())
.join(FRAG_REUSE_DETAILS_FILE_NAME);
let mut writer = dataset.object_store.create(&file_path).await?;
writer
.write_all(new_index_details_proto.encode_to_vec().as_slice())
.await?;
writer.shutdown().await?;
let external_file = ExternalFile {
path: FRAG_REUSE_DETAILS_FILE_NAME.to_owned(),
offset: 0,
size: new_index_details_proto.encoded_len() as u64,
};
FragmentReuseIndexDetails {
content: Some(Content::External(external_file)),
}
} else {
FragmentReuseIndexDetails {
content: Some(Content::Inline(new_index_details_proto)),
}
};
Ok(IndexMetadata {
uuid: index_id,
name: FRAG_REUSE_INDEX_NAME.to_string(),
fields: vec![],
covering_fields: vec![],
dataset_version: dataset.manifest.version,
fragment_bitmap: Some(new_fragment_bitmap),
index_details: Some(Arc::new(prost_types::Any::from_msg(&proto)?)),
index_version: index_meta.map_or(0, |index_meta| index_meta.index_version),
created_at: Some(chrono::Utc::now()),
base_id: None,
files: None,
})
}
fn encode_length_delimited_field(tag: u32, bytes: &[u8]) -> Vec<u8> {
let mut output = Vec::with_capacity(bytes.len() + 8);
prost::encoding::encode_key(tag, prost::encoding::WireType::LengthDelimited, &mut output);
prost::encoding::encode_varint(bytes.len() as u64, &mut output);
output.extend_from_slice(bytes);
output
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) async fn decode_frag_reuse_ledger(
dataset: &Dataset,
entry: &IndexMetadata,
) -> lance_core::Result<lance_table::system_index::frag_reuse::ledger::FragReuseLedger> {
let details = entry
.index_details
.as_ref()
.ok_or_else(|| Error::index("Index details is not for the fragment reuse index"))?;
lance_table::system_index::frag_reuse::ledger::FragReuseLedger::decode(
entry.index_version,
details,
|file| async move { read_fri_external_file(dataset, entry, &file).await },
)
.await
}
pub(crate) async fn load_raw_frag_reuse_content(
dataset: &Dataset,
index: &IndexMetadata,
) -> lance_core::Result<Vec<u8>> {
use bytes::Buf;
use prost::encoding::{DecodeContext, WireType, decode_key, decode_varint, skip_field};
let corrupt = |message: &str| Error::corrupt_file_named("FRI details", message);
let details = index
.index_details
.as_ref()
.filter(|details| details.type_url.ends_with("FragmentReuseIndexDetails"))
.ok_or_else(|| Error::index("Index details is not for the fragment reuse index"))?;
let mut wire = bytes::Bytes::copy_from_slice(&details.value);
let mut content: Option<(u32, bytes::Bytes)> = None;
while wire.has_remaining() {
let (tag, wire_type) = decode_key(&mut wire).map_err(|e| corrupt(&e.to_string()))?;
if wire_type == WireType::LengthDelimited {
let length = decode_varint(&mut wire).map_err(|e| corrupt(&e.to_string()))?;
if length > wire.remaining() as u64 {
return Err(corrupt("FRI details field length exceeds payload"));
}
let payload = wire.split_to(length as usize);
if matches!(tag, 1 | 2) && content.replace((tag, payload)).is_some() {
return Err(corrupt("multiple FRI content fields"));
}
} else {
skip_field(wire_type, tag, &mut wire, DecodeContext::default())
.map_err(|e| corrupt(&e.to_string()))?;
}
}
match content {
None => Err(corrupt("missing FRI content")),
Some((1, inline)) => Ok(inline.to_vec()),
Some((_, external)) => {
let external_file =
ExternalFile::decode(external).map_err(|e| corrupt(&e.to_string()))?;
let expected = external_file.size;
let data = read_fri_external_file(dataset, index, &external_file).await?;
if data.len() as u64 != expected {
return Err(corrupt(&format!(
"external FRI size mismatch: expected {expected}, received {}",
data.len()
)));
}
Ok(data.to_vec())
}
}
}
async fn current_deleted_rows(dataset: &Dataset, frag: &Fragment) -> lance_core::Result<u64> {
match &frag.deletion_file {
None => Ok(0),
Some(deletion) => match deletion.num_deleted_rows {
Some(count) => Ok(count as u64),
None => Ok(
crate::io::deletion::read_dataset_deletion_file(dataset, frag.id, deletion)
.await?
.len() as u64,
),
},
}
}
async fn validate_folded_deletions(
dataset: &Dataset,
group: &RewriteGroup,
transition: &lance_table::format::pb::fragment_reuse_index_details::Transition,
manifest_by_id: &HashMap<u64, &Fragment>,
source_deltas: &[u64],
) -> lance_core::Result<()> {
use lance_core::utils::fragment_reuse::{MappingReader, OrderedCompactionMapping};
use lance_index::frag_reuse::stable_partition::{MAPPING_FILE, StablePartitionMapping};
use lance_index::scalar::lance_format::LanceIndexStore;
use lance_table::format::pb::fragment_reuse_index_details::transition::Mapping;
const FOLDING_BATCH_ROWS: usize = 16 * 1024;
let mapping: Arc<dyn MappingReader> = match &transition.mapping {
Some(Mapping::OrderedCompaction(ordered)) => {
let mut cursor = std::io::Cursor::new(&ordered.changed_row_addrs);
let bitmap = roaring::RoaringTreemap::deserialize_from(&mut cursor)
.map_err(|e| Error::invalid_input(e.to_string()))?;
let layout =
|digests: &[lance_table::format::pb::fragment_reuse_index_details::FragmentDigest]| {
digests
.iter()
.map(|digest| (digest.id as u32, digest.physical_rows as u32))
.collect()
};
let remap = lance_core::utils::row_addr_remap::RowAddrRemap::compact_with_layout([
lance_core::utils::row_addr_remap::GroupInputWithLayout {
rewritten_old_row_addrs: bitmap,
old_frags: layout(&transition.sources),
new_frags: layout(&transition.destinations),
},
])
.map_err(|e| Error::invalid_input(e.to_string()))?;
Arc::new(OrderedCompactionMapping::new(Arc::new(remap)))
}
Some(Mapping::StablePartition(reference)) => {
let base = match reference.base_id {
None => dataset.base.clone(),
Some(id) => dataset
.manifest
.base_paths
.get(&id)
.ok_or_else(|| {
Error::invalid_input(format!(
"mapping {} references missing base {id}",
reference.map_id
))
})?
.extract_path(dataset.session.store_registry())?,
};
let directory = base.join("_fri").join(reference.map_id.as_str());
let store = dataset.object_store(reference.base_id).await?;
let metadata_cache = dataset.metadata_cache.file_metadata_cache(&directory);
let store =
LanceIndexStore::new(store, directory, Arc::new(metadata_cache)).with_file_sizes(
HashMap::from([(MAPPING_FILE.to_string(), reference.map_size_bytes)]),
);
Arc::new(StablePartitionMapping::try_new(
Arc::new(store),
transition.sources.clone(),
transition.destinations.clone(),
)?)
}
None => {
return Err(Error::invalid_input(
"a transition carrying folded deletions has no mapping",
));
}
};
let destination_slot: HashMap<u64, usize> = transition
.destinations
.iter()
.enumerate()
.map(|(slot, digest)| (digest.id, slot))
.collect();
let mut expected: Vec<RoaringBitmap> =
vec![RoaringBitmap::new(); transition.destinations.len()];
let mut expected_total = 0u64;
for digest in transition.sources.iter() {
let Some(frag) = manifest_by_id.get(&digest.id) else {
return Err(Error::invalid_input(format!(
"source fragment {} is no longer in the current dataset",
digest.id
)));
};
let mut offsets: Vec<u32> = match &frag.deletion_file {
None => Vec::new(),
Some(deletion) => {
crate::io::deletion::read_dataset_deletion_file(dataset, frag.id, deletion)
.await?
.iter()
.collect()
}
};
offsets.sort_unstable();
let mut rewrite_time_deleted = 0u64;
for chunk in offsets.chunks(FOLDING_BATCH_ROWS) {
let mut addresses = Vec::with_capacity(chunk.len());
for &offset in chunk {
if u64::from(offset) >= digest.physical_rows {
return Err(Error::invalid_input(format!(
"source fragment {} has a deleted row offset {offset} beyond its \
{} physical rows",
frag.id, digest.physical_rows
)));
}
addresses.push((digest.id << 32) | u64::from(offset));
}
for translation in mapping.remap_row_ids(&addresses).await? {
match translation {
None => rewrite_time_deleted += 1,
Some(address) => {
let slot = destination_slot.get(&(address >> 32)).ok_or_else(|| {
Error::invalid_input(format!(
"the mapping translates to fragment {} which is not a \
destination of this transition",
address >> 32
))
})?;
expected[*slot].insert(address as u32);
expected_total += 1;
}
}
}
}
if rewrite_time_deleted != digest.num_deleted_rows {
return Err(Error::invalid_input(format!(
"scan-time deletion accounting failed for source fragment {}: \
{rewrite_time_deleted} deleted rows predate the rewrite mapping but the \
digest records {} rewrite-time deletions",
digest.id, digest.num_deleted_rows
)));
}
}
let mut supplied: Vec<RoaringBitmap> = Vec::with_capacity(group.new_fragments.len());
let mut supplied_total = 0u64;
for frag in group.new_fragments.iter() {
let bitmap: RoaringBitmap = match &frag.deletion_file {
None => RoaringBitmap::new(),
Some(deletion) => {
let bitmap: RoaringBitmap =
crate::io::deletion::read_dataset_deletion_file(dataset, frag.id, deletion)
.await?
.iter()
.collect();
if let Some(num_deleted_rows) = deletion.num_deleted_rows
&& num_deleted_rows as u64 != bitmap.len()
{
return Err(Error::invalid_input(format!(
"destination fragment {} records {num_deleted_rows} deleted rows \
in its deletion file metadata but the deletion file holds {} \
positions",
frag.id,
bitmap.len()
)));
}
bitmap
}
};
supplied_total += bitmap.len();
supplied.push(bitmap);
}
let expected_delta: u64 = source_deltas.iter().sum();
if supplied_total != expected_delta {
return Err(Error::invalid_input(format!(
"destination deletion vectors hold {supplied_total} rows but the sources \
gained {expected_delta} deletions since the scan"
)));
}
debug_assert_eq!(expected_total, expected_delta);
for (slot, digest) in transition.destinations.iter().enumerate() {
if expected[slot] != supplied[slot] {
let missing = (&expected[slot] - &supplied[slot]).iter().next();
let extra = (&supplied[slot] - &expected[slot]).iter().next();
return Err(Error::invalid_input(format!(
"folded deletions do not land on the translated positions for destination \
fragment {}: missing translated offset {missing:?}, unexpected deletion \
offset {extra:?}",
digest.id
)));
}
}
Ok(())
}
pub async fn frag_reuse_entry_appending(
dataset: &Dataset,
transitions: Vec<pb_fri::Transition>,
) -> lance_core::Result<IndexMetadata> {
if dataset.manifest.uses_stable_row_ids() {
return Err(Error::not_supported(
"Tagged fragment reuse histories are address-based and excluded on \
stable-row-id datasets; this rewrite cannot carry transition intent here",
));
}
let stored = super::load_all_indices(dataset).await?;
let existing = stored.iter().find(|idx| idx.name == FRAG_REUSE_INDEX_NAME);
let (mut content, mut fragment_bitmap) = match existing {
None => (Vec::new(), RoaringBitmap::new()),
Some(entry) => {
if !matches!(entry.index_version, 0 | 1) {
return Err(Error::not_supported(format!(
"Cannot append a transition to FRI index_version {}; upgrade to a newer \
version of Lance",
entry.index_version
)));
}
(
load_raw_frag_reuse_content(dataset, entry).await?,
entry.fragment_bitmap.clone().unwrap_or_default(),
)
}
};
for transition in &transitions {
content.extend_from_slice(&encode_length_delimited_field(
2,
&transition.encode_to_vec(),
));
}
let details = prost_types::Any {
type_url: "/lance.table.FragmentReuseIndexDetails".into(),
value: encode_length_delimited_field(1, &content),
};
let ledger = lance_table::system_index::frag_reuse::ledger::FragReuseLedger::decode(
1,
&details,
|_| async {
Err(Error::invalid_input(
"the assembled FRI content is inline; no external read is possible",
))
},
)
.await?;
if ledger.has_unsupported_transitions() {
return Err(Error::not_supported(
"the fragment reuse history contains mappings this writer cannot maintain; \
upgrade to a newer version of Lance before rewriting this table",
));
}
for transition in &transitions {
for digest in transition
.sources
.iter()
.chain(transition.destinations.iter())
{
fragment_bitmap.insert(digest.id as u32);
}
}
Ok(IndexMetadata {
uuid: Uuid::new_v4(),
name: FRAG_REUSE_INDEX_NAME.to_string(),
fields: vec![],
covering_fields: vec![],
dataset_version: dataset.manifest.version,
fragment_bitmap: Some(fragment_bitmap),
index_details: Some(Arc::new(details)),
index_version: 1,
created_at: Some(chrono::Utc::now()),
base_id: None,
files: None,
})
}
pub(crate) async fn load_frag_reuse_records(
dataset: &Dataset,
entry: &IndexMetadata,
) -> lance_core::Result<InlineContent> {
let content = load_raw_frag_reuse_content(dataset, entry).await?;
decode_records(&content)
}
fn decode_records(content: &[u8]) -> lance_core::Result<InlineContent> {
InlineContent::decode(content).map_err(|err| {
Error::corrupt_file_named(
"FRI details",
format!("the fragment reuse content does not decode: {err}"),
)
})
}
fn transition_identity(transition: &pb_fri::Transition) -> (Vec<u64>, Vec<u64>) {
(
transition.sources.iter().map(|digest| digest.id).collect(),
transition
.destinations
.iter()
.map(|digest| digest.id)
.collect(),
)
}
#[derive(Debug, Default, Clone, PartialEq)]
pub(crate) struct AddedRecords {
pub transitions: Vec<pb_fri::Transition>,
pub legacy_versions: Vec<pb_fri::Version>,
}
pub(crate) fn records_added_since(
base: &InlineContent,
proposed: &InlineContent,
) -> lance_core::Result<AddedRecords> {
let mut added = AddedRecords::default();
let proposed_transitions: HashMap<(Vec<u64>, Vec<u64>), &pb_fri::Transition> = proposed
.transitions
.iter()
.map(|transition| (transition_identity(transition), transition))
.collect();
if proposed_transitions.len() != proposed.transitions.len() {
return Err(Error::invalid_input(
"the fragment reuse entry records the same transition more than once",
));
}
let mut base_transitions: HashMap<(Vec<u64>, Vec<u64>), &pb_fri::Transition> = HashMap::new();
for transition in &base.transitions {
let identity = transition_identity(transition);
match proposed_transitions.get(&identity) {
None => {
return Err(Error::invalid_input(format!(
"the fragment reuse entry drops the recorded transition from fragments \
{:?}; a rewrite only appends history, retire transitions through the \
fragment reuse index cleanup instead",
identity.0
)));
}
Some(proposed) if *proposed != transition => {
return Err(Error::invalid_input(format!(
"the fragment reuse entry changes the recorded transition from fragments \
{:?}; a recorded transition cannot be modified",
identity.0
)));
}
Some(_) => {}
}
base_transitions.insert(identity, transition);
}
for transition in &proposed.transitions {
if !base_transitions.contains_key(&transition_identity(transition)) {
added.transitions.push(transition.clone());
}
}
let proposed_versions: HashMap<u64, &pb_fri::Version> = proposed
.legacy_versions
.iter()
.map(|version| (version.dataset_version, version))
.collect();
if proposed_versions.len() != proposed.legacy_versions.len() {
return Err(Error::invalid_input(
"the fragment reuse entry records the same compaction version more than once",
));
}
let mut base_versions: HashSet<u64> = HashSet::new();
for version in &base.legacy_versions {
match proposed_versions.get(&version.dataset_version) {
None => {
return Err(Error::invalid_input(format!(
"the fragment reuse entry drops the recorded compaction version {}; a \
rewrite only appends history, retire versions through the fragment \
reuse index cleanup instead",
version.dataset_version
)));
}
Some(proposed) if *proposed != version => {
return Err(Error::invalid_input(format!(
"the fragment reuse entry changes the recorded compaction version {}; a \
recorded version cannot be modified",
version.dataset_version
)));
}
Some(_) => {}
}
base_versions.insert(version.dataset_version);
}
for version in &proposed.legacy_versions {
if !base_versions.contains(&version.dataset_version) {
added.legacy_versions.push(version.clone());
}
}
Ok(added)
}
pub(crate) fn legacy_version_transitions(version: &pb_fri::Version) -> Vec<pb_fri::Transition> {
version
.groups
.iter()
.map(|group| pb_fri::Transition {
sources: group.old_fragments.clone(),
destinations: group.new_fragments.clone(),
mapping: Some(pb_fri::transition::Mapping::OrderedCompaction(
pb_fri::OrderedCompaction {
changed_row_addrs: group.changed_row_addrs.clone(),
},
)),
})
.collect()
}
pub(crate) async fn build_frag_reuse_rewrite_entry(
dataset: &Dataset,
transitions: &[pb_fri::Transition],
groups: &[RewriteGroup],
) -> lance_core::Result<(IndexMetadata, Option<u64>)> {
if dataset.manifest.uses_stable_row_ids() {
return Err(Error::not_supported(
"Tagged fragment reuse histories are address-based and excluded on \
stable-row-id datasets; this rewrite cannot carry transition intent here",
));
}
if transitions.is_empty() {
return Err(Error::invalid_input(
"a fragment-reuse rewrite carries no transitions",
));
}
let source_ids: HashSet<u64> = transitions
.iter()
.flat_map(|transition| transition.sources.iter().map(|source| source.id))
.collect();
let mut covered_groups = Vec::with_capacity(transitions.len());
for group in groups {
let covered = group
.old_fragments
.iter()
.filter(|frag| source_ids.contains(&frag.id))
.count();
if covered == 0 {
continue;
}
if covered != group.old_fragments.len() {
return Err(Error::invalid_input(
"a rewrite group mixes transition-covered and order-preserving source fragments",
));
}
covered_groups.push(group);
}
if covered_groups.len() != transitions.len() {
return Err(Error::invalid_input(format!(
"the fragment-reuse rewrite lists {} transitions but {} rewrite groups are \
covered by their sources",
transitions.len(),
covered_groups.len()
)));
}
let live_fragments: HashSet<u64> = dataset.fragments().iter().map(|frag| frag.id).collect();
let manifest_by_id: HashMap<u64, &Fragment> = dataset
.fragments()
.iter()
.map(|frag| (frag.id, frag))
.collect();
for (group, transition) in covered_groups.iter().zip(transitions.iter()) {
if group.old_fragments.len() != transition.sources.len() {
return Err(Error::invalid_input(format!(
"a transition lists {} sources but its rewrite group holds {} old fragments",
transition.sources.len(),
group.old_fragments.len()
)));
}
let mut source_deltas: Vec<u64> = Vec::with_capacity(transition.sources.len());
for digest in transition.sources.iter() {
let current = match manifest_by_id.get(&digest.id) {
None => digest.num_deleted_rows,
Some(frag) => current_deleted_rows(dataset, frag).await?,
};
if current < digest.num_deleted_rows {
return Err(Error::invalid_input(format!(
"source fragment {} records {current} deleted rows in the manifest, below \
the {} its scan-time digest carries; a deletion vector cannot shrink",
digest.id, digest.num_deleted_rows
)));
}
source_deltas.push(current - digest.num_deleted_rows);
}
let folding = source_deltas.iter().any(|&delta| delta > 0);
for (source_index, (frag, digest)) in group
.old_fragments
.iter()
.zip(transition.sources.iter())
.enumerate()
{
if !frag.overlays.is_empty() {
return Err(Error::not_supported(format!(
"source fragment {} carries data overlay files; a rewrite recording \
fragment reuse transitions would materialize the overlaid values while \
indices keep translated coverage over the old addresses. Compact the \
overlays away or rebuild the covering indices eagerly before this \
rewrite",
frag.id
)));
}
let physical_rows = frag.physical_rows.ok_or_else(|| {
Error::invalid_input(format!(
"source fragment {} has no physical row count",
frag.id
))
})? as u64;
let num_deleted_rows = if folding {
current_deleted_rows(dataset, frag).await?
} else {
frag.deletion_file
.as_ref()
.and_then(|deletion| deletion.num_deleted_rows)
.unwrap_or(0) as u64
};
if !folding {
if digest.id != frag.id
|| digest.physical_rows != physical_rows
|| digest.num_deleted_rows != num_deleted_rows
{
return Err(Error::invalid_input(format!(
"transition source digest {:?} does not match old fragment {} \
({physical_rows} physical rows, {num_deleted_rows} deleted)",
digest, frag.id
)));
}
} else {
if digest.id != frag.id || digest.physical_rows != physical_rows {
return Err(Error::invalid_input(format!(
"transition source digest {:?} does not match old fragment {} \
({physical_rows} physical rows)",
digest, frag.id
)));
}
let current = digest.num_deleted_rows + source_deltas[source_index];
if num_deleted_rows != current {
return Err(Error::invalid_input(format!(
"deletion folding requires the rewrite group to carry the source's \
current metadata: fragment {} shows {num_deleted_rows} deleted rows \
but the manifest records {current}",
frag.id
)));
}
}
}
if group.new_fragments.len() != transition.destinations.len() {
return Err(Error::invalid_input(format!(
"a transition lists {} destinations but its rewrite group holds {} new fragments",
transition.destinations.len(),
group.new_fragments.len()
)));
}
for (frag, digest) in group
.new_fragments
.iter()
.zip(transition.destinations.iter())
{
if frag.id == 0 {
return Err(Error::invalid_input(
"destination fragment id 0 is unassigned (the commit path renumbers it); \
reserve ids with ReserveFragments and assign them before the rewrite is \
assembled",
));
}
if live_fragments.contains(&frag.id) {
return Err(Error::invalid_input(format!(
"destination fragment id {} is already live in the dataset; a rewrite \
destination must use a freshly reserved id",
frag.id
)));
}
if !folding && frag.deletion_file.is_some() {
return Err(Error::invalid_input(format!(
"destination fragment {} carries a deletion file; rewrite destinations must be written without deletions",
frag.id
)));
}
let physical_rows = frag.physical_rows.ok_or_else(|| {
Error::invalid_input(format!(
"destination fragment {} has no physical row count",
frag.id
))
})? as u64;
if digest.id != frag.id
|| digest.physical_rows != physical_rows
|| digest.num_deleted_rows != 0
{
return Err(Error::invalid_input(format!(
"transition destination digest {:?} does not match new fragment {} \
({physical_rows} physical rows)",
digest, frag.id
)));
}
}
let live_source_rows: u64 = transition
.sources
.iter()
.map(|digest| digest.physical_rows.saturating_sub(digest.num_deleted_rows))
.sum();
let destination_rows: u64 = transition
.destinations
.iter()
.map(|digest| digest.physical_rows)
.sum();
if live_source_rows != destination_rows {
return Err(Error::invalid_input(format!(
"a transition does not conserve rows: {live_source_rows} live source rows, \
{destination_rows} destination rows"
)));
}
if folding {
validate_folded_deletions(dataset, group, transition, &manifest_by_id, &source_deltas)
.await?;
}
}
let stored = super::load_all_indices(dataset).await?;
let existing = stored.iter().find(|idx| idx.name == FRAG_REUSE_INDEX_NAME);
let (mut content, base_bitmap, base_entry_version) = match existing {
None => (Vec::new(), RoaringBitmap::new(), None),
Some(entry) => {
if !matches!(entry.index_version, 0 | 1) {
return Err(Error::not_supported(format!(
"Cannot append a stable-partition transition to FRI index_version {}; \
upgrade to a newer version of Lance",
entry.index_version
)));
}
(
load_raw_frag_reuse_content(dataset, entry).await?,
entry.fragment_bitmap.clone().unwrap_or_default(),
Some(entry.dataset_version),
)
}
};
let recorded = decode_records(&content)?;
for transition in transitions {
if recorded
.transitions
.iter()
.any(|existing| transition_identity(existing) == transition_identity(transition))
{
return Err(Error::invalid_input(format!(
"the fragment reuse history already records a transition from fragments {:?}; \
the rewrite's entry cannot append it again",
transition_identity(transition).0
)));
}
}
for transition in transitions {
content.extend_from_slice(&encode_length_delimited_field(
2,
&transition.encode_to_vec(),
));
}
let assembled = prost_types::Any {
type_url: "/lance.table.FragmentReuseIndexDetails".into(),
value: encode_length_delimited_field(1, &content),
};
let ledger = lance_table::system_index::frag_reuse::ledger::FragReuseLedger::decode(
1,
&assembled,
|_| async {
Err(Error::invalid_input(
"the assembled FRI content is inline; no external read is possible",
))
},
)
.await?;
if ledger.has_unsupported_transitions() {
return Err(Error::not_supported(
"the fragment reuse history contains mappings this writer cannot maintain; upgrade to a newer version of Lance before rewriting this table",
));
}
let mut seen_map_ids = HashSet::new();
for transition in ledger.transitions() {
if let lance_table::system_index::frag_reuse::ledger::Mapping::StablePartition(partition) =
transition.mapping()
&& !seen_map_ids.insert(partition.map_id.clone())
{
return Err(Error::invalid_input(format!(
"stable-partition row-map id {} is referenced by more than one transition; each transition must own its row map",
partition.map_id
)));
}
}
let mut fragment_bitmap = base_bitmap;
for transition in transitions {
for digest in transition
.sources
.iter()
.chain(transition.destinations.iter())
{
fragment_bitmap.insert(digest.id as u32);
}
}
let index_id = Uuid::new_v4();
let details_value = if content.len() > 204800 {
let file_path = dataset
.indices_dir()
.join(index_id.to_string())
.join(FRAG_REUSE_DETAILS_FILE_NAME);
let mut writer = dataset.object_store.create(&file_path).await?;
writer.write_all(&content).await?;
writer.shutdown().await?;
let external_file = ExternalFile {
path: FRAG_REUSE_DETAILS_FILE_NAME.to_owned(),
offset: 0,
size: content.len() as u64,
};
encode_length_delimited_field(2, &external_file.encode_to_vec())
} else {
assembled.value
};
let entry = IndexMetadata {
uuid: index_id,
name: FRAG_REUSE_INDEX_NAME.to_string(),
fields: vec![],
covering_fields: vec![],
dataset_version: dataset.manifest.version,
fragment_bitmap: Some(fragment_bitmap),
index_details: Some(Arc::new(prost_types::Any {
type_url: "/lance.table.FragmentReuseIndexDetails".into(),
value: details_value,
})),
index_version: 1,
created_at: Some(chrono::Utc::now()),
base_id: None,
files: None,
};
Ok((entry, base_entry_version))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dataset::{InsertBuilder, WriteMode, WriteParams};
use crate::index::DatasetIndexExt;
use crate::index::frag_reuse_reader::tests as reader_tests;
use crate::utils::test::DatagenExt;
use arrow_array::cast::AsArray;
use arrow_array::types::Int32Type;
use arrow_array::{Int32Array, RecordBatch};
use lance_table::feature_flags::FLAG_FRAGMENT_REUSE_INDEX;
use lance_table::format::Fragment;
use lance_table::format::pb::fragment_reuse_index_details::{
FragmentDigest, StablePartition, Transition, transition,
};
use lance_table::system_index::frag_reuse::FragDigest;
use lance_table::system_index::frag_reuse::ledger::{FragReuseLedger, Mapping};
use lance_table::system_index::frag_reuse::metadata::is_tagged;
use lance_table::transaction::{Operation, Transaction};
use roaring::RoaringTreemap;
async fn entry_appending(dataset: &Dataset, transitions: Vec<Transition>) -> IndexMetadata {
frag_reuse_entry_appending(dataset, transitions)
.await
.unwrap()
}
async fn hand_built_entry(dataset: &Dataset, transitions: Vec<Transition>) -> IndexMetadata {
let stored = crate::index::load_all_indices(dataset).await.unwrap();
let existing = stored.iter().find(|idx| idx.name == FRAG_REUSE_INDEX_NAME);
let (mut content, mut bitmap) = match existing {
None => (Vec::new(), RoaringBitmap::new()),
Some(entry) => (
load_raw_frag_reuse_content(dataset, entry).await.unwrap(),
entry.fragment_bitmap.clone().unwrap_or_default(),
),
};
for transition in &transitions {
content.extend_from_slice(&encode_length_delimited_field(
2,
&transition.encode_to_vec(),
));
for digest in transition
.sources
.iter()
.chain(transition.destinations.iter())
{
bitmap.insert(digest.id as u32);
}
}
IndexMetadata {
uuid: Uuid::new_v4(),
name: FRAG_REUSE_INDEX_NAME.to_string(),
fields: vec![],
covering_fields: vec![],
dataset_version: dataset.manifest.version,
fragment_bitmap: Some(bitmap),
index_details: Some(Arc::new(prost_types::Any {
type_url: "/lance.table.FragmentReuseIndexDetails".into(),
value: encode_length_delimited_field(1, &content),
})),
index_version: 1,
created_at: None,
base_id: None,
files: None,
}
}
async fn sorted_values(dataset: &Dataset) -> Vec<i32> {
let batch = dataset.scan().try_into_batch().await.unwrap();
let mut values: Vec<i32> = batch["i"]
.as_primitive::<Int32Type>()
.iter()
.map(|value| value.unwrap())
.collect();
values.sort_unstable();
values
}
async fn reserve_fragments(dataset: &mut Dataset, num_fragments: u32) {
dataset
.apply_commit(
Transaction::new(
dataset.manifest.version,
Operation::ReserveFragments { num_fragments },
None,
),
&Default::default(),
&Default::default(),
)
.await
.unwrap();
}
fn stored_fri(indices: &[IndexMetadata]) -> IndexMetadata {
indices
.iter()
.find(|idx| idx.name == FRAG_REUSE_INDEX_NAME)
.cloned()
.unwrap()
}
async fn decode_entry(dataset: &Dataset, entry: &IndexMetadata) -> FragReuseLedger {
decode_frag_reuse_ledger(dataset, entry).await.unwrap()
}
#[test]
fn records_added_since_is_add_only() {
let digest = |id: u64| FragmentDigest {
id,
physical_rows: 4,
num_deleted_rows: 0,
};
let transition = |sources: &[u64], destinations: &[u64], map_id: &str| Transition {
sources: sources.iter().copied().map(digest).collect(),
destinations: destinations.iter().copied().map(digest).collect(),
mapping: Some(transition::Mapping::StablePartition(StablePartition {
map_id: map_id.to_string(),
map_size_bytes: 1,
base_id: None,
})),
};
let version = |dataset_version: u64| pb_fri::Version {
dataset_version,
groups: vec![pb_fri::Group {
changed_row_addrs: vec![],
old_fragments: vec![digest(100)],
new_fragments: vec![digest(110)],
}],
};
let t1 = transition(&[0, 1], &[10, 11], "a");
let t2 = transition(&[2, 3], &[12, 13], "b");
let base = InlineContent {
legacy_versions: vec![version(3)],
transitions: vec![t1.clone()],
};
let added = records_added_since(
&base,
&InlineContent {
legacy_versions: vec![version(3), version(5)],
transitions: vec![t1.clone(), t2.clone()],
},
)
.unwrap();
assert_eq!(added.transitions, vec![t2.clone()]);
assert_eq!(added.legacy_versions, vec![version(5)]);
assert_eq!(
records_added_since(&base, &base).unwrap(),
AddedRecords::default()
);
let error = records_added_since(
&base,
&InlineContent {
legacy_versions: vec![version(3)],
transitions: vec![t2],
},
)
.unwrap_err();
assert!(
error.to_string().contains("drops the recorded transition"),
"{error}"
);
let error = records_added_since(
&base,
&InlineContent {
legacy_versions: vec![],
transitions: vec![t1.clone()],
},
)
.unwrap_err();
assert!(
error
.to_string()
.contains("drops the recorded compaction version"),
"{error}"
);
let mut t1_changed = t1.clone();
t1_changed.mapping = Some(transition::Mapping::StablePartition(StablePartition {
map_id: "a".to_string(),
map_size_bytes: 2,
base_id: None,
}));
let error = records_added_since(
&base,
&InlineContent {
legacy_versions: vec![version(3)],
transitions: vec![t1_changed],
},
)
.unwrap_err();
assert!(
error
.to_string()
.contains("changes the recorded transition"),
"{error}"
);
let mut version_changed = version(3);
version_changed.groups.clear();
let error = records_added_since(
&base,
&InlineContent {
legacy_versions: vec![version_changed],
transitions: vec![t1.clone()],
},
)
.unwrap_err();
assert!(
error
.to_string()
.contains("changes the recorded compaction version"),
"{error}"
);
let error = records_added_since(
&base,
&InlineContent {
legacy_versions: vec![version(3)],
transitions: vec![t1.clone(), t1],
},
)
.unwrap_err();
assert!(error.to_string().contains("more than once"), "{error}");
}
#[tokio::test]
async fn rewrite_entry_dropping_a_recorded_transition_is_refused() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 40).await;
let untagged = dataset.clone();
let dataset = tag_fragment_zero(dataset).await;
let entry = stored_fri(&crate::index::load_all_indices(&dataset).await.unwrap());
assert_eq!(decode_entry(&dataset, &entry).await.transitions().len(), 1);
let old_fragments: Vec<Fragment> = dataset
.fragments()
.iter()
.filter(|f| f.id == 1)
.cloned()
.collect();
let (transition, destinations) = reader_tests::prepare_partition(&dataset, &[1], 20).await;
let stale_entry = entry_appending(&untagged, vec![transition]).await;
assert_eq!(
load_frag_reuse_records(&dataset, &stale_entry)
.await
.unwrap()
.transitions
.len(),
1
);
let read_version = dataset.manifest.version;
let stale_entry = IndexMetadata {
dataset_version: read_version,
..stale_entry
};
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index: Some(stale_entry),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("drops the recorded transition"),
"{error}"
);
let after = crate::index::load_all_indices(&dataset).await.unwrap();
assert_eq!(stored_fri(&after).uuid, entry.uuid);
assert_eq!(dataset.manifest.version, read_version);
}
#[tokio::test]
async fn rewrite_entry_changing_a_recorded_transition_is_refused() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 40).await;
let dataset = tag_fragment_zero(dataset).await;
let entry = stored_fri(&crate::index::load_all_indices(&dataset).await.unwrap());
let mut records = load_frag_reuse_records(&dataset, &entry).await.unwrap();
let Some(transition::Mapping::StablePartition(mapping)) =
records.transitions[0].mapping.as_mut()
else {
unreachable!()
};
mapping.map_size_bytes += 1;
let read_version = dataset.manifest.version;
let modified = IndexMetadata {
uuid: Uuid::new_v4(),
dataset_version: read_version,
index_details: Some(Arc::new(prost_types::Any {
type_url: "/lance.table.FragmentReuseIndexDetails".into(),
value: encode_length_delimited_field(1, &records.encode_to_vec()),
})),
..entry.clone()
};
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![],
rewritten_indices: vec![],
frag_reuse_index: Some(modified),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error
.to_string()
.contains("changes the recorded transition"),
"{error}"
);
let after = crate::index::load_all_indices(&dataset).await.unwrap();
assert_eq!(stored_fri(&after).uuid, entry.uuid);
}
#[tokio::test]
async fn stable_partition_rewrite_commits_atomically() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 20).await;
let before = sorted_values(&dataset).await;
assert_eq!(before, (0..8).collect::<Vec<_>>());
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let read_version = dataset.manifest.version;
let intent = entry_appending(&dataset, vec![transition.clone()]).await;
let intent_uuid = intent.uuid;
let frag_reuse_index = Some(intent);
let committed = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments: old_fragments.clone(),
new_fragments: destinations.clone(),
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
let mut dataset = committed;
let flag = FLAG_FRAGMENT_REUSE_INDEX;
assert_eq!(dataset.manifest.reader_feature_flags & flag, flag);
assert_eq!(dataset.manifest.writer_feature_flags & flag, flag);
let live_ids: Vec<u64> = dataset.fragments().iter().map(|frag| frag.id).collect();
assert_eq!(live_ids, vec![10, 11]);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert!(is_tagged(&entry));
assert_eq!(entry.index_version, 1);
assert_ne!(
entry.uuid, intent_uuid,
"the caller's entry is intent; the commit installs what it assembled"
);
assert_eq!(
entry.fragment_bitmap.as_ref().unwrap(),
&RoaringBitmap::from_iter([0u32, 1, 10, 11])
);
let scalar = stored.iter().find(|idx| idx.name == "i_idx").unwrap();
assert_eq!(
scalar.fragment_bitmap.as_ref().unwrap(),
&RoaringBitmap::from_iter([0u32, 1])
);
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 1);
assert_eq!(sorted_values(&dataset).await, before);
assert_eq!(filtered_values(&dataset, "i = 3").await, vec![3]);
assert_eq!(filtered_values(&dataset, "i >= 4").await, vec![4, 5, 6, 7]);
let first_content = load_raw_frag_reuse_content(&dataset, &entry).await.unwrap();
reserve_fragments(&mut dataset, 20).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (mut transition, mut destinations) = reader_tests::prepare(&dataset).await;
for (i, fragment) in destinations.iter_mut().enumerate() {
fragment.id = 20 + i as u64;
transition.destinations[i].id = 20 + i as u64;
}
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
let live_ids: Vec<u64> = dataset.fragments().iter().map(|frag| frag.id).collect();
assert_eq!(live_ids, vec![20, 21]);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.index_version, 1);
let second_content = load_raw_frag_reuse_content(&dataset, &entry).await.unwrap();
assert!(second_content.starts_with(&first_content));
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 2);
let scalar = stored.iter().find(|idx| idx.name == "i_idx").unwrap();
assert_eq!(
scalar.fragment_bitmap.as_ref().unwrap(),
&RoaringBitmap::from_iter([0u32, 1])
);
assert_eq!(sorted_values(&dataset).await, before);
assert_eq!(
dataset.count_rows(Some("i = 3".to_string())).await.unwrap(),
1
);
}
#[tokio::test]
async fn tagged_rewrite_recognizes_ambiguous_commit_as_own() {
use crate::utils::test::{AmbiguousCommitHandler, AmbiguousFailure};
use lance_datagen::{BatchCount, RowCount};
let handler = Arc::new(AmbiguousCommitHandler::default());
let data = lance_datagen::gen_batch()
.col("i", lance_datagen::array::step::<Int32Type>())
.into_reader_rows(RowCount::from(8), BatchCount::from(1));
let mut dataset = Dataset::write(
data,
"memory://frag-reuse-ambiguous-own-commit",
Some(WriteParams {
max_rows_per_file: 4,
commit_handler: Some(handler.clone()),
..Default::default()
}),
)
.await
.unwrap();
dataset
.create_index(
&["i"],
lance_index::IndexType::BTree,
Some("i_idx".into()),
&lance_index::scalar::ScalarIndexParams::default(),
true,
)
.await
.unwrap();
let before = sorted_values(&dataset).await;
assert_eq!(before, (0..8).collect::<Vec<_>>());
reserve_fragments(&mut dataset, 20).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let read_version = dataset.manifest.version;
handler.fail_next_rewrite(AmbiguousFailure::LandAndConflict);
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.expect("verification must recognize the landed rewrite as our own commit");
assert_eq!(dataset.manifest.version, read_version + 1);
let flag = FLAG_FRAGMENT_REUSE_INDEX;
assert_eq!(dataset.manifest.reader_feature_flags & flag, flag);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert!(is_tagged(&entry));
assert_eq!(sorted_values(&dataset).await, before);
let transaction_file = dataset
.manifest
.transaction_file
.as_deref()
.expect("the landed manifest records its transaction file");
let path = dataset
.base
.clone()
.join(crate::dataset::TRANSACTIONS_DIR)
.join(transaction_file);
assert!(dataset.object_store.exists(&path).await.unwrap());
}
#[tokio::test]
async fn lift_preserves_v0_content_bytes() {
let mut dataset = reader_tests::fixture().await;
let mut addrs = RoaringTreemap::new();
for offset in 0..4u64 {
addrs.insert((100 << 32) + offset);
}
let mut changed_row_addrs = Vec::new();
addrs.serialize_into(&mut changed_row_addrs).unwrap();
let digest = |id: u64| FragDigest {
id,
physical_rows: 4,
num_deleted_rows: 0,
};
let details = FragReuseIndexDetails {
versions: vec![FragReuseVersion {
dataset_version: 1,
groups: vec![FragReuseGroup {
changed_row_addrs,
old_frags: vec![digest(100)],
new_frags: vec![digest(110)],
}],
}],
};
let v0_entry = build_frag_reuse_index_metadata(
&dataset,
None,
details.clone(),
RoaringBitmap::from_iter([110u32]),
)
.await
.unwrap();
assert_eq!(v0_entry.index_version, 0);
dataset
.apply_commit(
Transaction::new(
dataset.manifest.version,
Operation::CreateIndex {
new_indices: vec![v0_entry],
removed_indices: vec![],
},
None,
),
&Default::default(),
&Default::default(),
)
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let v0_entry = stored_fri(&stored);
let v0_content = load_raw_frag_reuse_content(&dataset, &v0_entry)
.await
.unwrap();
assert_eq!(v0_content, InlineContent::from(&details).encode_to_vec());
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let rewrite_intent = vec![transition.clone()];
let groups = vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}];
let (entry, base_entry_version) =
build_frag_reuse_rewrite_entry(&dataset, &rewrite_intent, &groups)
.await
.unwrap();
assert_eq!(base_entry_version, Some(v0_entry.dataset_version));
assert_eq!(entry.index_version, 1);
let lifted = load_raw_frag_reuse_content(&dataset, &entry).await.unwrap();
assert!(lifted.starts_with(&v0_content));
assert_eq!(
&lifted[v0_content.len()..],
encode_length_delimited_field(2, &transition.encode_to_vec()).as_slice()
);
assert_eq!(
entry.fragment_bitmap.as_ref().unwrap(),
&RoaringBitmap::from_iter([0u32, 1, 10, 11, 110])
);
}
#[tokio::test]
async fn binding_mismatch_rejected() {
let dataset = reader_tests::fixture().await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let groups = |old: Vec<Fragment>, new: Vec<Fragment>| {
vec![RewriteGroup {
old_fragments: old,
new_fragments: new,
}]
};
let sp = |transitions: Vec<Transition>| transitions;
let mut with_extra = old_fragments.clone();
let mut foreign = Fragment::new(99);
foreign.physical_rows = Some(4);
with_extra.push(foreign);
let error = build_frag_reuse_rewrite_entry(
&dataset,
&sp(vec![transition.clone()]),
&groups(with_extra, destinations.clone()),
)
.await
.unwrap_err();
assert!(error.to_string().contains("mixes"), "{error}");
let error = build_frag_reuse_rewrite_entry(&dataset, &sp(vec![transition.clone()]), &[])
.await
.unwrap_err();
assert!(
error.to_string().contains("covered by their sources"),
"{error}"
);
let mut tampered = transition.clone();
tampered.sources[0].physical_rows += 1;
let error = build_frag_reuse_rewrite_entry(
&dataset,
&sp(vec![tampered]),
&groups(old_fragments.clone(), destinations.clone()),
)
.await
.unwrap_err();
assert!(error.to_string().contains("source digest"), "{error}");
let mut reversed = destinations.clone();
reversed.reverse();
let error = build_frag_reuse_rewrite_entry(
&dataset,
&sp(vec![transition.clone()]),
&groups(old_fragments, reversed),
)
.await
.unwrap_err();
assert!(error.to_string().contains("destination digest"), "{error}");
}
#[tokio::test]
async fn conservation_violation_rejected() {
let dataset = reader_tests::fixture().await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (mut transition, mut destinations) = reader_tests::prepare(&dataset).await;
transition.destinations.truncate(1);
destinations.truncate(1);
let error = build_frag_reuse_rewrite_entry(
&dataset,
&[transition],
&[RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
)
.await
.unwrap_err();
assert!(
error.to_string().contains("does not conserve rows"),
"{error}"
);
}
#[tokio::test]
async fn ledger_invalid_assembly_rejected() {
let dataset = reader_tests::fixture().await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (mut transition, destinations) = reader_tests::prepare(&dataset).await;
let Some(transition::Mapping::StablePartition(mapping)) = &mut transition.mapping else {
unreachable!()
};
mapping.map_id = "not-a-uuid".to_string();
let error = build_frag_reuse_rewrite_entry(
&dataset,
&[transition],
&[RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
)
.await
.unwrap_err();
assert!(error.to_string().contains("map_id"), "{error}");
}
#[tokio::test]
async fn oversized_assembly_spills_to_external_file() {
let dataset = reader_tests::fixture().await;
let mut transitions = Vec::new();
let mut groups = Vec::new();
for i in 0..4000u64 {
let fragment = |id: u64| {
let mut fragment = Fragment::new(id);
fragment.physical_rows = Some(4);
fragment
};
let digest = |id: u64| FragmentDigest {
id,
physical_rows: 4,
num_deleted_rows: 0,
};
transitions.push(Transition {
sources: vec![digest(1_000 + i)],
destinations: vec![digest(100_000 + i)],
mapping: Some(transition::Mapping::StablePartition(StablePartition {
map_id: Uuid::new_v4().to_string(),
map_size_bytes: 1,
base_id: None,
})),
});
groups.push(RewriteGroup {
old_fragments: vec![fragment(1_000 + i)],
new_fragments: vec![fragment(100_000 + i)],
});
}
let expected: Vec<u8> = transitions
.iter()
.flat_map(|transition| encode_length_delimited_field(2, &transition.encode_to_vec()))
.collect();
assert!(expected.len() > 204800);
let (entry, base_entry_version) =
build_frag_reuse_rewrite_entry(&dataset, &transitions, &groups)
.await
.unwrap();
assert_eq!(base_entry_version, None);
let details = entry.index_details.as_ref().unwrap();
let proto = FragmentReuseIndexDetails::decode(details.value.as_slice()).unwrap();
let Some(Content::External(external)) = proto.content else {
panic!("expected external content, got {proto:?}");
};
assert_eq!(external.path, FRAG_REUSE_DETAILS_FILE_NAME);
assert_eq!(external.size, expected.len() as u64);
assert_eq!(
load_raw_frag_reuse_content(&dataset, &entry).await.unwrap(),
expected
);
}
#[tokio::test]
async fn deferred_compaction_chains_onto_stable_partition() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 20).await;
let before = sorted_values(&dataset).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let mut dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let sp_entry = stored_fri(&stored);
let sp_content = load_raw_frag_reuse_content(&dataset, &sp_entry)
.await
.unwrap();
let metrics = crate::dataset::optimize::compact_files(
&mut dataset,
crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 100,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
assert_eq!(metrics.fragments_removed, 2);
assert_eq!(metrics.fragments_added, 1);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.index_version, 1);
assert!(is_tagged(&entry));
let content = load_raw_frag_reuse_content(&dataset, &entry).await.unwrap();
assert!(content.starts_with(&sp_content));
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 2);
assert!(matches!(
ledger.transitions()[0].mapping(),
Mapping::StablePartition(_)
));
assert!(matches!(
ledger.transitions()[1].mapping(),
Mapping::OrderedCompaction(_)
));
assert!(ledger.consumer(10).is_some());
assert!(ledger.consumer(11).is_some());
assert!(ledger.consumer(0).is_some());
let scalar = stored.iter().find(|idx| idx.name == "i_idx").unwrap();
assert_eq!(
scalar.fragment_bitmap.as_ref().unwrap(),
&RoaringBitmap::from_iter([0u32, 1])
);
assert_eq!(sorted_values(&dataset).await, before);
assert_eq!(filtered_values(&dataset, "i = 3").await, vec![3]);
assert_eq!(filtered_values(&dataset, "i >= 4").await, vec![4, 5, 6, 7]);
}
async fn filtered_values(dataset: &Dataset, predicate: &str) -> Vec<i32> {
let mut scan = dataset.scan();
scan.filter(predicate).unwrap();
let batch = scan.try_into_batch().await.unwrap();
let mut values: Vec<i32> = batch["i"]
.as_primitive::<Int32Type>()
.iter()
.map(|value| value.unwrap())
.collect();
values.sort_unstable();
values
}
#[tokio::test]
async fn tagged_entry_adding_nothing_is_refused() {
let mut dataset = reader_tests::fixture().await;
let entry = IndexMetadata {
uuid: Uuid::new_v4(),
name: FRAG_REUSE_INDEX_NAME.to_string(),
fields: vec![],
covering_fields: vec![],
dataset_version: dataset.manifest.version,
fragment_bitmap: Some(RoaringBitmap::from_iter([0u32, 1])),
index_details: Some(Arc::new(prost_types::Any {
type_url: "/lance.table.FragmentReuseIndexDetails".into(),
value: encode_length_delimited_field(1, &[]),
})),
index_version: 1,
created_at: None,
base_id: None,
files: None,
};
let error = dataset
.apply_commit(
Transaction::new(
dataset.manifest.version,
Operation::Rewrite {
groups: vec![],
rewritten_indices: vec![],
frag_reuse_index: Some(entry),
},
None,
),
&Default::default(),
&Default::default(),
)
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("carries no transitions"),
"{error}"
);
}
#[tokio::test]
async fn frag_reuse_rewrite_rejected_on_stable_row_id_dataset() {
let dataset = lance_datagen::gen_batch()
.col("i", lance_datagen::array::step::<Int32Type>())
.into_ram_dataset_with_params(
crate::utils::test::FragmentCount::from(2),
crate::utils::test::FragmentRowCount::from(4),
Some(crate::dataset::WriteParams {
enable_stable_row_ids: true,
max_rows_per_file: 4,
..Default::default()
}),
)
.await
.unwrap();
assert!(dataset.manifest.uses_stable_row_ids());
let error = frag_reuse_entry_appending(&dataset, vec![])
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("stable-row-id"), "{error}");
let entry = hand_built_entry(&dataset, vec![]).await;
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
dataset.manifest.version,
Operation::Rewrite {
groups: vec![],
rewritten_indices: vec![],
frag_reuse_index: Some(entry),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("stable-row-id"), "{error}");
}
#[tokio::test]
async fn unknown_mapping_in_history_rejects_rewrite() {
let mut dataset = reader_tests::fixture().await;
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let mut unknown_raw = transition.encode_to_vec();
unknown_raw.extend_from_slice(&reader_tests::field(9, b"future-mapping-payload"));
let content = reader_tests::field(2, &unknown_raw);
reader_tests::install(&mut dataset, content, destinations, false).await;
let indices = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
reader_tests::persist_fixture(&mut dataset, indices).await;
reserve_fragments(&mut dataset, 30).await;
let version = dataset.latest_version_id().await.unwrap();
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let source_ids: Vec<u64> = old_fragments.iter().map(|f| f.id).collect();
let (new_transition, new_destinations) =
reader_tests::prepare_partition(&dataset, &source_ids, 20).await;
let error = frag_reuse_entry_appending(&dataset, vec![new_transition.clone()])
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("cannot maintain"), "{error}");
let entry = hand_built_entry(&dataset, vec![new_transition]).await;
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: new_destinations,
}],
rewritten_indices: vec![],
frag_reuse_index: Some(entry),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("cannot maintain"), "{error}");
assert_eq!(dataset.latest_version_id().await.unwrap(), version);
}
#[tokio::test]
async fn shallow_clone_resolves_external_details_from_source_base() {
let source_dir = lance_core::utils::tempfile::TempStrDir::default();
let target_dir = lance_core::utils::tempfile::TempStrDir::default();
let target_uri = format!("{}/clone", target_dir.as_str());
let mut dataset = lance_datagen::gen_batch()
.col("i", lance_datagen::array::step::<Int32Type>())
.into_dataset(
source_dir.as_str(),
crate::utils::test::FragmentCount::from(2),
crate::utils::test::FragmentRowCount::from(4),
)
.await
.unwrap();
let digest = |id: u64| FragDigest {
id,
physical_rows: 4,
num_deleted_rows: 0,
};
let mut versions = Vec::new();
for i in 0..3500u64 {
let old_id = 1_000 + i;
let mut addrs = RoaringTreemap::new();
for offset in 0..4u64 {
addrs.insert((old_id << 32) + offset);
}
let mut serialized = Vec::new();
addrs.serialize_into(&mut serialized).unwrap();
versions.push(FragReuseVersion {
dataset_version: i + 1,
groups: vec![FragReuseGroup {
changed_row_addrs: serialized,
old_frags: vec![digest(old_id)],
new_frags: vec![digest(100_000 + i)],
}],
});
}
let details = FragReuseIndexDetails { versions };
let bitmap: RoaringBitmap = (0..3500u32).map(|i| 100_000 + i).collect();
let entry = build_frag_reuse_index_metadata(&dataset, None, details, bitmap)
.await
.unwrap();
dataset
.apply_commit(
Transaction::new(
dataset.manifest.version,
Operation::CreateIndex {
new_indices: vec![entry],
removed_indices: vec![],
},
None,
),
&Default::default(),
&Default::default(),
)
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let source_entry = stored_fri(&stored);
let source_content = load_raw_frag_reuse_content(&dataset, &source_entry)
.await
.unwrap();
assert!(source_content.len() > 204800);
let version = dataset.manifest.version;
let mut clone = dataset
.shallow_clone(target_uri.as_str(), version, None)
.await
.unwrap();
let stored = crate::index::load_all_indices(&clone).await.unwrap();
let cloned_entry = stored_fri(&stored);
assert!(cloned_entry.base_id.is_some());
reserve_fragments(&mut clone, 30).await;
let read_version = clone.manifest.version;
let old_fragments: Vec<Fragment> = clone.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&clone).await;
let frag_reuse_index = Some(entry_appending(&clone, vec![transition]).await);
let clone = crate::dataset::write::CommitBuilder::new(Arc::new(clone))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
let stored = crate::index::load_all_indices(&clone).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.index_version, 1);
let content = load_raw_frag_reuse_content(&clone, &entry).await.unwrap();
assert!(content.starts_with(&source_content));
let ledger = decode_entry(&clone, &entry).await;
assert_eq!(ledger.transitions().len(), 3501);
}
#[tokio::test]
async fn duplicate_row_map_id_rejected() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let Some(transition::Mapping::StablePartition(first_mapping)) = &transition.mapping else {
unreachable!()
};
let first_map_id = first_mapping.map_id.clone();
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let mut dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
reserve_fragments(&mut dataset, 30).await;
let version = dataset.latest_version_id().await.unwrap();
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (mut reused, new_destinations) =
reader_tests::prepare_partition(&dataset, &[10, 11], 40).await;
let Some(transition::Mapping::StablePartition(mapping)) = &mut reused.mapping else {
unreachable!()
};
mapping.map_id = first_map_id.clone();
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: new_destinations,
}],
rewritten_indices: vec![],
frag_reuse_index: Some(entry_appending(&dataset, vec![reused]).await),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(error.to_string().contains(&first_map_id), "{error}");
assert_eq!(dataset.latest_version_id().await.unwrap(), version);
}
#[tokio::test]
async fn uncovered_deferred_compaction_commits_plain_rewrite() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 20).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
let schema = Arc::new(arrow_schema::Schema::from(dataset.schema()));
let dataset = InsertBuilder::new(Arc::new(dataset))
.with_params(&WriteParams {
mode: WriteMode::Append,
..Default::default()
})
.execute(vec![
RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from_iter_values(100..102))],
)
.unwrap(),
])
.await
.unwrap();
let mut dataset = InsertBuilder::new(Arc::new(dataset.clone()))
.with_params(&WriteParams {
mode: WriteMode::Append,
..Default::default()
})
.execute(vec![
RecordBatch::try_new(
schema,
vec![Arc::new(Int32Array::from_iter_values(102..104))],
)
.unwrap(),
])
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry_before = stored_fri(&stored);
let before = sorted_values(&dataset).await;
let metrics = crate::dataset::optimize::compact_files(
&mut dataset,
crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 4,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
assert_eq!(metrics.fragments_removed, 2);
assert_eq!(metrics.fragments_added, 1);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.uuid, entry_before.uuid);
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 1);
assert_eq!(sorted_values(&dataset).await, before);
assert_eq!(filtered_values(&dataset, "i = 3").await, vec![3]);
assert_eq!(
filtered_values(&dataset, "i >= 100").await,
(100..104).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn detached_commit_rejects_transition_intent_and_tagged_entries() {
let dataset = reader_tests::fixture().await;
let version = dataset.manifest.version;
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.with_detached(true)
.execute(Transaction::new(
version,
Operation::Rewrite {
groups: vec![],
rewritten_indices: vec![],
frag_reuse_index: Some(entry_appending(&dataset, vec![]).await),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("Detached commits"), "{error}");
let entry = IndexMetadata {
uuid: Uuid::new_v4(),
name: FRAG_REUSE_INDEX_NAME.to_string(),
fields: vec![],
covering_fields: vec![],
dataset_version: version,
fragment_bitmap: Some(RoaringBitmap::from_iter([0u32, 1])),
index_details: Some(Arc::new(prost_types::Any {
type_url: "/lance.table.FragmentReuseIndexDetails".into(),
value: encode_length_delimited_field(1, &[]),
})),
index_version: 1,
created_at: None,
base_id: None,
files: None,
};
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.with_detached(true)
.execute(Transaction::new(
version,
Operation::Rewrite {
groups: vec![],
rewritten_indices: vec![],
frag_reuse_index: Some(entry),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("Detached commits"), "{error}");
}
#[tokio::test]
async fn destination_with_deletion_file_rejected() {
let dataset = reader_tests::fixture().await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, mut destinations) = reader_tests::prepare(&dataset).await;
destinations[0].deletion_file = Some(lance_table::format::DeletionFile {
read_version: 1,
id: 1,
file_type: lance_table::format::DeletionFileType::Array,
num_deleted_rows: Some(1),
base_id: None,
});
let error = build_frag_reuse_rewrite_entry(
&dataset,
&[transition],
&[RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
)
.await
.unwrap_err();
assert!(
error.to_string().contains("carries a deletion file"),
"{error}"
);
}
#[tokio::test]
async fn unassigned_destination_id_rejected_at_commit() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (mut transition, mut destinations) = reader_tests::prepare(&dataset).await;
destinations[0].id = 0;
transition.destinations[0].id = 0;
let read_version = dataset.manifest.version;
let entry = hand_built_entry(&dataset, vec![transition]).await;
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index: Some(entry),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(error.to_string().contains("unassigned"), "{error}");
assert_eq!(dataset.latest_version_id().await.unwrap(), read_version);
}
#[tokio::test]
async fn live_destination_id_rejected_at_commit() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (mut transition, mut destinations) = reader_tests::prepare(&dataset).await;
destinations[0].id = 1;
transition.destinations[0].id = 1;
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(hand_built_entry(&dataset, vec![transition]).await);
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(error.to_string().contains("already live"), "{error}");
}
async fn indexed_three_fragment_dataset() -> Dataset {
let mut dataset = lance_datagen::gen_batch()
.col("i", lance_datagen::array::step::<Int32Type>())
.into_ram_dataset(
crate::utils::test::FragmentCount::from(3),
crate::utils::test::FragmentRowCount::from(4),
)
.await
.unwrap();
dataset
.create_index(
&["i"],
lance_index::IndexType::Scalar,
Some("i_idx".into()),
&lance_index::scalar::ScalarIndexParams::default(),
false,
)
.await
.unwrap();
dataset
}
async fn tag_fragment_zero(mut dataset: Dataset) -> Dataset {
reserve_fragments(&mut dataset, 40).await;
let old_fragments: Vec<Fragment> = dataset
.fragments()
.iter()
.filter(|f| f.id == 0)
.cloned()
.collect();
let (transition, destinations) = reader_tests::prepare_partition(&dataset, &[0], 10).await;
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap()
}
#[tokio::test]
async fn deferred_compaction_materializes_missing_deletion_counts() {
let mut dataset = indexed_three_fragment_dataset().await;
dataset.delete("i = 5").await.unwrap();
let mut fragments: Vec<Fragment> = dataset.fragments().as_ref().clone();
let deletion = fragments
.iter_mut()
.find(|f| f.id == 1)
.unwrap()
.deletion_file
.as_mut()
.unwrap();
assert!(deletion.num_deleted_rows.is_some());
deletion.num_deleted_rows = None;
Arc::make_mut(&mut dataset.manifest).fragments = Arc::new(fragments);
let indices = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
reader_tests::persist_fixture(&mut dataset, indices).await;
assert!(
dataset
.fragments()
.iter()
.find(|f| f.id == 1)
.unwrap()
.deletion_file
.as_ref()
.unwrap()
.num_deleted_rows
.is_none()
);
let mut dataset = tag_fragment_zero(dataset).await;
let metrics = crate::dataset::optimize::compact_files(
&mut dataset,
crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 100,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
assert_eq!(metrics.fragments_removed, 4);
assert_eq!(metrics.fragments_added, 2);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 3);
assert!(ledger.consumer(1).is_some());
assert_eq!(dataset.count_rows(None).await.unwrap(), 11);
assert_eq!(filtered_values(&dataset, "i = 5").await, Vec::<i32>::new());
assert_eq!(filtered_values(&dataset, "i = 4").await, vec![4]);
}
#[tokio::test]
async fn deferred_compaction_consumes_heavily_deleted_source() {
let mut dataset = indexed_three_fragment_dataset().await;
dataset.delete("i >= 9").await.unwrap();
let mut dataset = tag_fragment_zero(dataset).await;
crate::dataset::optimize::compact_files(
&mut dataset,
crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 100,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
let ledger = decode_entry(&dataset, &entry).await;
assert!(ledger.consumer(2).is_some());
assert_eq!(dataset.count_rows(None).await.unwrap(), 9);
assert_eq!(filtered_values(&dataset, "i = 8").await, vec![8]);
assert_eq!(filtered_values(&dataset, "i >= 9").await, Vec::<i32>::new());
}
#[tokio::test]
async fn overlaid_source_rejects_stable_partition_rewrite() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let mut fragments: Vec<Fragment> = dataset.fragments().as_ref().clone();
let overlay_version = fragments[0].files[0].file_version().unwrap();
fragments[0]
.overlays
.push(lance_table::format::overlay::DataOverlayFile {
data_file: lance_table::format::DataFile::new(
"overlay-0.lance",
vec![0],
vec![0],
overlay_version,
None,
None,
),
coverage: lance_table::format::overlay::OverlayCoverage::dense(
RoaringBitmap::from_iter([0u32]),
),
committed_version: dataset.manifest.version,
});
Arc::make_mut(&mut dataset.manifest).fragments = Arc::new(fragments);
let indices = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
reader_tests::persist_fixture(&mut dataset, indices).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let version = dataset.latest_version_id().await.unwrap();
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index: Some(entry_appending(&dataset, vec![transition]).await),
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("overlay"), "{error}");
assert_eq!(dataset.latest_version_id().await.unwrap(), version);
}
#[tokio::test]
async fn overlaid_source_rejects_tagged_compaction() {
let dataset = lance_datagen::gen_batch()
.col("i", lance_datagen::array::step::<Int32Type>())
.into_ram_dataset(
crate::utils::test::FragmentCount::from(2),
crate::utils::test::FragmentRowCount::from(4),
)
.await
.unwrap();
let schema = Arc::new(arrow_schema::Schema::from(dataset.schema()));
let mut dataset = InsertBuilder::new(Arc::new(dataset))
.with_params(&WriteParams {
mode: WriteMode::Append,
..Default::default()
})
.execute(vec![
RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from_iter_values(100..104))],
)
.unwrap(),
])
.await
.unwrap();
let appended_id = dataset.fragments().last().unwrap().id;
dataset
.create_index(
&["i"],
lance_index::IndexType::Scalar,
Some("i_idx".into()),
&lance_index::scalar::ScalarIndexParams::default(),
false,
)
.await
.unwrap();
let mut fragments: Vec<Fragment> = dataset.fragments().as_ref().clone();
let overlay_version = fragments
.iter()
.find(|f| f.id == appended_id)
.unwrap()
.files[0]
.file_version()
.unwrap();
fragments
.iter_mut()
.find(|f| f.id == appended_id)
.unwrap()
.overlays
.push(lance_table::format::overlay::DataOverlayFile {
data_file: lance_table::format::DataFile::new(
"overlay-f.lance",
vec![0],
vec![0],
overlay_version,
None,
None,
),
coverage: lance_table::format::overlay::OverlayCoverage::dense(
RoaringBitmap::from_iter([0u32]),
),
committed_version: dataset.manifest.version,
});
Arc::make_mut(&mut dataset.manifest).fragments = Arc::new(fragments);
let indices = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
reader_tests::persist_fixture(&mut dataset, indices).await;
reserve_fragments(&mut dataset, 40).await;
let old_fragments: Vec<Fragment> = dataset
.fragments()
.iter()
.filter(|f| f.id < 2)
.cloned()
.collect();
let (transition, sp_destinations) =
reader_tests::prepare_partition(&dataset, &[0, 1], 10).await;
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let mut dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: sp_destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
let overlaid = dataset
.fragments()
.iter()
.find(|f| f.id == appended_id)
.unwrap()
.clone();
assert!(!overlaid.overlays.is_empty());
let txn = InsertBuilder::new(Arc::new(dataset.clone()))
.with_params(&WriteParams {
mode: WriteMode::Append,
..Default::default()
})
.execute_uncommitted(vec![
RecordBatch::try_new(
schema,
vec![Arc::new(Int32Array::from_iter_values(100..104))],
)
.unwrap(),
])
.await
.unwrap();
let Operation::Append {
fragments: new_fragments,
} = txn.operation
else {
unreachable!()
};
let mut row_addrs = RoaringTreemap::new();
for offset in 0..4u64 {
row_addrs.insert((appended_id << 32) + offset);
}
let mut serialized = Vec::new();
row_addrs.serialize_into(&mut serialized).unwrap();
let task = crate::dataset::optimize::RewriteResult {
metrics: Default::default(),
new_fragments,
read_version: dataset.manifest.version,
original_fragments: vec![overlaid],
row_addrs: Some(serialized),
};
let fragments_before: Vec<u64> = dataset.fragments().iter().map(|f| f.id).collect();
let error = crate::dataset::optimize::commit_compaction(
&mut dataset,
vec![task],
Arc::new(crate::dataset::optimize::IgnoreRemap::default()),
&crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 100,
defer_index_remap: true,
..Default::default()
},
)
.await
.unwrap_err();
assert!(matches!(error, Error::NotSupported { .. }), "{error}");
assert!(error.to_string().contains("overlay"), "{error}");
let fragments_after: Vec<u64> = dataset
.checkout_version(dataset.latest_version_id().await.unwrap())
.await
.unwrap()
.fragments()
.iter()
.map(|f| f.id)
.collect();
assert_eq!(fragments_after, fragments_before);
}
struct FoldingHarness {
dataset: Dataset,
read_version: u64,
old_fragments: Vec<Fragment>,
destinations: Vec<Fragment>,
transition: Transition,
}
async fn folding_harness(delta_delete: Option<&str>) -> FoldingHarness {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let (transition, destinations) = reader_tests::prepare(&dataset).await;
if let Some(predicate) = delta_delete {
dataset.delete(predicate).await.unwrap();
}
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
FoldingHarness {
read_version: dataset.manifest.version,
dataset,
old_fragments,
destinations,
transition,
}
}
async fn fold_destination(harness: &mut FoldingHarness, destination: usize, offsets: &[u32]) {
let deletion_vector: lance_core::utils::deletion::DeletionVector =
offsets.iter().copied().collect();
let file = lance_table::io::deletion::write_deletion_file(
&harness.dataset.base,
harness.destinations[destination].id,
harness.read_version,
&deletion_vector,
&harness.dataset.object_store,
)
.await
.unwrap()
.unwrap();
harness.destinations[destination].deletion_file = Some(file);
}
async fn commit_fold(harness: FoldingHarness) -> lance_core::Result<Dataset> {
let frag_reuse_index =
Some(entry_appending(&harness.dataset, vec![harness.transition]).await);
crate::dataset::write::CommitBuilder::new(Arc::new(harness.dataset))
.execute(Transaction::new(
harness.read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments: harness.old_fragments,
new_fragments: harness.destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
}
#[tokio::test]
async fn folded_delta_deletions_commit_and_read_correctly() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let job_read_version = dataset.manifest.version;
let scan_time_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let job_entry = entry_appending(&dataset, vec![transition.clone()]).await;
dataset.delete("i = 1 OR i = 2").await.unwrap();
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
job_read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments: scan_time_fragments,
new_fragments: destinations.clone(),
}],
rewritten_indices: vec![],
frag_reuse_index: Some(job_entry),
},
None,
))
.await
.unwrap_err();
assert!(
matches!(error, Error::RetryableCommitConflict { .. }),
"{error}"
);
let mut harness = FoldingHarness {
read_version: dataset.manifest.version,
old_fragments: dataset.fragments().iter().cloned().collect(),
destinations,
transition,
dataset,
};
fold_destination(&mut harness, 1, &[0]).await;
fold_destination(&mut harness, 0, &[1]).await;
let dataset = commit_fold(harness).await.unwrap();
assert_eq!(sorted_values(&dataset).await, vec![0, 3, 4, 5, 6, 7]);
assert_eq!(dataset.count_rows(None).await.unwrap(), 6);
assert_eq!(filtered_values(&dataset, "i = 1").await, Vec::<i32>::new());
assert_eq!(filtered_values(&dataset, "i = 2").await, Vec::<i32>::new());
assert_eq!(filtered_values(&dataset, "i = 3").await, vec![3]);
assert_eq!(filtered_values(&dataset, "i >= 4").await, vec![4, 5, 6, 7]);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 1);
let recorded = &ledger.transitions()[0];
assert!(recorded.sources().iter().all(|d| d.num_deleted_rows == 0));
assert!(
recorded
.destinations()
.iter()
.all(|d| d.num_deleted_rows == 0)
);
}
#[tokio::test]
async fn regime_a_still_rejects_destination_deletion_files() {
let mut harness = folding_harness(None).await;
fold_destination(&mut harness, 0, &[0]).await;
let version = harness.read_version;
let dataset = harness.dataset.clone();
let error = commit_fold(harness).await.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("carries a deletion file"),
"{error}"
);
assert_eq!(dataset.latest_version_id().await.unwrap(), version);
}
#[tokio::test]
async fn folded_count_mismatch_rejected() {
let mut harness = folding_harness(Some("i = 1 OR i = 2")).await;
fold_destination(&mut harness, 1, &[0]).await;
let error = commit_fold(harness).await.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error
.to_string()
.contains("hold 1 rows but the sources gained 2"),
"{error}"
);
}
#[tokio::test]
async fn folded_wrong_position_rejected() {
let mut harness = folding_harness(Some("i = 1 OR i = 2")).await;
fold_destination(&mut harness, 1, &[0]).await;
fold_destination(&mut harness, 0, &[2]).await;
let error = commit_fold(harness).await.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("translated positions"),
"{error}"
);
}
#[tokio::test]
async fn folded_lying_destination_count_rejected() {
let mut harness = folding_harness(Some("i = 1 OR i = 2")).await;
fold_destination(&mut harness, 1, &[0]).await;
fold_destination(&mut harness, 0, &[1]).await;
harness.destinations[0]
.deletion_file
.as_mut()
.unwrap()
.num_deleted_rows = Some(5);
let error = commit_fold(harness).await.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("records 5 deleted rows"),
"{error}"
);
}
#[tokio::test]
async fn shrunken_source_deletion_vector_rejected() {
let mut dataset = reader_tests::fixture().await;
dataset.delete("i = 0 OR i = 1").await.unwrap();
reserve_fragments(&mut dataset, 30).await;
let (transition, destinations) = reader_tests::prepare(&dataset).await;
assert_eq!(transition.sources[0].num_deleted_rows, 2);
let deletion_vector: lance_core::utils::deletion::DeletionVector =
[0u32].into_iter().collect();
let file = lance_table::io::deletion::write_deletion_file(
&dataset.base,
0,
dataset.manifest.version,
&deletion_vector,
&dataset.object_store,
)
.await
.unwrap()
.unwrap();
let mut fragments: Vec<Fragment> = dataset.fragments().as_ref().clone();
fragments[0].deletion_file = Some(file);
Arc::make_mut(&mut dataset.manifest).fragments = Arc::new(fragments);
let indices = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
reader_tests::persist_fixture(&mut dataset, indices).await;
let harness = FoldingHarness {
read_version: dataset.latest_version_id().await.unwrap(),
old_fragments: dataset.fragments().iter().cloned().collect(),
destinations,
transition,
dataset,
};
let error = commit_fold(harness).await.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(error.to_string().contains("cannot shrink"), "{error}");
}
#[tokio::test]
async fn scan_time_deletion_accounting_rejected() {
let mut dataset = reader_tests::fixture().await;
dataset.delete("i = 0").await.unwrap();
reserve_fragments(&mut dataset, 30).await;
let (transition, mut destinations) = reader_tests::prepare(&dataset).await;
assert_eq!(transition.sources[0].num_deleted_rows, 1);
let deletion_vector: lance_core::utils::deletion::DeletionVector =
[1u32, 2].into_iter().collect();
let file = lance_table::io::deletion::write_deletion_file(
&dataset.base,
0,
dataset.manifest.version,
&deletion_vector,
&dataset.object_store,
)
.await
.unwrap()
.unwrap();
let mut fragments: Vec<Fragment> = dataset.fragments().as_ref().clone();
fragments[0].deletion_file = Some(file);
Arc::make_mut(&mut dataset.manifest).fragments = Arc::new(fragments);
let indices = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
reader_tests::persist_fixture(&mut dataset, indices).await;
let read_version = dataset.latest_version_id().await.unwrap();
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let deletion_vector: lance_core::utils::deletion::DeletionVector =
[0u32].into_iter().collect();
let file = lance_table::io::deletion::write_deletion_file(
&dataset.base,
destinations[1].id,
read_version,
&deletion_vector,
&dataset.object_store,
)
.await
.unwrap()
.unwrap();
destinations[1].deletion_file = Some(file);
let harness = FoldingHarness {
read_version,
old_fragments,
destinations,
transition,
dataset,
};
let error = commit_fold(harness).await.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("scan-time deletion accounting"),
"{error}"
);
}
async fn oc_job(dataset: &Dataset, source_ids: &[u64], dest_id: u64) -> (Transition, Fragment) {
let source_fragments: Vec<Fragment> = source_ids
.iter()
.map(|id| {
dataset
.fragments()
.iter()
.find(|f| f.id == *id)
.unwrap()
.clone()
})
.collect();
let batch = {
let mut scan = dataset.scan();
scan.with_fragments(source_fragments.clone());
scan.try_into_batch().await.unwrap()
};
let txn = InsertBuilder::new(Arc::new(dataset.clone()))
.with_params(&WriteParams {
mode: WriteMode::Append,
..Default::default()
})
.execute_uncommitted(vec![batch])
.await
.unwrap();
let Operation::Append { mut fragments } = txn.operation else {
unreachable!()
};
assert_eq!(fragments.len(), 1);
fragments[0].id = dest_id;
let destination = fragments.pop().unwrap();
let mut survivors = RoaringTreemap::new();
let mut sources = Vec::new();
for frag in &source_fragments {
let deleted: Option<RoaringBitmap> = dataset
.get_fragment(frag.id as usize)
.unwrap()
.get_deletion_vector()
.await
.unwrap()
.map(|v| v.iter().collect());
let physical = frag.physical_rows.unwrap() as u64;
for offset in 0..physical {
if !deleted.as_ref().is_some_and(|d| d.contains(offset as u32)) {
survivors.insert((frag.id << 32) | offset);
}
}
sources.push(FragmentDigest {
id: frag.id,
physical_rows: physical,
num_deleted_rows: deleted.as_ref().map_or(0, |d| d.len()),
});
}
let mut changed_row_addrs = Vec::new();
survivors.serialize_into(&mut changed_row_addrs).unwrap();
let transition = Transition {
sources,
destinations: vec![FragmentDigest {
id: destination.id,
physical_rows: destination.physical_rows.unwrap() as u64,
num_deleted_rows: 0,
}],
mapping: Some(transition::Mapping::OrderedCompaction(
lance_table::format::pb::fragment_reuse_index_details::OrderedCompaction {
changed_row_addrs,
},
)),
};
(transition, destination)
}
async fn write_dv(
dataset: &Dataset,
fragment: &mut Fragment,
read_version: u64,
offsets: &[u32],
) {
let deletion_vector: lance_core::utils::deletion::DeletionVector =
offsets.iter().copied().collect();
let file = lance_table::io::deletion::write_deletion_file(
&dataset.base,
fragment.id,
read_version,
&deletion_vector,
&dataset.object_store,
)
.await
.unwrap()
.unwrap();
fragment.deletion_file = Some(file);
}
#[tokio::test]
async fn oc_folded_delta_deletions_commit_and_read_correctly() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let job_read_version = dataset.manifest.version;
let scan_time_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, mut destination) = oc_job(&dataset, &[0, 1], 20).await;
let job_entry = entry_appending(&dataset, vec![transition.clone()]).await;
dataset.delete("i = 1 OR i = 6").await.unwrap();
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset.clone()))
.execute(Transaction::new(
job_read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments: scan_time_fragments,
new_fragments: vec![destination.clone()],
}],
rewritten_indices: vec![],
frag_reuse_index: Some(job_entry),
},
None,
))
.await
.unwrap_err();
assert!(
matches!(error, Error::RetryableCommitConflict { .. }),
"{error}"
);
let read_version = dataset.manifest.version;
write_dv(&dataset, &mut destination, read_version, &[1, 6]).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: vec![destination],
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
assert_eq!(sorted_values(&dataset).await, vec![0, 2, 3, 4, 5, 7]);
assert_eq!(dataset.count_rows(None).await.unwrap(), 6);
assert_eq!(filtered_values(&dataset, "i = 1").await, Vec::<i32>::new());
assert_eq!(filtered_values(&dataset, "i = 6").await, Vec::<i32>::new());
assert_eq!(filtered_values(&dataset, "i = 5").await, vec![5]);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 1);
assert!(matches!(
ledger.transitions()[0].mapping(),
Mapping::OrderedCompaction(_)
));
assert!(
ledger.transitions()[0]
.sources()
.iter()
.all(|d| d.num_deleted_rows == 0)
);
}
#[tokio::test]
async fn oc_regime_a_still_rejects_destination_deletion_files() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let (transition, mut destination) = oc_job(&dataset, &[0, 1], 20).await;
let read_version = dataset.manifest.version;
write_dv(&dataset, &mut destination, read_version, &[0]).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: vec![destination],
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("carries a deletion file"),
"{error}"
);
}
#[tokio::test]
async fn oc_scan_time_deletion_accounting_rejected() {
let mut dataset = reader_tests::fixture().await;
dataset.delete("i = 0").await.unwrap();
reserve_fragments(&mut dataset, 30).await;
let (transition, mut destination) = oc_job(&dataset, &[0, 1], 20).await;
assert_eq!(transition.sources[0].num_deleted_rows, 1);
let mut fragments: Vec<Fragment> = dataset.fragments().as_ref().clone();
let base_version = dataset.manifest.version;
{
let frag = &mut fragments[0];
let deletion_vector: lance_core::utils::deletion::DeletionVector =
[1u32, 2].into_iter().collect();
let file = lance_table::io::deletion::write_deletion_file(
&dataset.base,
frag.id,
base_version,
&deletion_vector,
&dataset.object_store,
)
.await
.unwrap()
.unwrap();
frag.deletion_file = Some(file);
}
Arc::make_mut(&mut dataset.manifest).fragments = Arc::new(fragments);
let indices = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
reader_tests::persist_fixture(&mut dataset, indices).await;
let read_version = dataset.latest_version_id().await.unwrap();
write_dv(&dataset, &mut destination, read_version, &[1]).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: vec![destination],
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("scan-time deletion accounting"),
"{error}"
);
}
#[tokio::test]
async fn oc_folded_wrong_position_rejected() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let (transition, mut destination) = oc_job(&dataset, &[0, 1], 20).await;
dataset.delete("i = 1 OR i = 6").await.unwrap();
let read_version = dataset.manifest.version;
write_dv(&dataset, &mut destination, read_version, &[1, 5]).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let error = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: vec![destination],
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("translated positions"),
"{error}"
);
}
#[tokio::test]
async fn mixed_sp_and_oc_folding_validate_independently() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let (sp_transition, sp_destinations) =
reader_tests::prepare_partition(&dataset, &[0], 10).await;
let (oc_transition, mut oc_destination) = oc_job(&dataset, &[1], 21).await;
dataset.delete("i = 1 OR i = 6").await.unwrap();
let read_version = dataset.manifest.version;
let mut sp_destinations = sp_destinations;
write_dv(&dataset, &mut sp_destinations[1], read_version, &[0]).await;
write_dv(&dataset, &mut oc_destination, read_version, &[2]).await;
let sp_old: Vec<Fragment> = dataset
.fragments()
.iter()
.filter(|f| f.id == 0)
.cloned()
.collect();
let oc_old: Vec<Fragment> = dataset
.fragments()
.iter()
.filter(|f| f.id == 1)
.cloned()
.collect();
let frag_reuse_index =
Some(entry_appending(&dataset, vec![sp_transition, oc_transition]).await);
let dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![
RewriteGroup {
old_fragments: sp_old,
new_fragments: sp_destinations,
},
RewriteGroup {
old_fragments: oc_old,
new_fragments: vec![oc_destination],
},
],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
assert_eq!(sorted_values(&dataset).await, vec![0, 2, 3, 4, 5, 7]);
assert_eq!(filtered_values(&dataset, "i = 1").await, Vec::<i32>::new());
assert_eq!(filtered_values(&dataset, "i = 6").await, Vec::<i32>::new());
assert_eq!(filtered_values(&dataset, "i = 7").await, vec![7]);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 2);
let (sp_count, oc_count) = ledger
.transitions()
.iter()
.fold((0, 0), |(sp, oc), t| match t.mapping() {
Mapping::StablePartition(_) => (sp + 1, oc),
Mapping::OrderedCompaction(_) => (sp, oc + 1),
});
assert_eq!((sp_count, oc_count), (1, 1));
}
#[tokio::test]
async fn job_supplied_source_deletion_state_cannot_resurrect_rows() {
let mut harness = folding_harness(Some("i = 1")).await;
let forged: lance_core::utils::deletion::DeletionVector = [2u32].into_iter().collect();
let file = lance_table::io::deletion::write_deletion_file(
&harness.dataset.base,
0,
harness.read_version,
&forged,
&harness.dataset.object_store,
)
.await
.unwrap()
.unwrap();
let slot = harness
.old_fragments
.iter()
.position(|f| f.id == 0)
.unwrap();
harness.old_fragments[slot].deletion_file = Some(file);
fold_destination(&mut harness, 0, &[1]).await;
let error = commit_fold(harness).await.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
assert!(
error.to_string().contains("translated positions"),
"{error}"
);
}
#[tokio::test]
async fn drained_tagged_table_restarts_history_tagged() {
let mut dataset = reader_tests::fixture().await;
reserve_fragments(&mut dataset, 30).await;
let old_fragments: Vec<Fragment> = dataset.fragments().iter().cloned().collect();
let (transition, destinations) = reader_tests::prepare(&dataset).await;
let read_version = dataset.manifest.version;
let frag_reuse_index = Some(entry_appending(&dataset, vec![transition]).await);
let mut dataset = crate::dataset::write::CommitBuilder::new(Arc::new(dataset))
.execute(Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index,
},
None,
))
.await
.unwrap();
let indices: Vec<IndexMetadata> = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.iter()
.filter(|idx| idx.name != FRAG_REUSE_INDEX_NAME)
.cloned()
.collect();
reader_tests::persist_fixture(&mut dataset, indices).await;
let flag = FLAG_FRAGMENT_REUSE_INDEX;
assert_eq!(dataset.manifest.reader_feature_flags & flag, flag);
dataset
.create_index(
&["i"],
lance_index::IndexType::Scalar,
Some("i_idx".into()),
&lance_index::scalar::ScalarIndexParams::default(),
true,
)
.await
.unwrap();
let before = sorted_values(&dataset).await;
crate::dataset::optimize::compact_files(
&mut dataset,
crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 100,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.index_version, 1);
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 1);
assert!(matches!(
ledger.transitions()[0].mapping(),
Mapping::OrderedCompaction(_)
));
assert!(ledger.consumer(10).is_some());
assert_eq!(sorted_values(&dataset).await, before);
assert_eq!(filtered_values(&dataset, "i = 3").await, vec![3]);
}
#[tokio::test]
async fn flag_with_existing_v0_entry_compaction_upgrades() {
let mut dataset = indexed_three_fragment_dataset().await;
let mut addrs = RoaringTreemap::new();
for offset in 0..4u64 {
addrs.insert((100 << 32) + offset);
}
let mut changed_row_addrs = Vec::new();
addrs.serialize_into(&mut changed_row_addrs).unwrap();
let digest = |id: u64| FragDigest {
id,
physical_rows: 4,
num_deleted_rows: 0,
};
let details = FragReuseIndexDetails {
versions: vec![FragReuseVersion {
dataset_version: 1,
groups: vec![FragReuseGroup {
changed_row_addrs,
old_frags: vec![digest(100)],
new_frags: vec![digest(110)],
}],
}],
};
let v0_entry = build_frag_reuse_index_metadata(
&dataset,
None,
details,
RoaringBitmap::from_iter([110u32]),
)
.await
.unwrap();
assert_eq!(v0_entry.index_version, 0);
dataset
.apply_commit(
Transaction::new(
dataset.manifest.version,
Operation::CreateIndex {
new_indices: vec![v0_entry],
removed_indices: vec![],
},
None,
),
&Default::default(),
&Default::default(),
)
.await
.unwrap();
let indices: Vec<IndexMetadata> = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.as_ref()
.clone();
{
let manifest = Arc::make_mut(&mut dataset.manifest);
manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
}
reader_tests::persist_fixture(&mut dataset, indices).await;
let flag = FLAG_FRAGMENT_REUSE_INDEX;
assert_eq!(dataset.manifest.reader_feature_flags & flag, flag);
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let v0_entry = stored_fri(&stored);
assert_eq!(v0_entry.index_version, 0);
let v0_content = load_raw_frag_reuse_content(&dataset, &v0_entry)
.await
.unwrap();
let before = sorted_values(&dataset).await;
crate::dataset::optimize::compact_files(
&mut dataset,
crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 100,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.index_version, 1);
assert!(is_tagged(&entry));
let lifted = load_raw_frag_reuse_content(&dataset, &entry).await.unwrap();
assert!(lifted.starts_with(&v0_content));
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 2);
assert!(matches!(
ledger.transitions()[0].mapping(),
Mapping::OrderedCompaction(_)
));
assert!(matches!(
ledger.transitions()[1].mapping(),
Mapping::OrderedCompaction(_)
));
assert_eq!(
ledger.transitions()[1]
.sources()
.iter()
.map(|digest| digest.id)
.collect::<Vec<_>>(),
vec![0, 1, 2]
);
assert_eq!(sorted_values(&dataset).await, before);
}
#[tokio::test]
async fn stale_v0_writer_lands_v1_after_upgrade_and_trim() {
let mut dataset = indexed_three_fragment_dataset().await;
reserve_fragments(&mut dataset, 40).await;
let before = sorted_values(&dataset).await;
assert_eq!(before, (0..12).collect::<Vec<_>>());
let read_version = dataset.manifest.version;
let old_fragments: Vec<Fragment> = dataset
.fragments()
.iter()
.filter(|frag| frag.id == 1 || frag.id == 2)
.cloned()
.collect();
let compacted = {
let mut scan = dataset.scan();
scan.with_fragments(old_fragments.clone());
scan.try_into_batch().await.unwrap()
};
let append = InsertBuilder::new(Arc::new(dataset.clone()))
.with_params(&WriteParams {
mode: WriteMode::Append,
..Default::default()
})
.execute_uncommitted(vec![compacted])
.await
.unwrap();
let Operation::Append { fragments } = append.operation else {
unreachable!()
};
let mut destinations = fragments;
assert_eq!(destinations.len(), 1);
destinations[0].id = 30;
let mut addrs = RoaringTreemap::new();
for frag_id in [1u64, 2] {
for offset in 0..4u64 {
addrs.insert((frag_id << 32) + offset);
}
}
let mut changed_row_addrs = Vec::new();
addrs.serialize_into(&mut changed_row_addrs).unwrap();
let digest = |id: u64, physical_rows: usize| FragDigest {
id,
physical_rows,
num_deleted_rows: 0,
};
let details = FragReuseIndexDetails {
versions: vec![FragReuseVersion {
dataset_version: read_version,
groups: vec![FragReuseGroup {
changed_row_addrs,
old_frags: vec![digest(1, 4), digest(2, 4)],
new_frags: vec![digest(30, 8)],
}],
}],
};
let v0_entry = build_frag_reuse_index_metadata(
&dataset,
None,
details,
RoaringBitmap::from_iter([30u32]),
)
.await
.unwrap();
assert_eq!(v0_entry.index_version, 0);
let stale_transaction = Transaction::new(
read_version,
Operation::Rewrite {
groups: vec![RewriteGroup {
old_fragments,
new_fragments: destinations,
}],
rewritten_indices: vec![],
frag_reuse_index: Some(v0_entry),
},
None,
);
let mut dataset = tag_fragment_zero(dataset).await;
let flag = FLAG_FRAGMENT_REUSE_INDEX;
assert_eq!(dataset.manifest.reader_feature_flags & flag, flag);
let indices: Vec<IndexMetadata> = crate::index::load_all_indices(&dataset)
.await
.unwrap()
.iter()
.filter(|idx| idx.name != FRAG_REUSE_INDEX_NAME)
.cloned()
.collect();
reader_tests::persist_fixture(&mut dataset, indices).await;
assert_eq!(dataset.manifest.reader_feature_flags & flag, flag);
assert!(
crate::index::load_all_indices(&dataset)
.await
.unwrap()
.iter()
.all(|idx| idx.name != FRAG_REUSE_INDEX_NAME)
);
let trim_version = dataset.manifest.version;
dataset
.metadata_cache
.insert_with_key(
&crate::session::caches::TransactionKey {
version: trim_version,
},
Arc::new(Transaction::new(
trim_version - 1,
Operation::ReserveFragments { num_fragments: 0 },
None,
)),
)
.await;
dataset
.create_index(
&["i"],
lance_index::IndexType::Scalar,
Some("i_idx".into()),
&lance_index::scalar::ScalarIndexParams::default(),
true,
)
.await
.unwrap();
dataset
.apply_commit(stale_transaction, &Default::default(), &Default::default())
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.index_version, 1);
assert!(is_tagged(&entry));
let ledger = decode_entry(&dataset, &entry).await;
assert_eq!(ledger.transitions().len(), 1);
assert!(matches!(
ledger.transitions()[0].mapping(),
Mapping::OrderedCompaction(_)
));
assert_eq!(
ledger.transitions()[0]
.sources()
.iter()
.map(|digest| digest.id)
.collect::<Vec<_>>(),
vec![1, 2]
);
assert_eq!(ledger.transitions()[0].destinations()[0].id, 30);
assert_eq!(sorted_values(&dataset).await, before);
assert_eq!(filtered_values(&dataset, "i = 7").await, vec![7]);
}
#[tokio::test]
async fn untagged_table_still_creates_v0_entry() {
let mut dataset = indexed_three_fragment_dataset().await;
assert_eq!(
dataset.manifest.reader_feature_flags & FLAG_FRAGMENT_REUSE_INDEX,
0
);
crate::dataset::optimize::compact_files(
&mut dataset,
crate::dataset::optimize::CompactionOptions {
target_rows_per_fragment: 100,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
let stored = crate::index::load_all_indices(&dataset).await.unwrap();
let entry = stored_fri(&stored);
assert_eq!(entry.index_version, 0);
assert_eq!(
dataset.manifest.reader_feature_flags & FLAG_FRAGMENT_REUSE_INDEX,
0
);
}
}