@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.
- package/AbstractSqlConnection.d.ts +6 -7
- package/AbstractSqlConnection.js +27 -24
- package/AbstractSqlDriver.d.ts +82 -23
- package/AbstractSqlDriver.js +584 -184
- package/AbstractSqlPlatform.d.ts +3 -4
- package/AbstractSqlPlatform.js +0 -4
- package/PivotCollectionPersister.d.ts +5 -0
- package/PivotCollectionPersister.js +30 -12
- package/SqlEntityManager.d.ts +2 -2
- package/dialects/mysql/{MySqlPlatform.d.ts → BaseMySqlPlatform.d.ts} +3 -2
- package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +5 -1
- package/dialects/mysql/MySqlSchemaHelper.d.ts +12 -1
- package/dialects/mysql/MySqlSchemaHelper.js +97 -6
- package/dialects/mysql/index.d.ts +1 -2
- package/dialects/mysql/index.js +1 -2
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +106 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +350 -0
- package/dialects/postgresql/FullTextType.d.ts +14 -0
- package/dialects/postgresql/FullTextType.js +59 -0
- package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +8 -0
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +47 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +90 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +732 -0
- package/dialects/postgresql/index.d.ts +3 -0
- package/dialects/postgresql/index.js +3 -0
- package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -0
- package/dialects/sqlite/BaseSqliteConnection.js +14 -1
- package/dialects/sqlite/BaseSqlitePlatform.d.ts +6 -0
- package/dialects/sqlite/BaseSqlitePlatform.js +12 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +25 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +145 -19
- package/dialects/sqlite/index.d.ts +0 -1
- package/dialects/sqlite/index.js +0 -1
- package/package.json +5 -6
- package/plugin/transformer.d.ts +1 -1
- package/plugin/transformer.js +1 -1
- package/query/CriteriaNode.d.ts +9 -5
- package/query/CriteriaNode.js +16 -15
- package/query/CriteriaNodeFactory.d.ts +6 -6
- package/query/CriteriaNodeFactory.js +33 -31
- package/query/NativeQueryBuilder.d.ts +3 -2
- package/query/NativeQueryBuilder.js +1 -2
- package/query/ObjectCriteriaNode.js +50 -35
- package/query/QueryBuilder.d.ts +548 -79
- package/query/QueryBuilder.js +537 -159
- package/query/QueryBuilderHelper.d.ts +22 -14
- package/query/QueryBuilderHelper.js +158 -69
- package/query/ScalarCriteriaNode.js +2 -2
- package/query/raw.d.ts +11 -3
- package/query/raw.js +1 -2
- package/schema/DatabaseSchema.d.ts +15 -2
- package/schema/DatabaseSchema.js +143 -15
- package/schema/DatabaseTable.d.ts +12 -0
- package/schema/DatabaseTable.js +91 -31
- package/schema/SchemaComparator.d.ts +8 -0
- package/schema/SchemaComparator.js +126 -3
- package/schema/SchemaHelper.d.ts +26 -3
- package/schema/SchemaHelper.js +98 -11
- package/schema/SqlSchemaGenerator.d.ts +10 -0
- package/schema/SqlSchemaGenerator.js +137 -9
- package/tsconfig.build.tsbuildinfo +1 -0
- package/typings.d.ts +74 -36
- package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +0 -1
- package/dialects/postgresql/PostgreSqlTableCompiler.js +0 -1
package/query/QueryBuilder.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { helper, isRaw, LoadStrategy, LockMode, PopulateHint, QueryFlag, QueryHelper, raw, RawQueryFragment, Reference, ReferenceKind, serialize, Utils, ValidationError,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
|
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
|
-
|
|
310
|
-
if (
|
|
311
|
-
const sql = this.platform.formatQuery(
|
|
312
|
-
|
|
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
|
-
|
|
404
|
+
processedCond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
|
|
317
405
|
operator ??= '$and';
|
|
318
406
|
}
|
|
319
407
|
else {
|
|
320
|
-
|
|
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,
|
|
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.
|
|
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, {
|
|
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
|
-
|
|
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 =
|
|
506
|
+
this._having = processed;
|
|
400
507
|
}
|
|
401
508
|
else {
|
|
402
|
-
const cond1 = [this._having,
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
1005
|
+
finalAlias = meta.properties[alias]?.fieldNames[0] ?? alias;
|
|
879
1006
|
}
|
|
880
|
-
qb.as(
|
|
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:
|
|
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', '
|
|
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
|
|
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.
|
|
1090
|
+
prop.type = Utils.className(field.mainAlias.entityName);
|
|
1091
|
+
prop.targetMeta = field.mainAlias.meta;
|
|
955
1092
|
field = field.getNativeQuery();
|
|
956
1093
|
}
|
|
957
|
-
if (field
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
1128
|
-
qb.from(raw(this.fromVirtual(
|
|
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.
|
|
1173
|
-
if (!meta
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1361
|
-
const
|
|
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
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1429
|
-
|
|
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
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
-
|
|
1758
|
+
const fullPath = prefix ? prefix + '.' + field : field;
|
|
1759
|
+
paths.add(fullPath);
|
|
1444
1760
|
if (hint.children) {
|
|
1445
|
-
addPath(hint.children,
|
|
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 =
|
|
1464
|
-
if (!
|
|
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
|
|
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 {
|
|
1487
|
-
const metaSchema =
|
|
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
|
|
1496
|
-
const alias = { aliasName, entityName,
|
|
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
|
}
|