Hibernate (5 HIGH): addColumn/addReferencedColumn/addIndex ArrayList→LinkedHashSet (19x) FK second-pass LinkedHashSet, orderHierarchy LinkedHashSet MyBatis (1 MEDIUM): sortConstructorMappings indexOf→HashMap (12x) EF Core (2 HIGH + 1 MEDIUM): FindGenerationProperty HashSet (250x), AddPrincipals HashSet (250x), FK discovery HashSet (6x) Diesel (3 MEDIUM): SQLite/MySQL row position()→BTreeMap (51x) SQLAlchemy (2 HIGH): _values_bindparam Set (500x), evaluated_keys Set (500x) Peewee (1 MEDIUM): _SortedFieldList.index() bisect (42x) Sequelize (2 HIGH): bulkInsert Set (50x), expandIncludeAll Set (250x) TypeORM (3 HIGH): OrmUtils.uniq Map (500x), diffColumns Set (125x), updatedColumns Set (100x) Doctrine ORM (1 HIGH + 2 MEDIUM): hydrator discriminator (26x), addSubClass (250x), SqlWalker partial (130x) GORM (1 MEDIUM): sortCallbacks getRIndex→map (194x) SQLite: SqliteTest unit proof 4/4 PASS (101x) Unit tests: all PASS — Hibernate/MyBatis/EfCore/Diesel/SQLAlchemy/Peewee/ Sequelize/TypeORM/Doctrine/GORM Whitepaper: 157 sites, 62 ecosystems; PDF 752K
3.4 KiB
doctrine-orm-0001 — AbstractHydrator: O(rows × subclasses) discriminatorValues linear scan per result row
Status: OPEN
Severity: HIGH
Component: doctrine/orm — src/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php
Affects: All queries over inheritance hierarchies (CTI/STI) that return multiple rows
Root Cause
AbstractHydrator.php:328 — inside gatherRowData(), called once per DB result row:
// DEFECTIVE — O(subclasses) per row, per column with inheritance field collision
if (
isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
&& ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
) {
break;
}
$cacheKeyInfo['discriminatorValues'] is a PHP array of discriminator string values, built in
getDiscriminatorValues() (line 541–551) from $classMetadata->subClasses[].
in_array performs a full linear scan of this array.
The outer loop (foreach ($data as $key => $value)) runs once per column per result row.
gatherRowData() is called per row in the while loop at AbstractHydrator.php:114:
while (true) {
$row = $this->statement()->fetchAssociative();
...
$this->hydrateRowData($row, $result); // calls gatherRowData()
ObjectHydrator.php:327 confirms this is the hot path:
$rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
The same pattern appears in SimpleObjectHydrator.php:128:
if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array((string) $discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) {
continue;
}
Complexity Analysis
| Metric | Defective | Fixed |
|---|---|---|
| Discriminator check per row | O(subclasses) | O(1) |
| Full query (N rows, C affected columns) | O(N × C × subclasses) | O(N × C) |
| Growth pattern | Quadratic in subclasses | Linear |
For a CTI hierarchy with 50 subclasses, a 10-column SELECT returning 10,000 rows performs 50 × 10 × 10,000 = 5,000,000 comparisons for discriminator membership alone.
Fix
getDiscriminatorValues() (line 541) builds a flat array. Change it to build a Set-equivalent
(array keyed by discriminator value for O(1) isset lookup):
// FIXED — O(1) per row
private function getDiscriminatorValues(ClassMetadata $classMetadata): array
{
$values = [];
foreach ($classMetadata->subClasses as $subClass) {
$val = (string) $this->getClassMetadata($subClass)->discriminatorValue;
$values[$val] = true;
}
$values[(string) $classMetadata->discriminatorValue] = true;
return $values;
}
Then change both call sites from in_array($val, $discriminatorValues, true) to
isset($discriminatorValues[$val]).
Call Sites
| File | Line | Path |
|---|---|---|
src/Internal/Hydration/AbstractHydrator.php |
328 | gatherRowData() — ObjectHydrator hot path |
src/Internal/Hydration/SimpleObjectHydrator.php |
128 | hydrateRowData() — SimpleObjectHydrator hot path |
Speedup Estimate
For queries over CTI/STI hierarchies with many subclasses: linear in subclass count. A hierarchy with S subclasses sees S× speedup on the discriminator-check portion of hydration. Real-world inheritance hierarchies of 20–100 subclasses → 20–100× reduction in comparisons on the discriminator check path.