@mikro-orm/sql 7.0.0-dev.227 → 7.0.0-dev.229
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/AbstractSqlDriver.d.ts +36 -1
- package/AbstractSqlDriver.js +188 -28
- package/package.json +2 -2
- package/query/QueryBuilder.d.ts +33 -2
- package/query/QueryBuilder.js +125 -9
- package/query/QueryBuilderHelper.d.ts +7 -1
- package/query/QueryBuilderHelper.js +54 -14
- package/schema/DatabaseSchema.d.ts +5 -0
- package/schema/DatabaseSchema.js +55 -2
- package/schema/DatabaseTable.js +6 -8
- package/tsconfig.build.tsbuildinfo +1 -1
package/query/QueryBuilder.js
CHANGED
|
@@ -75,9 +75,12 @@ export class QueryBuilder {
|
|
|
75
75
|
subQueries = {};
|
|
76
76
|
_mainAlias;
|
|
77
77
|
_aliases = {};
|
|
78
|
+
_tptAlias = {}; // maps entity className to alias for TPT parent tables
|
|
78
79
|
_helper;
|
|
79
80
|
_query;
|
|
80
81
|
platform;
|
|
82
|
+
tptJoinsApplied = false;
|
|
83
|
+
autoJoinedPaths = [];
|
|
81
84
|
/**
|
|
82
85
|
* @internal
|
|
83
86
|
*/
|
|
@@ -337,7 +340,6 @@ export class QueryBuilder {
|
|
|
337
340
|
const cond = await this.em.applyFilters(this.mainAlias.entityName, {}, filterOptions, 'read');
|
|
338
341
|
this.andWhere(cond);
|
|
339
342
|
}
|
|
340
|
-
autoJoinedPaths = [];
|
|
341
343
|
/**
|
|
342
344
|
* @internal
|
|
343
345
|
*/
|
|
@@ -1062,6 +1064,21 @@ export class QueryBuilder {
|
|
|
1062
1064
|
/* v8 ignore next */
|
|
1063
1065
|
return res;
|
|
1064
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Adds a join from a property object. Used internally for TPT joins where the property
|
|
1069
|
+
* is synthetic (not in entity.properties) but defined on metadata (e.g., tptParentProp).
|
|
1070
|
+
* The caller must create the alias first via createAlias().
|
|
1071
|
+
* @internal
|
|
1072
|
+
*/
|
|
1073
|
+
addPropertyJoin(prop, ownerAlias, alias, type, path, schema) {
|
|
1074
|
+
schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta);
|
|
1075
|
+
const key = `[tpt]${ownerAlias}#${alias}`;
|
|
1076
|
+
this._joins[key] = prop.kind === ReferenceKind.MANY_TO_ONE
|
|
1077
|
+
? this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, {}, schema)
|
|
1078
|
+
: this.helper.joinOneToReference(prop, ownerAlias, alias, type, {}, schema);
|
|
1079
|
+
this._joins[key].path = path;
|
|
1080
|
+
return key;
|
|
1081
|
+
}
|
|
1065
1082
|
joinReference(field, alias, cond, type, path, schema, subquery) {
|
|
1066
1083
|
this.ensureNotFinalized();
|
|
1067
1084
|
if (typeof field === 'object') {
|
|
@@ -1103,6 +1120,11 @@ export class QueryBuilder {
|
|
|
1103
1120
|
if (!prop) {
|
|
1104
1121
|
throw new Error(`Trying to join ${q(field)}, but ${q(fromField)} is not a defined relation on ${meta.className}.`);
|
|
1105
1122
|
}
|
|
1123
|
+
// For TPT inheritance, owning relations (M:1 and owning 1:1) may have FK columns in a parent table
|
|
1124
|
+
// Resolve the correct alias for the table that owns the FK column
|
|
1125
|
+
const ownerAlias = (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))
|
|
1126
|
+
? this.helper.getTPTAliasForProperty(fromField, fromAlias)
|
|
1127
|
+
: fromAlias;
|
|
1106
1128
|
this.createAlias(prop.targetMeta.class, alias);
|
|
1107
1129
|
cond = QueryHelper.processWhere({
|
|
1108
1130
|
where: cond,
|
|
@@ -1134,11 +1156,11 @@ export class QueryBuilder {
|
|
|
1134
1156
|
aliasedName = Object.keys(joins)[1];
|
|
1135
1157
|
}
|
|
1136
1158
|
else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
|
|
1137
|
-
this._joins[aliasedName] = this.helper.joinOneToReference(prop,
|
|
1159
|
+
this._joins[aliasedName] = this.helper.joinOneToReference(prop, ownerAlias, alias, type, cond, schema);
|
|
1138
1160
|
this._joins[aliasedName].path ??= path;
|
|
1139
1161
|
}
|
|
1140
1162
|
else { // MANY_TO_ONE
|
|
1141
|
-
this._joins[aliasedName] = this.helper.joinManyToOneReference(prop,
|
|
1163
|
+
this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
|
|
1142
1164
|
this._joins[aliasedName].path ??= path;
|
|
1143
1165
|
}
|
|
1144
1166
|
return { prop, key: aliasedName };
|
|
@@ -1360,7 +1382,7 @@ export class QueryBuilder {
|
|
|
1360
1382
|
}
|
|
1361
1383
|
applyDiscriminatorCondition() {
|
|
1362
1384
|
const meta = this.mainAlias.meta;
|
|
1363
|
-
if (!meta.discriminatorValue) {
|
|
1385
|
+
if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
|
|
1364
1386
|
return;
|
|
1365
1387
|
}
|
|
1366
1388
|
const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
|
|
@@ -1376,6 +1398,94 @@ export class QueryBuilder {
|
|
|
1376
1398
|
[meta.root.discriminatorColumn]: children.length > 0 ? { $in: [meta.discriminatorValue, ...children.map(c => c.discriminatorValue)] } : meta.discriminatorValue,
|
|
1377
1399
|
});
|
|
1378
1400
|
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Ensures TPT joins are applied. Can be called early before finalize() to populate
|
|
1403
|
+
* the _tptAlias map for use in join resolution. Safe to call multiple times.
|
|
1404
|
+
* @internal
|
|
1405
|
+
*/
|
|
1406
|
+
ensureTPTJoins() {
|
|
1407
|
+
this.applyTPTJoins();
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* For TPT (Table-Per-Type) inheritance: INNER JOINs parent tables.
|
|
1411
|
+
* When querying a child entity, we need to join all parent tables.
|
|
1412
|
+
* Field selection is handled separately in addTPTParentFields().
|
|
1413
|
+
*/
|
|
1414
|
+
applyTPTJoins() {
|
|
1415
|
+
const meta = this.mainAlias.meta;
|
|
1416
|
+
if (meta?.inheritanceType !== 'tpt' || !meta.tptParent || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (this.tptJoinsApplied) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
this.tptJoinsApplied = true;
|
|
1423
|
+
let childMeta = meta;
|
|
1424
|
+
let childAlias = this.mainAlias.aliasName;
|
|
1425
|
+
while (childMeta.tptParent) {
|
|
1426
|
+
const parentMeta = childMeta.tptParent;
|
|
1427
|
+
const parentAlias = this.getNextAlias(parentMeta.className);
|
|
1428
|
+
this.createAlias(parentMeta.class, parentAlias);
|
|
1429
|
+
this._tptAlias[parentMeta.className] = parentAlias;
|
|
1430
|
+
this.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `[tpt]${childMeta.className}`);
|
|
1431
|
+
childMeta = parentMeta;
|
|
1432
|
+
childAlias = parentAlias;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* For TPT inheritance: adds field selections from parent tables.
|
|
1437
|
+
*/
|
|
1438
|
+
addTPTParentFields() {
|
|
1439
|
+
const meta = this.mainAlias.meta;
|
|
1440
|
+
if (meta?.inheritanceType !== 'tpt' || !meta.tptParent || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (!this._fields?.includes('*') && !this._fields?.includes(`${this.mainAlias.aliasName}.*`)) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
let parentMeta = meta.tptParent;
|
|
1447
|
+
while (parentMeta) {
|
|
1448
|
+
const parentAlias = this._tptAlias[parentMeta.className];
|
|
1449
|
+
if (parentAlias) {
|
|
1450
|
+
const schema = parentMeta.schema === '*' ? '*' : this.driver.getSchemaName(parentMeta);
|
|
1451
|
+
parentMeta.ownProps
|
|
1452
|
+
.filter(prop => this.platform.shouldHaveColumn(prop, []))
|
|
1453
|
+
.forEach(prop => this._fields.push(...this.driver.mapPropToFieldNames(this, prop, parentAlias, parentMeta, schema)));
|
|
1454
|
+
}
|
|
1455
|
+
parentMeta = parentMeta.tptParent;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* For TPT polymorphic queries: LEFT JOINs all child tables when querying a TPT base class.
|
|
1460
|
+
* Adds discriminator and child fields to determine and load the concrete type.
|
|
1461
|
+
*/
|
|
1462
|
+
applyTPTPolymorphicJoins() {
|
|
1463
|
+
const meta = this.mainAlias.meta;
|
|
1464
|
+
const descendants = meta?.allTPTDescendants;
|
|
1465
|
+
if (!descendants?.length || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (!this._fields?.includes('*') && !this._fields?.includes(`${this.mainAlias.aliasName}.*`)) {
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
// LEFT JOIN each descendant table and add their fields
|
|
1472
|
+
for (const childMeta of descendants) {
|
|
1473
|
+
const childAlias = this.getNextAlias(childMeta.className);
|
|
1474
|
+
this.createAlias(childMeta.class, childAlias);
|
|
1475
|
+
this._tptAlias[childMeta.className] = childAlias;
|
|
1476
|
+
this.addPropertyJoin(childMeta.tptInverseProp, this.mainAlias.aliasName, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
|
|
1477
|
+
// Add child fields
|
|
1478
|
+
const schema = childMeta.schema === '*' ? '*' : this.driver.getSchemaName(childMeta);
|
|
1479
|
+
childMeta.ownProps
|
|
1480
|
+
.filter(prop => !prop.primary && this.platform.shouldHaveColumn(prop, []))
|
|
1481
|
+
.forEach(prop => this._fields.push(...this.driver.mapPropToFieldNames(this, prop, childAlias, childMeta, schema)));
|
|
1482
|
+
}
|
|
1483
|
+
// Add computed discriminator (CASE WHEN to determine concrete type)
|
|
1484
|
+
// descendants is pre-sorted by depth (deepest first) during discovery
|
|
1485
|
+
if (meta.tptDiscriminatorColumn) {
|
|
1486
|
+
this._fields.push(this.driver.buildTPTDiscriminatorExpression(meta, descendants, this._tptAlias, this.mainAlias.aliasName));
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1379
1489
|
finalize() {
|
|
1380
1490
|
if (this.finalized) {
|
|
1381
1491
|
return;
|
|
@@ -1385,18 +1495,23 @@ export class QueryBuilder {
|
|
|
1385
1495
|
}
|
|
1386
1496
|
const meta = this.mainAlias.meta;
|
|
1387
1497
|
this.applyDiscriminatorCondition();
|
|
1498
|
+
this.applyTPTJoins();
|
|
1499
|
+
this.addTPTParentFields();
|
|
1500
|
+
this.applyTPTPolymorphicJoins();
|
|
1388
1501
|
this.processPopulateHint();
|
|
1389
1502
|
this.processNestedJoins();
|
|
1390
1503
|
if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
|
|
1391
1504
|
const schema = this.getSchema(this.mainAlias);
|
|
1392
|
-
|
|
1505
|
+
// Create a column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
|
|
1506
|
+
// For TPT, use helper to resolve correct alias per property (inherited props use parent alias)
|
|
1507
|
+
const quotedMainAlias = this.platform.quoteIdentifier(this.mainAlias.aliasName).toString();
|
|
1508
|
+
const columns = meta.createColumnMappingObject(prop => this.helper.getTPTAliasForProperty(prop.name, this.mainAlias.aliasName), quotedMainAlias);
|
|
1393
1509
|
meta.props
|
|
1394
1510
|
.filter(prop => prop.formula && (!prop.lazy || this.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
|
|
1395
1511
|
.map(prop => {
|
|
1396
|
-
const alias = this.platform.quoteIdentifier(this.mainAlias.aliasName);
|
|
1397
1512
|
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
1398
|
-
const table = this.helper.createFormulaTable(
|
|
1399
|
-
return `${(prop.formula
|
|
1513
|
+
const table = this.helper.createFormulaTable(quotedMainAlias, meta, schema);
|
|
1514
|
+
return `${this.driver.evaluateFormula(prop.formula, columns, table)} as ${aliased}`;
|
|
1400
1515
|
})
|
|
1401
1516
|
.filter(field => !this._fields.some(f => {
|
|
1402
1517
|
if (isRaw(f)) {
|
|
@@ -1753,6 +1868,7 @@ export class QueryBuilder {
|
|
|
1753
1868
|
}
|
|
1754
1869
|
return schema;
|
|
1755
1870
|
}
|
|
1871
|
+
/** @internal */
|
|
1756
1872
|
createAlias(entityName, aliasName, subQuery) {
|
|
1757
1873
|
const meta = this.metadata.find(entityName);
|
|
1758
1874
|
const alias = { aliasName, entityName, meta, subQuery };
|
|
@@ -1775,7 +1891,7 @@ export class QueryBuilder {
|
|
|
1775
1891
|
this.createMainAlias(entityName, aliasName);
|
|
1776
1892
|
}
|
|
1777
1893
|
createQueryBuilderHelper() {
|
|
1778
|
-
return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver);
|
|
1894
|
+
return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver, this._tptAlias);
|
|
1779
1895
|
}
|
|
1780
1896
|
ensureFromClause() {
|
|
1781
1897
|
/* v8 ignore next */
|
|
@@ -12,9 +12,15 @@ export declare class QueryBuilderHelper {
|
|
|
12
12
|
private readonly aliasMap;
|
|
13
13
|
private readonly subQueries;
|
|
14
14
|
private readonly driver;
|
|
15
|
+
private readonly tptAliasMap;
|
|
15
16
|
private readonly platform;
|
|
16
17
|
private readonly metadata;
|
|
17
|
-
constructor(entityName: EntityName, alias: string, aliasMap: Dictionary<Alias<any>>, subQueries: Dictionary<string>, driver: AbstractSqlDriver);
|
|
18
|
+
constructor(entityName: EntityName, alias: string, aliasMap: Dictionary<Alias<any>>, subQueries: Dictionary<string>, driver: AbstractSqlDriver, tptAliasMap?: Dictionary<string>);
|
|
19
|
+
/**
|
|
20
|
+
* For TPT inheritance, finds the correct alias for a property based on which entity owns it.
|
|
21
|
+
* Returns the main alias if not a TPT property or if the property belongs to the main entity.
|
|
22
|
+
*/
|
|
23
|
+
getTPTAliasForProperty(propName: string, defaultAlias: string): string;
|
|
18
24
|
mapper(field: string | Raw | RawQueryFragmentSymbol, type?: QueryType): string;
|
|
19
25
|
mapper(field: string | Raw | RawQueryFragmentSymbol, type?: QueryType, value?: any, alias?: string | null, schema?: string): string;
|
|
20
26
|
processData(data: Dictionary, convertCustomTypes: boolean, multi?: boolean): any;
|
|
@@ -10,17 +10,44 @@ export class QueryBuilderHelper {
|
|
|
10
10
|
aliasMap;
|
|
11
11
|
subQueries;
|
|
12
12
|
driver;
|
|
13
|
+
tptAliasMap;
|
|
13
14
|
platform;
|
|
14
15
|
metadata;
|
|
15
|
-
constructor(entityName, alias, aliasMap, subQueries, driver) {
|
|
16
|
+
constructor(entityName, alias, aliasMap, subQueries, driver, tptAliasMap = {}) {
|
|
16
17
|
this.entityName = entityName;
|
|
17
18
|
this.alias = alias;
|
|
18
19
|
this.aliasMap = aliasMap;
|
|
19
20
|
this.subQueries = subQueries;
|
|
20
21
|
this.driver = driver;
|
|
22
|
+
this.tptAliasMap = tptAliasMap;
|
|
21
23
|
this.platform = this.driver.getPlatform();
|
|
22
24
|
this.metadata = this.driver.getMetadata();
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* For TPT inheritance, finds the correct alias for a property based on which entity owns it.
|
|
28
|
+
* Returns the main alias if not a TPT property or if the property belongs to the main entity.
|
|
29
|
+
*/
|
|
30
|
+
getTPTAliasForProperty(propName, defaultAlias) {
|
|
31
|
+
const meta = this.aliasMap[defaultAlias]?.meta ?? this.metadata.get(this.entityName);
|
|
32
|
+
if (meta?.inheritanceType !== 'tpt' || !meta.tptParent) {
|
|
33
|
+
return defaultAlias;
|
|
34
|
+
}
|
|
35
|
+
// Check if property is in the main entity's ownProps
|
|
36
|
+
if (meta.ownProps?.some(p => p.name === propName || p.fieldNames?.includes(propName))) {
|
|
37
|
+
return defaultAlias;
|
|
38
|
+
}
|
|
39
|
+
// Walk up the TPT hierarchy to find which parent owns this property
|
|
40
|
+
let parentMeta = meta.tptParent;
|
|
41
|
+
while (parentMeta) {
|
|
42
|
+
const parentAlias = this.tptAliasMap[parentMeta.className];
|
|
43
|
+
if (parentAlias && parentMeta.ownProps?.some(p => p.name === propName || p.fieldNames?.includes(propName))) {
|
|
44
|
+
return parentAlias;
|
|
45
|
+
}
|
|
46
|
+
parentMeta = parentMeta.tptParent;
|
|
47
|
+
}
|
|
48
|
+
// Property not found in hierarchy, return default alias
|
|
49
|
+
return defaultAlias;
|
|
50
|
+
}
|
|
24
51
|
mapper(field, type = QueryType.SELECT, value, alias, schema) {
|
|
25
52
|
if (isRaw(field)) {
|
|
26
53
|
return raw(field.sql, field.params);
|
|
@@ -63,9 +90,15 @@ export class QueryBuilderHelper {
|
|
|
63
90
|
}
|
|
64
91
|
return raw('(' + parts.map(part => this.platform.quoteIdentifier(part)).join(', ') + ')');
|
|
65
92
|
}
|
|
66
|
-
const aliasPrefix = isTableNameAliasRequired ? this.alias + '.' : '';
|
|
67
93
|
const [a, f] = this.splitField(field);
|
|
68
94
|
const prop = this.getProperty(f, a);
|
|
95
|
+
// For TPT inheritance, resolve the correct alias for this property
|
|
96
|
+
// Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
|
|
97
|
+
// not when it's an embedded property name like 'profile1.identity.links'
|
|
98
|
+
const isTableAlias = !!this.aliasMap[a];
|
|
99
|
+
const baseAlias = isTableAlias ? a : this.alias;
|
|
100
|
+
const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(prop?.name ?? f, a) : this.alias;
|
|
101
|
+
const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : '';
|
|
69
102
|
const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
|
|
70
103
|
const fkIdx = fkIdx2 === -1 ? 0 : fkIdx2;
|
|
71
104
|
if (a === prop?.embedded?.[0]) {
|
|
@@ -81,8 +114,8 @@ export class QueryBuilderHelper {
|
|
|
81
114
|
const as = alias === null ? '' : ` as ${aliased}`;
|
|
82
115
|
const meta = this.aliasMap[a]?.meta ?? this.metadata.get(this.entityName);
|
|
83
116
|
const table = this.createFormulaTable(alias2, meta, schema);
|
|
84
|
-
const columns = meta.createColumnMappingObject();
|
|
85
|
-
let value = prop.formula
|
|
117
|
+
const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, a), alias2);
|
|
118
|
+
let value = this.driver.evaluateFormula(prop.formula, columns, table);
|
|
86
119
|
if (!this.isTableNameAliasRequired(type)) {
|
|
87
120
|
value = value.replaceAll(alias2 + '.', '');
|
|
88
121
|
}
|
|
@@ -111,7 +144,7 @@ export class QueryBuilderHelper {
|
|
|
111
144
|
if (!isTableNameAliasRequired || this.isPrefixed(ret) || noPrefix) {
|
|
112
145
|
return ret;
|
|
113
146
|
}
|
|
114
|
-
return
|
|
147
|
+
return resolvedAlias + '.' + ret;
|
|
115
148
|
}
|
|
116
149
|
processData(data, convertCustomTypes, multi = false) {
|
|
117
150
|
if (Array.isArray(data)) {
|
|
@@ -209,11 +242,11 @@ export class QueryBuilderHelper {
|
|
|
209
242
|
join.primaryKeys.forEach((primaryKey, idx) => {
|
|
210
243
|
const right = `${join.alias}.${join.joinColumns[idx]}`;
|
|
211
244
|
if (join.prop.formula) {
|
|
212
|
-
const
|
|
245
|
+
const quotedAlias = this.platform.quoteIdentifier(join.ownerAlias).toString();
|
|
213
246
|
const ownerMeta = this.aliasMap[join.ownerAlias]?.meta ?? this.metadata.get(this.entityName);
|
|
214
|
-
const table = this.createFormulaTable(
|
|
215
|
-
const columns = ownerMeta.createColumnMappingObject();
|
|
216
|
-
const left = join.prop.formula
|
|
247
|
+
const table = this.createFormulaTable(quotedAlias, ownerMeta, schema);
|
|
248
|
+
const columns = ownerMeta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, join.ownerAlias), quotedAlias);
|
|
249
|
+
const left = this.driver.evaluateFormula(join.prop.formula, columns, table);
|
|
217
250
|
conditions.push(`${left} = ${this.platform.quoteIdentifier(right)}`);
|
|
218
251
|
return;
|
|
219
252
|
}
|
|
@@ -223,7 +256,7 @@ export class QueryBuilderHelper {
|
|
|
223
256
|
conditions.push(`${left} = ${this.platform.quoteIdentifier(right)}`);
|
|
224
257
|
});
|
|
225
258
|
}
|
|
226
|
-
if (join.prop.targetMeta?.discriminatorValue && !join.path?.endsWith('[pivot]')) {
|
|
259
|
+
if (join.prop.targetMeta?.root.inheritanceType === 'sti' && join.prop.targetMeta?.discriminatorValue && !join.path?.endsWith('[pivot]')) {
|
|
227
260
|
const typeProperty = join.prop.targetMeta.root.discriminatorColumn;
|
|
228
261
|
const alias = join.inverseAlias ?? join.alias;
|
|
229
262
|
join.cond[`${alias}.${typeProperty}`] = join.prop.targetMeta.discriminatorValue;
|
|
@@ -695,8 +728,10 @@ export class QueryBuilderHelper {
|
|
|
695
728
|
prefix(field, always = false, quote = false, idx) {
|
|
696
729
|
let ret;
|
|
697
730
|
if (!this.isPrefixed(field)) {
|
|
698
|
-
|
|
699
|
-
const
|
|
731
|
+
// For TPT inheritance, resolve the correct alias for this property
|
|
732
|
+
const tptAlias = this.getTPTAliasForProperty(field, this.alias);
|
|
733
|
+
const alias = always ? (quote ? tptAlias : this.platform.quoteIdentifier(tptAlias)) + '.' : '';
|
|
734
|
+
const fieldName = this.fieldName(field, tptAlias, always, idx);
|
|
700
735
|
if (fieldName instanceof Raw) {
|
|
701
736
|
return fieldName.sql;
|
|
702
737
|
}
|
|
@@ -705,11 +740,16 @@ export class QueryBuilderHelper {
|
|
|
705
740
|
else {
|
|
706
741
|
const [a, ...rest] = field.split('.');
|
|
707
742
|
const f = rest.join('.');
|
|
708
|
-
|
|
743
|
+
// For TPT inheritance, resolve the correct alias for this property
|
|
744
|
+
// Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
|
|
745
|
+
// not when it's an embedded property name like 'profile1.identity.links'
|
|
746
|
+
const isTableAlias = !!this.aliasMap[a];
|
|
747
|
+
const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(f, a) : a;
|
|
748
|
+
const fieldName = this.fieldName(f, resolvedAlias, always, idx);
|
|
709
749
|
if (fieldName instanceof Raw) {
|
|
710
750
|
return fieldName.sql;
|
|
711
751
|
}
|
|
712
|
-
ret =
|
|
752
|
+
ret = resolvedAlias + '.' + fieldName;
|
|
713
753
|
}
|
|
714
754
|
if (quote) {
|
|
715
755
|
return this.platform.quoteIdentifier(ret);
|
|
@@ -44,6 +44,11 @@ export declare class DatabaseSchema {
|
|
|
44
44
|
static fromMetadata(metadata: EntityMetadata[], platform: AbstractSqlPlatform, config: Configuration, schemaName?: string, em?: any): DatabaseSchema;
|
|
45
45
|
private static getViewDefinition;
|
|
46
46
|
private static getSchemaName;
|
|
47
|
+
/**
|
|
48
|
+
* Add a foreign key from a TPT child entity's PK to its parent entity's PK.
|
|
49
|
+
* This FK uses ON DELETE CASCADE to ensure child rows are deleted when parent is deleted.
|
|
50
|
+
*/
|
|
51
|
+
private static addTPTForeignKey;
|
|
47
52
|
private static matchName;
|
|
48
53
|
private static isNameAllowed;
|
|
49
54
|
private static isTableNameAllowed;
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -138,15 +138,43 @@ export class DatabaseSchema {
|
|
|
138
138
|
}
|
|
139
139
|
const table = schema.addTable(meta.collection, this.getSchemaName(meta, config, schemaName));
|
|
140
140
|
table.comment = meta.comment;
|
|
141
|
-
|
|
141
|
+
// For TPT child entities, only use ownProps (properties defined in this entity only)
|
|
142
|
+
// For all other entities (including TPT root), use all props
|
|
143
|
+
const propsToProcess = meta.inheritanceType === 'tpt' && meta.tptParent && meta.ownProps
|
|
144
|
+
? meta.ownProps
|
|
145
|
+
: meta.props;
|
|
146
|
+
for (const prop of propsToProcess) {
|
|
142
147
|
if (!this.shouldHaveColumn(meta, prop, skipColumns)) {
|
|
143
148
|
continue;
|
|
144
149
|
}
|
|
145
150
|
table.addColumnFromProperty(prop, meta, config);
|
|
146
151
|
}
|
|
152
|
+
// For TPT child entities, always include the PK columns (they form the FK to parent)
|
|
153
|
+
if (meta.inheritanceType === 'tpt' && meta.tptParent) {
|
|
154
|
+
const pkProps = meta.primaryKeys.map(pk => meta.properties[pk]);
|
|
155
|
+
for (const pkProp of pkProps) {
|
|
156
|
+
// Only add if not already added (it might be in ownProps if defined in this entity)
|
|
157
|
+
if (!propsToProcess.includes(pkProp)) {
|
|
158
|
+
table.addColumnFromProperty(pkProp, meta, config);
|
|
159
|
+
}
|
|
160
|
+
// Child PK must not be autoincrement — it references the parent PK via FK
|
|
161
|
+
for (const field of pkProp.fieldNames) {
|
|
162
|
+
const col = table.getColumn(field);
|
|
163
|
+
if (col) {
|
|
164
|
+
col.autoincrement = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Add FK from child PK to parent PK with ON DELETE CASCADE
|
|
169
|
+
this.addTPTForeignKey(table, meta, config, platform);
|
|
170
|
+
}
|
|
147
171
|
meta.indexes.forEach(index => table.addIndex(meta, index, 'index'));
|
|
148
172
|
meta.uniques.forEach(index => table.addIndex(meta, index, 'unique'));
|
|
149
|
-
|
|
173
|
+
// For TPT child entities, the PK is also defined here
|
|
174
|
+
const pkPropsForIndex = meta.inheritanceType === 'tpt' && meta.tptParent
|
|
175
|
+
? meta.primaryKeys.map(pk => meta.properties[pk])
|
|
176
|
+
: meta.props.filter(prop => prop.primary);
|
|
177
|
+
table.addIndex(meta, { properties: pkPropsForIndex.map(prop => prop.name) }, 'primary');
|
|
150
178
|
for (const check of meta.checks) {
|
|
151
179
|
const columnName = check.property ? meta.properties[check.property].fieldNames[0] : undefined;
|
|
152
180
|
const expression = isRaw(check.expression) ? platform.formatQuery(check.expression.sql, check.expression.params) : check.expression;
|
|
@@ -192,6 +220,31 @@ export class DatabaseSchema {
|
|
|
192
220
|
static getSchemaName(meta, config, schema) {
|
|
193
221
|
return (meta.schema === '*' ? schema : meta.schema) ?? config.get('schema');
|
|
194
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Add a foreign key from a TPT child entity's PK to its parent entity's PK.
|
|
225
|
+
* This FK uses ON DELETE CASCADE to ensure child rows are deleted when parent is deleted.
|
|
226
|
+
*/
|
|
227
|
+
static addTPTForeignKey(table, meta, config, platform) {
|
|
228
|
+
const parent = meta.tptParent;
|
|
229
|
+
const pkColumnNames = meta.primaryKeys.flatMap(pk => meta.properties[pk].fieldNames);
|
|
230
|
+
const parentPkColumnNames = parent.primaryKeys.flatMap(pk => parent.properties[pk].fieldNames);
|
|
231
|
+
// Determine the parent table name with schema
|
|
232
|
+
const parentSchema = parent.schema === '*' ? undefined : parent.schema ?? config.get('schema', platform.getDefaultSchemaName());
|
|
233
|
+
const parentTableName = parentSchema ? `${parentSchema}.${parent.tableName}` : parent.tableName;
|
|
234
|
+
// Create FK constraint name
|
|
235
|
+
const constraintName = platform.getIndexName(table.name, pkColumnNames, 'foreign');
|
|
236
|
+
// Add the foreign key to the table
|
|
237
|
+
const fks = table.getForeignKeys();
|
|
238
|
+
fks[constraintName] = {
|
|
239
|
+
constraintName,
|
|
240
|
+
columnNames: pkColumnNames,
|
|
241
|
+
localTableName: table.getShortestName(false),
|
|
242
|
+
referencedColumnNames: parentPkColumnNames,
|
|
243
|
+
referencedTableName: parentTableName,
|
|
244
|
+
deleteRule: 'cascade', // TPT always uses cascade delete
|
|
245
|
+
updateRule: 'cascade', // TPT always uses cascade update
|
|
246
|
+
};
|
|
247
|
+
}
|
|
195
248
|
static matchName(name, nameToMatch) {
|
|
196
249
|
return typeof nameToMatch === 'string'
|
|
197
250
|
? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase()
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -80,7 +80,7 @@ export class DatabaseTable {
|
|
|
80
80
|
this.columns[field] = {
|
|
81
81
|
name: prop.fieldNames[idx],
|
|
82
82
|
type: prop.columnTypes[idx],
|
|
83
|
-
generated: prop.generated,
|
|
83
|
+
generated: prop.generated instanceof RawQueryFragment ? this.platform.formatQuery(prop.generated.sql, prop.generated.params) : prop.generated,
|
|
84
84
|
mappedType,
|
|
85
85
|
unsigned: prop.unsigned && this.platform.isNumericColumn(mappedType),
|
|
86
86
|
autoincrement: prop.autoincrement ?? (primary && prop.kind === ReferenceKind.SCALAR && this.platform.isNumericColumn(mappedType)),
|
|
@@ -710,17 +710,15 @@ export class DatabaseTable {
|
|
|
710
710
|
}
|
|
711
711
|
processIndexExpression(indexName, expression, meta) {
|
|
712
712
|
if (expression instanceof Function) {
|
|
713
|
+
const qualifiedName = this.schema ? `${this.schema}.${this.name}` : this.name;
|
|
713
714
|
const table = {
|
|
714
715
|
name: this.name,
|
|
715
716
|
schema: this.schema,
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return `${this.schema}.${this.name}`;
|
|
719
|
-
}
|
|
720
|
-
return this.name;
|
|
721
|
-
},
|
|
717
|
+
qualifiedName,
|
|
718
|
+
toString: () => qualifiedName,
|
|
722
719
|
};
|
|
723
|
-
const
|
|
720
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
721
|
+
const exp = expression(columns, table, indexName);
|
|
724
722
|
return exp instanceof RawQueryFragment ? this.platform.formatQuery(exp.sql, exp.params) : exp;
|
|
725
723
|
}
|
|
726
724
|
return expression;
|