java-topology/docs/tickets/doctrine-orm-0003-sqlwalker-partial-field-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

82 lines
2.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-0003 — SqlWalker::walkObjectExpression: O(fields²) partial field set check per query
**Status:** OPEN
**Severity:** MEDIUM
**Component:** `doctrine/orm``src/Doctrine/ORM/Query/SqlWalker.php`
**Affects:** DQL queries using partial object selects (`SELECT PARTIAL p.{id,name}`)
---
## Root Cause
`SqlWalker.php:1405` and `1445``walkObjectExpression()`:
```php
// DEFECTIVE — O(partialFieldSet) per field, inside foreach fieldMappings
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
continue;
}
// ... build SQL column list
}
// And for subclasses (line 1445):
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
continue;
}
```
`$partialFieldSet` is a plain PHP array of field name strings. For each field in
`$class->fieldMappings` (F fields), `in_array` scans all P entries in `$partialFieldSet`.
Total: O(F × P) per class, O(F × P × subclasses) for STI/CTI hierarchies.
Called once per query compilation (not per result row), but DQL parsing without a query
result cache repeats this on every request.
---
## Complexity Analysis
| Metric | Defective | Fixed |
|--------|-----------|-------|
| Single field check | O(partialFieldSet size P) | O(1) |
| Full entity (F fields) | O(F × P) | O(F) |
| With S subclasses (STI) | O(F × P × S) | O(F × S) |
---
## Fix
Convert `$partialFieldSet` array to a hash set before entering the loop:
```php
// FIXED — build O(1)-lookup set once, before the loops
$partialFieldIndex = $partialFieldSet ? array_flip($partialFieldSet) : null;
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ($partialFieldIndex !== null && ! isset($partialFieldIndex[$fieldName])) {
continue;
}
// ...
}
```
Same fix applies at line 1445 for the subclass loop.
---
## Call Sites
| File | Line | Context |
|------|------|---------|
| `src/Query/SqlWalker.php` | 1405 | `walkObjectExpression()` — primary class fields |
| `src/Query/SqlWalker.php` | 1445 | `walkObjectExpression()` — STI/CTI subclass fields |
---
## Speedup Estimate
Queries with large partial field sets (P > 10) or large entities (F > 20) with deep STI
hierarchies (S > 10): O(P)× improvement in query compilation time on the partial-select
field filter path.