java-topology/docs/tickets/doctrine-orm-0001-hydrator-discriminator-in-array.md
russell@unturf.com d4ed2dff91 ORM wave: 24 defects patched across 10 ORMs (157 sites, 62 ecosystems)
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
2026-03-27 13:34:26 -04:00

105 lines
3.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```php
// 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 541551) 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`:
```php
while (true) {
$row = $this->statement()->fetchAssociative();
...
$this->hydrateRowData($row, $result); // calls gatherRowData()
```
`ObjectHydrator.php:327` confirms this is the hot path:
```php
$rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
```
The same pattern appears in `SimpleObjectHydrator.php:128`:
```php
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):
```php
// 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 20100 subclasses → 20100× reduction in comparisons
on the discriminator check path.