@mikro-orm/sql 7.1.10-dev.6 → 7.1.10-dev.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.10-dev.6",
3
+ "version": "7.1.10-dev.7",
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.6"
56
+ "@mikro-orm/core": "7.1.10-dev.7"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -507,11 +507,16 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
507
507
  */
508
508
  applyJoinedFilters(em: EntityManager, filterOptions: FilterOptions | undefined): Promise<void>;
509
509
  /**
510
- * Auto-joins added while processing a condition of `condJoin` would render after it and produce a
511
- * forward alias reference in its `on` clause. Fold them into `condJoin`, so they render as a single
512
- * parenthesized join group and share the scope of the outer `on` clause (issue #7681).
513
- */
514
- private nestNewJoins;
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;
515
520
  withSubQuery(subQuery: RawQueryFragment | NativeQueryBuilder, alias: string): this;
516
521
  /**
517
522
  * Adds a WHERE clause to the query using an object condition.
@@ -411,7 +411,6 @@ export class QueryBuilder {
411
411
  filterOptions = QueryHelper.mergePropertyFilters(join.prop.filters, filterOptions);
412
412
  let cond = await em.applyFilters(join.prop.targetMeta.class, join.cond, filterOptions, 'read');
413
413
  const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, join.prop.targetMeta.class, cond);
414
- const joinCountBefore = Object.keys(this.#state.joins).length;
415
414
  cond = criteriaNode.process(this, {
416
415
  matchPopulateJoins: true,
417
416
  filter: true,
@@ -419,7 +418,6 @@ export class QueryBuilder {
419
418
  ignoreBranching: true,
420
419
  parentPath: join.path,
421
420
  });
422
- this.nestNewJoins(join, joinCountBefore);
423
421
  if (Utils.hasObjectKeys(cond) || RawQueryFragment.hasObjectFragments(cond)) {
424
422
  // remove nested filters, we only care about scalars here, nesting would require another join branch
425
423
  for (const key of Object.keys(cond)) {
@@ -435,6 +433,7 @@ export class QueryBuilder {
435
433
  else {
436
434
  join.cond = { ...cond };
437
435
  }
436
+ this.nestReferencedJoins(join);
438
437
  // For polymorphic LEFT JOIN filters, add a WHERE condition to enforce the filter
439
438
  // only for rows matching this target's discriminator value. This ensures rows pointing
440
439
  // to other polymorphic targets are not excluded.
@@ -459,26 +458,44 @@ export class QueryBuilder {
459
458
  }
460
459
  }
461
460
  /**
462
- * Auto-joins added while processing a condition of `condJoin` would render after it and produce a
463
- * forward alias reference in its `on` clause. Fold them into `condJoin`, so they render as a single
464
- * parenthesized join group and share the scope of the outer `on` clause (issue #7681).
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.
465
467
  */
466
- nestNewJoins(condJoin, joinCountBefore) {
468
+ nestReferencedJoins(condJoin) {
467
469
  // m:n pivot joins might not have the target join entry created
468
470
  if (!condJoin) {
469
471
  return;
470
472
  }
471
- const joinKeys = Object.keys(this.#state.joins);
472
- for (let i = joinCountBefore; i < joinKeys.length; i++) {
473
- const j = this.#state.joins[joinKeys[i]];
474
- if (j === condJoin || j.ownerAlias !== condJoin.alias) {
475
- continue;
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);
476
483
  }
477
- const nested = (condJoin.nested ??= new Set());
478
- j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
479
- nested.add(j);
480
484
  }
481
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
+ }
482
499
  withSubQuery(subQuery, alias) {
483
500
  this.ensureNotFinalized();
484
501
  if (isRaw(subQuery)) {
@@ -1437,7 +1454,6 @@ export class QueryBuilder {
1437
1454
  aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
1438
1455
  });
1439
1456
  const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
1440
- const joinCountBefore = Object.keys(this.#state.joins).length;
1441
1457
  cond = criteriaNode.process(this, { ignoreBranching: true, alias });
1442
1458
  let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
1443
1459
  path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
@@ -1467,7 +1483,7 @@ export class QueryBuilder {
1467
1483
  this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
1468
1484
  this.#state.joins[aliasedName].path ??= path;
1469
1485
  }
1470
- this.nestNewJoins(this.#state.joins[aliasedName], joinCountBefore);
1486
+ this.nestReferencedJoins(this.#state.joins[aliasedName]);
1471
1487
  return { prop, key: aliasedName };
1472
1488
  }
1473
1489
  prepareFields(fields, type = 'where', schema) {