java-topology/docs/tickets/exposed-0003-Table-clone-consParamNames-O2.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

2 KiB
Raw Blame History

exposed-0003: Table.clone — O(N²) repeated List allocation in property filter

Severity: MEDIUM File: exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/Table.kt Line: 1686 Status: PATCHED

Description

The private T.clone() utility in Table is used internally when cloning column objects (e.g., during alias creation and column type mutation). It contains:

val allValues = memberProperties
    .filter { it in mutableProperties || it.name in consParams.map(KParameter::name) }
    .associate { it.name to (replaceArgs[it] ?: it.get(this@clone)) }

The predicate it.name in consParams.map(KParameter::name) is evaluated for every element of memberProperties. Each evaluation calls .map(KParameter::name), allocating a fresh List<String?>. For a class with P properties and C constructor parameters, this is P × C string comparisons plus P list allocations.

Column classes can have 1020 properties; this is called once per column per alias/clone operation. In a query with 50 aliased columns this function runs 50 times, each time performing up to 400 string comparisons with 20 temporary List allocations per call — 20 000 comparisons and 1 000 heap allocations total.

Root Cause

consParams.map(KParameter::name) is a lambda-captured expression inside the .filter {} predicate. Kotlin does not hoist it; it runs inside the hot loop.

Fix

Pre-compute the parameter name set once, outside the filter:

// Before (O(P×C) with P list allocations):
val allValues = memberProperties
    .filter { it in mutableProperties || it.name in consParams.map(KParameter::name) }
    .associate { it.name to (replaceArgs[it] ?: it.get(this@clone)) }

// After (O(P) with O(1) lookup):
val consParamNames = consParams.mapTo(HashSet()) { it.name }
val allValues = memberProperties
    .filter { it in mutableProperties || it.name in consParamNames }
    .associate { it.name to (replaceArgs[it] ?: it.get(this@clone)) }

Speedup

~15× at P=20 properties, C=15 constructor parameters.