java-topology/defects/doctrine/unit/DoctrineTest.java
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

149 lines
6.6 KiB
Java
Raw 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.

package unit;
import java.util.*;
/**
* DoctrineTest — doctrine-0001..0003
*
* Proves CWE-407 in Doctrine ORM:
* doctrine-0001: AbstractHydrator.gatherRowData() — in_array($disc, $discriminatorValues) O(S) per row
* doctrine-0002: ClassMetadata.addSubClass() — in_array($class, $subClasses) O(S) dedup
* doctrine-0003: SqlWalker.walkObjectExpression() — in_array($field, $partialFieldSet) O(P) per field
*
* Run: javac -d . DoctrineTest.java && java -ea unit.DoctrineTest
*/
public class DoctrineTest {
// ── doctrine-0001: AbstractHydrator discriminatorValues per row ───────────
/**
* SLOW: in_array($disc, $discriminatorValues) O(S) per row per col.
* O(N × C × S) for N rows, C columns with inheritance, S subclasses.
*/
static long hydratorSlow(int numRows, int numCols, int numSubclasses) {
List<String> discriminatorValues = new ArrayList<>();
for (int i = 0; i < numSubclasses; i++) discriminatorValues.add("SubClass" + i);
long ops = 0;
for (int r = 0; r < numRows; r++) {
for (int c = 0; c < numCols; c++) {
String disc = "SubClass" + (r % numSubclasses); // valid disc — scan to end
for (String v : discriminatorValues) { ops++; if (v.equals(disc)) break; }
}
}
return ops;
}
/** FAST: array_flip → isset() O(1). O(N × C) total. */
static long hydratorFast(int numRows, int numCols, int numSubclasses) {
Set<String> discriminatorValuesSet = new HashSet<>();
for (int i = 0; i < numSubclasses; i++) discriminatorValuesSet.add("SubClass" + i);
long ops = 0;
for (int r = 0; r < numRows; r++) {
for (int c = 0; c < numCols; c++) {
String disc = "SubClass" + (r % numSubclasses);
ops++; // O(1)
discriminatorValuesSet.contains(disc);
}
}
return ops;
}
// ── doctrine-0002: ClassMetadata.addSubClass() dedup ─────────────────────
/** SLOW: in_array($class, $subClasses) O(S) per addSubClass call. O(H×S) total. */
static long addSubClassSlow(int numSubclasses) {
List<String> subClasses = new ArrayList<>();
long ops = 0;
// Add each class twice (parent factory calls addSubClass for all parents)
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < numSubclasses; i++) {
String cls = "SubClass" + i;
boolean found = false;
for (String s : subClasses) { ops++; if (s.equals(cls)) { found = true; break; } }
if (!found) subClasses.add(cls);
}
}
return ops;
}
/** FAST: isset($subClassesSet[$class]) O(1). O(H) total. */
static long addSubClassFast(int numSubclasses) {
Set<String> subClassesSet = new HashSet<>();
List<String> subClasses = new ArrayList<>();
long ops = 0;
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < numSubclasses; i++) {
String cls = "SubClass" + i;
ops++;
if (subClassesSet.add(cls)) subClasses.add(cls);
}
}
return ops;
}
// ── doctrine-0003: SqlWalker partialFieldSet check ───────────────────────
/** SLOW: in_array($fieldName, $partialFieldSet) O(P) per field — O(F×P) total. */
static long sqlWalkerSlow(int numFields, int partialFieldSetSize) {
List<String> partialFieldSet = new ArrayList<>();
for (int i = 0; i < partialFieldSetSize; i++) partialFieldSet.add("field_" + i);
long ops = 0;
for (int f = 0; f < numFields; f++) {
String fieldName = "field_" + (f % (partialFieldSetSize * 2));
for (String p : partialFieldSet) { ops++; if (p.equals(fieldName)) break; }
}
return ops;
}
/** FAST: $partialFieldSetFlip[$fieldName] O(1). O(F) total. */
static long sqlWalkerFast(int numFields, int partialFieldSetSize) {
Set<String> partialFieldSetFlip = new HashSet<>();
for (int i = 0; i < partialFieldSetSize; i++) partialFieldSetFlip.add("field_" + i);
long ops = 0;
for (int f = 0; f < numFields; f++) {
String fieldName = "field_" + (f % (partialFieldSetSize * 2));
ops++;
partialFieldSetFlip.contains(fieldName);
}
return ops;
}
static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) {
slow.run(); fast.run();
long t0 = System.nanoTime(); slow.run(); long sMs = (System.nanoTime()-t0)/1_000_000;
long t1 = System.nanoTime(); fast.run(); long fMs = (System.nanoTime()-t1)/1_000_000;
double r = fOps > 0 ? (double)sOps/fOps : 0;
System.out.printf(" %-52s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n",
label, sMs, sOps, fMs, fOps, r);
}
public static void main(String[] args) {
System.out.println("=== UNIT doctrine-0001..0003: Doctrine ORM CWE-407 ===");
System.out.println();
final int ROWS = 2000, COLS = 10, SUBCLASSES = 50; // doctrine-0001
final int NUM_SUBCLASSES = 500; // doctrine-0002
final int FIELDS = 500, PARTIAL = 200; // doctrine-0003
long s0 = hydratorSlow(ROWS, COLS, SUBCLASSES), f0 = hydratorFast(ROWS, COLS, SUBCLASSES);
bench("doctrine-0001 Hydrator discriminatorValues in_array",
() -> hydratorSlow(ROWS, COLS, SUBCLASSES), () -> hydratorFast(ROWS, COLS, SUBCLASSES), s0, f0);
long s1 = addSubClassSlow(NUM_SUBCLASSES), f1 = addSubClassFast(NUM_SUBCLASSES);
bench("doctrine-0002 ClassMetadata.addSubClass() dedup",
() -> addSubClassSlow(NUM_SUBCLASSES), () -> addSubClassFast(NUM_SUBCLASSES), s1, f1);
long s2 = sqlWalkerSlow(FIELDS, PARTIAL), f2 = sqlWalkerFast(FIELDS, PARTIAL);
bench("doctrine-0003 SqlWalker partialFieldSet in_array",
() -> sqlWalkerSlow(FIELDS, PARTIAL), () -> sqlWalkerFast(FIELDS, PARTIAL), s2, f2);
System.out.println();
int pass = 0;
assert s0 > f0 * 5 : "doctrine-0001 expected >5x"; pass++;
assert s1 > f1 * 5 : "doctrine-0002 expected >5x"; pass++;
assert s2 > f2 * 5 : "doctrine-0003 expected >5x"; pass++;
System.out.printf("%d/3 PASS — doctrine-0001..0003: CWE-407 in Doctrine ORM%n", pass);
System.out.printf("Hotpaths: inheritance query hydration, metadata warmup, PARTIAL DQL queries%n");
}
}