@mikro-orm/core 7.0.0-dev.99 → 7.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/EntityManager.d.ts +34 -17
  2. package/EntityManager.js +95 -103
  3. package/MikroORM.d.ts +5 -5
  4. package/MikroORM.js +25 -20
  5. package/cache/FileCacheAdapter.js +11 -3
  6. package/connections/Connection.d.ts +3 -2
  7. package/connections/Connection.js +4 -3
  8. package/drivers/DatabaseDriver.d.ts +11 -11
  9. package/drivers/DatabaseDriver.js +91 -25
  10. package/drivers/IDatabaseDriver.d.ts +50 -20
  11. package/entity/BaseEntity.d.ts +61 -1
  12. package/entity/Collection.d.ts +8 -1
  13. package/entity/Collection.js +12 -13
  14. package/entity/EntityAssigner.js +9 -9
  15. package/entity/EntityFactory.d.ts +6 -1
  16. package/entity/EntityFactory.js +40 -22
  17. package/entity/EntityHelper.d.ts +2 -2
  18. package/entity/EntityHelper.js +27 -4
  19. package/entity/EntityLoader.d.ts +5 -4
  20. package/entity/EntityLoader.js +193 -80
  21. package/entity/EntityRepository.d.ts +27 -7
  22. package/entity/EntityRepository.js +8 -2
  23. package/entity/PolymorphicRef.d.ts +12 -0
  24. package/entity/PolymorphicRef.js +18 -0
  25. package/entity/WrappedEntity.d.ts +2 -2
  26. package/entity/WrappedEntity.js +1 -1
  27. package/entity/defineEntity.d.ts +89 -50
  28. package/entity/defineEntity.js +12 -0
  29. package/entity/index.d.ts +1 -0
  30. package/entity/index.js +1 -0
  31. package/entity/utils.d.ts +6 -1
  32. package/entity/utils.js +33 -0
  33. package/entity/validators.js +2 -2
  34. package/enums.d.ts +2 -2
  35. package/enums.js +1 -0
  36. package/errors.d.ts +16 -8
  37. package/errors.js +40 -13
  38. package/hydration/ObjectHydrator.js +63 -21
  39. package/index.d.ts +1 -1
  40. package/logging/colors.d.ts +1 -1
  41. package/logging/colors.js +7 -6
  42. package/logging/inspect.js +1 -6
  43. package/metadata/EntitySchema.d.ts +43 -13
  44. package/metadata/EntitySchema.js +82 -27
  45. package/metadata/MetadataDiscovery.d.ts +60 -3
  46. package/metadata/MetadataDiscovery.js +665 -154
  47. package/metadata/MetadataProvider.js +3 -1
  48. package/metadata/MetadataStorage.d.ts +13 -6
  49. package/metadata/MetadataStorage.js +64 -19
  50. package/metadata/MetadataValidator.d.ts +32 -2
  51. package/metadata/MetadataValidator.js +196 -31
  52. package/metadata/discover-entities.js +5 -5
  53. package/metadata/types.d.ts +111 -14
  54. package/naming-strategy/AbstractNamingStrategy.d.ts +11 -3
  55. package/naming-strategy/AbstractNamingStrategy.js +12 -0
  56. package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
  57. package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
  58. package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
  59. package/naming-strategy/MongoNamingStrategy.js +6 -6
  60. package/naming-strategy/NamingStrategy.d.ts +17 -3
  61. package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
  62. package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
  63. package/package.json +2 -2
  64. package/platforms/Platform.d.ts +4 -2
  65. package/platforms/Platform.js +5 -2
  66. package/serialization/EntitySerializer.d.ts +3 -0
  67. package/serialization/EntitySerializer.js +15 -13
  68. package/serialization/EntityTransformer.js +6 -6
  69. package/serialization/SerializationContext.d.ts +6 -6
  70. package/typings.d.ts +325 -110
  71. package/typings.js +84 -17
  72. package/unit-of-work/ChangeSet.d.ts +4 -3
  73. package/unit-of-work/ChangeSet.js +2 -3
  74. package/unit-of-work/ChangeSetComputer.d.ts +3 -6
  75. package/unit-of-work/ChangeSetComputer.js +34 -13
  76. package/unit-of-work/ChangeSetPersister.d.ts +12 -10
  77. package/unit-of-work/ChangeSetPersister.js +55 -25
  78. package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
  79. package/unit-of-work/CommitOrderCalculator.js +13 -13
  80. package/unit-of-work/IdentityMap.d.ts +12 -0
  81. package/unit-of-work/IdentityMap.js +39 -1
  82. package/unit-of-work/UnitOfWork.d.ts +21 -3
  83. package/unit-of-work/UnitOfWork.js +203 -56
  84. package/utils/AbstractSchemaGenerator.js +17 -8
  85. package/utils/AsyncContext.d.ts +6 -0
  86. package/utils/AsyncContext.js +42 -0
  87. package/utils/Configuration.d.ts +52 -11
  88. package/utils/Configuration.js +12 -8
  89. package/utils/Cursor.js +21 -8
  90. package/utils/DataloaderUtils.js +13 -11
  91. package/utils/EntityComparator.d.ts +14 -7
  92. package/utils/EntityComparator.js +132 -46
  93. package/utils/QueryHelper.d.ts +16 -6
  94. package/utils/QueryHelper.js +53 -18
  95. package/utils/RawQueryFragment.d.ts +28 -23
  96. package/utils/RawQueryFragment.js +34 -56
  97. package/utils/RequestContext.js +2 -2
  98. package/utils/TransactionContext.js +2 -2
  99. package/utils/TransactionManager.js +1 -1
  100. package/utils/Utils.d.ts +7 -26
  101. package/utils/Utils.js +25 -79
  102. package/utils/clone.js +7 -21
  103. package/utils/env-vars.d.ts +4 -0
  104. package/utils/env-vars.js +13 -3
  105. package/utils/fs-utils.d.ts +21 -0
  106. package/utils/fs-utils.js +106 -11
  107. package/utils/upsert-utils.d.ts +4 -4
@@ -1,13 +1,16 @@
1
1
  import { EntityMetadata, } from '../typings.js';
2
- import { Utils } from '../utils/Utils.js';
2
+ import { compareArrays, Utils } from '../utils/Utils.js';
3
+ import { QueryHelper } from '../utils/QueryHelper.js';
3
4
  import { MetadataValidator } from './MetadataValidator.js';
5
+ import { MetadataProvider } from './MetadataProvider.js';
4
6
  import { MetadataStorage } from './MetadataStorage.js';
5
7
  import { EntitySchema } from './EntitySchema.js';
6
8
  import { Cascade, ReferenceKind } from '../enums.js';
7
9
  import { MetadataError } from '../errors.js';
8
10
  import { t, Type } from '../types/index.js';
9
11
  import { colors } from '../logging/colors.js';
10
- import { raw, RawQueryFragment } from '../utils/RawQueryFragment.js';
12
+ import { raw, Raw } from '../utils/RawQueryFragment.js';
13
+ import { BaseEntity } from '../entity/BaseEntity.js';
11
14
  export class MetadataDiscovery {
12
15
  metadata;
13
16
  platform;
@@ -30,7 +33,8 @@ export class MetadataDiscovery {
30
33
  async discover(preferTs = true) {
31
34
  this.discovered.length = 0;
32
35
  const startTime = Date.now();
33
- this.logger.log('discovery', `ORM entity discovery started, using ${colors.cyan(this.metadataProvider.constructor.name)}`);
36
+ const suffix = this.metadataProvider.constructor === MetadataProvider ? '' : `, using ${colors.cyan(this.metadataProvider.constructor.name)}`;
37
+ this.logger.log('discovery', `ORM entity discovery started${suffix}`);
34
38
  await this.findEntities(preferTs);
35
39
  for (const meta of this.discovered) {
36
40
  /* v8 ignore next */
@@ -47,7 +51,8 @@ export class MetadataDiscovery {
47
51
  discoverSync() {
48
52
  this.discovered.length = 0;
49
53
  const startTime = Date.now();
50
- this.logger.log('discovery', `ORM entity discovery started, using ${colors.cyan(this.metadataProvider.constructor.name)} in sync mode`);
54
+ const suffix = this.metadataProvider.constructor === MetadataProvider ? '' : `, using ${colors.cyan(this.metadataProvider.constructor.name)}`;
55
+ this.logger.log('discovery', `ORM entity discovery started${suffix} in sync mode`);
51
56
  const refs = this.config.get('entities');
52
57
  this.discoverReferences(refs);
53
58
  for (const meta of this.discovered) {
@@ -62,9 +67,6 @@ export class MetadataDiscovery {
62
67
  void this.config.get('discovery').afterDiscovered?.(storage, this.platform);
63
68
  return storage;
64
69
  }
65
- validateDiscovered(metadata) {
66
- return this.validator.validateDiscovered(metadata, this.config.get('discovery'));
67
- }
68
70
  mapDiscoveredEntities() {
69
71
  const discovered = new MetadataStorage();
70
72
  this.discovered
@@ -72,8 +74,14 @@ export class MetadataDiscovery {
72
74
  .sort((a, b) => b.root.name.localeCompare(a.root.name))
73
75
  .forEach(meta => {
74
76
  this.platform.validateMetadata(meta);
75
- discovered.set(meta.className, meta);
77
+ discovered.set(meta.class, meta);
76
78
  });
79
+ for (const meta of discovered) {
80
+ meta.root = discovered.get(meta.root.class);
81
+ if (meta.inheritanceType === 'tpt') {
82
+ this.computeTPTOwnProps(meta);
83
+ }
84
+ }
77
85
  return discovered;
78
86
  }
79
87
  initAccessors(meta) {
@@ -117,35 +125,48 @@ export class MetadataDiscovery {
117
125
  // sort so we discover entities first to get around issues with nested embeddables
118
126
  filtered.sort((a, b) => !a.embeddable === !b.embeddable ? 0 : (a.embeddable ? 1 : -1));
119
127
  filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
128
+ filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
120
129
  filtered.forEach(meta => this.defineBaseEntityProperties(meta));
121
- filtered.forEach(meta => this.metadata.set(meta.className, EntitySchema.fromMetadata(meta).init().meta));
130
+ filtered.forEach(meta => {
131
+ const newMeta = EntitySchema.fromMetadata(meta).init().meta;
132
+ return this.metadata.set(newMeta.class, newMeta);
133
+ });
122
134
  filtered.forEach(meta => this.initAutoincrement(meta));
123
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initEmbeddables(meta, prop)));
124
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initFactoryField(meta, prop)));
125
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initFieldName(prop)));
126
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initVersionProperty(meta, prop)));
127
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initCustomType(meta, prop)));
128
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initGeneratedColumn(meta, prop)));
135
+ const forEachProp = (cb) => {
136
+ filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
137
+ };
138
+ forEachProp((m, p) => this.initFactoryField(m, p));
139
+ forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
140
+ forEachProp((_m, p) => this.initRelation(p));
141
+ forEachProp((m, p) => this.initEmbeddables(m, p));
142
+ forEachProp((_m, p) => this.initFieldName(p));
143
+ filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
144
+ filtered.forEach(meta => this.computeTPTOwnProps(meta));
145
+ forEachProp((m, p) => this.initVersionProperty(m, p));
146
+ forEachProp((m, p) => this.initCustomType(m, p));
147
+ forEachProp((m, p) => this.initGeneratedColumn(m, p));
129
148
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
130
149
  filtered.forEach(meta => this.initCheckConstraints(meta));
131
- for (const meta of filtered) {
132
- for (const prop of Object.values(meta.properties)) {
133
- this.initDefaultValue(prop);
134
- this.inferTypeFromDefault(prop);
135
- this.initColumnType(prop);
136
- }
137
- }
138
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initIndexes(meta, prop)));
150
+ forEachProp((_m, p) => {
151
+ this.initDefaultValue(p);
152
+ this.inferTypeFromDefault(p);
153
+ this.initRelation(p);
154
+ this.initColumnType(p);
155
+ });
156
+ forEachProp((m, p) => this.initIndexes(m, p));
139
157
  filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
140
- filtered.forEach(meta => this.findReferencingProperties(meta, filtered));
141
158
  for (const meta of filtered) {
142
159
  discovered.push(...this.processEntity(meta));
143
160
  }
144
161
  discovered.forEach(meta => meta.sync(true));
145
162
  this.metadataProvider.combineCache();
146
163
  return discovered.map(meta => {
147
- meta = this.metadata.get(meta.className);
164
+ meta = this.metadata.get(meta.class);
148
165
  meta.sync(true);
166
+ this.findReferencingProperties(meta, filtered);
167
+ if (meta.inheritanceType === 'tpt') {
168
+ this.computeTPTOwnProps(meta);
169
+ }
149
170
  return meta;
150
171
  });
151
172
  }
@@ -153,15 +174,19 @@ export class MetadataDiscovery {
153
174
  const { entities, entitiesTs, baseDir } = this.config.getAll();
154
175
  const targets = (preferTs && entitiesTs.length > 0) ? entitiesTs : entities;
155
176
  const processed = [];
177
+ const paths = [];
156
178
  for (const entity of targets) {
157
179
  if (typeof entity === 'string') {
158
- const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
159
- processed.push(...await discoverEntities(entity, { baseDir }));
180
+ paths.push(entity);
160
181
  }
161
182
  else {
162
183
  processed.push(entity);
163
184
  }
164
185
  }
186
+ if (paths.length > 0) {
187
+ const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
188
+ processed.push(...await discoverEntities(paths, { baseDir }));
189
+ }
165
190
  return this.discoverReferences(processed);
166
191
  }
167
192
  discoverMissingTargets() {
@@ -171,17 +196,22 @@ export class MetadataDiscovery {
171
196
  .replace(/\((.*)\)/, '$1'); // unwrap union types
172
197
  const missing = [];
173
198
  this.discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
174
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity && !this.discovered.find(m => m.className === Utils.className(prop.pivotEntity))) {
175
- const target = typeof prop.pivotEntity === 'function'
176
- ? prop.pivotEntity()
177
- : prop.pivotEntity;
178
- missing.push(target);
179
- }
180
- if (prop.kind !== ReferenceKind.SCALAR && !unwrap(prop.type).split(/ ?\| ?/).every(type => this.discovered.find(m => m.className === type))) {
181
- const target = typeof prop.entity === 'function'
199
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
200
+ const pivotEntity = prop.pivotEntity;
201
+ const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
202
+ ? pivotEntity()
203
+ : pivotEntity;
204
+ if (!this.discovered.find(m => m.className === Utils.className(target))) {
205
+ missing.push(target);
206
+ }
207
+ }
208
+ if (prop.kind !== ReferenceKind.SCALAR) {
209
+ const target = typeof prop.entity === 'function' && !prop.entity.prototype
182
210
  ? prop.entity()
183
211
  : prop.type;
184
- missing.push(...Utils.asArray(target));
212
+ if (!unwrap(prop.type).split(/ ?\| ?/).every(type => this.discovered.find(m => m.className === type))) {
213
+ missing.push(...Utils.asArray(target));
214
+ }
185
215
  }
186
216
  }));
187
217
  if (missing.length > 0) {
@@ -190,10 +220,15 @@ export class MetadataDiscovery {
190
220
  }
191
221
  tryDiscoverTargets(targets) {
192
222
  for (const target of targets) {
193
- const isDiscoverable = typeof target === 'function' || target instanceof EntitySchema;
194
- if (isDiscoverable && target.name && !this.metadata.has(target.name)) {
195
- this.discoverReferences([target], false);
196
- this.discoverMissingTargets();
223
+ const schema = target instanceof EntitySchema ? target : undefined;
224
+ const isDiscoverable = typeof target === 'function' || schema;
225
+ if (isDiscoverable && target.name) {
226
+ // Get the actual class for EntitySchema, or use target directly for classes
227
+ const targetClass = schema ? schema.meta.class : target;
228
+ if (!this.metadata.has(targetClass)) {
229
+ this.discoverReferences([target], false);
230
+ this.discoverMissingTargets();
231
+ }
197
232
  }
198
233
  }
199
234
  }
@@ -205,16 +240,16 @@ export class MetadataDiscovery {
205
240
  }
206
241
  const schema = this.getSchema(entity);
207
242
  const meta = schema.init().meta;
208
- this.metadata.set(meta.className, meta);
243
+ this.metadata.set(meta.class, meta);
209
244
  found.push(schema);
210
245
  }
211
246
  // discover parents (base entities) automatically
212
247
  for (const meta of this.metadata) {
213
248
  let parent = meta.extends;
214
- if (parent instanceof EntitySchema && !this.metadata.has(parent.meta.className)) {
249
+ if (parent instanceof EntitySchema && !this.metadata.has(parent.init().meta.class)) {
215
250
  this.discoverReferences([parent], false);
216
251
  }
217
- if (typeof parent === 'function' && parent.name && !this.metadata.has(parent.name)) {
252
+ if (typeof parent === 'function' && parent.name && !this.metadata.has(parent)) {
218
253
  this.discoverReferences([parent], false);
219
254
  }
220
255
  /* v8 ignore next */
@@ -222,7 +257,8 @@ export class MetadataDiscovery {
222
257
  continue;
223
258
  }
224
259
  parent = Object.getPrototypeOf(meta.class);
225
- if (parent.name !== '' && !this.metadata.has(parent.name)) {
260
+ // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
261
+ if (parent.name !== '' && parent.name !== meta.className && !this.metadata.has(parent) && parent !== BaseEntity) {
226
262
  this.discoverReferences([parent], false);
227
263
  }
228
264
  }
@@ -231,14 +267,14 @@ export class MetadataDiscovery {
231
267
  }
232
268
  this.discoverMissingTargets();
233
269
  if (validate) {
234
- this.validateDiscovered(this.discovered);
270
+ this.validator.validateDiscovered(this.discovered, this.config.get('discovery'));
235
271
  }
236
272
  return this.discovered.filter(meta => found.find(m => m.name === meta.className));
237
273
  }
238
- reset(className) {
239
- const exists = this.discovered.findIndex(m => m.className === className);
274
+ reset(entityName) {
275
+ const exists = this.discovered.findIndex(m => m.class === entityName || m.className === Utils.className(entityName));
240
276
  if (exists !== -1) {
241
- this.metadata.reset(this.discovered[exists].className);
277
+ this.metadata.reset(this.discovered[exists].class);
242
278
  this.discovered.splice(exists, 1);
243
279
  }
244
280
  }
@@ -253,23 +289,25 @@ export class MetadataDiscovery {
253
289
  const path = entity[MetadataStorage.PATH_SYMBOL];
254
290
  if (path) {
255
291
  const meta = Utils.copy(MetadataStorage.getMetadata(entity.name, path), false);
256
- meta.path = Utils.relativePath(path, this.config.get('baseDir'));
257
- this.metadata.set(entity.name, meta);
292
+ meta.path = path;
293
+ this.metadata.set(entity, meta);
258
294
  }
259
- const exists = this.metadata.has(entity.name);
260
- const meta = this.metadata.get(entity.name, true);
295
+ const exists = this.metadata.has(entity);
296
+ const meta = this.metadata.get(entity, true);
261
297
  meta.abstract ??= !(exists && meta.name);
262
298
  const schema = EntitySchema.fromMetadata(meta);
263
299
  schema.setClass(entity);
264
300
  return schema;
265
301
  }
266
302
  getRootEntity(meta) {
267
- const base = meta.extends && this.metadata.find(Utils.className(meta.extends));
303
+ const base = meta.extends && this.metadata.find(meta.extends);
268
304
  if (!base || base === meta) { // make sure we do not fall into infinite loop
269
305
  return meta;
270
306
  }
271
307
  const root = this.getRootEntity(base);
272
- 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') {
273
311
  return root;
274
312
  }
275
313
  return meta;
@@ -279,7 +317,7 @@ export class MetadataDiscovery {
279
317
  const path = meta.path;
280
318
  this.logger.log('discovery', `- processing entity ${colors.cyan(meta.className)}${colors.grey(path ? ` (${path})` : '')}`);
281
319
  const root = this.getRootEntity(meta);
282
- schema.meta.path = Utils.relativePath(meta.path, this.config.get('baseDir'));
320
+ schema.meta.path = meta.path;
283
321
  const cache = this.metadataProvider.getCachedMetadata(meta, root);
284
322
  if (cache) {
285
323
  this.logger.log('discovery', `- using cached metadata for entity ${colors.cyan(meta.className)}`);
@@ -292,9 +330,9 @@ export class MetadataDiscovery {
292
330
  }
293
331
  // if the definition is using EntitySchema we still want it to go through the metadata provider to validate no types are missing
294
332
  this.metadataProvider.loadEntityMetadata(meta);
295
- if (!meta.collection && meta.name) {
333
+ if (!meta.tableName && meta.name) {
296
334
  const entityName = root.discriminatorColumn ? root.name : meta.name;
297
- meta.collection = this.namingStrategy.classToTableName(entityName);
335
+ meta.tableName = this.namingStrategy.classToTableName(entityName);
298
336
  }
299
337
  this.metadataProvider.saveToCache(meta);
300
338
  meta.root = root;
@@ -326,6 +364,12 @@ export class MetadataDiscovery {
326
364
  if (!prop.joinColumns || !prop.columnTypes || prop.ownColumns || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
327
365
  continue;
328
366
  }
367
+ // For polymorphic relations, ownColumns should include all fieldNames
368
+ // (discriminator + ID columns) since they are all owned by this relation
369
+ if (prop.polymorphic) {
370
+ prop.ownColumns = prop.fieldNames;
371
+ continue;
372
+ }
329
373
  if (prop.joinColumns.length > 1) {
330
374
  prop.ownColumns = prop.joinColumns.filter(col => {
331
375
  return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
@@ -356,30 +400,30 @@ export class MetadataDiscovery {
356
400
  if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
357
401
  prop.fieldNames = [this.namingStrategy.propertyToColumnName(prop.name, object)];
358
402
  }
359
- else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
403
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
360
404
  prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
361
405
  }
362
406
  else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
363
407
  prop.fieldNames = this.initManyToManyFieldName(prop, prop.name);
364
408
  }
365
409
  }
366
- initManyToOneFieldName(prop, name) {
367
- const meta2 = this.metadata.get(prop.type);
410
+ initManyToOneFieldName(prop, name, tableName) {
411
+ const meta2 = prop.targetMeta;
368
412
  const ret = [];
369
413
  for (const primaryKey of meta2.primaryKeys) {
370
414
  this.initFieldName(meta2.properties[primaryKey]);
371
415
  for (const fieldName of meta2.properties[primaryKey].fieldNames) {
372
- ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK));
416
+ ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK, tableName));
373
417
  }
374
418
  }
375
419
  return ret;
376
420
  }
377
421
  initManyToManyFieldName(prop, name) {
378
- const meta2 = this.metadata.get(prop.type);
422
+ const meta2 = prop.targetMeta;
379
423
  return meta2.primaryKeys.map(() => this.namingStrategy.propertyToColumnName(name));
380
424
  }
381
425
  initManyToManyFields(meta, prop) {
382
- const meta2 = this.metadata.get(prop.type);
426
+ const meta2 = prop.targetMeta;
383
427
  Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
384
428
  const pivotMeta = this.metadata.find(prop.pivotEntity);
385
429
  const props = Object.values(pivotMeta?.properties ?? {});
@@ -400,35 +444,77 @@ export class MetadataDiscovery {
400
444
  prop.inverseJoinColumns ??= second.fieldNames;
401
445
  }
402
446
  if (!prop.pivotTable && prop.owner && this.platform.usesPivotTable()) {
403
- prop.pivotTable = this.namingStrategy.joinTableName(meta.tableName, meta2.tableName, prop.name);
447
+ prop.pivotTable = this.namingStrategy.joinTableName(meta.className, meta2.tableName, prop.name, meta.tableName);
404
448
  }
405
449
  if (prop.mappedBy) {
406
450
  const prop2 = meta2.properties[prop.mappedBy];
407
451
  this.initManyToManyFields(meta2, prop2);
408
452
  prop.pivotTable = prop2.pivotTable;
409
- prop.pivotEntity = prop2.pivotEntity ?? prop2.pivotTable;
453
+ prop.pivotEntity = prop2.pivotEntity;
410
454
  prop.fixedOrder = prop2.fixedOrder;
411
455
  prop.fixedOrderColumn = prop2.fixedOrderColumn;
412
456
  prop.joinColumns = prop2.inverseJoinColumns;
413
457
  prop.inverseJoinColumns = prop2.joinColumns;
458
+ prop.polymorphic = prop2.polymorphic;
459
+ prop.discriminator = prop2.discriminator;
460
+ prop.discriminatorColumn = prop2.discriminatorColumn;
461
+ prop.discriminatorValue = prop2.discriminatorValue;
414
462
  }
415
463
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
416
- prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
417
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
464
+ // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
465
+ if (prop.polymorphic && prop.discriminator) {
466
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
467
+ }
468
+ else {
469
+ const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
470
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK, ownerTableName));
471
+ }
472
+ const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
473
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
474
+ }
475
+ isExplicitTableName(meta) {
476
+ return meta.tableName !== this.namingStrategy.classToTableName(meta.className);
418
477
  }
419
478
  initManyToOneFields(prop) {
420
- const meta2 = this.metadata.get(prop.type);
421
- const fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
422
- Utils.defaultValue(prop, 'referencedTableName', meta2.collection);
479
+ if (prop.polymorphic && prop.polymorphTargets) {
480
+ const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
481
+ const idColumns = fieldNames1.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
482
+ prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
483
+ prop.joinColumns ??= idColumns;
484
+ prop.referencedColumnNames ??= fieldNames1;
485
+ prop.referencedTableName ??= prop.targetMeta.tableName;
486
+ return;
487
+ }
488
+ const meta2 = prop.targetMeta;
489
+ let fieldNames;
490
+ // If targetKey is specified, use that property's field names instead of PKs
491
+ if (prop.targetKey) {
492
+ const targetProp = meta2.properties[prop.targetKey];
493
+ fieldNames = targetProp.fieldNames;
494
+ }
495
+ else {
496
+ fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
497
+ }
498
+ Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
423
499
  if (!prop.joinColumns) {
424
500
  prop.joinColumns = fieldNames.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
425
501
  }
426
502
  if (!prop.referencedColumnNames) {
427
503
  prop.referencedColumnNames = fieldNames;
428
504
  }
505
+ // Relations to composite PK targets need cascade update by default,
506
+ // since composite PKs are more likely to have mutable components
507
+ if (meta2.compositePK) {
508
+ prop.updateRule ??= 'cascade';
509
+ }
510
+ // Nullable relations default to 'set null' on delete - when the referenced
511
+ // entity is deleted, set the FK to null rather than failing
512
+ if (prop.nullable) {
513
+ prop.deleteRule ??= 'set null';
514
+ }
429
515
  }
430
516
  initOneToManyFields(prop) {
431
- const meta2 = this.metadata.get(prop.type);
517
+ const meta2 = prop.targetMeta;
432
518
  if (!prop.joinColumns) {
433
519
  prop.joinColumns = [this.namingStrategy.joinColumnName(prop.name)];
434
520
  }
@@ -441,12 +527,17 @@ export class MetadataDiscovery {
441
527
  const pks = Object.values(meta.properties).filter(prop => prop.primary);
442
528
  meta.primaryKeys = pks.map(prop => prop.name);
443
529
  meta.compositePK = pks.length > 1;
444
- // FK used as PK, we need to cascade
445
- if (pks.length === 1 && pks[0].kind !== ReferenceKind.SCALAR) {
446
- pks[0].deleteRule ??= 'cascade';
530
+ // FK used as PK, we need to cascade - applies to both single FK-as-PK
531
+ // and composite PKs where all PKs are FKs (e.g., pivot-like entities)
532
+ const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
533
+ if (fkPks.length > 0 && fkPks.length === pks.length) {
534
+ for (const pk of fkPks) {
535
+ pk.deleteRule ??= 'cascade';
536
+ pk.updateRule ??= 'cascade';
537
+ }
447
538
  }
448
539
  meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
449
- this.validator.validateEntityDefinition(this.metadata, meta.className, this.config.get('discovery'));
540
+ this.validator.validateEntityDefinition(this.metadata, meta.class, this.config.get('discovery'));
450
541
  for (const prop of Object.values(meta.properties)) {
451
542
  this.initNullability(prop);
452
543
  this.applyNamingStrategy(meta, prop);
@@ -466,7 +557,14 @@ export class MetadataDiscovery {
466
557
  if (this.platform.usesPivotTable()) {
467
558
  return Object.values(meta.properties)
468
559
  .filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
469
- .map(prop => this.definePivotTableEntity(meta, prop));
560
+ .map(prop => {
561
+ const pivotMeta = this.definePivotTableEntity(meta, prop);
562
+ prop.pivotEntity = pivotMeta.class;
563
+ if (prop.inversedBy) {
564
+ prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
565
+ }
566
+ return pivotMeta;
567
+ });
470
568
  }
471
569
  return [];
472
570
  }
@@ -483,8 +581,11 @@ export class MetadataDiscovery {
483
581
  ['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
484
582
  const value = prop[type];
485
583
  if (value instanceof Function) {
486
- const meta2 = this.metadata.get(prop.type);
584
+ const meta2 = prop.targetMeta ?? this.metadata.get(prop.target);
487
585
  prop[type] = value(meta2.properties)?.name;
586
+ if (type === 'pivotEntity' && value) {
587
+ prop[type] = value(meta2.properties);
588
+ }
488
589
  if (prop[type] == null) {
489
590
  throw MetadataError.fromWrongReference(meta, prop, type);
490
591
  }
@@ -500,9 +601,9 @@ export class MetadataDiscovery {
500
601
  }
501
602
  else if (fks.length >= 2) {
502
603
  [first, second] = fks;
503
- /* v8 ignore next */
504
604
  }
505
605
  else {
606
+ /* v8 ignore next */
506
607
  return [];
507
608
  }
508
609
  // wrong FK order, first FK needs to point to the owning side
@@ -516,7 +617,9 @@ export class MetadataDiscovery {
516
617
  return [first, second];
517
618
  }
518
619
  definePivotTableEntity(meta, prop) {
519
- const pivotMeta = this.metadata.find(prop.pivotEntity);
620
+ const pivotMeta = prop.pivotEntity
621
+ ? this.metadata.find(prop.pivotEntity)
622
+ : this.metadata.getByClassName(prop.pivotTable, false);
520
623
  // ensure inverse side exists so we can join it when populating via pivot tables
521
624
  if (!prop.inversedBy && prop.targetMeta) {
522
625
  const inverseName = `${meta.className}_${prop.name}__inverse`;
@@ -525,6 +628,8 @@ export class MetadataDiscovery {
525
628
  name: inverseName,
526
629
  kind: ReferenceKind.MANY_TO_MANY,
527
630
  type: meta.className,
631
+ target: meta.class,
632
+ targetMeta: meta,
528
633
  mappedBy: prop.name,
529
634
  pivotEntity: prop.pivotEntity,
530
635
  pivotTable: prop.pivotTable,
@@ -533,55 +638,190 @@ export class MetadataDiscovery {
533
638
  };
534
639
  this.applyNamingStrategy(prop.targetMeta, inverseProp);
535
640
  this.initCustomType(prop.targetMeta, inverseProp);
536
- this.initRelation(inverseProp);
537
641
  prop.targetMeta.properties[inverseName] = inverseProp;
538
642
  }
539
643
  if (pivotMeta) {
644
+ prop.pivotEntity = pivotMeta.class;
540
645
  this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
646
+ if (prop.polymorphic && prop.discriminatorValue) {
647
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
648
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
649
+ // For composite PK entities sharing a polymorphic pivot table,
650
+ // we need to add columns for each entity type's PKs
651
+ this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
652
+ // Add virtual M:1 relation for this polymorphic owner (for join loading)
653
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
654
+ if (!pivotMeta.properties[ownerRelationName]) {
655
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
656
+ }
657
+ }
541
658
  return pivotMeta;
542
659
  }
543
- const exists = this.metadata.find(prop.pivotTable);
544
- if (exists) {
545
- prop.pivotEntity = exists.className;
546
- return exists;
547
- }
548
660
  let tableName = prop.pivotTable;
549
661
  let schemaName;
550
662
  if (prop.pivotTable.includes('.')) {
551
663
  [schemaName, tableName] = prop.pivotTable.split('.');
552
664
  }
553
665
  schemaName ??= meta.schema;
554
- const targetType = prop.targetMeta.className;
555
- const data = new EntityMetadata({
666
+ const targetMeta = prop.targetMeta;
667
+ const targetType = targetMeta.className;
668
+ const pivotMeta2 = new EntityMetadata({
556
669
  name: prop.pivotTable,
557
670
  className: prop.pivotTable,
558
671
  collection: tableName,
559
672
  schema: schemaName,
560
673
  pivotTable: true,
561
674
  });
562
- prop.pivotEntity = data.className;
675
+ prop.pivotEntity = pivotMeta2.class;
563
676
  if (prop.fixedOrder) {
564
- const primaryProp = this.defineFixedOrderProperty(prop, targetType);
565
- data.properties[primaryProp.name] = primaryProp;
677
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
678
+ pivotMeta2.properties[primaryProp.name] = primaryProp;
566
679
  }
567
680
  else {
568
- data.compositePK = true;
681
+ pivotMeta2.compositePK = true;
569
682
  }
570
683
  // handle self-referenced m:n with same default field names
571
684
  if (meta.className === targetType && prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
572
- prop.joinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(meta.className + '_1', name, meta.compositePK));
573
- prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(meta.className + '_2', name, meta.compositePK));
685
+ // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
686
+ const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
687
+ prop.joinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
688
+ prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
574
689
  if (prop.inversedBy) {
575
- const prop2 = this.metadata.get(targetType).properties[prop.inversedBy];
690
+ const prop2 = targetMeta.properties[prop.inversedBy];
576
691
  prop2.inverseJoinColumns = prop.joinColumns;
577
692
  prop2.joinColumns = prop.inverseJoinColumns;
578
693
  }
694
+ // propagate updated joinColumns to all child entities that inherit this property (STI)
695
+ for (const childMeta of this.discovered.filter(m => m.root === meta && m !== meta)) {
696
+ const childProp = childMeta.properties[prop.name];
697
+ if (childProp) {
698
+ childProp.joinColumns = prop.joinColumns;
699
+ childProp.inverseJoinColumns = prop.inverseJoinColumns;
700
+ }
701
+ }
702
+ }
703
+ // For polymorphic M:N, create discriminator column and polymorphic FK
704
+ if (prop.polymorphic && prop.discriminatorColumn) {
705
+ this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
706
+ }
707
+ else {
708
+ pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, targetType + '_inverse', true, meta.className === targetType);
709
+ pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetMeta.class, meta.name + '_owner', false, meta.className === targetType);
579
710
  }
580
- data.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.className, targetType + '_inverse', true, meta.className === targetType);
581
- data.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetType, meta.name + '_owner', false, meta.className === targetType);
582
- return this.metadata.set(data.className, EntitySchema.fromMetadata(data).init().meta);
711
+ return this.metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
712
+ }
713
+ /**
714
+ * Create a scalar property for a pivot table column.
715
+ */
716
+ createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
717
+ return {
718
+ name,
719
+ fieldNames,
720
+ columnTypes,
721
+ type: options.type ?? 'number',
722
+ kind: ReferenceKind.SCALAR,
723
+ primary: options.primary ?? false,
724
+ nullable: options.nullable ?? true,
725
+ ...(options.persist !== undefined && { persist: options.persist }),
726
+ };
727
+ }
728
+ /**
729
+ * Get column types for an entity's primary keys, initializing them if needed.
730
+ */
731
+ getPrimaryKeyColumnTypes(meta) {
732
+ const columnTypes = [];
733
+ for (const pk of meta.primaryKeys) {
734
+ const pkProp = meta.properties[pk];
735
+ this.initCustomType(meta, pkProp);
736
+ this.initColumnType(pkProp);
737
+ columnTypes.push(...pkProp.columnTypes);
738
+ }
739
+ return columnTypes;
740
+ }
741
+ /**
742
+ * Add missing FK columns for a polymorphic entity to an existing pivot table.
743
+ */
744
+ addPolymorphicPivotColumns(pivotMeta, meta, prop) {
745
+ const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
746
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
747
+ for (let i = 0; i < prop.joinColumns.length; i++) {
748
+ const joinColumn = prop.joinColumns[i];
749
+ if (!existingFieldNames.has(joinColumn)) {
750
+ pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
751
+ }
752
+ }
753
+ }
754
+ /**
755
+ * Define properties for a polymorphic pivot table.
756
+ */
757
+ definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
758
+ const discriminatorColumn = prop.discriminatorColumn;
759
+ const isCompositePK = meta.compositePK;
760
+ // For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
761
+ if (isCompositePK && !prop.fixedOrder) {
762
+ prop.fixedOrder = true;
763
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
764
+ pivotMeta.properties[primaryProp.name] = primaryProp;
765
+ pivotMeta.compositePK = false;
766
+ }
767
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
768
+ this.initFieldName(discriminatorProp);
769
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
770
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
771
+ if (isCompositePK) {
772
+ // Create separate properties for each PK column (nullable for other entity types)
773
+ for (let i = 0; i < prop.joinColumns.length; i++) {
774
+ pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [columnTypes[i]]);
775
+ }
776
+ // Virtual property combining all columns (for compatibility)
777
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
778
+ }
779
+ else {
780
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
781
+ }
782
+ pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
783
+ // Create virtual M:1 relation to the polymorphic owner for single-query join loading
784
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
785
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
786
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
787
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
788
+ }
789
+ /**
790
+ * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
791
+ * This enables single-query join loading for inverse-side polymorphic M:N.
792
+ */
793
+ definePolymorphicOwnerRelation(prop, name, targetMeta) {
794
+ const ret = {
795
+ name,
796
+ type: targetMeta.className,
797
+ target: targetMeta.class,
798
+ kind: ReferenceKind.MANY_TO_ONE,
799
+ nullable: true,
800
+ owner: true,
801
+ primary: false,
802
+ createForeignKeyConstraint: false,
803
+ persist: false,
804
+ index: false,
805
+ };
806
+ ret.targetMeta = targetMeta;
807
+ ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
808
+ ret.referencedColumnNames = [];
809
+ ret.inverseJoinColumns = [];
810
+ for (const primaryKey of targetMeta.primaryKeys) {
811
+ const pkProp = targetMeta.properties[primaryKey];
812
+ ret.referencedColumnNames.push(...pkProp.fieldNames);
813
+ ret.inverseJoinColumns.push(...pkProp.fieldNames);
814
+ ret.length = pkProp.length;
815
+ ret.precision = pkProp.precision;
816
+ ret.scale = pkProp.scale;
817
+ }
818
+ const schema = targetMeta.schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
819
+ ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
820
+ this.initColumnType(ret);
821
+ this.initRelation(ret);
822
+ return ret;
583
823
  }
584
- defineFixedOrderProperty(prop, targetType) {
824
+ defineFixedOrderProperty(prop, targetMeta) {
585
825
  const pk = prop.fixedOrderColumn || this.namingStrategy.referenceColumnName();
586
826
  const primaryProp = {
587
827
  name: pk,
@@ -595,7 +835,7 @@ export class MetadataDiscovery {
595
835
  this.initColumnType(primaryProp);
596
836
  prop.fixedOrderColumn = pk;
597
837
  if (prop.inversedBy) {
598
- const prop2 = this.metadata.get(targetType).properties[prop.inversedBy];
838
+ const prop2 = targetMeta.properties[prop.inversedBy];
599
839
  prop2.fixedOrder = true;
600
840
  prop2.fixedOrderColumn = pk;
601
841
  }
@@ -604,7 +844,8 @@ export class MetadataDiscovery {
604
844
  definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
605
845
  const ret = {
606
846
  name,
607
- type,
847
+ type: Utils.className(type),
848
+ target: type,
608
849
  kind: ReferenceKind.MANY_TO_ONE,
609
850
  cascade: [Cascade.ALL],
610
851
  fixedOrder: prop.fixedOrder,
@@ -616,10 +857,9 @@ export class MetadataDiscovery {
616
857
  deleteRule: prop.deleteRule,
617
858
  createForeignKeyConstraint: prop.createForeignKeyConstraint,
618
859
  };
619
- if (selfReferencing && !this.platform.supportsMultipleCascadePaths()) {
620
- ret.updateRule ??= 'no action';
621
- ret.deleteRule ??= 'no action';
622
- }
860
+ const defaultRule = selfReferencing && !this.platform.supportsMultipleCascadePaths() ? 'no action' : 'cascade';
861
+ ret.updateRule ??= defaultRule;
862
+ ret.deleteRule ??= defaultRule;
623
863
  const meta = this.metadata.get(type);
624
864
  ret.targetMeta = meta;
625
865
  ret.joinColumns = [];
@@ -662,7 +902,7 @@ export class MetadataDiscovery {
662
902
  Object.values(meta.properties)
663
903
  .filter(prop => prop.kind !== ReferenceKind.SCALAR && !prop.owner && prop.mappedBy)
664
904
  .forEach(prop => {
665
- const meta2 = this.metadata.get(prop.type);
905
+ const meta2 = prop.targetMeta;
666
906
  const prop2 = meta2.properties[prop.mappedBy];
667
907
  if (prop2 && !prop2.inversedBy) {
668
908
  prop2.inversedBy = prop.name;
@@ -670,7 +910,7 @@ export class MetadataDiscovery {
670
910
  });
671
911
  }
672
912
  defineBaseEntityProperties(meta) {
673
- const base = meta.extends && this.metadata.get(Utils.className(meta.extends));
913
+ const base = meta.extends && this.metadata.get(meta.extends);
674
914
  if (!base || base === meta) { // make sure we do not fall into infinite loop
675
915
  return 0;
676
916
  }
@@ -760,6 +1000,54 @@ export class MetadataDiscovery {
760
1000
  polymorphs.forEach(meta => meta.root = embeddable);
761
1001
  }
762
1002
  }
1003
+ initPolymorphicRelation(meta, prop, discovered) {
1004
+ if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
1005
+ return;
1006
+ }
1007
+ prop.polymorphic = true;
1008
+ prop.discriminator ??= prop.name;
1009
+ prop.discriminatorColumn ??= this.namingStrategy.discriminatorColumnName(prop.discriminator);
1010
+ prop.createForeignKeyConstraint = false;
1011
+ const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1012
+ if (isToOne) {
1013
+ const types = prop.type.split(/ ?\| ?/);
1014
+ prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1015
+ prop.targetMeta = prop.polymorphTargets[0];
1016
+ prop.referencedPKs = prop.targetMeta?.primaryKeys;
1017
+ }
1018
+ if (prop.discriminatorMap) {
1019
+ const normalizedMap = {};
1020
+ for (const [key, value] of Object.entries(prop.discriminatorMap)) {
1021
+ const targetMeta = this.metadata.getByClassName(value, false);
1022
+ if (!targetMeta) {
1023
+ throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
1024
+ }
1025
+ normalizedMap[key] = targetMeta.class;
1026
+ if (targetMeta.class === meta.class) {
1027
+ prop.discriminatorValue = key;
1028
+ }
1029
+ }
1030
+ prop.discriminatorMap = normalizedMap;
1031
+ }
1032
+ else if (isToOne) {
1033
+ prop.discriminatorMap = {};
1034
+ const tableNameToTarget = new Map();
1035
+ for (const target of prop.polymorphTargets) {
1036
+ const existing = tableNameToTarget.get(target.tableName);
1037
+ if (existing) {
1038
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, existing, target, `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`);
1039
+ }
1040
+ tableNameToTarget.set(target.tableName, target);
1041
+ prop.discriminatorMap[target.tableName] = target.class;
1042
+ }
1043
+ }
1044
+ else {
1045
+ prop.discriminatorValue ??= meta.tableName;
1046
+ if (!prop.discriminatorMap) {
1047
+ prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
1048
+ }
1049
+ }
1050
+ }
763
1051
  initEmbeddables(meta, embeddedProp, visited = new Set()) {
764
1052
  if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
765
1053
  return;
@@ -795,7 +1083,7 @@ export class MetadataDiscovery {
795
1083
  const glue = object ? '~' : '_';
796
1084
  for (const prop of Object.values(embeddable.properties)) {
797
1085
  const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
798
- meta.properties[name] = Utils.copy(prop, false);
1086
+ meta.properties[name] = Utils.copy(prop);
799
1087
  meta.properties[name].name = name;
800
1088
  meta.properties[name].embedded = [embeddedProp.name, prop.name];
801
1089
  meta.propertyOrder.set(name, (order += 0.01));
@@ -832,10 +1120,12 @@ export class MetadataDiscovery {
832
1120
  path = [embeddedProp.fieldNames[0]];
833
1121
  }
834
1122
  this.initFieldName(prop, true);
1123
+ this.initRelation(prop);
835
1124
  path.push(prop.fieldNames[0]);
836
1125
  meta.properties[name].fieldNames = prop.fieldNames;
837
1126
  meta.properties[name].embeddedPath = path;
838
- const fieldName = raw(this.platform.getSearchJsonPropertySQL(path.join('->'), prop.runtimeType ?? prop.type, true));
1127
+ const targetProp = prop.targetMeta?.getPrimaryProp() ?? prop;
1128
+ const fieldName = raw(this.platform.getSearchJsonPropertySQL(path.join('->'), targetProp.runtimeType ?? targetProp.type, true));
839
1129
  meta.properties[name].fieldNameRaw = fieldName.sql; // for querying in SQL drivers
840
1130
  meta.properties[name].persist = false; // only virtual as we store the whole object
841
1131
  meta.properties[name].userDefined = false; // mark this as a generated/internal property, so we can distinguish from user-defined non-persist properties
@@ -863,7 +1153,7 @@ export class MetadataDiscovery {
863
1153
  }
864
1154
  initSingleTableInheritance(meta, metadata) {
865
1155
  if (meta.root !== meta && !meta.__processed) {
866
- meta.root = metadata.find(m => m.className === meta.root.className);
1156
+ meta.root = metadata.find(m => m.class === meta.root.class);
867
1157
  meta.root.__processed = true;
868
1158
  }
869
1159
  else {
@@ -872,17 +1162,24 @@ export class MetadataDiscovery {
872
1162
  if (!meta.root.discriminatorColumn) {
873
1163
  return;
874
1164
  }
875
- if (!meta.root.discriminatorMap) {
1165
+ meta.root.inheritanceType = 'sti';
1166
+ if (meta.root.discriminatorMap) {
1167
+ const map = meta.root.discriminatorMap;
1168
+ Object.keys(map)
1169
+ .filter(key => typeof map[key] === 'string')
1170
+ .forEach(key => map[key] = this.metadata.getByClassName(map[key]).class);
1171
+ }
1172
+ else {
876
1173
  meta.root.discriminatorMap = {};
877
1174
  const children = metadata
878
- .filter(m => m.root.className === meta.root.className && !m.abstract)
1175
+ .filter(m => m.root.class === meta.root.class && !m.abstract)
879
1176
  .sort((a, b) => a.className.localeCompare(b.className));
880
1177
  for (const m of children) {
881
1178
  const name = m.discriminatorValue ?? this.namingStrategy.classToTableName(m.className);
882
- meta.root.discriminatorMap[name] = m.className;
1179
+ meta.root.discriminatorMap[name] = m.class;
883
1180
  }
884
1181
  }
885
- meta.discriminatorValue = Object.entries(meta.root.discriminatorMap).find(([, className]) => className === meta.className)?.[0];
1182
+ meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
886
1183
  if (!meta.root.properties[meta.root.discriminatorColumn]) {
887
1184
  this.createDiscriminatorProperty(meta.root);
888
1185
  }
@@ -891,32 +1188,219 @@ export class MetadataDiscovery {
891
1188
  if (meta.root === meta) {
892
1189
  return;
893
1190
  }
894
- let i = 1;
895
1191
  Object.values(meta.properties).forEach(prop => {
896
1192
  const newProp = { ...prop };
897
- if (meta.root.properties[prop.name] && meta.root.properties[prop.name].type !== prop.type) {
1193
+ const rootProp = meta.root.properties[prop.name];
1194
+ if (rootProp && (rootProp.type !== prop.type || (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))) {
898
1195
  const name = newProp.name;
899
1196
  this.initFieldName(newProp, newProp.object);
900
- newProp.name = name + '_' + (i++);
1197
+ newProp.renamedFrom = name;
1198
+ newProp.name = `${name}_${meta._id}`;
901
1199
  meta.root.addProperty(newProp);
1200
+ this.initFieldName(prop, prop.object);
1201
+ // Track all field variants and map discriminator values to field names
1202
+ if (!rootProp.stiFieldNames) {
1203
+ this.initFieldName(rootProp, rootProp.object);
1204
+ rootProp.stiFieldNames = [...rootProp.fieldNames];
1205
+ rootProp.stiFieldNameMap = {};
1206
+ // Find which discriminator owns the original fieldNames
1207
+ for (const [discValue, childClass] of Object.entries(meta.root.discriminatorMap)) {
1208
+ const childMeta = this.metadata.find(childClass);
1209
+ if (childMeta?.properties[prop.name]?.fieldNames && compareArrays(childMeta.properties[prop.name].fieldNames, rootProp.fieldNames)) {
1210
+ rootProp.stiFieldNameMap[discValue] = rootProp.fieldNames[0];
1211
+ break;
1212
+ }
1213
+ }
1214
+ }
1215
+ rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
1216
+ rootProp.stiFieldNames.push(...prop.fieldNames);
902
1217
  newProp.nullable = true;
903
1218
  newProp.name = name;
904
1219
  newProp.hydrate = false;
905
1220
  newProp.inherited = true;
906
1221
  return;
907
1222
  }
908
- if (prop.enum && prop.items && meta.root.properties[prop.name]?.items) {
909
- newProp.items = Utils.unique([...meta.root.properties[prop.name].items, ...prop.items]);
1223
+ if (prop.enum && prop.items && rootProp?.items) {
1224
+ newProp.items = Utils.unique([...rootProp.items, ...prop.items]);
910
1225
  }
911
1226
  newProp.nullable = true;
912
- newProp.inherited = !meta.root.properties[prop.name];
1227
+ newProp.inherited = !rootProp;
913
1228
  meta.root.addProperty(newProp);
914
1229
  });
915
- meta.collection = meta.root.collection;
1230
+ meta.tableName = meta.root.tableName;
916
1231
  meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]);
917
1232
  meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
918
1233
  meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
919
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
+ }
920
1404
  createDiscriminatorProperty(meta) {
921
1405
  meta.addProperty({
922
1406
  name: meta.discriminatorColumn,
@@ -933,13 +1417,23 @@ export class MetadataDiscovery {
933
1417
  pks[0].autoincrement ??= true;
934
1418
  }
935
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
+ }
936
1429
  initCheckConstraints(meta) {
937
- const map = meta.createColumnMappingObject();
1430
+ const columns = meta.createSchemaColumnMappingObject();
1431
+ const table = this.createSchemaTable(meta);
938
1432
  for (const check of meta.checks) {
939
- const columns = check.property ? meta.properties[check.property].fieldNames : [];
940
- 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');
941
1435
  if (check.expression instanceof Function) {
942
- check.expression = check.expression(map);
1436
+ check.expression = check.expression(columns, table);
943
1437
  }
944
1438
  }
945
1439
  if (this.platform.usesEnumCheckConstraints() && !meta.embeddable) {
@@ -969,9 +1463,9 @@ export class MetadataDiscovery {
969
1463
  }
970
1464
  return;
971
1465
  }
972
- const map = meta.createColumnMappingObject();
973
1466
  if (prop.generated instanceof Function) {
974
- prop.generated = prop.generated(map);
1467
+ const columns = meta.createSchemaColumnMappingObject();
1468
+ prop.generated = prop.generated(columns, this.createSchemaTable(meta));
975
1469
  }
976
1470
  }
977
1471
  getDefaultVersionValue(meta, prop) {
@@ -991,9 +1485,6 @@ export class MetadataDiscovery {
991
1485
  return '1';
992
1486
  }
993
1487
  inferDefaultValue(meta, prop) {
994
- if (!meta.class) {
995
- return;
996
- }
997
1488
  try {
998
1489
  // try to create two entity instances to detect the value is stable
999
1490
  const now = Date.now();
@@ -1021,7 +1512,7 @@ export class MetadataDiscovery {
1021
1512
  return;
1022
1513
  }
1023
1514
  let val = prop.default;
1024
- const raw = RawQueryFragment.getKnownFragment(val);
1515
+ const raw = Raw.getKnownFragment(val);
1025
1516
  if (raw) {
1026
1517
  prop.defaultRaw = this.platform.formatQuery(raw.sql, raw.params);
1027
1518
  return;
@@ -1149,9 +1640,9 @@ export class MetadataDiscovery {
1149
1640
  if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
1150
1641
  prop.type = prop.customType.name;
1151
1642
  }
1152
- if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && this.metadata.get(prop.type).compositePK) {
1643
+ if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.polymorphic && prop.targetMeta.compositePK) {
1153
1644
  prop.customTypes = [];
1154
- for (const pk of this.metadata.get(prop.type).getPrimaryProps()) {
1645
+ for (const pk of prop.targetMeta.getPrimaryProps()) {
1155
1646
  if (pk.customType) {
1156
1647
  prop.customTypes.push(pk.customType);
1157
1648
  prop.hasConvertToJSValueSQL ||= !!pk.customType.convertToJSValueSQL && pk.customType.convertToJSValueSQL('', this.platform) !== '';
@@ -1178,22 +1669,36 @@ export class MetadataDiscovery {
1178
1669
  }
1179
1670
  }
1180
1671
  initRelation(prop) {
1181
- if (prop.kind === ReferenceKind.SCALAR) {
1672
+ if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
1182
1673
  return;
1183
1674
  }
1184
- const meta2 = this.discovered.find(m => m.className === prop.type);
1185
- prop.referencedPKs = meta2.primaryKeys;
1675
+ // when the target is a polymorphic embedded entity, `prop.target` is an array of classes, we need to get the metadata by the type name instead
1676
+ const meta2 = this.metadata.find(prop.target) ?? this.metadata.getByClassName(prop.type);
1677
+ // If targetKey is specified, use that property instead of PKs for referencedPKs
1678
+ prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
1186
1679
  prop.targetMeta = meta2;
1680
+ if (meta2.view) {
1681
+ prop.createForeignKeyConstraint = false;
1682
+ }
1683
+ // Auto-generate formula for persist: false relations, but only for single-column FKs
1684
+ // Composite FK relations need standard JOIN conditions, not formula-based
1187
1685
  if (!prop.formula && prop.persist === false && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.embedded) {
1188
- prop.formula = a => `${a}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1686
+ this.initFieldName(prop);
1687
+ if (prop.fieldNames?.length === 1) {
1688
+ prop.formula = table => `${table}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1689
+ }
1189
1690
  }
1190
1691
  }
1191
1692
  initColumnType(prop) {
1192
1693
  this.initUnsigned(prop);
1193
- this.metadata.find(prop.type)?.getPrimaryProps().map(pk => {
1194
- prop.length ??= pk.length;
1195
- prop.precision ??= pk.precision;
1196
- prop.scale ??= pk.scale;
1694
+ // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
1695
+ const targetProps = prop.targetMeta
1696
+ ? (prop.targetKey ? [prop.targetMeta.properties[prop.targetKey]] : prop.targetMeta.getPrimaryProps())
1697
+ : [];
1698
+ targetProps.map(targetProp => {
1699
+ prop.length ??= targetProp.length;
1700
+ prop.precision ??= targetProp.precision;
1701
+ prop.scale ??= targetProp.scale;
1197
1702
  });
1198
1703
  if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
1199
1704
  delete prop.type;
@@ -1207,7 +1712,6 @@ export class MetadataDiscovery {
1207
1712
  const mappedType = this.getMappedType(prop);
1208
1713
  const SCALAR_TYPES = ['string', 'number', 'boolean', 'bigint', 'Date', 'Buffer', 'RegExp', 'any', 'unknown'];
1209
1714
  if (mappedType instanceof t.unknown
1210
- && !prop.columnTypes
1211
1715
  // it could be a runtime type from reflect-metadata
1212
1716
  && !SCALAR_TYPES.includes(prop.type)
1213
1717
  // or it might be inferred via ts-morph to some generic type alias
@@ -1220,23 +1724,31 @@ export class MetadataDiscovery {
1220
1724
  }
1221
1725
  return;
1222
1726
  }
1223
- if (prop.kind === ReferenceKind.EMBEDDED && prop.object && !prop.columnTypes) {
1727
+ /* v8 ignore next */
1728
+ if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
1224
1729
  prop.columnTypes = [this.platform.getJsonDeclarationSQL()];
1225
1730
  return;
1226
1731
  }
1227
- const targetMeta = this.metadata.get(prop.type);
1732
+ const targetMeta = prop.targetMeta;
1228
1733
  prop.columnTypes = [];
1229
- for (const pk of targetMeta.getPrimaryProps()) {
1230
- this.initCustomType(targetMeta, pk);
1231
- this.initColumnType(pk);
1232
- const mappedType = this.getMappedType(pk);
1233
- let columnTypes = pk.columnTypes;
1234
- if (pk.autoincrement) {
1235
- columnTypes = [mappedType.getColumnType({ ...pk, autoincrement: false }, this.platform)];
1734
+ // Use targetKey property if specified, otherwise use primary key properties
1735
+ const referencedProps = prop.targetKey
1736
+ ? [targetMeta.properties[prop.targetKey]]
1737
+ : targetMeta.getPrimaryProps();
1738
+ if (prop.polymorphic && prop.polymorphTargets) {
1739
+ prop.columnTypes.push(this.platform.getVarcharTypeDeclarationSQL(prop));
1740
+ }
1741
+ for (const referencedProp of referencedProps) {
1742
+ this.initCustomType(targetMeta, referencedProp);
1743
+ this.initColumnType(referencedProp);
1744
+ const mappedType = this.getMappedType(referencedProp);
1745
+ let columnTypes = referencedProp.columnTypes;
1746
+ if (referencedProp.autoincrement) {
1747
+ columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.platform)];
1236
1748
  }
1237
1749
  prop.columnTypes.push(...columnTypes);
1238
- if (!targetMeta.compositePK) {
1239
- prop.customType = pk.customType;
1750
+ if (!targetMeta.compositePK || prop.targetKey) {
1751
+ prop.customType = referencedProp.customType;
1240
1752
  }
1241
1753
  }
1242
1754
  }
@@ -1274,8 +1786,7 @@ export class MetadataDiscovery {
1274
1786
  return;
1275
1787
  }
1276
1788
  if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1277
- const meta2 = this.metadata.get(prop.type);
1278
- prop.unsigned = meta2.getPrimaryProps().some(pk => {
1789
+ prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
1279
1790
  this.initUnsigned(pk);
1280
1791
  return pk.unsigned;
1281
1792
  });