@mikro-orm/sql 7.0.0-dev.99 → 7.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AbstractSqlConnection.d.ts +2 -4
- package/AbstractSqlConnection.js +3 -7
- package/AbstractSqlDriver.d.ts +82 -23
- package/AbstractSqlDriver.js +584 -184
- package/AbstractSqlPlatform.d.ts +3 -4
- package/AbstractSqlPlatform.js +0 -4
- package/PivotCollectionPersister.d.ts +5 -0
- package/PivotCollectionPersister.js +30 -12
- package/SqlEntityManager.d.ts +2 -2
- package/dialects/mysql/{MySqlPlatform.d.ts → BaseMySqlPlatform.d.ts} +3 -2
- package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +5 -1
- package/dialects/mysql/MySqlSchemaHelper.d.ts +12 -1
- package/dialects/mysql/MySqlSchemaHelper.js +97 -6
- package/dialects/mysql/index.d.ts +1 -2
- package/dialects/mysql/index.js +1 -2
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +106 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +350 -0
- package/dialects/postgresql/FullTextType.d.ts +14 -0
- package/dialects/postgresql/FullTextType.js +59 -0
- package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +8 -0
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +47 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +90 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +732 -0
- package/dialects/postgresql/index.d.ts +3 -0
- package/dialects/postgresql/index.js +3 -0
- package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -0
- package/dialects/sqlite/BaseSqliteConnection.js +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 +50 -35
- package/query/QueryBuilder.d.ts +548 -79
- package/query/QueryBuilder.js +537 -159
- package/query/QueryBuilderHelper.d.ts +22 -14
- package/query/QueryBuilderHelper.js +158 -69
- package/query/ScalarCriteriaNode.js +2 -2
- package/query/raw.d.ts +11 -3
- package/query/raw.js +1 -2
- package/schema/DatabaseSchema.d.ts +15 -2
- package/schema/DatabaseSchema.js +143 -15
- package/schema/DatabaseTable.d.ts +12 -0
- package/schema/DatabaseTable.js +91 -31
- package/schema/SchemaComparator.d.ts +8 -0
- package/schema/SchemaComparator.js +126 -3
- package/schema/SchemaHelper.d.ts +26 -3
- package/schema/SchemaHelper.js +98 -11
- package/schema/SqlSchemaGenerator.d.ts +10 -0
- package/schema/SqlSchemaGenerator.js +137 -9
- package/tsconfig.build.tsbuildinfo +1 -0
- package/typings.d.ts +74 -36
- package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +0 -1
- package/dialects/postgresql/PostgreSqlTableCompiler.js +0 -1
package/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,21 @@ 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
|
+
}
|
|
20
35
|
createEntityManager(useContext) {
|
|
21
36
|
const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
|
|
22
37
|
return new EntityManagerClass(this.config, this, this.metadata, useContext);
|
|
@@ -25,8 +40,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
25
40
|
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
|
|
26
41
|
const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
|
|
27
42
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
28
|
-
const
|
|
29
|
-
const
|
|
43
|
+
const schema = this.getSchemaName(meta, options);
|
|
44
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em).withSchema(schema);
|
|
45
|
+
const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
30
46
|
const orderBy = this.buildOrderBy(qb, meta, populate, options);
|
|
31
47
|
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
32
48
|
Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
|
|
@@ -44,8 +60,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
44
60
|
.having(options.having)
|
|
45
61
|
.indexHint(options.indexHint)
|
|
46
62
|
.comment(options.comments)
|
|
47
|
-
.hintComment(options.hintComments)
|
|
48
|
-
.withSchema(this.getSchemaName(meta, options));
|
|
63
|
+
.hintComment(options.hintComments);
|
|
49
64
|
if (isCursorPagination) {
|
|
50
65
|
const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
|
|
51
66
|
qb.andWhere(where).orderBy(newOrderBy);
|
|
@@ -66,8 +81,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
66
81
|
}
|
|
67
82
|
async find(entityName, where, options = {}) {
|
|
68
83
|
options = { populate: [], orderBy: [], ...options };
|
|
69
|
-
const meta = this.metadata.
|
|
70
|
-
if (meta
|
|
84
|
+
const meta = this.metadata.get(entityName);
|
|
85
|
+
if (meta.virtual) {
|
|
71
86
|
return this.findVirtual(entityName, where, options);
|
|
72
87
|
}
|
|
73
88
|
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
@@ -170,11 +185,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
170
185
|
const isCursorPagination = [options.first, options.last, options.before, options.after].some(v => v != null);
|
|
171
186
|
const native = qb.getNativeQuery(false);
|
|
172
187
|
if (type === QueryType.COUNT) {
|
|
173
|
-
native
|
|
174
|
-
.clear('select')
|
|
175
|
-
.clear('limit')
|
|
176
|
-
.clear('offset')
|
|
177
|
-
.count();
|
|
188
|
+
native.clear('select').clear('limit').clear('offset').count();
|
|
178
189
|
}
|
|
179
190
|
native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
|
|
180
191
|
const query = native.compile();
|
|
@@ -200,6 +211,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
mapResult(result, meta, populate = [], qb, map = {}) {
|
|
214
|
+
// For TPT inheritance, map aliased parent table columns back to their field names
|
|
215
|
+
if (qb && meta.inheritanceType === 'tpt' && meta.tptParent) {
|
|
216
|
+
this.mapTPTColumns(result, meta, qb);
|
|
217
|
+
}
|
|
218
|
+
// For TPT polymorphic queries (querying a base class), map child table fields
|
|
219
|
+
if (qb && meta.inheritanceType === 'tpt' && meta.allTPTDescendants?.length) {
|
|
220
|
+
const mainAlias = qb.mainAlias?.aliasName ?? 'e0';
|
|
221
|
+
this.mapTPTChildFields(result, meta, mainAlias, qb, result);
|
|
222
|
+
}
|
|
203
223
|
const ret = super.mapResult(result, meta);
|
|
204
224
|
/* v8 ignore next */
|
|
205
225
|
if (!ret) {
|
|
@@ -211,6 +231,33 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
211
231
|
}
|
|
212
232
|
return ret;
|
|
213
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Maps aliased columns from TPT parent tables back to their original field names.
|
|
236
|
+
* TPT parent columns are selected with aliases like `parent_alias__column_name`,
|
|
237
|
+
* and need to be renamed back to `column_name` for the result mapper to work.
|
|
238
|
+
*/
|
|
239
|
+
mapTPTColumns(result, meta, qb) {
|
|
240
|
+
const tptAliases = qb._tptAlias;
|
|
241
|
+
// Walk up the TPT hierarchy
|
|
242
|
+
let parentMeta = meta.tptParent;
|
|
243
|
+
while (parentMeta) {
|
|
244
|
+
const parentAlias = tptAliases[parentMeta.className];
|
|
245
|
+
if (parentAlias) {
|
|
246
|
+
// Rename columns from this parent table
|
|
247
|
+
for (const prop of parentMeta.ownProps) {
|
|
248
|
+
for (const fieldName of prop.fieldNames) {
|
|
249
|
+
const aliasedKey = `${parentAlias}__${fieldName}`;
|
|
250
|
+
if (aliasedKey in result) {
|
|
251
|
+
// Copy the value to the unaliased field name and remove the aliased key
|
|
252
|
+
result[fieldName] = result[aliasedKey];
|
|
253
|
+
delete result[aliasedKey];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
parentMeta = parentMeta.tptParent;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
214
261
|
mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
|
|
215
262
|
const joinedProps = this.joinedProps(meta, populate);
|
|
216
263
|
joinedProps.forEach(hint => {
|
|
@@ -220,8 +267,45 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
220
267
|
if (!prop) {
|
|
221
268
|
return;
|
|
222
269
|
}
|
|
270
|
+
// Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
|
|
271
|
+
// Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
|
|
272
|
+
if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
273
|
+
const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
274
|
+
const pathPrefix = !parentJoinPath ? '[populate]' : '';
|
|
275
|
+
let matched = false;
|
|
276
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
277
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
278
|
+
const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
|
|
279
|
+
const meta2 = targetMeta;
|
|
280
|
+
const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
|
|
281
|
+
const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
|
|
282
|
+
if (hasPK && !matched) {
|
|
283
|
+
matched = true;
|
|
284
|
+
let relationPojo = {};
|
|
285
|
+
const tz = this.platform.getTimezone();
|
|
286
|
+
for (const p of targetProps) {
|
|
287
|
+
this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
|
|
288
|
+
}
|
|
289
|
+
// Inject the entity class constructor so that the factory creates the correct type
|
|
290
|
+
Object.defineProperty(relationPojo, 'constructor', { value: meta2.class, enumerable: false, configurable: true });
|
|
291
|
+
result[prop.name] = relationPojo;
|
|
292
|
+
const populateChildren = hint.children || [];
|
|
293
|
+
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
|
|
294
|
+
}
|
|
295
|
+
// Clean up aliased columns for ALL targets (even non-matching ones)
|
|
296
|
+
for (const p of targetProps) {
|
|
297
|
+
for (const name of p.fieldNames) {
|
|
298
|
+
delete root[`${relationAlias}__${name}`];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!matched) {
|
|
303
|
+
result[prop.name] = null;
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
223
307
|
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
224
|
-
const meta2 =
|
|
308
|
+
const meta2 = prop.targetMeta;
|
|
225
309
|
let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
226
310
|
if (!parentJoinPath) {
|
|
227
311
|
path = '[populate]' + path;
|
|
@@ -237,7 +321,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
237
321
|
// pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
|
|
238
322
|
if (pivotRefJoin) {
|
|
239
323
|
let item;
|
|
240
|
-
if (prop.inverseJoinColumns.length > 1) {
|
|
324
|
+
if (prop.inverseJoinColumns.length > 1) {
|
|
325
|
+
// composite keys
|
|
241
326
|
item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
|
|
242
327
|
}
|
|
243
328
|
else {
|
|
@@ -252,10 +337,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
252
337
|
}
|
|
253
338
|
return;
|
|
254
339
|
}
|
|
255
|
-
const mapToPk = !!(ref || prop.mapToPk);
|
|
256
|
-
const targetProps = mapToPk
|
|
257
|
-
? meta2.getPrimaryProps()
|
|
258
|
-
: meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
|
|
340
|
+
const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
|
|
341
|
+
const targetProps = mapToPk ? meta2.getPrimaryProps() : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
|
|
259
342
|
// If the primary key value for the relation is null, we know we haven't joined to anything
|
|
260
343
|
// and therefore we don't return any record (since all values would be null)
|
|
261
344
|
const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => {
|
|
@@ -280,7 +363,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
280
363
|
.filter(prop => !ref && prop.persist === false && prop.fieldNames)
|
|
281
364
|
.forEach(prop => {
|
|
282
365
|
/* v8 ignore next */
|
|
283
|
-
if (prop.fieldNames.length > 1) {
|
|
366
|
+
if (prop.fieldNames.length > 1) {
|
|
367
|
+
// composite keys
|
|
284
368
|
relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
285
369
|
}
|
|
286
370
|
else {
|
|
@@ -290,41 +374,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
290
374
|
});
|
|
291
375
|
const tz = this.platform.getTimezone();
|
|
292
376
|
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
|
-
}
|
|
377
|
+
this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
|
|
327
378
|
}
|
|
379
|
+
// Handle TPT polymorphic child fields - map fields from child table aliases
|
|
380
|
+
this.mapTPTChildFields(relationPojo, meta2, relationAlias, qb, root);
|
|
328
381
|
// properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
|
|
329
382
|
// so we need to delete them after everything is mapped from given level
|
|
330
383
|
for (const prop of targetProps) {
|
|
@@ -348,6 +401,65 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
348
401
|
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
|
|
349
402
|
});
|
|
350
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Maps a single property from a joined result row into the relation pojo.
|
|
406
|
+
* Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
|
|
407
|
+
*/
|
|
408
|
+
mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta, options) {
|
|
409
|
+
if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (prop.polymorphic) {
|
|
413
|
+
const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
414
|
+
const discriminatorValue = root[discriminatorAlias];
|
|
415
|
+
const pkFieldNames = prop.fieldNames.slice(1);
|
|
416
|
+
const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
417
|
+
const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
|
|
418
|
+
if (discriminatorValue != null && pkValue != null) {
|
|
419
|
+
relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
relationPojo[prop.name] = null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (prop.fieldNames.length > 1) {
|
|
426
|
+
// composite keys
|
|
427
|
+
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
428
|
+
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
|
|
429
|
+
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
|
|
430
|
+
}
|
|
431
|
+
else if (prop.runtimeType === 'Date') {
|
|
432
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
433
|
+
const value = root[alias];
|
|
434
|
+
if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
|
|
435
|
+
relationPojo[prop.name] = this.platform.parseDate(value + tz);
|
|
436
|
+
}
|
|
437
|
+
else if (['string', 'number'].includes(typeof value)) {
|
|
438
|
+
relationPojo[prop.name] = this.platform.parseDate(value);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
relationPojo[prop.name] = value;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
446
|
+
relationPojo[prop.name] = root[alias];
|
|
447
|
+
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
448
|
+
const item = parseJsonSafe(relationPojo[prop.name]);
|
|
449
|
+
if (Array.isArray(item)) {
|
|
450
|
+
relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (options?.deleteFromRoot) {
|
|
458
|
+
for (const name of prop.fieldNames) {
|
|
459
|
+
delete root[`${relationAlias}__${name}`];
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
351
463
|
async count(entityName, where, options = {}) {
|
|
352
464
|
const meta = this.metadata.get(entityName);
|
|
353
465
|
if (meta.virtual) {
|
|
@@ -356,10 +468,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
356
468
|
options = { populate: [], ...options };
|
|
357
469
|
const populate = options.populate;
|
|
358
470
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
471
|
+
const schema = this.getSchemaName(meta, options);
|
|
359
472
|
const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
|
|
360
473
|
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
361
474
|
if (meta && !Utils.isEmpty(populate)) {
|
|
362
|
-
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options);
|
|
475
|
+
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
363
476
|
}
|
|
364
477
|
qb.__populateWhere = options._populateWhere;
|
|
365
478
|
qb.indexHint(options.indexHint)
|
|
@@ -368,7 +481,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
368
481
|
.groupBy(options.groupBy)
|
|
369
482
|
.having(options.having)
|
|
370
483
|
.populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
|
|
371
|
-
.withSchema(
|
|
484
|
+
.withSchema(schema)
|
|
372
485
|
.where(where);
|
|
373
486
|
if (options.em) {
|
|
374
487
|
await qb.applyJoinedFilters(options.em, options.filters);
|
|
@@ -377,19 +490,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
377
490
|
}
|
|
378
491
|
async nativeInsert(entityName, data, options = {}) {
|
|
379
492
|
options.convertCustomTypes ??= true;
|
|
380
|
-
const meta = this.metadata.
|
|
381
|
-
const collections = this.extractManyToMany(
|
|
382
|
-
const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
|
|
493
|
+
const meta = this.metadata.get(entityName);
|
|
494
|
+
const collections = this.extractManyToMany(meta, data);
|
|
383
495
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
384
496
|
const res = await this.rethrow(qb.insert(data).execute('run', false));
|
|
385
497
|
res.row = res.row || {};
|
|
386
498
|
let pk;
|
|
387
|
-
if (
|
|
388
|
-
|
|
499
|
+
if (meta.primaryKeys.length > 1) {
|
|
500
|
+
// owner has composite pk
|
|
501
|
+
pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
|
|
389
502
|
}
|
|
390
503
|
else {
|
|
391
504
|
/* v8 ignore next */
|
|
392
|
-
res.insertId = data[
|
|
505
|
+
res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
|
|
393
506
|
pk = [res.insertId];
|
|
394
507
|
}
|
|
395
508
|
await this.processManyToMany(meta, pk, collections, false, options);
|
|
@@ -398,25 +511,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
398
511
|
async nativeInsertMany(entityName, data, options = {}, transform) {
|
|
399
512
|
options.processCollections ??= true;
|
|
400
513
|
options.convertCustomTypes ??= true;
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
514
|
+
const entityMeta = this.metadata.get(entityName);
|
|
515
|
+
const meta = entityMeta.inheritanceType === 'tpt' ? entityMeta : entityMeta.root;
|
|
516
|
+
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
517
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
404
518
|
const set = new Set();
|
|
405
519
|
data.forEach(row => Utils.keys(row).forEach(k => set.add(k)));
|
|
406
|
-
const props = [...set].map(name => meta
|
|
407
|
-
|
|
520
|
+
const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] });
|
|
521
|
+
// For STI with conflicting fieldNames, include all alternative columns
|
|
522
|
+
let fields = Utils.flatten(props.map(prop => prop.stiFieldNames ?? prop.fieldNames));
|
|
408
523
|
const duplicates = Utils.findDuplicates(fields);
|
|
409
524
|
const params = [];
|
|
410
525
|
if (duplicates.length) {
|
|
411
526
|
fields = Utils.unique(fields);
|
|
412
527
|
}
|
|
413
|
-
|
|
414
|
-
const tableName = meta ? this.getTableName(meta, options) : this.platform.quoteIdentifier(entityName);
|
|
528
|
+
const tableName = this.getTableName(meta, options);
|
|
415
529
|
let sql = `insert into ${tableName} `;
|
|
416
530
|
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)
|
|
531
|
+
if (this.platform.usesOutputStatement()) {
|
|
532
|
+
const returningProps = this.getTableProps(meta)
|
|
533
|
+
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
|
|
420
534
|
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
|
|
421
535
|
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
422
536
|
sql += returningFields.length > 0 ? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}` : '';
|
|
@@ -457,25 +571,44 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
457
571
|
params.push(value);
|
|
458
572
|
};
|
|
459
573
|
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
|
|
460
|
-
sql += data
|
|
574
|
+
sql += data
|
|
575
|
+
.map(row => {
|
|
461
576
|
const keys = [];
|
|
462
577
|
const usedDups = [];
|
|
463
578
|
props.forEach(prop => {
|
|
579
|
+
// For STI with conflicting fieldNames, use discriminator to determine which field gets value
|
|
580
|
+
if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
|
|
581
|
+
const activeField = prop.stiFieldNameMap[row[meta.discriminatorColumn]];
|
|
582
|
+
for (const field of prop.stiFieldNames) {
|
|
583
|
+
params.push(field === activeField ? row[prop.name] : null);
|
|
584
|
+
keys.push('?');
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
464
588
|
if (prop.fieldNames.length > 1) {
|
|
465
589
|
const newFields = [];
|
|
466
|
-
|
|
590
|
+
let rawParam;
|
|
591
|
+
const target = row[prop.name];
|
|
592
|
+
if (prop.polymorphic && target instanceof PolymorphicRef) {
|
|
593
|
+
rawParam = target.toTuple();
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
|
|
597
|
+
}
|
|
598
|
+
// Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
|
|
599
|
+
const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
|
|
600
|
+
const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
|
|
467
601
|
// TODO(v7): instead of making this conditional here, the entity snapshot should respect `ownColumns`,
|
|
468
602
|
// but that means changing the compiled PK getters, which might be seen as breaking
|
|
469
603
|
const columns = allParam.length > 1 ? prop.fieldNames : prop.ownColumns;
|
|
470
|
-
const
|
|
604
|
+
const param = [];
|
|
471
605
|
columns.forEach((field, idx) => {
|
|
472
606
|
if (usedDups.includes(field)) {
|
|
473
607
|
return;
|
|
474
608
|
}
|
|
475
609
|
newFields.push(field);
|
|
476
|
-
|
|
610
|
+
param.push(allParam[idx]);
|
|
477
611
|
});
|
|
478
|
-
const param = Utils.flatten(newParam);
|
|
479
612
|
newFields.forEach((field, idx) => {
|
|
480
613
|
if (!duplicates.includes(field) || !usedDups.includes(field)) {
|
|
481
614
|
params.push(param[idx]);
|
|
@@ -499,11 +632,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
499
632
|
}
|
|
500
633
|
});
|
|
501
634
|
return '(' + (keys.join(', ') || 'default') + ')';
|
|
502
|
-
})
|
|
635
|
+
})
|
|
636
|
+
.join(', ');
|
|
503
637
|
}
|
|
504
638
|
if (meta && this.platform.usesReturningStatement()) {
|
|
505
|
-
const returningProps = meta
|
|
506
|
-
.filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
|
|
639
|
+
const returningProps = this.getTableProps(meta)
|
|
640
|
+
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
|
|
507
641
|
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
|
|
508
642
|
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
509
643
|
/* v8 ignore next */
|
|
@@ -515,7 +649,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
515
649
|
const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
|
|
516
650
|
let pk;
|
|
517
651
|
/* v8 ignore next */
|
|
518
|
-
if (pks.length > 1) {
|
|
652
|
+
if (pks.length > 1) {
|
|
653
|
+
// owner has composite pk
|
|
519
654
|
pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
|
|
520
655
|
}
|
|
521
656
|
else {
|
|
@@ -531,17 +666,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
531
666
|
}
|
|
532
667
|
async nativeUpdate(entityName, where, data, options = {}) {
|
|
533
668
|
options.convertCustomTypes ??= true;
|
|
534
|
-
const meta = this.metadata.
|
|
535
|
-
const pks = this.getPrimaryKeyFields(
|
|
536
|
-
const collections = this.extractManyToMany(
|
|
669
|
+
const meta = this.metadata.get(entityName);
|
|
670
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
671
|
+
const collections = this.extractManyToMany(meta, data);
|
|
537
672
|
let res = { affectedRows: 0, insertId: 0, row: {} };
|
|
538
673
|
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
539
674
|
/* v8 ignore next */
|
|
540
|
-
where = { [meta
|
|
675
|
+
where = { [meta.primaryKeys[0] ?? pks[0]]: where };
|
|
541
676
|
}
|
|
542
677
|
if (Utils.hasObjectKeys(data)) {
|
|
543
|
-
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext)
|
|
544
|
-
.withSchema(this.getSchemaName(meta, options));
|
|
678
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
545
679
|
if (options.upsert) {
|
|
546
680
|
/* v8 ignore next */
|
|
547
681
|
const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
|
|
@@ -556,14 +690,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
556
690
|
if (options.onConflictAction === 'ignore') {
|
|
557
691
|
qb.ignore();
|
|
558
692
|
}
|
|
693
|
+
if (options.onConflictWhere) {
|
|
694
|
+
qb.where(options.onConflictWhere);
|
|
695
|
+
}
|
|
559
696
|
}
|
|
560
697
|
else {
|
|
561
698
|
qb.update(data).where(where);
|
|
562
699
|
// reload generated columns and version fields
|
|
563
700
|
const returning = [];
|
|
564
|
-
meta
|
|
565
|
-
.filter(prop => (prop.generated && !prop.primary) || prop.version)
|
|
566
|
-
.forEach(prop => returning.push(prop.name));
|
|
701
|
+
meta.props.filter(prop => (prop.generated && !prop.primary) || prop.version).forEach(prop => returning.push(prop.name));
|
|
567
702
|
qb.returning(returning);
|
|
568
703
|
}
|
|
569
704
|
res = await this.rethrow(qb.execute('run', false));
|
|
@@ -578,7 +713,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
578
713
|
options.convertCustomTypes ??= true;
|
|
579
714
|
const meta = this.metadata.get(entityName);
|
|
580
715
|
if (options.upsert) {
|
|
581
|
-
const uniqueFields = options.onConflictFields ??
|
|
716
|
+
const uniqueFields = options.onConflictFields ??
|
|
717
|
+
(Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
|
|
582
718
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
583
719
|
const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
|
|
584
720
|
qb.insert(data)
|
|
@@ -591,9 +727,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
591
727
|
if (options.onConflictAction === 'ignore') {
|
|
592
728
|
qb.ignore();
|
|
593
729
|
}
|
|
730
|
+
if (options.onConflictWhere) {
|
|
731
|
+
qb.where(options.onConflictWhere);
|
|
732
|
+
}
|
|
594
733
|
return this.rethrow(qb.execute('run', false));
|
|
595
734
|
}
|
|
596
|
-
const collections = options.processCollections ? data.map(d => this.extractManyToMany(
|
|
735
|
+
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
597
736
|
const keys = new Set();
|
|
598
737
|
const fields = new Set();
|
|
599
738
|
const returning = new Set();
|
|
@@ -606,10 +745,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
606
745
|
}
|
|
607
746
|
}
|
|
608
747
|
// reload generated columns and version fields
|
|
609
|
-
meta
|
|
610
|
-
|
|
611
|
-
.
|
|
612
|
-
|
|
748
|
+
meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
|
|
749
|
+
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
|
|
750
|
+
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
|
|
751
|
+
.join(' and ');
|
|
613
752
|
const params = [];
|
|
614
753
|
let sql = `update ${this.getTableName(meta, options)} set `;
|
|
615
754
|
const addParams = (prop, value) => {
|
|
@@ -628,6 +767,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
628
767
|
};
|
|
629
768
|
for (const key of keys) {
|
|
630
769
|
const prop = meta.properties[key] ?? meta.root.properties[key];
|
|
770
|
+
if (prop.polymorphic && prop.fieldNames.length > 1) {
|
|
771
|
+
for (let idx = 0; idx < data.length; idx++) {
|
|
772
|
+
const rowValue = data[idx][key];
|
|
773
|
+
if (rowValue instanceof PolymorphicRef) {
|
|
774
|
+
data[idx][key] = rowValue.toTuple();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
631
778
|
prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
|
|
632
779
|
if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
|
|
633
780
|
return;
|
|
@@ -699,12 +846,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
699
846
|
return res;
|
|
700
847
|
}
|
|
701
848
|
async nativeDelete(entityName, where, options = {}) {
|
|
702
|
-
const meta = this.metadata.
|
|
703
|
-
const pks = this.getPrimaryKeyFields(
|
|
849
|
+
const meta = this.metadata.get(entityName);
|
|
850
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
704
851
|
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
705
852
|
where = { [pks[0]]: where };
|
|
706
853
|
}
|
|
707
|
-
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
|
|
854
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
|
|
855
|
+
.delete(where)
|
|
856
|
+
.withSchema(this.getSchemaName(meta, options));
|
|
708
857
|
return this.rethrow(qb.execute('run', false));
|
|
709
858
|
}
|
|
710
859
|
/**
|
|
@@ -753,45 +902,42 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
753
902
|
}
|
|
754
903
|
if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
|
|
755
904
|
const cols = coll.property.referencedColumnNames;
|
|
756
|
-
const qb = this.createQueryBuilder(coll.property.
|
|
757
|
-
.withSchema(this.getSchemaName(meta, options));
|
|
905
|
+
const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
|
|
758
906
|
if (coll.getSnapshot() === undefined) {
|
|
759
907
|
if (coll.property.orphanRemoval) {
|
|
760
|
-
const query = qb.delete({ [coll.property.mappedBy]: pks })
|
|
761
|
-
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
908
|
+
const query = qb.delete({ [coll.property.mappedBy]: pks }).andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
762
909
|
await this.rethrow(query.execute());
|
|
763
910
|
continue;
|
|
764
911
|
}
|
|
765
|
-
const query = qb
|
|
912
|
+
const query = qb
|
|
913
|
+
.update({ [coll.property.mappedBy]: null })
|
|
766
914
|
.where({ [coll.property.mappedBy]: pks })
|
|
767
915
|
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
768
916
|
await this.rethrow(query.execute());
|
|
769
917
|
continue;
|
|
770
918
|
}
|
|
771
919
|
/* v8 ignore next */
|
|
772
|
-
const query = qb.update({ [coll.property.mappedBy]: pks })
|
|
773
|
-
.where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
|
|
920
|
+
const query = qb.update({ [coll.property.mappedBy]: pks }).where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
|
|
774
921
|
await this.rethrow(query.execute());
|
|
775
922
|
continue;
|
|
776
923
|
}
|
|
777
|
-
/* v8 ignore next */
|
|
778
924
|
const pivotMeta = this.metadata.find(coll.property.pivotEntity);
|
|
779
925
|
let schema = pivotMeta.schema;
|
|
780
926
|
if (schema === '*') {
|
|
781
927
|
if (coll.property.owner) {
|
|
782
|
-
schema = wrapped.getSchema() === '*' ? options?.schema ?? this.config.get('schema') : wrapped.getSchema();
|
|
928
|
+
schema = wrapped.getSchema() === '*' ? (options?.schema ?? this.config.get('schema')) : wrapped.getSchema();
|
|
783
929
|
}
|
|
784
930
|
else {
|
|
785
931
|
const targetMeta = coll.property.targetMeta;
|
|
786
932
|
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;
|
|
933
|
+
schema = targetMeta.schema === '*' ? (options?.schema ?? targetSchema ?? this.config.get('schema')) : targetMeta.schema;
|
|
788
934
|
}
|
|
789
935
|
}
|
|
790
936
|
else if (schema == null) {
|
|
791
937
|
schema = this.config.get('schema');
|
|
792
938
|
}
|
|
793
939
|
const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
|
|
794
|
-
const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext);
|
|
940
|
+
const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
|
|
795
941
|
persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
|
|
796
942
|
}
|
|
797
943
|
for (const persister of Utils.values(groups)) {
|
|
@@ -799,13 +945,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
799
945
|
}
|
|
800
946
|
}
|
|
801
947
|
async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
948
|
+
/* v8 ignore next */
|
|
802
949
|
if (owners.length === 0) {
|
|
803
950
|
return {};
|
|
804
951
|
}
|
|
805
|
-
const pivotMeta = this.metadata.
|
|
952
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
953
|
+
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
954
|
+
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
955
|
+
}
|
|
806
956
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
807
957
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
808
|
-
const ownerMeta =
|
|
958
|
+
const ownerMeta = pivotProp2.targetMeta;
|
|
809
959
|
const cond = {
|
|
810
960
|
[pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
|
|
811
961
|
};
|
|
@@ -817,34 +967,143 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
817
967
|
const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
|
|
818
968
|
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
|
|
819
969
|
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, {
|
|
970
|
+
const fields = pivotJoin ? [pivotProp1.name, pivotProp2.name] : [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
971
|
+
const res = await this.find(pivotMeta.class, where, {
|
|
824
972
|
ctx,
|
|
825
973
|
...options,
|
|
826
974
|
fields,
|
|
827
975
|
exclude: childExclude,
|
|
828
976
|
orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
|
|
977
|
+
populate: [
|
|
978
|
+
{
|
|
979
|
+
field: populateField,
|
|
980
|
+
strategy: LoadStrategy.JOINED,
|
|
981
|
+
joinType: JoinType.innerJoin,
|
|
982
|
+
children: populate,
|
|
983
|
+
dataOnly: pivotProp1.mapToPk && !pivotJoin,
|
|
984
|
+
},
|
|
985
|
+
],
|
|
986
|
+
populateWhere: undefined,
|
|
987
|
+
// @ts-ignore
|
|
988
|
+
_populateWhere: 'infer',
|
|
989
|
+
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
990
|
+
});
|
|
991
|
+
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Load from a polymorphic M:N pivot table.
|
|
995
|
+
*/
|
|
996
|
+
async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
997
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
998
|
+
// Find the M:1 relation on the pivot pointing to the target entity.
|
|
999
|
+
// We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
|
|
1000
|
+
const inverseProp = pivotMeta.relations.find(r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta);
|
|
1001
|
+
if (inverseProp) {
|
|
1002
|
+
return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
|
|
1003
|
+
}
|
|
1004
|
+
return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Load from owner side of polymorphic M:N (e.g., Post -> Tags)
|
|
1008
|
+
*/
|
|
1009
|
+
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
|
|
1010
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1011
|
+
const targetMeta = prop.targetMeta;
|
|
1012
|
+
// Build condition: discriminator = 'post' AND {discriminator} IN (...)
|
|
1013
|
+
const cond = {
|
|
1014
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1015
|
+
[prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1016
|
+
};
|
|
1017
|
+
if (!Utils.isEmpty(where)) {
|
|
1018
|
+
cond[inverseProp.name] = { ...where };
|
|
1019
|
+
}
|
|
1020
|
+
const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
|
|
1021
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1022
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
|
|
1023
|
+
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
|
|
1024
|
+
const fields = pivotJoin
|
|
1025
|
+
? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
|
|
1026
|
+
: [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
|
|
1027
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
1028
|
+
ctx,
|
|
1029
|
+
...options,
|
|
1030
|
+
fields,
|
|
1031
|
+
exclude: childExclude,
|
|
1032
|
+
orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
|
|
1033
|
+
populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate, dataOnly: inverseProp.mapToPk && !pivotJoin }],
|
|
1034
|
+
populateWhere: undefined,
|
|
1035
|
+
// @ts-ignore
|
|
1036
|
+
_populateWhere: 'infer',
|
|
1037
|
+
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
|
|
1038
|
+
});
|
|
1039
|
+
return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
1043
|
+
* Uses single query with join via virtual relation on pivot.
|
|
1044
|
+
*/
|
|
1045
|
+
async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
|
|
1046
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1047
|
+
const targetMeta = prop.targetMeta;
|
|
1048
|
+
// Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
|
|
1049
|
+
// Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
|
|
1050
|
+
const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
|
|
1051
|
+
// Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
|
|
1052
|
+
const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
1053
|
+
const ownerProp = pivotMeta.properties[ownerRelationName];
|
|
1054
|
+
// Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
|
|
1055
|
+
const cond = {
|
|
1056
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1057
|
+
[tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1058
|
+
};
|
|
1059
|
+
if (!Utils.isEmpty(where)) {
|
|
1060
|
+
cond[ownerRelationName] = { ...where };
|
|
1061
|
+
}
|
|
1062
|
+
const populateField = ownerRelationName;
|
|
1063
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1064
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1065
|
+
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1066
|
+
const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
|
|
1067
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
1068
|
+
ctx,
|
|
1069
|
+
...options,
|
|
1070
|
+
fields,
|
|
1071
|
+
exclude: childExclude,
|
|
1072
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
829
1073
|
populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
|
|
830
1074
|
populateWhere: undefined,
|
|
831
1075
|
// @ts-ignore
|
|
832
1076
|
_populateWhere: 'infer',
|
|
833
|
-
populateFilter:
|
|
1077
|
+
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
|
|
834
1078
|
});
|
|
1079
|
+
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1083
|
+
*/
|
|
1084
|
+
buildPivotResultMap(owners, results, keyProp, valueProp) {
|
|
835
1085
|
const map = {};
|
|
836
1086
|
for (const owner of owners) {
|
|
837
1087
|
const key = Utils.getPrimaryKeyHash(owner);
|
|
838
1088
|
map[key] = [];
|
|
839
1089
|
}
|
|
840
|
-
for (const item of
|
|
841
|
-
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[
|
|
842
|
-
|
|
1090
|
+
for (const item of results) {
|
|
1091
|
+
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
|
|
1092
|
+
const entity = item[valueProp];
|
|
1093
|
+
if (map[key]) {
|
|
1094
|
+
map[key].push(entity);
|
|
1095
|
+
}
|
|
843
1096
|
}
|
|
844
1097
|
return map;
|
|
845
1098
|
}
|
|
1099
|
+
wrapPopulateFilter(options, propName) {
|
|
1100
|
+
if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
|
|
1101
|
+
return { [propName]: options?.populateFilter };
|
|
1102
|
+
}
|
|
1103
|
+
return undefined;
|
|
1104
|
+
}
|
|
846
1105
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
847
|
-
if (!Utils.isEmpty(orderBy)) {
|
|
1106
|
+
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
848
1107
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
849
1108
|
}
|
|
850
1109
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
|
|
@@ -852,7 +1111,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
852
1111
|
.filter(o => o[prop.name])
|
|
853
1112
|
.map(o => ({ [pivotProp.name]: o[prop.name] }));
|
|
854
1113
|
}
|
|
855
|
-
if (!Utils.isEmpty(prop.orderBy)) {
|
|
1114
|
+
if (!Utils.isEmpty(prop.orderBy) || RawQueryFragment.hasObjectFragments(prop.orderBy)) {
|
|
856
1115
|
return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
857
1116
|
}
|
|
858
1117
|
if (prop.fixedOrder) {
|
|
@@ -865,8 +1124,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
865
1124
|
}
|
|
866
1125
|
async *stream(entityName, where, options) {
|
|
867
1126
|
options = { populate: [], orderBy: [], ...options };
|
|
868
|
-
const meta = this.metadata.
|
|
869
|
-
if (meta
|
|
1127
|
+
const meta = this.metadata.get(entityName);
|
|
1128
|
+
if (meta.virtual) {
|
|
870
1129
|
yield* this.streamFromVirtual(entityName, where, options);
|
|
871
1130
|
return;
|
|
872
1131
|
}
|
|
@@ -989,21 +1248,46 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
989
1248
|
const populate = options.populate ?? [];
|
|
990
1249
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
991
1250
|
const populateWhereAll = options?._populateWhere === 'all' || Utils.isEmpty(options?._populateWhere);
|
|
1251
|
+
// Ensure TPT joins are applied early so that _tptAlias is available for join resolution
|
|
1252
|
+
// This is needed when populating relations that are inherited from TPT parent entities
|
|
1253
|
+
if (!options.parentJoinPath) {
|
|
1254
|
+
qb.ensureTPTJoins();
|
|
1255
|
+
}
|
|
992
1256
|
// root entity is already handled, skip that
|
|
993
1257
|
if (options.parentJoinPath) {
|
|
994
1258
|
// alias all fields in the primary table
|
|
995
1259
|
meta.props
|
|
996
1260
|
.filter(prop => this.shouldHaveColumn(meta, prop, populate, options.explicitFields, options.exclude))
|
|
997
|
-
.forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, options.explicitFields)));
|
|
1261
|
+
.forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, meta, options.schema, options.explicitFields)));
|
|
998
1262
|
}
|
|
999
1263
|
for (const hint of joinedProps) {
|
|
1000
1264
|
const [propName, ref] = hint.field.split(':', 2);
|
|
1001
1265
|
const prop = meta.properties[propName];
|
|
1266
|
+
// Polymorphic to-one: create a LEFT JOIN per target type
|
|
1267
|
+
// Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
|
|
1268
|
+
if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
1269
|
+
const basePath = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
1270
|
+
const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
|
|
1271
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
1272
|
+
const tableAlias = qb.getNextAlias(targetMeta.className);
|
|
1273
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
1274
|
+
const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
|
|
1275
|
+
qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
|
|
1276
|
+
// Select fields from each target table
|
|
1277
|
+
fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
|
|
1278
|
+
...options,
|
|
1279
|
+
populate: hint.children,
|
|
1280
|
+
parentTableAlias: tableAlias,
|
|
1281
|
+
parentJoinPath: targetPath,
|
|
1282
|
+
}));
|
|
1283
|
+
}
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1002
1286
|
// ignore ref joins of known FKs unless it's a filter hint
|
|
1003
1287
|
if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
|
|
1004
1288
|
continue;
|
|
1005
1289
|
}
|
|
1006
|
-
const meta2 =
|
|
1290
|
+
const meta2 = prop.targetMeta;
|
|
1007
1291
|
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
1008
1292
|
const tableAlias = qb.getNextAlias(prop.name);
|
|
1009
1293
|
const field = `${options.parentTableAlias}.${prop.name}`;
|
|
@@ -1019,7 +1303,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1019
1303
|
: (hint.filter && !prop.nullable) || mandatoryToOneProperty
|
|
1020
1304
|
? JoinType.innerJoin
|
|
1021
1305
|
: JoinType.leftJoin;
|
|
1022
|
-
|
|
1306
|
+
const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
|
|
1307
|
+
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1308
|
+
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1309
|
+
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
1310
|
+
// Use the registry metadata to ensure allTPTDescendants is available
|
|
1311
|
+
const tptMeta = this.metadata.get(meta2.class);
|
|
1312
|
+
this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
|
|
1313
|
+
}
|
|
1023
1314
|
if (pivotRefJoin) {
|
|
1024
1315
|
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
1316
|
}
|
|
@@ -1040,7 +1331,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1040
1331
|
}
|
|
1041
1332
|
});
|
|
1042
1333
|
const childExclude = options.exclude ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
|
|
1043
|
-
if (!ref && !prop.mapToPk) {
|
|
1334
|
+
if (!ref && (!prop.mapToPk || hint.dataOnly)) {
|
|
1044
1335
|
fields.push(...this.getFieldsForJoinedLoad(qb, meta2, {
|
|
1045
1336
|
...options,
|
|
1046
1337
|
explicitFields: childExplicitFields.length === 0 ? undefined : childExplicitFields,
|
|
@@ -1050,23 +1341,127 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1050
1341
|
parentJoinPath: path,
|
|
1051
1342
|
}));
|
|
1052
1343
|
}
|
|
1053
|
-
else if (hint.filter || prop.mapToPk || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
|
|
1344
|
+
else if (hint.filter || (prop.mapToPk && !hint.dataOnly) || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
|
|
1054
1345
|
fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
|
|
1055
1346
|
}
|
|
1056
1347
|
}
|
|
1057
1348
|
return fields;
|
|
1058
1349
|
}
|
|
1059
1350
|
/**
|
|
1351
|
+
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
1060
1352
|
* @internal
|
|
1061
1353
|
*/
|
|
1062
|
-
|
|
1354
|
+
addTPTPolymorphicJoinsForRelation(qb, meta, baseAlias, fields) {
|
|
1355
|
+
// allTPTDescendants is pre-computed during discovery, sorted by depth (deepest first)
|
|
1356
|
+
const descendants = meta.allTPTDescendants;
|
|
1357
|
+
const childAliases = {};
|
|
1358
|
+
// LEFT JOIN each descendant table
|
|
1359
|
+
for (const childMeta of descendants) {
|
|
1360
|
+
const childAlias = qb.getNextAlias(childMeta.className);
|
|
1361
|
+
qb.createAlias(childMeta.class, childAlias);
|
|
1362
|
+
childAliases[childMeta.className] = childAlias;
|
|
1363
|
+
qb.addPropertyJoin(childMeta.tptInverseProp, baseAlias, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
|
|
1364
|
+
// Add fields from this child (only ownProps, skip PKs)
|
|
1365
|
+
for (const prop of childMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
|
|
1366
|
+
for (const fieldName of prop.fieldNames) {
|
|
1367
|
+
const field = `${childAlias}.${fieldName}`;
|
|
1368
|
+
const fieldAlias = `${childAlias}__${fieldName}`;
|
|
1369
|
+
fields.push(raw(`${this.platform.quoteIdentifier(field)} as ${this.platform.quoteIdentifier(fieldAlias)}`));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
// Add computed discriminator (descendants already sorted by depth)
|
|
1374
|
+
if (meta.root.tptDiscriminatorColumn) {
|
|
1375
|
+
fields.push(this.buildTPTDiscriminatorExpression(meta, descendants, childAliases, baseAlias));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Find the alias for a TPT child table in the query builder.
|
|
1380
|
+
* @internal
|
|
1381
|
+
*/
|
|
1382
|
+
findTPTChildAlias(qb, childMeta) {
|
|
1383
|
+
const joins = qb._joins;
|
|
1384
|
+
for (const key of Object.keys(joins)) {
|
|
1385
|
+
if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
|
|
1386
|
+
return joins[key].alias;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return undefined;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Builds a CASE WHEN expression for TPT discriminator.
|
|
1393
|
+
* Determines concrete entity type based on which child table has a non-null PK.
|
|
1394
|
+
* @internal
|
|
1395
|
+
*/
|
|
1396
|
+
buildTPTDiscriminatorExpression(meta, descendants, aliasMap, baseAlias) {
|
|
1397
|
+
const cases = descendants.map(child => {
|
|
1398
|
+
const childAlias = aliasMap[child.className];
|
|
1399
|
+
const pkFieldName = child.properties[child.primaryKeys[0]].fieldNames[0];
|
|
1400
|
+
return `when ${this.platform.quoteIdentifier(`${childAlias}.${pkFieldName}`)} is not null then '${child.discriminatorValue}'`;
|
|
1401
|
+
});
|
|
1402
|
+
const defaultValue = meta.abstract ? 'null' : `'${meta.discriminatorValue}'`;
|
|
1403
|
+
const caseExpr = `case ${cases.join(' ')} else ${defaultValue} end`;
|
|
1404
|
+
const aliased = this.platform.quoteIdentifier(`${baseAlias}__${meta.root.tptDiscriminatorColumn}`);
|
|
1405
|
+
return raw(`${caseExpr} as ${aliased}`);
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Maps TPT child-specific fields during hydration.
|
|
1409
|
+
* When a relation points to a TPT base class, the actual entity might be a child class.
|
|
1410
|
+
* This method reads the discriminator to determine the concrete type and maps child-specific fields.
|
|
1411
|
+
* @internal
|
|
1412
|
+
*/
|
|
1413
|
+
mapTPTChildFields(relationPojo, meta, relationAlias, qb, root) {
|
|
1414
|
+
// Check if this is a TPT base with polymorphic children
|
|
1415
|
+
if (meta.inheritanceType !== 'tpt' || !meta.root.tptDiscriminatorColumn) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
// Read the discriminator value
|
|
1419
|
+
const discriminatorAlias = `${relationAlias}__${meta.root.tptDiscriminatorColumn}`;
|
|
1420
|
+
const discriminatorValue = root[discriminatorAlias];
|
|
1421
|
+
if (!discriminatorValue) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
// Set the discriminator in the pojo for EntityFactory
|
|
1425
|
+
relationPojo[meta.root.tptDiscriminatorColumn] = discriminatorValue;
|
|
1426
|
+
// Find the concrete metadata from discriminator map
|
|
1427
|
+
const concreteClass = meta.root.discriminatorMap?.[discriminatorValue];
|
|
1428
|
+
/* v8 ignore next 3 - defensive check for invalid discriminator values */
|
|
1429
|
+
if (!concreteClass) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const concreteMeta = this.metadata.get(concreteClass);
|
|
1433
|
+
if (concreteMeta === meta) {
|
|
1434
|
+
// Already the concrete type, no child fields to map
|
|
1435
|
+
delete root[discriminatorAlias];
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
// Traverse up from concrete type and map fields from each level's table
|
|
1439
|
+
const tz = this.platform.getTimezone();
|
|
1440
|
+
let currentMeta = concreteMeta;
|
|
1441
|
+
while (currentMeta && currentMeta !== meta) {
|
|
1442
|
+
const childAlias = this.findTPTChildAlias(qb, currentMeta);
|
|
1443
|
+
if (childAlias) {
|
|
1444
|
+
// Map fields using same filtering as joined loading, plus skip PKs
|
|
1445
|
+
for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
|
|
1446
|
+
this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, { deleteFromRoot: true });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
currentMeta = currentMeta.tptParent;
|
|
1450
|
+
}
|
|
1451
|
+
// Clean up the discriminator alias
|
|
1452
|
+
delete root[discriminatorAlias];
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* @internal
|
|
1456
|
+
*/
|
|
1457
|
+
mapPropToFieldNames(qb, prop, tableAlias, meta, schema, explicitFields) {
|
|
1063
1458
|
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
1064
1459
|
return Object.entries(prop.embeddedProps).flatMap(([name, childProp]) => {
|
|
1065
1460
|
const childFields = explicitFields ? Utils.extractChildElements(explicitFields, prop.name) : [];
|
|
1066
1461
|
if (!this.shouldHaveColumn(prop.targetMeta, { ...childProp, name }, [], childFields.length > 0 ? childFields : undefined)) {
|
|
1067
1462
|
return [];
|
|
1068
1463
|
}
|
|
1069
|
-
return this.mapPropToFieldNames(qb, childProp, tableAlias, childFields);
|
|
1464
|
+
return this.mapPropToFieldNames(qb, childProp, tableAlias, meta, schema, childFields);
|
|
1070
1465
|
});
|
|
1071
1466
|
}
|
|
1072
1467
|
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
|
|
@@ -1085,8 +1480,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1085
1480
|
return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
|
|
1086
1481
|
}
|
|
1087
1482
|
if (prop.formula) {
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1483
|
+
const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
|
|
1484
|
+
const table = this.createFormulaTable(quotedAlias, meta, schema);
|
|
1485
|
+
const columns = meta.createColumnMappingObject(tableAlias);
|
|
1486
|
+
return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
|
|
1090
1487
|
}
|
|
1091
1488
|
return prop.fieldNames.map(fieldName => {
|
|
1092
1489
|
return `${tableAlias}.${fieldName} as ${tableAlias}__${fieldName}`;
|
|
@@ -1114,26 +1511,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1114
1511
|
}
|
|
1115
1512
|
return 'write';
|
|
1116
1513
|
}
|
|
1117
|
-
extractManyToMany(
|
|
1118
|
-
if (!this.metadata.has(entityName)) {
|
|
1119
|
-
return {};
|
|
1120
|
-
}
|
|
1514
|
+
extractManyToMany(meta, data) {
|
|
1121
1515
|
const ret = {};
|
|
1122
|
-
|
|
1516
|
+
for (const prop of meta.relations) {
|
|
1123
1517
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
|
|
1124
1518
|
ret[prop.name] = data[prop.name].map((item) => Utils.asArray(item));
|
|
1125
1519
|
delete data[prop.name];
|
|
1126
1520
|
}
|
|
1127
|
-
}
|
|
1521
|
+
}
|
|
1128
1522
|
return ret;
|
|
1129
1523
|
}
|
|
1130
1524
|
async processManyToMany(meta, pks, collections, clear, options) {
|
|
1131
|
-
if (!meta) {
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
1525
|
for (const prop of meta.relations) {
|
|
1135
1526
|
if (collections[prop.name]) {
|
|
1136
|
-
const pivotMeta = this.metadata.
|
|
1527
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1137
1528
|
const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
|
|
1138
1529
|
persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
|
|
1139
1530
|
await this.rethrow(persister.execute());
|
|
@@ -1142,9 +1533,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1142
1533
|
}
|
|
1143
1534
|
async lockPessimistic(entity, options) {
|
|
1144
1535
|
const meta = helper(entity).__meta;
|
|
1145
|
-
const qb = this.createQueryBuilder(
|
|
1536
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
|
|
1146
1537
|
const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
|
|
1147
|
-
qb.select(raw('1'))
|
|
1538
|
+
qb.select(raw('1'))
|
|
1539
|
+
.where(cond)
|
|
1540
|
+
.setLockMode(options.lockMode, options.lockTableAliases);
|
|
1148
1541
|
await this.rethrow(qb.execute());
|
|
1149
1542
|
}
|
|
1150
1543
|
buildPopulateWhere(meta, joinedProps, options) {
|
|
@@ -1152,21 +1545,24 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1152
1545
|
for (const hint of joinedProps) {
|
|
1153
1546
|
const [propName] = hint.field.split(':', 2);
|
|
1154
1547
|
const prop = meta.properties[propName];
|
|
1155
|
-
if (!Utils.isEmpty(prop.where)) {
|
|
1548
|
+
if (!Utils.isEmpty(prop.where) || RawQueryFragment.hasObjectFragments(prop.where)) {
|
|
1156
1549
|
where[prop.name] = Utils.copy(prop.where);
|
|
1157
1550
|
}
|
|
1158
1551
|
if (hint.children) {
|
|
1159
|
-
const
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
1552
|
+
const targetMeta = prop.targetMeta;
|
|
1553
|
+
if (targetMeta) {
|
|
1554
|
+
const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
|
|
1555
|
+
if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
|
|
1556
|
+
where[prop.name] ??= {};
|
|
1557
|
+
Object.assign(where[prop.name], inner);
|
|
1558
|
+
}
|
|
1163
1559
|
}
|
|
1164
1560
|
}
|
|
1165
1561
|
}
|
|
1166
|
-
if (Utils.isEmpty(options.populateWhere)) {
|
|
1562
|
+
if (Utils.isEmpty(options.populateWhere) && !RawQueryFragment.hasObjectFragments(options.populateWhere)) {
|
|
1167
1563
|
return where;
|
|
1168
1564
|
}
|
|
1169
|
-
if (Utils.isEmpty(where)) {
|
|
1565
|
+
if (Utils.isEmpty(where) && !RawQueryFragment.hasObjectFragments(where)) {
|
|
1170
1566
|
return options.populateWhere;
|
|
1171
1567
|
}
|
|
1172
1568
|
/* v8 ignore next */
|
|
@@ -1178,31 +1574,32 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1178
1574
|
// as `options.populateWhere` will be always recomputed to respect filters
|
|
1179
1575
|
const populateWhereAll = options._populateWhere !== 'infer' && !Utils.isEmpty(options._populateWhere);
|
|
1180
1576
|
const path = (populateWhereAll ? '[populate]' : '') + meta.className;
|
|
1577
|
+
const optionsOrderBy = Utils.asArray(options.orderBy);
|
|
1181
1578
|
const populateOrderBy = this.buildPopulateOrderBy(qb, meta, Utils.asArray(options.populateOrderBy ?? options.orderBy), path, !!options.populateOrderBy);
|
|
1182
1579
|
const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
|
|
1183
|
-
return [...
|
|
1580
|
+
return [...optionsOrderBy, ...populateOrderBy, ...joinedPropsOrderBy];
|
|
1184
1581
|
}
|
|
1185
1582
|
buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, explicit, parentAlias = qb.alias) {
|
|
1186
1583
|
const orderBy = [];
|
|
1187
1584
|
for (let i = 0; i < populateOrderBy.length; i++) {
|
|
1188
1585
|
const orderHint = populateOrderBy[i];
|
|
1189
|
-
for (const
|
|
1190
|
-
const
|
|
1191
|
-
if (
|
|
1192
|
-
const sql =
|
|
1193
|
-
const
|
|
1194
|
-
orderBy.push({ [
|
|
1586
|
+
for (const field of Utils.getObjectQueryKeys(orderHint)) {
|
|
1587
|
+
const childOrder = orderHint[field];
|
|
1588
|
+
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
1589
|
+
const { sql, params } = RawQueryFragment.getKnownFragment(field);
|
|
1590
|
+
const key = raw(sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias), params);
|
|
1591
|
+
orderBy.push({ [key]: childOrder });
|
|
1195
1592
|
continue;
|
|
1196
1593
|
}
|
|
1197
|
-
const prop = meta.properties[
|
|
1594
|
+
const prop = meta.properties[field];
|
|
1198
1595
|
if (!prop) {
|
|
1199
|
-
throw new Error(`Trying to order by not existing property ${meta.className}.${
|
|
1596
|
+
throw new Error(`Trying to order by not existing property ${meta.className}.${field}`);
|
|
1200
1597
|
}
|
|
1201
1598
|
let path = parentPath;
|
|
1202
|
-
const meta2 =
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
path += `.${
|
|
1599
|
+
const meta2 = prop.targetMeta;
|
|
1600
|
+
if (prop.kind !== ReferenceKind.SCALAR &&
|
|
1601
|
+
(![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
|
|
1602
|
+
path += `.${field}`;
|
|
1206
1603
|
}
|
|
1207
1604
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
|
|
1208
1605
|
path += '[pivot]';
|
|
@@ -1228,9 +1625,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1228
1625
|
}
|
|
1229
1626
|
continue;
|
|
1230
1627
|
}
|
|
1231
|
-
const order = typeof childOrder === 'object' ? childOrder[
|
|
1628
|
+
const order = typeof childOrder === 'object' ? childOrder[field] : childOrder;
|
|
1232
1629
|
if (order) {
|
|
1233
|
-
orderBy.push({ [`${propAlias}.${
|
|
1630
|
+
orderBy.push({ [`${propAlias}.${field}`]: order });
|
|
1234
1631
|
}
|
|
1235
1632
|
}
|
|
1236
1633
|
}
|
|
@@ -1249,27 +1646,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1249
1646
|
}
|
|
1250
1647
|
const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
|
|
1251
1648
|
const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
|
|
1252
|
-
const meta2 = this.metadata.find(prop.type);
|
|
1253
1649
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
|
|
1254
1650
|
const alias = ref ? propAlias : join.ownerAlias;
|
|
1255
1651
|
orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
|
|
1256
1652
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
orderBy.push({ [`${propAlias}.${field}`]: item[field] });
|
|
1653
|
+
const effectiveOrderBy = QueryHelper.mergeOrderBy(propOrderBy, prop.targetMeta?.orderBy);
|
|
1654
|
+
for (const item of effectiveOrderBy) {
|
|
1655
|
+
for (const field of Utils.getObjectQueryKeys(item)) {
|
|
1656
|
+
const order = item[field];
|
|
1657
|
+
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
1658
|
+
const { sql, params } = RawQueryFragment.getKnownFragment(field);
|
|
1659
|
+
const sql2 = propAlias ? sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : sql;
|
|
1660
|
+
const key = raw(sql2, params);
|
|
1661
|
+
orderBy.push({ [key]: order });
|
|
1662
|
+
continue;
|
|
1268
1663
|
}
|
|
1664
|
+
orderBy.push({ [`${propAlias}.${field}`]: order });
|
|
1269
1665
|
}
|
|
1270
1666
|
}
|
|
1271
1667
|
if (hint.children) {
|
|
1272
|
-
const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb,
|
|
1668
|
+
const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path);
|
|
1273
1669
|
orderBy.push(...buildJoinedPropsOrderBy);
|
|
1274
1670
|
}
|
|
1275
1671
|
}
|
|
@@ -1313,7 +1709,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1313
1709
|
}
|
|
1314
1710
|
ret.push(prop.name);
|
|
1315
1711
|
}
|
|
1316
|
-
buildFields(meta, populate, joinedProps, qb, alias, options) {
|
|
1712
|
+
buildFields(meta, populate, joinedProps, qb, alias, options, schema) {
|
|
1317
1713
|
const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => this.isPopulated(meta, prop, p)));
|
|
1318
1714
|
const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
|
|
1319
1715
|
const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
|
|
@@ -1332,7 +1728,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1332
1728
|
const prop = QueryHelper.findProperty(rootPropName, {
|
|
1333
1729
|
metadata: this.metadata,
|
|
1334
1730
|
platform: this.platform,
|
|
1335
|
-
entityName: meta.
|
|
1731
|
+
entityName: meta.class,
|
|
1336
1732
|
where: {},
|
|
1337
1733
|
aliasMap: qb.getAliasMap(),
|
|
1338
1734
|
});
|
|
@@ -1341,7 +1737,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1341
1737
|
if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
|
|
1342
1738
|
ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
|
|
1343
1739
|
}
|
|
1344
|
-
if (meta.root.
|
|
1740
|
+
if (meta.root.inheritanceType === 'sti' && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
|
|
1345
1741
|
ret.push(meta.root.discriminatorColumn);
|
|
1346
1742
|
}
|
|
1347
1743
|
}
|
|
@@ -1358,14 +1754,18 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1358
1754
|
ret.push('*');
|
|
1359
1755
|
}
|
|
1360
1756
|
if (ret.length > 0 && !hasExplicitFields && addFormulas) {
|
|
1757
|
+
// Create formula column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
|
|
1758
|
+
const quotedAlias = this.platform.quoteIdentifier(alias);
|
|
1759
|
+
const columns = meta.createColumnMappingObject(alias);
|
|
1760
|
+
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
1361
1761
|
for (const prop of meta.props) {
|
|
1362
1762
|
if (lazyProps.includes(prop)) {
|
|
1363
1763
|
continue;
|
|
1364
1764
|
}
|
|
1365
1765
|
if (prop.formula) {
|
|
1366
|
-
const a = this.platform.quoteIdentifier(alias);
|
|
1367
1766
|
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
1368
|
-
|
|
1767
|
+
const table = this.createFormulaTable(quotedAlias.toString(), meta, effectiveSchema);
|
|
1768
|
+
ret.push(raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`));
|
|
1369
1769
|
}
|
|
1370
1770
|
if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
|
|
1371
1771
|
ret.push(prop.name);
|