Skip to content

Commit 9e3da71

Browse files
committed
pileup/mpileup: lazy-copy Bam::Record on first record() call
1 parent b916054 commit 9e3da71

3 files changed

Lines changed: 52 additions & 13 deletions

File tree

lib/hts/bam/pileup.rb

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,23 @@ class PileupRecord
1818
def initialize(entry, header)
1919
@entry = entry
2020
@header = header
21+
@record = nil
2122
end
2223

23-
# Lightweight read-only view over the underlying bam1_t without taking ownership.
24+
# Return Bam::Record. On the first call, duplicate the underlying bam1_t (bam_dup1)
25+
# so the record becomes safe to keep beyond the current pileup step. Subsequent calls
26+
# return the cached Bam::Record instance.
27+
# NOTE: Without duplication, bam1_t memory may be reused by HTSlib on the next step.
2428
def record
25-
@record_view ||= ReadView.new(@entry[:b])
26-
end
29+
return @record if @record
2730

28-
class ReadView
29-
def initialize(bam1_ptr)
30-
# Accept either FFI::Pointer or FFI::Struct (Managed/Unmanaged); store as raw pointer
31-
@bam1_ptr = bam1_ptr.is_a?(FFI::Pointer) ? bam1_ptr : bam1_ptr.to_ptr
32-
end
31+
# Normalize to a raw pointer and duplicate to obtain owned memory.
32+
b_ptr = @entry[:b].is_a?(FFI::Pointer) ? @entry[:b] : @entry[:b].to_ptr
33+
dup_ptr = HTS::LibHTS.bam_dup1(b_ptr)
34+
raise "bam_dup1 failed" if dup_ptr.null?
3335

34-
def base(i)
35-
view = HTS::LibHTS::Bam1View.new(@bam1_ptr)
36-
seq_ptr = HTS::LibHTS.bam_get_seq(view)
37-
HTS::Bam::Record::SEQ_NT16_STR[HTS::LibHTS.bam_seqi(seq_ptr, i)]
38-
end
36+
# Build a Bam::Record backed by the duplicated bam1_t.
37+
@record = HTS::Bam::Record.new(@header, dup_ptr)
3938
end
4039

4140
def query_position

test/bam/mpileup_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,26 @@ def test_multipileup_accepts_bam_instances
4343
b2.close
4444
end
4545
end
46+
47+
def test_multipileup_record_lazy_copy
48+
inputs = [Fixtures["moo.bam"], Fixtures["moo.bam"]]
49+
mp = HTS::Bam::Mpileup.new(inputs)
50+
begin
51+
kept = nil
52+
mp.each do |columns|
53+
# find first column with at least one alignment in any input
54+
col = columns.find { |c| !c.alignments.empty? }
55+
next unless col
56+
57+
kept = col.alignments.first.record
58+
break
59+
end
60+
refute_nil kept
61+
# Should be usable after iteration moved on
62+
assert_kind_of Integer, kept.len
63+
assert_includes %w[= A C G T M R S V W Y H K D B N], kept.base(0)
64+
ensure
65+
mp.close
66+
end
67+
end
4668
end

test/bam/pileup_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,22 @@ def test_pileup_basic_each
3939
assert_operator count, :>, 0
4040
end
4141
end
42+
43+
def test_pileup_record_persists_beyond_step
44+
HTS::Bam.open(Fixtures["moo.bam"]) do |bam|
45+
kept = nil
46+
# take a record from the first non-empty column and keep it
47+
bam.pileup do |col|
48+
next if col.alignments.empty?
49+
50+
kept = col.alignments.first.record
51+
break
52+
end
53+
refute_nil kept
54+
# Access after the pileup iterator has moved on; should be safe
55+
assert_kind_of String, kept.qname
56+
# Access a base in the read sequence
57+
assert_includes %w[= A C G T M R S V W Y H K D B N], kept.base(0)
58+
end
59+
end
4260
end

0 commit comments

Comments
 (0)