Skip to content

fix: tolerate duplicate VCF header definitions#17

Merged
tfenne merged 1 commit into
mainfrom
nh/vcf-tolerate-duplicate-headers
Jun 15, 2026
Merged

fix: tolerate duplicate VCF header definitions#17
tfenne merged 1 commit into
mainfrom
nh/vcf-tolerate-duplicate-headers

Conversation

@nh13

@nh13 nh13 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

simulate/methylate aborted on any VCF that redeclares a header ID with duplicate INFO ID: BREAKSIMLENGTH (or the FORMAT/FILTER/ALT/contig equivalent). Such duplicates are routine in output from Manta, DELLY, and hand-merged files, and bcftools reads them without complaint — holodeck shouldn't be stricter than bcftools.

Root cause: noodles' header parser rejects redeclared ##INFO/##FORMAT/##FILTER/##ALT/##contig IDs outright, and parse_variants_by_contig / validate_vcf_sample fed the file straight to it via build_from_path.

Fix

  1. New open_lenient_vcf_reader: read the raw header before handing it to noodles, drop every repeated structured definition (keeping the first, matching bcftools), then re-parse the cleaned header. The record stream is untouched — only the header is rewritten. simulate and methylate both read through it.
  2. Compression is now detected from the leading magic bytes (1f 8b) rather than the file extension, reusing the same MultiGzDecoder-based opener the methylation reader already used. As a side benefit the variant reader now also accepts plain-gzip and extension-less inputs — previously it was BGZF-and-.gz-only.
  3. That opener (open_vcf_buf_reader) moves from vcf::methylation up to vcf so both readers share one copy instead of duplicating the peek-and-decompress logic.

Test plan

  • cargo ci-fmt clean
  • cargo ci-lint clean (clippy pedantic, -D warnings)
  • cargo ci-test — 422 tests pass, including new coverage:
    • parse_variants_by_contig tolerates duplicate ##INFO, and duplicate ##FORMAT/##FILTER/##contig
    • validate_vcf_sample tolerates duplicate definitions
    • structured_header_key unit test: directive/ID extraction, no-ID lines skipped, ID-not-first conservatively skipped
    • all existing vcf / methylate / simulate tests green (the shared opener move is covered by the methylation suite)
  • Manual CLI repro: the exact ##INFO=<ID=BREAKSIMLENGTH,...> duplicate that produced duplicate INFO ID: BREAKSIMLENGTH now runs to completion (simulate -v dup.vcf → 33 read pairs)

Summary by CodeRabbit

  • Bug Fixes
    • VCF files with duplicate header definitions now parse successfully, keeping the first definition and discarding repeats instead of erroring.
    • Compression detection now uses file magic bytes, allowing extension-less and plain gzip-compressed files to be processed correctly.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds lenient VCF header parsing to src/vcf/mod.rs by introducing open_vcf_buf_reader (magic-byte compression detection via MultiGzDecoder) and open_lenient_vcf_reader (deduplicates repeated structured ##INFO/##FORMAT/##FILTER/##ALT/##contig IDs). Both parse_variants_by_contig and validate_vcf_sample are switched to the lenient path. The local open_vcf_buf_reader helper in methylation.rs is removed in favor of the centralized crate-level function.

Changes

Lenient VCF Header Parsing and Compression Detection

Layer / File(s) Summary
open_vcf_buf_reader and open_lenient_vcf_reader implementation
src/vcf/mod.rs
Adds magic-byte-based gzip/BGZF detection (open_vcf_buf_reader) and a raw-header-deduplication path (open_lenient_vcf_reader) using structured_header_key to drop repeated structured meta lines while replaying a cleaned header into noodles. Adds HashSet and Read imports.
Wire entry points to lenient reader
src/vcf/mod.rs
Switches parse_variants_by_contig and validate_vcf_sample to call open_lenient_vcf_reader; updates the path doc comment to reflect magic-byte autodetection.
Centralize buf-reader, remove local duplicate
src/vcf/methylation.rs
Deletes the local open_vcf_buf_reader helper and updates vcf_has_mt_mb_records and parse_methylation_vcf to call crate::vcf::open_vcf_buf_reader.
Tests and changelog
src/vcf/mod.rs, CHANGELOG.md
Adds unit tests for duplicate-header tolerance (parsing and sample validation) and structured_header_key extraction. Documents both fixes in CHANGELOG.md under Unreleased/Fixed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • fg-labs/holodeck#2: Calls validate_vcf_sample from simulate, which this PR modifies to use the new lenient header parsing path.
  • fg-labs/holodeck#3: Also modifies validate_vcf_sample in src/vcf/mod.rs to return the resolved sample name, overlapping directly with this PR's changes to the same function.

Poem

🐇 A header appeared twice in the VCF file,
Old noodles would error — but now, with a smile,
We peek at the bytes for a gzip disguise,
Deduplicate IDs with our own sharp eyes,
The first one is kept, the rest we discard,
No extension required — magic bytes stand guard!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: making VCF header parsing more tolerant of duplicate structured definitions, which is the core objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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 and usage tips.

noodles' header parser rejects any redeclared `##INFO`/`##FORMAT`/`##FILTER`/
`##ALT`/`##contig` ID outright (`duplicate INFO ID: BREAKSIMLENGTH`), so
`simulate`/`methylate` choked on VCFs that bcftools reads without complaint --
duplicates are routine in output from Manta, DELLY, and hand-merged files.
holodeck shouldn't be stricter than bcftools.

Read the raw header before handing it to noodles and drop every repeated
structured definition, keeping the first (matching bcftools), then re-parse
the cleaned header. The record stream is untouched. Compression is detected
from the leading magic bytes instead of the file extension, reusing the same
opener the methylation reader already used, so the variant reader now also
accepts plain-gzip and extension-less inputs. That opener moves from
`vcf::methylation` to `vcf` so both readers share one copy.
@nh13
nh13 force-pushed the nh/vcf-tolerate-duplicate-headers branch from 7b9cf6d to 68919c5 Compare June 15, 2026 09:16
@nh13

nh13 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 15, 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/vcf/mod.rs (1)

503-523: ⚡ Quick win

Add a dedicated duplicate-##ALT tolerance test.

You cover INFO/FORMAT/FILTER/contig, but ALT is also part of the supported duplicate-structured-header contract and should be pinned by test to prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vcf/mod.rs` around lines 503 - 523, Add a new dedicated test function to
verify that duplicate ##ALT declarations are tolerated by the parser. Create a
test similar to the existing
parse_variants_by_contig_tolerates_duplicate_format_filter_contig function, but
include duplicate ##ALT header entries in the VCF string (e.g., two ##ALT
declarations with the same ID), and assert that the parsing succeeds without
errors. This ensures that ##ALT duplicate tolerance is explicitly covered by
tests alongside the already-tested ##INFO, ##FORMAT, ##FILTER, and ##contig
duplicate handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vcf/mod.rs`:
- Around line 243-247: The deduplication logic in the is_meta condition block is
too broad and will drop any structured metadata line matching the ##...=<ID=...>
pattern, not just the intended structured directives. Modify the logic to first
check if the line contains one of the targeted directive types before attempting
deduplication, so that unrelated structured header definitions are preserved.
Apply this same constraint to all affected deduplication checks (including the
additional occurrences noted in the review).

---

Nitpick comments:
In `@src/vcf/mod.rs`:
- Around line 503-523: Add a new dedicated test function to verify that
duplicate ##ALT declarations are tolerated by the parser. Create a test similar
to the existing
parse_variants_by_contig_tolerates_duplicate_format_filter_contig function, but
include duplicate ##ALT header entries in the VCF string (e.g., two ##ALT
declarations with the same ID), and assert that the parsing succeeds without
errors. This ensures that ##ALT duplicate tolerance is explicitly covered by
tests alongside the already-tested ##INFO, ##FORMAT, ##FILTER, and ##contig
duplicate handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 846ca6fe-6238-44af-bb75-7f3d79c6a8ee

📥 Commits

Reviewing files that changed from the base of the PR and between 07444d0 and 68919c5.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/vcf/methylation.rs
  • src/vcf/mod.rs

Comment thread src/vcf/mod.rs
Comment on lines +243 to +247
if is_meta
&& let Some(key) = structured_header_key(&line)
&& !seen.insert(key.clone())
{
log::warn!("dropping duplicate VCF header definition: ##{}=<ID={},...>", key.0, key.1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Constrain deduplication to the intended structured directives.

Current logic deduplicates any ##...=<ID=...> line, which can silently drop unrelated structured metadata. Limit dedup to the targeted directive set so non-target header semantics are preserved.

💡 Proposed fix
-        if is_meta
-            && let Some(key) = structured_header_key(&line)
-            && !seen.insert(key.clone())
-        {
+        if is_meta
+            && let Some(key) = structured_header_key(&line)
+            && (matches!(key.0.as_str(), "INFO" | "FORMAT" | "FILTER" | "ALT")
+                || key.0.eq_ignore_ascii_case("contig"))
+            && !seen.insert(key.clone())
+        {
             log::warn!("dropping duplicate VCF header definition: ##{}=<ID={},...>", key.0, key.1);
             continue;
         }

Also applies to: 294-307

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vcf/mod.rs` around lines 243 - 247, The deduplication logic in the
is_meta condition block is too broad and will drop any structured metadata line
matching the ##...=<ID=...> pattern, not just the intended structured
directives. Modify the logic to first check if the line contains one of the
targeted directive types before attempting deduplication, so that unrelated
structured header definitions are preserved. Apply this same constraint to all
affected deduplication checks (including the additional occurrences noted in the
review).

@tfenne tfenne left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks!

@tfenne
tfenne merged commit 7b36fd5 into main Jun 15, 2026
5 checks passed
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.

2 participants