java-topology/docs/tickets/rails-0006-postgresql-schema-include-columns-array.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.3 KiB
Raw Permalink Blame History

rails-0006: PostgreSQL schema_statements — O(C×I) include_columns Array#include? in reject

Severity: MEDIUM File: activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb Line: 133-139 Status: PATCHED

Description

When loading PostgreSQL index metadata, include_columns is built as an Array by splitting and mapping the include clause. Then columns.reject! { |c| include_columns.include?(c) } scans the full include_columns Array for each column — O(C×I) where C = column count and I = include_column count.

This runs during every schema reflection call (e.g., connection.indexes(table_name)), which is invoked on every model class load and on every db:schema:dump.

Root Cause

include.split(",").map { ... } returns an Array. The subsequent Array#include? in the reject! block is O(I) per column.

Fix

# BEFORE
include_columns = include ? include.split(",").map { |c| Utils.unquote_identifier(c.strip.gsub('""', '"')) } : []
columns.reject! { |c| include_columns.include?(c) }

# AFTER
include_set = include ? include.split(",").map { |c| Utils.unquote_identifier(c.strip.gsub('""', '"')) }.to_set : Set.new
columns.reject! { |c| include_set.include?(c) }

Speedup

~50x at C=500 columns, I=200 include columns