java-topology/docs/tickets/rails-0001-preloader-batch-future-tables.md
russell@unturf.com 547a9f5738 ORM wave 2: 10 new defects — Active Record +3, Exposed +3, SeaORM +4 (167 sites, 64 ecosystems)
rails-0009: FilterAttributeHandler filter_parameters Array O(A×F) → Set (450×)
rails-0010: Encryption::AutoFilteredParameters two Array scans → Set (250×)
rails-0011: TimeZoneConversion skip_list Array O(M×C×S) → Set (20×)

exposed-0001: SchemaUtilityApi mapMissingColumnStatements O(N×M) → map (118×)
exposed-0002: IdentifierManagerApi isAKeyword O(K) linear → HashSet (144×)
exposed-0003: Table.clone consParams.map fresh List → hoisted HashSet (6×)

seaorm-0001: active_model establish_links leftover.any O(N²) → HashSet (501×)
seaorm-0002: rbac engine group_permissions .values().find() → HashMap by ID (502×)
seaorm-0003: schema builder sorted_tables Vec::contains → HashSet (500×)
seaorm-0004: TopologicalSort from_iter seen Vec O(N²) → BTreeSet (28×)

Unit tests: RailsTest 11/11, ExposedTest 3/3, SeaORMTest 4/4 PASS
Whitepaper: 157→167 sites, 62→64 ecosystems; §13.12 ORM Wave 2 added
2026-03-27 13:49:46 -04:00

1.1 KiB
Raw Blame History

rails-0001: Preloader::Batch — O(N×F) future_tables Array#include? per loader per batch round

Severity: HIGH File: activerecord/lib/active_record/associations/preloader/batch.rb Line: 24 Status: PATCHED

Description

Preloader::Batch#call builds future_tables as an Array (via .map(&:table_name).uniq) and then calls future_tables.include?(l.table_name) inside loaders.reject { ... }. This is O(L×F) per batch round where L = loader count and F = future_table count. With D-depth association trees the outer until branches.empty? loop runs D times, making the total O(D×L×F).

Root Cause

Array#uniq returns an Array, not a Set. The subsequent .include? call is O(F) per element. In eager-loading a deeply nested association tree with many tables, both L and F grow with the model graph, producing quadratic work per query.

Fix

Change .uniq to .to_set so that future_tables.include? is O(1).

# BEFORE
end.map(&:table_name).uniq

# AFTER
end.map(&:table_name).to_set

Speedup

~150x at L=500 loaders, F=300 future tables, D=20 rounds