@mikro-orm/sql 7.1.10-dev.1 → 7.1.10-dev.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1675,14 +1675,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
1675
1675
  // (including virtual ones, as the owner FK of a polymorphic pivot is only mapped via non-persisted relations)
1676
1676
  const pivotRelations = meta.pivotTable && !meta.compositePK ? meta.relations.filter(p => p.kind === ReferenceKind.MANY_TO_ONE) : [];
1677
1677
  for (const item of rawResults) {
1678
- let pk = Utils.getCompositeKeyHash(item, meta);
1678
+ // flat hash, so nested composite PK values keep their own separators and cannot collide
1679
+ let pk = Utils.getCompositeKeyHash(item, meta, false, undefined, true);
1679
1680
  if (pivotRelations.length > 0) {
1680
1681
  pk = Utils.getPrimaryKeyHash([
1681
1682
  pk,
1682
1683
  ...pivotRelations.flatMap(p => {
1683
1684
  const value = item[p.name];
1684
1685
  // composite FKs are mapped to an array of values, which `extractPK` does not accept
1685
- return (Array.isArray(value) ? value : Utils.extractPK(value, p.targetMeta));
1686
+ return (Array.isArray(value) ? Utils.flatten(value, true) : Utils.extractPK(value, p.targetMeta));
1686
1687
  }),
1687
1688
  ]);
1688
1689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.10-dev.1",
3
+ "version": "7.1.10-dev.11",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.1.9"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.10-dev.1"
56
+ "@mikro-orm/core": "7.1.10-dev.11"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -506,6 +506,17 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
506
506
  * @internal
507
507
  */
508
508
  applyJoinedFilters(em: EntityManager, filterOptions: FilterOptions | undefined): Promise<void>;
509
+ /**
510
+ * The `on` clause of `condJoin` — its explicit join condition or a filter condition merged into
511
+ * it — can reference the alias of any join in its subtree, both auto-joins created while
512
+ * processing the condition and pre-existing joined paths, all of which render after `condJoin`
513
+ * and would be forward alias references (issues #7681, #8090, #8099). When that happens, fold the
514
+ * subtree into `condJoin`, so it renders as a single parenthesized join group and every alias
515
+ * shares the scope of the outer `on` clause.
516
+ */
517
+ private nestReferencedJoins;
518
+ private getJoinSubtree;
519
+ private condReferencesAlias;
509
520
  withSubQuery(subQuery: RawQueryFragment | NativeQueryBuilder, alias: string): this;
510
521
  /**
511
522
  * Adds a WHERE clause to the query using an object condition.
@@ -433,6 +433,7 @@ export class QueryBuilder {
433
433
  else {
434
434
  join.cond = { ...cond };
435
435
  }
436
+ this.nestReferencedJoins(join);
436
437
  // For polymorphic LEFT JOIN filters, add a WHERE condition to enforce the filter
437
438
  // only for rows matching this target's discriminator value. This ensures rows pointing
438
439
  // to other polymorphic targets are not excluded.
@@ -456,6 +457,45 @@ export class QueryBuilder {
456
457
  }
457
458
  }
458
459
  }
460
+ /**
461
+ * The `on` clause of `condJoin` — its explicit join condition or a filter condition merged into
462
+ * it — can reference the alias of any join in its subtree, both auto-joins created while
463
+ * processing the condition and pre-existing joined paths, all of which render after `condJoin`
464
+ * and would be forward alias references (issues #7681, #8090, #8099). When that happens, fold the
465
+ * subtree into `condJoin`, so it renders as a single parenthesized join group and every alias
466
+ * shares the scope of the outer `on` clause.
467
+ */
468
+ nestReferencedJoins(condJoin) {
469
+ // m:n pivot joins might not have the target join entry created
470
+ if (!condJoin) {
471
+ return;
472
+ }
473
+ const subtree = this.getJoinSubtree(condJoin);
474
+ if (!subtree.some(j => this.condReferencesAlias(condJoin.cond, j.alias))) {
475
+ return;
476
+ }
477
+ for (const j of subtree) {
478
+ const parent = j.ownerAlias === condJoin.alias ? condJoin : subtree.find(p => p.alias === j.ownerAlias);
479
+ if (!parent.nested?.has(j)) {
480
+ const nested = (parent.nested ??= new Set());
481
+ j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
482
+ nested.add(j);
483
+ }
484
+ }
485
+ }
486
+ getJoinSubtree(join) {
487
+ const children = Object.values(this.#state.joins).filter(j => j !== join && j.ownerAlias === join.alias);
488
+ return children.flatMap(j => [j, ...this.getJoinSubtree(j)]);
489
+ }
490
+ condReferencesAlias(cond, alias) {
491
+ if (Array.isArray(cond)) {
492
+ return cond.some(c => this.condReferencesAlias(c, alias));
493
+ }
494
+ if (Utils.isPlainObject(cond)) {
495
+ return Object.entries(cond).some(([key, value]) => key.startsWith(`${alias}.`) || this.condReferencesAlias(value, alias));
496
+ }
497
+ return false;
498
+ }
459
499
  withSubQuery(subQuery, alias) {
460
500
  this.ensureNotFinalized();
461
501
  if (isRaw(subQuery)) {
@@ -1104,7 +1144,9 @@ export class QueryBuilder {
1104
1144
  }
1105
1145
  if (stack.length > 0) {
1106
1146
  const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
1107
- yield this.mapResult(merged[0], options.mapResults);
1147
+ for (const row of merged) {
1148
+ yield this.mapResult(row, options.mapResults);
1149
+ }
1108
1150
  }
1109
1151
  }
1110
1152
  /**
@@ -1412,7 +1454,6 @@ export class QueryBuilder {
1412
1454
  aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
1413
1455
  });
1414
1456
  const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
1415
- const joinCountBefore = Object.keys(this.#state.joins).length;
1416
1457
  cond = criteriaNode.process(this, { ignoreBranching: true, alias });
1417
1458
  let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
1418
1459
  path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
@@ -1442,20 +1483,7 @@ export class QueryBuilder {
1442
1483
  this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
1443
1484
  this.#state.joins[aliasedName].path ??= path;
1444
1485
  }
1445
- // auto-joins added by cond processing that depend on the new alias would otherwise produce a
1446
- // forward reference (the auto-join's ON refers to alias, while alias's ON refers back to it);
1447
- // fold them into the new join so both aliases share scope in the outer ON clause (issue #7681)
1448
- const condJoin = this.#state.joins[aliasedName];
1449
- const joinKeys = Object.keys(this.#state.joins);
1450
- for (let i = joinCountBefore; i < joinKeys.length; i++) {
1451
- const j = this.#state.joins[joinKeys[i]];
1452
- if (j === condJoin || j.ownerAlias !== alias) {
1453
- continue;
1454
- }
1455
- const nested = (condJoin.nested ??= new Set());
1456
- j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
1457
- nested.add(j);
1458
- }
1486
+ this.nestReferencedJoins(this.#state.joins[aliasedName]);
1459
1487
  return { prop, key: aliasedName };
1460
1488
  }
1461
1489
  prepareFields(fields, type = 'where', schema) {
@@ -124,6 +124,8 @@ export declare abstract class SchemaHelper {
124
124
  getAddColumnsSQL(table: DatabaseTable, columns: Column[]): string[];
125
125
  getDropColumnsSQL(tableName: string, columns: Column[], schemaName?: string): string;
126
126
  hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
127
+ /** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
128
+ protected getPrimaryKeyConstraintPrefix(table: DatabaseTable, index: IndexDef): string;
127
129
  castColumn(name: string, type: string): string;
128
130
  alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
129
131
  /** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
@@ -521,6 +521,10 @@ export class SchemaHelper {
521
521
  const defaultName = this.platform.getDefaultPrimaryName(table.name, pkIndex.columnNames);
522
522
  return pkIndex?.keyName !== defaultName;
523
523
  }
524
+ /** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
525
+ getPrimaryKeyConstraintPrefix(table, index) {
526
+ return this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(index.keyName)} ` : '';
527
+ }
524
528
  /* v8 ignore next */
525
529
  castColumn(name, type) {
526
530
  return '';
@@ -766,7 +770,7 @@ export class SchemaHelper {
766
770
  const primaryKey = table.getPrimaryKey();
767
771
  const createPrimary = !table.getColumns().some(c => c.autoincrement && c.primary) || this.hasNonDefaultPrimaryKeyName(table);
768
772
  if (createPrimary && primaryKey) {
769
- const name = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(primaryKey.keyName)} ` : '';
773
+ const name = this.getPrimaryKeyConstraintPrefix(table, primaryKey);
770
774
  sql += `, ${name}primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`;
771
775
  }
772
776
  sql += ')';
@@ -849,7 +853,7 @@ export class SchemaHelper {
849
853
  const columns = index.columnNames.map(c => this.quote(c)).join(', ');
850
854
  const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
851
855
  if (index.primary) {
852
- const keyName = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${index.keyName} ` : '';
856
+ const keyName = this.getPrimaryKeyConstraintPrefix(table, index);
853
857
  return `alter table ${table.getQuotedName()} add ${keyName}primary key (${columns})${defer}`;
854
858
  }
855
859
  if (index.type === 'fulltext') {