java-topology/defects/hibernate/patch/hibernate-0006-persister-alias-closure-hashset.md

5.2 KiB
Raw Blame History

UNDF: UNDF-2026-000000417

hibernate-0006: AbstractEntityPersister — O(T²) alias dedup in subclass property closure

CWE-407 — Algorithmic Complexity: Linear Membership Test in Loop

Field Value
ID hibernate-0006
Severity MEDIUM
Ecosystem hibernate-orm
Package hibernate-core
File hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java
Lines 665709
Complexity O(T²) where T = total columns across subclass property closure
Fix Replace ArrayList<String> with LinkedHashSet<String> for alias dedup

Defective Code

// Lines 665-709 in AbstractEntityPersister constructor
final ArrayList<String> aliases = new ArrayList<>();          // O(N) contains
final ArrayList<String> formulaAliases = new ArrayList<>();   // O(N) contains

for (var prop : persistentClass.getSubclassPropertyClosure()) {  // outer: O(P properties)
    ...
    for (int i = 0; i < selectables.size(); i++) {               // inner: O(C columns/prop)
        ...
        final String columnAlias = selectable.getAlias(...);
        if (prop.isSelectable() && !aliases.contains(columnAlias)) {  // O(T) — CWE-407
            aliases.add(columnAlias);
        }
        ...
        final String formulaAlias = selectable.getAlias(dialect);
        if (prop.isSelectable() && !formulaAliases.contains(formulaAlias)) { // O(F) — CWE-407
            formulaAliases.add(formulaAlias);
        }
    }
}
subclassColumnAliasClosure = toStringArray(aliases);
subclassFormulaAliasClosure = toStringArray(formulaAliases);

Pattern: For each column selectable (total T across all subclass properties), calls ArrayList.contains() on a list that grows to size T. Total cost: O(T²).

For table-per-hierarchy (TPH) inheritance with many subclasses (e.g., 50 subclasses × 20 columns each = 1000 total selectables), this creates a 1,000,000 operation dedup phase during SessionFactory startup.

Complexity Table

T (total subclass columns) Operations (before) Operations (after)
100 ~10,000 ~100
500 ~250,000 ~500
1,000 ~1,000,000 ~1,000

Speedup ratio: ~1000x at T=1000.

Fix

// hibernate-0006 fix: LinkedHashSet for O(1) contains() + insertion-order preservation.
// Previously ArrayList<String>: each aliases.contains() is O(T) in a loop over T columns
// → O(T²) total. LinkedHashSet.add() is O(1) amortized and handles dedup automatically.
final LinkedHashSet<String> aliases = new LinkedHashSet<>();
final LinkedHashSet<String> formulaAliases = new LinkedHashSet<>();

for (var prop : persistentClass.getSubclassPropertyClosure()) {
    ...
    for (int i = 0; i < selectables.size(); i++) {
        ...
        if (selectable instanceof Formula) {
            ...
            if (prop.isSelectable()) {
                formulaAliases.add(formulaAlias);  // O(1) — dedup handled by set
            }
        }
        else if (selectable instanceof Column column) {
            ...
            if (prop.isSelectable()) {
                aliases.add(columnAlias);  // O(1) — dedup handled by set
            }
        }
    }
}
subclassColumnAliasClosure = toStringArray(new ArrayList<>(aliases));
subclassFormulaAliasClosure = toStringArray(new ArrayList<>(formulaAliases));

Patch

--- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java
+++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java
@@ -xxx import section +++
+import java.util.LinkedHashSet;

@@ -664,8 +664,11 @@ public abstract class AbstractEntityPersister
 		// SUBCLASS PROPERTY CLOSURE
-		final ArrayList<String> aliases = new ArrayList<>();
-		final ArrayList<String> formulaAliases = new ArrayList<>();
+		// hibernate-0006 fix: LinkedHashSet for O(1) dedup; was O(T²) with ArrayList.contains()
+		// where T = total columns across the subclass property closure.
+		final LinkedHashSet<String> aliasSet = new LinkedHashSet<>();
+		final LinkedHashSet<String> formulaAliasSet = new LinkedHashSet<>();
 		...

@@ -699,10 +703,10 @@ public abstract class AbstractEntityPersister
 				if ( selectable instanceof Formula ) {
 					...
-					if ( prop.isSelectable() && !formulaAliases.contains( formulaAlias ) ) {
-						formulaAliases.add( formulaAlias );
+					if ( prop.isSelectable() ) {
+						formulaAliasSet.add( formulaAlias );  // O(1)
 					}
 				}
 				else if ( selectable instanceof Column column ) {
 					...
-					if ( prop.isSelectable() && !aliases.contains( columnAlias ) ) {
-						aliases.add( columnAlias );
+					if ( prop.isSelectable() ) {
+						aliasSet.add( columnAlias );  // O(1)
 					}
 					...
 				}
 			}
 		}
-		subclassColumnAliasClosure = toStringArray( aliases );
-		subclassFormulaAliasClosure = toStringArray( formulaAliases );
+		subclassColumnAliasClosure = toStringArray( new ArrayList<>( aliasSet ) );
+		subclassFormulaAliasClosure = toStringArray( new ArrayList<>( formulaAliasSet ) );