@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/AbstractSqlDriver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getLoadingStrategy, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getLoadingStrategy, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, PolymorphicRef, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { QueryBuilder } from './query/QueryBuilder.js';
|
|
3
3
|
import { JoinType, QueryType } from './query/enums.js';
|
|
4
4
|
import { SqlEntityManager } from './SqlEntityManager.js';
|
|
@@ -17,6 +17,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
17
17
|
getPlatform() {
|
|
18
18
|
return this.platform;
|
|
19
19
|
}
|
|
20
|
+
/** Evaluates a formula callback, handling both string and Raw return values. */
|
|
21
|
+
evaluateFormula(formula, columns, table) {
|
|
22
|
+
const result = formula(columns, table);
|
|
23
|
+
return isRaw(result) ? this.platform.formatQuery(result.sql, result.params) : result;
|
|
24
|
+
}
|
|
25
|
+
/** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
|
|
26
|
+
getTableProps(meta) {
|
|
27
|
+
return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
|
|
28
|
+
}
|
|
29
|
+
/** Creates a FormulaTable object for use in formula callbacks. */
|
|
30
|
+
createFormulaTable(alias, meta, schema) {
|
|
31
|
+
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
32
|
+
const qualifiedName = effectiveSchema ? `${effectiveSchema}.${meta.tableName}` : meta.tableName;
|
|
33
|
+
return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
|
|
34
|
+
}
|
|
35
|
+
validateSqlOptions(options) {
|
|
36
|
+
if (options.collation != null && typeof options.collation !== 'string') {
|
|
37
|
+
throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
|
|
38
|
+
}
|
|
39
|
+
if (options.indexHint != null && typeof options.indexHint !== 'string') {
|
|
40
|
+
throw new Error('indexHint for SQL drivers must be a string (e.g. \'force index(my_index)\'). Use an object only with MongoDB.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
20
43
|
createEntityManager(useContext) {
|
|
21
44
|
const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
|
|
22
45
|
return new EntityManagerClass(this.config, this, this.metadata, useContext);
|
|
@@ -25,14 +48,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
25
48
|
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
|
|
26
49
|
const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
|
|
27
50
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
28
|
-
const
|
|
29
|
-
const
|
|
51
|
+
const schema = this.getSchemaName(meta, options);
|
|
52
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em).withSchema(schema);
|
|
53
|
+
const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
30
54
|
const orderBy = this.buildOrderBy(qb, meta, populate, options);
|
|
31
55
|
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
32
56
|
Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
|
|
33
57
|
if (Utils.isPrimaryKey(where, meta.compositePK)) {
|
|
34
58
|
where = { [Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
|
|
35
59
|
}
|
|
60
|
+
this.validateSqlOptions(options);
|
|
36
61
|
const { first, last, before, after } = options;
|
|
37
62
|
const isCursorPagination = [first, last, before, after].some(v => v != null);
|
|
38
63
|
qb.__populateWhere = options._populateWhere;
|
|
@@ -43,9 +68,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
43
68
|
.groupBy(options.groupBy)
|
|
44
69
|
.having(options.having)
|
|
45
70
|
.indexHint(options.indexHint)
|
|
71
|
+
.collation(options.collation)
|
|
46
72
|
.comment(options.comments)
|
|
47
|
-
.hintComment(options.hintComments)
|
|
48
|
-
.withSchema(this.getSchemaName(meta, options));
|
|
73
|
+
.hintComment(options.hintComments);
|
|
49
74
|
if (isCursorPagination) {
|
|
50
75
|
const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
|
|
51
76
|
qb.andWhere(where).orderBy(newOrderBy);
|
|
@@ -66,8 +91,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
66
91
|
}
|
|
67
92
|
async find(entityName, where, options = {}) {
|
|
68
93
|
options = { populate: [], orderBy: [], ...options };
|
|
69
|
-
const meta = this.metadata.
|
|
70
|
-
if (meta
|
|
94
|
+
const meta = this.metadata.get(entityName);
|
|
95
|
+
if (meta.virtual) {
|
|
71
96
|
return this.findVirtual(entityName, where, options);
|
|
72
97
|
}
|
|
73
98
|
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
@@ -165,16 +190,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
165
190
|
yield* res;
|
|
166
191
|
}
|
|
167
192
|
async wrapVirtualExpressionInSubquery(meta, expression, where, options, type) {
|
|
168
|
-
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
193
|
+
const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
|
|
169
194
|
qb.setFlag(QueryFlag.DISABLE_PAGINATE);
|
|
170
195
|
const isCursorPagination = [options.first, options.last, options.before, options.after].some(v => v != null);
|
|
171
196
|
const native = qb.getNativeQuery(false);
|
|
172
197
|
if (type === QueryType.COUNT) {
|
|
173
|
-
native
|
|
174
|
-
.clear('select')
|
|
175
|
-
.clear('limit')
|
|
176
|
-
.clear('offset')
|
|
177
|
-
.count();
|
|
198
|
+
native.clear('select').clear('limit').clear('offset').count();
|
|
178
199
|
}
|
|
179
200
|
native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
|
|
180
201
|
const query = native.compile();
|
|
@@ -188,7 +209,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
188
209
|
return res.map(row => this.mapResult(row, meta));
|
|
189
210
|
}
|
|
190
211
|
async *wrapVirtualExpressionInSubqueryStream(meta, expression, where, options, type) {
|
|
191
|
-
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
212
|
+
const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
|
|
192
213
|
qb.unsetFlag(QueryFlag.DISABLE_PAGINATE);
|
|
193
214
|
const native = qb.getNativeQuery(false);
|
|
194
215
|
native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
|
|
@@ -199,7 +220,34 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
199
220
|
yield this.mapResult(row, meta);
|
|
200
221
|
}
|
|
201
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
|
|
225
|
+
* Force balanced strategy to load to-many relations via separate queries.
|
|
226
|
+
*/
|
|
227
|
+
forceBalancedStrategy(options) {
|
|
228
|
+
const clearStrategy = (hints) => {
|
|
229
|
+
return hints.map(hint => ({
|
|
230
|
+
...hint,
|
|
231
|
+
strategy: undefined,
|
|
232
|
+
children: hint.children ? clearStrategy(hint.children) : undefined,
|
|
233
|
+
}));
|
|
234
|
+
};
|
|
235
|
+
const opts = { ...options, strategy: 'balanced' };
|
|
236
|
+
if (Array.isArray(opts.populate)) {
|
|
237
|
+
opts.populate = clearStrategy(opts.populate);
|
|
238
|
+
}
|
|
239
|
+
return opts;
|
|
240
|
+
}
|
|
202
241
|
mapResult(result, meta, populate = [], qb, map = {}) {
|
|
242
|
+
// For TPT inheritance, map aliased parent table columns back to their field names
|
|
243
|
+
if (qb && meta.inheritanceType === 'tpt' && meta.tptParent) {
|
|
244
|
+
this.mapTPTColumns(result, meta, qb);
|
|
245
|
+
}
|
|
246
|
+
// For TPT polymorphic queries (querying a base class), map child table fields
|
|
247
|
+
if (qb && meta.inheritanceType === 'tpt' && meta.allTPTDescendants?.length) {
|
|
248
|
+
const mainAlias = qb.mainAlias?.aliasName ?? 'e0';
|
|
249
|
+
this.mapTPTChildFields(result, meta, mainAlias, qb, result);
|
|
250
|
+
}
|
|
203
251
|
const ret = super.mapResult(result, meta);
|
|
204
252
|
/* v8 ignore next */
|
|
205
253
|
if (!ret) {
|
|
@@ -211,6 +259,33 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
211
259
|
}
|
|
212
260
|
return ret;
|
|
213
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Maps aliased columns from TPT parent tables back to their original field names.
|
|
264
|
+
* TPT parent columns are selected with aliases like `parent_alias__column_name`,
|
|
265
|
+
* and need to be renamed back to `column_name` for the result mapper to work.
|
|
266
|
+
*/
|
|
267
|
+
mapTPTColumns(result, meta, qb) {
|
|
268
|
+
const tptAliases = qb._tptAlias;
|
|
269
|
+
// Walk up the TPT hierarchy
|
|
270
|
+
let parentMeta = meta.tptParent;
|
|
271
|
+
while (parentMeta) {
|
|
272
|
+
const parentAlias = tptAliases[parentMeta.className];
|
|
273
|
+
if (parentAlias) {
|
|
274
|
+
// Rename columns from this parent table
|
|
275
|
+
for (const prop of parentMeta.ownProps) {
|
|
276
|
+
for (const fieldName of prop.fieldNames) {
|
|
277
|
+
const aliasedKey = `${parentAlias}__${fieldName}`;
|
|
278
|
+
if (aliasedKey in result) {
|
|
279
|
+
// Copy the value to the unaliased field name and remove the aliased key
|
|
280
|
+
result[fieldName] = result[aliasedKey];
|
|
281
|
+
delete result[aliasedKey];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
parentMeta = parentMeta.tptParent;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
214
289
|
mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
|
|
215
290
|
const joinedProps = this.joinedProps(meta, populate);
|
|
216
291
|
joinedProps.forEach(hint => {
|
|
@@ -220,8 +295,45 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
220
295
|
if (!prop) {
|
|
221
296
|
return;
|
|
222
297
|
}
|
|
298
|
+
// Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
|
|
299
|
+
// Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
|
|
300
|
+
if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
301
|
+
const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
302
|
+
const pathPrefix = !parentJoinPath ? '[populate]' : '';
|
|
303
|
+
let matched = false;
|
|
304
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
305
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
306
|
+
const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
|
|
307
|
+
const meta2 = targetMeta;
|
|
308
|
+
const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
|
|
309
|
+
const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
|
|
310
|
+
if (hasPK && !matched) {
|
|
311
|
+
matched = true;
|
|
312
|
+
let relationPojo = {};
|
|
313
|
+
const tz = this.platform.getTimezone();
|
|
314
|
+
for (const p of targetProps) {
|
|
315
|
+
this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
|
|
316
|
+
}
|
|
317
|
+
// Inject the entity class constructor so that the factory creates the correct type
|
|
318
|
+
Object.defineProperty(relationPojo, 'constructor', { value: meta2.class, enumerable: false, configurable: true });
|
|
319
|
+
result[prop.name] = relationPojo;
|
|
320
|
+
const populateChildren = hint.children || [];
|
|
321
|
+
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
|
|
322
|
+
}
|
|
323
|
+
// Clean up aliased columns for ALL targets (even non-matching ones)
|
|
324
|
+
for (const p of targetProps) {
|
|
325
|
+
for (const name of p.fieldNames) {
|
|
326
|
+
delete root[`${relationAlias}__${name}`];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!matched) {
|
|
331
|
+
result[prop.name] = null;
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
223
335
|
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
224
|
-
const meta2 =
|
|
336
|
+
const meta2 = prop.targetMeta;
|
|
225
337
|
let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
226
338
|
if (!parentJoinPath) {
|
|
227
339
|
path = '[populate]' + path;
|
|
@@ -237,7 +349,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
237
349
|
// pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
|
|
238
350
|
if (pivotRefJoin) {
|
|
239
351
|
let item;
|
|
240
|
-
if (prop.inverseJoinColumns.length > 1) {
|
|
352
|
+
if (prop.inverseJoinColumns.length > 1) {
|
|
353
|
+
// composite keys
|
|
241
354
|
item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
|
|
242
355
|
}
|
|
243
356
|
else {
|
|
@@ -252,10 +365,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
252
365
|
}
|
|
253
366
|
return;
|
|
254
367
|
}
|
|
255
|
-
const mapToPk = !!(ref || prop.mapToPk);
|
|
256
|
-
const targetProps = mapToPk
|
|
257
|
-
? meta2.getPrimaryProps()
|
|
258
|
-
: meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
|
|
368
|
+
const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
|
|
369
|
+
const targetProps = mapToPk ? meta2.getPrimaryProps() : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
|
|
259
370
|
// If the primary key value for the relation is null, we know we haven't joined to anything
|
|
260
371
|
// and therefore we don't return any record (since all values would be null)
|
|
261
372
|
const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => {
|
|
@@ -280,7 +391,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
280
391
|
.filter(prop => !ref && prop.persist === false && prop.fieldNames)
|
|
281
392
|
.forEach(prop => {
|
|
282
393
|
/* v8 ignore next */
|
|
283
|
-
if (prop.fieldNames.length > 1) {
|
|
394
|
+
if (prop.fieldNames.length > 1) {
|
|
395
|
+
// composite keys
|
|
284
396
|
relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
285
397
|
}
|
|
286
398
|
else {
|
|
@@ -290,41 +402,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
290
402
|
});
|
|
291
403
|
const tz = this.platform.getTimezone();
|
|
292
404
|
for (const prop of targetProps) {
|
|
293
|
-
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
if (prop.fieldNames.length > 1) { // composite keys
|
|
297
|
-
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
298
|
-
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
|
|
299
|
-
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
|
|
300
|
-
}
|
|
301
|
-
else if (prop.runtimeType === 'Date') {
|
|
302
|
-
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
303
|
-
const value = root[alias];
|
|
304
|
-
if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
|
|
305
|
-
relationPojo[prop.name] = this.platform.parseDate(value + tz);
|
|
306
|
-
}
|
|
307
|
-
else if (['string', 'number'].includes(typeof value)) {
|
|
308
|
-
relationPojo[prop.name] = this.platform.parseDate(value);
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
relationPojo[prop.name] = value;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
316
|
-
relationPojo[prop.name] = root[alias];
|
|
317
|
-
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
318
|
-
const item = parseJsonSafe(relationPojo[prop.name]);
|
|
319
|
-
if (Array.isArray(item)) {
|
|
320
|
-
relationPojo[prop.name] = item.map(row => row == null ? row : this.comparator.mapResult(prop.type, row));
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.type, item);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
405
|
+
this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
|
|
327
406
|
}
|
|
407
|
+
// Handle TPT polymorphic child fields - map fields from child table aliases
|
|
408
|
+
this.mapTPTChildFields(relationPojo, meta2, relationAlias, qb, root);
|
|
328
409
|
// properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
|
|
329
410
|
// so we need to delete them after everything is mapped from given level
|
|
330
411
|
for (const prop of targetProps) {
|
|
@@ -348,6 +429,65 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
348
429
|
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
|
|
349
430
|
});
|
|
350
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Maps a single property from a joined result row into the relation pojo.
|
|
434
|
+
* Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
|
|
435
|
+
*/
|
|
436
|
+
mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta, options) {
|
|
437
|
+
if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (prop.polymorphic) {
|
|
441
|
+
const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
442
|
+
const discriminatorValue = root[discriminatorAlias];
|
|
443
|
+
const pkFieldNames = prop.fieldNames.slice(1);
|
|
444
|
+
const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
445
|
+
const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
|
|
446
|
+
if (discriminatorValue != null && pkValue != null) {
|
|
447
|
+
relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
relationPojo[prop.name] = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else if (prop.fieldNames.length > 1) {
|
|
454
|
+
// composite keys
|
|
455
|
+
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
456
|
+
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
|
|
457
|
+
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
|
|
458
|
+
}
|
|
459
|
+
else if (prop.runtimeType === 'Date') {
|
|
460
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
461
|
+
const value = root[alias];
|
|
462
|
+
if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
|
|
463
|
+
relationPojo[prop.name] = this.platform.parseDate(value + tz);
|
|
464
|
+
}
|
|
465
|
+
else if (['string', 'number'].includes(typeof value)) {
|
|
466
|
+
relationPojo[prop.name] = this.platform.parseDate(value);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
relationPojo[prop.name] = value;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
474
|
+
relationPojo[prop.name] = root[alias];
|
|
475
|
+
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
476
|
+
const item = parseJsonSafe(relationPojo[prop.name]);
|
|
477
|
+
if (Array.isArray(item)) {
|
|
478
|
+
relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (options?.deleteFromRoot) {
|
|
486
|
+
for (const name of prop.fieldNames) {
|
|
487
|
+
delete root[`${relationAlias}__${name}`];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
351
491
|
async count(entityName, where, options = {}) {
|
|
352
492
|
const meta = this.metadata.get(entityName);
|
|
353
493
|
if (meta.virtual) {
|
|
@@ -356,19 +496,22 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
356
496
|
options = { populate: [], ...options };
|
|
357
497
|
const populate = options.populate;
|
|
358
498
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
499
|
+
const schema = this.getSchemaName(meta, options);
|
|
359
500
|
const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
|
|
360
501
|
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
361
502
|
if (meta && !Utils.isEmpty(populate)) {
|
|
362
|
-
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options);
|
|
503
|
+
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
363
504
|
}
|
|
505
|
+
this.validateSqlOptions(options);
|
|
364
506
|
qb.__populateWhere = options._populateWhere;
|
|
365
507
|
qb.indexHint(options.indexHint)
|
|
508
|
+
.collation(options.collation)
|
|
366
509
|
.comment(options.comments)
|
|
367
510
|
.hintComment(options.hintComments)
|
|
368
511
|
.groupBy(options.groupBy)
|
|
369
512
|
.having(options.having)
|
|
370
513
|
.populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
|
|
371
|
-
.withSchema(
|
|
514
|
+
.withSchema(schema)
|
|
372
515
|
.where(where);
|
|
373
516
|
if (options.em) {
|
|
374
517
|
await qb.applyJoinedFilters(options.em, options.filters);
|
|
@@ -377,19 +520,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
377
520
|
}
|
|
378
521
|
async nativeInsert(entityName, data, options = {}) {
|
|
379
522
|
options.convertCustomTypes ??= true;
|
|
380
|
-
const meta = this.metadata.
|
|
381
|
-
const collections = this.extractManyToMany(
|
|
382
|
-
const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
|
|
523
|
+
const meta = this.metadata.get(entityName);
|
|
524
|
+
const collections = this.extractManyToMany(meta, data);
|
|
383
525
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
384
526
|
const res = await this.rethrow(qb.insert(data).execute('run', false));
|
|
385
527
|
res.row = res.row || {};
|
|
386
528
|
let pk;
|
|
387
|
-
if (
|
|
388
|
-
|
|
529
|
+
if (meta.primaryKeys.length > 1) {
|
|
530
|
+
// owner has composite pk
|
|
531
|
+
pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
|
|
389
532
|
}
|
|
390
533
|
else {
|
|
391
534
|
/* v8 ignore next */
|
|
392
|
-
res.insertId = data[
|
|
535
|
+
res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
|
|
393
536
|
pk = [res.insertId];
|
|
394
537
|
}
|
|
395
538
|
await this.processManyToMany(meta, pk, collections, false, options);
|
|
@@ -398,25 +541,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
398
541
|
async nativeInsertMany(entityName, data, options = {}, transform) {
|
|
399
542
|
options.processCollections ??= true;
|
|
400
543
|
options.convertCustomTypes ??= true;
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
544
|
+
const entityMeta = this.metadata.get(entityName);
|
|
545
|
+
const meta = entityMeta.inheritanceType === 'tpt' ? entityMeta : entityMeta.root;
|
|
546
|
+
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
547
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
404
548
|
const set = new Set();
|
|
405
549
|
data.forEach(row => Utils.keys(row).forEach(k => set.add(k)));
|
|
406
|
-
const props = [...set].map(name => meta
|
|
407
|
-
|
|
550
|
+
const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] });
|
|
551
|
+
// For STI with conflicting fieldNames, include all alternative columns
|
|
552
|
+
let fields = Utils.flatten(props.map(prop => prop.stiFieldNames ?? prop.fieldNames));
|
|
408
553
|
const duplicates = Utils.findDuplicates(fields);
|
|
409
554
|
const params = [];
|
|
410
555
|
if (duplicates.length) {
|
|
411
556
|
fields = Utils.unique(fields);
|
|
412
557
|
}
|
|
413
|
-
|
|
414
|
-
const tableName = meta ? this.getTableName(meta, options) : this.platform.quoteIdentifier(entityName);
|
|
558
|
+
const tableName = this.getTableName(meta, options);
|
|
415
559
|
let sql = `insert into ${tableName} `;
|
|
416
560
|
sql += fields.length > 0 ? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')' : `(${this.platform.quoteIdentifier(pks[0])})`;
|
|
417
|
-
if (
|
|
418
|
-
const returningProps = meta
|
|
419
|
-
.filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
|
|
561
|
+
if (this.platform.usesOutputStatement()) {
|
|
562
|
+
const returningProps = this.getTableProps(meta)
|
|
563
|
+
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
|
|
420
564
|
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
|
|
421
565
|
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
422
566
|
sql += returningFields.length > 0 ? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}` : '';
|
|
@@ -457,25 +601,44 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
457
601
|
params.push(value);
|
|
458
602
|
};
|
|
459
603
|
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
|
|
460
|
-
sql += data
|
|
604
|
+
sql += data
|
|
605
|
+
.map(row => {
|
|
461
606
|
const keys = [];
|
|
462
607
|
const usedDups = [];
|
|
463
608
|
props.forEach(prop => {
|
|
609
|
+
// For STI with conflicting fieldNames, use discriminator to determine which field gets value
|
|
610
|
+
if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
|
|
611
|
+
const activeField = prop.stiFieldNameMap[row[meta.discriminatorColumn]];
|
|
612
|
+
for (const field of prop.stiFieldNames) {
|
|
613
|
+
params.push(field === activeField ? row[prop.name] : null);
|
|
614
|
+
keys.push('?');
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
464
618
|
if (prop.fieldNames.length > 1) {
|
|
465
619
|
const newFields = [];
|
|
466
|
-
|
|
620
|
+
let rawParam;
|
|
621
|
+
const target = row[prop.name];
|
|
622
|
+
if (prop.polymorphic && target instanceof PolymorphicRef) {
|
|
623
|
+
rawParam = target.toTuple();
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
|
|
627
|
+
}
|
|
628
|
+
// Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
|
|
629
|
+
const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
|
|
630
|
+
const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
|
|
467
631
|
// TODO(v7): instead of making this conditional here, the entity snapshot should respect `ownColumns`,
|
|
468
632
|
// but that means changing the compiled PK getters, which might be seen as breaking
|
|
469
633
|
const columns = allParam.length > 1 ? prop.fieldNames : prop.ownColumns;
|
|
470
|
-
const
|
|
634
|
+
const param = [];
|
|
471
635
|
columns.forEach((field, idx) => {
|
|
472
636
|
if (usedDups.includes(field)) {
|
|
473
637
|
return;
|
|
474
638
|
}
|
|
475
639
|
newFields.push(field);
|
|
476
|
-
|
|
640
|
+
param.push(allParam[idx]);
|
|
477
641
|
});
|
|
478
|
-
const param = Utils.flatten(newParam);
|
|
479
642
|
newFields.forEach((field, idx) => {
|
|
480
643
|
if (!duplicates.includes(field) || !usedDups.includes(field)) {
|
|
481
644
|
params.push(param[idx]);
|
|
@@ -499,11 +662,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
499
662
|
}
|
|
500
663
|
});
|
|
501
664
|
return '(' + (keys.join(', ') || 'default') + ')';
|
|
502
|
-
})
|
|
665
|
+
})
|
|
666
|
+
.join(', ');
|
|
503
667
|
}
|
|
504
668
|
if (meta && this.platform.usesReturningStatement()) {
|
|
505
|
-
const returningProps = meta
|
|
506
|
-
.filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
|
|
669
|
+
const returningProps = this.getTableProps(meta)
|
|
670
|
+
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
|
|
507
671
|
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
|
|
508
672
|
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
509
673
|
/* v8 ignore next */
|
|
@@ -515,7 +679,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
515
679
|
const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
|
|
516
680
|
let pk;
|
|
517
681
|
/* v8 ignore next */
|
|
518
|
-
if (pks.length > 1) {
|
|
682
|
+
if (pks.length > 1) {
|
|
683
|
+
// owner has composite pk
|
|
519
684
|
pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
|
|
520
685
|
}
|
|
521
686
|
else {
|
|
@@ -531,17 +696,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
531
696
|
}
|
|
532
697
|
async nativeUpdate(entityName, where, data, options = {}) {
|
|
533
698
|
options.convertCustomTypes ??= true;
|
|
534
|
-
const meta = this.metadata.
|
|
535
|
-
const pks = this.getPrimaryKeyFields(
|
|
536
|
-
const collections = this.extractManyToMany(
|
|
699
|
+
const meta = this.metadata.get(entityName);
|
|
700
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
701
|
+
const collections = this.extractManyToMany(meta, data);
|
|
537
702
|
let res = { affectedRows: 0, insertId: 0, row: {} };
|
|
538
703
|
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
539
704
|
/* v8 ignore next */
|
|
540
|
-
where = { [meta
|
|
705
|
+
where = { [meta.primaryKeys[0] ?? pks[0]]: where };
|
|
541
706
|
}
|
|
542
707
|
if (Utils.hasObjectKeys(data)) {
|
|
543
|
-
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext)
|
|
544
|
-
.withSchema(this.getSchemaName(meta, options));
|
|
708
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
545
709
|
if (options.upsert) {
|
|
546
710
|
/* v8 ignore next */
|
|
547
711
|
const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
|
|
@@ -556,14 +720,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
556
720
|
if (options.onConflictAction === 'ignore') {
|
|
557
721
|
qb.ignore();
|
|
558
722
|
}
|
|
723
|
+
if (options.onConflictWhere) {
|
|
724
|
+
qb.where(options.onConflictWhere);
|
|
725
|
+
}
|
|
559
726
|
}
|
|
560
727
|
else {
|
|
561
728
|
qb.update(data).where(where);
|
|
562
729
|
// reload generated columns and version fields
|
|
563
730
|
const returning = [];
|
|
564
|
-
meta
|
|
565
|
-
.filter(prop => (prop.generated && !prop.primary) || prop.version)
|
|
566
|
-
.forEach(prop => returning.push(prop.name));
|
|
731
|
+
meta.props.filter(prop => (prop.generated && !prop.primary) || prop.version).forEach(prop => returning.push(prop.name));
|
|
567
732
|
qb.returning(returning);
|
|
568
733
|
}
|
|
569
734
|
res = await this.rethrow(qb.execute('run', false));
|
|
@@ -578,7 +743,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
578
743
|
options.convertCustomTypes ??= true;
|
|
579
744
|
const meta = this.metadata.get(entityName);
|
|
580
745
|
if (options.upsert) {
|
|
581
|
-
const uniqueFields = options.onConflictFields ??
|
|
746
|
+
const uniqueFields = options.onConflictFields ??
|
|
747
|
+
(Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
|
|
582
748
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
583
749
|
const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
|
|
584
750
|
qb.insert(data)
|
|
@@ -591,9 +757,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
591
757
|
if (options.onConflictAction === 'ignore') {
|
|
592
758
|
qb.ignore();
|
|
593
759
|
}
|
|
760
|
+
if (options.onConflictWhere) {
|
|
761
|
+
qb.where(options.onConflictWhere);
|
|
762
|
+
}
|
|
594
763
|
return this.rethrow(qb.execute('run', false));
|
|
595
764
|
}
|
|
596
|
-
const collections = options.processCollections ? data.map(d => this.extractManyToMany(
|
|
765
|
+
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
597
766
|
const keys = new Set();
|
|
598
767
|
const fields = new Set();
|
|
599
768
|
const returning = new Set();
|
|
@@ -606,10 +775,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
606
775
|
}
|
|
607
776
|
}
|
|
608
777
|
// reload generated columns and version fields
|
|
609
|
-
meta
|
|
610
|
-
|
|
611
|
-
.
|
|
612
|
-
|
|
778
|
+
meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
|
|
779
|
+
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
|
|
780
|
+
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
|
|
781
|
+
.join(' and ');
|
|
613
782
|
const params = [];
|
|
614
783
|
let sql = `update ${this.getTableName(meta, options)} set `;
|
|
615
784
|
const addParams = (prop, value) => {
|
|
@@ -628,6 +797,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
628
797
|
};
|
|
629
798
|
for (const key of keys) {
|
|
630
799
|
const prop = meta.properties[key] ?? meta.root.properties[key];
|
|
800
|
+
if (prop.polymorphic && prop.fieldNames.length > 1) {
|
|
801
|
+
for (let idx = 0; idx < data.length; idx++) {
|
|
802
|
+
const rowValue = data[idx][key];
|
|
803
|
+
if (rowValue instanceof PolymorphicRef) {
|
|
804
|
+
data[idx][key] = rowValue.toTuple();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
631
808
|
prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
|
|
632
809
|
if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
|
|
633
810
|
return;
|
|
@@ -699,12 +876,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
699
876
|
return res;
|
|
700
877
|
}
|
|
701
878
|
async nativeDelete(entityName, where, options = {}) {
|
|
702
|
-
const meta = this.metadata.
|
|
703
|
-
const pks = this.getPrimaryKeyFields(
|
|
879
|
+
const meta = this.metadata.get(entityName);
|
|
880
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
704
881
|
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
705
882
|
where = { [pks[0]]: where };
|
|
706
883
|
}
|
|
707
|
-
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
|
|
884
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
|
|
885
|
+
.delete(where)
|
|
886
|
+
.withSchema(this.getSchemaName(meta, options));
|
|
708
887
|
return this.rethrow(qb.execute('run', false));
|
|
709
888
|
}
|
|
710
889
|
/**
|
|
@@ -753,45 +932,42 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
753
932
|
}
|
|
754
933
|
if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
|
|
755
934
|
const cols = coll.property.referencedColumnNames;
|
|
756
|
-
const qb = this.createQueryBuilder(coll.property.
|
|
757
|
-
.withSchema(this.getSchemaName(meta, options));
|
|
935
|
+
const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
|
|
758
936
|
if (coll.getSnapshot() === undefined) {
|
|
759
937
|
if (coll.property.orphanRemoval) {
|
|
760
|
-
const query = qb.delete({ [coll.property.mappedBy]: pks })
|
|
761
|
-
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
938
|
+
const query = qb.delete({ [coll.property.mappedBy]: pks }).andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
762
939
|
await this.rethrow(query.execute());
|
|
763
940
|
continue;
|
|
764
941
|
}
|
|
765
|
-
const query = qb
|
|
942
|
+
const query = qb
|
|
943
|
+
.update({ [coll.property.mappedBy]: null })
|
|
766
944
|
.where({ [coll.property.mappedBy]: pks })
|
|
767
945
|
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
768
946
|
await this.rethrow(query.execute());
|
|
769
947
|
continue;
|
|
770
948
|
}
|
|
771
949
|
/* v8 ignore next */
|
|
772
|
-
const query = qb.update({ [coll.property.mappedBy]: pks })
|
|
773
|
-
.where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
|
|
950
|
+
const query = qb.update({ [coll.property.mappedBy]: pks }).where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
|
|
774
951
|
await this.rethrow(query.execute());
|
|
775
952
|
continue;
|
|
776
953
|
}
|
|
777
|
-
/* v8 ignore next */
|
|
778
954
|
const pivotMeta = this.metadata.find(coll.property.pivotEntity);
|
|
779
955
|
let schema = pivotMeta.schema;
|
|
780
956
|
if (schema === '*') {
|
|
781
957
|
if (coll.property.owner) {
|
|
782
|
-
schema = wrapped.getSchema() === '*' ? options?.schema ?? this.config.get('schema') : wrapped.getSchema();
|
|
958
|
+
schema = wrapped.getSchema() === '*' ? (options?.schema ?? this.config.get('schema')) : wrapped.getSchema();
|
|
783
959
|
}
|
|
784
960
|
else {
|
|
785
961
|
const targetMeta = coll.property.targetMeta;
|
|
786
962
|
const targetSchema = (coll[0] ?? snap?.[0]) && helper(coll[0] ?? snap?.[0]).getSchema();
|
|
787
|
-
schema = targetMeta.schema === '*' ? options?.schema ?? targetSchema ?? this.config.get('schema') : targetMeta.schema;
|
|
963
|
+
schema = targetMeta.schema === '*' ? (options?.schema ?? targetSchema ?? this.config.get('schema')) : targetMeta.schema;
|
|
788
964
|
}
|
|
789
965
|
}
|
|
790
966
|
else if (schema == null) {
|
|
791
967
|
schema = this.config.get('schema');
|
|
792
968
|
}
|
|
793
969
|
const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
|
|
794
|
-
const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext);
|
|
970
|
+
const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
|
|
795
971
|
persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
|
|
796
972
|
}
|
|
797
973
|
for (const persister of Utils.values(groups)) {
|
|
@@ -799,13 +975,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
799
975
|
}
|
|
800
976
|
}
|
|
801
977
|
async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
978
|
+
/* v8 ignore next */
|
|
802
979
|
if (owners.length === 0) {
|
|
803
980
|
return {};
|
|
804
981
|
}
|
|
805
|
-
const pivotMeta = this.metadata.
|
|
982
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
983
|
+
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
984
|
+
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
985
|
+
}
|
|
806
986
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
807
987
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
808
|
-
const ownerMeta =
|
|
988
|
+
const ownerMeta = pivotProp2.targetMeta;
|
|
809
989
|
const cond = {
|
|
810
990
|
[pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
|
|
811
991
|
};
|
|
@@ -817,34 +997,143 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
817
997
|
const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
|
|
818
998
|
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
|
|
819
999
|
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
|
|
820
|
-
const fields = pivotJoin
|
|
821
|
-
|
|
822
|
-
: [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
823
|
-
const res = await this.find(pivotMeta.className, where, {
|
|
1000
|
+
const fields = pivotJoin ? [pivotProp1.name, pivotProp2.name] : [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
1001
|
+
const res = await this.find(pivotMeta.class, where, {
|
|
824
1002
|
ctx,
|
|
825
1003
|
...options,
|
|
826
1004
|
fields,
|
|
827
1005
|
exclude: childExclude,
|
|
828
1006
|
orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
|
|
1007
|
+
populate: [
|
|
1008
|
+
{
|
|
1009
|
+
field: populateField,
|
|
1010
|
+
strategy: LoadStrategy.JOINED,
|
|
1011
|
+
joinType: JoinType.innerJoin,
|
|
1012
|
+
children: populate,
|
|
1013
|
+
dataOnly: pivotProp1.mapToPk && !pivotJoin,
|
|
1014
|
+
},
|
|
1015
|
+
],
|
|
1016
|
+
populateWhere: undefined,
|
|
1017
|
+
// @ts-ignore
|
|
1018
|
+
_populateWhere: 'infer',
|
|
1019
|
+
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
1020
|
+
});
|
|
1021
|
+
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Load from a polymorphic M:N pivot table.
|
|
1025
|
+
*/
|
|
1026
|
+
async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
1027
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1028
|
+
// Find the M:1 relation on the pivot pointing to the target entity.
|
|
1029
|
+
// We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
|
|
1030
|
+
const inverseProp = pivotMeta.relations.find(r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta);
|
|
1031
|
+
if (inverseProp) {
|
|
1032
|
+
return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
|
|
1033
|
+
}
|
|
1034
|
+
return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Load from owner side of polymorphic M:N (e.g., Post -> Tags)
|
|
1038
|
+
*/
|
|
1039
|
+
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
|
|
1040
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1041
|
+
const targetMeta = prop.targetMeta;
|
|
1042
|
+
// Build condition: discriminator = 'post' AND {discriminator} IN (...)
|
|
1043
|
+
const cond = {
|
|
1044
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1045
|
+
[prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1046
|
+
};
|
|
1047
|
+
if (!Utils.isEmpty(where)) {
|
|
1048
|
+
cond[inverseProp.name] = { ...where };
|
|
1049
|
+
}
|
|
1050
|
+
const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
|
|
1051
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1052
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
|
|
1053
|
+
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
|
|
1054
|
+
const fields = pivotJoin
|
|
1055
|
+
? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
|
|
1056
|
+
: [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
|
|
1057
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
1058
|
+
ctx,
|
|
1059
|
+
...options,
|
|
1060
|
+
fields,
|
|
1061
|
+
exclude: childExclude,
|
|
1062
|
+
orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
|
|
1063
|
+
populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate, dataOnly: inverseProp.mapToPk && !pivotJoin }],
|
|
1064
|
+
populateWhere: undefined,
|
|
1065
|
+
// @ts-ignore
|
|
1066
|
+
_populateWhere: 'infer',
|
|
1067
|
+
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
|
|
1068
|
+
});
|
|
1069
|
+
return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
1073
|
+
* Uses single query with join via virtual relation on pivot.
|
|
1074
|
+
*/
|
|
1075
|
+
async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
|
|
1076
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1077
|
+
const targetMeta = prop.targetMeta;
|
|
1078
|
+
// Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
|
|
1079
|
+
// Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
|
|
1080
|
+
const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
|
|
1081
|
+
// Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
|
|
1082
|
+
const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
1083
|
+
const ownerProp = pivotMeta.properties[ownerRelationName];
|
|
1084
|
+
// Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
|
|
1085
|
+
const cond = {
|
|
1086
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1087
|
+
[tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1088
|
+
};
|
|
1089
|
+
if (!Utils.isEmpty(where)) {
|
|
1090
|
+
cond[ownerRelationName] = { ...where };
|
|
1091
|
+
}
|
|
1092
|
+
const populateField = ownerRelationName;
|
|
1093
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1094
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1095
|
+
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1096
|
+
const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
|
|
1097
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
1098
|
+
ctx,
|
|
1099
|
+
...options,
|
|
1100
|
+
fields,
|
|
1101
|
+
exclude: childExclude,
|
|
1102
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
829
1103
|
populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
|
|
830
1104
|
populateWhere: undefined,
|
|
831
1105
|
// @ts-ignore
|
|
832
1106
|
_populateWhere: 'infer',
|
|
833
|
-
populateFilter:
|
|
1107
|
+
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
|
|
834
1108
|
});
|
|
1109
|
+
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1113
|
+
*/
|
|
1114
|
+
buildPivotResultMap(owners, results, keyProp, valueProp) {
|
|
835
1115
|
const map = {};
|
|
836
1116
|
for (const owner of owners) {
|
|
837
1117
|
const key = Utils.getPrimaryKeyHash(owner);
|
|
838
1118
|
map[key] = [];
|
|
839
1119
|
}
|
|
840
|
-
for (const item of
|
|
841
|
-
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[
|
|
842
|
-
|
|
1120
|
+
for (const item of results) {
|
|
1121
|
+
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
|
|
1122
|
+
const entity = item[valueProp];
|
|
1123
|
+
if (map[key]) {
|
|
1124
|
+
map[key].push(entity);
|
|
1125
|
+
}
|
|
843
1126
|
}
|
|
844
1127
|
return map;
|
|
845
1128
|
}
|
|
1129
|
+
wrapPopulateFilter(options, propName) {
|
|
1130
|
+
if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
|
|
1131
|
+
return { [propName]: options?.populateFilter };
|
|
1132
|
+
}
|
|
1133
|
+
return undefined;
|
|
1134
|
+
}
|
|
846
1135
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
847
|
-
if (!Utils.isEmpty(orderBy)) {
|
|
1136
|
+
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
848
1137
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
849
1138
|
}
|
|
850
1139
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
|
|
@@ -852,7 +1141,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
852
1141
|
.filter(o => o[prop.name])
|
|
853
1142
|
.map(o => ({ [pivotProp.name]: o[prop.name] }));
|
|
854
1143
|
}
|
|
855
|
-
if (!Utils.isEmpty(prop.orderBy)) {
|
|
1144
|
+
if (!Utils.isEmpty(prop.orderBy) || RawQueryFragment.hasObjectFragments(prop.orderBy)) {
|
|
856
1145
|
return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
857
1146
|
}
|
|
858
1147
|
if (prop.fixedOrder) {
|
|
@@ -865,8 +1154,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
865
1154
|
}
|
|
866
1155
|
async *stream(entityName, where, options) {
|
|
867
1156
|
options = { populate: [], orderBy: [], ...options };
|
|
868
|
-
const meta = this.metadata.
|
|
869
|
-
if (meta
|
|
1157
|
+
const meta = this.metadata.get(entityName);
|
|
1158
|
+
if (meta.virtual) {
|
|
870
1159
|
yield* this.streamFromVirtual(entityName, where, options);
|
|
871
1160
|
return;
|
|
872
1161
|
}
|
|
@@ -989,21 +1278,46 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
989
1278
|
const populate = options.populate ?? [];
|
|
990
1279
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
991
1280
|
const populateWhereAll = options?._populateWhere === 'all' || Utils.isEmpty(options?._populateWhere);
|
|
1281
|
+
// Ensure TPT joins are applied early so that _tptAlias is available for join resolution
|
|
1282
|
+
// This is needed when populating relations that are inherited from TPT parent entities
|
|
1283
|
+
if (!options.parentJoinPath) {
|
|
1284
|
+
qb.ensureTPTJoins();
|
|
1285
|
+
}
|
|
992
1286
|
// root entity is already handled, skip that
|
|
993
1287
|
if (options.parentJoinPath) {
|
|
994
1288
|
// alias all fields in the primary table
|
|
995
1289
|
meta.props
|
|
996
1290
|
.filter(prop => this.shouldHaveColumn(meta, prop, populate, options.explicitFields, options.exclude))
|
|
997
|
-
.forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, options.explicitFields)));
|
|
1291
|
+
.forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, meta, options.schema, options.explicitFields)));
|
|
998
1292
|
}
|
|
999
1293
|
for (const hint of joinedProps) {
|
|
1000
1294
|
const [propName, ref] = hint.field.split(':', 2);
|
|
1001
1295
|
const prop = meta.properties[propName];
|
|
1296
|
+
// Polymorphic to-one: create a LEFT JOIN per target type
|
|
1297
|
+
// Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
|
|
1298
|
+
if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
1299
|
+
const basePath = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
1300
|
+
const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
|
|
1301
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
1302
|
+
const tableAlias = qb.getNextAlias(targetMeta.className);
|
|
1303
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
1304
|
+
const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
|
|
1305
|
+
qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
|
|
1306
|
+
// Select fields from each target table
|
|
1307
|
+
fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
|
|
1308
|
+
...options,
|
|
1309
|
+
populate: hint.children,
|
|
1310
|
+
parentTableAlias: tableAlias,
|
|
1311
|
+
parentJoinPath: targetPath,
|
|
1312
|
+
}));
|
|
1313
|
+
}
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1002
1316
|
// ignore ref joins of known FKs unless it's a filter hint
|
|
1003
1317
|
if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
|
|
1004
1318
|
continue;
|
|
1005
1319
|
}
|
|
1006
|
-
const meta2 =
|
|
1320
|
+
const meta2 = prop.targetMeta;
|
|
1007
1321
|
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
1008
1322
|
const tableAlias = qb.getNextAlias(prop.name);
|
|
1009
1323
|
const field = `${options.parentTableAlias}.${prop.name}`;
|
|
@@ -1019,7 +1333,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1019
1333
|
: (hint.filter && !prop.nullable) || mandatoryToOneProperty
|
|
1020
1334
|
? JoinType.innerJoin
|
|
1021
1335
|
: JoinType.leftJoin;
|
|
1022
|
-
|
|
1336
|
+
const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
|
|
1337
|
+
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1338
|
+
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1339
|
+
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
1340
|
+
// Use the registry metadata to ensure allTPTDescendants is available
|
|
1341
|
+
const tptMeta = this.metadata.get(meta2.class);
|
|
1342
|
+
this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
|
|
1343
|
+
}
|
|
1023
1344
|
if (pivotRefJoin) {
|
|
1024
1345
|
fields.push(...prop.joinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)), ...prop.inverseJoinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
|
|
1025
1346
|
}
|
|
@@ -1040,7 +1361,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1040
1361
|
}
|
|
1041
1362
|
});
|
|
1042
1363
|
const childExclude = options.exclude ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
|
|
1043
|
-
if (!ref && !prop.mapToPk) {
|
|
1364
|
+
if (!ref && (!prop.mapToPk || hint.dataOnly)) {
|
|
1044
1365
|
fields.push(...this.getFieldsForJoinedLoad(qb, meta2, {
|
|
1045
1366
|
...options,
|
|
1046
1367
|
explicitFields: childExplicitFields.length === 0 ? undefined : childExplicitFields,
|
|
@@ -1050,23 +1371,127 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1050
1371
|
parentJoinPath: path,
|
|
1051
1372
|
}));
|
|
1052
1373
|
}
|
|
1053
|
-
else if (hint.filter || prop.mapToPk || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
|
|
1374
|
+
else if (hint.filter || (prop.mapToPk && !hint.dataOnly) || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
|
|
1054
1375
|
fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
|
|
1055
1376
|
}
|
|
1056
1377
|
}
|
|
1057
1378
|
return fields;
|
|
1058
1379
|
}
|
|
1059
1380
|
/**
|
|
1381
|
+
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
1060
1382
|
* @internal
|
|
1061
1383
|
*/
|
|
1062
|
-
|
|
1384
|
+
addTPTPolymorphicJoinsForRelation(qb, meta, baseAlias, fields) {
|
|
1385
|
+
// allTPTDescendants is pre-computed during discovery, sorted by depth (deepest first)
|
|
1386
|
+
const descendants = meta.allTPTDescendants;
|
|
1387
|
+
const childAliases = {};
|
|
1388
|
+
// LEFT JOIN each descendant table
|
|
1389
|
+
for (const childMeta of descendants) {
|
|
1390
|
+
const childAlias = qb.getNextAlias(childMeta.className);
|
|
1391
|
+
qb.createAlias(childMeta.class, childAlias);
|
|
1392
|
+
childAliases[childMeta.className] = childAlias;
|
|
1393
|
+
qb.addPropertyJoin(childMeta.tptInverseProp, baseAlias, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
|
|
1394
|
+
// Add fields from this child (only ownProps, skip PKs)
|
|
1395
|
+
for (const prop of childMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
|
|
1396
|
+
for (const fieldName of prop.fieldNames) {
|
|
1397
|
+
const field = `${childAlias}.${fieldName}`;
|
|
1398
|
+
const fieldAlias = `${childAlias}__${fieldName}`;
|
|
1399
|
+
fields.push(raw(`${this.platform.quoteIdentifier(field)} as ${this.platform.quoteIdentifier(fieldAlias)}`));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
// Add computed discriminator (descendants already sorted by depth)
|
|
1404
|
+
if (meta.root.tptDiscriminatorColumn) {
|
|
1405
|
+
fields.push(this.buildTPTDiscriminatorExpression(meta, descendants, childAliases, baseAlias));
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Find the alias for a TPT child table in the query builder.
|
|
1410
|
+
* @internal
|
|
1411
|
+
*/
|
|
1412
|
+
findTPTChildAlias(qb, childMeta) {
|
|
1413
|
+
const joins = qb._joins;
|
|
1414
|
+
for (const key of Object.keys(joins)) {
|
|
1415
|
+
if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
|
|
1416
|
+
return joins[key].alias;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return undefined;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Builds a CASE WHEN expression for TPT discriminator.
|
|
1423
|
+
* Determines concrete entity type based on which child table has a non-null PK.
|
|
1424
|
+
* @internal
|
|
1425
|
+
*/
|
|
1426
|
+
buildTPTDiscriminatorExpression(meta, descendants, aliasMap, baseAlias) {
|
|
1427
|
+
const cases = descendants.map(child => {
|
|
1428
|
+
const childAlias = aliasMap[child.className];
|
|
1429
|
+
const pkFieldName = child.properties[child.primaryKeys[0]].fieldNames[0];
|
|
1430
|
+
return `when ${this.platform.quoteIdentifier(`${childAlias}.${pkFieldName}`)} is not null then '${child.discriminatorValue}'`;
|
|
1431
|
+
});
|
|
1432
|
+
const defaultValue = meta.abstract ? 'null' : `'${meta.discriminatorValue}'`;
|
|
1433
|
+
const caseExpr = `case ${cases.join(' ')} else ${defaultValue} end`;
|
|
1434
|
+
const aliased = this.platform.quoteIdentifier(`${baseAlias}__${meta.root.tptDiscriminatorColumn}`);
|
|
1435
|
+
return raw(`${caseExpr} as ${aliased}`);
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Maps TPT child-specific fields during hydration.
|
|
1439
|
+
* When a relation points to a TPT base class, the actual entity might be a child class.
|
|
1440
|
+
* This method reads the discriminator to determine the concrete type and maps child-specific fields.
|
|
1441
|
+
* @internal
|
|
1442
|
+
*/
|
|
1443
|
+
mapTPTChildFields(relationPojo, meta, relationAlias, qb, root) {
|
|
1444
|
+
// Check if this is a TPT base with polymorphic children
|
|
1445
|
+
if (meta.inheritanceType !== 'tpt' || !meta.root.tptDiscriminatorColumn) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
// Read the discriminator value
|
|
1449
|
+
const discriminatorAlias = `${relationAlias}__${meta.root.tptDiscriminatorColumn}`;
|
|
1450
|
+
const discriminatorValue = root[discriminatorAlias];
|
|
1451
|
+
if (!discriminatorValue) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
// Set the discriminator in the pojo for EntityFactory
|
|
1455
|
+
relationPojo[meta.root.tptDiscriminatorColumn] = discriminatorValue;
|
|
1456
|
+
// Find the concrete metadata from discriminator map
|
|
1457
|
+
const concreteClass = meta.root.discriminatorMap?.[discriminatorValue];
|
|
1458
|
+
/* v8 ignore next 3 - defensive check for invalid discriminator values */
|
|
1459
|
+
if (!concreteClass) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const concreteMeta = this.metadata.get(concreteClass);
|
|
1463
|
+
if (concreteMeta === meta) {
|
|
1464
|
+
// Already the concrete type, no child fields to map
|
|
1465
|
+
delete root[discriminatorAlias];
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
// Traverse up from concrete type and map fields from each level's table
|
|
1469
|
+
const tz = this.platform.getTimezone();
|
|
1470
|
+
let currentMeta = concreteMeta;
|
|
1471
|
+
while (currentMeta && currentMeta !== meta) {
|
|
1472
|
+
const childAlias = this.findTPTChildAlias(qb, currentMeta);
|
|
1473
|
+
if (childAlias) {
|
|
1474
|
+
// Map fields using same filtering as joined loading, plus skip PKs
|
|
1475
|
+
for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
|
|
1476
|
+
this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, { deleteFromRoot: true });
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
currentMeta = currentMeta.tptParent;
|
|
1480
|
+
}
|
|
1481
|
+
// Clean up the discriminator alias
|
|
1482
|
+
delete root[discriminatorAlias];
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* @internal
|
|
1486
|
+
*/
|
|
1487
|
+
mapPropToFieldNames(qb, prop, tableAlias, meta, schema, explicitFields) {
|
|
1063
1488
|
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
1064
1489
|
return Object.entries(prop.embeddedProps).flatMap(([name, childProp]) => {
|
|
1065
1490
|
const childFields = explicitFields ? Utils.extractChildElements(explicitFields, prop.name) : [];
|
|
1066
1491
|
if (!this.shouldHaveColumn(prop.targetMeta, { ...childProp, name }, [], childFields.length > 0 ? childFields : undefined)) {
|
|
1067
1492
|
return [];
|
|
1068
1493
|
}
|
|
1069
|
-
return this.mapPropToFieldNames(qb, childProp, tableAlias, childFields);
|
|
1494
|
+
return this.mapPropToFieldNames(qb, childProp, tableAlias, meta, schema, childFields);
|
|
1070
1495
|
});
|
|
1071
1496
|
}
|
|
1072
1497
|
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
|
|
@@ -1085,11 +1510,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1085
1510
|
return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
|
|
1086
1511
|
}
|
|
1087
1512
|
if (prop.formula) {
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1513
|
+
const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
|
|
1514
|
+
const table = this.createFormulaTable(quotedAlias, meta, schema);
|
|
1515
|
+
const columns = meta.createColumnMappingObject(tableAlias);
|
|
1516
|
+
return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
|
|
1090
1517
|
}
|
|
1091
1518
|
return prop.fieldNames.map(fieldName => {
|
|
1092
|
-
return `${tableAlias}.${fieldName}
|
|
1519
|
+
return raw('?? as ??', [`${tableAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
|
|
1093
1520
|
});
|
|
1094
1521
|
}
|
|
1095
1522
|
/** @internal */
|
|
@@ -1114,26 +1541,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1114
1541
|
}
|
|
1115
1542
|
return 'write';
|
|
1116
1543
|
}
|
|
1117
|
-
extractManyToMany(
|
|
1118
|
-
if (!this.metadata.has(entityName)) {
|
|
1119
|
-
return {};
|
|
1120
|
-
}
|
|
1544
|
+
extractManyToMany(meta, data) {
|
|
1121
1545
|
const ret = {};
|
|
1122
|
-
|
|
1546
|
+
for (const prop of meta.relations) {
|
|
1123
1547
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
|
|
1124
1548
|
ret[prop.name] = data[prop.name].map((item) => Utils.asArray(item));
|
|
1125
1549
|
delete data[prop.name];
|
|
1126
1550
|
}
|
|
1127
|
-
}
|
|
1551
|
+
}
|
|
1128
1552
|
return ret;
|
|
1129
1553
|
}
|
|
1130
1554
|
async processManyToMany(meta, pks, collections, clear, options) {
|
|
1131
|
-
if (!meta) {
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
1555
|
for (const prop of meta.relations) {
|
|
1135
1556
|
if (collections[prop.name]) {
|
|
1136
|
-
const pivotMeta = this.metadata.
|
|
1557
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1137
1558
|
const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
|
|
1138
1559
|
persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
|
|
1139
1560
|
await this.rethrow(persister.execute());
|
|
@@ -1142,9 +1563,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1142
1563
|
}
|
|
1143
1564
|
async lockPessimistic(entity, options) {
|
|
1144
1565
|
const meta = helper(entity).__meta;
|
|
1145
|
-
const qb = this.createQueryBuilder(
|
|
1566
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
|
|
1146
1567
|
const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
|
|
1147
|
-
qb.select(raw('1'))
|
|
1568
|
+
qb.select(raw('1'))
|
|
1569
|
+
.where(cond)
|
|
1570
|
+
.setLockMode(options.lockMode, options.lockTableAliases);
|
|
1148
1571
|
await this.rethrow(qb.execute());
|
|
1149
1572
|
}
|
|
1150
1573
|
buildPopulateWhere(meta, joinedProps, options) {
|
|
@@ -1152,21 +1575,24 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1152
1575
|
for (const hint of joinedProps) {
|
|
1153
1576
|
const [propName] = hint.field.split(':', 2);
|
|
1154
1577
|
const prop = meta.properties[propName];
|
|
1155
|
-
if (!Utils.isEmpty(prop.where)) {
|
|
1578
|
+
if (!Utils.isEmpty(prop.where) || RawQueryFragment.hasObjectFragments(prop.where)) {
|
|
1156
1579
|
where[prop.name] = Utils.copy(prop.where);
|
|
1157
1580
|
}
|
|
1158
1581
|
if (hint.children) {
|
|
1159
|
-
const
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
1582
|
+
const targetMeta = prop.targetMeta;
|
|
1583
|
+
if (targetMeta) {
|
|
1584
|
+
const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
|
|
1585
|
+
if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
|
|
1586
|
+
where[prop.name] ??= {};
|
|
1587
|
+
Object.assign(where[prop.name], inner);
|
|
1588
|
+
}
|
|
1163
1589
|
}
|
|
1164
1590
|
}
|
|
1165
1591
|
}
|
|
1166
|
-
if (Utils.isEmpty(options.populateWhere)) {
|
|
1592
|
+
if (Utils.isEmpty(options.populateWhere) && !RawQueryFragment.hasObjectFragments(options.populateWhere)) {
|
|
1167
1593
|
return where;
|
|
1168
1594
|
}
|
|
1169
|
-
if (Utils.isEmpty(where)) {
|
|
1595
|
+
if (Utils.isEmpty(where) && !RawQueryFragment.hasObjectFragments(where)) {
|
|
1170
1596
|
return options.populateWhere;
|
|
1171
1597
|
}
|
|
1172
1598
|
/* v8 ignore next */
|
|
@@ -1178,31 +1604,32 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1178
1604
|
// as `options.populateWhere` will be always recomputed to respect filters
|
|
1179
1605
|
const populateWhereAll = options._populateWhere !== 'infer' && !Utils.isEmpty(options._populateWhere);
|
|
1180
1606
|
const path = (populateWhereAll ? '[populate]' : '') + meta.className;
|
|
1607
|
+
const optionsOrderBy = Utils.asArray(options.orderBy);
|
|
1181
1608
|
const populateOrderBy = this.buildPopulateOrderBy(qb, meta, Utils.asArray(options.populateOrderBy ?? options.orderBy), path, !!options.populateOrderBy);
|
|
1182
1609
|
const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
|
|
1183
|
-
return [...
|
|
1610
|
+
return [...optionsOrderBy, ...populateOrderBy, ...joinedPropsOrderBy];
|
|
1184
1611
|
}
|
|
1185
1612
|
buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, explicit, parentAlias = qb.alias) {
|
|
1186
1613
|
const orderBy = [];
|
|
1187
1614
|
for (let i = 0; i < populateOrderBy.length; i++) {
|
|
1188
1615
|
const orderHint = populateOrderBy[i];
|
|
1189
|
-
for (const
|
|
1190
|
-
const
|
|
1191
|
-
if (
|
|
1192
|
-
const sql =
|
|
1193
|
-
const
|
|
1194
|
-
orderBy.push({ [
|
|
1616
|
+
for (const field of Utils.getObjectQueryKeys(orderHint)) {
|
|
1617
|
+
const childOrder = orderHint[field];
|
|
1618
|
+
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
1619
|
+
const { sql, params } = RawQueryFragment.getKnownFragment(field);
|
|
1620
|
+
const key = raw(sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias), params);
|
|
1621
|
+
orderBy.push({ [key]: childOrder });
|
|
1195
1622
|
continue;
|
|
1196
1623
|
}
|
|
1197
|
-
const prop = meta.properties[
|
|
1624
|
+
const prop = meta.properties[field];
|
|
1198
1625
|
if (!prop) {
|
|
1199
|
-
throw new Error(`Trying to order by not existing property ${meta.className}.${
|
|
1626
|
+
throw new Error(`Trying to order by not existing property ${meta.className}.${field}`);
|
|
1200
1627
|
}
|
|
1201
1628
|
let path = parentPath;
|
|
1202
|
-
const meta2 =
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
path += `.${
|
|
1629
|
+
const meta2 = prop.targetMeta;
|
|
1630
|
+
if (prop.kind !== ReferenceKind.SCALAR &&
|
|
1631
|
+
(![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
|
|
1632
|
+
path += `.${field}`;
|
|
1206
1633
|
}
|
|
1207
1634
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
|
|
1208
1635
|
path += '[pivot]';
|
|
@@ -1228,9 +1655,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1228
1655
|
}
|
|
1229
1656
|
continue;
|
|
1230
1657
|
}
|
|
1231
|
-
const order = typeof childOrder === 'object' ? childOrder[
|
|
1658
|
+
const order = typeof childOrder === 'object' ? childOrder[field] : childOrder;
|
|
1232
1659
|
if (order) {
|
|
1233
|
-
orderBy.push({ [`${propAlias}.${
|
|
1660
|
+
orderBy.push({ [`${propAlias}.${field}`]: order });
|
|
1234
1661
|
}
|
|
1235
1662
|
}
|
|
1236
1663
|
}
|
|
@@ -1242,39 +1669,41 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1242
1669
|
for (const hint of joinedProps) {
|
|
1243
1670
|
const [propName, ref] = hint.field.split(':', 2);
|
|
1244
1671
|
const prop = meta.properties[propName];
|
|
1245
|
-
const propOrderBy = prop.orderBy;
|
|
1246
1672
|
let path = `${parentPath}.${propName}`;
|
|
1247
1673
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && ref) {
|
|
1248
1674
|
path += '[pivot]';
|
|
1249
1675
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const meta2 = this.metadata.find(prop.type);
|
|
1253
|
-
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
|
|
1254
|
-
const alias = ref ? propAlias : join.ownerAlias;
|
|
1255
|
-
orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
|
|
1256
|
-
}
|
|
1257
|
-
if (propOrderBy) {
|
|
1258
|
-
for (const item of Utils.asArray(propOrderBy)) {
|
|
1259
|
-
for (const field of Utils.keys(item)) {
|
|
1260
|
-
const rawField = RawQueryFragment.getKnownFragment(field, false);
|
|
1261
|
-
if (rawField) {
|
|
1262
|
-
const sql = propAlias ? rawField.sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : rawField.sql;
|
|
1263
|
-
const raw2 = raw(sql, rawField.params);
|
|
1264
|
-
orderBy.push({ [raw2.toString()]: item[field] });
|
|
1265
|
-
continue;
|
|
1266
|
-
}
|
|
1267
|
-
orderBy.push({ [`${propAlias}.${field}`]: item[field] });
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1676
|
+
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
|
|
1677
|
+
this.buildToManyOrderBy(qb, prop, path, ref, orderBy);
|
|
1270
1678
|
}
|
|
1271
1679
|
if (hint.children) {
|
|
1272
|
-
|
|
1273
|
-
orderBy.push(...buildJoinedPropsOrderBy);
|
|
1680
|
+
orderBy.push(...this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path));
|
|
1274
1681
|
}
|
|
1275
1682
|
}
|
|
1276
1683
|
return orderBy;
|
|
1277
1684
|
}
|
|
1685
|
+
buildToManyOrderBy(qb, prop, path, ref, orderBy) {
|
|
1686
|
+
const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
|
|
1687
|
+
const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
|
|
1688
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
|
|
1689
|
+
const alias = ref ? propAlias : join.ownerAlias;
|
|
1690
|
+
orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
|
|
1691
|
+
}
|
|
1692
|
+
const effectiveOrderBy = QueryHelper.mergeOrderBy(prop.orderBy, prop.targetMeta?.orderBy);
|
|
1693
|
+
for (const item of effectiveOrderBy) {
|
|
1694
|
+
for (const field of Utils.getObjectQueryKeys(item)) {
|
|
1695
|
+
const order = item[field];
|
|
1696
|
+
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
1697
|
+
const { sql, params } = RawQueryFragment.getKnownFragment(field);
|
|
1698
|
+
const sql2 = propAlias ? sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : sql;
|
|
1699
|
+
const key = raw(sql2, params);
|
|
1700
|
+
orderBy.push({ [key]: order });
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
orderBy.push({ [`${propAlias}.${field}`]: order });
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1278
1707
|
normalizeFields(fields, prefix = '') {
|
|
1279
1708
|
const ret = [];
|
|
1280
1709
|
for (const field of fields) {
|
|
@@ -1313,7 +1742,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1313
1742
|
}
|
|
1314
1743
|
ret.push(prop.name);
|
|
1315
1744
|
}
|
|
1316
|
-
buildFields(meta, populate, joinedProps, qb, alias, options) {
|
|
1745
|
+
buildFields(meta, populate, joinedProps, qb, alias, options, schema) {
|
|
1317
1746
|
const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => this.isPopulated(meta, prop, p)));
|
|
1318
1747
|
const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
|
|
1319
1748
|
const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
|
|
@@ -1332,7 +1761,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1332
1761
|
const prop = QueryHelper.findProperty(rootPropName, {
|
|
1333
1762
|
metadata: this.metadata,
|
|
1334
1763
|
platform: this.platform,
|
|
1335
|
-
entityName: meta.
|
|
1764
|
+
entityName: meta.class,
|
|
1336
1765
|
where: {},
|
|
1337
1766
|
aliasMap: qb.getAliasMap(),
|
|
1338
1767
|
});
|
|
@@ -1341,7 +1770,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1341
1770
|
if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
|
|
1342
1771
|
ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
|
|
1343
1772
|
}
|
|
1344
|
-
if (meta.root.
|
|
1773
|
+
if (meta.root.inheritanceType === 'sti' && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
|
|
1345
1774
|
ret.push(meta.root.discriminatorColumn);
|
|
1346
1775
|
}
|
|
1347
1776
|
}
|
|
@@ -1358,14 +1787,18 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1358
1787
|
ret.push('*');
|
|
1359
1788
|
}
|
|
1360
1789
|
if (ret.length > 0 && !hasExplicitFields && addFormulas) {
|
|
1790
|
+
// Create formula column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
|
|
1791
|
+
const quotedAlias = this.platform.quoteIdentifier(alias);
|
|
1792
|
+
const columns = meta.createColumnMappingObject(alias);
|
|
1793
|
+
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
1361
1794
|
for (const prop of meta.props) {
|
|
1362
1795
|
if (lazyProps.includes(prop)) {
|
|
1363
1796
|
continue;
|
|
1364
1797
|
}
|
|
1365
1798
|
if (prop.formula) {
|
|
1366
|
-
const a = this.platform.quoteIdentifier(alias);
|
|
1367
1799
|
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
1368
|
-
|
|
1800
|
+
const table = this.createFormulaTable(quotedAlias.toString(), meta, effectiveSchema);
|
|
1801
|
+
ret.push(raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`));
|
|
1369
1802
|
}
|
|
1370
1803
|
if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
|
|
1371
1804
|
ret.push(prop.name);
|