Skip to content

Build the CSI index of an existing BGZF BCF through IndexFactory - #1937

Merged
tfenne merged 2 commits into
masterfrom
tf_bcf_csi_index_factory
Oct 21, 2026
Merged

tfenne merged 2 commits into
masterfrom
tf_bcf_csi_index_factory

Conversation

@tfenne

@tfenne tfenne commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

IndexFactory.createIndex(path, codec, IndexType.CSI, dictionary) now builds the CSI index of an existing BGZF-compressed BCF, so a BCF written without INDEX_ON_THE_FLY, or by bcftools, can be indexed without bcftools.

  • Before, a BCF had a CSI only if htsjdk wrote one on the fly. IndexFactory's CSI path needs a text codec and failed with "Binary codecs does not support tabix" (found by GATK's IndexFeatureFile tests on 6.0).
  • IndexFactory recognises a BCF by its contents, since callers may wrap the codec. BCFFileReader.createCsiIndex() reads the records into the index the writer builds on the fly. The new BCFCsiIndex writes it as the bare CSI bcftools index writes.
  • IndexType.TABIX is refused for a BCF, as htslib refuses it. IndexType.CSI is refused for a BCF that is not BGZF-compressed. A file that can't be read still fails with a TribbleException, as before.
  • The reader and the writer now start the index through one helper, BCFCsiIndex.binningIndexBuilderFor(VCFHeader). This fixes two bugs in on-the-fly indexing, which is new in 6.0. Both failed before; both now match bcftools:
    • A header whose contig lines give no length= got an index reaching only 131,072 bases. It now reaches 2^31 - 1, as htslib does.
    • A record at POS 0 (a telomere) is indexed at 1, as htslib indexes it.
  • getFeatureReader(resource, codec, Index) now refuses a TBI or CSI index given in memory (TabixIndex or BCFCsiIndex). It always builds a TribbleIndexedFeatureReader, which reads their BGZF virtual offsets as file offsets. A BCF query then found nothing, and a bgzipped VCF query through an in-memory TBI failed with a parse error on compressed bytes. The message points to the overloads that take an index path, which pick the right reader. Nothing in GATK, Picard or IGV uses this overload.
  • Error messages that told users to run bcftools index now also name IndexFactory. IndexFactory.loadIndex on a bare CSI says that such an index is read through the file it indexes.
  • Checked against bcftools index --csi (1.24): the indexes are equal (BinningIndex.equals). Inputs: htsjdk-written BCFs with block-aligned records, long END/SVLEN records, empty and sparse contigs, 200 KB records, contigs without lengths, POS 0, and bcftools' own vqsr.bcf (131,821 records, 0.9 s). Tests compare with bcftools when it is installed.
  • Known gaps:
    • Records must follow the header's contig order. htslib takes contigs in any order, as long as each contig's records are together. Allowing that would change BinningIndex.Builder, which BAM, CRAM and tabix share.
    • When the highest-numbered contigs of a sparse-IDX header have no records, n_ref differs from bcftools'. Queries are unaffected.
  • CHANGELOG: new VCF bullet.

Reviewed with Sonnet and Opus.

Summary by CodeRabbit

  • New Features
    • Added CSI indexing support for existing BGZF-compressed BCF files, including index creation and region-based queries.
    • CSI indexes can also be generated while writing BCF files, including for records at position zero and contigs without declared lengths.
  • Bug Fixes
    • Improved error messages for unsupported index and input combinations, and clarified that CSI indexes without tabix headers must be read alongside their indexed files.

htsjdk wrote a BCF's CSI only while writing it, with INDEX_ON_THE_FLY,
and IndexFactory's CSI path went through TabixIndexCreator, which needs
a text codec, so a BCF written without the option or by bcftools could
not be indexed ("Binary codecs does not support tabix"). IndexFactory
now builds a BGZF BCF's CSI, recognising the BCF by its contents since
callers may wrap the codec: BCFFileReader.createCsiIndex() reads the
records and builds the index BCF2Writer builds while writing, and the
new BCFCsiIndex writes it as the bare CSI bcftools index writes. TABIX
is refused for a BCF, as htslib refuses it, and CSI for a raw BCF. A
file that cannot be read still fails as the other formats' indexer
reports it, with a TribbleException.

The reader and the writer now start the index the same way, through
BCFCsiIndex.binningIndexBuilderFor, which gives a header whose contig
lines carry no lengths htslib's reach of 2^31 - 1 bases rather than
2^17, and both index a record at POS 0 at 1, as htslib does; either
used to fail. getFeatureReader refuses a BCFCsiIndex given in memory,
whose virtual offsets TribbleIndexedFeatureReader would read as file
offsets and so find nothing.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 71043726-1584-4ebf-b360-5d0eb502ba87
📥 Commits

Reviewing files that changed from the base of the PR and between 39fac6a and 773879e.

📒 Files selected for processing: 13
  • CHANGELOG.md
  • src/main/java/htsjdk/tribble/AbstractFeatureReader.java
  • src/main/java/htsjdk/tribble/index/IndexFactory.java
  • src/main/java/htsjdk/tribble/index/tabix/TabixIndex.java
  • src/main/java/htsjdk/variant/bcf2/BCF2Codec.java
  • src/main/java/htsjdk/variant/bcf2/BCFCsiIndex.java
  • src/main/java/htsjdk/variant/bcf2/BCFFileReader.java
  • src/main/java/htsjdk/variant/variantcontext/writer/BCF2Writer.java
  • src/test/java/htsjdk/tribble/AbstractFeatureReaderTest.java
  • src/test/java/htsjdk/tribble/index/IndexFactoryTest.java
  • src/test/java/htsjdk/variant/bcf2/BCF2WriterUnitTest.java
  • src/test/java/htsjdk/variant/bcf2/BCFCsiIndexTest.java
  • src/test/java/htsjdk/variant/bcf2/BCFFileReaderTest.java

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

📝 Walkthrough

This change adds CSI index support for BGZF-compressed BCF files. Callers can create indexes for existing files or write them on the fly. The reader scans and validates record order, and AbstractFeatureReader rejects in-memory TBI and CSI indexes.

Changes: BCF CSI indexing

Layer / File(s) Summary
CSI index representation
src/main/java/htsjdk/variant/bcf2/BCFCsiIndex.java, src/test/java/htsjdk/variant/bcf2/BCFCsiIndexTest.java
Adds CSI geometry, contig mappings, block lookup, and index serialization. Tests cover region queries, index placement, dictionary order, and unknown contigs.
On-the-fly BCF CSI writing
src/main/java/htsjdk/variant/variantcontext/writer/BCF2Writer.java, src/test/java/htsjdk/variant/bcf2/BCF2WriterUnitTest.java
Uses shared CSI geometry and indexes records at position zero using position one. Tests cover contigs without declared lengths and position-zero records.
CSI creation for existing BCF
src/main/java/htsjdk/tribble/index/IndexFactory.java, src/main/java/htsjdk/variant/bcf2/BCFFileReader.java, src/main/java/htsjdk/variant/bcf2/BCF2Codec.java, src/test/java/htsjdk/tribble/index/IndexFactoryTest.java, src/test/java/htsjdk/variant/bcf2/BCFFileReaderTest.java, CHANGELOG.md
Routes CSI requests for BGZF BCF through BCFFileReader, which scans records and checks their order. Tests cover unsupported input, interoperability, contigs without lengths, and position-zero records. The changelog and codec guidance describe CSI creation.
File-backed index use
src/main/java/htsjdk/tribble/AbstractFeatureReader.java, src/main/java/htsjdk/tribble/index/tabix/TabixIndex.java, src/test/java/htsjdk/tribble/AbstractFeatureReaderTest.java
Rejects in-memory TBI and CSI indexes in AbstractFeatureReader. Updates the error message for CSI files without tabix headers.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant IndexFactory
  participant BCFFileReader
  participant BCFCsiIndex
  Caller->>IndexFactory: createIndex for CSI
  IndexFactory->>BCFFileReader: Open BGZF BCF and create CSI
  BCFFileReader->>BCFFileReader: Scan and decode records
  BCFFileReader-->>IndexFactory: Return BCFCsiIndex
  IndexFactory-->>Caller: Return index
Loading
Merge Risk: ⚪ Minimal · up to 77387

No confirmed issue prevents merging after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 66 functions across 12 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: building a CSI index for an existing BGZF-compressed BCF through IndexFactory.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 66 functions across 12 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

getFeatureReader(resource, codec, Index) always builds a
TribbleIndexedFeatureReader, which takes an index's blocks for file
offsets. A TabixIndex's blocks, like a BCFCsiIndex's, are BGZF virtual
offsets, so a query of a bgzipped VCF through a TBI given this way fed
compressed bytes to the codec and failed with a parse error. Both are
now refused with a message saying to open the file with the index's
path, which the other getFeatureReader overloads route to the right
reader.
@tfenne

tfenne commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@tfenne
tfenne merged commit 7b10809 into master Oct 21, 2026
4 checks passed
@tfenne
tfenne deleted the tf_bcf_csi_index_factory branch October 21, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant