@mikro-orm/sql 7.0.0-dev.228 → 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.
@@ -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, fromAlias, alias, type, cond, schema);
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, fromAlias, alias, type, cond, schema);
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
- const columns = meta.createColumnMappingObject();
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(alias.toString(), meta, schema);
1399
- return `${(prop.formula(table, columns))} as ${aliased}`;
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(table, columns);
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 this.alias + '.' + ret;
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 alias = this.platform.quoteIdentifier(join.ownerAlias);
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(alias.toString(), ownerMeta, schema);
215
- const columns = ownerMeta.createColumnMappingObject();
216
- const left = join.prop.formula(table, columns);
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
- const alias = always ? (quote ? this.alias : this.platform.quoteIdentifier(this.alias)) + '.' : '';
699
- const fieldName = this.fieldName(field, this.alias, always, idx);
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
- const fieldName = this.fieldName(f, a, always, idx);
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 = a + '.' + fieldName;
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;
@@ -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
- for (const prop of meta.props) {
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
- table.addIndex(meta, { properties: meta.props.filter(prop => prop.primary).map(prop => prop.name) }, 'primary');
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()
@@ -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
- toString() {
717
- if (this.schema) {
718
- return `${this.schema}.${this.name}`;
719
- }
720
- return this.name;
721
- },
717
+ qualifiedName,
718
+ toString: () => qualifiedName,
722
719
  };
723
- const exp = expression(table, meta.createColumnMappingObject(), indexName);
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;