fix(cbq): correct silent N to A corruption on CBQ decode (#94)#95
Closed
BenjaminDEMAILLE wants to merge 3 commits into
Closed
fix(cbq): correct silent N to A corruption on CBQ decode (#94)#95BenjaminDEMAILLE wants to merge 3 commits into
BenjaminDEMAILLE wants to merge 3 commits into
Conversation
CBQ stores N bases as a 2-bit placeholder A plus an Elias-Fano index of the N-positions that is backfilled on decode. In the serial Reader path, decompress_columns pre-filled ef_bytes with `resize(len_nef, 0)` and then called copy_decode, whose io::Write for Vec<u8> appends. The serialized EliasFano bytes therefore landed after len_nef zero bytes, so deserialize_from read the leading zeros and produced an empty index (num_ones == 0). backfill_npos then panicked in debug (sucds debug_assert) or silently no-oped in release, leaving every N decoded as A. Clear ef_bytes before copy_decode so the buffer holds exactly the decompressed bytes, mirroring the headers/qual columns. Also reset len_nef in ColumnarBlock::clear() for consistent block reuse. The mmap process_parallel path (dctx.decompress writes at offset 0) was already correct, and the on-disk bytes are unchanged, so no format bump is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Existing CBQ tests were ACGT-only, which is why the N-decode bug shipped. Add coverage for N at leading, interior, trailing, and all-N positions, across single and multiple blocks, with and without quality plus headers, over both the serial Reader path and the parallel MmapReader / process_parallel path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request fixes a bug (issue #94) where N-nucleotides were silently dropped during decoding. Instead of resizing self.ef_bytes with zeros (which caused subsequent decodes to append and fail to reconstruct the Elias-Fano index), the code now clears self.ef_bytes before decoding. Additionally, self.len_nef is now properly reset when clearing the block, and comprehensive serial and parallel round-trip tests have been added. The reviewer suggested a performance improvement to pre-allocate capacity for self.ef_bytes using reserve after clearing it, avoiding unnecessary reallocations during decompression.
Collaborator
|
done with #96 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #94.
Problem
A
.cbqwritten withNbases does not round-trip: on decode everyNcomes back asA. In debug builds this panics inside sucds (elias_fano/iter.rs:23,debug_assert_ne!(num_ones, 0)); in the shipped release config it is silent, returning a realAwhere the read had a no-callN. For methylation/variant consumers this is a silent correctness violation.Root cause
CBQ stores
Nas a 2-bit placeholderAplus a sucdsEliasFanoindex of the N-positions, backfilled on decode. In the serialReaderpath,ColumnarBlock::decompress_columns()did:copy_decode's destination is a&mut Vec<u8>, whoseio::Writeimpl appends. Soef_bytesbecame[0u8; len_nef] ++ [real EF bytes], andEliasFano::deserialize_fromread the leading zeros, producing an empty index (num_ones() == 0).backfill_npos()then panicked in debug or silently no-oped in release, leaving the placeholderAs.This is not a sucds bug: its serialize/deserialize are symmetric. The other columns are fine because numeric columns decode into
cast_slice_mut(&mut vec)(an in-place&mut [u8]), andheaders/qualappend into an already-cleared buffer with noresize. Onlyef_bytesdid bothresize(_, 0)and append.The mmap /
process_parallelpath (decompress_from_bytes, usingdctx.decompress) writes at offset 0 via zstd-safe'sWriteBuf, so it was already correct.Fix
Clear
ef_bytesbeforecopy_decodeso it holds exactly the decompressed bytes, mirroring theheaders/qualcolumns. Also resetlen_nefinColumnarBlock::clear()for consistent block reuse.No on-disk format change:
z_nposandlen_nefare written correctly and already read back correctly by the mmap path, soFILE_VERSIONis unchanged and existing files (N-free or not) are unaffected.Tests
Existing CBQ tests were ACGT-only, which is why this shipped. Added N-nucleotide round-trip coverage (leading / interior / trailing / all-N, single and multi-block, with and without quality+headers) over both the serial
Readerpath and the parallelMmapReader/process_parallelpath. Verified these new tests fail (serial) on the pre-fix code and pass after; full suite green in debug and release; the exact issue repro printsNACGTACGT,NNNNNNNN,ACGTNACGTunchanged in both profiles.🤖 Generated with Claude Code