37 lines
1.8 KiB
Diff
37 lines
1.8 KiB
Diff
# UNDF: UNDF-2026-000000257
|
||
Fixes rails-0018: CollectionAssociation#find_by_scan — ids Array#include? inside load_target.select O(T×I).
|
||
|
||
--- a/activerecord/lib/active_record/associations/collection_association.rb
|
||
+++ b/activerecord/lib/active_record/associations/collection_association.rb
|
||
|
||
@@ DEFECT rails-0018: lines 524-531 find_by_scan
|
||
|
||
def find_by_scan(*args)
|
||
expects_array = args.first.kind_of?(Array)
|
||
ids = args.flatten.compact.map(&:to_s).uniq
|
||
|
||
if ids.size == 1
|
||
id = ids.first
|
||
record = load_target.detect { |r| id == r.id.to_s }
|
||
expects_array ? [ record ] : record
|
||
else
|
||
- load_target.select { |r| ids.include?(r.id.to_s) } # ids is Array — O(I) per record
|
||
+ ids_set = ids.to_set # FIX: build Set once O(I)
|
||
+ load_target.select { |r| ids_set.include?(r.id.to_s) } # O(1) per record
|
||
end
|
||
end
|
||
|
||
# BEFORE: ids is Array (from args.flatten.compact.map(&:to_s).uniq — Array#uniq returns Array)
|
||
# load_target.select iterates T records; each calls ids.include? which is O(I) Array scan
|
||
# Total: O(T × I) where T = association target size, I = number of requested ids
|
||
#
|
||
# AFTER: ids_set = ids.to_set — one-time O(I) build
|
||
# ids_set.include? is O(1) hash lookup
|
||
# Total: O(T + I)
|
||
#
|
||
# Severity: MEDIUM — triggered by collection.find([id1, id2, ...]) when target is loaded in memory
|
||
# In deeply nested eager-loaded associations, T can be thousands of records,
|
||
# and I can be dozens. E.g., find 50 records from a 2000-element loaded association = 100,000 ops
|
||
#
|
||
# Speedup: ~I× where I = number of ids searched
|
||
# At T=2000 records, I=100 ids: 200,000 → 2,100 ops — ~95x improvement
|