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

2.4 KiB
Raw Permalink Blame History

doctrine-orm-0003 — SqlWalker::walkObjectExpression: O(fields²) partial field set check per query

Status: OPEN Severity: MEDIUM Component: doctrine/ormsrc/Doctrine/ORM/Query/SqlWalker.php Affects: DQL queries using partial object selects (SELECT PARTIAL p.{id,name})


Root Cause

SqlWalker.php:1405 and 1445walkObjectExpression():

// 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:

// 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.