@mikro-orm/sql 7.0.0-dev.98 → 7.0.0-rc.0

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 +6 -7
  2. package/AbstractSqlConnection.js +27 -24
  3. package/AbstractSqlDriver.d.ts +82 -23
  4. package/AbstractSqlDriver.js +584 -184
  5. package/AbstractSqlPlatform.d.ts +3 -4
  6. package/AbstractSqlPlatform.js +0 -4
  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} +3 -2
  11. package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +5 -1
  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 +14 -1
  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 +50 -35
  44. package/query/QueryBuilder.d.ts +548 -79
  45. package/query/QueryBuilder.js +537 -159
  46. package/query/QueryBuilderHelper.d.ts +22 -14
  47. package/query/QueryBuilderHelper.js +158 -69
  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 +126 -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 +74 -36
  63. package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +0 -1
  64. package/dialects/postgresql/PostgreSqlTableCompiler.js +0 -1
@@ -1,4 +1,4 @@
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';
@@ -47,8 +47,6 @@ export class QueryBuilder {
47
47
  _populate = [];
48
48
  /** @internal */
49
49
  _populateMap = {};
50
- /** @internal */
51
- rawFragments = new Set();
52
50
  aliasCounter = 0;
53
51
  flags = new Set([QueryFlag.CONVERT_CUSTOM_TYPES]);
54
52
  finalized = false;
@@ -77,9 +75,12 @@ export class QueryBuilder {
77
75
  subQueries = {};
78
76
  _mainAlias;
79
77
  _aliases = {};
78
+ _tptAlias = {}; // maps entity className to alias for TPT parent tables
80
79
  _helper;
81
80
  _query;
82
81
  platform;
82
+ tptJoinsApplied = false;
83
+ autoJoinedPaths = [];
83
84
  /**
84
85
  * @internal
85
86
  */
@@ -100,12 +101,20 @@ export class QueryBuilder {
100
101
  }
101
102
  select(fields, distinct = false) {
102
103
  this.ensureNotFinalized();
103
- this._fields = Utils.asArray(fields);
104
+ this._fields = Utils.asArray(fields).flatMap(f => {
105
+ if (typeof f !== 'string') {
106
+ return f;
107
+ }
108
+ return this.resolveNestedPath(f);
109
+ });
104
110
  if (distinct) {
105
111
  this.flags.add(QueryFlag.DISTINCT);
106
112
  }
107
113
  return this.init(QueryType.SELECT);
108
114
  }
115
+ /**
116
+ * Adds fields to an existing SELECT query.
117
+ */
109
118
  addSelect(fields) {
110
119
  this.ensureNotFinalized();
111
120
  if (this._type && this._type !== QueryType.SELECT) {
@@ -117,30 +126,87 @@ export class QueryBuilder {
117
126
  this.ensureNotFinalized();
118
127
  return this.setFlag(QueryFlag.DISTINCT);
119
128
  }
120
- /** postgres only */
121
129
  distinctOn(fields) {
122
130
  this.ensureNotFinalized();
123
131
  this._distinctOn = Utils.asArray(fields);
124
132
  return this;
125
133
  }
134
+ /**
135
+ * Creates an INSERT query with the given data.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * await em.createQueryBuilder(User)
140
+ * .insert({ name: 'John', email: 'john@example.com' })
141
+ * .execute();
142
+ *
143
+ * // Bulk insert
144
+ * await em.createQueryBuilder(User)
145
+ * .insert([{ name: 'John' }, { name: 'Jane' }])
146
+ * .execute();
147
+ * ```
148
+ */
126
149
  insert(data) {
127
150
  return this.init(QueryType.INSERT, data);
128
151
  }
152
+ /**
153
+ * Creates an UPDATE query with the given data.
154
+ * Use `where()` to specify which rows to update.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * await em.createQueryBuilder(User)
159
+ * .update({ name: 'John Doe' })
160
+ * .where({ id: 1 })
161
+ * .execute();
162
+ * ```
163
+ */
129
164
  update(data) {
130
165
  return this.init(QueryType.UPDATE, data);
131
166
  }
167
+ /**
168
+ * Creates a DELETE query.
169
+ * Use `where()` to specify which rows to delete.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * await em.createQueryBuilder(User)
174
+ * .delete()
175
+ * .where({ id: 1 })
176
+ * .execute();
177
+ *
178
+ * // Or pass the condition directly
179
+ * await em.createQueryBuilder(User)
180
+ * .delete({ isActive: false })
181
+ * .execute();
182
+ * ```
183
+ */
132
184
  delete(cond) {
133
185
  return this.init(QueryType.DELETE, undefined, cond);
134
186
  }
187
+ /**
188
+ * Creates a TRUNCATE query to remove all rows from the table.
189
+ */
135
190
  truncate() {
136
191
  return this.init(QueryType.TRUNCATE);
137
192
  }
193
+ /**
194
+ * Creates a COUNT query to count matching rows.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const count = await em.createQueryBuilder(User)
199
+ * .count()
200
+ * .where({ isActive: true })
201
+ * .execute('get');
202
+ * ```
203
+ */
138
204
  count(field, distinct = false) {
139
205
  if (field) {
140
206
  this._fields = Utils.asArray(field);
141
207
  }
142
208
  else if (distinct || this.hasToManyJoins()) {
143
- this._fields = this.mainAlias.metadata.primaryKeys;
209
+ this._fields = this.mainAlias.meta.primaryKeys;
144
210
  }
145
211
  else {
146
212
  this._fields = [raw('*')];
@@ -158,16 +224,27 @@ export class QueryBuilder {
158
224
  this.join(field, alias, cond, JoinType.innerJoin, undefined, schema);
159
225
  return this;
160
226
  }
161
- innerJoinLateral(field, alias, cond, schema) {
162
- this.join(field, alias, cond, JoinType.innerJoinLateral, undefined, schema);
163
- return this;
227
+ innerJoinLateral(field, alias, cond = {}, schema) {
228
+ return this.join(field, alias, cond, JoinType.innerJoinLateral, undefined, schema);
164
229
  }
165
230
  leftJoin(field, alias, cond = {}, schema) {
166
231
  return this.join(field, alias, cond, JoinType.leftJoin, undefined, schema);
167
232
  }
168
- leftJoinLateral(field, alias, cond, schema) {
233
+ leftJoinLateral(field, alias, cond = {}, schema) {
169
234
  return this.join(field, alias, cond, JoinType.leftJoinLateral, undefined, schema);
170
235
  }
236
+ /**
237
+ * Adds a JOIN clause and automatically selects the joined entity's fields.
238
+ * This is useful for eager loading related entities.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * const qb = em.createQueryBuilder(Book, 'b');
243
+ * qb.select('*')
244
+ * .joinAndSelect('b.author', 'a')
245
+ * .where({ 'a.name': 'John' });
246
+ * ```
247
+ */
171
248
  joinAndSelect(field, alias, cond = {}, type = JoinType.innerJoin, path, fields, schema) {
172
249
  if (!this._type) {
173
250
  this.select('*');
@@ -199,18 +276,22 @@ export class QueryBuilder {
199
276
  return this.joinAndSelect(field, alias, cond, JoinType.leftJoin, undefined, fields, schema);
200
277
  }
201
278
  leftJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
202
- return this.joinAndSelect(field, alias, cond, JoinType.leftJoinLateral, undefined, fields, schema);
279
+ this.joinAndSelect(field, alias, cond, JoinType.leftJoinLateral, undefined, fields, schema);
280
+ return this;
203
281
  }
204
282
  innerJoinAndSelect(field, alias, cond = {}, fields, schema) {
205
283
  return this.joinAndSelect(field, alias, cond, JoinType.innerJoin, undefined, fields, schema);
206
284
  }
207
285
  innerJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
208
- return this.joinAndSelect(field, alias, cond, JoinType.innerJoinLateral, undefined, fields, schema);
286
+ this.joinAndSelect(field, alias, cond, JoinType.innerJoinLateral, undefined, fields, schema);
287
+ return this;
209
288
  }
210
289
  getFieldsForJoinedLoad(prop, alias, explicitFields) {
211
290
  const fields = [];
212
291
  const populate = [];
213
292
  const joinKey = Object.keys(this._joins).find(join => join.endsWith(`#${alias}`));
293
+ const targetMeta = prop.targetMeta;
294
+ const schema = this._schema ?? (targetMeta.schema !== '*' ? targetMeta.schema : undefined);
214
295
  if (joinKey) {
215
296
  const path = this._joins[joinKey].path.split('.').slice(1);
216
297
  let children = this._populate;
@@ -223,29 +304,29 @@ export class QueryBuilder {
223
304
  }
224
305
  populate.push(...children);
225
306
  }
226
- for (const p of prop.targetMeta.getPrimaryProps()) {
227
- fields.push(...this.driver.mapPropToFieldNames(this, p, alias));
307
+ for (const p of targetMeta.getPrimaryProps()) {
308
+ fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
228
309
  }
229
310
  if (explicitFields) {
230
311
  for (const field of explicitFields) {
231
312
  const [a, f] = this.helper.splitField(field);
232
- const p = prop.targetMeta.properties[f];
313
+ const p = targetMeta.properties[f];
233
314
  if (p) {
234
- fields.push(...this.driver.mapPropToFieldNames(this, p, alias));
315
+ fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
235
316
  }
236
317
  else {
237
318
  fields.push(`${a}.${f} as ${a}__${f}`);
238
319
  }
239
320
  }
240
321
  }
241
- prop.targetMeta.props
322
+ targetMeta.props
242
323
  .filter(prop => {
243
324
  if (!explicitFields) {
244
325
  return this.platform.shouldHaveColumn(prop, populate);
245
326
  }
246
327
  return prop.primary && !explicitFields.includes(prop.name) && !explicitFields.includes(`${alias}.${prop.name}`);
247
328
  })
248
- .forEach(prop => fields.push(...this.driver.mapPropToFieldNames(this, prop, alias)));
329
+ .forEach(prop => fields.push(...this.driver.mapPropToFieldNames(this, prop, alias, targetMeta, schema)));
249
330
  return fields;
250
331
  }
251
332
  /**
@@ -259,7 +340,6 @@ export class QueryBuilder {
259
340
  const cond = await this.em.applyFilters(this.mainAlias.entityName, {}, filterOptions, 'read');
260
341
  this.andWhere(cond);
261
342
  }
262
- autoJoinedPaths = [];
263
343
  /**
264
344
  * @internal
265
345
  */
@@ -276,15 +356,23 @@ export class QueryBuilder {
276
356
  continue;
277
357
  }
278
358
  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)) {
359
+ let cond = await em.applyFilters(join.prop.targetMeta.class, join.cond, filterOptions, 'read');
360
+ const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, join.prop.targetMeta.class, cond);
361
+ cond = criteriaNode.process(this, {
362
+ matchPopulateJoins: true,
363
+ filter: true,
364
+ alias: join.alias,
365
+ ignoreBranching: true,
366
+ parentPath: join.path,
367
+ });
368
+ if (Utils.hasObjectKeys(cond) || RawQueryFragment.hasObjectFragments(cond)) {
281
369
  // remove nested filters, we only care about scalars here, nesting would require another join branch
282
370
  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)))) {
371
+ if (Utils.isPlainObject(cond[key]) && Object.keys(cond[key]).every(k => !(Utils.isOperator(k) && !['$some', '$none', '$every', '$size'].includes(k)))) {
284
372
  delete cond[key];
285
373
  }
286
374
  }
287
- if (Utils.hasObjectKeys(join.cond)) {
375
+ if (Utils.hasObjectKeys(join.cond) || RawQueryFragment.hasObjectFragments(join.cond)) {
288
376
  /* v8 ignore next */
289
377
  join.cond = { $and: [join.cond, cond] };
290
378
  }
@@ -296,7 +384,7 @@ export class QueryBuilder {
296
384
  }
297
385
  withSubQuery(subQuery, alias) {
298
386
  this.ensureNotFinalized();
299
- if (subQuery instanceof RawQueryFragment) {
387
+ if (isRaw(subQuery)) {
300
388
  this.subQueries[alias] = this.platform.formatQuery(subQuery.sql, subQuery.params);
301
389
  }
302
390
  else {
@@ -306,18 +394,18 @@ export class QueryBuilder {
306
394
  }
307
395
  where(cond, params, operator) {
308
396
  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) };
397
+ let processedCond;
398
+ if (isRaw(cond)) {
399
+ const sql = this.platform.formatQuery(cond.sql, cond.params);
400
+ processedCond = { [raw(`(${sql})`)]: Utils.asArray(params) };
313
401
  operator ??= '$and';
314
402
  }
315
403
  else if (typeof cond === 'string') {
316
- cond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
404
+ processedCond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
317
405
  operator ??= '$and';
318
406
  }
319
407
  else {
320
- cond = QueryHelper.processWhere({
408
+ processedCond = QueryHelper.processWhere({
321
409
  where: cond,
322
410
  entityName: this.mainAlias.entityName,
323
411
  metadata: this.metadata,
@@ -328,13 +416,13 @@ export class QueryBuilder {
328
416
  });
329
417
  }
330
418
  const op = operator || params;
331
- const topLevel = !op || !Utils.hasObjectKeys(this._cond);
332
- const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond);
419
+ const topLevel = !op || !(Utils.hasObjectKeys(this._cond) || RawQueryFragment.hasObjectFragments(this._cond));
420
+ const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processedCond);
333
421
  const ignoreBranching = this.__populateWhere === 'infer';
334
422
  if ([QueryType.UPDATE, QueryType.DELETE].includes(this.type) && criteriaNode.willAutoJoin(this, undefined, { ignoreBranching })) {
335
423
  // use sub-query to support joining
336
424
  this.setFlag(this.type === QueryType.UPDATE ? QueryFlag.UPDATE_SUB_QUERY : QueryFlag.DELETE_SUB_QUERY);
337
- this.select(this.mainAlias.metadata.primaryKeys, true);
425
+ this.select(this.mainAlias.meta.primaryKeys, true);
338
426
  }
339
427
  if (topLevel) {
340
428
  this._cond = criteriaNode.process(this, { ignoreBranching });
@@ -370,6 +458,7 @@ export class QueryBuilder {
370
458
  this._orderBy = [];
371
459
  }
372
460
  Utils.asArray(orderBy).forEach(o => {
461
+ this.helper.validateQueryOrder(o);
373
462
  const processed = QueryHelper.processWhere({
374
463
  where: o,
375
464
  entityName: this.mainAlias.entityName,
@@ -380,26 +469,44 @@ export class QueryBuilder {
380
469
  convertCustomTypes: false,
381
470
  type: 'orderBy',
382
471
  });
383
- this._orderBy.push(CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, { matchPopulateJoins: true, type: 'orderBy' }));
472
+ this._orderBy.push(CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, {
473
+ matchPopulateJoins: true,
474
+ type: 'orderBy',
475
+ }));
384
476
  });
385
477
  return this;
386
478
  }
387
479
  groupBy(fields) {
388
480
  this.ensureNotFinalized();
389
- this._groupBy = Utils.asArray(fields);
481
+ this._groupBy = Utils.asArray(fields).flatMap(f => {
482
+ if (typeof f !== 'string') {
483
+ return f;
484
+ }
485
+ return this.resolveNestedPath(f);
486
+ });
390
487
  return this;
391
488
  }
489
+ /**
490
+ * Adds a HAVING clause to the query, typically used with GROUP BY.
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * qb.select([raw('count(*) as count'), 'status'])
495
+ * .groupBy('status')
496
+ * .having({ count: { $gt: 5 } });
497
+ * ```
498
+ */
392
499
  having(cond = {}, params, operator) {
393
500
  this.ensureNotFinalized();
394
501
  if (typeof cond === 'string') {
395
502
  cond = { [raw(`(${cond})`, params)]: [] };
396
503
  }
397
- cond = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond).process(this);
504
+ const processed = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond, undefined, undefined, false).process(this);
398
505
  if (!this._having || !operator) {
399
- this._having = cond;
506
+ this._having = processed;
400
507
  }
401
508
  else {
402
- const cond1 = [this._having, cond];
509
+ const cond1 = [this._having, processed];
403
510
  this._having = { [operator]: cond1 };
404
511
  }
405
512
  return this;
@@ -411,7 +518,7 @@ export class QueryBuilder {
411
518
  return this.having(cond, params, '$or');
412
519
  }
413
520
  onConflict(fields = []) {
414
- const meta = this.mainAlias.metadata;
521
+ const meta = this.mainAlias.meta;
415
522
  this.ensureNotFinalized();
416
523
  this._onConflict ??= [];
417
524
  this._onConflict.push({
@@ -456,6 +563,15 @@ export class QueryBuilder {
456
563
  this._populateFilter = populateFilter;
457
564
  return this;
458
565
  }
566
+ /**
567
+ * Sets a LIMIT clause to restrict the number of results.
568
+ *
569
+ * @example
570
+ * ```ts
571
+ * qb.select('*').limit(10); // First 10 results
572
+ * qb.select('*').limit(10, 20); // 10 results starting from offset 20
573
+ * ```
574
+ */
459
575
  limit(limit, offset = 0) {
460
576
  this.ensureNotFinalized();
461
577
  this._limit = limit;
@@ -464,6 +580,14 @@ export class QueryBuilder {
464
580
  }
465
581
  return this;
466
582
  }
583
+ /**
584
+ * Sets an OFFSET clause to skip a number of results.
585
+ *
586
+ * @example
587
+ * ```ts
588
+ * qb.select('*').limit(10).offset(20); // Results 21-30
589
+ * ```
590
+ */
467
591
  offset(offset) {
468
592
  this.ensureNotFinalized();
469
593
  this._offset = offset;
@@ -538,11 +662,10 @@ export class QueryBuilder {
538
662
  this.fromSubQuery(target, aliasName);
539
663
  }
540
664
  else {
541
- const entityName = Utils.className(target);
542
- if (aliasName && this._mainAlias && entityName !== this._mainAlias.aliasName) {
665
+ if (aliasName && this._mainAlias && Utils.className(target) !== this._mainAlias.aliasName) {
543
666
  throw new Error(`Cannot override the alias to '${aliasName}' since a query already contains references to '${this._mainAlias.aliasName}'`);
544
667
  }
545
- this.fromEntityName(entityName, aliasName);
668
+ this.fromEntityName(target, aliasName);
546
669
  }
547
670
  return this;
548
671
  }
@@ -553,9 +676,11 @@ export class QueryBuilder {
553
676
  this._query = {};
554
677
  this.finalize();
555
678
  const qb = this.getQueryBase(processVirtualEntity);
679
+ const schema = this.getSchema(this.mainAlias);
680
+ const isNotEmptyObject = (obj) => Utils.hasObjectKeys(obj) || RawQueryFragment.hasObjectFragments(obj);
556
681
  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);
682
+ Utils.runIfNotEmpty(() => qb.groupBy(this.prepareFields(this._groupBy, 'groupBy', schema)), isNotEmptyObject(this._groupBy));
683
+ Utils.runIfNotEmpty(() => this.helper.appendQueryCondition(this.type, this._having, qb, undefined, 'having'), isNotEmptyObject(this._having));
559
684
  Utils.runIfNotEmpty(() => {
560
685
  const queryOrder = this.helper.getQueryOrder(this.type, this._orderBy, this._populateMap);
561
686
  if (queryOrder.length > 0) {
@@ -563,7 +688,7 @@ export class QueryBuilder {
563
688
  qb.orderBy(sql);
564
689
  return;
565
690
  }
566
- }, this._orderBy);
691
+ }, isNotEmptyObject(this._orderBy));
567
692
  Utils.runIfNotEmpty(() => qb.limit(this._limit), this._limit != null);
568
693
  Utils.runIfNotEmpty(() => qb.offset(this._offset), this._offset);
569
694
  Utils.runIfNotEmpty(() => qb.comment(this._comments), this._comments);
@@ -572,17 +697,9 @@ export class QueryBuilder {
572
697
  if (this.lockMode) {
573
698
  this.helper.getLockSQL(qb, this.lockMode, this.lockTables, this._joins);
574
699
  }
575
- this.helper.finalize(this.type, qb, this.mainAlias.metadata, this._data, this._returning);
576
- this.clearRawFragmentsCache();
700
+ this.helper.finalize(this.type, qb, this.mainAlias.meta, this._data, this._returning);
577
701
  return this._query.qb = qb;
578
702
  }
579
- /**
580
- * @internal
581
- */
582
- clearRawFragmentsCache() {
583
- this.rawFragments.forEach(key => RawQueryFragment.remove(key));
584
- this.rawFragments.clear();
585
- }
586
703
  /**
587
704
  * Returns the query with parameters as wildcards.
588
705
  */
@@ -622,7 +739,7 @@ export class QueryBuilder {
622
739
  * @internal
623
740
  */
624
741
  getAliasForJoinPath(path, options) {
625
- if (!path || path === this.mainAlias.entityName) {
742
+ if (!path || path === Utils.className(this.mainAlias.entityName)) {
626
743
  return this.mainAlias.aliasName;
627
744
  }
628
745
  const join = typeof path === 'string' ? this.getJoinForPath(path, options) : path;
@@ -666,8 +783,24 @@ export class QueryBuilder {
666
783
  * @internal
667
784
  */
668
785
  getNextAlias(entityName = 'e') {
786
+ entityName = Utils.className(entityName);
669
787
  return this.driver.config.getNamingStrategy().aliasName(entityName, this.aliasCounter++);
670
788
  }
789
+ /**
790
+ * Registers a join for a specific polymorphic target type.
791
+ * Used by the driver to create per-target LEFT JOINs for JOINED loading.
792
+ * @internal
793
+ */
794
+ addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
795
+ // Override referencedColumnNames to use the specific target's PK columns
796
+ // (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
797
+ const referencedColumnNames = targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
798
+ const targetProp = { ...prop, targetMeta, referencedColumnNames };
799
+ const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
800
+ this._joins[aliasedName] = this.helper.joinManyToOneReference(targetProp, ownerAlias, alias, type, {}, schema);
801
+ this._joins[aliasedName].path = path;
802
+ this.createAlias(targetMeta.class, alias);
803
+ }
671
804
  /**
672
805
  * @internal
673
806
  */
@@ -697,7 +830,7 @@ export class QueryBuilder {
697
830
  }
698
831
  const loggerContext = { id: this.em?.id, ...this.loggerContext };
699
832
  const res = await this.getConnection().execute(query.sql, query.params, method, this.context, loggerContext);
700
- const meta = this.mainAlias.metadata;
833
+ const meta = this.mainAlias.meta;
701
834
  if (!options.mapResults || !meta) {
702
835
  await this.em?.storeCache(this._cache, cached, res);
703
836
  return res;
@@ -711,7 +844,7 @@ export class QueryBuilder {
711
844
  const map = {};
712
845
  mapped = res.map(r => this.driver.mapResult(r, meta, this._populate, this, map));
713
846
  if (options.mergeResults && joinedProps.length > 0) {
714
- mapped = this.driver.mergeJoinedResult(mapped, this.mainAlias.metadata, joinedProps);
847
+ mapped = this.driver.mergeJoinedResult(mapped, this.mainAlias.meta, joinedProps);
715
848
  }
716
849
  }
717
850
  else {
@@ -752,7 +885,7 @@ export class QueryBuilder {
752
885
  const query = this.toQuery();
753
886
  const loggerContext = { id: this.em?.id, ...this.loggerContext };
754
887
  const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
755
- const meta = this.mainAlias.metadata;
888
+ const meta = this.mainAlias.meta;
756
889
  if (options.rawResults || !meta) {
757
890
  yield* res;
758
891
  return;
@@ -769,7 +902,7 @@ export class QueryBuilder {
769
902
  continue;
770
903
  }
771
904
  if (stack.length > 0 && hash(stack[stack.length - 1]) !== hash(mapped)) {
772
- const res = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
905
+ const res = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
773
906
  for (const row of res) {
774
907
  yield this.mapResult(row, options.mapResults);
775
908
  }
@@ -778,7 +911,7 @@ export class QueryBuilder {
778
911
  stack.push(mapped);
779
912
  }
780
913
  if (stack.length > 0) {
781
- const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
914
+ const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
782
915
  yield this.mapResult(merged[0], options.mapResults);
783
916
  }
784
917
  }
@@ -840,16 +973,13 @@ export class QueryBuilder {
840
973
  const [res] = await this.getResultList(1);
841
974
  return res || null;
842
975
  }
843
- /**
844
- * Executes count query (without offset and limit), returning total count of results
845
- */
846
976
  async getCount(field, distinct) {
847
977
  let res;
848
978
  if (this.type === QueryType.COUNT) {
849
979
  res = await this.execute('get', false);
850
980
  }
851
981
  else {
852
- const qb = this._type === undefined ? this : this.clone();
982
+ const qb = (this._type === undefined ? this : this.clone());
853
983
  qb.processPopulateHint(); // needs to happen sooner so `qb.hasToManyJoins()` reports correctly
854
984
  qb.count(field, distinct ?? qb.hasToManyJoins()).limit(undefined).offset(undefined).orderBy([]);
855
985
  res = await qb.execute('get', false);
@@ -860,50 +990,42 @@ export class QueryBuilder {
860
990
  * Executes the query, returning both array of results and total count query (without offset and limit).
861
991
  */
862
992
  async getResultAndCount() {
863
- return [
864
- await this.clone().getResultList(),
865
- await this.clone().getCount(),
866
- ];
993
+ return [await this.clone().getResultList(), await this.clone().getCount()];
867
994
  }
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) {
995
+ as(aliasOrTargetEntity, alias) {
873
996
  const qb = this.getNativeQuery();
874
- if (alias.includes('.')) {
875
- const [a, f] = alias.split('.');
876
- const meta = this.metadata.find(a);
997
+ let finalAlias = aliasOrTargetEntity;
998
+ /* v8 ignore next */
999
+ if (typeof aliasOrTargetEntity === 'string' && aliasOrTargetEntity.includes('.')) {
1000
+ throw new Error('qb.as(alias) no longer supports target entity name prefix, use qb.as(TargetEntity, key) signature instead');
1001
+ }
1002
+ if (alias) {
1003
+ const meta = this.metadata.get(aliasOrTargetEntity);
877
1004
  /* v8 ignore next */
878
- alias = meta?.properties[f]?.fieldNames[0] ?? alias;
1005
+ finalAlias = meta.properties[alias]?.fieldNames[0] ?? alias;
879
1006
  }
880
- qb.as(alias);
1007
+ qb.as(finalAlias);
881
1008
  // tag the instance, so it is possible to detect it easily
882
- Object.defineProperty(qb, '__as', { enumerable: false, value: alias });
1009
+ Object.defineProperty(qb, '__as', { enumerable: false, value: finalAlias });
883
1010
  return qb;
884
1011
  }
885
- clone(reset) {
1012
+ clone(reset, preserve) {
886
1013
  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
1014
  reset = reset || [];
891
1015
  // clone array/object properties
892
1016
  const properties = [
893
1017
  'flags', '_populate', '_populateWhere', '_populateFilter', '__populateWhere', '_populateMap', '_joins', '_joinedProps', '_cond', '_data', '_orderBy',
894
1018
  '_schema', '_indexHint', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning',
895
- '_comments', '_hintComments', 'rawFragments', 'aliasCounter',
1019
+ '_comments', '_hintComments', 'aliasCounter',
896
1020
  ];
897
- RawQueryFragment.cloneRegistry = this.rawFragments;
898
1021
  for (const prop of Object.keys(this)) {
899
- if (reset.includes(prop) || ['_helper', '_query'].includes(prop)) {
1022
+ if (!preserve?.includes(prop) && (reset === true || reset.includes(prop) || ['_helper', '_query'].includes(prop))) {
900
1023
  continue;
901
1024
  }
902
1025
  qb[prop] = properties.includes(prop) ? Utils.copy(this[prop]) : this[prop];
903
1026
  }
904
- delete RawQueryFragment.cloneRegistry;
905
1027
  /* v8 ignore next */
906
- if (this._fields && !reset.includes('_fields')) {
1028
+ if (this._fields && reset !== true && !reset.includes('_fields')) {
907
1029
  qb._fields = [...this._fields];
908
1030
  }
909
1031
  qb._aliases = { ...this._aliases };
@@ -935,13 +1057,28 @@ export class QueryBuilder {
935
1057
  if (res instanceof QueryBuilder) {
936
1058
  return `(${res.getFormattedQuery()}) as ${this.platform.quoteIdentifier(this.alias)}`;
937
1059
  }
938
- if (res instanceof RawQueryFragment) {
1060
+ if (isRaw(res)) {
939
1061
  const query = this.platform.formatQuery(res.sql, res.params);
940
1062
  return `(${query}) as ${this.platform.quoteIdentifier(this.alias)}`;
941
1063
  }
942
1064
  /* v8 ignore next */
943
1065
  return res;
944
1066
  }
1067
+ /**
1068
+ * Adds a join from a property object. Used internally for TPT joins where the property
1069
+ * is synthetic (not in entity.properties) but defined on metadata (e.g., tptParentProp).
1070
+ * The caller must create the alias first via createAlias().
1071
+ * @internal
1072
+ */
1073
+ addPropertyJoin(prop, ownerAlias, alias, type, path, schema) {
1074
+ schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta);
1075
+ const key = `[tpt]${ownerAlias}#${alias}`;
1076
+ this._joins[key] = prop.kind === ReferenceKind.MANY_TO_ONE
1077
+ ? this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, {}, schema)
1078
+ : this.helper.joinOneToReference(prop, ownerAlias, alias, type, {}, schema);
1079
+ this._joins[key].path = path;
1080
+ return key;
1081
+ }
945
1082
  joinReference(field, alias, cond, type, path, schema, subquery) {
946
1083
  this.ensureNotFinalized();
947
1084
  if (typeof field === 'object') {
@@ -950,11 +1087,11 @@ export class QueryBuilder {
950
1087
  kind: ReferenceKind.MANY_TO_ONE,
951
1088
  };
952
1089
  if (field instanceof QueryBuilder) {
953
- prop.type = field.mainAlias.entityName;
954
- prop.targetMeta = field.mainAlias.metadata;
1090
+ prop.type = Utils.className(field.mainAlias.entityName);
1091
+ prop.targetMeta = field.mainAlias.meta;
955
1092
  field = field.getNativeQuery();
956
1093
  }
957
- if (field instanceof RawQueryFragment) {
1094
+ if (isRaw(field)) {
958
1095
  field = this.platform.formatQuery(field.sql, field.params);
959
1096
  }
960
1097
  const key = `${this.alias}.${prop.name}#${alias}`;
@@ -983,7 +1120,12 @@ export class QueryBuilder {
983
1120
  if (!prop) {
984
1121
  throw new Error(`Trying to join ${q(field)}, but ${q(fromField)} is not a defined relation on ${meta.className}.`);
985
1122
  }
986
- this.createAlias(prop.type, alias);
1123
+ // For TPT inheritance, owning relations (M:1 and owning 1:1) may have FK columns in a parent table
1124
+ // Resolve the correct alias for the table that owns the FK column
1125
+ const ownerAlias = (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))
1126
+ ? this.helper.getTPTAliasForProperty(fromField, fromAlias)
1127
+ : fromAlias;
1128
+ this.createAlias(prop.targetMeta.class, alias);
987
1129
  cond = QueryHelper.processWhere({
988
1130
  where: cond,
989
1131
  entityName: this.mainAlias.entityName,
@@ -992,10 +1134,10 @@ export class QueryBuilder {
992
1134
  aliasMap: this.getAliasMap(),
993
1135
  aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
994
1136
  });
995
- const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.className, cond);
1137
+ const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
996
1138
  cond = criteriaNode.process(this, { ignoreBranching: true, alias });
997
1139
  let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
998
- path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? entityName)}.${prop.name}`;
1140
+ path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName))}.${prop.name}`;
999
1141
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
1000
1142
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
1001
1143
  this._joins[aliasedName].path ??= path;
@@ -1014,26 +1156,21 @@ export class QueryBuilder {
1014
1156
  aliasedName = Object.keys(joins)[1];
1015
1157
  }
1016
1158
  else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
1017
- this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
1159
+ this._joins[aliasedName] = this.helper.joinOneToReference(prop, ownerAlias, alias, type, cond, schema);
1018
1160
  this._joins[aliasedName].path ??= path;
1019
1161
  }
1020
1162
  else { // MANY_TO_ONE
1021
- this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond, schema);
1163
+ this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
1022
1164
  this._joins[aliasedName].path ??= path;
1023
1165
  }
1024
1166
  return { prop, key: aliasedName };
1025
1167
  }
1026
- prepareFields(fields, type = 'where') {
1168
+ prepareFields(fields, type = 'where', schema) {
1027
1169
  const ret = [];
1028
1170
  const getFieldName = (name) => {
1029
- return this.helper.mapper(name, this.type, undefined, type === 'groupBy' ? null : undefined);
1171
+ return this.helper.mapper(name, this.type, undefined, type === 'groupBy' ? null : undefined, schema);
1030
1172
  };
1031
1173
  fields.forEach(field => {
1032
- const rawField = RawQueryFragment.getKnownFragment(field, false);
1033
- if (rawField) {
1034
- ret.push(rawField);
1035
- return;
1036
- }
1037
1174
  if (typeof field !== 'string') {
1038
1175
  ret.push(field);
1039
1176
  return;
@@ -1078,24 +1215,99 @@ export class QueryBuilder {
1078
1215
  }
1079
1216
  ret.push(getFieldName(field));
1080
1217
  });
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) {
1218
+ const requiresSQLConversion = this.mainAlias.meta.props.filter(p => p.hasConvertToJSValueSQL && p.persist !== false);
1219
+ if (this.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES) &&
1220
+ (fields.includes('*') || fields.includes(`${this.mainAlias.aliasName}.*`)) &&
1221
+ requiresSQLConversion.length > 0) {
1085
1222
  for (const p of requiresSQLConversion) {
1086
1223
  ret.push(this.helper.mapper(p.name, this.type));
1087
1224
  }
1088
1225
  }
1089
1226
  for (const f of Object.keys(this._populateMap)) {
1090
1227
  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
- }
1228
+ ret.push(...this.helper.mapJoinColumns(this.type, this._joins[f]));
1095
1229
  }
1096
1230
  }
1097
1231
  return Utils.unique(ret);
1098
1232
  }
1233
+ /**
1234
+ * Resolves nested paths like `a.books.title` to their actual field references.
1235
+ * Auto-joins relations as needed and returns `{alias}.{field}`.
1236
+ * For embeddeds: navigates into flattened embeddeds to return the correct field name.
1237
+ */
1238
+ resolveNestedPath(field) {
1239
+ if (typeof field !== 'string' || !field.includes('.')) {
1240
+ return field;
1241
+ }
1242
+ const parts = field.split('.');
1243
+ // Simple alias.property case - let prepareFields handle it
1244
+ if (parts.length === 2 && this._aliases[parts[0]]) {
1245
+ return field;
1246
+ }
1247
+ // Start with root alias
1248
+ let currentAlias = parts[0];
1249
+ let currentMeta = this._aliases[currentAlias] ? this.metadata.get(this._aliases[currentAlias].entityName) : this.mainAlias.meta;
1250
+ // If first part is not an alias, it's a property of the main entity
1251
+ if (!this._aliases[currentAlias]) {
1252
+ currentAlias = this.mainAlias.aliasName;
1253
+ parts.unshift(currentAlias);
1254
+ }
1255
+ // Walk through the path parts (skip the alias)
1256
+ for (let i = 1; i < parts.length; i++) {
1257
+ const propName = parts[i];
1258
+ const prop = currentMeta.properties[propName];
1259
+ if (!prop) {
1260
+ return field; // Unknown property, return as-is for raw SQL support
1261
+ }
1262
+ const isLastPart = i === parts.length - 1;
1263
+ // Handle embedded properties - navigate into flattened embeddeds
1264
+ if (prop.kind === ReferenceKind.EMBEDDED) {
1265
+ if (prop.object) {
1266
+ return `${currentAlias}.${propName}`;
1267
+ }
1268
+ // Navigate through remaining path to find the leaf property
1269
+ const remainingPath = parts.slice(i + 1);
1270
+ let embeddedProp = prop;
1271
+ for (const part of remainingPath) {
1272
+ embeddedProp = embeddedProp?.embeddedProps?.[part];
1273
+ if (embeddedProp?.object && embeddedProp.fieldNames?.[0]) {
1274
+ return `${currentAlias}.${embeddedProp.fieldNames[0]}`;
1275
+ }
1276
+ }
1277
+ return `${currentAlias}.${embeddedProp?.fieldNames?.[0] ?? propName}`;
1278
+ }
1279
+ // Handle relations - auto-join if not the last part
1280
+ if (prop.kind === ReferenceKind.MANY_TO_ONE ||
1281
+ prop.kind === ReferenceKind.ONE_TO_ONE ||
1282
+ prop.kind === ReferenceKind.ONE_TO_MANY ||
1283
+ prop.kind === ReferenceKind.MANY_TO_MANY) {
1284
+ if (isLastPart) {
1285
+ return `${currentAlias}.${propName}`;
1286
+ }
1287
+ // Find existing join or create new one
1288
+ const joinPath = parts.slice(0, i + 1).join('.');
1289
+ const existingJoinKey = Object.keys(this._joins).find(k => {
1290
+ const join = this._joins[k];
1291
+ // Check by path or by key prefix (key format is `alias.field#joinAlias`)
1292
+ return join.path === joinPath || k.startsWith(`${currentAlias}.${propName}#`);
1293
+ });
1294
+ let joinAlias;
1295
+ if (existingJoinKey) {
1296
+ joinAlias = this._joins[existingJoinKey].alias;
1297
+ }
1298
+ else {
1299
+ joinAlias = this.getNextAlias(prop.targetMeta?.className ?? propName);
1300
+ this.join(`${currentAlias}.${propName}`, joinAlias, {}, JoinType.leftJoin);
1301
+ }
1302
+ currentAlias = joinAlias;
1303
+ currentMeta = prop.targetMeta;
1304
+ continue;
1305
+ }
1306
+ // Scalar property - return it (if not last part, it's an invalid path but let SQL handle it)
1307
+ return `${currentAlias}.${propName}`;
1308
+ }
1309
+ return field;
1310
+ }
1099
1311
  init(type, data, cond) {
1100
1312
  this.ensureNotFinalized();
1101
1313
  this._type = type;
@@ -1118,14 +1330,14 @@ export class QueryBuilder {
1118
1330
  }
1119
1331
  getQueryBase(processVirtualEntity) {
1120
1332
  const qb = this.platform.createNativeQueryBuilder().setFlags(this.flags);
1121
- const { subQuery, aliasName, entityName, metadata } = this.mainAlias;
1333
+ const { subQuery, aliasName, entityName, meta } = this.mainAlias;
1122
1334
  const requiresAlias = this.finalized && (this._explicitAlias || this.helper.isTableNameAliasRequired(this.type));
1123
1335
  const alias = requiresAlias ? aliasName : undefined;
1124
1336
  const schema = this.getSchema(this.mainAlias);
1125
1337
  const tableName = subQuery ? subQuery.as(aliasName) : this.helper.getTableName(entityName);
1126
1338
  const joinSchema = this._schema ?? this.em?.schema ?? schema;
1127
- if (metadata?.virtual && processVirtualEntity) {
1128
- qb.from(raw(this.fromVirtual(metadata)), { indexHint: this._indexHint });
1339
+ if (meta.virtual && processVirtualEntity) {
1340
+ qb.from(raw(this.fromVirtual(meta)), { indexHint: this._indexHint });
1129
1341
  }
1130
1342
  else {
1131
1343
  qb.from(tableName, {
@@ -1136,9 +1348,9 @@ export class QueryBuilder {
1136
1348
  }
1137
1349
  switch (this.type) {
1138
1350
  case QueryType.SELECT:
1139
- qb.select(this.prepareFields(this._fields));
1351
+ qb.select(this.prepareFields(this._fields, 'where', schema));
1140
1352
  if (this._distinctOn) {
1141
- qb.distinctOn(this.prepareFields(this._distinctOn));
1353
+ qb.distinctOn(this.prepareFields(this._distinctOn, 'where', schema));
1142
1354
  }
1143
1355
  else if (this.flags.has(QueryFlag.DISTINCT)) {
1144
1356
  qb.distinct();
@@ -1146,7 +1358,7 @@ export class QueryBuilder {
1146
1358
  this.helper.processJoins(qb, this._joins, joinSchema);
1147
1359
  break;
1148
1360
  case QueryType.COUNT: {
1149
- const fields = this._fields.map(f => this.helper.mapper(f, this.type));
1361
+ const fields = this._fields.map(f => this.helper.mapper(f, this.type, undefined, undefined, schema));
1150
1362
  qb.count(fields, this.flags.has(QueryFlag.DISTINCT));
1151
1363
  this.helper.processJoins(qb, this._joins, joinSchema);
1152
1364
  break;
@@ -1169,23 +1381,111 @@ export class QueryBuilder {
1169
1381
  return qb;
1170
1382
  }
1171
1383
  applyDiscriminatorCondition() {
1172
- const meta = this.mainAlias.metadata;
1173
- if (!meta?.discriminatorValue) {
1384
+ const meta = this.mainAlias.meta;
1385
+ if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
1174
1386
  return;
1175
1387
  }
1176
- const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.find(cls));
1388
+ const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
1177
1389
  const children = [];
1178
1390
  const lookUpChildren = (ret, type) => {
1179
1391
  const children = types.filter(meta2 => meta2.extends === type);
1180
- children.forEach(m => lookUpChildren(ret, m.className));
1392
+ children.forEach(m => lookUpChildren(ret, m.class));
1181
1393
  ret.push(...children.filter(c => c.discriminatorValue));
1182
1394
  return children;
1183
1395
  };
1184
- lookUpChildren(children, meta.className);
1396
+ lookUpChildren(children, meta.class);
1185
1397
  this.andWhere({
1186
1398
  [meta.root.discriminatorColumn]: children.length > 0 ? { $in: [meta.discriminatorValue, ...children.map(c => c.discriminatorValue)] } : meta.discriminatorValue,
1187
1399
  });
1188
1400
  }
1401
+ /**
1402
+ * Ensures TPT joins are applied. Can be called early before finalize() to populate
1403
+ * the _tptAlias map for use in join resolution. Safe to call multiple times.
1404
+ * @internal
1405
+ */
1406
+ ensureTPTJoins() {
1407
+ this.applyTPTJoins();
1408
+ }
1409
+ /**
1410
+ * For TPT (Table-Per-Type) inheritance: INNER JOINs parent tables.
1411
+ * When querying a child entity, we need to join all parent tables.
1412
+ * Field selection is handled separately in addTPTParentFields().
1413
+ */
1414
+ applyTPTJoins() {
1415
+ const meta = this.mainAlias.meta;
1416
+ if (meta?.inheritanceType !== 'tpt' || !meta.tptParent || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
1417
+ return;
1418
+ }
1419
+ if (this.tptJoinsApplied) {
1420
+ return;
1421
+ }
1422
+ this.tptJoinsApplied = true;
1423
+ let childMeta = meta;
1424
+ let childAlias = this.mainAlias.aliasName;
1425
+ while (childMeta.tptParent) {
1426
+ const parentMeta = childMeta.tptParent;
1427
+ const parentAlias = this.getNextAlias(parentMeta.className);
1428
+ this.createAlias(parentMeta.class, parentAlias);
1429
+ this._tptAlias[parentMeta.className] = parentAlias;
1430
+ this.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `[tpt]${childMeta.className}`);
1431
+ childMeta = parentMeta;
1432
+ childAlias = parentAlias;
1433
+ }
1434
+ }
1435
+ /**
1436
+ * For TPT inheritance: adds field selections from parent tables.
1437
+ */
1438
+ addTPTParentFields() {
1439
+ const meta = this.mainAlias.meta;
1440
+ if (meta?.inheritanceType !== 'tpt' || !meta.tptParent || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
1441
+ return;
1442
+ }
1443
+ if (!this._fields?.includes('*') && !this._fields?.includes(`${this.mainAlias.aliasName}.*`)) {
1444
+ return;
1445
+ }
1446
+ let parentMeta = meta.tptParent;
1447
+ while (parentMeta) {
1448
+ const parentAlias = this._tptAlias[parentMeta.className];
1449
+ if (parentAlias) {
1450
+ const schema = parentMeta.schema === '*' ? '*' : this.driver.getSchemaName(parentMeta);
1451
+ parentMeta.ownProps
1452
+ .filter(prop => this.platform.shouldHaveColumn(prop, []))
1453
+ .forEach(prop => this._fields.push(...this.driver.mapPropToFieldNames(this, prop, parentAlias, parentMeta, schema)));
1454
+ }
1455
+ parentMeta = parentMeta.tptParent;
1456
+ }
1457
+ }
1458
+ /**
1459
+ * For TPT polymorphic queries: LEFT JOINs all child tables when querying a TPT base class.
1460
+ * Adds discriminator and child fields to determine and load the concrete type.
1461
+ */
1462
+ applyTPTPolymorphicJoins() {
1463
+ const meta = this.mainAlias.meta;
1464
+ const descendants = meta?.allTPTDescendants;
1465
+ if (!descendants?.length || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
1466
+ return;
1467
+ }
1468
+ if (!this._fields?.includes('*') && !this._fields?.includes(`${this.mainAlias.aliasName}.*`)) {
1469
+ return;
1470
+ }
1471
+ // LEFT JOIN each descendant table and add their fields
1472
+ for (const childMeta of descendants) {
1473
+ const childAlias = this.getNextAlias(childMeta.className);
1474
+ this.createAlias(childMeta.class, childAlias);
1475
+ this._tptAlias[childMeta.className] = childAlias;
1476
+ this.addPropertyJoin(childMeta.tptInverseProp, this.mainAlias.aliasName, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
1477
+ // Add child fields
1478
+ const schema = childMeta.schema === '*' ? '*' : this.driver.getSchemaName(childMeta);
1479
+ childMeta.ownProps
1480
+ .filter(prop => !prop.primary && this.platform.shouldHaveColumn(prop, []))
1481
+ .forEach(prop => this._fields.push(...this.driver.mapPropToFieldNames(this, prop, childAlias, childMeta, schema)));
1482
+ }
1483
+ // Add computed discriminator (CASE WHEN to determine concrete type)
1484
+ // descendants is pre-sorted by depth (deepest first) during discovery
1485
+ if (meta.tptDiscriminatorColumn) {
1486
+ this._fields.push(this.driver.buildTPTDiscriminatorExpression(meta, descendants, this._tptAlias, this.mainAlias.aliasName));
1487
+ }
1488
+ }
1189
1489
  finalize() {
1190
1490
  if (this.finalized) {
1191
1491
  return;
@@ -1193,20 +1493,28 @@ export class QueryBuilder {
1193
1493
  if (!this._type) {
1194
1494
  this.select('*');
1195
1495
  }
1196
- const meta = this.mainAlias.metadata;
1496
+ const meta = this.mainAlias.meta;
1197
1497
  this.applyDiscriminatorCondition();
1498
+ this.applyTPTJoins();
1499
+ this.addTPTParentFields();
1500
+ this.applyTPTPolymorphicJoins();
1198
1501
  this.processPopulateHint();
1199
1502
  this.processNestedJoins();
1200
1503
  if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
1504
+ const schema = this.getSchema(this.mainAlias);
1505
+ // Create a column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
1506
+ // For TPT, use helper to resolve correct alias per property (inherited props use parent alias)
1507
+ const quotedMainAlias = this.platform.quoteIdentifier(this.mainAlias.aliasName).toString();
1508
+ const columns = meta.createColumnMappingObject(prop => this.helper.getTPTAliasForProperty(prop.name, this.mainAlias.aliasName), quotedMainAlias);
1201
1509
  meta.props
1202
1510
  .filter(prop => prop.formula && (!prop.lazy || this.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
1203
1511
  .map(prop => {
1204
- const alias = this.platform.quoteIdentifier(this.mainAlias.aliasName);
1205
1512
  const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1206
- return `${prop.formula(alias)} as ${aliased}`;
1513
+ const table = this.helper.createFormulaTable(quotedMainAlias, meta, schema);
1514
+ return `${this.driver.evaluateFormula(prop.formula, columns, table)} as ${aliased}`;
1207
1515
  })
1208
1516
  .filter(field => !this._fields.some(f => {
1209
- if (f instanceof RawQueryFragment) {
1517
+ if (isRaw(f)) {
1210
1518
  return f.sql === field && f.params.length === 0;
1211
1519
  }
1212
1520
  return f === field;
@@ -1233,7 +1541,7 @@ export class QueryBuilder {
1233
1541
  if (this.populateHintFinalized) {
1234
1542
  return;
1235
1543
  }
1236
- const meta = this.mainAlias.metadata;
1544
+ const meta = this.mainAlias.meta;
1237
1545
  if (meta && this.flags.has(QueryFlag.AUTO_JOIN_ONE_TO_ONE_OWNER)) {
1238
1546
  const relationsToPopulate = this._populate.map(({ field }) => field);
1239
1547
  meta.relations
@@ -1251,12 +1559,12 @@ export class QueryBuilder {
1251
1559
  }
1252
1560
  if (meta && this.helper.isOneToOneInverse(fromField)) {
1253
1561
  const prop = meta.properties[fromField];
1254
- const alias = this.getNextAlias(prop.pivotEntity ?? prop.type);
1562
+ const alias = this.getNextAlias(prop.pivotEntity ?? prop.targetMeta.class);
1255
1563
  const aliasedName = `${fromAlias}.${prop.name}#${alias}`;
1256
1564
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, this.mainAlias.aliasName, alias, JoinType.leftJoin);
1257
1565
  this._joins[aliasedName].path = `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? meta.className)}.${prop.name}`;
1258
1566
  this._populateMap[aliasedName] = this._joins[aliasedName].alias;
1259
- this.createAlias(prop.type, alias);
1567
+ this.createAlias(prop.targetMeta.class, alias);
1260
1568
  }
1261
1569
  });
1262
1570
  this.processPopulateWhere(false);
@@ -1357,8 +1665,12 @@ export class QueryBuilder {
1357
1665
  });
1358
1666
  }
1359
1667
  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);
1668
+ const schema = this.getSchema(this.mainAlias);
1669
+ const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
1670
+ const subQuery = this.clone(['_orderBy', '_fields', 'lockMode', 'lockTableAliases'])
1671
+ .select(pks)
1672
+ .groupBy(pks)
1673
+ .limit(this._limit);
1362
1674
  // revert the on conditions added via populateWhere, we want to apply those only once
1363
1675
  for (const join of Object.values(subQuery._joins)) {
1364
1676
  if (join.cond_) {
@@ -1372,11 +1684,10 @@ export class QueryBuilder {
1372
1684
  if (this._orderBy.length > 0) {
1373
1685
  const orderBy = [];
1374
1686
  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 });
1687
+ for (const field of Utils.getObjectQueryKeys(orderMap)) {
1688
+ const direction = orderMap[field];
1689
+ if (RawQueryFragment.isKnownFragmentSymbol(field)) {
1690
+ orderBy.push({ [field]: direction });
1380
1691
  continue;
1381
1692
  }
1382
1693
  const [a, f] = this.helper.splitField(field);
@@ -1401,14 +1712,14 @@ export class QueryBuilder {
1401
1712
  if (typeof field === 'object' && field && '__as' in field) {
1402
1713
  return field.__as === prop;
1403
1714
  }
1404
- if (field instanceof RawQueryFragment) {
1715
+ if (isRaw(field)) {
1405
1716
  // not perfect, but should work most of the time, ideally we should check only the alias (`... as alias`)
1406
1717
  return field.sql.includes(prop);
1407
1718
  }
1408
1719
  return false;
1409
1720
  });
1410
1721
  /* v8 ignore next */
1411
- if (field instanceof RawQueryFragment) {
1722
+ if (isRaw(field)) {
1412
1723
  innerQuery.select(field);
1413
1724
  }
1414
1725
  else if (field instanceof NativeQueryBuilder) {
@@ -1425,28 +1736,67 @@ export class QueryBuilder {
1425
1736
  subSubQuery.select(pks).from(innerQuery);
1426
1737
  this._limit = undefined;
1427
1738
  this._offset = undefined;
1428
- if (!this._fields.some(f => RawQueryFragment.isKnownFragment(f))) {
1429
- this.pruneExtraJoins(meta);
1430
- }
1739
+ // Save the original WHERE conditions before pruning joins
1740
+ const originalCond = this._cond;
1741
+ const populatePaths = this.getPopulatePaths();
1742
+ if (!this._fields.some(field => isRaw(field))) {
1743
+ this.pruneJoinsForPagination(meta, populatePaths);
1744
+ }
1745
+ // Transfer WHERE conditions to ORDER BY joins (GH #6160)
1746
+ this.transferConditionsForOrderByJoins(meta, originalCond, populatePaths);
1431
1747
  const { sql, params } = subSubQuery.compile();
1432
1748
  this.select(this._fields).where({ [Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) } });
1433
1749
  }
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]);
1750
+ /**
1751
+ * Computes the set of populate paths from the _populate hints.
1752
+ */
1753
+ getPopulatePaths() {
1754
+ const paths = new Set();
1440
1755
  function addPath(hints, prefix = '') {
1441
1756
  for (const hint of hints) {
1442
1757
  const field = hint.field.split(':')[0];
1443
- populate.add((prefix ? prefix + '.' : '') + field);
1758
+ const fullPath = prefix ? prefix + '.' + field : field;
1759
+ paths.add(fullPath);
1444
1760
  if (hint.children) {
1445
- addPath(hint.children, (prefix ? prefix + '.' : '') + field);
1761
+ addPath(hint.children, fullPath);
1446
1762
  }
1447
1763
  }
1448
1764
  }
1449
1765
  addPath(this._populate);
1766
+ return paths;
1767
+ }
1768
+ normalizeJoinPath(join, meta) {
1769
+ return join.path?.replace(/\[populate]|\[pivot]|:ref/g, '').replace(new RegExp(`^${meta.className}.`), '') ?? '';
1770
+ }
1771
+ /**
1772
+ * Transfers WHERE conditions to ORDER BY joins that are not used for population.
1773
+ * This ensures the outer query's ORDER BY uses the same filtered rows as the subquery.
1774
+ * GH #6160
1775
+ */
1776
+ transferConditionsForOrderByJoins(meta, cond, populatePaths) {
1777
+ if (!cond || this._orderBy.length === 0) {
1778
+ return;
1779
+ }
1780
+ const orderByAliases = new Set(this._orderBy
1781
+ .flatMap(hint => Object.keys(hint))
1782
+ .filter(k => !RawQueryFragment.isKnownFragmentSymbol(k))
1783
+ .map(k => k.split('.')[0]));
1784
+ for (const join of Object.values(this._joins)) {
1785
+ const joinPath = this.normalizeJoinPath(join, meta);
1786
+ const isPopulateJoin = populatePaths.has(joinPath);
1787
+ // Only transfer conditions for joins used for ORDER BY but not for population
1788
+ if (orderByAliases.has(join.alias) && !isPopulateJoin) {
1789
+ this.transferConditionsToJoin(cond, join);
1790
+ }
1791
+ }
1792
+ }
1793
+ /**
1794
+ * Removes joins that are not used for population or ordering to improve performance.
1795
+ */
1796
+ pruneJoinsForPagination(meta, populatePaths) {
1797
+ const orderByAliases = this._orderBy
1798
+ .flatMap(hint => Object.keys(hint))
1799
+ .map(k => k.split('.')[0]);
1450
1800
  const joins = Object.entries(this._joins);
1451
1801
  const rootAlias = this.alias;
1452
1802
  function addParentAlias(alias) {
@@ -1460,12 +1810,38 @@ export class QueryBuilder {
1460
1810
  addParentAlias(orderByAlias);
1461
1811
  }
1462
1812
  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)) {
1813
+ const path = this.normalizeJoinPath(join, meta);
1814
+ if (!populatePaths.has(path) && !orderByAliases.includes(join.alias)) {
1465
1815
  delete this._joins[key];
1466
1816
  }
1467
1817
  }
1468
1818
  }
1819
+ /**
1820
+ * Transfers WHERE conditions that reference a join alias to the join's ON clause.
1821
+ * This is needed when a join is kept for ORDER BY after pagination wrapping,
1822
+ * so the outer query orders by the same filtered rows as the subquery.
1823
+ * @internal
1824
+ */
1825
+ transferConditionsToJoin(cond, join, path = '') {
1826
+ const aliasPrefix = join.alias + '.';
1827
+ for (const key of Object.keys(cond)) {
1828
+ const value = cond[key];
1829
+ // Handle $and/$or operators - recurse into nested conditions
1830
+ if (key === '$and' || key === '$or') {
1831
+ if (Array.isArray(value)) {
1832
+ for (const item of value) {
1833
+ this.transferConditionsToJoin(item, join, path);
1834
+ }
1835
+ }
1836
+ continue;
1837
+ }
1838
+ // Check if this condition references the join alias
1839
+ if (key.startsWith(aliasPrefix)) {
1840
+ // Add condition to the join's ON clause
1841
+ join.cond[key] = value;
1842
+ }
1843
+ }
1844
+ }
1469
1845
  wrapModifySubQuery(meta) {
1470
1846
  const subQuery = this.clone();
1471
1847
  subQuery.finalized = true;
@@ -1473,7 +1849,8 @@ export class QueryBuilder {
1473
1849
  // https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause
1474
1850
  const subSubQuery = this.platform.createNativeQueryBuilder();
1475
1851
  const method = this.flags.has(QueryFlag.UPDATE_SUB_QUERY) ? 'update' : 'delete';
1476
- const pks = this.prepareFields(meta.primaryKeys, 'sub-query');
1852
+ const schema = this.getSchema(this.mainAlias);
1853
+ const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
1477
1854
  this._cond = {}; // otherwise we would trigger validation error
1478
1855
  this._joins = {}; // included in the subquery
1479
1856
  subSubQuery.select(pks).from(subQuery.as(this.mainAlias.aliasName));
@@ -1483,17 +1860,18 @@ export class QueryBuilder {
1483
1860
  });
1484
1861
  }
1485
1862
  getSchema(alias) {
1486
- const { metadata } = alias;
1487
- const metaSchema = metadata?.schema && metadata.schema !== '*' ? metadata.schema : undefined;
1863
+ const { meta } = alias;
1864
+ const metaSchema = meta.schema && meta.schema !== '*' ? meta.schema : undefined;
1488
1865
  const schema = this._schema ?? metaSchema ?? this.em?.schema ?? this.em?.config.getSchema(true);
1489
1866
  if (schema === this.platform.getDefaultSchemaName()) {
1490
1867
  return undefined;
1491
1868
  }
1492
1869
  return schema;
1493
1870
  }
1871
+ /** @internal */
1494
1872
  createAlias(entityName, aliasName, subQuery) {
1495
- const metadata = this.metadata.find(entityName);
1496
- const alias = { aliasName, entityName, metadata, subQuery };
1873
+ const meta = this.metadata.find(entityName);
1874
+ const alias = { aliasName, entityName, meta, subQuery };
1497
1875
  this._aliases[aliasName] = alias;
1498
1876
  return alias;
1499
1877
  }
@@ -1513,7 +1891,7 @@ export class QueryBuilder {
1513
1891
  this.createMainAlias(entityName, aliasName);
1514
1892
  }
1515
1893
  createQueryBuilderHelper() {
1516
- return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver);
1894
+ return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver, this._tptAlias);
1517
1895
  }
1518
1896
  ensureFromClause() {
1519
1897
  /* v8 ignore next */
@@ -1551,7 +1929,7 @@ export class QueryBuilder {
1551
1929
  if (!Utils.isEmpty(this._orderBy)) {
1552
1930
  object.orderBy = this._orderBy;
1553
1931
  }
1554
- const name = this._mainAlias ? `${prefix}QueryBuilder<${this._mainAlias?.entityName}>` : 'QueryBuilder';
1932
+ const name = this._mainAlias ? `${prefix}QueryBuilder<${Utils.className(this._mainAlias?.entityName)}>` : 'QueryBuilder';
1555
1933
  const ret = inspect(object, { depth });
1556
1934
  return ret === '[Object]' ? `[${name}]` : name + ' ' + ret;
1557
1935
  }