@mikro-orm/sql 7.0.0-dev.99 → 7.0.0-rc.1

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.
Files changed (64) hide show
  1. package/AbstractSqlConnection.d.ts +2 -4
  2. package/AbstractSqlConnection.js +3 -7
  3. package/AbstractSqlDriver.d.ts +89 -23
  4. package/AbstractSqlDriver.js +630 -197
  5. package/AbstractSqlPlatform.d.ts +11 -5
  6. package/AbstractSqlPlatform.js +18 -5
  7. package/PivotCollectionPersister.d.ts +5 -0
  8. package/PivotCollectionPersister.js +30 -12
  9. package/SqlEntityManager.d.ts +2 -2
  10. package/dialects/mysql/{MySqlPlatform.d.ts → BaseMySqlPlatform.d.ts} +4 -3
  11. package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +9 -4
  12. package/dialects/mysql/MySqlSchemaHelper.d.ts +12 -1
  13. package/dialects/mysql/MySqlSchemaHelper.js +97 -6
  14. package/dialects/mysql/index.d.ts +1 -2
  15. package/dialects/mysql/index.js +1 -2
  16. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +106 -0
  17. package/dialects/postgresql/BasePostgreSqlPlatform.js +350 -0
  18. package/dialects/postgresql/FullTextType.d.ts +14 -0
  19. package/dialects/postgresql/FullTextType.js +59 -0
  20. package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +8 -0
  21. package/dialects/postgresql/PostgreSqlExceptionConverter.js +47 -0
  22. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +90 -0
  23. package/dialects/postgresql/PostgreSqlSchemaHelper.js +732 -0
  24. package/dialects/postgresql/index.d.ts +3 -0
  25. package/dialects/postgresql/index.js +3 -0
  26. package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -0
  27. package/dialects/sqlite/BaseSqliteConnection.js +13 -0
  28. package/dialects/sqlite/BaseSqlitePlatform.d.ts +6 -0
  29. package/dialects/sqlite/BaseSqlitePlatform.js +12 -0
  30. package/dialects/sqlite/SqliteSchemaHelper.d.ts +25 -0
  31. package/dialects/sqlite/SqliteSchemaHelper.js +145 -19
  32. package/dialects/sqlite/index.d.ts +0 -1
  33. package/dialects/sqlite/index.js +0 -1
  34. package/package.json +5 -6
  35. package/plugin/transformer.d.ts +1 -1
  36. package/plugin/transformer.js +1 -1
  37. package/query/CriteriaNode.d.ts +9 -5
  38. package/query/CriteriaNode.js +16 -15
  39. package/query/CriteriaNodeFactory.d.ts +6 -6
  40. package/query/CriteriaNodeFactory.js +33 -31
  41. package/query/NativeQueryBuilder.d.ts +3 -2
  42. package/query/NativeQueryBuilder.js +1 -2
  43. package/query/ObjectCriteriaNode.js +51 -36
  44. package/query/QueryBuilder.d.ts +569 -79
  45. package/query/QueryBuilder.js +614 -171
  46. package/query/QueryBuilderHelper.d.ts +24 -16
  47. package/query/QueryBuilderHelper.js +167 -78
  48. package/query/ScalarCriteriaNode.js +2 -2
  49. package/query/raw.d.ts +11 -3
  50. package/query/raw.js +1 -2
  51. package/schema/DatabaseSchema.d.ts +15 -2
  52. package/schema/DatabaseSchema.js +143 -15
  53. package/schema/DatabaseTable.d.ts +12 -0
  54. package/schema/DatabaseTable.js +91 -31
  55. package/schema/SchemaComparator.d.ts +8 -0
  56. package/schema/SchemaComparator.js +127 -3
  57. package/schema/SchemaHelper.d.ts +26 -3
  58. package/schema/SchemaHelper.js +98 -11
  59. package/schema/SqlSchemaGenerator.d.ts +10 -0
  60. package/schema/SqlSchemaGenerator.js +137 -9
  61. package/tsconfig.build.tsbuildinfo +1 -0
  62. package/typings.d.ts +78 -38
  63. package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +0 -1
  64. package/dialects/postgresql/PostgreSqlTableCompiler.js +0 -1
@@ -1,8 +1,10 @@
1
- import { helper, isRaw, LoadStrategy, LockMode, PopulateHint, QueryFlag, QueryHelper, raw, RawQueryFragment, Reference, ReferenceKind, serialize, Utils, ValidationError, inspect, } from '@mikro-orm/core';
1
+ import { helper, inspect, isRaw, LoadStrategy, LockMode, PopulateHint, QueryFlag, QueryHelper, raw, RawQueryFragment, Reference, ReferenceKind, serialize, Utils, ValidationError, } from '@mikro-orm/core';
2
2
  import { JoinType, QueryType } from './enums.js';
3
3
  import { QueryBuilderHelper } from './QueryBuilderHelper.js';
4
4
  import { CriteriaNodeFactory } from './CriteriaNodeFactory.js';
5
5
  import { NativeQueryBuilder } from './NativeQueryBuilder.js';
6
+ /** Matches 'path as alias' — safe because ORM property names are JS identifiers (no spaces). */
7
+ const FIELD_ALIAS_RE = /^(.+?)\s+as\s+(\w+)$/i;
6
8
  /**
7
9
  * SQL query builder with fluent interface.
8
10
  *
@@ -47,8 +49,6 @@ export class QueryBuilder {
47
49
  _populate = [];
48
50
  /** @internal */
49
51
  _populateMap = {};
50
- /** @internal */
51
- rawFragments = new Set();
52
52
  aliasCounter = 0;
53
53
  flags = new Set([QueryFlag.CONVERT_CUSTOM_TYPES]);
54
54
  finalized = false;
@@ -69,6 +69,7 @@ export class QueryBuilder {
69
69
  _joinedProps = new Map();
70
70
  _cache;
71
71
  _indexHint;
72
+ _collation;
72
73
  _comments = [];
73
74
  _hintComments = [];
74
75
  flushMode;
@@ -77,9 +78,12 @@ export class QueryBuilder {
77
78
  subQueries = {};
78
79
  _mainAlias;
79
80
  _aliases = {};
81
+ _tptAlias = {}; // maps entity className to alias for TPT parent tables
80
82
  _helper;
81
83
  _query;
82
84
  platform;
85
+ tptJoinsApplied = false;
86
+ autoJoinedPaths = [];
83
87
  /**
84
88
  * @internal
85
89
  */
@@ -100,12 +104,31 @@ export class QueryBuilder {
100
104
  }
101
105
  select(fields, distinct = false) {
102
106
  this.ensureNotFinalized();
103
- this._fields = Utils.asArray(fields);
107
+ this._fields = Utils.asArray(fields).flatMap(f => {
108
+ if (typeof f !== 'string') {
109
+ // Normalize sql.ref('prop') and sql.ref('prop').as('alias') to string form
110
+ if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
111
+ return this.resolveNestedPath(String(f.params[0]));
112
+ }
113
+ if (isRaw(f) && f.sql === '?? as ??' && f.params.length === 2) {
114
+ return `${this.resolveNestedPath(String(f.params[0]))} as ${String(f.params[1])}`;
115
+ }
116
+ return f;
117
+ }
118
+ const asMatch = f.match(FIELD_ALIAS_RE);
119
+ if (asMatch) {
120
+ return `${this.resolveNestedPath(asMatch[1].trim())} as ${asMatch[2]}`;
121
+ }
122
+ return this.resolveNestedPath(f);
123
+ });
104
124
  if (distinct) {
105
125
  this.flags.add(QueryFlag.DISTINCT);
106
126
  }
107
127
  return this.init(QueryType.SELECT);
108
128
  }
129
+ /**
130
+ * Adds fields to an existing SELECT query.
131
+ */
109
132
  addSelect(fields) {
110
133
  this.ensureNotFinalized();
111
134
  if (this._type && this._type !== QueryType.SELECT) {
@@ -117,30 +140,87 @@ export class QueryBuilder {
117
140
  this.ensureNotFinalized();
118
141
  return this.setFlag(QueryFlag.DISTINCT);
119
142
  }
120
- /** postgres only */
121
143
  distinctOn(fields) {
122
144
  this.ensureNotFinalized();
123
145
  this._distinctOn = Utils.asArray(fields);
124
146
  return this;
125
147
  }
148
+ /**
149
+ * Creates an INSERT query with the given data.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * await em.createQueryBuilder(User)
154
+ * .insert({ name: 'John', email: 'john@example.com' })
155
+ * .execute();
156
+ *
157
+ * // Bulk insert
158
+ * await em.createQueryBuilder(User)
159
+ * .insert([{ name: 'John' }, { name: 'Jane' }])
160
+ * .execute();
161
+ * ```
162
+ */
126
163
  insert(data) {
127
164
  return this.init(QueryType.INSERT, data);
128
165
  }
166
+ /**
167
+ * Creates an UPDATE query with the given data.
168
+ * Use `where()` to specify which rows to update.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * await em.createQueryBuilder(User)
173
+ * .update({ name: 'John Doe' })
174
+ * .where({ id: 1 })
175
+ * .execute();
176
+ * ```
177
+ */
129
178
  update(data) {
130
179
  return this.init(QueryType.UPDATE, data);
131
180
  }
181
+ /**
182
+ * Creates a DELETE query.
183
+ * Use `where()` to specify which rows to delete.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * await em.createQueryBuilder(User)
188
+ * .delete()
189
+ * .where({ id: 1 })
190
+ * .execute();
191
+ *
192
+ * // Or pass the condition directly
193
+ * await em.createQueryBuilder(User)
194
+ * .delete({ isActive: false })
195
+ * .execute();
196
+ * ```
197
+ */
132
198
  delete(cond) {
133
199
  return this.init(QueryType.DELETE, undefined, cond);
134
200
  }
201
+ /**
202
+ * Creates a TRUNCATE query to remove all rows from the table.
203
+ */
135
204
  truncate() {
136
205
  return this.init(QueryType.TRUNCATE);
137
206
  }
207
+ /**
208
+ * Creates a COUNT query to count matching rows.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const count = await em.createQueryBuilder(User)
213
+ * .count()
214
+ * .where({ isActive: true })
215
+ * .execute('get');
216
+ * ```
217
+ */
138
218
  count(field, distinct = false) {
139
219
  if (field) {
140
220
  this._fields = Utils.asArray(field);
141
221
  }
142
222
  else if (distinct || this.hasToManyJoins()) {
143
- this._fields = this.mainAlias.metadata.primaryKeys;
223
+ this._fields = this.mainAlias.meta.primaryKeys;
144
224
  }
145
225
  else {
146
226
  this._fields = [raw('*')];
@@ -158,16 +238,27 @@ export class QueryBuilder {
158
238
  this.join(field, alias, cond, JoinType.innerJoin, undefined, schema);
159
239
  return this;
160
240
  }
161
- innerJoinLateral(field, alias, cond, schema) {
162
- this.join(field, alias, cond, JoinType.innerJoinLateral, undefined, schema);
163
- return this;
241
+ innerJoinLateral(field, alias, cond = {}, schema) {
242
+ return this.join(field, alias, cond, JoinType.innerJoinLateral, undefined, schema);
164
243
  }
165
244
  leftJoin(field, alias, cond = {}, schema) {
166
245
  return this.join(field, alias, cond, JoinType.leftJoin, undefined, schema);
167
246
  }
168
- leftJoinLateral(field, alias, cond, schema) {
247
+ leftJoinLateral(field, alias, cond = {}, schema) {
169
248
  return this.join(field, alias, cond, JoinType.leftJoinLateral, undefined, schema);
170
249
  }
250
+ /**
251
+ * Adds a JOIN clause and automatically selects the joined entity's fields.
252
+ * This is useful for eager loading related entities.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * const qb = em.createQueryBuilder(Book, 'b');
257
+ * qb.select('*')
258
+ * .joinAndSelect('b.author', 'a')
259
+ * .where({ 'a.name': 'John' });
260
+ * ```
261
+ */
171
262
  joinAndSelect(field, alias, cond = {}, type = JoinType.innerJoin, path, fields, schema) {
172
263
  if (!this._type) {
173
264
  this.select('*');
@@ -199,18 +290,22 @@ export class QueryBuilder {
199
290
  return this.joinAndSelect(field, alias, cond, JoinType.leftJoin, undefined, fields, schema);
200
291
  }
201
292
  leftJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
202
- return this.joinAndSelect(field, alias, cond, JoinType.leftJoinLateral, undefined, fields, schema);
293
+ this.joinAndSelect(field, alias, cond, JoinType.leftJoinLateral, undefined, fields, schema);
294
+ return this;
203
295
  }
204
296
  innerJoinAndSelect(field, alias, cond = {}, fields, schema) {
205
297
  return this.joinAndSelect(field, alias, cond, JoinType.innerJoin, undefined, fields, schema);
206
298
  }
207
299
  innerJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
208
- return this.joinAndSelect(field, alias, cond, JoinType.innerJoinLateral, undefined, fields, schema);
300
+ this.joinAndSelect(field, alias, cond, JoinType.innerJoinLateral, undefined, fields, schema);
301
+ return this;
209
302
  }
210
303
  getFieldsForJoinedLoad(prop, alias, explicitFields) {
211
304
  const fields = [];
212
305
  const populate = [];
213
306
  const joinKey = Object.keys(this._joins).find(join => join.endsWith(`#${alias}`));
307
+ const targetMeta = prop.targetMeta;
308
+ const schema = this._schema ?? (targetMeta.schema !== '*' ? targetMeta.schema : undefined);
214
309
  if (joinKey) {
215
310
  const path = this._joins[joinKey].path.split('.').slice(1);
216
311
  let children = this._populate;
@@ -223,29 +318,29 @@ export class QueryBuilder {
223
318
  }
224
319
  populate.push(...children);
225
320
  }
226
- for (const p of prop.targetMeta.getPrimaryProps()) {
227
- fields.push(...this.driver.mapPropToFieldNames(this, p, alias));
321
+ for (const p of targetMeta.getPrimaryProps()) {
322
+ fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
228
323
  }
229
- if (explicitFields) {
324
+ if (explicitFields && explicitFields.length > 0) {
230
325
  for (const field of explicitFields) {
231
326
  const [a, f] = this.helper.splitField(field);
232
- const p = prop.targetMeta.properties[f];
327
+ const p = targetMeta.properties[f];
233
328
  if (p) {
234
- fields.push(...this.driver.mapPropToFieldNames(this, p, alias));
329
+ fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
235
330
  }
236
331
  else {
237
332
  fields.push(`${a}.${f} as ${a}__${f}`);
238
333
  }
239
334
  }
240
335
  }
241
- prop.targetMeta.props
336
+ targetMeta.props
242
337
  .filter(prop => {
243
- if (!explicitFields) {
338
+ if (!explicitFields || explicitFields.length === 0) {
244
339
  return this.platform.shouldHaveColumn(prop, populate);
245
340
  }
246
341
  return prop.primary && !explicitFields.includes(prop.name) && !explicitFields.includes(`${alias}.${prop.name}`);
247
342
  })
248
- .forEach(prop => fields.push(...this.driver.mapPropToFieldNames(this, prop, alias)));
343
+ .forEach(prop => fields.push(...this.driver.mapPropToFieldNames(this, prop, alias, targetMeta, schema)));
249
344
  return fields;
250
345
  }
251
346
  /**
@@ -259,7 +354,6 @@ export class QueryBuilder {
259
354
  const cond = await this.em.applyFilters(this.mainAlias.entityName, {}, filterOptions, 'read');
260
355
  this.andWhere(cond);
261
356
  }
262
- autoJoinedPaths = [];
263
357
  /**
264
358
  * @internal
265
359
  */
@@ -276,15 +370,23 @@ export class QueryBuilder {
276
370
  continue;
277
371
  }
278
372
  filterOptions = QueryHelper.mergePropertyFilters(join.prop.filters, filterOptions);
279
- const cond = await em.applyFilters(join.prop.type, join.cond, filterOptions, 'read');
280
- if (Utils.hasObjectKeys(cond)) {
373
+ let cond = await em.applyFilters(join.prop.targetMeta.class, join.cond, filterOptions, 'read');
374
+ const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, join.prop.targetMeta.class, cond);
375
+ cond = criteriaNode.process(this, {
376
+ matchPopulateJoins: true,
377
+ filter: true,
378
+ alias: join.alias,
379
+ ignoreBranching: true,
380
+ parentPath: join.path,
381
+ });
382
+ if (Utils.hasObjectKeys(cond) || RawQueryFragment.hasObjectFragments(cond)) {
281
383
  // remove nested filters, we only care about scalars here, nesting would require another join branch
282
384
  for (const key of Object.keys(cond)) {
283
- if (Utils.isPlainObject(cond[key]) && Object.keys(cond[key]).every(k => !(Utils.isOperator(k) && !['$some', '$none', '$every'].includes(k)))) {
385
+ if (Utils.isPlainObject(cond[key]) && Object.keys(cond[key]).every(k => !(Utils.isOperator(k) && !['$some', '$none', '$every', '$size'].includes(k)))) {
284
386
  delete cond[key];
285
387
  }
286
388
  }
287
- if (Utils.hasObjectKeys(join.cond)) {
389
+ if (Utils.hasObjectKeys(join.cond) || RawQueryFragment.hasObjectFragments(join.cond)) {
288
390
  /* v8 ignore next */
289
391
  join.cond = { $and: [join.cond, cond] };
290
392
  }
@@ -296,7 +398,7 @@ export class QueryBuilder {
296
398
  }
297
399
  withSubQuery(subQuery, alias) {
298
400
  this.ensureNotFinalized();
299
- if (subQuery instanceof RawQueryFragment) {
401
+ if (isRaw(subQuery)) {
300
402
  this.subQueries[alias] = this.platform.formatQuery(subQuery.sql, subQuery.params);
301
403
  }
302
404
  else {
@@ -306,18 +408,18 @@ export class QueryBuilder {
306
408
  }
307
409
  where(cond, params, operator) {
308
410
  this.ensureNotFinalized();
309
- const rawField = RawQueryFragment.getKnownFragment(cond);
310
- if (rawField) {
311
- const sql = this.platform.formatQuery(rawField.sql, rawField.params);
312
- cond = { [raw(`(${sql})`)]: Utils.asArray(params) };
411
+ let processedCond;
412
+ if (isRaw(cond)) {
413
+ const sql = this.platform.formatQuery(cond.sql, cond.params);
414
+ processedCond = { [raw(`(${sql})`)]: Utils.asArray(params) };
313
415
  operator ??= '$and';
314
416
  }
315
417
  else if (typeof cond === 'string') {
316
- cond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
418
+ processedCond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
317
419
  operator ??= '$and';
318
420
  }
319
421
  else {
320
- cond = QueryHelper.processWhere({
422
+ processedCond = QueryHelper.processWhere({
321
423
  where: cond,
322
424
  entityName: this.mainAlias.entityName,
323
425
  metadata: this.metadata,
@@ -328,13 +430,13 @@ export class QueryBuilder {
328
430
  });
329
431
  }
330
432
  const op = operator || params;
331
- const topLevel = !op || !Utils.hasObjectKeys(this._cond);
332
- const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond);
433
+ const topLevel = !op || !(Utils.hasObjectKeys(this._cond) || RawQueryFragment.hasObjectFragments(this._cond));
434
+ const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processedCond);
333
435
  const ignoreBranching = this.__populateWhere === 'infer';
334
436
  if ([QueryType.UPDATE, QueryType.DELETE].includes(this.type) && criteriaNode.willAutoJoin(this, undefined, { ignoreBranching })) {
335
437
  // use sub-query to support joining
336
438
  this.setFlag(this.type === QueryType.UPDATE ? QueryFlag.UPDATE_SUB_QUERY : QueryFlag.DELETE_SUB_QUERY);
337
- this.select(this.mainAlias.metadata.primaryKeys, true);
439
+ this.select(this.mainAlias.meta.primaryKeys, true);
338
440
  }
339
441
  if (topLevel) {
340
442
  this._cond = criteriaNode.process(this, { ignoreBranching });
@@ -369,7 +471,19 @@ export class QueryBuilder {
369
471
  if (reset) {
370
472
  this._orderBy = [];
371
473
  }
372
- Utils.asArray(orderBy).forEach(o => {
474
+ const selectAliases = this.getSelectAliases();
475
+ Utils.asArray(orderBy).forEach(orig => {
476
+ // Shallow clone to avoid mutating the caller's object — safe because the clone
477
+ // is only used within this loop iteration and `orig` is not referenced afterward.
478
+ const o = { ...orig };
479
+ // Wrap known select aliases in raw() so they bypass property validation and alias prefixing
480
+ for (const key of Object.keys(o)) {
481
+ if (selectAliases.has(key)) {
482
+ o[raw('??', [key])] = o[key];
483
+ delete o[key];
484
+ }
485
+ }
486
+ this.helper.validateQueryOrder(o);
373
487
  const processed = QueryHelper.processWhere({
374
488
  where: o,
375
489
  entityName: this.mainAlias.entityName,
@@ -380,26 +494,61 @@ export class QueryBuilder {
380
494
  convertCustomTypes: false,
381
495
  type: 'orderBy',
382
496
  });
383
- this._orderBy.push(CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, { matchPopulateJoins: true, type: 'orderBy' }));
497
+ this._orderBy.push(CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, {
498
+ matchPopulateJoins: true,
499
+ type: 'orderBy',
500
+ }));
384
501
  });
385
502
  return this;
386
503
  }
504
+ /** Collect custom aliases from select fields (stored as 'resolved as alias' strings by select()). */
505
+ getSelectAliases() {
506
+ const aliases = new Set();
507
+ for (const field of this._fields ?? []) {
508
+ if (typeof field === 'string') {
509
+ const m = field.match(FIELD_ALIAS_RE);
510
+ if (m) {
511
+ aliases.add(m[2]);
512
+ }
513
+ }
514
+ }
515
+ return aliases;
516
+ }
387
517
  groupBy(fields) {
388
518
  this.ensureNotFinalized();
389
- this._groupBy = Utils.asArray(fields);
519
+ this._groupBy = Utils.asArray(fields).flatMap(f => {
520
+ if (typeof f !== 'string') {
521
+ // Normalize sql.ref('prop') to string for proper formula resolution
522
+ if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
523
+ return this.resolveNestedPath(String(f.params[0]));
524
+ }
525
+ return f;
526
+ }
527
+ return this.resolveNestedPath(f);
528
+ });
390
529
  return this;
391
530
  }
531
+ /**
532
+ * Adds a HAVING clause to the query, typically used with GROUP BY.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * qb.select([raw('count(*) as count'), 'status'])
537
+ * .groupBy('status')
538
+ * .having({ count: { $gt: 5 } });
539
+ * ```
540
+ */
392
541
  having(cond = {}, params, operator) {
393
542
  this.ensureNotFinalized();
394
543
  if (typeof cond === 'string') {
395
544
  cond = { [raw(`(${cond})`, params)]: [] };
396
545
  }
397
- cond = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond).process(this);
546
+ const processed = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond, undefined, undefined, false).process(this, { type: 'having' });
398
547
  if (!this._having || !operator) {
399
- this._having = cond;
548
+ this._having = processed;
400
549
  }
401
550
  else {
402
- const cond1 = [this._having, cond];
551
+ const cond1 = [this._having, processed];
403
552
  this._having = { [operator]: cond1 };
404
553
  }
405
554
  return this;
@@ -411,7 +560,7 @@ export class QueryBuilder {
411
560
  return this.having(cond, params, '$or');
412
561
  }
413
562
  onConflict(fields = []) {
414
- const meta = this.mainAlias.metadata;
563
+ const meta = this.mainAlias.meta;
415
564
  this.ensureNotFinalized();
416
565
  this._onConflict ??= [];
417
566
  this._onConflict.push({
@@ -456,6 +605,15 @@ export class QueryBuilder {
456
605
  this._populateFilter = populateFilter;
457
606
  return this;
458
607
  }
608
+ /**
609
+ * Sets a LIMIT clause to restrict the number of results.
610
+ *
611
+ * @example
612
+ * ```ts
613
+ * qb.select('*').limit(10); // First 10 results
614
+ * qb.select('*').limit(10, 20); // 10 results starting from offset 20
615
+ * ```
616
+ */
459
617
  limit(limit, offset = 0) {
460
618
  this.ensureNotFinalized();
461
619
  this._limit = limit;
@@ -464,6 +622,14 @@ export class QueryBuilder {
464
622
  }
465
623
  return this;
466
624
  }
625
+ /**
626
+ * Sets an OFFSET clause to skip a number of results.
627
+ *
628
+ * @example
629
+ * ```ts
630
+ * qb.select('*').limit(10).offset(20); // Results 21-30
631
+ * ```
632
+ */
467
633
  offset(offset) {
468
634
  this.ensureNotFinalized();
469
635
  this._offset = offset;
@@ -514,6 +680,14 @@ export class QueryBuilder {
514
680
  this._indexHint = sql;
515
681
  return this;
516
682
  }
683
+ /**
684
+ * Adds COLLATE clause to ORDER BY expressions.
685
+ */
686
+ collation(collation) {
687
+ this.ensureNotFinalized();
688
+ this._collation = collation;
689
+ return this;
690
+ }
517
691
  /**
518
692
  * Prepend comment to the sql query using the syntax `/* ... *‍/`. Some characters are forbidden such as `/*, *‍/` and `?`.
519
693
  */
@@ -538,11 +712,10 @@ export class QueryBuilder {
538
712
  this.fromSubQuery(target, aliasName);
539
713
  }
540
714
  else {
541
- const entityName = Utils.className(target);
542
- if (aliasName && this._mainAlias && entityName !== this._mainAlias.aliasName) {
715
+ if (aliasName && this._mainAlias && Utils.className(target) !== this._mainAlias.aliasName) {
543
716
  throw new Error(`Cannot override the alias to '${aliasName}' since a query already contains references to '${this._mainAlias.aliasName}'`);
544
717
  }
545
- this.fromEntityName(entityName, aliasName);
718
+ this.fromEntityName(target, aliasName);
546
719
  }
547
720
  return this;
548
721
  }
@@ -553,17 +726,19 @@ export class QueryBuilder {
553
726
  this._query = {};
554
727
  this.finalize();
555
728
  const qb = this.getQueryBase(processVirtualEntity);
729
+ const schema = this.getSchema(this.mainAlias);
730
+ const isNotEmptyObject = (obj) => Utils.hasObjectKeys(obj) || RawQueryFragment.hasObjectFragments(obj);
556
731
  Utils.runIfNotEmpty(() => this.helper.appendQueryCondition(this.type, this._cond, qb), this._cond && !this._onConflict);
557
- Utils.runIfNotEmpty(() => qb.groupBy(this.prepareFields(this._groupBy, 'groupBy')), this._groupBy);
558
- Utils.runIfNotEmpty(() => this.helper.appendQueryCondition(this.type, this._having, qb, undefined, 'having'), this._having);
732
+ Utils.runIfNotEmpty(() => qb.groupBy(this.prepareFields(this._groupBy, 'groupBy', schema)), isNotEmptyObject(this._groupBy));
733
+ Utils.runIfNotEmpty(() => this.helper.appendQueryCondition(this.type, this._having, qb, undefined, 'having'), isNotEmptyObject(this._having));
559
734
  Utils.runIfNotEmpty(() => {
560
- const queryOrder = this.helper.getQueryOrder(this.type, this._orderBy, this._populateMap);
735
+ const queryOrder = this.helper.getQueryOrder(this.type, this._orderBy, this._populateMap, this._collation);
561
736
  if (queryOrder.length > 0) {
562
737
  const sql = Utils.unique(queryOrder).join(', ');
563
738
  qb.orderBy(sql);
564
739
  return;
565
740
  }
566
- }, this._orderBy);
741
+ }, isNotEmptyObject(this._orderBy));
567
742
  Utils.runIfNotEmpty(() => qb.limit(this._limit), this._limit != null);
568
743
  Utils.runIfNotEmpty(() => qb.offset(this._offset), this._offset);
569
744
  Utils.runIfNotEmpty(() => qb.comment(this._comments), this._comments);
@@ -572,17 +747,9 @@ export class QueryBuilder {
572
747
  if (this.lockMode) {
573
748
  this.helper.getLockSQL(qb, this.lockMode, this.lockTables, this._joins);
574
749
  }
575
- this.helper.finalize(this.type, qb, this.mainAlias.metadata, this._data, this._returning);
576
- this.clearRawFragmentsCache();
750
+ this.helper.finalize(this.type, qb, this.mainAlias.meta, this._data, this._returning);
577
751
  return this._query.qb = qb;
578
752
  }
579
- /**
580
- * @internal
581
- */
582
- clearRawFragmentsCache() {
583
- this.rawFragments.forEach(key => RawQueryFragment.remove(key));
584
- this.rawFragments.clear();
585
- }
586
753
  /**
587
754
  * Returns the query with parameters as wildcards.
588
755
  */
@@ -622,7 +789,7 @@ export class QueryBuilder {
622
789
  * @internal
623
790
  */
624
791
  getAliasForJoinPath(path, options) {
625
- if (!path || path === this.mainAlias.entityName) {
792
+ if (!path || path === Utils.className(this.mainAlias.entityName)) {
626
793
  return this.mainAlias.aliasName;
627
794
  }
628
795
  const join = typeof path === 'string' ? this.getJoinForPath(path, options) : path;
@@ -666,8 +833,24 @@ export class QueryBuilder {
666
833
  * @internal
667
834
  */
668
835
  getNextAlias(entityName = 'e') {
836
+ entityName = Utils.className(entityName);
669
837
  return this.driver.config.getNamingStrategy().aliasName(entityName, this.aliasCounter++);
670
838
  }
839
+ /**
840
+ * Registers a join for a specific polymorphic target type.
841
+ * Used by the driver to create per-target LEFT JOINs for JOINED loading.
842
+ * @internal
843
+ */
844
+ addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
845
+ // Override referencedColumnNames to use the specific target's PK columns
846
+ // (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
847
+ const referencedColumnNames = targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
848
+ const targetProp = { ...prop, targetMeta, referencedColumnNames };
849
+ const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
850
+ this._joins[aliasedName] = this.helper.joinManyToOneReference(targetProp, ownerAlias, alias, type, {}, schema);
851
+ this._joins[aliasedName].path = path;
852
+ this.createAlias(targetMeta.class, alias);
853
+ }
671
854
  /**
672
855
  * @internal
673
856
  */
@@ -697,7 +880,7 @@ export class QueryBuilder {
697
880
  }
698
881
  const loggerContext = { id: this.em?.id, ...this.loggerContext };
699
882
  const res = await this.getConnection().execute(query.sql, query.params, method, this.context, loggerContext);
700
- const meta = this.mainAlias.metadata;
883
+ const meta = this.mainAlias.meta;
701
884
  if (!options.mapResults || !meta) {
702
885
  await this.em?.storeCache(this._cache, cached, res);
703
886
  return res;
@@ -711,7 +894,7 @@ export class QueryBuilder {
711
894
  const map = {};
712
895
  mapped = res.map(r => this.driver.mapResult(r, meta, this._populate, this, map));
713
896
  if (options.mergeResults && joinedProps.length > 0) {
714
- mapped = this.driver.mergeJoinedResult(mapped, this.mainAlias.metadata, joinedProps);
897
+ mapped = this.driver.mergeJoinedResult(mapped, this.mainAlias.meta, joinedProps);
715
898
  }
716
899
  }
717
900
  else {
@@ -752,7 +935,7 @@ export class QueryBuilder {
752
935
  const query = this.toQuery();
753
936
  const loggerContext = { id: this.em?.id, ...this.loggerContext };
754
937
  const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
755
- const meta = this.mainAlias.metadata;
938
+ const meta = this.mainAlias.meta;
756
939
  if (options.rawResults || !meta) {
757
940
  yield* res;
758
941
  return;
@@ -769,7 +952,7 @@ export class QueryBuilder {
769
952
  continue;
770
953
  }
771
954
  if (stack.length > 0 && hash(stack[stack.length - 1]) !== hash(mapped)) {
772
- const res = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
955
+ const res = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
773
956
  for (const row of res) {
774
957
  yield this.mapResult(row, options.mapResults);
775
958
  }
@@ -778,7 +961,7 @@ export class QueryBuilder {
778
961
  stack.push(mapped);
779
962
  }
780
963
  if (stack.length > 0) {
781
- const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
964
+ const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
782
965
  yield this.mapResult(merged[0], options.mapResults);
783
966
  }
784
967
  }
@@ -840,16 +1023,13 @@ export class QueryBuilder {
840
1023
  const [res] = await this.getResultList(1);
841
1024
  return res || null;
842
1025
  }
843
- /**
844
- * Executes count query (without offset and limit), returning total count of results
845
- */
846
1026
  async getCount(field, distinct) {
847
1027
  let res;
848
1028
  if (this.type === QueryType.COUNT) {
849
1029
  res = await this.execute('get', false);
850
1030
  }
851
1031
  else {
852
- const qb = this._type === undefined ? this : this.clone();
1032
+ const qb = (this._type === undefined ? this : this.clone());
853
1033
  qb.processPopulateHint(); // needs to happen sooner so `qb.hasToManyJoins()` reports correctly
854
1034
  qb.count(field, distinct ?? qb.hasToManyJoins()).limit(undefined).offset(undefined).orderBy([]);
855
1035
  res = await qb.execute('get', false);
@@ -860,50 +1040,42 @@ export class QueryBuilder {
860
1040
  * Executes the query, returning both array of results and total count query (without offset and limit).
861
1041
  */
862
1042
  async getResultAndCount() {
863
- return [
864
- await this.clone().getResultList(),
865
- await this.clone().getCount(),
866
- ];
1043
+ return [await this.clone().getResultList(), await this.clone().getCount()];
867
1044
  }
868
- /**
869
- * Returns native query builder instance with sub-query aliased with given alias.
870
- * You can provide `EntityName.propName` as alias, then the field name will be used based on the metadata
871
- */
872
- as(alias) {
1045
+ as(aliasOrTargetEntity, alias) {
873
1046
  const qb = this.getNativeQuery();
874
- if (alias.includes('.')) {
875
- const [a, f] = alias.split('.');
876
- const meta = this.metadata.find(a);
1047
+ let finalAlias = aliasOrTargetEntity;
1048
+ /* v8 ignore next */
1049
+ if (typeof aliasOrTargetEntity === 'string' && aliasOrTargetEntity.includes('.')) {
1050
+ throw new Error('qb.as(alias) no longer supports target entity name prefix, use qb.as(TargetEntity, key) signature instead');
1051
+ }
1052
+ if (alias) {
1053
+ const meta = this.metadata.get(aliasOrTargetEntity);
877
1054
  /* v8 ignore next */
878
- alias = meta?.properties[f]?.fieldNames[0] ?? alias;
1055
+ finalAlias = meta.properties[alias]?.fieldNames[0] ?? alias;
879
1056
  }
880
- qb.as(alias);
1057
+ qb.as(finalAlias);
881
1058
  // tag the instance, so it is possible to detect it easily
882
- Object.defineProperty(qb, '__as', { enumerable: false, value: alias });
1059
+ Object.defineProperty(qb, '__as', { enumerable: false, value: finalAlias });
883
1060
  return qb;
884
1061
  }
885
- clone(reset) {
1062
+ clone(reset, preserve) {
886
1063
  const qb = new QueryBuilder(this.mainAlias.entityName, this.metadata, this.driver, this.context, this.mainAlias.aliasName, this.connectionType, this.em);
887
- if (reset === true) {
888
- return qb;
889
- }
890
1064
  reset = reset || [];
891
1065
  // clone array/object properties
892
1066
  const properties = [
893
1067
  'flags', '_populate', '_populateWhere', '_populateFilter', '__populateWhere', '_populateMap', '_joins', '_joinedProps', '_cond', '_data', '_orderBy',
894
- '_schema', '_indexHint', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning',
895
- '_comments', '_hintComments', 'rawFragments', 'aliasCounter',
1068
+ '_schema', '_indexHint', '_collation', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning',
1069
+ '_comments', '_hintComments', 'aliasCounter',
896
1070
  ];
897
- RawQueryFragment.cloneRegistry = this.rawFragments;
898
1071
  for (const prop of Object.keys(this)) {
899
- if (reset.includes(prop) || ['_helper', '_query'].includes(prop)) {
1072
+ if (!preserve?.includes(prop) && (reset === true || reset.includes(prop) || ['_helper', '_query'].includes(prop))) {
900
1073
  continue;
901
1074
  }
902
1075
  qb[prop] = properties.includes(prop) ? Utils.copy(this[prop]) : this[prop];
903
1076
  }
904
- delete RawQueryFragment.cloneRegistry;
905
1077
  /* v8 ignore next */
906
- if (this._fields && !reset.includes('_fields')) {
1078
+ if (this._fields && reset !== true && !reset.includes('_fields')) {
907
1079
  qb._fields = [...this._fields];
908
1080
  }
909
1081
  qb._aliases = { ...this._aliases };
@@ -935,13 +1107,28 @@ export class QueryBuilder {
935
1107
  if (res instanceof QueryBuilder) {
936
1108
  return `(${res.getFormattedQuery()}) as ${this.platform.quoteIdentifier(this.alias)}`;
937
1109
  }
938
- if (res instanceof RawQueryFragment) {
1110
+ if (isRaw(res)) {
939
1111
  const query = this.platform.formatQuery(res.sql, res.params);
940
1112
  return `(${query}) as ${this.platform.quoteIdentifier(this.alias)}`;
941
1113
  }
942
1114
  /* v8 ignore next */
943
1115
  return res;
944
1116
  }
1117
+ /**
1118
+ * Adds a join from a property object. Used internally for TPT joins where the property
1119
+ * is synthetic (not in entity.properties) but defined on metadata (e.g., tptParentProp).
1120
+ * The caller must create the alias first via createAlias().
1121
+ * @internal
1122
+ */
1123
+ addPropertyJoin(prop, ownerAlias, alias, type, path, schema) {
1124
+ schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta);
1125
+ const key = `[tpt]${ownerAlias}#${alias}`;
1126
+ this._joins[key] = prop.kind === ReferenceKind.MANY_TO_ONE
1127
+ ? this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, {}, schema)
1128
+ : this.helper.joinOneToReference(prop, ownerAlias, alias, type, {}, schema);
1129
+ this._joins[key].path = path;
1130
+ return key;
1131
+ }
945
1132
  joinReference(field, alias, cond, type, path, schema, subquery) {
946
1133
  this.ensureNotFinalized();
947
1134
  if (typeof field === 'object') {
@@ -950,11 +1137,11 @@ export class QueryBuilder {
950
1137
  kind: ReferenceKind.MANY_TO_ONE,
951
1138
  };
952
1139
  if (field instanceof QueryBuilder) {
953
- prop.type = field.mainAlias.entityName;
954
- prop.targetMeta = field.mainAlias.metadata;
1140
+ prop.type = Utils.className(field.mainAlias.entityName);
1141
+ prop.targetMeta = field.mainAlias.meta;
955
1142
  field = field.getNativeQuery();
956
1143
  }
957
- if (field instanceof RawQueryFragment) {
1144
+ if (isRaw(field)) {
958
1145
  field = this.platform.formatQuery(field.sql, field.params);
959
1146
  }
960
1147
  const key = `${this.alias}.${prop.name}#${alias}`;
@@ -983,7 +1170,12 @@ export class QueryBuilder {
983
1170
  if (!prop) {
984
1171
  throw new Error(`Trying to join ${q(field)}, but ${q(fromField)} is not a defined relation on ${meta.className}.`);
985
1172
  }
986
- this.createAlias(prop.type, alias);
1173
+ // For TPT inheritance, owning relations (M:1 and owning 1:1) may have FK columns in a parent table
1174
+ // Resolve the correct alias for the table that owns the FK column
1175
+ const ownerAlias = (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))
1176
+ ? this.helper.getTPTAliasForProperty(fromField, fromAlias)
1177
+ : fromAlias;
1178
+ this.createAlias(prop.targetMeta.class, alias);
987
1179
  cond = QueryHelper.processWhere({
988
1180
  where: cond,
989
1181
  entityName: this.mainAlias.entityName,
@@ -992,10 +1184,10 @@ export class QueryBuilder {
992
1184
  aliasMap: this.getAliasMap(),
993
1185
  aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
994
1186
  });
995
- const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.className, cond);
1187
+ const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
996
1188
  cond = criteriaNode.process(this, { ignoreBranching: true, alias });
997
1189
  let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
998
- path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? entityName)}.${prop.name}`;
1190
+ path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName))}.${prop.name}`;
999
1191
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
1000
1192
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
1001
1193
  this._joins[aliasedName].path ??= path;
@@ -1014,29 +1206,33 @@ export class QueryBuilder {
1014
1206
  aliasedName = Object.keys(joins)[1];
1015
1207
  }
1016
1208
  else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
1017
- this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
1209
+ this._joins[aliasedName] = this.helper.joinOneToReference(prop, ownerAlias, alias, type, cond, schema);
1018
1210
  this._joins[aliasedName].path ??= path;
1019
1211
  }
1020
1212
  else { // MANY_TO_ONE
1021
- this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond, schema);
1213
+ this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
1022
1214
  this._joins[aliasedName].path ??= path;
1023
1215
  }
1024
1216
  return { prop, key: aliasedName };
1025
1217
  }
1026
- prepareFields(fields, type = 'where') {
1218
+ prepareFields(fields, type = 'where', schema) {
1027
1219
  const ret = [];
1028
- const getFieldName = (name) => {
1029
- return this.helper.mapper(name, this.type, undefined, type === 'groupBy' ? null : undefined);
1220
+ const getFieldName = (name, customAlias) => {
1221
+ const alias = customAlias ?? (type === 'groupBy' ? null : undefined);
1222
+ return this.helper.mapper(name, this.type, undefined, alias, schema);
1030
1223
  };
1031
- fields.forEach(field => {
1032
- const rawField = RawQueryFragment.getKnownFragment(field, false);
1033
- if (rawField) {
1034
- ret.push(rawField);
1224
+ fields.forEach(originalField => {
1225
+ if (typeof originalField !== 'string') {
1226
+ ret.push(originalField);
1035
1227
  return;
1036
1228
  }
1037
- if (typeof field !== 'string') {
1038
- ret.push(field);
1039
- return;
1229
+ // Strip 'as alias' suffix if present the alias is passed to mapper at the end
1230
+ let field = originalField;
1231
+ let customAlias;
1232
+ const asMatch = originalField.match(FIELD_ALIAS_RE);
1233
+ if (asMatch) {
1234
+ field = asMatch[1].trim();
1235
+ customAlias = asMatch[2];
1040
1236
  }
1041
1237
  const join = Object.keys(this._joins).find(k => field === k.substring(0, k.indexOf('#')));
1042
1238
  if (join && type === 'where') {
@@ -1055,10 +1251,13 @@ export class QueryBuilder {
1055
1251
  if (prop?.embedded || (prop?.kind === ReferenceKind.EMBEDDED && prop.object)) {
1056
1252
  const name = prop.embeddedPath?.join('.') ?? prop.fieldNames[0];
1057
1253
  const aliased = this._aliases[a] ? `${a}.${name}` : name;
1058
- ret.push(getFieldName(aliased));
1254
+ ret.push(getFieldName(aliased, customAlias));
1059
1255
  return;
1060
1256
  }
1061
1257
  if (prop?.kind === ReferenceKind.EMBEDDED) {
1258
+ if (customAlias) {
1259
+ throw new Error(`Cannot use 'as ${customAlias}' alias on embedded property '${field}' because it expands to multiple columns. Alias individual fields instead (e.g. '${field}.propertyName as ${customAlias}').`);
1260
+ }
1062
1261
  const nest = (prop) => {
1063
1262
  for (const childProp of Object.values(prop.embeddedProps)) {
1064
1263
  if (childProp.fieldNames && (childProp.kind !== ReferenceKind.EMBEDDED || childProp.object) && childProp.persist !== false) {
@@ -1072,30 +1271,108 @@ export class QueryBuilder {
1072
1271
  nest(prop);
1073
1272
  return;
1074
1273
  }
1075
- if (prop && prop.fieldNames.length > 1) {
1274
+ if (prop && prop.fieldNames.length > 1 && !prop.fieldNames.includes(f)) {
1275
+ if (customAlias) {
1276
+ throw new Error(`Cannot use 'as ${customAlias}' alias on '${field}' because it expands to multiple columns (${prop.fieldNames.join(', ')}).`);
1277
+ }
1076
1278
  ret.push(...prop.fieldNames.map(f => getFieldName(f)));
1077
1279
  return;
1078
1280
  }
1079
- ret.push(getFieldName(field));
1281
+ ret.push(getFieldName(field, customAlias));
1080
1282
  });
1081
- const meta = this.mainAlias.metadata;
1082
- /* v8 ignore next */
1083
- const requiresSQLConversion = meta?.props.filter(p => p.hasConvertToJSValueSQL && p.persist !== false) ?? [];
1084
- if (this.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES) && (fields.includes('*') || fields.includes(`${this.mainAlias.aliasName}.*`)) && requiresSQLConversion.length > 0) {
1283
+ const requiresSQLConversion = this.mainAlias.meta.props.filter(p => p.hasConvertToJSValueSQL && p.persist !== false);
1284
+ if (this.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES) &&
1285
+ (fields.includes('*') || fields.includes(`${this.mainAlias.aliasName}.*`)) &&
1286
+ requiresSQLConversion.length > 0) {
1085
1287
  for (const p of requiresSQLConversion) {
1086
1288
  ret.push(this.helper.mapper(p.name, this.type));
1087
1289
  }
1088
1290
  }
1089
1291
  for (const f of Object.keys(this._populateMap)) {
1090
1292
  if (type === 'where' && this._joins[f]) {
1091
- const cols = this.helper.mapJoinColumns(this.type, this._joins[f]);
1092
- for (const col of cols) {
1093
- ret.push(col);
1094
- }
1293
+ ret.push(...this.helper.mapJoinColumns(this.type, this._joins[f]));
1095
1294
  }
1096
1295
  }
1097
1296
  return Utils.unique(ret);
1098
1297
  }
1298
+ /**
1299
+ * Resolves nested paths like `a.books.title` to their actual field references.
1300
+ * Auto-joins relations as needed and returns `{alias}.{field}`.
1301
+ * For embeddeds: navigates into flattened embeddeds to return the correct field name.
1302
+ */
1303
+ resolveNestedPath(field) {
1304
+ if (typeof field !== 'string' || !field.includes('.')) {
1305
+ return field;
1306
+ }
1307
+ const parts = field.split('.');
1308
+ // Simple alias.property case - let prepareFields handle it
1309
+ if (parts.length === 2 && this._aliases[parts[0]]) {
1310
+ return field;
1311
+ }
1312
+ // Start with root alias
1313
+ let currentAlias = parts[0];
1314
+ let currentMeta = this._aliases[currentAlias] ? this.metadata.get(this._aliases[currentAlias].entityName) : this.mainAlias.meta;
1315
+ // If first part is not an alias, it's a property of the main entity
1316
+ if (!this._aliases[currentAlias]) {
1317
+ currentAlias = this.mainAlias.aliasName;
1318
+ parts.unshift(currentAlias);
1319
+ }
1320
+ // Walk through the path parts (skip the alias)
1321
+ for (let i = 1; i < parts.length; i++) {
1322
+ const propName = parts[i];
1323
+ const prop = currentMeta.properties[propName];
1324
+ if (!prop) {
1325
+ return field; // Unknown property, return as-is for raw SQL support
1326
+ }
1327
+ const isLastPart = i === parts.length - 1;
1328
+ // Handle embedded properties - navigate into flattened embeddeds
1329
+ if (prop.kind === ReferenceKind.EMBEDDED) {
1330
+ if (prop.object) {
1331
+ return `${currentAlias}.${propName}`;
1332
+ }
1333
+ // Navigate through remaining path to find the leaf property
1334
+ const remainingPath = parts.slice(i + 1);
1335
+ let embeddedProp = prop;
1336
+ for (const part of remainingPath) {
1337
+ embeddedProp = embeddedProp?.embeddedProps?.[part];
1338
+ if (embeddedProp?.object && embeddedProp.fieldNames?.[0]) {
1339
+ return `${currentAlias}.${embeddedProp.fieldNames[0]}`;
1340
+ }
1341
+ }
1342
+ return `${currentAlias}.${embeddedProp?.fieldNames?.[0] ?? propName}`;
1343
+ }
1344
+ // Handle relations - auto-join if not the last part
1345
+ if (prop.kind === ReferenceKind.MANY_TO_ONE ||
1346
+ prop.kind === ReferenceKind.ONE_TO_ONE ||
1347
+ prop.kind === ReferenceKind.ONE_TO_MANY ||
1348
+ prop.kind === ReferenceKind.MANY_TO_MANY) {
1349
+ if (isLastPart) {
1350
+ return `${currentAlias}.${propName}`;
1351
+ }
1352
+ // Find existing join or create new one
1353
+ const joinPath = parts.slice(0, i + 1).join('.');
1354
+ const existingJoinKey = Object.keys(this._joins).find(k => {
1355
+ const join = this._joins[k];
1356
+ // Check by path or by key prefix (key format is `alias.field#joinAlias`)
1357
+ return join.path === joinPath || k.startsWith(`${currentAlias}.${propName}#`);
1358
+ });
1359
+ let joinAlias;
1360
+ if (existingJoinKey) {
1361
+ joinAlias = this._joins[existingJoinKey].alias;
1362
+ }
1363
+ else {
1364
+ joinAlias = this.getNextAlias(prop.targetMeta?.className ?? propName);
1365
+ this.join(`${currentAlias}.${propName}`, joinAlias, {}, JoinType.leftJoin);
1366
+ }
1367
+ currentAlias = joinAlias;
1368
+ currentMeta = prop.targetMeta;
1369
+ continue;
1370
+ }
1371
+ // Scalar property - return it (if not last part, it's an invalid path but let SQL handle it)
1372
+ return `${currentAlias}.${propName}`;
1373
+ }
1374
+ return field;
1375
+ }
1099
1376
  init(type, data, cond) {
1100
1377
  this.ensureNotFinalized();
1101
1378
  this._type = type;
@@ -1118,14 +1395,14 @@ export class QueryBuilder {
1118
1395
  }
1119
1396
  getQueryBase(processVirtualEntity) {
1120
1397
  const qb = this.platform.createNativeQueryBuilder().setFlags(this.flags);
1121
- const { subQuery, aliasName, entityName, metadata } = this.mainAlias;
1398
+ const { subQuery, aliasName, entityName, meta } = this.mainAlias;
1122
1399
  const requiresAlias = this.finalized && (this._explicitAlias || this.helper.isTableNameAliasRequired(this.type));
1123
1400
  const alias = requiresAlias ? aliasName : undefined;
1124
1401
  const schema = this.getSchema(this.mainAlias);
1125
1402
  const tableName = subQuery ? subQuery.as(aliasName) : this.helper.getTableName(entityName);
1126
1403
  const joinSchema = this._schema ?? this.em?.schema ?? schema;
1127
- if (metadata?.virtual && processVirtualEntity) {
1128
- qb.from(raw(this.fromVirtual(metadata)), { indexHint: this._indexHint });
1404
+ if (meta.virtual && processVirtualEntity) {
1405
+ qb.from(raw(this.fromVirtual(meta)), { indexHint: this._indexHint });
1129
1406
  }
1130
1407
  else {
1131
1408
  qb.from(tableName, {
@@ -1136,9 +1413,9 @@ export class QueryBuilder {
1136
1413
  }
1137
1414
  switch (this.type) {
1138
1415
  case QueryType.SELECT:
1139
- qb.select(this.prepareFields(this._fields));
1416
+ qb.select(this.prepareFields(this._fields, 'where', schema));
1140
1417
  if (this._distinctOn) {
1141
- qb.distinctOn(this.prepareFields(this._distinctOn));
1418
+ qb.distinctOn(this.prepareFields(this._distinctOn, 'where', schema));
1142
1419
  }
1143
1420
  else if (this.flags.has(QueryFlag.DISTINCT)) {
1144
1421
  qb.distinct();
@@ -1146,7 +1423,7 @@ export class QueryBuilder {
1146
1423
  this.helper.processJoins(qb, this._joins, joinSchema);
1147
1424
  break;
1148
1425
  case QueryType.COUNT: {
1149
- const fields = this._fields.map(f => this.helper.mapper(f, this.type));
1426
+ const fields = this._fields.map(f => this.helper.mapper(f, this.type, undefined, undefined, schema));
1150
1427
  qb.count(fields, this.flags.has(QueryFlag.DISTINCT));
1151
1428
  this.helper.processJoins(qb, this._joins, joinSchema);
1152
1429
  break;
@@ -1169,23 +1446,111 @@ export class QueryBuilder {
1169
1446
  return qb;
1170
1447
  }
1171
1448
  applyDiscriminatorCondition() {
1172
- const meta = this.mainAlias.metadata;
1173
- if (!meta?.discriminatorValue) {
1449
+ const meta = this.mainAlias.meta;
1450
+ if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
1174
1451
  return;
1175
1452
  }
1176
- const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.find(cls));
1453
+ const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
1177
1454
  const children = [];
1178
1455
  const lookUpChildren = (ret, type) => {
1179
1456
  const children = types.filter(meta2 => meta2.extends === type);
1180
- children.forEach(m => lookUpChildren(ret, m.className));
1457
+ children.forEach(m => lookUpChildren(ret, m.class));
1181
1458
  ret.push(...children.filter(c => c.discriminatorValue));
1182
1459
  return children;
1183
1460
  };
1184
- lookUpChildren(children, meta.className);
1461
+ lookUpChildren(children, meta.class);
1185
1462
  this.andWhere({
1186
1463
  [meta.root.discriminatorColumn]: children.length > 0 ? { $in: [meta.discriminatorValue, ...children.map(c => c.discriminatorValue)] } : meta.discriminatorValue,
1187
1464
  });
1188
1465
  }
1466
+ /**
1467
+ * Ensures TPT joins are applied. Can be called early before finalize() to populate
1468
+ * the _tptAlias map for use in join resolution. Safe to call multiple times.
1469
+ * @internal
1470
+ */
1471
+ ensureTPTJoins() {
1472
+ this.applyTPTJoins();
1473
+ }
1474
+ /**
1475
+ * For TPT (Table-Per-Type) inheritance: INNER JOINs parent tables.
1476
+ * When querying a child entity, we need to join all parent tables.
1477
+ * Field selection is handled separately in addTPTParentFields().
1478
+ */
1479
+ applyTPTJoins() {
1480
+ const meta = this.mainAlias.meta;
1481
+ if (meta?.inheritanceType !== 'tpt' || !meta.tptParent || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
1482
+ return;
1483
+ }
1484
+ if (this.tptJoinsApplied) {
1485
+ return;
1486
+ }
1487
+ this.tptJoinsApplied = true;
1488
+ let childMeta = meta;
1489
+ let childAlias = this.mainAlias.aliasName;
1490
+ while (childMeta.tptParent) {
1491
+ const parentMeta = childMeta.tptParent;
1492
+ const parentAlias = this.getNextAlias(parentMeta.className);
1493
+ this.createAlias(parentMeta.class, parentAlias);
1494
+ this._tptAlias[parentMeta.className] = parentAlias;
1495
+ this.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `[tpt]${childMeta.className}`);
1496
+ childMeta = parentMeta;
1497
+ childAlias = parentAlias;
1498
+ }
1499
+ }
1500
+ /**
1501
+ * For TPT inheritance: adds field selections from parent tables.
1502
+ */
1503
+ addTPTParentFields() {
1504
+ const meta = this.mainAlias.meta;
1505
+ if (meta?.inheritanceType !== 'tpt' || !meta.tptParent || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
1506
+ return;
1507
+ }
1508
+ if (!this._fields?.includes('*') && !this._fields?.includes(`${this.mainAlias.aliasName}.*`)) {
1509
+ return;
1510
+ }
1511
+ let parentMeta = meta.tptParent;
1512
+ while (parentMeta) {
1513
+ const parentAlias = this._tptAlias[parentMeta.className];
1514
+ if (parentAlias) {
1515
+ const schema = parentMeta.schema === '*' ? '*' : this.driver.getSchemaName(parentMeta);
1516
+ parentMeta.ownProps
1517
+ .filter(prop => this.platform.shouldHaveColumn(prop, []))
1518
+ .forEach(prop => this._fields.push(...this.driver.mapPropToFieldNames(this, prop, parentAlias, parentMeta, schema)));
1519
+ }
1520
+ parentMeta = parentMeta.tptParent;
1521
+ }
1522
+ }
1523
+ /**
1524
+ * For TPT polymorphic queries: LEFT JOINs all child tables when querying a TPT base class.
1525
+ * Adds discriminator and child fields to determine and load the concrete type.
1526
+ */
1527
+ applyTPTPolymorphicJoins() {
1528
+ const meta = this.mainAlias.meta;
1529
+ const descendants = meta?.allTPTDescendants;
1530
+ if (!descendants?.length || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
1531
+ return;
1532
+ }
1533
+ if (!this._fields?.includes('*') && !this._fields?.includes(`${this.mainAlias.aliasName}.*`)) {
1534
+ return;
1535
+ }
1536
+ // LEFT JOIN each descendant table and add their fields
1537
+ for (const childMeta of descendants) {
1538
+ const childAlias = this.getNextAlias(childMeta.className);
1539
+ this.createAlias(childMeta.class, childAlias);
1540
+ this._tptAlias[childMeta.className] = childAlias;
1541
+ this.addPropertyJoin(childMeta.tptInverseProp, this.mainAlias.aliasName, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
1542
+ // Add child fields
1543
+ const schema = childMeta.schema === '*' ? '*' : this.driver.getSchemaName(childMeta);
1544
+ childMeta.ownProps
1545
+ .filter(prop => !prop.primary && this.platform.shouldHaveColumn(prop, []))
1546
+ .forEach(prop => this._fields.push(...this.driver.mapPropToFieldNames(this, prop, childAlias, childMeta, schema)));
1547
+ }
1548
+ // Add computed discriminator (CASE WHEN to determine concrete type)
1549
+ // descendants is pre-sorted by depth (deepest first) during discovery
1550
+ if (meta.tptDiscriminatorColumn) {
1551
+ this._fields.push(this.driver.buildTPTDiscriminatorExpression(meta, descendants, this._tptAlias, this.mainAlias.aliasName));
1552
+ }
1553
+ }
1189
1554
  finalize() {
1190
1555
  if (this.finalized) {
1191
1556
  return;
@@ -1193,20 +1558,28 @@ export class QueryBuilder {
1193
1558
  if (!this._type) {
1194
1559
  this.select('*');
1195
1560
  }
1196
- const meta = this.mainAlias.metadata;
1561
+ const meta = this.mainAlias.meta;
1197
1562
  this.applyDiscriminatorCondition();
1563
+ this.applyTPTJoins();
1564
+ this.addTPTParentFields();
1565
+ this.applyTPTPolymorphicJoins();
1198
1566
  this.processPopulateHint();
1199
1567
  this.processNestedJoins();
1200
1568
  if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
1569
+ const schema = this.getSchema(this.mainAlias);
1570
+ // Create a column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
1571
+ // For TPT, use helper to resolve correct alias per property (inherited props use parent alias)
1572
+ const quotedMainAlias = this.platform.quoteIdentifier(this.mainAlias.aliasName).toString();
1573
+ const columns = meta.createColumnMappingObject(prop => this.helper.getTPTAliasForProperty(prop.name, this.mainAlias.aliasName), quotedMainAlias);
1201
1574
  meta.props
1202
1575
  .filter(prop => prop.formula && (!prop.lazy || this.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
1203
1576
  .map(prop => {
1204
- const alias = this.platform.quoteIdentifier(this.mainAlias.aliasName);
1205
1577
  const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1206
- return `${prop.formula(alias)} as ${aliased}`;
1578
+ const table = this.helper.createFormulaTable(quotedMainAlias, meta, schema);
1579
+ return `${this.driver.evaluateFormula(prop.formula, columns, table)} as ${aliased}`;
1207
1580
  })
1208
1581
  .filter(field => !this._fields.some(f => {
1209
- if (f instanceof RawQueryFragment) {
1582
+ if (isRaw(f)) {
1210
1583
  return f.sql === field && f.params.length === 0;
1211
1584
  }
1212
1585
  return f === field;
@@ -1220,7 +1593,7 @@ export class QueryBuilder {
1220
1593
  if (!this.flags.has(QueryFlag.DISABLE_PAGINATE) && this._groupBy.length === 0 && this.hasToManyJoins()) {
1221
1594
  this.flags.add(QueryFlag.PAGINATE);
1222
1595
  }
1223
- if (meta && this.flags.has(QueryFlag.PAGINATE) && !this.flags.has(QueryFlag.DISABLE_PAGINATE) && (this._limit > 0 || this._offset > 0)) {
1596
+ if (meta && !meta.virtual && this.flags.has(QueryFlag.PAGINATE) && !this.flags.has(QueryFlag.DISABLE_PAGINATE) && (this._limit > 0 || this._offset > 0)) {
1224
1597
  this.wrapPaginateSubQuery(meta);
1225
1598
  }
1226
1599
  if (meta && (this.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.flags.has(QueryFlag.DELETE_SUB_QUERY))) {
@@ -1233,7 +1606,7 @@ export class QueryBuilder {
1233
1606
  if (this.populateHintFinalized) {
1234
1607
  return;
1235
1608
  }
1236
- const meta = this.mainAlias.metadata;
1609
+ const meta = this.mainAlias.meta;
1237
1610
  if (meta && this.flags.has(QueryFlag.AUTO_JOIN_ONE_TO_ONE_OWNER)) {
1238
1611
  const relationsToPopulate = this._populate.map(({ field }) => field);
1239
1612
  meta.relations
@@ -1251,12 +1624,12 @@ export class QueryBuilder {
1251
1624
  }
1252
1625
  if (meta && this.helper.isOneToOneInverse(fromField)) {
1253
1626
  const prop = meta.properties[fromField];
1254
- const alias = this.getNextAlias(prop.pivotEntity ?? prop.type);
1627
+ const alias = this.getNextAlias(prop.pivotEntity ?? prop.targetMeta.class);
1255
1628
  const aliasedName = `${fromAlias}.${prop.name}#${alias}`;
1256
1629
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, this.mainAlias.aliasName, alias, JoinType.leftJoin);
1257
1630
  this._joins[aliasedName].path = `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? meta.className)}.${prop.name}`;
1258
1631
  this._populateMap[aliasedName] = this._joins[aliasedName].alias;
1259
- this.createAlias(prop.type, alias);
1632
+ this.createAlias(prop.targetMeta.class, alias);
1260
1633
  }
1261
1634
  });
1262
1635
  this.processPopulateWhere(false);
@@ -1357,8 +1730,12 @@ export class QueryBuilder {
1357
1730
  });
1358
1731
  }
1359
1732
  wrapPaginateSubQuery(meta) {
1360
- const pks = this.prepareFields(meta.primaryKeys, 'sub-query');
1361
- const subQuery = this.clone(['_orderBy', '_fields', 'lockMode', 'lockTableAliases']).select(pks).groupBy(pks).limit(this._limit);
1733
+ const schema = this.getSchema(this.mainAlias);
1734
+ const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
1735
+ const subQuery = this.clone(['_orderBy', '_fields', 'lockMode', 'lockTableAliases'])
1736
+ .select(pks)
1737
+ .groupBy(pks)
1738
+ .limit(this._limit);
1362
1739
  // revert the on conditions added via populateWhere, we want to apply those only once
1363
1740
  for (const join of Object.values(subQuery._joins)) {
1364
1741
  if (join.cond_) {
@@ -1372,11 +1749,10 @@ export class QueryBuilder {
1372
1749
  if (this._orderBy.length > 0) {
1373
1750
  const orderBy = [];
1374
1751
  for (const orderMap of this._orderBy) {
1375
- for (const [field, direction] of Object.entries(orderMap)) {
1376
- if (RawQueryFragment.isKnownFragment(field)) {
1377
- const rawField = RawQueryFragment.getKnownFragment(field, false);
1378
- this.rawFragments.add(field);
1379
- orderBy.push({ [rawField.clone()]: direction });
1752
+ for (const field of Utils.getObjectQueryKeys(orderMap)) {
1753
+ const direction = orderMap[field];
1754
+ if (RawQueryFragment.isKnownFragmentSymbol(field)) {
1755
+ orderBy.push({ [field]: direction });
1380
1756
  continue;
1381
1757
  }
1382
1758
  const [a, f] = this.helper.splitField(field);
@@ -1401,14 +1777,14 @@ export class QueryBuilder {
1401
1777
  if (typeof field === 'object' && field && '__as' in field) {
1402
1778
  return field.__as === prop;
1403
1779
  }
1404
- if (field instanceof RawQueryFragment) {
1780
+ if (isRaw(field)) {
1405
1781
  // not perfect, but should work most of the time, ideally we should check only the alias (`... as alias`)
1406
1782
  return field.sql.includes(prop);
1407
1783
  }
1408
1784
  return false;
1409
1785
  });
1410
1786
  /* v8 ignore next */
1411
- if (field instanceof RawQueryFragment) {
1787
+ if (isRaw(field)) {
1412
1788
  innerQuery.select(field);
1413
1789
  }
1414
1790
  else if (field instanceof NativeQueryBuilder) {
@@ -1425,28 +1801,67 @@ export class QueryBuilder {
1425
1801
  subSubQuery.select(pks).from(innerQuery);
1426
1802
  this._limit = undefined;
1427
1803
  this._offset = undefined;
1428
- if (!this._fields.some(f => RawQueryFragment.isKnownFragment(f))) {
1429
- this.pruneExtraJoins(meta);
1430
- }
1804
+ // Save the original WHERE conditions before pruning joins
1805
+ const originalCond = this._cond;
1806
+ const populatePaths = this.getPopulatePaths();
1807
+ if (!this._fields.some(field => isRaw(field))) {
1808
+ this.pruneJoinsForPagination(meta, populatePaths);
1809
+ }
1810
+ // Transfer WHERE conditions to ORDER BY joins (GH #6160)
1811
+ this.transferConditionsForOrderByJoins(meta, originalCond, populatePaths);
1431
1812
  const { sql, params } = subSubQuery.compile();
1432
1813
  this.select(this._fields).where({ [Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) } });
1433
1814
  }
1434
- pruneExtraJoins(meta) {
1435
- // remove joins that are not used for population or ordering to improve performance
1436
- const populate = new Set();
1437
- const orderByAliases = this._orderBy
1438
- .flatMap(hint => Object.keys(hint))
1439
- .map(k => k.split('.')[0]);
1815
+ /**
1816
+ * Computes the set of populate paths from the _populate hints.
1817
+ */
1818
+ getPopulatePaths() {
1819
+ const paths = new Set();
1440
1820
  function addPath(hints, prefix = '') {
1441
1821
  for (const hint of hints) {
1442
1822
  const field = hint.field.split(':')[0];
1443
- populate.add((prefix ? prefix + '.' : '') + field);
1823
+ const fullPath = prefix ? prefix + '.' + field : field;
1824
+ paths.add(fullPath);
1444
1825
  if (hint.children) {
1445
- addPath(hint.children, (prefix ? prefix + '.' : '') + field);
1826
+ addPath(hint.children, fullPath);
1446
1827
  }
1447
1828
  }
1448
1829
  }
1449
1830
  addPath(this._populate);
1831
+ return paths;
1832
+ }
1833
+ normalizeJoinPath(join, meta) {
1834
+ return join.path?.replace(/\[populate]|\[pivot]|:ref/g, '').replace(new RegExp(`^${meta.className}.`), '') ?? '';
1835
+ }
1836
+ /**
1837
+ * Transfers WHERE conditions to ORDER BY joins that are not used for population.
1838
+ * This ensures the outer query's ORDER BY uses the same filtered rows as the subquery.
1839
+ * GH #6160
1840
+ */
1841
+ transferConditionsForOrderByJoins(meta, cond, populatePaths) {
1842
+ if (!cond || this._orderBy.length === 0) {
1843
+ return;
1844
+ }
1845
+ const orderByAliases = new Set(this._orderBy
1846
+ .flatMap(hint => Object.keys(hint))
1847
+ .filter(k => !RawQueryFragment.isKnownFragmentSymbol(k))
1848
+ .map(k => k.split('.')[0]));
1849
+ for (const join of Object.values(this._joins)) {
1850
+ const joinPath = this.normalizeJoinPath(join, meta);
1851
+ const isPopulateJoin = populatePaths.has(joinPath);
1852
+ // Only transfer conditions for joins used for ORDER BY but not for population
1853
+ if (orderByAliases.has(join.alias) && !isPopulateJoin) {
1854
+ this.transferConditionsToJoin(cond, join);
1855
+ }
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Removes joins that are not used for population or ordering to improve performance.
1860
+ */
1861
+ pruneJoinsForPagination(meta, populatePaths) {
1862
+ const orderByAliases = this._orderBy
1863
+ .flatMap(hint => Object.keys(hint))
1864
+ .map(k => k.split('.')[0]);
1450
1865
  const joins = Object.entries(this._joins);
1451
1866
  const rootAlias = this.alias;
1452
1867
  function addParentAlias(alias) {
@@ -1460,12 +1875,38 @@ export class QueryBuilder {
1460
1875
  addParentAlias(orderByAlias);
1461
1876
  }
1462
1877
  for (const [key, join] of joins) {
1463
- const path = join.path?.replace(/\[populate]|\[pivot]|:ref/g, '').replace(new RegExp(`^${meta.className}.`), '');
1464
- if (!populate.has(path ?? '') && !orderByAliases.includes(join.alias)) {
1878
+ const path = this.normalizeJoinPath(join, meta);
1879
+ if (!populatePaths.has(path) && !orderByAliases.includes(join.alias)) {
1465
1880
  delete this._joins[key];
1466
1881
  }
1467
1882
  }
1468
1883
  }
1884
+ /**
1885
+ * Transfers WHERE conditions that reference a join alias to the join's ON clause.
1886
+ * This is needed when a join is kept for ORDER BY after pagination wrapping,
1887
+ * so the outer query orders by the same filtered rows as the subquery.
1888
+ * @internal
1889
+ */
1890
+ transferConditionsToJoin(cond, join, path = '') {
1891
+ const aliasPrefix = join.alias + '.';
1892
+ for (const key of Object.keys(cond)) {
1893
+ const value = cond[key];
1894
+ // Handle $and/$or operators - recurse into nested conditions
1895
+ if (key === '$and' || key === '$or') {
1896
+ if (Array.isArray(value)) {
1897
+ for (const item of value) {
1898
+ this.transferConditionsToJoin(item, join, path);
1899
+ }
1900
+ }
1901
+ continue;
1902
+ }
1903
+ // Check if this condition references the join alias
1904
+ if (key.startsWith(aliasPrefix)) {
1905
+ // Add condition to the join's ON clause
1906
+ join.cond[key] = value;
1907
+ }
1908
+ }
1909
+ }
1469
1910
  wrapModifySubQuery(meta) {
1470
1911
  const subQuery = this.clone();
1471
1912
  subQuery.finalized = true;
@@ -1473,7 +1914,8 @@ export class QueryBuilder {
1473
1914
  // https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause
1474
1915
  const subSubQuery = this.platform.createNativeQueryBuilder();
1475
1916
  const method = this.flags.has(QueryFlag.UPDATE_SUB_QUERY) ? 'update' : 'delete';
1476
- const pks = this.prepareFields(meta.primaryKeys, 'sub-query');
1917
+ const schema = this.getSchema(this.mainAlias);
1918
+ const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
1477
1919
  this._cond = {}; // otherwise we would trigger validation error
1478
1920
  this._joins = {}; // included in the subquery
1479
1921
  subSubQuery.select(pks).from(subQuery.as(this.mainAlias.aliasName));
@@ -1483,17 +1925,18 @@ export class QueryBuilder {
1483
1925
  });
1484
1926
  }
1485
1927
  getSchema(alias) {
1486
- const { metadata } = alias;
1487
- const metaSchema = metadata?.schema && metadata.schema !== '*' ? metadata.schema : undefined;
1928
+ const { meta } = alias;
1929
+ const metaSchema = meta.schema && meta.schema !== '*' ? meta.schema : undefined;
1488
1930
  const schema = this._schema ?? metaSchema ?? this.em?.schema ?? this.em?.config.getSchema(true);
1489
1931
  if (schema === this.platform.getDefaultSchemaName()) {
1490
1932
  return undefined;
1491
1933
  }
1492
1934
  return schema;
1493
1935
  }
1936
+ /** @internal */
1494
1937
  createAlias(entityName, aliasName, subQuery) {
1495
- const metadata = this.metadata.find(entityName);
1496
- const alias = { aliasName, entityName, metadata, subQuery };
1938
+ const meta = this.metadata.find(entityName);
1939
+ const alias = { aliasName, entityName, meta, subQuery };
1497
1940
  this._aliases[aliasName] = alias;
1498
1941
  return alias;
1499
1942
  }
@@ -1513,7 +1956,7 @@ export class QueryBuilder {
1513
1956
  this.createMainAlias(entityName, aliasName);
1514
1957
  }
1515
1958
  createQueryBuilderHelper() {
1516
- return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver);
1959
+ return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver, this._tptAlias);
1517
1960
  }
1518
1961
  ensureFromClause() {
1519
1962
  /* v8 ignore next */
@@ -1551,7 +1994,7 @@ export class QueryBuilder {
1551
1994
  if (!Utils.isEmpty(this._orderBy)) {
1552
1995
  object.orderBy = this._orderBy;
1553
1996
  }
1554
- const name = this._mainAlias ? `${prefix}QueryBuilder<${this._mainAlias?.entityName}>` : 'QueryBuilder';
1997
+ const name = this._mainAlias ? `${prefix}QueryBuilder<${Utils.className(this._mainAlias?.entityName)}>` : 'QueryBuilder';
1555
1998
  const ret = inspect(object, { depth });
1556
1999
  return ret === '[Object]' ? `[${name}]` : name + ' ' + ret;
1557
2000
  }