@mikro-orm/core 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.
package/EntityManager.js CHANGED
@@ -297,7 +297,7 @@ export class EntityManager {
297
297
  // this method only handles the problem for mongo driver, SQL drivers have their implementation inside QueryBuilder
298
298
  applyDiscriminatorCondition(entityName, where) {
299
299
  const meta = this.metadata.find(entityName);
300
- if (!meta?.discriminatorValue) {
300
+ if (meta?.root.inheritanceType !== 'sti' || !meta?.discriminatorValue) {
301
301
  return where;
302
302
  }
303
303
  const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
@@ -1413,9 +1413,11 @@ export class EntityManager {
1413
1413
  if (p.includes(':')) {
1414
1414
  p = p.split(':', 2)[0];
1415
1415
  }
1416
- const ret = p in meta.root.properties;
1416
+ // For TPT inheritance, check the entity's own properties, not just the root's
1417
+ // For STI, meta.properties includes all properties anyway
1418
+ const ret = p in meta.properties;
1417
1419
  if (parts.length > 0) {
1418
- return this.canPopulate(meta.root.properties[p].targetMeta.class, parts.join('.'));
1420
+ return this.canPopulate(meta.properties[p].targetMeta.class, parts.join('.'));
1419
1421
  }
1420
1422
  return ret;
1421
1423
  }
@@ -83,7 +83,7 @@ export class EntityFactory {
83
83
  else {
84
84
  this.hydrate(entity, meta2, data, options);
85
85
  }
86
- if (exists && meta.discriminatorColumn && !(entity instanceof meta2.class)) {
86
+ if (exists && meta.root.inheritanceType && !(entity instanceof meta2.class)) {
87
87
  Object.setPrototypeOf(entity, meta2.prototype);
88
88
  }
89
89
  if (options.merge && wrapped.hasPrimaryKey()) {
@@ -281,13 +281,22 @@ export class EntityFactory {
281
281
  return this.unitOfWork.getById(meta.class, pks, schema);
282
282
  }
283
283
  processDiscriminatorColumn(meta, data) {
284
- if (!meta.root.discriminatorColumn) {
284
+ // Handle STI discriminator (persisted column)
285
+ if (meta.root.inheritanceType === 'sti') {
286
+ const prop = meta.properties[meta.root.discriminatorColumn];
287
+ const value = data[prop.name];
288
+ const type = meta.root.discriminatorMap[value];
289
+ meta = type ? this.metadata.get(type) : meta;
285
290
  return meta;
286
291
  }
287
- const prop = meta.properties[meta.root.discriminatorColumn];
288
- const value = data[prop.name];
289
- const type = meta.root.discriminatorMap[value];
290
- meta = type ? this.metadata.get(type) : meta;
292
+ // Handle TPT discriminator (computed at query time)
293
+ if (meta.root.inheritanceType === 'tpt' && meta.root.discriminatorMap) {
294
+ const value = data[meta.root.tptDiscriminatorColumn];
295
+ if (value) {
296
+ const type = meta.root.discriminatorMap[value];
297
+ meta = type ? this.metadata.get(type) : meta;
298
+ }
299
+ }
291
300
  return meta;
292
301
  }
293
302
  /**
@@ -429,6 +429,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
429
429
  primaryKeys?: TPK & InferPrimaryKey<TProperties>[];
430
430
  hooks?: DefineEntityHooks;
431
431
  repository?: () => TRepository;
432
+ inheritance?: 'tpt';
432
433
  discriminatorColumn?: keyof TProperties;
433
434
  versionProperty?: keyof TProperties;
434
435
  concurrencyCheckKeys?: Set<keyof TProperties>;
@@ -579,18 +580,18 @@ type MaybeOpt<Value, Options> = Options extends {
579
580
  mapToPk: true;
580
581
  } ? Value extends Opt<infer OriginalValue> ? OriginalValue : Value : Options extends {
581
582
  autoincrement: true;
582
- } ? Opt<Value> : Options extends {
583
+ } | {
583
584
  onCreate: Function;
584
- } ? Opt<Value> : Options extends {
585
+ } | {
585
586
  default: string | string[] | number | number[] | boolean | null | Date | Raw;
586
- } ? Opt<Value> : Options extends {
587
+ } | {
587
588
  defaultRaw: string;
588
- } ? Opt<Value> : Options extends {
589
+ } | {
589
590
  persist: false;
590
- } ? Opt<Value> : Options extends {
591
+ } | {
591
592
  version: true;
592
- } ? Opt<Value> : Options extends {
593
- formula: string | ((...args: any[]) => string);
593
+ } | {
594
+ formula: string | ((...args: any[]) => any);
594
595
  } ? Opt<Value> : Value;
595
596
  type MaybeHidden<Value, Options> = Options extends {
596
597
  hidden: true;
package/errors.d.ts CHANGED
@@ -68,6 +68,8 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
68
68
  static incompatiblePolymorphicTargets(meta: EntityMetadata, prop: EntityProperty, target1: EntityMetadata, target2: EntityMetadata, reason: string): MetadataError<Partial<any>>;
69
69
  static dangerousPropertyName(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
70
70
  static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
71
+ static mixedInheritanceStrategies(root: EntityMetadata, child: EntityMetadata): MetadataError;
72
+ static tptNotSupportedByDriver(meta: EntityMetadata): MetadataError;
71
73
  private static fromMessage;
72
74
  }
73
75
  export declare class NotFoundError<T extends AnyEntity = AnyEntity> extends ValidationError<T> {
package/errors.js CHANGED
@@ -233,6 +233,12 @@ export class MetadataError extends ValidationError {
233
233
  static viewEntityWithoutExpression(meta) {
234
234
  return new MetadataError(`View entity ${meta.className} is missing 'expression'. View entities must have an expression defining the SQL query.`);
235
235
  }
236
+ static mixedInheritanceStrategies(root, child) {
237
+ return new MetadataError(`Entity ${child.className} cannot mix STI (Single Table Inheritance) and TPT (Table-Per-Type) inheritance. Root entity ${root.className} uses STI (discriminatorColumn) but also has inheritance: 'tpt'. Choose one inheritance strategy per hierarchy.`);
238
+ }
239
+ static tptNotSupportedByDriver(meta) {
240
+ return new MetadataError(`Entity ${meta.className} uses TPT (Table-Per-Type) inheritance which is not supported by the current driver. TPT requires SQL JOINs and is only available with SQL drivers.`);
241
+ }
236
242
  static fromMessage(meta, prop, message) {
237
243
  return new MetadataError(`${meta.className}.${prop.name} ${message}`);
238
244
  }
package/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module core
4
4
  */
5
5
  export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config } from './typings.js';
6
- export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, MigrationDiff, GenerateOptions, FilterObject, IEntityGenerator, ISeedManager, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaTable, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, UmzugMigration, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, } from './typings.js';
6
+ export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, MigrationDiff, GenerateOptions, FilterObject, IEntityGenerator, ISeedManager, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, UmzugMigration, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, } from './typings.js';
7
7
  export * from './enums.js';
8
8
  export * from './errors.js';
9
9
  export * from './exceptions.js';
@@ -39,6 +39,8 @@ export type EntitySchemaMetadata<Entity, Base = never, Class extends EntityCtor
39
39
  properties?: {
40
40
  [Key in keyof OmitBaseProps<Entity, Base> as CleanKeys<OmitBaseProps<Entity, Base>, Key>]-?: EntitySchemaProperty<ExpandProperty<NonNullable<Entity[Key]>>, Entity>;
41
41
  };
42
+ } & {
43
+ inheritance?: 'tpt';
42
44
  };
43
45
  export declare class EntitySchema<Entity = any, Base = never, Class extends EntityCtor = EntityCtor<Entity>> {
44
46
  /**
@@ -78,6 +80,11 @@ export declare class EntitySchema<Entity = any, Base = never, Class extends Enti
78
80
  * @internal
79
81
  */
80
82
  init(): this;
83
+ /**
84
+ * Check if this entity is part of a TPT hierarchy by walking up the extends chain.
85
+ * This handles mid-level abstract entities (e.g., Animal -> Mammal -> Dog where Mammal is abstract).
86
+ */
87
+ private isPartOfTPTHierarchy;
81
88
  private initProperties;
82
89
  private initPrimaryKeys;
83
90
  private normalizeType;
@@ -201,7 +201,9 @@ export class EntitySchema {
201
201
  return this;
202
202
  }
203
203
  this.setClass(this._meta.class);
204
- if (this._meta.abstract && !this._meta.discriminatorColumn) {
204
+ // Abstract TPT entities keep their name because they have their own table
205
+ const isTPT = this._meta.inheritance === 'tpt' || this.isPartOfTPTHierarchy();
206
+ if (this._meta.abstract && !this._meta.discriminatorColumn && !isTPT) {
205
207
  delete this._meta.name;
206
208
  }
207
209
  const tableName = this._meta.collection ?? this._meta.tableName;
@@ -216,6 +218,24 @@ export class EntitySchema {
216
218
  this.initialized = true;
217
219
  return this;
218
220
  }
221
+ /**
222
+ * Check if this entity is part of a TPT hierarchy by walking up the extends chain.
223
+ * This handles mid-level abstract entities (e.g., Animal -> Mammal -> Dog where Mammal is abstract).
224
+ */
225
+ isPartOfTPTHierarchy() {
226
+ let parent = this._meta.extends;
227
+ while (parent) {
228
+ const parentSchema = parent instanceof EntitySchema ? parent : EntitySchema.REGISTRY.get(parent);
229
+ if (!parentSchema) {
230
+ break;
231
+ }
232
+ if (parentSchema._meta.inheritance === 'tpt') {
233
+ return true;
234
+ }
235
+ parent = parentSchema._meta.extends;
236
+ }
237
+ return false;
238
+ }
219
239
  initProperties() {
220
240
  Utils.entries(this._meta.properties).forEach(([name, options]) => {
221
241
  if (Type.isMappedType(options.type)) {
@@ -71,8 +71,43 @@ export declare class MetadataDiscovery {
71
71
  private initPolymorphicRelation;
72
72
  private initEmbeddables;
73
73
  private initSingleTableInheritance;
74
+ /**
75
+ * First pass of TPT initialization: sets up hierarchy relationships
76
+ * (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
77
+ */
78
+ private initTPTRelationships;
79
+ /**
80
+ * Second pass of TPT initialization: re-resolves metadata references after fieldNames
81
+ * are set, syncs to registry metadata, and sets up discriminators.
82
+ */
83
+ private finalizeTPTInheritance;
84
+ /**
85
+ * Initialize TPT discriminator map and virtual discriminator property.
86
+ * Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
87
+ * at query time using CASE WHEN expressions based on which child table has data.
88
+ */
89
+ private initTPTDiscriminator;
90
+ /**
91
+ * Recursively collect all TPT descendants (children, grandchildren, etc.)
92
+ */
93
+ private collectAllTPTDescendants;
94
+ /**
95
+ * Computes ownProps for TPT entities - only properties defined in THIS entity,
96
+ * not inherited from parent. Also creates synthetic join properties for parent/child relationships.
97
+ *
98
+ * Called multiple times during discovery as metadata is progressively built.
99
+ * Each pass overwrites earlier results to reflect the final state of properties.
100
+ */
101
+ private computeTPTOwnProps;
102
+ /** Returns the depth of a TPT entity in its hierarchy (0 for root). */
103
+ private getTPTDepth;
104
+ /**
105
+ * Find the direct TPT parent entity for the given entity.
106
+ */
107
+ private getTPTParent;
74
108
  private createDiscriminatorProperty;
75
109
  private initAutoincrement;
110
+ private createSchemaTable;
76
111
  private initCheckConstraints;
77
112
  private initGeneratedColumn;
78
113
  private getDefaultVersionValue;
@@ -78,6 +78,9 @@ export class MetadataDiscovery {
78
78
  });
79
79
  for (const meta of discovered) {
80
80
  meta.root = discovered.get(meta.root.class);
81
+ if (meta.inheritanceType === 'tpt') {
82
+ this.computeTPTOwnProps(meta);
83
+ }
81
84
  }
82
85
  return discovered;
83
86
  }
@@ -122,6 +125,7 @@ export class MetadataDiscovery {
122
125
  // sort so we discover entities first to get around issues with nested embeddables
123
126
  filtered.sort((a, b) => !a.embeddable === !b.embeddable ? 0 : (a.embeddable ? 1 : -1));
124
127
  filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
128
+ filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
125
129
  filtered.forEach(meta => this.defineBaseEntityProperties(meta));
126
130
  filtered.forEach(meta => {
127
131
  const newMeta = EntitySchema.fromMetadata(meta).init().meta;
@@ -136,6 +140,8 @@ export class MetadataDiscovery {
136
140
  forEachProp((_m, p) => this.initRelation(p));
137
141
  forEachProp((m, p) => this.initEmbeddables(m, p));
138
142
  forEachProp((_m, p) => this.initFieldName(p));
143
+ filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
144
+ filtered.forEach(meta => this.computeTPTOwnProps(meta));
139
145
  forEachProp((m, p) => this.initVersionProperty(m, p));
140
146
  forEachProp((m, p) => this.initCustomType(m, p));
141
147
  forEachProp((m, p) => this.initGeneratedColumn(m, p));
@@ -158,6 +164,9 @@ export class MetadataDiscovery {
158
164
  meta = this.metadata.get(meta.class);
159
165
  meta.sync(true);
160
166
  this.findReferencingProperties(meta, filtered);
167
+ if (meta.inheritanceType === 'tpt') {
168
+ this.computeTPTOwnProps(meta);
169
+ }
161
170
  return meta;
162
171
  });
163
172
  }
@@ -296,7 +305,9 @@ export class MetadataDiscovery {
296
305
  return meta;
297
306
  }
298
307
  const root = this.getRootEntity(base);
299
- if (root.discriminatorColumn) {
308
+ // For STI or TPT, use the root entity.
309
+ // Check both `inheritanceType` (set during discovery) and raw `inheritance` option (set before discovery).
310
+ if (root.discriminatorColumn || root.inheritanceType || root.inheritance === 'tpt') {
300
311
  return root;
301
312
  }
302
313
  return meta;
@@ -1151,6 +1162,7 @@ export class MetadataDiscovery {
1151
1162
  if (!meta.root.discriminatorColumn) {
1152
1163
  return;
1153
1164
  }
1165
+ meta.root.inheritanceType = 'sti';
1154
1166
  if (meta.root.discriminatorMap) {
1155
1167
  const map = meta.root.discriminatorMap;
1156
1168
  Object.keys(map)
@@ -1220,6 +1232,175 @@ export class MetadataDiscovery {
1220
1232
  meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
1221
1233
  meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
1222
1234
  }
1235
+ /**
1236
+ * First pass of TPT initialization: sets up hierarchy relationships
1237
+ * (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
1238
+ */
1239
+ initTPTRelationships(meta, metadata) {
1240
+ if (meta.root !== meta) {
1241
+ meta.root = metadata.find(m => m.class === meta.root.class);
1242
+ }
1243
+ const inheritance = meta.inheritance;
1244
+ if (inheritance === 'tpt' && meta.root === meta) {
1245
+ meta.inheritanceType = 'tpt';
1246
+ meta.tptChildren = [];
1247
+ }
1248
+ if (meta.root.inheritanceType !== 'tpt') {
1249
+ return;
1250
+ }
1251
+ const parent = this.getTPTParent(meta, metadata);
1252
+ if (parent) {
1253
+ meta.tptParent = parent;
1254
+ meta.inheritanceType = 'tpt';
1255
+ parent.tptChildren ??= [];
1256
+ if (!parent.tptChildren.includes(meta)) {
1257
+ parent.tptChildren.push(meta);
1258
+ }
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Second pass of TPT initialization: re-resolves metadata references after fieldNames
1263
+ * are set, syncs to registry metadata, and sets up discriminators.
1264
+ */
1265
+ finalizeTPTInheritance(meta, metadata) {
1266
+ if (meta.inheritanceType !== 'tpt') {
1267
+ return;
1268
+ }
1269
+ if (meta.tptParent) {
1270
+ meta.tptParent = metadata.find(m => m.class === meta.tptParent.class) ?? meta.tptParent;
1271
+ }
1272
+ if (meta.tptChildren) {
1273
+ meta.tptChildren = meta.tptChildren.map(child => metadata.find(m => m.class === child.class) ?? child);
1274
+ }
1275
+ const registryMeta = this.metadata.get(meta.class);
1276
+ if (registryMeta && registryMeta !== meta) {
1277
+ registryMeta.inheritanceType = meta.inheritanceType;
1278
+ registryMeta.tptParent = meta.tptParent ? this.metadata.get(meta.tptParent.class) : undefined;
1279
+ registryMeta.tptChildren = meta.tptChildren?.map(child => this.metadata.get(child.class));
1280
+ }
1281
+ this.initTPTDiscriminator(meta, metadata);
1282
+ }
1283
+ /**
1284
+ * Initialize TPT discriminator map and virtual discriminator property.
1285
+ * Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
1286
+ * at query time using CASE WHEN expressions based on which child table has data.
1287
+ */
1288
+ initTPTDiscriminator(meta, metadata) {
1289
+ const allDescendants = this.collectAllTPTDescendants(meta, metadata);
1290
+ allDescendants.sort((a, b) => this.getTPTDepth(b) - this.getTPTDepth(a));
1291
+ meta.allTPTDescendants = allDescendants;
1292
+ if (meta.root !== meta) {
1293
+ return;
1294
+ }
1295
+ meta.root.discriminatorMap = {};
1296
+ for (const m of allDescendants) {
1297
+ const name = this.namingStrategy.classToTableName(m.className);
1298
+ meta.root.discriminatorMap[name] = m.class;
1299
+ m.discriminatorValue = name;
1300
+ }
1301
+ if (!meta.abstract) {
1302
+ const name = this.namingStrategy.classToTableName(meta.className);
1303
+ meta.root.discriminatorMap[name] = meta.class;
1304
+ meta.discriminatorValue = name;
1305
+ }
1306
+ // Virtual discriminator property - computed at query time via CASE WHEN, not persisted
1307
+ const discriminatorColumn = '__tpt_type';
1308
+ if (!meta.root.properties[discriminatorColumn]) {
1309
+ meta.root.addProperty({
1310
+ name: discriminatorColumn,
1311
+ type: 'string',
1312
+ kind: ReferenceKind.SCALAR,
1313
+ persist: false,
1314
+ userDefined: false,
1315
+ hidden: true,
1316
+ });
1317
+ }
1318
+ meta.root.tptDiscriminatorColumn = discriminatorColumn;
1319
+ }
1320
+ /**
1321
+ * Recursively collect all TPT descendants (children, grandchildren, etc.)
1322
+ */
1323
+ collectAllTPTDescendants(meta, metadata) {
1324
+ const descendants = [];
1325
+ const collect = (parent) => {
1326
+ for (const child of parent.tptChildren ?? []) {
1327
+ const resolved = metadata.find(m => m.class === child.class) ?? child;
1328
+ if (!resolved.abstract) {
1329
+ descendants.push(resolved);
1330
+ }
1331
+ collect(resolved);
1332
+ }
1333
+ };
1334
+ collect(meta);
1335
+ return descendants;
1336
+ }
1337
+ /**
1338
+ * Computes ownProps for TPT entities - only properties defined in THIS entity,
1339
+ * not inherited from parent. Also creates synthetic join properties for parent/child relationships.
1340
+ *
1341
+ * Called multiple times during discovery as metadata is progressively built.
1342
+ * Each pass overwrites earlier results to reflect the final state of properties.
1343
+ */
1344
+ computeTPTOwnProps(meta) {
1345
+ if (meta.inheritanceType !== 'tpt') {
1346
+ return;
1347
+ }
1348
+ const belongsToTable = (prop) => prop.persist !== false || prop.primary;
1349
+ // Use meta.properties (object) since meta.props (array) may not be populated yet
1350
+ const allProps = Object.values(meta.properties);
1351
+ if (!meta.tptParent) {
1352
+ meta.ownProps = allProps.filter(belongsToTable);
1353
+ return;
1354
+ }
1355
+ const parentPropNames = new Set(Object.values(meta.tptParent.properties).map(p => p.name));
1356
+ meta.ownProps = allProps.filter(prop => !parentPropNames.has(prop.name) && belongsToTable(prop));
1357
+ // Create synthetic join properties for the parent-child relationship
1358
+ const childFieldNames = meta.getPrimaryProps().flatMap(p => p.fieldNames);
1359
+ const parentFieldNames = meta.tptParent.getPrimaryProps().flatMap(p => p.fieldNames);
1360
+ meta.tptParentProp = {
1361
+ name: '[tpt:parent]',
1362
+ kind: ReferenceKind.MANY_TO_ONE,
1363
+ targetMeta: meta.tptParent,
1364
+ fieldNames: childFieldNames,
1365
+ referencedColumnNames: parentFieldNames,
1366
+ persist: false,
1367
+ };
1368
+ meta.tptInverseProp = {
1369
+ name: '[tpt:child]',
1370
+ kind: ReferenceKind.ONE_TO_ONE,
1371
+ owner: true,
1372
+ targetMeta: meta,
1373
+ fieldNames: parentFieldNames,
1374
+ joinColumns: parentFieldNames,
1375
+ referencedColumnNames: childFieldNames,
1376
+ persist: false,
1377
+ };
1378
+ }
1379
+ /** Returns the depth of a TPT entity in its hierarchy (0 for root). */
1380
+ getTPTDepth(meta) {
1381
+ let depth = 0;
1382
+ let current = meta;
1383
+ while (current.tptParent) {
1384
+ depth++;
1385
+ current = current.tptParent;
1386
+ }
1387
+ return depth;
1388
+ }
1389
+ /**
1390
+ * Find the direct TPT parent entity for the given entity.
1391
+ */
1392
+ getTPTParent(meta, metadata) {
1393
+ if (meta.root === meta) {
1394
+ return undefined;
1395
+ }
1396
+ return metadata.find(m => {
1397
+ const ext = meta.extends;
1398
+ if (ext instanceof EntitySchema) {
1399
+ return m.class === ext.meta.class || m.className === ext.meta.className;
1400
+ }
1401
+ return m.class === ext || m.className === Utils.className(ext);
1402
+ });
1403
+ }
1223
1404
  createDiscriminatorProperty(meta) {
1224
1405
  meta.addProperty({
1225
1406
  name: meta.discriminatorColumn,
@@ -1236,13 +1417,23 @@ export class MetadataDiscovery {
1236
1417
  pks[0].autoincrement ??= true;
1237
1418
  }
1238
1419
  }
1420
+ createSchemaTable(meta) {
1421
+ const qualifiedName = meta.schema ? `${meta.schema}.${meta.tableName}` : meta.tableName;
1422
+ return {
1423
+ name: meta.tableName,
1424
+ schema: meta.schema,
1425
+ qualifiedName,
1426
+ toString: () => qualifiedName,
1427
+ };
1428
+ }
1239
1429
  initCheckConstraints(meta) {
1240
- const map = meta.createColumnMappingObject();
1430
+ const columns = meta.createSchemaColumnMappingObject();
1431
+ const table = this.createSchemaTable(meta);
1241
1432
  for (const check of meta.checks) {
1242
- const columns = check.property ? meta.properties[check.property].fieldNames : [];
1243
- check.name ??= this.namingStrategy.indexName(meta.tableName, columns, 'check');
1433
+ const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
1434
+ check.name ??= this.namingStrategy.indexName(meta.tableName, fieldNames, 'check');
1244
1435
  if (check.expression instanceof Function) {
1245
- check.expression = check.expression(map);
1436
+ check.expression = check.expression(columns, table);
1246
1437
  }
1247
1438
  }
1248
1439
  if (this.platform.usesEnumCheckConstraints() && !meta.embeddable) {
@@ -1272,9 +1463,9 @@ export class MetadataDiscovery {
1272
1463
  }
1273
1464
  return;
1274
1465
  }
1275
- const map = meta.createColumnMappingObject();
1276
1466
  if (prop.generated instanceof Function) {
1277
- prop.generated = prop.generated(map);
1467
+ const columns = meta.createSchemaColumnMappingObject();
1468
+ prop.generated = prop.generated(columns, this.createSchemaTable(meta));
1278
1469
  }
1279
1470
  }
1280
1471
  getDefaultVersionValue(meta, prop) {
@@ -35,4 +35,13 @@ export declare class MetadataValidator {
35
35
  * View entities must have an expression.
36
36
  */
37
37
  private validateViewEntity;
38
+ /**
39
+ * Validates that STI and TPT are not mixed in the same inheritance hierarchy.
40
+ * An entity hierarchy can use either STI (discriminatorColumn) or TPT (inheritance: 'tpt'),
41
+ * but not both.
42
+ *
43
+ * Note: This validation runs before `initTablePerTypeInheritance` sets `inheritanceType`,
44
+ * so we check the raw `inheritance` option from the decorator/schema.
45
+ */
46
+ private validateInheritanceStrategies;
38
47
  }
@@ -61,6 +61,8 @@ export class MetadataValidator {
61
61
  if (discovered.length === 0 && options.warnWhenNoEntities) {
62
62
  throw MetadataError.noEntityDiscovered();
63
63
  }
64
+ // Validate no mixing of STI and TPT in the same hierarchy
65
+ this.validateInheritanceStrategies(discovered);
64
66
  const tableNames = discovered.filter(meta => !meta.abstract && !meta.embeddable && meta === meta.root && (meta.tableName || meta.collection) && meta.schema !== '*');
65
67
  const duplicateTableNames = Utils.findDuplicates(tableNames.map(meta => {
66
68
  const tableName = meta.tableName || meta.collection;
@@ -117,7 +119,7 @@ export class MetadataValidator {
117
119
  if (!targetMeta) {
118
120
  throw MetadataError.fromWrongTypeDefinition(meta, prop);
119
121
  }
120
- if (targetMeta.abstract && !targetMeta.discriminatorColumn && !targetMeta.embeddable) {
122
+ if (targetMeta.abstract && !targetMeta.root?.inheritanceType && !targetMeta.embeddable) {
121
123
  throw MetadataError.targetIsAbstract(meta, prop);
122
124
  }
123
125
  if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.persist === false && targetMeta.compositePK && options.checkNonPersistentCompositeProps) {
@@ -336,4 +338,30 @@ export class MetadataValidator {
336
338
  // Validate property names
337
339
  this.validatePropertyNames(meta);
338
340
  }
341
+ /**
342
+ * Validates that STI and TPT are not mixed in the same inheritance hierarchy.
343
+ * An entity hierarchy can use either STI (discriminatorColumn) or TPT (inheritance: 'tpt'),
344
+ * but not both.
345
+ *
346
+ * Note: This validation runs before `initTablePerTypeInheritance` sets `inheritanceType`,
347
+ * so we check the raw `inheritance` option from the decorator/schema.
348
+ */
349
+ validateInheritanceStrategies(discovered) {
350
+ const checkedRoots = new Set();
351
+ for (const meta of discovered) {
352
+ if (meta.embeddable) {
353
+ continue;
354
+ }
355
+ const root = meta.root;
356
+ if (checkedRoots.has(root)) {
357
+ continue;
358
+ }
359
+ checkedRoots.add(root);
360
+ const hasSTI = !!root.discriminatorColumn;
361
+ const hasTPT = root.inheritanceType === 'tpt' || root.inheritance === 'tpt';
362
+ if (hasSTI && hasTPT) {
363
+ throw MetadataError.mixedInheritanceStrategies(root, meta);
364
+ }
365
+ }
366
+ }
339
367
  }
@@ -1,10 +1,9 @@
1
- import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery } from '../typings.js';
1
+ import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw } from '../typings.js';
2
2
  import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
3
3
  import type { Type, types } from '../types/index.js';
4
4
  import type { EntityManager } from '../EntityManager.js';
5
5
  import type { FilterOptions, FindOptions } from '../drivers/IDatabaseDriver.js';
6
6
  import type { SerializeOptions } from '../serialization/EntitySerializer.js';
7
- import type { Raw } from '../utils/RawQueryFragment.js';
8
7
  export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
9
8
  /** Override default collection/table name. Alias for `collection`. */
10
9
  tableName?: string;
@@ -18,6 +17,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
18
17
  discriminatorMap?: Dictionary<string>;
19
18
  /** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. */
20
19
  discriminatorValue?: number | string;
20
+ /** Set inheritance strategy: 'tpt' for {@doclink inheritance-mapping#table-per-type-inheritance-tpt | Table-Per-Type} inheritance. When set on the root entity, each entity in the hierarchy gets its own table with a FK from child PK to parent PK. */
21
+ inheritance?: 'tpt';
21
22
  /** Enforce use of constructor when creating managed entity instances. */
22
23
  forceConstructor?: boolean;
23
24
  /** Specify constructor parameters to be used in `em.create` or when `forceConstructor` is enabled. Those should be names of declared entity properties in the same order as your constructor uses them. The ORM tries to infer those automatically, use this option in case the inference fails. */
@@ -137,7 +138,7 @@ export interface PropertyOptions<Owner> {
137
138
  /**
138
139
  * For generated columns. This will be appended to the column type after the `generated always` clause.
139
140
  */
140
- generated?: string | GeneratedColumnCallback<Owner>;
141
+ generated?: string | Raw | GeneratedColumnCallback<Owner>;
141
142
  /**
142
143
  * Set column as nullable for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}.
143
144
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
3
  "type": "module",
4
- "version": "7.0.0-dev.228",
4
+ "version": "7.0.0-dev.229",
5
5
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
6
6
  "exports": {
7
7
  "./package.json": "./package.json",
package/typings.d.ts CHANGED
@@ -18,6 +18,7 @@ import type { Raw } from './utils/RawQueryFragment.js';
18
18
  import type { EntityManager } from './EntityManager.js';
19
19
  import type { EventSubscriber } from './events/EventSubscriber.js';
20
20
  import type { FilterOptions, FindOneOptions, FindOptions, LoadHint } from './drivers/IDatabaseDriver.js';
21
+ export type { Raw };
21
22
  export type Constructor<T = unknown> = new (...args: any[]) => T;
22
23
  export type Dictionary<T = any> = {
23
24
  [k: string]: T;
@@ -400,22 +401,46 @@ export type EntityDTO<T, C extends TypeConfig = never> = {
400
401
  };
401
402
  type TargetKeys<T> = T extends EntityClass<infer P> ? keyof P : keyof T;
402
403
  type PropertyName<T> = IsUnknown<T> extends false ? TargetKeys<T> : string;
403
- type TableName = {
404
+ export type FormulaTable = {
405
+ alias: string;
404
406
  name: string;
405
407
  schema?: string;
408
+ qualifiedName: string;
406
409
  toString: () => string;
407
410
  };
408
- export type FormulaTable = {
409
- alias: string;
411
+ /**
412
+ * Table reference for schema callbacks (indexes, checks, generated columns).
413
+ * Unlike FormulaTable, this has no alias since schema generation doesn't use query aliases.
414
+ */
415
+ export type SchemaTable = {
410
416
  name: string;
411
417
  schema?: string;
412
418
  qualifiedName: string;
413
419
  toString: () => string;
414
420
  };
415
- export type IndexCallback<T> = (table: TableName, columns: Record<PropertyName<T>, string>, indexName: string) => string | Raw;
416
- export type FormulaCallback<T> = (table: FormulaTable, columns: Record<PropertyName<T>, string>) => string;
417
- export type CheckCallback<T> = (columns: Record<PropertyName<T>, string>) => string | Raw;
418
- export type GeneratedColumnCallback<T> = (columns: Record<keyof T, string>) => string;
421
+ /**
422
+ * Column mapping for formula callbacks. Maps property names to fully-qualified alias.fieldName.
423
+ * Has toString() returning the main alias for backwards compatibility with old formula syntax.
424
+ * @example
425
+ * // New recommended syntax - use cols.propName for fully-qualified references
426
+ * formula: cols => `${cols.firstName} || ' ' || ${cols.lastName}`
427
+ *
428
+ * // Old syntax still works - cols.toString() returns the alias
429
+ * formula: cols => `${cols}.first_name || ' ' || ${cols}.last_name`
430
+ */
431
+ export type FormulaColumns<T> = Record<PropertyName<T>, string> & {
432
+ toString(): string;
433
+ };
434
+ /**
435
+ * Column mapping for schema callbacks (indexes, checks, generated columns).
436
+ * Maps property names to field names. For TPT entities, only includes properties
437
+ * that belong to the current table (not inherited properties from parent tables).
438
+ */
439
+ export type SchemaColumns<T> = Record<PropertyName<T>, string>;
440
+ export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable, indexName: string) => string | Raw;
441
+ export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
442
+ export type CheckCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
443
+ export type GeneratedColumnCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
419
444
  export interface CheckConstraint<T = any> {
420
445
  name?: string;
421
446
  property?: string;
@@ -430,7 +455,7 @@ export interface EntityProperty<Owner = any, Target = any> {
430
455
  runtimeType: 'number' | 'string' | 'boolean' | 'bigint' | 'Buffer' | 'Date' | 'object' | 'any' | AnyString;
431
456
  targetMeta?: EntityMetadata<Target>;
432
457
  columnTypes: string[];
433
- generated?: string | GeneratedColumnCallback<Owner>;
458
+ generated?: string | Raw | GeneratedColumnCallback<Owner>;
434
459
  customType?: Type<any>;
435
460
  customTypes: (Type<any> | undefined)[];
436
461
  hasConvertToJSValueSQL: boolean;
@@ -533,7 +558,18 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
533
558
  removeProperty(name: string, sync?: boolean): void;
534
559
  getPrimaryProps(flatten?: boolean): EntityProperty<Entity>[];
535
560
  getPrimaryProp(): EntityProperty<Entity>;
536
- createColumnMappingObject(): Record<PropertyName<Entity>, string>;
561
+ /**
562
+ * Creates a mapping from property names to field names.
563
+ * @param alias - Optional alias to prefix field names. Can be a string (same for all) or a function (per-property).
564
+ * When provided, also adds toString() returning the alias for backwards compatibility with formulas.
565
+ * @param toStringAlias - Optional alias to return from toString(). Defaults to `alias` when it's a string.
566
+ */
567
+ createColumnMappingObject(alias?: string | ((prop: EntityProperty<Entity>) => string), toStringAlias?: string): FormulaColumns<Entity>;
568
+ /**
569
+ * Creates a column mapping for schema callbacks (indexes, checks, generated columns).
570
+ * For TPT entities, only includes properties that belong to the current table (ownProps).
571
+ */
572
+ createSchemaColumnMappingObject(): SchemaColumns<Entity>;
537
573
  get tableName(): string;
538
574
  set tableName(name: string);
539
575
  get uniqueName(): string;
@@ -628,6 +664,22 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
628
664
  definedProperties: Dictionary;
629
665
  /** For polymorphic M:N pivot tables, maps discriminator values to entity classes */
630
666
  polymorphicDiscriminatorMap?: Dictionary<EntityClass>;
667
+ /** Inheritance type: 'sti' (Single Table Inheritance) or 'tpt' (Table-Per-Type). Only set on root entities. */
668
+ inheritanceType?: 'sti' | 'tpt';
669
+ /** For TPT: direct parent entity metadata (the entity this one extends). */
670
+ tptParent?: EntityMetadata;
671
+ /** For TPT: direct child entities (entities that extend this one). */
672
+ tptChildren?: EntityMetadata[];
673
+ /** For TPT: all non-abstract descendants, sorted by depth (deepest first). Precomputed during discovery. */
674
+ allTPTDescendants?: EntityMetadata[];
675
+ /** For TPT: synthetic property representing the join to the parent table (child PK → parent PK). */
676
+ tptParentProp?: EntityProperty;
677
+ /** For TPT: inverse of tptParentProp, used for joining from parent to child (parent PK → child PK). */
678
+ tptInverseProp?: EntityProperty;
679
+ /** For TPT: virtual discriminator property name (computed at query time, not persisted). */
680
+ tptDiscriminatorColumn?: string;
681
+ /** For TPT: properties defined only in THIS entity (not inherited from parent). */
682
+ ownProps?: EntityProperty<Entity>[];
631
683
  hasTriggers?: boolean;
632
684
  /** @internal can be used for computed numeric cache keys */
633
685
  readonly _id: number;
@@ -997,4 +1049,3 @@ export interface EntitySchemaWithMeta<TName extends string = string, TTableName
997
1049
  export type InferEntity<Schema> = Schema extends {
998
1050
  '~entity': infer E;
999
1051
  } ? E : Schema extends EntitySchema<infer Entity> ? Entity : Schema extends EntityClass<infer Entity> ? Entity : Schema;
1000
- export {};
package/typings.js CHANGED
@@ -64,8 +64,52 @@ export class EntityMetadata {
64
64
  getPrimaryProp() {
65
65
  return this.properties[this.primaryKeys[0]];
66
66
  }
67
- createColumnMappingObject() {
68
- return Object.values(this.properties).reduce((o, prop) => {
67
+ /**
68
+ * Creates a mapping from property names to field names.
69
+ * @param alias - Optional alias to prefix field names. Can be a string (same for all) or a function (per-property).
70
+ * When provided, also adds toString() returning the alias for backwards compatibility with formulas.
71
+ * @param toStringAlias - Optional alias to return from toString(). Defaults to `alias` when it's a string.
72
+ */
73
+ createColumnMappingObject(alias, toStringAlias) {
74
+ const resolveAlias = typeof alias === 'function' ? alias : () => alias;
75
+ const defaultAlias = toStringAlias ?? (typeof alias === 'string' ? alias : undefined);
76
+ const result = Object.values(this.properties).reduce((o, prop) => {
77
+ if (prop.fieldNames) {
78
+ const propAlias = resolveAlias(prop);
79
+ o[prop.name] = propAlias ? `${propAlias}.${prop.fieldNames[0]}` : prop.fieldNames[0];
80
+ }
81
+ return o;
82
+ }, {});
83
+ // Add toString() for backwards compatibility when alias is provided
84
+ Object.defineProperty(result, 'toString', {
85
+ value: () => defaultAlias ?? '',
86
+ enumerable: false,
87
+ });
88
+ // Wrap in Proxy to detect old formula signature usage where the first param was FormulaTable.
89
+ // If user accesses `.alias` or `.qualifiedName` (FormulaTable-only properties), warn them.
90
+ const warnedProps = new Set(['alias', 'qualifiedName']);
91
+ return new Proxy(result, {
92
+ get(target, prop, receiver) {
93
+ if (typeof prop === 'string' && warnedProps.has(prop) && !(prop in target)) {
94
+ // eslint-disable-next-line no-console
95
+ console.warn(`[MikroORM] Detected old formula callback signature. The first parameter is now 'columns', not 'table'. ` +
96
+ `Accessing '.${prop}' on the columns object will return undefined. ` +
97
+ `Update your formula: formula(cols => quote\`\${cols.propName} ...\`). See the v7 upgrade guide.`);
98
+ }
99
+ return Reflect.get(target, prop, receiver);
100
+ },
101
+ });
102
+ }
103
+ /**
104
+ * Creates a column mapping for schema callbacks (indexes, checks, generated columns).
105
+ * For TPT entities, only includes properties that belong to the current table (ownProps).
106
+ */
107
+ createSchemaColumnMappingObject() {
108
+ // For TPT entities, only include properties that belong to this entity's table
109
+ const props = this.inheritanceType === 'tpt' && this.ownProps
110
+ ? this.ownProps
111
+ : Object.values(this.properties);
112
+ return props.reduce((o, prop) => {
69
113
  if (prop.fieldNames) {
70
114
  o[prop.name] = prop.fieldNames[0];
71
115
  }
@@ -149,8 +193,8 @@ export class EntityMetadata {
149
193
  this.props.forEach(prop => this.initIndexes(prop));
150
194
  }
151
195
  this.definedProperties = this.trackingProps.reduce((o, prop) => {
152
- const isReference = (prop.inversedBy || prop.mappedBy) && !prop.mapToPk;
153
- if (isReference) {
196
+ const hasInverse = (prop.inversedBy || prop.mappedBy) && !prop.mapToPk;
197
+ if (hasInverse) {
154
198
  // eslint-disable-next-line @typescript-eslint/no-this-alias
155
199
  const meta = this;
156
200
  o[prop.name] = {
@@ -19,6 +19,8 @@ export interface ChangeSet<T> {
19
19
  payload: EntityDictionary<T>;
20
20
  persisted: boolean;
21
21
  originalEntity?: EntityData<T>;
22
+ /** For TPT: changesets for parent tables, ordered from immediate parent to root */
23
+ tptChangeSets?: ChangeSet<T>[];
22
24
  }
23
25
  export declare enum ChangeSetType {
24
26
  CREATE = "create",
@@ -43,6 +43,11 @@ export declare class ChangeSetPersister {
43
43
  * so we use a single query in case of both versioning and default values is used.
44
44
  */
45
45
  private reloadVersionValues;
46
+ /**
47
+ * For TPT child tables, resolve EntityIdentifier values in PK fields.
48
+ * The parent table insert assigns the actual PK value, which the child table references.
49
+ */
50
+ private resolveTPTIdentifiers;
46
51
  private processProperty;
47
52
  /**
48
53
  * Maps values returned via `returning` statement (postgres) or the inserted id (other sql drivers).
@@ -116,6 +116,7 @@ export class ChangeSetPersister {
116
116
  options = this.prepareOptions(meta, options, {
117
117
  convertCustomTypes: false,
118
118
  });
119
+ this.resolveTPTIdentifiers(changeSet);
119
120
  // Use changeSet's own meta for STI entities to get correct field mappings
120
121
  const res = await this.driver.nativeInsertMany(changeSet.meta.class, [changeSet.payload], options);
121
122
  if (!wrapped.hasPrimaryKey()) {
@@ -154,6 +155,9 @@ export class ChangeSetPersister {
154
155
  convertCustomTypes: false,
155
156
  processCollections: false,
156
157
  });
158
+ for (const changeSet of changeSets) {
159
+ this.resolveTPTIdentifiers(changeSet);
160
+ }
157
161
  const res = await this.driver.nativeInsertMany(meta.class, changeSets.map(cs => cs.payload), options);
158
162
  for (let i = 0; i < changeSets.length; i++) {
159
163
  const changeSet = changeSets[i];
@@ -370,6 +374,21 @@ export class ChangeSetPersister {
370
374
  Object.assign(changeSet.payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
371
375
  }
372
376
  }
377
+ /**
378
+ * For TPT child tables, resolve EntityIdentifier values in PK fields.
379
+ * The parent table insert assigns the actual PK value, which the child table references.
380
+ */
381
+ resolveTPTIdentifiers(changeSet) {
382
+ if (changeSet.meta.inheritanceType !== 'tpt' || !changeSet.meta.tptParent) {
383
+ return;
384
+ }
385
+ for (const pk of changeSet.meta.primaryKeys) {
386
+ const value = changeSet.payload[pk];
387
+ if (value instanceof EntityIdentifier) {
388
+ changeSet.payload[pk] = value.getValue();
389
+ }
390
+ }
391
+ }
373
392
  processProperty(changeSet, prop) {
374
393
  const meta = changeSet.meta;
375
394
  const value = changeSet.payload[prop.name]; // for inline embeddables
@@ -96,6 +96,11 @@ export declare class UnitOfWork {
96
96
  getOrphanRemoveStack(): Set<AnyEntity>;
97
97
  getChangeSetPersister(): ChangeSetPersister;
98
98
  private findNewEntities;
99
+ /**
100
+ * For TPT inheritance, creates separate changesets for each table in the hierarchy.
101
+ * Uses the same entity instance for all changesets - only the metadata and payload differ.
102
+ */
103
+ private createTPTChangeSets;
99
104
  /**
100
105
  * Returns `true` when the change set should be skipped as it will be empty after the extra update.
101
106
  */
@@ -559,7 +559,68 @@ export class UnitOfWork {
559
559
  }
560
560
  const changeSet = this.changeSetComputer.computeChangeSet(entity);
561
561
  if (changeSet && !this.checkUniqueProps(changeSet)) {
562
- this.changeSets.set(entity, changeSet);
562
+ // For TPT child entities, create changesets for each table in hierarchy
563
+ if (wrapped.__meta.inheritanceType === 'tpt' && wrapped.__meta.tptParent) {
564
+ this.createTPTChangeSets(entity, changeSet);
565
+ }
566
+ else {
567
+ this.changeSets.set(entity, changeSet);
568
+ }
569
+ }
570
+ }
571
+ /**
572
+ * For TPT inheritance, creates separate changesets for each table in the hierarchy.
573
+ * Uses the same entity instance for all changesets - only the metadata and payload differ.
574
+ */
575
+ createTPTChangeSets(entity, originalChangeSet) {
576
+ const meta = helper(entity).__meta;
577
+ const isCreate = originalChangeSet.type === ChangeSetType.CREATE;
578
+ let current = meta;
579
+ let leafCs;
580
+ const parentChangeSets = [];
581
+ while (current) {
582
+ const isRoot = !current.tptParent;
583
+ const payload = {};
584
+ for (const prop of current.ownProps) {
585
+ if (prop.name in originalChangeSet.payload) {
586
+ payload[prop.name] = originalChangeSet.payload[prop.name];
587
+ }
588
+ }
589
+ // For CREATE on non-root tables, include the PK (EntityIdentifier for deferred resolution)
590
+ if (isCreate && !isRoot) {
591
+ const wrapped = helper(entity);
592
+ const identifier = wrapped.__identifier;
593
+ const identifiers = Array.isArray(identifier) ? identifier : [identifier];
594
+ for (let i = 0; i < current.primaryKeys.length; i++) {
595
+ const pk = current.primaryKeys[i];
596
+ payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
597
+ }
598
+ }
599
+ if (!isCreate && Object.keys(payload).length === 0) {
600
+ current = current.tptParent;
601
+ continue;
602
+ }
603
+ const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
604
+ if (current === meta) {
605
+ cs.originalEntity = originalChangeSet.originalEntity;
606
+ leafCs = cs;
607
+ }
608
+ else {
609
+ parentChangeSets.push(cs);
610
+ }
611
+ current = current.tptParent;
612
+ }
613
+ // When only parent properties changed (UPDATE), leaf payload is empty—create a stub anchor
614
+ if (!leafCs && parentChangeSets.length > 0) {
615
+ leafCs = new ChangeSet(entity, originalChangeSet.type, {}, meta);
616
+ leafCs.originalEntity = originalChangeSet.originalEntity;
617
+ }
618
+ // Store the leaf changeset in the main map (entity as key), with parent CSs attached
619
+ if (leafCs) {
620
+ if (parentChangeSets.length > 0) {
621
+ leafCs.tptChangeSets = parentChangeSets;
622
+ }
623
+ this.changeSets.set(entity, leafCs);
563
624
  }
564
625
  }
565
626
  /**
@@ -876,8 +937,20 @@ export class UnitOfWork {
876
937
  }
877
938
  return false;
878
939
  });
940
+ continue;
941
+ }
942
+ const refEntity = Reference.unwrapReference(ref);
943
+ // For mapToPk properties, the value is a primitive (string/array), not an entity
944
+ if (!Utils.isEntity(refEntity)) {
945
+ continue;
946
+ }
947
+ // For TPT entities, check if the ROOT table's changeset has been persisted
948
+ // (since the FK is to the root table, not the concrete entity's table)
949
+ let cs = this.changeSets.get(refEntity);
950
+ if (cs?.tptChangeSets?.length) {
951
+ // Root table changeset is the last one (ordered immediate parent → root)
952
+ cs = cs.tptChangeSets[cs.tptChangeSets.length - 1];
879
953
  }
880
- const cs = this.changeSets.get(Reference.unwrapReference(ref));
881
954
  const isScheduledForInsert = cs?.type === ChangeSetType.CREATE && !cs.persisted;
882
955
  if (isScheduledForInsert) {
883
956
  this.scheduleExtraUpdate(changeSet, [prop]);
@@ -1000,12 +1073,23 @@ export class UnitOfWork {
1000
1073
  [ChangeSetType.UPDATE_EARLY]: new Map(),
1001
1074
  [ChangeSetType.DELETE_EARLY]: new Map(),
1002
1075
  };
1003
- for (const cs of this.changeSets.values()) {
1076
+ const addToGroup = (cs) => {
1077
+ // Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
1078
+ if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) && !Utils.hasObjectKeys(cs.payload)) {
1079
+ return;
1080
+ }
1004
1081
  const group = groups[cs.type];
1005
- const classGroup = group.get(cs.rootMeta) ?? [];
1082
+ const groupKey = cs.meta.inheritanceType === 'tpt' ? cs.meta : cs.rootMeta;
1083
+ const classGroup = group.get(groupKey) ?? [];
1006
1084
  classGroup.push(cs);
1007
- if (!group.has(cs.rootMeta)) {
1008
- group.set(cs.rootMeta, classGroup);
1085
+ if (!group.has(groupKey)) {
1086
+ group.set(groupKey, classGroup);
1087
+ }
1088
+ };
1089
+ for (const cs of this.changeSets.values()) {
1090
+ addToGroup(cs);
1091
+ for (const parentCs of cs.tptChangeSets ?? []) {
1092
+ addToGroup(parentCs);
1009
1093
  }
1010
1094
  }
1011
1095
  return groups;
@@ -1013,7 +1097,17 @@ export class UnitOfWork {
1013
1097
  getCommitOrder() {
1014
1098
  const calc = new CommitOrderCalculator();
1015
1099
  const set = new Set();
1016
- this.changeSets.forEach(cs => set.add(cs.rootMeta));
1100
+ this.changeSets.forEach(cs => {
1101
+ if (cs.meta.inheritanceType === 'tpt') {
1102
+ set.add(cs.meta);
1103
+ for (const parentCs of cs.tptChangeSets ?? []) {
1104
+ set.add(parentCs.meta);
1105
+ }
1106
+ }
1107
+ else {
1108
+ set.add(cs.rootMeta);
1109
+ }
1110
+ });
1017
1111
  set.forEach(meta => calc.addNode(meta._id));
1018
1112
  for (const meta of set) {
1019
1113
  for (const prop of meta.relations) {
@@ -1026,6 +1120,10 @@ export class UnitOfWork {
1026
1120
  calc.discoverProperty(prop, meta._id);
1027
1121
  }
1028
1122
  }
1123
+ // For TPT, parent table must be inserted BEFORE child tables
1124
+ if (meta.inheritanceType === 'tpt' && meta.tptParent && set.has(meta.tptParent)) {
1125
+ calc.addDependency(meta.tptParent._id, meta._id, 1);
1126
+ }
1029
1127
  }
1030
1128
  return calc.sort().map(id => this.metadata.getById(id));
1031
1129
  }
@@ -92,14 +92,23 @@ export class AbstractSchemaGenerator {
92
92
  getOrderedMetadata(schema) {
93
93
  const metadata = [...this.metadata.getAll().values()].filter(meta => {
94
94
  const isRootEntity = meta.root.class === meta.class;
95
- return isRootEntity && !meta.embeddable && !meta.virtual;
95
+ const isTPTChild = meta.inheritanceType === 'tpt' && meta.tptParent;
96
+ return (isRootEntity || isTPTChild) && !meta.embeddable && !meta.virtual;
96
97
  });
97
98
  const calc = new CommitOrderCalculator();
98
- metadata.forEach(meta => calc.addNode(meta.root._id));
99
+ metadata.forEach(meta => {
100
+ const nodeId = meta.inheritanceType === 'tpt' && meta.tptParent ? meta._id : meta.root._id;
101
+ calc.addNode(nodeId);
102
+ });
99
103
  let meta = metadata.pop();
100
104
  while (meta) {
105
+ const nodeId = meta.inheritanceType === 'tpt' && meta.tptParent ? meta._id : meta.root._id;
101
106
  for (const prop of meta.relations) {
102
- calc.discoverProperty(prop, meta.root._id);
107
+ calc.discoverProperty(prop, nodeId);
108
+ }
109
+ if (meta.inheritanceType === 'tpt' && meta.tptParent) {
110
+ const parentId = meta.tptParent._id;
111
+ calc.addDependency(parentId, nodeId, 1);
103
112
  }
104
113
  meta = metadata.pop();
105
114
  }
@@ -215,7 +215,7 @@ export class EntityComparator {
215
215
  const context = new Map();
216
216
  context.set('clone', clone);
217
217
  context.set('cloneEmbeddable', (o) => this.platform.cloneEmbeddable(o)); // do not clone prototypes
218
- if (meta.discriminatorValue) {
218
+ if (meta.root.inheritanceType === 'sti' && meta.discriminatorValue) {
219
219
  lines.push(` ret${this.wrap(meta.root.discriminatorColumn)} = '${meta.discriminatorValue}'`);
220
220
  }
221
221
  const getRootProperty = (prop) => prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop;
package/utils/Utils.js CHANGED
@@ -123,7 +123,7 @@ export function parseJsonSafe(value) {
123
123
  }
124
124
  export class Utils {
125
125
  static PK_SEPARATOR = '~~~';
126
- static #ORM_VERSION = '7.0.0-dev.228';
126
+ static #ORM_VERSION = '7.0.0-dev.229';
127
127
  /**
128
128
  * Checks if the argument is instance of `Object`. Returns false for arrays.
129
129
  */