Skip to content

Commit 8b7b394

Browse files
committed
fix: compose successive batch_slice_columns calls (#186)
batch_slice_columns memoized a single index set for the whole import, so a second call reused the first call's column positions against already-sliced rows — corrupting values instead of narrowing further (e.g. the name column received the last_name). Reset the working headers to the parsed source set at the start of each batch, so each call computes indices from the current headers and successive calls narrow the previous call's result. @source_headers holds the complete parsed header row; @headers is its per-batch working copy. Cover successive calls across one and multiple batches.
1 parent 72ed02b commit 8b7b394

2 files changed

Lines changed: 58 additions & 22 deletions

File tree

lib/active_admin_import/importer.rb

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,15 @@ def batch_replace(header_key, options)
8282
# end
8383
#
8484
def batch_slice_columns(slice_columns)
85-
# Only set @use_indexes for the first batch so that @use_indexes are in correct
86-
# position for subsequent batches
87-
unless defined?(@use_indexes)
88-
@use_indexes = []
89-
headers.values.each_with_index do |val, index|
90-
@use_indexes << index if val.in?(slice_columns)
91-
end
92-
return csv_lines if @use_indexes.empty?
93-
94-
# slice CSV headers
95-
@headers = headers.to_a.values_at(*@use_indexes).to_h
96-
end
85+
columns = headers.values
86+
indexes = columns.each_index.select { |i| columns[i].in?(slice_columns) }
87+
return csv_lines if indexes.empty?
9788

98-
# slice CSV values
99-
csv_lines.map! do |line|
100-
line.values_at(*@use_indexes)
101-
end
89+
# @headers is reset to the full set at the start of every batch (see
90+
# #batch_import), so each call narrows the previous call's result and every
91+
# batch slices the same way — calling this more than once now composes (#186).
92+
@headers = headers.to_a.values_at(*indexes).to_h
93+
csv_lines.map! { |line| line.values_at(*indexes) }
10294
end
10395

10496
def values_at(header_key)
@@ -129,17 +121,18 @@ def process_file
129121
end
130122

131123
def prepare_headers
132-
headers = self.headers.present? ? self.headers : yield
133-
blank_positions = headers.each_index.select { |i| headers[i].to_s.strip.empty? }
124+
names = self.headers.present? ? self.headers : yield
125+
blank_positions = names.each_index.select { |i| names[i].to_s.strip.empty? }
134126
unless blank_positions.empty?
135127
raise ActiveAdminImport::Exception,
136128
"blank column header at column #{blank_positions.map { |i| i + 1 }.join(', ')}"
137129
end
138130

139-
headers = headers.map(&:to_s)
140-
@headers = Hash[headers.zip(headers.map { |el| el.underscore.gsub(/\s+/, '_') })].with_indifferent_access
141-
@headers.merge!(options[:headers_rewrites].symbolize_keys.slice(*@headers.symbolize_keys.keys))
142-
@headers
131+
names = names.map(&:to_s)
132+
# @source_headers is the complete parsed header row; batch_import copies it
133+
# into the per-batch working @headers (see #batch_import).
134+
@source_headers = Hash[names.zip(names.map { |el| el.underscore.gsub(/\s+/, '_') })].with_indifferent_access
135+
@source_headers.merge!(options[:headers_rewrites].symbolize_keys.slice(*@source_headers.symbolize_keys.keys))
143136
end
144137

145138
def run_callback(name)
@@ -148,6 +141,9 @@ def run_callback(name)
148141

149142
def batch_import
150143
batch_result = nil
144+
# Every batch re-parses full-width rows, so restore the full header set
145+
# before slicing; batch_slice_columns then narrows this working copy.
146+
@headers = @source_headers.dup
151147
@resource.transaction do
152148
run_callback(:before_batch_import)
153149
batch_result = resource.import(headers.values, csv_lines, import_options)

spec/import_spec.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,46 @@ def upload_file!(name, ext = 'csv')
656656
end
657657
end
658658

659+
# Issue #186: each call must slice the result of the previous one, not re-apply
660+
# the first call's indices to an already-sliced row.
661+
context "with successive batch_slice_columns calls" do
662+
let(:batch_size) { 2 }
663+
664+
before do
665+
# validate: false isolates the slicing behaviour from the model's
666+
# uniqueness validation, which would otherwise reject the second batch's
667+
# author for sharing a NULL last_name with the first.
668+
add_author_resource template_object: ActiveAdminImport::Model.new,
669+
validate: false,
670+
before_batch_import: lambda { |importer|
671+
importer.batch_slice_columns(%w(name last_name))
672+
importer.batch_slice_columns(%w(name))
673+
},
674+
batch_size: batch_size
675+
visit "/admin/authors/import"
676+
upload_file!(:authors)
677+
end
678+
679+
shared_examples_for "only the name column kept" do
680+
it "narrows the columns progressively" do
681+
expect(Author.pluck(:name, :last_name, :birthday)).to match_array(
682+
[
683+
["Jane", nil, nil],
684+
["John", nil, nil]
685+
]
686+
)
687+
end
688+
end
689+
690+
it_behaves_like "only the name column kept"
691+
692+
context "across multiple batches" do
693+
let(:batch_size) { 1 }
694+
695+
it_behaves_like "only the name column kept"
696+
end
697+
end
698+
659699
context 'with invalid options' do
660700
let(:options) { { invalid_option: :invalid_value } }
661701

0 commit comments

Comments
 (0)