From f628197cfa07ce8215757d5916220f23539f6f1b Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:31:41 -0700 Subject: [PATCH 1/4] tests: added testing for sequential reader and N-sequences --- src/cbq/write.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/cbq/write.rs b/src/cbq/write.rs index 6301f7d..8ff0b53 100644 --- a/src/cbq/write.rs +++ b/src/cbq/write.rs @@ -427,6 +427,54 @@ mod tests { Ok(()) } + /// Sequences containing `N`s, which drive the Elias-Fano `npos` column. + /// Blocks with no `N`s at all never populate `z_npos`, so a suite built + /// only from `sample_sequences` (all ACGT) never exercises this path. + fn sample_sequences_with_n(n_seq: usize, seq_len: usize) -> Vec> { + const BASES: [u8; 4] = [b'A', b'C', b'G', b'T']; + (0..n_seq) + .map(|i| { + (0..seq_len) + .map(|j| { + if i % 5 == 0 { + b'N' + } else { + BASES[(i as usize + j) % 4] + } + }) + .collect::>() + }) + .collect() + } + + /// Round-trips sequences containing `N`s through a `ColumnarBlockWriter` + /// and back through a `Reader`, exercising the Elias-Fano `npos` + /// decompression path in [`ColumnarBlock::decompress_columns`]. + #[test] + fn test_roundtrip_sequences_with_n() -> Result<()> { + // Small block size so N-bearing sequences span multiple blocks. + let block_size = 256; + let mut writer = ColumnarBlockWriter::new(Vec::new(), header(block_size))?; + + let seqs = sample_sequences_with_n(1024, 100); + assert!( + seqs.iter().any(|s| s.contains(&b'N')), + "test fixture must actually contain N's to exercise npos" + ); + for seq in &seqs { + writer.push(record(seq))?; + } + writer.finish()?; + + let read_back = read_all_sequences(writer.inner); + assert_eq!( + read_back, seqs, + "round-trip mismatch for N-bearing sequences" + ); + + Ok(()) + } + /// `ingest_completed` on a source with no completed blocks is a no-op for /// the global writer and preserves the source's incomplete block. #[test] From e1ca0dd2bff541e24966fe451e79347374f8e622 Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:31:57 -0700 Subject: [PATCH 2/4] fix: use reserve instead of resize --- src/cbq/core/block.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cbq/core/block.rs b/src/cbq/core/block.rs index 96b75b9..c434a6b 100644 --- a/src/cbq/core/block.rs +++ b/src/cbq/core/block.rs @@ -432,6 +432,11 @@ impl ColumnarBlock { } /// Decompress all columns back to native representation + /// + /// Note: `resize` can be only be used with `copy_decode` if passing + /// as `&mut [T]`. Passing a resized `&mut Vec` will lead to an + /// append operation, not an overwrite. If passing `&mut Vec`, the + /// `Vec` will be resized automatically by `copy_decode`. pub fn decompress_columns(&mut self) -> Result<()> { // decompress sequence lengths { @@ -450,7 +455,7 @@ impl ColumnarBlock { // decompress npos if !self.z_npos.is_empty() { - self.ef_bytes.resize(self.len_nef, 0); + self.ef_bytes.reserve(self.len_nef); copy_decode(self.z_npos.as_slice(), &mut self.ef_bytes)?; let ef = EliasFano::deserialize_from(self.ef_bytes.as_slice())?; From c88f36b2f548fdd4d49be7aa86197f0a657d4dcc Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:33:42 -0700 Subject: [PATCH 3/4] chore(semver): bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8c10bdb..6e31503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "binseq" -version = "0.9.2" +version = "0.9.3" edition = "2024" description = "A high efficiency binary format for sequencing data" license = "MIT" From bb749baa35b2f7e45792b18fd332f72548870955 Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:41:02 -0700 Subject: [PATCH 4/4] chore: ensure len_nef is cleared --- src/cbq/core/block.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cbq/core/block.rs b/src/cbq/core/block.rs index c434a6b..fbfe4fc 100644 --- a/src/cbq/core/block.rs +++ b/src/cbq/core/block.rs @@ -112,6 +112,7 @@ impl ColumnarBlock { self.num_records = 0; self.current_size = 0; self.num_npos = 0; + self.len_nef = 0; } // clear spans