@mikro-orm/sql 7.0.0-dev.191 → 7.0.0-dev.192

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.0.0-dev.191",
3
+ "version": "7.0.0-dev.192",
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
  "type": "module",
6
6
  "exports": {
@@ -56,6 +56,6 @@
56
56
  "@mikro-orm/core": "^6.6.4"
57
57
  },
58
58
  "peerDependencies": {
59
- "@mikro-orm/core": "7.0.0-dev.191"
59
+ "@mikro-orm/core": "7.0.0-dev.192"
60
60
  }
61
61
  }
@@ -341,7 +341,28 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
341
341
  private processNestedJoins;
342
342
  private hasToManyJoins;
343
343
  protected wrapPaginateSubQuery(meta: EntityMetadata): void;
344
- private pruneExtraJoins;
344
+ /**
345
+ * Computes the set of populate paths from the _populate hints.
346
+ */
347
+ protected getPopulatePaths(): Set<string>;
348
+ protected normalizeJoinPath(join: JoinOptions, meta: EntityMetadata): string;
349
+ /**
350
+ * Transfers WHERE conditions to ORDER BY joins that are not used for population.
351
+ * This ensures the outer query's ORDER BY uses the same filtered rows as the subquery.
352
+ * GH #6160
353
+ */
354
+ protected transferConditionsForOrderByJoins(meta: EntityMetadata, cond: Dictionary | undefined, populatePaths: Set<string>): void;
355
+ /**
356
+ * Removes joins that are not used for population or ordering to improve performance.
357
+ */
358
+ protected pruneJoinsForPagination(meta: EntityMetadata, populatePaths: Set<string>): void;
359
+ /**
360
+ * Transfers WHERE conditions that reference a join alias to the join's ON clause.
361
+ * This is needed when a join is kept for ORDER BY after pagination wrapping,
362
+ * so the outer query orders by the same filtered rows as the subquery.
363
+ * @internal
364
+ */
365
+ protected transferConditionsToJoin(cond: Dictionary, join: JoinOptions, path?: string): void;
345
366
  private wrapModifySubQuery;
346
367
  private getSchema;
347
368
  private createAlias;
@@ -1410,28 +1410,67 @@ export class QueryBuilder {
1410
1410
  subSubQuery.select(pks).from(innerQuery);
1411
1411
  this._limit = undefined;
1412
1412
  this._offset = undefined;
1413
+ // Save the original WHERE conditions before pruning joins
1414
+ const originalCond = this._cond;
1415
+ const populatePaths = this.getPopulatePaths();
1413
1416
  if (!this._fields.some(field => isRaw(field))) {
1414
- this.pruneExtraJoins(meta);
1417
+ this.pruneJoinsForPagination(meta, populatePaths);
1415
1418
  }
1419
+ // Transfer WHERE conditions to ORDER BY joins (GH #6160)
1420
+ this.transferConditionsForOrderByJoins(meta, originalCond, populatePaths);
1416
1421
  const { sql, params } = subSubQuery.compile();
1417
1422
  this.select(this._fields).where({ [Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) } });
1418
1423
  }
1419
- pruneExtraJoins(meta) {
1420
- // remove joins that are not used for population or ordering to improve performance
1421
- const populate = new Set();
1422
- const orderByAliases = this._orderBy
1423
- .flatMap(hint => Object.keys(hint))
1424
- .map(k => k.split('.')[0]);
1424
+ /**
1425
+ * Computes the set of populate paths from the _populate hints.
1426
+ */
1427
+ getPopulatePaths() {
1428
+ const paths = new Set();
1425
1429
  function addPath(hints, prefix = '') {
1426
1430
  for (const hint of hints) {
1427
1431
  const field = hint.field.split(':')[0];
1428
- populate.add((prefix ? prefix + '.' : '') + field);
1432
+ const fullPath = prefix ? prefix + '.' + field : field;
1433
+ paths.add(fullPath);
1429
1434
  if (hint.children) {
1430
- addPath(hint.children, (prefix ? prefix + '.' : '') + field);
1435
+ addPath(hint.children, fullPath);
1431
1436
  }
1432
1437
  }
1433
1438
  }
1434
1439
  addPath(this._populate);
1440
+ return paths;
1441
+ }
1442
+ normalizeJoinPath(join, meta) {
1443
+ return join.path?.replace(/\[populate]|\[pivot]|:ref/g, '').replace(new RegExp(`^${meta.className}.`), '') ?? '';
1444
+ }
1445
+ /**
1446
+ * Transfers WHERE conditions to ORDER BY joins that are not used for population.
1447
+ * This ensures the outer query's ORDER BY uses the same filtered rows as the subquery.
1448
+ * GH #6160
1449
+ */
1450
+ transferConditionsForOrderByJoins(meta, cond, populatePaths) {
1451
+ if (!cond || this._orderBy.length === 0) {
1452
+ return;
1453
+ }
1454
+ const orderByAliases = new Set(this._orderBy
1455
+ .flatMap(hint => Object.keys(hint))
1456
+ .filter(k => !RawQueryFragment.isKnownFragmentSymbol(k))
1457
+ .map(k => k.split('.')[0]));
1458
+ for (const join of Object.values(this._joins)) {
1459
+ const joinPath = this.normalizeJoinPath(join, meta);
1460
+ const isPopulateJoin = populatePaths.has(joinPath);
1461
+ // Only transfer conditions for joins used for ORDER BY but not for population
1462
+ if (orderByAliases.has(join.alias) && !isPopulateJoin) {
1463
+ this.transferConditionsToJoin(cond, join);
1464
+ }
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Removes joins that are not used for population or ordering to improve performance.
1469
+ */
1470
+ pruneJoinsForPagination(meta, populatePaths) {
1471
+ const orderByAliases = this._orderBy
1472
+ .flatMap(hint => Object.keys(hint))
1473
+ .map(k => k.split('.')[0]);
1435
1474
  const joins = Object.entries(this._joins);
1436
1475
  const rootAlias = this.alias;
1437
1476
  function addParentAlias(alias) {
@@ -1445,12 +1484,38 @@ export class QueryBuilder {
1445
1484
  addParentAlias(orderByAlias);
1446
1485
  }
1447
1486
  for (const [key, join] of joins) {
1448
- const path = join.path?.replace(/\[populate]|\[pivot]|:ref/g, '').replace(new RegExp(`^${meta.className}.`), '');
1449
- if (!populate.has(path ?? '') && !orderByAliases.includes(join.alias)) {
1487
+ const path = this.normalizeJoinPath(join, meta);
1488
+ if (!populatePaths.has(path) && !orderByAliases.includes(join.alias)) {
1450
1489
  delete this._joins[key];
1451
1490
  }
1452
1491
  }
1453
1492
  }
1493
+ /**
1494
+ * Transfers WHERE conditions that reference a join alias to the join's ON clause.
1495
+ * This is needed when a join is kept for ORDER BY after pagination wrapping,
1496
+ * so the outer query orders by the same filtered rows as the subquery.
1497
+ * @internal
1498
+ */
1499
+ transferConditionsToJoin(cond, join, path = '') {
1500
+ const aliasPrefix = join.alias + '.';
1501
+ for (const key of Object.keys(cond)) {
1502
+ const value = cond[key];
1503
+ // Handle $and/$or operators - recurse into nested conditions
1504
+ if (key === '$and' || key === '$or') {
1505
+ if (Array.isArray(value)) {
1506
+ for (const item of value) {
1507
+ this.transferConditionsToJoin(item, join, path);
1508
+ }
1509
+ }
1510
+ continue;
1511
+ }
1512
+ // Check if this condition references the join alias
1513
+ if (key.startsWith(aliasPrefix)) {
1514
+ // Add condition to the join's ON clause
1515
+ join.cond[key] = value;
1516
+ }
1517
+ }
1518
+ }
1454
1519
  wrapModifySubQuery(meta) {
1455
1520
  const subQuery = this.clone();
1456
1521
  subQuery.finalized = true;