Skip to content

Commit 57bb532

Browse files
fix(scan): decompress non-spec-compliant compressed deletion vectors
The Iceberg spec requires a deletion-vector-v1 blob to be stored uncompressed ('Omit compression-codec; deletion-vector-v1 is not compressed'), so reading the bytes at content_offset/content_size and parsing them directly is correct for all compliant deletion vectors (the fast path). A non-conforming writer could, however, store a compressed deletion vector blob; in that case the bytes at content_offset are compressed and the direct parse fails. Fall back to reading the blob through the Puffin footer, which records the per-blob compression-codec and decompresses accordingly (matching the blob by content_offset). Container-less blob files (no footer) stay on the fast path. The fast path is covered by the existing round-trip test and real-data validation; the compressed fallback is defensive (the spec forbids compressed DVs, so no fixture is produced by conforming writers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Raghvendra Singh <raghav@cashify.in>
1 parent 02aecba commit 57bb532

1 file changed

Lines changed: 40 additions & 1 deletion

File tree

crates/iceberg/src/arrow/caching_delete_file_loader.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use crate::delete_vector::DeleteVector;
3131
use crate::expr::Predicate::AlwaysTrue;
3232
use crate::expr::{Predicate, Reference};
3333
use crate::io::FileIO;
34+
use crate::puffin::PuffinReader;
3435
use crate::runtime::Runtime;
3536
use crate::scan::{ArrowRecordBatchStream, FileScanTaskDeleteFile};
3637
use crate::spec::{
@@ -412,7 +413,45 @@ impl CachingDeleteFileLoader {
412413
),
413414
)
414415
})?;
415-
DeleteVector::from_serialized_bytes(&bytes[start..end])
416+
417+
// Fast path: per the Iceberg spec a `deletion-vector-v1` blob is stored
418+
// uncompressed ("Omit compression-codec; deletion-vector-v1 is not
419+
// compressed"), so the bytes located by content_offset/content_size are the
420+
// serialized blob directly. This also covers container-less blob files
421+
// (e.g. written by DuckDB) that have no Puffin footer.
422+
if let Ok(delete_vector) = DeleteVector::from_serialized_bytes(&bytes[start..end]) {
423+
return Ok(delete_vector);
424+
}
425+
426+
// Fallback: a deletion vector blob that is compressed (which the spec
427+
// forbids, but a non-conforming writer could produce) can only be
428+
// decompressed using the codec recorded in the Puffin footer. If this file
429+
// is a valid Puffin file, read the blob through its footer, which honors
430+
// the per-blob compression codec.
431+
let reader = PuffinReader::new(file_io.new_input(file_path)?);
432+
let metadata = reader.file_metadata().await.map_err(|err| {
433+
Error::new(
434+
ErrorKind::DataInvalid,
435+
format!(
436+
"deletion vector at offset {content_offset} in {file_path} is neither a valid uncompressed deletion-vector-v1 blob nor a Puffin container"
437+
),
438+
)
439+
.with_source(err)
440+
})?;
441+
let blob_metadata = metadata
442+
.blobs()
443+
.iter()
444+
.find(|blob| blob.offset() == content_offset)
445+
.ok_or_else(|| {
446+
Error::new(
447+
ErrorKind::DataInvalid,
448+
format!(
449+
"Puffin file {file_path} has no blob at content_offset {content_offset}"
450+
),
451+
)
452+
})?;
453+
let blob = reader.blob(blob_metadata).await?;
454+
DeleteVector::from_puffin_blob(blob)
416455
}
417456

418457
/// Parses a record batch stream coming from positional delete files

0 commit comments

Comments
 (0)