Skip to content

Commit f4b6d38

Browse files
committed
Fix loaded relation batching for models with composite primary keys
1 parent f74dc0d commit f4b6d38

File tree

2 files changed

+46
-11
lines changed

2 files changed

+46
-11
lines changed

activerecord/lib/active_record/relation/batches.rb

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -338,23 +338,17 @@ def get_the_order_of_primary_key(order)
338338

339339
def batch_on_loaded_relation(relation:, start:, finish:, order:, batch_limit:)
340340
records = relation.to_a
341+
order = build_batch_orders(order).map(&:second)
341342

342343
if start || finish
343344
records = records.filter do |record|
344-
id = record.id
345-
346-
if order == :asc
347-
(start.nil? || id >= start) && (finish.nil? || id <= finish)
348-
else
349-
(start.nil? || id <= start) && (finish.nil? || id >= finish)
350-
end
345+
(start.nil? || compare_values_for_order(record.id, start, order) >= 0) &&
346+
(finish.nil? || compare_values_for_order(record.id, finish, order) <= 0)
351347
end
352348
end
353349

354-
records.sort_by!(&:id)
355-
356-
if order == :desc
357-
records.reverse!
350+
records.sort! do |record1, record2|
351+
compare_values_for_order(record1.id, record2.id, order)
358352
end
359353

360354
records.each_slice(batch_limit) do |subrecords|
@@ -367,6 +361,28 @@ def batch_on_loaded_relation(relation:, start:, finish:, order:, batch_limit:)
367361
nil
368362
end
369363

364+
# This is a custom implementation of `<=>` operator,
365+
# which also takes into account how the collection will be ordered.
366+
def compare_values_for_order(value1, value2, order)
367+
# Multiple column values.
368+
if value1.is_a?(Array)
369+
value1.each_with_index do |element1, index|
370+
element2 = value2[index]
371+
direction = order[index]
372+
comparison = element1 <=> element2
373+
comparison = -comparison if direction == :desc
374+
return comparison if comparison != 0
375+
end
376+
377+
0
378+
# Single column values.
379+
elsif order.first == :asc
380+
value1 <=> value2
381+
else
382+
value2 <=> value1
383+
end
384+
end
385+
370386
def batch_on_unloaded_relation(relation:, start:, finish:, load:, order:, use_ranges:, remaining:, batch_limit:)
371387
batch_orders = build_batch_orders(order)
372388
relation = relation.reorder(batch_orders.to_h).limit(batch_limit)

activerecord/test/cases/batches_test.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,25 @@ def test_in_batches_when_loaded_can_return_an_enum
538538
assert_equal posts.size, batch_count
539539
end
540540

541+
def test_in_batches_when_loaded_runs_no_queries_when_batching_over_cpk_model
542+
incorrectly_sorted_orders = Cpk::Order.order(shop_id: :asc, id: :desc)
543+
incorrectly_sorted_orders.load
544+
545+
correctly_sorted_orders = Cpk::Order.order(shop_id: :desc, id: :asc).to_a
546+
expected_orders = correctly_sorted_orders[1..-2]
547+
start_id = expected_orders.first.id
548+
finish_id = expected_orders.last.id
549+
orders = []
550+
551+
assert_no_queries do
552+
incorrectly_sorted_orders.find_each(batch_size: 1, start: start_id, finish: finish_id, order: [:desc, :asc]) do |order|
553+
orders << order
554+
end
555+
end
556+
557+
assert_equal expected_orders, orders
558+
end
559+
541560
def test_in_batches_should_return_relations
542561
assert_queries_count(@total + 1) do
543562
Post.in_batches(of: 1) do |relation|

0 commit comments

Comments
 (0)