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