@mikro-orm/core 7.0.0-dev.3 → 7.0.0-dev.300

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 (214) hide show
  1. package/EntityManager.d.ts +114 -63
  2. package/EntityManager.js +385 -310
  3. package/MikroORM.d.ts +44 -35
  4. package/MikroORM.js +109 -143
  5. package/README.md +3 -2
  6. package/cache/FileCacheAdapter.d.ts +1 -1
  7. package/cache/FileCacheAdapter.js +17 -8
  8. package/cache/GeneratedCacheAdapter.d.ts +0 -1
  9. package/cache/GeneratedCacheAdapter.js +0 -2
  10. package/cache/index.d.ts +0 -1
  11. package/cache/index.js +0 -1
  12. package/connections/Connection.d.ts +16 -7
  13. package/connections/Connection.js +23 -14
  14. package/drivers/DatabaseDriver.d.ts +25 -16
  15. package/drivers/DatabaseDriver.js +119 -36
  16. package/drivers/IDatabaseDriver.d.ts +125 -23
  17. package/entity/BaseEntity.d.ts +63 -4
  18. package/entity/BaseEntity.js +0 -3
  19. package/entity/Collection.d.ts +102 -31
  20. package/entity/Collection.js +446 -108
  21. package/entity/EntityAssigner.d.ts +1 -1
  22. package/entity/EntityAssigner.js +26 -18
  23. package/entity/EntityFactory.d.ts +13 -1
  24. package/entity/EntityFactory.js +106 -60
  25. package/entity/EntityHelper.d.ts +2 -2
  26. package/entity/EntityHelper.js +65 -20
  27. package/entity/EntityLoader.d.ts +13 -11
  28. package/entity/EntityLoader.js +257 -107
  29. package/entity/EntityRepository.d.ts +28 -8
  30. package/entity/EntityRepository.js +8 -2
  31. package/entity/PolymorphicRef.d.ts +12 -0
  32. package/entity/PolymorphicRef.js +18 -0
  33. package/entity/Reference.d.ts +9 -12
  34. package/entity/Reference.js +34 -9
  35. package/entity/WrappedEntity.d.ts +3 -8
  36. package/entity/WrappedEntity.js +3 -8
  37. package/entity/defineEntity.d.ts +753 -0
  38. package/entity/defineEntity.js +537 -0
  39. package/entity/index.d.ts +4 -2
  40. package/entity/index.js +4 -2
  41. package/entity/utils.d.ts +13 -1
  42. package/entity/utils.js +49 -4
  43. package/entity/validators.d.ts +11 -0
  44. package/entity/validators.js +65 -0
  45. package/enums.d.ts +23 -8
  46. package/enums.js +15 -1
  47. package/errors.d.ts +25 -9
  48. package/errors.js +67 -21
  49. package/events/EventManager.d.ts +2 -1
  50. package/events/EventManager.js +19 -11
  51. package/events/EventSubscriber.d.ts +3 -1
  52. package/hydration/Hydrator.js +1 -2
  53. package/hydration/ObjectHydrator.d.ts +4 -4
  54. package/hydration/ObjectHydrator.js +89 -36
  55. package/index.d.ts +2 -2
  56. package/index.js +1 -2
  57. package/logging/DefaultLogger.d.ts +1 -1
  58. package/logging/DefaultLogger.js +1 -0
  59. package/logging/SimpleLogger.d.ts +1 -1
  60. package/logging/colors.d.ts +1 -1
  61. package/logging/colors.js +7 -6
  62. package/logging/index.d.ts +1 -0
  63. package/logging/index.js +1 -0
  64. package/logging/inspect.d.ts +2 -0
  65. package/logging/inspect.js +11 -0
  66. package/metadata/EntitySchema.d.ts +53 -27
  67. package/metadata/EntitySchema.js +125 -52
  68. package/metadata/MetadataDiscovery.d.ts +64 -10
  69. package/metadata/MetadataDiscovery.js +823 -344
  70. package/metadata/MetadataProvider.d.ts +11 -2
  71. package/metadata/MetadataProvider.js +66 -2
  72. package/metadata/MetadataStorage.d.ts +13 -11
  73. package/metadata/MetadataStorage.js +71 -38
  74. package/metadata/MetadataValidator.d.ts +32 -9
  75. package/metadata/MetadataValidator.js +198 -42
  76. package/metadata/discover-entities.d.ts +5 -0
  77. package/metadata/discover-entities.js +40 -0
  78. package/metadata/index.d.ts +1 -1
  79. package/metadata/index.js +1 -1
  80. package/metadata/types.d.ts +577 -0
  81. package/metadata/types.js +1 -0
  82. package/naming-strategy/AbstractNamingStrategy.d.ts +16 -4
  83. package/naming-strategy/AbstractNamingStrategy.js +20 -2
  84. package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
  85. package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
  86. package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
  87. package/naming-strategy/MongoNamingStrategy.js +6 -6
  88. package/naming-strategy/NamingStrategy.d.ts +28 -4
  89. package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
  90. package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
  91. package/not-supported.d.ts +2 -0
  92. package/not-supported.js +4 -0
  93. package/package.json +22 -11
  94. package/platforms/ExceptionConverter.js +1 -1
  95. package/platforms/Platform.d.ts +14 -16
  96. package/platforms/Platform.js +24 -44
  97. package/serialization/EntitySerializer.d.ts +8 -3
  98. package/serialization/EntitySerializer.js +47 -27
  99. package/serialization/EntityTransformer.js +33 -21
  100. package/serialization/SerializationContext.d.ts +6 -6
  101. package/serialization/SerializationContext.js +16 -13
  102. package/types/ArrayType.d.ts +1 -1
  103. package/types/ArrayType.js +2 -3
  104. package/types/BigIntType.d.ts +9 -6
  105. package/types/BigIntType.js +4 -1
  106. package/types/BlobType.d.ts +0 -1
  107. package/types/BlobType.js +0 -3
  108. package/types/BooleanType.d.ts +2 -1
  109. package/types/BooleanType.js +3 -0
  110. package/types/DecimalType.d.ts +6 -4
  111. package/types/DecimalType.js +3 -3
  112. package/types/DoubleType.js +2 -2
  113. package/types/EnumArrayType.js +1 -2
  114. package/types/JsonType.d.ts +1 -1
  115. package/types/JsonType.js +7 -2
  116. package/types/TinyIntType.js +1 -1
  117. package/types/Type.d.ts +2 -4
  118. package/types/Type.js +3 -3
  119. package/types/Uint8ArrayType.d.ts +0 -1
  120. package/types/Uint8ArrayType.js +1 -4
  121. package/types/index.d.ts +1 -1
  122. package/typings.d.ts +469 -175
  123. package/typings.js +120 -45
  124. package/unit-of-work/ChangeSet.d.ts +4 -6
  125. package/unit-of-work/ChangeSet.js +4 -5
  126. package/unit-of-work/ChangeSetComputer.d.ts +3 -8
  127. package/unit-of-work/ChangeSetComputer.js +44 -21
  128. package/unit-of-work/ChangeSetPersister.d.ts +15 -12
  129. package/unit-of-work/ChangeSetPersister.js +113 -45
  130. package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
  131. package/unit-of-work/CommitOrderCalculator.js +13 -13
  132. package/unit-of-work/IdentityMap.d.ts +12 -0
  133. package/unit-of-work/IdentityMap.js +39 -1
  134. package/unit-of-work/UnitOfWork.d.ts +28 -3
  135. package/unit-of-work/UnitOfWork.js +315 -110
  136. package/utils/AbstractMigrator.d.ts +101 -0
  137. package/utils/AbstractMigrator.js +305 -0
  138. package/utils/AbstractSchemaGenerator.d.ts +5 -5
  139. package/utils/AbstractSchemaGenerator.js +32 -18
  140. package/utils/AsyncContext.d.ts +6 -0
  141. package/utils/AsyncContext.js +42 -0
  142. package/utils/Configuration.d.ts +801 -207
  143. package/utils/Configuration.js +150 -191
  144. package/utils/ConfigurationLoader.d.ts +1 -54
  145. package/utils/ConfigurationLoader.js +1 -352
  146. package/utils/Cursor.d.ts +3 -6
  147. package/utils/Cursor.js +27 -11
  148. package/utils/DataloaderUtils.d.ts +15 -5
  149. package/utils/DataloaderUtils.js +65 -17
  150. package/utils/EntityComparator.d.ts +21 -10
  151. package/utils/EntityComparator.js +243 -106
  152. package/utils/QueryHelper.d.ts +24 -6
  153. package/utils/QueryHelper.js +122 -26
  154. package/utils/RawQueryFragment.d.ts +60 -32
  155. package/utils/RawQueryFragment.js +69 -66
  156. package/utils/RequestContext.js +2 -2
  157. package/utils/TransactionContext.js +2 -2
  158. package/utils/TransactionManager.d.ts +65 -0
  159. package/utils/TransactionManager.js +223 -0
  160. package/utils/Utils.d.ts +15 -122
  161. package/utils/Utils.js +108 -376
  162. package/utils/clone.js +8 -23
  163. package/utils/env-vars.d.ts +7 -0
  164. package/utils/env-vars.js +97 -0
  165. package/utils/fs-utils.d.ts +34 -0
  166. package/utils/fs-utils.js +196 -0
  167. package/utils/index.d.ts +2 -3
  168. package/utils/index.js +2 -3
  169. package/utils/upsert-utils.d.ts +9 -4
  170. package/utils/upsert-utils.js +55 -4
  171. package/decorators/Check.d.ts +0 -3
  172. package/decorators/Check.js +0 -13
  173. package/decorators/CreateRequestContext.d.ts +0 -3
  174. package/decorators/CreateRequestContext.js +0 -32
  175. package/decorators/Embeddable.d.ts +0 -8
  176. package/decorators/Embeddable.js +0 -11
  177. package/decorators/Embedded.d.ts +0 -18
  178. package/decorators/Embedded.js +0 -18
  179. package/decorators/Entity.d.ts +0 -18
  180. package/decorators/Entity.js +0 -13
  181. package/decorators/Enum.d.ts +0 -9
  182. package/decorators/Enum.js +0 -16
  183. package/decorators/Filter.d.ts +0 -2
  184. package/decorators/Filter.js +0 -8
  185. package/decorators/Formula.d.ts +0 -5
  186. package/decorators/Formula.js +0 -15
  187. package/decorators/Indexed.d.ts +0 -17
  188. package/decorators/Indexed.js +0 -20
  189. package/decorators/ManyToMany.d.ts +0 -40
  190. package/decorators/ManyToMany.js +0 -14
  191. package/decorators/ManyToOne.d.ts +0 -30
  192. package/decorators/ManyToOne.js +0 -14
  193. package/decorators/OneToMany.d.ts +0 -28
  194. package/decorators/OneToMany.js +0 -17
  195. package/decorators/OneToOne.d.ts +0 -24
  196. package/decorators/OneToOne.js +0 -7
  197. package/decorators/PrimaryKey.d.ts +0 -9
  198. package/decorators/PrimaryKey.js +0 -20
  199. package/decorators/Property.d.ts +0 -250
  200. package/decorators/Property.js +0 -32
  201. package/decorators/Transactional.d.ts +0 -13
  202. package/decorators/Transactional.js +0 -28
  203. package/decorators/hooks.d.ts +0 -16
  204. package/decorators/hooks.js +0 -47
  205. package/decorators/index.d.ts +0 -17
  206. package/decorators/index.js +0 -17
  207. package/entity/ArrayCollection.d.ts +0 -116
  208. package/entity/ArrayCollection.js +0 -395
  209. package/entity/EntityValidator.d.ts +0 -19
  210. package/entity/EntityValidator.js +0 -150
  211. package/metadata/ReflectMetadataProvider.d.ts +0 -8
  212. package/metadata/ReflectMetadataProvider.js +0 -44
  213. package/utils/resolveContextProvider.d.ts +0 -10
  214. package/utils/resolveContextProvider.js +0 -28
@@ -1,22 +1,22 @@
1
- import { basename, extname } from 'node:path';
2
- import globby from 'globby';
3
1
  import { EntityMetadata, } from '../typings.js';
4
- import { Utils } from '../utils/Utils.js';
2
+ import { compareArrays, Utils } from '../utils/Utils.js';
3
+ import { QueryHelper } from '../utils/QueryHelper.js';
5
4
  import { MetadataValidator } from './MetadataValidator.js';
5
+ import { MetadataProvider } from './MetadataProvider.js';
6
6
  import { MetadataStorage } from './MetadataStorage.js';
7
7
  import { EntitySchema } from './EntitySchema.js';
8
8
  import { Cascade, ReferenceKind } from '../enums.js';
9
9
  import { MetadataError } from '../errors.js';
10
- import { ArrayType, BigIntType, BlobType, DecimalType, DoubleType, EnumArrayType, IntervalType, JsonType, t, Type, Uint8ArrayType, UnknownType, } from '../types/index.js';
10
+ import { t, Type } from '../types/index.js';
11
11
  import { colors } from '../logging/colors.js';
12
- import { raw, RawQueryFragment } from '../utils/RawQueryFragment.js';
12
+ import { raw, Raw } from '../utils/RawQueryFragment.js';
13
+ import { BaseEntity } from '../entity/BaseEntity.js';
13
14
  export class MetadataDiscovery {
14
15
  metadata;
15
16
  platform;
16
17
  config;
17
18
  namingStrategy;
18
19
  metadataProvider;
19
- cache;
20
20
  logger;
21
21
  schemaHelper;
22
22
  validator = new MetadataValidator();
@@ -27,13 +27,14 @@ export class MetadataDiscovery {
27
27
  this.config = config;
28
28
  this.namingStrategy = this.config.getNamingStrategy();
29
29
  this.metadataProvider = this.config.getMetadataProvider();
30
- this.cache = this.config.getMetadataCacheAdapter();
31
30
  this.logger = this.config.getLogger();
32
31
  this.schemaHelper = this.platform.getSchemaHelper();
33
32
  }
34
33
  async discover(preferTs = true) {
34
+ this.discovered.length = 0;
35
35
  const startTime = Date.now();
36
- 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}`);
37
38
  await this.findEntities(preferTs);
38
39
  for (const meta of this.discovered) {
39
40
  /* v8 ignore next */
@@ -47,10 +48,13 @@ export class MetadataDiscovery {
47
48
  await this.config.get('discovery').afterDiscovered?.(storage, this.platform);
48
49
  return storage;
49
50
  }
50
- discoverSync(preferTs = true) {
51
+ discoverSync() {
52
+ this.discovered.length = 0;
51
53
  const startTime = Date.now();
52
- this.logger.log('discovery', `ORM entity discovery started, using ${colors.cyan(this.metadataProvider.constructor.name)} in sync mode`);
53
- this.findEntities(preferTs, true);
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`);
56
+ const refs = this.config.get('entities');
57
+ this.discoverReferences(refs);
54
58
  for (const meta of this.discovered) {
55
59
  /* v8 ignore next */
56
60
  void this.config.get('discovery').onMetadata?.(meta, this.platform);
@@ -70,82 +74,124 @@ export class MetadataDiscovery {
70
74
  .sort((a, b) => b.root.name.localeCompare(a.root.name))
71
75
  .forEach(meta => {
72
76
  this.platform.validateMetadata(meta);
73
- discovered.set(meta.className, meta);
77
+ discovered.set(meta.class, meta);
74
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
+ }
75
85
  return discovered;
76
86
  }
87
+ initAccessors(meta) {
88
+ for (const prop of Object.values(meta.properties)) {
89
+ if (!prop.accessor || meta.properties[prop.accessor]) {
90
+ continue;
91
+ }
92
+ const desc = Object.getOwnPropertyDescriptor(meta.prototype, prop.name);
93
+ if (desc?.get || desc?.set) {
94
+ this.initRelation(prop);
95
+ this.initFieldName(prop);
96
+ const accessor = prop.name;
97
+ prop.name = typeof prop.accessor === 'string' ? prop.accessor : prop.name;
98
+ if (prop.accessor === true) {
99
+ prop.getter = prop.setter = true;
100
+ }
101
+ else {
102
+ prop.getter = prop.setter = false;
103
+ }
104
+ prop.accessor = accessor;
105
+ prop.serializedName ??= accessor;
106
+ Utils.renameKey(meta.properties, accessor, prop.name);
107
+ }
108
+ else {
109
+ const name = prop.name;
110
+ if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
111
+ prop.name = prop.accessor;
112
+ }
113
+ this.initRelation(prop);
114
+ this.initFieldName(prop);
115
+ prop.serializedName ??= prop.accessor;
116
+ prop.name = name;
117
+ }
118
+ }
119
+ }
77
120
  processDiscoveredEntities(discovered) {
78
121
  for (const meta of discovered) {
79
122
  let i = 1;
80
123
  Object.values(meta.properties).forEach(prop => meta.propertyOrder.set(prop.name, i++));
81
124
  Object.values(meta.properties).forEach(prop => this.initPolyEmbeddables(prop, discovered));
125
+ this.initAccessors(meta);
82
126
  }
83
127
  // ignore base entities (not annotated with @Entity)
84
128
  const filtered = discovered.filter(meta => meta.root.name);
85
129
  // sort so we discover entities first to get around issues with nested embeddables
86
130
  filtered.sort((a, b) => !a.embeddable === !b.embeddable ? 0 : (a.embeddable ? 1 : -1));
87
131
  filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
132
+ filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
88
133
  filtered.forEach(meta => this.defineBaseEntityProperties(meta));
89
- filtered.forEach(meta => this.metadata.set(meta.className, EntitySchema.fromMetadata(meta).init().meta));
134
+ filtered.forEach(meta => {
135
+ const newMeta = EntitySchema.fromMetadata(meta).init().meta;
136
+ return this.metadata.set(newMeta.class, newMeta);
137
+ });
90
138
  filtered.forEach(meta => this.initAutoincrement(meta));
91
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initEmbeddables(meta, prop)));
92
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initFactoryField(meta, prop)));
93
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initFieldName(prop)));
94
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initVersionProperty(meta, prop)));
95
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initCustomType(meta, prop)));
96
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initGeneratedColumn(meta, prop)));
139
+ const forEachProp = (cb) => {
140
+ filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
141
+ };
142
+ forEachProp((m, p) => this.initFactoryField(m, p));
143
+ forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
144
+ forEachProp((_m, p) => this.initRelation(p));
145
+ forEachProp((m, p) => this.initEmbeddables(m, p));
146
+ forEachProp((_m, p) => this.initFieldName(p));
147
+ filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
148
+ filtered.forEach(meta => this.computeTPTOwnProps(meta));
149
+ forEachProp((m, p) => this.initVersionProperty(m, p));
150
+ forEachProp((m, p) => this.initCustomType(m, p));
151
+ forEachProp((m, p) => this.initGeneratedColumn(m, p));
97
152
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
98
153
  filtered.forEach(meta => this.initCheckConstraints(meta));
99
- for (const meta of filtered) {
100
- for (const prop of Object.values(meta.properties)) {
101
- this.initDefaultValue(prop);
102
- this.inferTypeFromDefault(prop);
103
- this.initColumnType(prop);
104
- // change tracking on scalars is used only for "auto" flushMode
105
- if (this.config.get('flushMode') !== 'auto' && [ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind)) {
106
- prop.trackChanges = false;
107
- }
108
- }
109
- }
110
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initIndexes(meta, prop)));
154
+ forEachProp((_m, p) => {
155
+ this.initDefaultValue(p);
156
+ this.inferTypeFromDefault(p);
157
+ this.initRelation(p);
158
+ this.initColumnType(p);
159
+ });
160
+ forEachProp((m, p) => this.initIndexes(m, p));
111
161
  filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
112
- filtered.forEach(meta => this.findReferencingProperties(meta, filtered));
113
162
  for (const meta of filtered) {
114
163
  discovered.push(...this.processEntity(meta));
115
164
  }
116
165
  discovered.forEach(meta => meta.sync(true));
117
- const combinedCachePath = this.cache.combine?.();
118
- // override the path in the options, so we can log it from the CLI in `cache:generate` command
119
- if (combinedCachePath) {
120
- this.config.get('metadataCache').combined = combinedCachePath;
121
- }
166
+ this.metadataProvider.combineCache();
122
167
  return discovered.map(meta => {
123
- meta = this.metadata.get(meta.className);
168
+ meta = this.metadata.get(meta.class);
124
169
  meta.sync(true);
170
+ this.findReferencingProperties(meta, filtered);
171
+ if (meta.inheritanceType === 'tpt') {
172
+ this.computeTPTOwnProps(meta);
173
+ }
125
174
  return meta;
126
175
  });
127
176
  }
128
- findEntities(preferTs, sync = false) {
129
- this.discovered.length = 0;
130
- const options = this.config.get('discovery');
131
- const key = (preferTs && this.config.get('preferTs', Utils.detectTypeScriptSupport()) && this.config.get('entitiesTs').length > 0) ? 'entitiesTs' : 'entities';
132
- const paths = this.config.get(key).filter(item => Utils.isString(item));
133
- const refs = this.config.get(key).filter(item => !Utils.isString(item));
177
+ async findEntities(preferTs) {
178
+ const { entities, entitiesTs, baseDir } = this.config.getAll();
179
+ const targets = (preferTs && entitiesTs.length > 0) ? entitiesTs : entities;
180
+ const processed = [];
181
+ const paths = [];
182
+ for (const entity of targets) {
183
+ if (typeof entity === 'string') {
184
+ paths.push(entity);
185
+ }
186
+ else {
187
+ processed.push(entity);
188
+ }
189
+ }
134
190
  if (paths.length > 0) {
135
- if (sync || options.requireEntitiesArray) {
136
- throw new Error(`[requireEntitiesArray] Explicit list of entities is required, please use the 'entities' option.`);
137
- }
138
- return this.discoverDirectories(paths).then(() => {
139
- this.discoverReferences(refs);
140
- this.discoverMissingTargets();
141
- this.validator.validateDiscovered(this.discovered, options);
142
- return this.discovered;
143
- });
191
+ const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
192
+ processed.push(...await discoverEntities(paths, { baseDir }));
144
193
  }
145
- this.discoverReferences(refs);
146
- this.discoverMissingTargets();
147
- this.validator.validateDiscovered(this.discovered, options);
148
- return this.discovered;
194
+ return this.discoverReferences(processed);
149
195
  }
150
196
  discoverMissingTargets() {
151
197
  const unwrap = (type) => type
@@ -154,17 +200,22 @@ export class MetadataDiscovery {
154
200
  .replace(/\((.*)\)/, '$1'); // unwrap union types
155
201
  const missing = [];
156
202
  this.discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
157
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity && !this.discovered.find(m => m.className === Utils.className(prop.pivotEntity))) {
158
- const target = typeof prop.pivotEntity === 'function'
159
- ? prop.pivotEntity()
160
- : prop.pivotEntity;
161
- missing.push(target);
162
- }
163
- if (prop.kind !== ReferenceKind.SCALAR && !unwrap(prop.type).split(/ ?\| ?/).every(type => this.discovered.find(m => m.className === type))) {
164
- const target = typeof prop.entity === 'function'
203
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
204
+ const pivotEntity = prop.pivotEntity;
205
+ const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
206
+ ? pivotEntity()
207
+ : pivotEntity;
208
+ if (!this.discovered.find(m => m.className === Utils.className(target))) {
209
+ missing.push(target);
210
+ }
211
+ }
212
+ if (prop.kind !== ReferenceKind.SCALAR) {
213
+ const target = typeof prop.entity === 'function' && !prop.entity.prototype
165
214
  ? prop.entity()
166
215
  : prop.type;
167
- missing.push(...Utils.asArray(target));
216
+ if (!unwrap(prop.type).split(/ ?\| ?/).every(type => this.discovered.find(m => m.className === type))) {
217
+ missing.push(...Utils.asArray(target));
218
+ }
168
219
  }
169
220
  }));
170
221
  if (missing.length > 0) {
@@ -173,122 +224,107 @@ export class MetadataDiscovery {
173
224
  }
174
225
  tryDiscoverTargets(targets) {
175
226
  for (const target of targets) {
176
- if (typeof target === 'function' && target.name && !this.metadata.has(target.name)) {
177
- this.discoverReferences([target]);
178
- this.discoverMissingTargets();
179
- }
180
- }
181
- }
182
- async discoverDirectories(paths) {
183
- paths = paths.map(path => Utils.normalizePath(path));
184
- const files = await globby(paths, { cwd: Utils.normalizePath(this.config.get('baseDir')) });
185
- this.logger.log('discovery', `- processing ${colors.cyan('' + files.length)} files`);
186
- const found = [];
187
- for (const filepath of files) {
188
- const filename = basename(filepath);
189
- if (!filename.match(/\.[cm]?[jt]s$/) ||
190
- filename.endsWith('.js.map') ||
191
- filename.match(/\.d\.[cm]?ts/) ||
192
- filename.startsWith('.') ||
193
- filename.match(/index\.[cm]?[jt]s$/)) {
194
- this.logger.log('discovery', `- ignoring file ${filename}`);
195
- continue;
196
- }
197
- const name = this.namingStrategy.getClassName(filename);
198
- const path = Utils.normalizePath(this.config.get('baseDir'), filepath);
199
- const targets = await this.getEntityClassOrSchema(path, name);
200
- for (const target of targets) {
201
- if (!(target instanceof Function) && !(target instanceof EntitySchema)) {
202
- this.logger.log('discovery', `- ignoring file ${filename}`);
203
- continue;
227
+ const schema = target instanceof EntitySchema ? target : undefined;
228
+ const isDiscoverable = typeof target === 'function' || schema;
229
+ if (isDiscoverable && target.name) {
230
+ // Get the actual class for EntitySchema, or use target directly for classes
231
+ const targetClass = schema ? schema.meta.class : target;
232
+ if (!this.metadata.has(targetClass)) {
233
+ this.discoverReferences([target], false);
234
+ this.discoverMissingTargets();
204
235
  }
205
- const entity = this.prepare(target);
206
- const schema = this.getSchema(entity, path);
207
- const meta = schema.init().meta;
208
- this.metadata.set(meta.className, meta);
209
- found.push([schema, path]);
210
236
  }
211
237
  }
212
- for (const [schema, path] of found) {
213
- this.discoverEntity(schema, path);
214
- }
215
238
  }
216
- discoverReferences(refs) {
239
+ discoverReferences(refs, validate = true) {
217
240
  const found = [];
218
241
  for (const entity of refs) {
219
- const schema = this.getSchema(this.prepare(entity));
242
+ if (typeof entity === 'string') {
243
+ throw new Error('Folder based discovery requires the async `MikroORM.init()` method.');
244
+ }
245
+ const schema = this.getSchema(entity);
220
246
  const meta = schema.init().meta;
221
- this.metadata.set(meta.className, meta);
247
+ this.metadata.set(meta.class, meta);
222
248
  found.push(schema);
223
249
  }
224
250
  // discover parents (base entities) automatically
225
251
  for (const meta of this.metadata) {
226
252
  let parent = meta.extends;
227
- if (parent instanceof EntitySchema && !this.metadata.has(parent.meta.className)) {
228
- this.discoverReferences([parent]);
253
+ if (parent instanceof EntitySchema && !this.metadata.has(parent.init().meta.class)) {
254
+ this.discoverReferences([parent], false);
255
+ }
256
+ if (typeof parent === 'function' && parent.name && !this.metadata.has(parent)) {
257
+ this.discoverReferences([parent], false);
229
258
  }
230
- /* v8 ignore next 3 */
259
+ /* v8 ignore next */
231
260
  if (!meta.class) {
232
261
  continue;
233
262
  }
234
263
  parent = Object.getPrototypeOf(meta.class);
235
- if (parent.name !== '' && !this.metadata.has(parent.name)) {
236
- this.discoverReferences([parent]);
264
+ // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
265
+ if (parent.name !== '' && parent.name !== meta.className && !this.metadata.has(parent) && parent !== BaseEntity) {
266
+ this.discoverReferences([parent], false);
237
267
  }
238
268
  }
239
269
  for (const schema of found) {
240
270
  this.discoverEntity(schema);
241
271
  }
272
+ this.discoverMissingTargets();
273
+ if (validate) {
274
+ this.validator.validateDiscovered(this.discovered, this.config.get('discovery'));
275
+ }
242
276
  return this.discovered.filter(meta => found.find(m => m.name === meta.className));
243
277
  }
244
- reset(className) {
245
- const exists = this.discovered.findIndex(m => m.className === className);
278
+ reset(entityName) {
279
+ const exists = this.discovered.findIndex(m => m.class === entityName || m.className === Utils.className(entityName));
246
280
  if (exists !== -1) {
247
- this.metadata.reset(this.discovered[exists].className);
281
+ this.metadata.reset(this.discovered[exists].class);
248
282
  this.discovered.splice(exists, 1);
249
283
  }
250
284
  }
251
- prepare(entity) {
252
- /* v8 ignore next 3 */
253
- if ('schema' in entity && entity.schema instanceof EntitySchema) {
254
- return entity.schema;
255
- }
285
+ getSchema(entity) {
256
286
  if (EntitySchema.REGISTRY.has(entity)) {
257
- return EntitySchema.REGISTRY.get(entity);
287
+ entity = EntitySchema.REGISTRY.get(entity);
258
288
  }
259
- return entity;
260
- }
261
- getSchema(entity, filepath) {
262
289
  if (entity instanceof EntitySchema) {
263
- if (filepath) {
264
- // initialize global metadata for given entity
265
- MetadataStorage.getMetadata(entity.meta.className, filepath);
266
- }
267
- return entity;
290
+ const meta = Utils.copy(entity.meta, false);
291
+ return EntitySchema.fromMetadata(meta);
268
292
  }
269
293
  const path = entity[MetadataStorage.PATH_SYMBOL];
270
294
  if (path) {
271
295
  const meta = Utils.copy(MetadataStorage.getMetadata(entity.name, path), false);
272
- meta.path = Utils.relativePath(path, this.config.get('baseDir'));
273
- this.metadata.set(entity.name, meta);
296
+ meta.path = path;
297
+ this.metadata.set(entity, meta);
274
298
  }
275
- const exists = this.metadata.has(entity.name);
276
- const meta = this.metadata.get(entity.name, true);
299
+ const exists = this.metadata.has(entity);
300
+ const meta = this.metadata.get(entity, true);
277
301
  meta.abstract ??= !(exists && meta.name);
278
302
  const schema = EntitySchema.fromMetadata(meta);
279
303
  schema.setClass(entity);
280
304
  return schema;
281
305
  }
282
- discoverEntity(schema, path) {
283
- this.logger.log('discovery', `- processing entity ${colors.cyan(schema.meta.className)}${colors.grey(path ? ` (${path})` : '')}`);
306
+ getRootEntity(meta) {
307
+ const base = meta.extends && this.metadata.find(meta.extends);
308
+ if (!base || base === meta) { // make sure we do not fall into infinite loop
309
+ return meta;
310
+ }
311
+ const root = this.getRootEntity(base);
312
+ // For STI or TPT, use the root entity.
313
+ // Check both `inheritanceType` (set during discovery) and raw `inheritance` option (set before discovery).
314
+ if (root.discriminatorColumn || root.inheritanceType || root.inheritance === 'tpt') {
315
+ return root;
316
+ }
317
+ return meta;
318
+ }
319
+ discoverEntity(schema) {
284
320
  const meta = schema.meta;
285
- const root = Utils.getRootEntity(this.metadata, meta);
286
- schema.meta.path = Utils.relativePath(path || meta.path, this.config.get('baseDir'));
287
- const cache = this.metadataProvider.useCache() && meta.path && this.cache.get(meta.className + extname(meta.path));
321
+ const path = meta.path;
322
+ this.logger.log('discovery', `- processing entity ${colors.cyan(meta.className)}${colors.grey(path ? ` (${path})` : '')}`);
323
+ const root = this.getRootEntity(meta);
324
+ schema.meta.path = meta.path;
325
+ const cache = this.metadataProvider.getCachedMetadata(meta, root);
288
326
  if (cache) {
289
327
  this.logger.log('discovery', `- using cached metadata for entity ${colors.cyan(meta.className)}`);
290
- this.metadataProvider.loadFromCache(meta, cache);
291
- meta.root = root;
292
328
  this.discovered.push(meta);
293
329
  return;
294
330
  }
@@ -297,50 +333,18 @@ export class MetadataDiscovery {
297
333
  this.inferDefaultValue(meta, prop);
298
334
  }
299
335
  // if the definition is using EntitySchema we still want it to go through the metadata provider to validate no types are missing
300
- this.metadataProvider.loadEntityMetadata(meta, meta.className);
301
- if (!meta.collection && meta.name) {
336
+ this.metadataProvider.loadEntityMetadata(meta);
337
+ if (!meta.tableName && meta.name) {
302
338
  const entityName = root.discriminatorColumn ? root.name : meta.name;
303
- meta.collection = this.namingStrategy.classToTableName(entityName);
339
+ meta.tableName = this.namingStrategy.classToTableName(entityName);
304
340
  }
305
- delete meta.root; // to allow caching (as root can contain cycles)
306
- this.saveToCache(meta);
341
+ this.metadataProvider.saveToCache(meta);
307
342
  meta.root = root;
308
343
  this.discovered.push(meta);
309
344
  }
310
- saveToCache(meta) {
311
- if (!this.metadataProvider.useCache()) {
312
- return;
313
- }
314
- const copy = Utils.copy(meta, false);
315
- for (const prop of copy.props) {
316
- if (Type.isMappedType(prop.type)) {
317
- Reflect.deleteProperty(prop, 'type');
318
- Reflect.deleteProperty(prop, 'customType');
319
- }
320
- if (prop.default) {
321
- const raw = RawQueryFragment.getKnownFragment(prop.default);
322
- if (raw) {
323
- prop.defaultRaw ??= this.platform.formatQuery(raw.sql, raw.params);
324
- Reflect.deleteProperty(prop, 'default');
325
- }
326
- }
327
- Reflect.deleteProperty(prop, 'targetMeta');
328
- }
329
- [
330
- 'prototype', 'props', 'referencingProperties', 'propertyOrder', 'relations',
331
- 'concurrencyCheckKeys', 'checks',
332
- ].forEach(key => delete copy[key]);
333
- // base entity without properties might not have path, but nothing to cache there
334
- if (meta.path) {
335
- this.cache.set(meta.className + extname(meta.path), copy, meta.path);
336
- }
337
- }
338
345
  initNullability(prop) {
339
- if (prop.kind === ReferenceKind.MANY_TO_ONE) {
340
- return Utils.defaultValue(prop, 'nullable', prop.optional || prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL));
341
- }
342
346
  if (prop.kind === ReferenceKind.ONE_TO_ONE) {
343
- return Utils.defaultValue(prop, 'nullable', prop.optional || !prop.owner || prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL));
347
+ return Utils.defaultValue(prop, 'nullable', prop.optional || !prop.owner);
344
348
  }
345
349
  return Utils.defaultValue(prop, 'nullable', prop.optional);
346
350
  }
@@ -364,6 +368,12 @@ export class MetadataDiscovery {
364
368
  if (!prop.joinColumns || !prop.columnTypes || prop.ownColumns || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
365
369
  continue;
366
370
  }
371
+ // For polymorphic relations, ownColumns should include all fieldNames
372
+ // (discriminator + ID columns) since they are all owned by this relation
373
+ if (prop.polymorphic) {
374
+ prop.ownColumns = prop.fieldNames;
375
+ continue;
376
+ }
367
377
  if (prop.joinColumns.length > 1) {
368
378
  prop.ownColumns = prop.joinColumns.filter(col => {
369
379
  return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
@@ -375,7 +385,7 @@ export class MetadataDiscovery {
375
385
  if (prop.joinColumns.length !== prop.columnTypes.length) {
376
386
  prop.columnTypes = prop.joinColumns.flatMap(field => {
377
387
  const matched = meta.props.find(p => p.fieldNames?.includes(field));
378
- /* v8 ignore next 3 */
388
+ /* v8 ignore next */
379
389
  if (!matched) {
380
390
  throw MetadataError.fromWrongForeignKey(meta, prop, 'columnTypes');
381
391
  }
@@ -394,30 +404,30 @@ export class MetadataDiscovery {
394
404
  if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
395
405
  prop.fieldNames = [this.namingStrategy.propertyToColumnName(prop.name, object)];
396
406
  }
397
- else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
407
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
398
408
  prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
399
409
  }
400
410
  else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
401
411
  prop.fieldNames = this.initManyToManyFieldName(prop, prop.name);
402
412
  }
403
413
  }
404
- initManyToOneFieldName(prop, name) {
405
- const meta2 = this.metadata.get(prop.type);
414
+ initManyToOneFieldName(prop, name, tableName) {
415
+ const meta2 = prop.targetMeta;
406
416
  const ret = [];
407
417
  for (const primaryKey of meta2.primaryKeys) {
408
418
  this.initFieldName(meta2.properties[primaryKey]);
409
419
  for (const fieldName of meta2.properties[primaryKey].fieldNames) {
410
- ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK));
420
+ ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK, tableName));
411
421
  }
412
422
  }
413
423
  return ret;
414
424
  }
415
425
  initManyToManyFieldName(prop, name) {
416
- const meta2 = this.metadata.get(prop.type);
426
+ const meta2 = prop.targetMeta;
417
427
  return meta2.primaryKeys.map(() => this.namingStrategy.propertyToColumnName(name));
418
428
  }
419
429
  initManyToManyFields(meta, prop) {
420
- const meta2 = this.metadata.get(prop.type);
430
+ const meta2 = prop.targetMeta;
421
431
  Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
422
432
  const pivotMeta = this.metadata.find(prop.pivotEntity);
423
433
  const props = Object.values(pivotMeta?.properties ?? {});
@@ -438,35 +448,77 @@ export class MetadataDiscovery {
438
448
  prop.inverseJoinColumns ??= second.fieldNames;
439
449
  }
440
450
  if (!prop.pivotTable && prop.owner && this.platform.usesPivotTable()) {
441
- prop.pivotTable = this.namingStrategy.joinTableName(meta.tableName, meta2.tableName, prop.name);
451
+ prop.pivotTable = this.namingStrategy.joinTableName(meta.className, meta2.tableName, prop.name, meta.tableName);
442
452
  }
443
453
  if (prop.mappedBy) {
444
454
  const prop2 = meta2.properties[prop.mappedBy];
445
455
  this.initManyToManyFields(meta2, prop2);
446
456
  prop.pivotTable = prop2.pivotTable;
447
- prop.pivotEntity = prop2.pivotEntity ?? prop2.pivotTable;
457
+ prop.pivotEntity = prop2.pivotEntity;
448
458
  prop.fixedOrder = prop2.fixedOrder;
449
459
  prop.fixedOrderColumn = prop2.fixedOrderColumn;
450
460
  prop.joinColumns = prop2.inverseJoinColumns;
451
461
  prop.inverseJoinColumns = prop2.joinColumns;
462
+ prop.polymorphic = prop2.polymorphic;
463
+ prop.discriminator = prop2.discriminator;
464
+ prop.discriminatorColumn = prop2.discriminatorColumn;
465
+ prop.discriminatorValue = prop2.discriminatorValue;
452
466
  }
453
467
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
454
- prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
455
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
468
+ // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
469
+ if (prop.polymorphic && prop.discriminator) {
470
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
471
+ }
472
+ else {
473
+ const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
474
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK, ownerTableName));
475
+ }
476
+ const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
477
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
478
+ }
479
+ isExplicitTableName(meta) {
480
+ return meta.tableName !== this.namingStrategy.classToTableName(meta.className);
456
481
  }
457
482
  initManyToOneFields(prop) {
458
- const meta2 = this.metadata.get(prop.type);
459
- const fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
460
- Utils.defaultValue(prop, 'referencedTableName', meta2.collection);
483
+ if (prop.polymorphic && prop.polymorphTargets) {
484
+ const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
485
+ const idColumns = fieldNames1.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
486
+ prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
487
+ prop.joinColumns ??= idColumns;
488
+ prop.referencedColumnNames ??= fieldNames1;
489
+ prop.referencedTableName ??= prop.targetMeta.tableName;
490
+ return;
491
+ }
492
+ const meta2 = prop.targetMeta;
493
+ let fieldNames;
494
+ // If targetKey is specified, use that property's field names instead of PKs
495
+ if (prop.targetKey) {
496
+ const targetProp = meta2.properties[prop.targetKey];
497
+ fieldNames = targetProp.fieldNames;
498
+ }
499
+ else {
500
+ fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
501
+ }
502
+ Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
461
503
  if (!prop.joinColumns) {
462
504
  prop.joinColumns = fieldNames.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
463
505
  }
464
506
  if (!prop.referencedColumnNames) {
465
507
  prop.referencedColumnNames = fieldNames;
466
508
  }
509
+ // Relations to composite PK targets need cascade update by default,
510
+ // since composite PKs are more likely to have mutable components
511
+ if (meta2.compositePK) {
512
+ prop.updateRule ??= 'cascade';
513
+ }
514
+ // Nullable relations default to 'set null' on delete - when the referenced
515
+ // entity is deleted, set the FK to null rather than failing
516
+ if (prop.nullable) {
517
+ prop.deleteRule ??= 'set null';
518
+ }
467
519
  }
468
520
  initOneToManyFields(prop) {
469
- const meta2 = this.metadata.get(prop.type);
521
+ const meta2 = prop.targetMeta;
470
522
  if (!prop.joinColumns) {
471
523
  prop.joinColumns = [this.namingStrategy.joinColumnName(prop.name)];
472
524
  }
@@ -479,12 +531,17 @@ export class MetadataDiscovery {
479
531
  const pks = Object.values(meta.properties).filter(prop => prop.primary);
480
532
  meta.primaryKeys = pks.map(prop => prop.name);
481
533
  meta.compositePK = pks.length > 1;
482
- // FK used as PK, we need to cascade
483
- if (pks.length === 1 && pks[0].kind !== ReferenceKind.SCALAR) {
484
- pks[0].deleteRule ??= 'cascade';
534
+ // FK used as PK, we need to cascade - applies to both single FK-as-PK
535
+ // and composite PKs where all PKs are FKs (e.g., pivot-like entities)
536
+ const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
537
+ if (fkPks.length > 0 && fkPks.length === pks.length) {
538
+ for (const pk of fkPks) {
539
+ pk.deleteRule ??= 'cascade';
540
+ pk.updateRule ??= 'cascade';
541
+ }
485
542
  }
486
543
  meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
487
- this.validator.validateEntityDefinition(this.metadata, meta.className, this.config.get('discovery'));
544
+ this.validator.validateEntityDefinition(this.metadata, meta.class, this.config.get('discovery'));
488
545
  for (const prop of Object.values(meta.properties)) {
489
546
  this.initNullability(prop);
490
547
  this.applyNamingStrategy(meta, prop);
@@ -497,15 +554,21 @@ export class MetadataDiscovery {
497
554
  }
498
555
  this.initOwnColumns(meta);
499
556
  meta.simplePK = pks.length === 1 && pks[0].kind === ReferenceKind.SCALAR && !pks[0].customType && pks[0].runtimeType !== 'Date';
500
- meta.serializedPrimaryKey = this.platform.getSerializedPrimaryKeyField(meta.primaryKeys[0]);
501
- const serializedPKProp = meta.properties[meta.serializedPrimaryKey];
502
- if (serializedPKProp && meta.serializedPrimaryKey !== meta.primaryKeys[0]) {
503
- serializedPKProp.persist = false;
557
+ meta.serializedPrimaryKey ??= meta.props.find(prop => prop.serializedPrimaryKey)?.name;
558
+ if (meta.serializedPrimaryKey && meta.serializedPrimaryKey !== meta.primaryKeys[0]) {
559
+ meta.properties[meta.serializedPrimaryKey].persist ??= false;
504
560
  }
505
561
  if (this.platform.usesPivotTable()) {
506
562
  return Object.values(meta.properties)
507
563
  .filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
508
- .map(prop => this.definePivotTableEntity(meta, prop));
564
+ .map(prop => {
565
+ const pivotMeta = this.definePivotTableEntity(meta, prop);
566
+ prop.pivotEntity = pivotMeta.class;
567
+ if (prop.inversedBy) {
568
+ prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
569
+ }
570
+ return pivotMeta;
571
+ });
509
572
  }
510
573
  return [];
511
574
  }
@@ -522,8 +585,11 @@ export class MetadataDiscovery {
522
585
  ['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
523
586
  const value = prop[type];
524
587
  if (value instanceof Function) {
525
- const meta2 = this.metadata.get(prop.type);
588
+ const meta2 = prop.targetMeta ?? this.metadata.get(prop.target);
526
589
  prop[type] = value(meta2.properties)?.name;
590
+ if (type === 'pivotEntity' && value) {
591
+ prop[type] = value(meta2.properties);
592
+ }
527
593
  if (prop[type] == null) {
528
594
  throw MetadataError.fromWrongReference(meta, prop, type);
529
595
  }
@@ -539,9 +605,9 @@ export class MetadataDiscovery {
539
605
  }
540
606
  else if (fks.length >= 2) {
541
607
  [first, second] = fks;
542
- /* v8 ignore next 3 */
543
608
  }
544
609
  else {
610
+ /* v8 ignore next */
545
611
  return [];
546
612
  }
547
613
  // wrong FK order, first FK needs to point to the owning side
@@ -555,7 +621,9 @@ export class MetadataDiscovery {
555
621
  return [first, second];
556
622
  }
557
623
  definePivotTableEntity(meta, prop) {
558
- const pivotMeta = this.metadata.find(prop.pivotEntity);
624
+ const pivotMeta = prop.pivotEntity
625
+ ? this.metadata.find(prop.pivotEntity)
626
+ : this.metadata.getByClassName(prop.pivotTable, false);
559
627
  // ensure inverse side exists so we can join it when populating via pivot tables
560
628
  if (!prop.inversedBy && prop.targetMeta) {
561
629
  const inverseName = `${meta.className}_${prop.name}__inverse`;
@@ -564,6 +632,8 @@ export class MetadataDiscovery {
564
632
  name: inverseName,
565
633
  kind: ReferenceKind.MANY_TO_MANY,
566
634
  type: meta.className,
635
+ target: meta.class,
636
+ targetMeta: meta,
567
637
  mappedBy: prop.name,
568
638
  pivotEntity: prop.pivotEntity,
569
639
  pivotTable: prop.pivotTable,
@@ -572,55 +642,190 @@ export class MetadataDiscovery {
572
642
  };
573
643
  this.applyNamingStrategy(prop.targetMeta, inverseProp);
574
644
  this.initCustomType(prop.targetMeta, inverseProp);
575
- this.initRelation(inverseProp);
576
645
  prop.targetMeta.properties[inverseName] = inverseProp;
577
646
  }
578
647
  if (pivotMeta) {
648
+ prop.pivotEntity = pivotMeta.class;
579
649
  this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
650
+ if (prop.polymorphic && prop.discriminatorValue) {
651
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
652
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
653
+ // For composite PK entities sharing a polymorphic pivot table,
654
+ // we need to add columns for each entity type's PKs
655
+ this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
656
+ // Add virtual M:1 relation for this polymorphic owner (for join loading)
657
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
658
+ if (!pivotMeta.properties[ownerRelationName]) {
659
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
660
+ }
661
+ }
580
662
  return pivotMeta;
581
663
  }
582
- const exists = this.metadata.find(prop.pivotTable);
583
- if (exists) {
584
- prop.pivotEntity = exists.className;
585
- return exists;
586
- }
587
664
  let tableName = prop.pivotTable;
588
665
  let schemaName;
589
666
  if (prop.pivotTable.includes('.')) {
590
667
  [schemaName, tableName] = prop.pivotTable.split('.');
591
668
  }
592
669
  schemaName ??= meta.schema;
593
- const targetType = prop.targetMeta.className;
594
- const data = new EntityMetadata({
670
+ const targetMeta = prop.targetMeta;
671
+ const targetType = targetMeta.className;
672
+ const pivotMeta2 = new EntityMetadata({
595
673
  name: prop.pivotTable,
596
674
  className: prop.pivotTable,
597
675
  collection: tableName,
598
676
  schema: schemaName,
599
677
  pivotTable: true,
600
678
  });
601
- prop.pivotEntity = data.className;
679
+ prop.pivotEntity = pivotMeta2.class;
602
680
  if (prop.fixedOrder) {
603
- const primaryProp = this.defineFixedOrderProperty(prop, targetType);
604
- data.properties[primaryProp.name] = primaryProp;
681
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
682
+ pivotMeta2.properties[primaryProp.name] = primaryProp;
605
683
  }
606
684
  else {
607
- data.compositePK = true;
685
+ pivotMeta2.compositePK = true;
608
686
  }
609
687
  // handle self-referenced m:n with same default field names
610
688
  if (meta.className === targetType && prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
611
- prop.joinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(meta.className + '_1', name, meta.compositePK));
612
- prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(meta.className + '_2', name, meta.compositePK));
689
+ // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
690
+ const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
691
+ prop.joinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
692
+ prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
613
693
  if (prop.inversedBy) {
614
- const prop2 = this.metadata.get(targetType).properties[prop.inversedBy];
694
+ const prop2 = targetMeta.properties[prop.inversedBy];
615
695
  prop2.inverseJoinColumns = prop.joinColumns;
616
696
  prop2.joinColumns = prop.inverseJoinColumns;
617
697
  }
698
+ // propagate updated joinColumns to all child entities that inherit this property (STI)
699
+ for (const childMeta of this.discovered.filter(m => m.root === meta && m !== meta)) {
700
+ const childProp = childMeta.properties[prop.name];
701
+ if (childProp) {
702
+ childProp.joinColumns = prop.joinColumns;
703
+ childProp.inverseJoinColumns = prop.inverseJoinColumns;
704
+ }
705
+ }
706
+ }
707
+ // For polymorphic M:N, create discriminator column and polymorphic FK
708
+ if (prop.polymorphic && prop.discriminatorColumn) {
709
+ this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
710
+ }
711
+ else {
712
+ pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, targetType + '_inverse', true, meta.className === targetType);
713
+ pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetMeta.class, meta.name + '_owner', false, meta.className === targetType);
714
+ }
715
+ return this.metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
716
+ }
717
+ /**
718
+ * Create a scalar property for a pivot table column.
719
+ */
720
+ createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
721
+ return {
722
+ name,
723
+ fieldNames,
724
+ columnTypes,
725
+ type: options.type ?? 'number',
726
+ kind: ReferenceKind.SCALAR,
727
+ primary: options.primary ?? false,
728
+ nullable: options.nullable ?? true,
729
+ ...(options.persist !== undefined && { persist: options.persist }),
730
+ };
731
+ }
732
+ /**
733
+ * Get column types for an entity's primary keys, initializing them if needed.
734
+ */
735
+ getPrimaryKeyColumnTypes(meta) {
736
+ const columnTypes = [];
737
+ for (const pk of meta.primaryKeys) {
738
+ const pkProp = meta.properties[pk];
739
+ this.initCustomType(meta, pkProp);
740
+ this.initColumnType(pkProp);
741
+ columnTypes.push(...pkProp.columnTypes);
742
+ }
743
+ return columnTypes;
744
+ }
745
+ /**
746
+ * Add missing FK columns for a polymorphic entity to an existing pivot table.
747
+ */
748
+ addPolymorphicPivotColumns(pivotMeta, meta, prop) {
749
+ const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
750
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
751
+ for (let i = 0; i < prop.joinColumns.length; i++) {
752
+ const joinColumn = prop.joinColumns[i];
753
+ if (!existingFieldNames.has(joinColumn)) {
754
+ pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
755
+ }
756
+ }
757
+ }
758
+ /**
759
+ * Define properties for a polymorphic pivot table.
760
+ */
761
+ definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
762
+ const discriminatorColumn = prop.discriminatorColumn;
763
+ const isCompositePK = meta.compositePK;
764
+ // For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
765
+ if (isCompositePK && !prop.fixedOrder) {
766
+ prop.fixedOrder = true;
767
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
768
+ pivotMeta.properties[primaryProp.name] = primaryProp;
769
+ pivotMeta.compositePK = false;
770
+ }
771
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
772
+ this.initFieldName(discriminatorProp);
773
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
774
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
775
+ if (isCompositePK) {
776
+ // Create separate properties for each PK column (nullable for other entity types)
777
+ for (let i = 0; i < prop.joinColumns.length; i++) {
778
+ pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [columnTypes[i]]);
779
+ }
780
+ // Virtual property combining all columns (for compatibility)
781
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
618
782
  }
619
- data.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.className, targetType + '_inverse', true, meta.className === targetType);
620
- data.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetType, meta.name + '_owner', false, meta.className === targetType);
621
- return this.metadata.set(data.className, data);
783
+ else {
784
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
785
+ }
786
+ pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
787
+ // Create virtual M:1 relation to the polymorphic owner for single-query join loading
788
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
789
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
790
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
791
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
792
+ }
793
+ /**
794
+ * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
795
+ * This enables single-query join loading for inverse-side polymorphic M:N.
796
+ */
797
+ definePolymorphicOwnerRelation(prop, name, targetMeta) {
798
+ const ret = {
799
+ name,
800
+ type: targetMeta.className,
801
+ target: targetMeta.class,
802
+ kind: ReferenceKind.MANY_TO_ONE,
803
+ nullable: true,
804
+ owner: true,
805
+ primary: false,
806
+ createForeignKeyConstraint: false,
807
+ persist: false,
808
+ index: false,
809
+ };
810
+ ret.targetMeta = targetMeta;
811
+ ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
812
+ ret.referencedColumnNames = [];
813
+ ret.inverseJoinColumns = [];
814
+ for (const primaryKey of targetMeta.primaryKeys) {
815
+ const pkProp = targetMeta.properties[primaryKey];
816
+ ret.referencedColumnNames.push(...pkProp.fieldNames);
817
+ ret.inverseJoinColumns.push(...pkProp.fieldNames);
818
+ ret.length = pkProp.length;
819
+ ret.precision = pkProp.precision;
820
+ ret.scale = pkProp.scale;
821
+ }
822
+ const schema = targetMeta.schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
823
+ ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
824
+ this.initColumnType(ret);
825
+ this.initRelation(ret);
826
+ return ret;
622
827
  }
623
- defineFixedOrderProperty(prop, targetType) {
828
+ defineFixedOrderProperty(prop, targetMeta) {
624
829
  const pk = prop.fixedOrderColumn || this.namingStrategy.referenceColumnName();
625
830
  const primaryProp = {
626
831
  name: pk,
@@ -634,7 +839,7 @@ export class MetadataDiscovery {
634
839
  this.initColumnType(primaryProp);
635
840
  prop.fixedOrderColumn = pk;
636
841
  if (prop.inversedBy) {
637
- const prop2 = this.metadata.get(targetType).properties[prop.inversedBy];
842
+ const prop2 = targetMeta.properties[prop.inversedBy];
638
843
  prop2.fixedOrder = true;
639
844
  prop2.fixedOrderColumn = pk;
640
845
  }
@@ -643,7 +848,8 @@ export class MetadataDiscovery {
643
848
  definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
644
849
  const ret = {
645
850
  name,
646
- type,
851
+ type: Utils.className(type),
852
+ target: type,
647
853
  kind: ReferenceKind.MANY_TO_ONE,
648
854
  cascade: [Cascade.ALL],
649
855
  fixedOrder: prop.fixedOrder,
@@ -653,11 +859,11 @@ export class MetadataDiscovery {
653
859
  autoincrement: false,
654
860
  updateRule: prop.updateRule,
655
861
  deleteRule: prop.deleteRule,
862
+ createForeignKeyConstraint: prop.createForeignKeyConstraint,
656
863
  };
657
- if (selfReferencing && !this.platform.supportsMultipleCascadePaths()) {
658
- ret.updateRule ??= 'no action';
659
- ret.deleteRule ??= 'no action';
660
- }
864
+ const defaultRule = selfReferencing && !this.platform.supportsMultipleCascadePaths() ? 'no action' : 'cascade';
865
+ ret.updateRule ??= defaultRule;
866
+ ret.deleteRule ??= defaultRule;
661
867
  const meta = this.metadata.get(type);
662
868
  ret.targetMeta = meta;
663
869
  ret.joinColumns = [];
@@ -700,7 +906,7 @@ export class MetadataDiscovery {
700
906
  Object.values(meta.properties)
701
907
  .filter(prop => prop.kind !== ReferenceKind.SCALAR && !prop.owner && prop.mappedBy)
702
908
  .forEach(prop => {
703
- const meta2 = this.metadata.get(prop.type);
909
+ const meta2 = prop.targetMeta;
704
910
  const prop2 = meta2.properties[prop.mappedBy];
705
911
  if (prop2 && !prop2.inversedBy) {
706
912
  prop2.inversedBy = prop.name;
@@ -708,7 +914,7 @@ export class MetadataDiscovery {
708
914
  });
709
915
  }
710
916
  defineBaseEntityProperties(meta) {
711
- const base = meta.extends && this.metadata.get(Utils.className(meta.extends));
917
+ const base = meta.extends && this.metadata.get(meta.extends);
712
918
  if (!base || base === meta) { // make sure we do not fall into infinite loop
713
919
  return 0;
714
920
  }
@@ -739,12 +945,9 @@ export class MetadataDiscovery {
739
945
  Utils.keys(base.hooks).forEach(type => {
740
946
  meta.hooks[type] = Utils.unique([...base.hooks[type], ...(meta.hooks[type] || [])]);
741
947
  });
742
- if (meta.constructorParams.length === 0 && base.constructorParams.length > 0) {
948
+ if ((meta.constructorParams?.length ?? 0) === 0 && (base.constructorParams?.length ?? 0) > 0) {
743
949
  meta.constructorParams = [...base.constructorParams];
744
950
  }
745
- if (meta.toJsonParams.length === 0 && base.toJsonParams.length > 0) {
746
- meta.toJsonParams = [...base.toJsonParams];
747
- }
748
951
  return order;
749
952
  }
750
953
  initPolyEmbeddables(embeddedProp, discovered, visited = new Set()) {
@@ -765,6 +968,7 @@ export class MetadataDiscovery {
765
968
  delete prop.default;
766
969
  if (properties[prop.name] && properties[prop.name].type !== prop.type) {
767
970
  properties[prop.name].type = `${properties[prop.name].type} | ${prop.type}`;
971
+ properties[prop.name].runtimeType = 'any';
768
972
  return properties[prop.name];
769
973
  }
770
974
  return properties[prop.name] = prop;
@@ -800,6 +1004,54 @@ export class MetadataDiscovery {
800
1004
  polymorphs.forEach(meta => meta.root = embeddable);
801
1005
  }
802
1006
  }
1007
+ initPolymorphicRelation(meta, prop, discovered) {
1008
+ if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
1009
+ return;
1010
+ }
1011
+ prop.polymorphic = true;
1012
+ prop.discriminator ??= prop.name;
1013
+ prop.discriminatorColumn ??= this.namingStrategy.discriminatorColumnName(prop.discriminator);
1014
+ prop.createForeignKeyConstraint = false;
1015
+ const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1016
+ if (isToOne) {
1017
+ const types = prop.type.split(/ ?\| ?/);
1018
+ prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1019
+ prop.targetMeta = prop.polymorphTargets[0];
1020
+ prop.referencedPKs = prop.targetMeta?.primaryKeys;
1021
+ }
1022
+ if (prop.discriminatorMap) {
1023
+ const normalizedMap = {};
1024
+ for (const [key, value] of Object.entries(prop.discriminatorMap)) {
1025
+ const targetMeta = this.metadata.getByClassName(value, false);
1026
+ if (!targetMeta) {
1027
+ throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
1028
+ }
1029
+ normalizedMap[key] = targetMeta.class;
1030
+ if (targetMeta.class === meta.class) {
1031
+ prop.discriminatorValue = key;
1032
+ }
1033
+ }
1034
+ prop.discriminatorMap = normalizedMap;
1035
+ }
1036
+ else if (isToOne) {
1037
+ prop.discriminatorMap = {};
1038
+ const tableNameToTarget = new Map();
1039
+ for (const target of prop.polymorphTargets) {
1040
+ const existing = tableNameToTarget.get(target.tableName);
1041
+ if (existing) {
1042
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, existing, target, `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`);
1043
+ }
1044
+ tableNameToTarget.set(target.tableName, target);
1045
+ prop.discriminatorMap[target.tableName] = target.class;
1046
+ }
1047
+ }
1048
+ else {
1049
+ prop.discriminatorValue ??= meta.tableName;
1050
+ if (!prop.discriminatorMap) {
1051
+ prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
1052
+ }
1053
+ }
1054
+ }
803
1055
  initEmbeddables(meta, embeddedProp, visited = new Set()) {
804
1056
  if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
805
1057
  return;
@@ -819,22 +1071,30 @@ export class MetadataDiscovery {
819
1071
  }
820
1072
  return prop.embedded ? isParentObject(meta.properties[prop.embedded[0]]) : false;
821
1073
  };
1074
+ const isParentArray = (prop) => {
1075
+ if (prop.array) {
1076
+ return true;
1077
+ }
1078
+ return prop.embedded ? isParentArray(meta.properties[prop.embedded[0]]) : false;
1079
+ };
822
1080
  const rootProperty = getRootProperty(embeddedProp);
823
1081
  const parentProperty = meta.properties[embeddedProp.embedded?.[0] ?? ''];
824
1082
  const object = isParentObject(embeddedProp);
1083
+ const array = isParentArray(embeddedProp);
825
1084
  this.initFieldName(embeddedProp, rootProperty !== embeddedProp && object);
826
1085
  // the prefix of the parent cannot be a boolean; it already passed here
827
1086
  const prefix = this.getPrefix(embeddedProp, parentProperty);
828
1087
  const glue = object ? '~' : '_';
829
1088
  for (const prop of Object.values(embeddable.properties)) {
830
1089
  const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
831
- meta.properties[name] = Utils.copy(prop, false);
1090
+ meta.properties[name] = Utils.copy(prop);
832
1091
  meta.properties[name].name = name;
833
1092
  meta.properties[name].embedded = [embeddedProp.name, prop.name];
834
1093
  meta.propertyOrder.set(name, (order += 0.01));
835
1094
  embeddedProp.embeddedProps[prop.name] = meta.properties[name];
836
1095
  meta.properties[name].persist ??= embeddedProp.persist;
837
- if (embeddedProp.nullable) {
1096
+ const refInArray = array && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.owner;
1097
+ if (embeddedProp.nullable || refInArray) {
838
1098
  meta.properties[name].nullable = true;
839
1099
  }
840
1100
  if (meta.properties[name].fieldNames) {
@@ -864,14 +1124,17 @@ export class MetadataDiscovery {
864
1124
  path = [embeddedProp.fieldNames[0]];
865
1125
  }
866
1126
  this.initFieldName(prop, true);
1127
+ this.initRelation(prop);
867
1128
  path.push(prop.fieldNames[0]);
868
1129
  meta.properties[name].fieldNames = prop.fieldNames;
869
1130
  meta.properties[name].embeddedPath = path;
870
- const fieldName = raw(this.platform.getSearchJsonPropertySQL(path.join('->'), prop.runtimeType ?? prop.type, true));
1131
+ const targetProp = prop.targetMeta?.getPrimaryProp() ?? prop;
1132
+ const fieldName = raw(this.platform.getSearchJsonPropertySQL(path.join('->'), targetProp.runtimeType ?? targetProp.type, true));
871
1133
  meta.properties[name].fieldNameRaw = fieldName.sql; // for querying in SQL drivers
872
1134
  meta.properties[name].persist = false; // only virtual as we store the whole object
873
1135
  meta.properties[name].userDefined = false; // mark this as a generated/internal property, so we can distinguish from user-defined non-persist properties
874
1136
  meta.properties[name].object = true;
1137
+ this.initCustomType(meta, meta.properties[name], false, true);
875
1138
  }
876
1139
  this.initEmbeddables(meta, meta.properties[name], visited);
877
1140
  }
@@ -894,7 +1157,7 @@ export class MetadataDiscovery {
894
1157
  }
895
1158
  initSingleTableInheritance(meta, metadata) {
896
1159
  if (meta.root !== meta && !meta.__processed) {
897
- meta.root = metadata.find(m => m.className === meta.root.className);
1160
+ meta.root = metadata.find(m => m.class === meta.root.class);
898
1161
  meta.root.__processed = true;
899
1162
  }
900
1163
  else {
@@ -903,15 +1166,24 @@ export class MetadataDiscovery {
903
1166
  if (!meta.root.discriminatorColumn) {
904
1167
  return;
905
1168
  }
906
- if (!meta.root.discriminatorMap) {
1169
+ meta.root.inheritanceType = 'sti';
1170
+ if (meta.root.discriminatorMap) {
1171
+ const map = meta.root.discriminatorMap;
1172
+ Object.keys(map)
1173
+ .filter(key => typeof map[key] === 'string')
1174
+ .forEach(key => map[key] = this.metadata.getByClassName(map[key]).class);
1175
+ }
1176
+ else {
907
1177
  meta.root.discriminatorMap = {};
908
- const children = metadata.filter(m => m.root.className === meta.root.className && !m.abstract);
909
- children.forEach(m => {
1178
+ const children = metadata
1179
+ .filter(m => m.root.class === meta.root.class && !m.abstract)
1180
+ .sort((a, b) => a.className.localeCompare(b.className));
1181
+ for (const m of children) {
910
1182
  const name = m.discriminatorValue ?? this.namingStrategy.classToTableName(m.className);
911
- meta.root.discriminatorMap[name] = m.className;
912
- });
1183
+ meta.root.discriminatorMap[name] = m.class;
1184
+ }
913
1185
  }
914
- meta.discriminatorValue = Object.entries(meta.root.discriminatorMap).find(([, className]) => className === meta.className)?.[0];
1186
+ meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
915
1187
  if (!meta.root.properties[meta.root.discriminatorColumn]) {
916
1188
  this.createDiscriminatorProperty(meta.root);
917
1189
  }
@@ -920,30 +1192,218 @@ export class MetadataDiscovery {
920
1192
  if (meta.root === meta) {
921
1193
  return;
922
1194
  }
923
- let i = 1;
924
1195
  Object.values(meta.properties).forEach(prop => {
925
- const newProp = Utils.copy(prop, false);
926
- if (meta.root.properties[prop.name] && meta.root.properties[prop.name].type !== prop.type) {
1196
+ const newProp = { ...prop };
1197
+ const rootProp = meta.root.properties[prop.name];
1198
+ if (rootProp && (rootProp.type !== prop.type || (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))) {
927
1199
  const name = newProp.name;
928
1200
  this.initFieldName(newProp, newProp.object);
929
- newProp.name = name + '_' + (i++);
1201
+ newProp.renamedFrom = name;
1202
+ newProp.name = `${name}_${meta._id}`;
930
1203
  meta.root.addProperty(newProp);
1204
+ this.initFieldName(prop, prop.object);
1205
+ // Track all field variants and map discriminator values to field names
1206
+ if (!rootProp.stiFieldNames) {
1207
+ this.initFieldName(rootProp, rootProp.object);
1208
+ rootProp.stiFieldNames = [...rootProp.fieldNames];
1209
+ rootProp.stiFieldNameMap = {};
1210
+ // Find which discriminator owns the original fieldNames
1211
+ for (const [discValue, childClass] of Object.entries(meta.root.discriminatorMap)) {
1212
+ const childMeta = this.metadata.find(childClass);
1213
+ if (childMeta?.properties[prop.name]?.fieldNames && compareArrays(childMeta.properties[prop.name].fieldNames, rootProp.fieldNames)) {
1214
+ rootProp.stiFieldNameMap[discValue] = rootProp.fieldNames[0];
1215
+ break;
1216
+ }
1217
+ }
1218
+ }
1219
+ rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
1220
+ rootProp.stiFieldNames.push(...prop.fieldNames);
931
1221
  newProp.nullable = true;
932
1222
  newProp.name = name;
933
1223
  newProp.hydrate = false;
934
1224
  newProp.inherited = true;
935
1225
  return;
936
1226
  }
937
- if (prop.enum && prop.items && meta.root.properties[prop.name]?.items) {
938
- newProp.items = Utils.unique([...meta.root.properties[prop.name].items, ...prop.items]);
1227
+ if (prop.enum && prop.items && rootProp?.items) {
1228
+ newProp.items = Utils.unique([...rootProp.items, ...prop.items]);
939
1229
  }
940
1230
  newProp.nullable = true;
941
- newProp.inherited = true;
1231
+ newProp.inherited = !rootProp;
942
1232
  meta.root.addProperty(newProp);
943
1233
  });
944
- meta.collection = meta.root.collection;
1234
+ meta.tableName = meta.root.tableName;
945
1235
  meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]);
946
1236
  meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
1237
+ meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
1238
+ }
1239
+ /**
1240
+ * First pass of TPT initialization: sets up hierarchy relationships
1241
+ * (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
1242
+ */
1243
+ initTPTRelationships(meta, metadata) {
1244
+ if (meta.root !== meta) {
1245
+ meta.root = metadata.find(m => m.class === meta.root.class);
1246
+ }
1247
+ const inheritance = meta.inheritance;
1248
+ if (inheritance === 'tpt' && meta.root === meta) {
1249
+ meta.inheritanceType = 'tpt';
1250
+ meta.tptChildren = [];
1251
+ }
1252
+ if (meta.root.inheritanceType !== 'tpt') {
1253
+ return;
1254
+ }
1255
+ const parent = this.getTPTParent(meta, metadata);
1256
+ if (parent) {
1257
+ meta.tptParent = parent;
1258
+ meta.inheritanceType = 'tpt';
1259
+ parent.tptChildren ??= [];
1260
+ if (!parent.tptChildren.includes(meta)) {
1261
+ parent.tptChildren.push(meta);
1262
+ }
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Second pass of TPT initialization: re-resolves metadata references after fieldNames
1267
+ * are set, syncs to registry metadata, and sets up discriminators.
1268
+ */
1269
+ finalizeTPTInheritance(meta, metadata) {
1270
+ if (meta.inheritanceType !== 'tpt') {
1271
+ return;
1272
+ }
1273
+ if (meta.tptParent) {
1274
+ meta.tptParent = metadata.find(m => m.class === meta.tptParent.class) ?? meta.tptParent;
1275
+ }
1276
+ if (meta.tptChildren) {
1277
+ meta.tptChildren = meta.tptChildren.map(child => metadata.find(m => m.class === child.class) ?? child);
1278
+ }
1279
+ const registryMeta = this.metadata.get(meta.class);
1280
+ if (registryMeta && registryMeta !== meta) {
1281
+ registryMeta.inheritanceType = meta.inheritanceType;
1282
+ registryMeta.tptParent = meta.tptParent ? this.metadata.get(meta.tptParent.class) : undefined;
1283
+ registryMeta.tptChildren = meta.tptChildren?.map(child => this.metadata.get(child.class));
1284
+ }
1285
+ this.initTPTDiscriminator(meta, metadata);
1286
+ }
1287
+ /**
1288
+ * Initialize TPT discriminator map and virtual discriminator property.
1289
+ * Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
1290
+ * at query time using CASE WHEN expressions based on which child table has data.
1291
+ */
1292
+ initTPTDiscriminator(meta, metadata) {
1293
+ const allDescendants = this.collectAllTPTDescendants(meta, metadata);
1294
+ allDescendants.sort((a, b) => this.getTPTDepth(b) - this.getTPTDepth(a));
1295
+ meta.allTPTDescendants = allDescendants;
1296
+ if (meta.root !== meta) {
1297
+ return;
1298
+ }
1299
+ meta.root.discriminatorMap = {};
1300
+ for (const m of allDescendants) {
1301
+ const name = this.namingStrategy.classToTableName(m.className);
1302
+ meta.root.discriminatorMap[name] = m.class;
1303
+ m.discriminatorValue = name;
1304
+ }
1305
+ if (!meta.abstract) {
1306
+ const name = this.namingStrategy.classToTableName(meta.className);
1307
+ meta.root.discriminatorMap[name] = meta.class;
1308
+ meta.discriminatorValue = name;
1309
+ }
1310
+ // Virtual discriminator property - computed at query time via CASE WHEN, not persisted
1311
+ const discriminatorColumn = '__tpt_type';
1312
+ if (!meta.root.properties[discriminatorColumn]) {
1313
+ meta.root.addProperty({
1314
+ name: discriminatorColumn,
1315
+ type: 'string',
1316
+ kind: ReferenceKind.SCALAR,
1317
+ persist: false,
1318
+ userDefined: false,
1319
+ hidden: true,
1320
+ });
1321
+ }
1322
+ meta.root.tptDiscriminatorColumn = discriminatorColumn;
1323
+ }
1324
+ /**
1325
+ * Recursively collect all TPT descendants (children, grandchildren, etc.)
1326
+ */
1327
+ collectAllTPTDescendants(meta, metadata) {
1328
+ const descendants = [];
1329
+ const collect = (parent) => {
1330
+ for (const child of parent.tptChildren ?? []) {
1331
+ const resolved = metadata.find(m => m.class === child.class) ?? child;
1332
+ if (!resolved.abstract) {
1333
+ descendants.push(resolved);
1334
+ }
1335
+ collect(resolved);
1336
+ }
1337
+ };
1338
+ collect(meta);
1339
+ return descendants;
1340
+ }
1341
+ /**
1342
+ * Computes ownProps for TPT entities - only properties defined in THIS entity,
1343
+ * not inherited from parent. Also creates synthetic join properties for parent/child relationships.
1344
+ *
1345
+ * Called multiple times during discovery as metadata is progressively built.
1346
+ * Each pass overwrites earlier results to reflect the final state of properties.
1347
+ */
1348
+ computeTPTOwnProps(meta) {
1349
+ if (meta.inheritanceType !== 'tpt') {
1350
+ return;
1351
+ }
1352
+ const belongsToTable = (prop) => prop.persist !== false || prop.primary;
1353
+ // Use meta.properties (object) since meta.props (array) may not be populated yet
1354
+ const allProps = Object.values(meta.properties);
1355
+ if (!meta.tptParent) {
1356
+ meta.ownProps = allProps.filter(belongsToTable);
1357
+ return;
1358
+ }
1359
+ const parentPropNames = new Set(Object.values(meta.tptParent.properties).map(p => p.name));
1360
+ meta.ownProps = allProps.filter(prop => !parentPropNames.has(prop.name) && belongsToTable(prop));
1361
+ // Create synthetic join properties for the parent-child relationship
1362
+ const childFieldNames = meta.getPrimaryProps().flatMap(p => p.fieldNames);
1363
+ const parentFieldNames = meta.tptParent.getPrimaryProps().flatMap(p => p.fieldNames);
1364
+ meta.tptParentProp = {
1365
+ name: '[tpt:parent]',
1366
+ kind: ReferenceKind.MANY_TO_ONE,
1367
+ targetMeta: meta.tptParent,
1368
+ fieldNames: childFieldNames,
1369
+ referencedColumnNames: parentFieldNames,
1370
+ persist: false,
1371
+ };
1372
+ meta.tptInverseProp = {
1373
+ name: '[tpt:child]',
1374
+ kind: ReferenceKind.ONE_TO_ONE,
1375
+ owner: true,
1376
+ targetMeta: meta,
1377
+ fieldNames: parentFieldNames,
1378
+ joinColumns: parentFieldNames,
1379
+ referencedColumnNames: childFieldNames,
1380
+ persist: false,
1381
+ };
1382
+ }
1383
+ /** Returns the depth of a TPT entity in its hierarchy (0 for root). */
1384
+ getTPTDepth(meta) {
1385
+ let depth = 0;
1386
+ let current = meta;
1387
+ while (current.tptParent) {
1388
+ depth++;
1389
+ current = current.tptParent;
1390
+ }
1391
+ return depth;
1392
+ }
1393
+ /**
1394
+ * Find the direct TPT parent entity for the given entity.
1395
+ */
1396
+ getTPTParent(meta, metadata) {
1397
+ if (meta.root === meta) {
1398
+ return undefined;
1399
+ }
1400
+ return metadata.find(m => {
1401
+ const ext = meta.extends;
1402
+ if (ext instanceof EntitySchema) {
1403
+ return m.class === ext.meta.class || m.className === ext.meta.className;
1404
+ }
1405
+ return m.class === ext || m.className === Utils.className(ext);
1406
+ });
947
1407
  }
948
1408
  createDiscriminatorProperty(meta) {
949
1409
  meta.addProperty({
@@ -961,18 +1421,28 @@ export class MetadataDiscovery {
961
1421
  pks[0].autoincrement ??= true;
962
1422
  }
963
1423
  }
1424
+ createSchemaTable(meta) {
1425
+ const qualifiedName = meta.schema ? `${meta.schema}.${meta.tableName}` : meta.tableName;
1426
+ return {
1427
+ name: meta.tableName,
1428
+ schema: meta.schema,
1429
+ qualifiedName,
1430
+ toString: () => qualifiedName,
1431
+ };
1432
+ }
964
1433
  initCheckConstraints(meta) {
965
- const map = this.createColumnMappingObject(meta);
1434
+ const columns = meta.createSchemaColumnMappingObject();
1435
+ const table = this.createSchemaTable(meta);
966
1436
  for (const check of meta.checks) {
967
- const columns = check.property ? meta.properties[check.property].fieldNames : [];
968
- check.name ??= this.namingStrategy.indexName(meta.tableName, columns, 'check');
1437
+ const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
1438
+ check.name ??= this.namingStrategy.indexName(meta.tableName, fieldNames, 'check');
969
1439
  if (check.expression instanceof Function) {
970
- check.expression = check.expression(map);
1440
+ check.expression = check.expression(columns, table);
971
1441
  }
972
1442
  }
973
1443
  if (this.platform.usesEnumCheckConstraints() && !meta.embeddable) {
974
1444
  for (const prop of meta.props) {
975
- if (prop.enum && !prop.nativeEnumName && prop.items?.every(item => Utils.isString(item))) {
1445
+ if (prop.enum && prop.persist !== false && !prop.nativeEnumName && prop.items?.every(item => typeof item === 'string')) {
976
1446
  this.initFieldName(prop);
977
1447
  meta.checks.push({
978
1448
  name: this.namingStrategy.indexName(meta.tableName, prop.fieldNames, 'check'),
@@ -997,38 +1467,28 @@ export class MetadataDiscovery {
997
1467
  }
998
1468
  return;
999
1469
  }
1000
- const map = this.createColumnMappingObject(meta);
1001
1470
  if (prop.generated instanceof Function) {
1002
- prop.generated = prop.generated(map);
1471
+ const columns = meta.createSchemaColumnMappingObject();
1472
+ prop.generated = prop.generated(columns, this.createSchemaTable(meta));
1003
1473
  }
1004
1474
  }
1005
- createColumnMappingObject(meta) {
1006
- return Object.values(meta.properties).reduce((o, prop) => {
1007
- if (prop.fieldNames) {
1008
- o[prop.name] = prop.fieldNames[0];
1009
- }
1010
- return o;
1011
- }, {});
1012
- }
1013
- getDefaultVersionValue(prop) {
1475
+ getDefaultVersionValue(meta, prop) {
1014
1476
  if (typeof prop.defaultRaw !== 'undefined') {
1015
1477
  return prop.defaultRaw;
1016
1478
  }
1017
- /* v8 ignore next 3 */
1479
+ /* v8 ignore next */
1018
1480
  if (prop.default != null) {
1019
- return '' + this.platform.quoteVersionValue(prop.default, prop);
1481
+ return '' + this.platform.convertVersionValue(prop.default, prop);
1020
1482
  }
1021
- if (prop.type.toLowerCase() === 'date') {
1483
+ this.initCustomType(meta, prop, true);
1484
+ const type = prop.customType?.runtimeType ?? prop.runtimeType ?? prop.type;
1485
+ if (type === 'Date') {
1022
1486
  prop.length ??= this.platform.getDefaultVersionLength();
1023
1487
  return this.platform.getCurrentTimestampSQL(prop.length);
1024
1488
  }
1025
1489
  return '1';
1026
1490
  }
1027
1491
  inferDefaultValue(meta, prop) {
1028
- /* v8 ignore next 3 */
1029
- if (!meta.class) {
1030
- return;
1031
- }
1032
1492
  try {
1033
1493
  // try to create two entity instances to detect the value is stable
1034
1494
  const now = Date.now();
@@ -1056,12 +1516,12 @@ export class MetadataDiscovery {
1056
1516
  return;
1057
1517
  }
1058
1518
  let val = prop.default;
1059
- const raw = RawQueryFragment.getKnownFragment(val);
1519
+ const raw = Raw.getKnownFragment(val);
1060
1520
  if (raw) {
1061
1521
  prop.defaultRaw = this.platform.formatQuery(raw.sql, raw.params);
1062
1522
  return;
1063
1523
  }
1064
- if (prop.customType instanceof ArrayType && Array.isArray(prop.default)) {
1524
+ if (Array.isArray(prop.default) && prop.customType) {
1065
1525
  val = prop.customType.convertToDatabaseValue(prop.default, this.platform);
1066
1526
  }
1067
1527
  prop.defaultRaw = typeof val === 'string' ? `'${val}'` : '' + val;
@@ -1089,13 +1549,13 @@ export class MetadataDiscovery {
1089
1549
  if (prop.version) {
1090
1550
  this.initDefaultValue(prop);
1091
1551
  meta.versionProperty = prop.name;
1092
- prop.defaultRaw = this.getDefaultVersionValue(prop);
1552
+ prop.defaultRaw = this.getDefaultVersionValue(meta, prop);
1093
1553
  }
1094
1554
  if (prop.concurrencyCheck && !prop.primary) {
1095
1555
  meta.concurrencyCheckKeys.add(prop.name);
1096
1556
  }
1097
1557
  }
1098
- initCustomType(meta, prop) {
1558
+ initCustomType(meta, prop, simple = false, objectEmbeddable = false) {
1099
1559
  // `prop.type` might be actually instance of custom type class
1100
1560
  if (Type.isMappedType(prop.type) && !prop.customType) {
1101
1561
  prop.customType = prop.type;
@@ -1103,47 +1563,70 @@ export class MetadataDiscovery {
1103
1563
  }
1104
1564
  // `prop.type` might also be custom type class (not instance), so `typeof MyType` will give us `function`, not `object`
1105
1565
  if (typeof prop.type === 'function' && Type.isMappedType(prop.type.prototype) && !prop.customType) {
1106
- prop.customType = new prop.type();
1107
- prop.type = prop.customType.constructor.name;
1566
+ // if the type is an ORM defined mapped type without `ensureComparable: true`,
1567
+ // we use just the type name, to have more performant hydration code
1568
+ const type = Utils.keys(t).find(type => {
1569
+ return !Type.getType(t[type]).ensureComparable(meta, prop) && prop.type === t[type];
1570
+ });
1571
+ if (type) {
1572
+ prop.type = type === 'datetime' ? 'Date' : type;
1573
+ }
1574
+ else {
1575
+ prop.customType = new prop.type();
1576
+ prop.type = prop.customType.constructor.name;
1577
+ }
1578
+ }
1579
+ if (simple) {
1580
+ return;
1108
1581
  }
1109
1582
  if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
1110
- prop.customType = new JsonType();
1583
+ prop.customType = new t.json();
1111
1584
  }
1112
1585
  if (prop.kind === ReferenceKind.SCALAR && !prop.customType && prop.columnTypes && ['json', 'jsonb'].includes(prop.columnTypes[0])) {
1113
- prop.customType = new JsonType();
1586
+ prop.customType = new t.json();
1587
+ }
1588
+ if (prop.kind === ReferenceKind.EMBEDDED && !prop.customType && (prop.object || prop.array)) {
1589
+ prop.customType = new t.json();
1114
1590
  }
1115
1591
  if (!prop.customType && prop.array && prop.items) {
1116
- prop.customType = new EnumArrayType(`${meta.className}.${prop.name}`, prop.items);
1592
+ prop.customType = new t.enumArray(`${meta.className}.${prop.name}`, prop.items);
1593
+ }
1594
+ const isArray = prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]');
1595
+ if (objectEmbeddable && !prop.customType && isArray) {
1596
+ prop.customType = new t.json();
1117
1597
  }
1118
1598
  // for number arrays we make sure to convert the items to numbers
1119
1599
  if (!prop.customType && prop.type === 'number[]') {
1120
- prop.customType = new ArrayType(i => +i);
1600
+ prop.customType = new t.array(i => +i);
1121
1601
  }
1122
1602
  // `string[]` can be returned via ts-morph, while reflect metadata will give us just `array`
1123
- if (!prop.customType && (prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]'))) {
1124
- prop.customType = new ArrayType();
1603
+ if (!prop.customType && isArray) {
1604
+ prop.customType = new t.array();
1125
1605
  }
1126
1606
  if (!prop.customType && prop.type?.toLowerCase() === 'buffer') {
1127
- prop.customType = new BlobType();
1607
+ prop.customType = new t.blob();
1128
1608
  }
1129
1609
  if (!prop.customType && prop.type?.toLowerCase() === 'uint8array') {
1130
- prop.customType = new Uint8ArrayType();
1610
+ prop.customType = new t.uint8array();
1131
1611
  }
1132
1612
  const mappedType = this.getMappedType(prop);
1133
1613
  if (prop.fieldNames?.length === 1 && !prop.customType) {
1134
- [BigIntType, DoubleType, DecimalType, IntervalType]
1614
+ [t.bigint, t.double, t.decimal, t.interval, t.date]
1135
1615
  .filter(type => mappedType instanceof type)
1136
- .forEach(type => prop.customType = new type());
1616
+ .forEach((type) => prop.customType = new type());
1137
1617
  }
1138
1618
  if (prop.customType && !prop.columnTypes) {
1139
1619
  const mappedType = this.getMappedType({ columnTypes: [prop.customType.getColumnType(prop, this.platform)] });
1140
- if (prop.customType.compareAsType() === 'any' && ![JsonType].some(t => prop.customType instanceof t)) {
1620
+ if (prop.customType.compareAsType() === 'any' && ![t.json].some(t => prop.customType instanceof t)) {
1141
1621
  prop.runtimeType ??= mappedType.runtimeType;
1142
1622
  }
1143
1623
  else {
1144
1624
  prop.runtimeType ??= prop.customType.runtimeType;
1145
1625
  }
1146
1626
  }
1627
+ else if (prop.runtimeType === 'object') {
1628
+ prop.runtimeType = mappedType.runtimeType;
1629
+ }
1147
1630
  else {
1148
1631
  prop.runtimeType ??= mappedType.runtimeType;
1149
1632
  }
@@ -1154,16 +1637,16 @@ export class MetadataDiscovery {
1154
1637
  prop.columnTypes ??= [prop.customType.getColumnType(prop, this.platform)];
1155
1638
  prop.hasConvertToJSValueSQL = !!prop.customType.convertToJSValueSQL && prop.customType.convertToJSValueSQL('', this.platform) !== '';
1156
1639
  prop.hasConvertToDatabaseValueSQL = !!prop.customType.convertToDatabaseValueSQL && prop.customType.convertToDatabaseValueSQL('', this.platform) !== '';
1157
- if (prop.customType instanceof BigIntType && ['string', 'bigint', 'number'].includes(prop.runtimeType.toLowerCase())) {
1640
+ if (prop.customType instanceof t.bigint && ['string', 'bigint', 'number'].includes(prop.runtimeType.toLowerCase())) {
1158
1641
  prop.customType.mode = prop.runtimeType.toLowerCase();
1159
1642
  }
1160
1643
  }
1161
- if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !prop.type?.toString().endsWith('[]')) {
1644
+ if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
1162
1645
  prop.type = prop.customType.name;
1163
1646
  }
1164
- if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && this.metadata.get(prop.type).compositePK) {
1647
+ if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.polymorphic && prop.targetMeta.compositePK) {
1165
1648
  prop.customTypes = [];
1166
- for (const pk of this.metadata.get(prop.type).getPrimaryProps()) {
1649
+ for (const pk of prop.targetMeta.getPrimaryProps()) {
1167
1650
  if (pk.customType) {
1168
1651
  prop.customTypes.push(pk.customType);
1169
1652
  prop.hasConvertToJSValueSQL ||= !!pk.customType.convertToJSValueSQL && pk.customType.convertToJSValueSQL('', this.platform) !== '';
@@ -1175,7 +1658,7 @@ export class MetadataDiscovery {
1175
1658
  }
1176
1659
  }
1177
1660
  }
1178
- if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof UnknownType)) {
1661
+ if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof t.unknown)) {
1179
1662
  if (!prop.columnTypes && prop.nativeEnumName && meta.schema !== this.platform.getDefaultSchemaName() && meta.schema && !prop.nativeEnumName.includes('.')) {
1180
1663
  prop.columnTypes = [`${meta.schema}.${prop.nativeEnumName}`];
1181
1664
  }
@@ -1190,22 +1673,36 @@ export class MetadataDiscovery {
1190
1673
  }
1191
1674
  }
1192
1675
  initRelation(prop) {
1193
- if (prop.kind === ReferenceKind.SCALAR) {
1676
+ if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
1194
1677
  return;
1195
1678
  }
1196
- const meta2 = this.discovered.find(m => m.className === prop.type);
1197
- prop.referencedPKs = meta2.primaryKeys;
1679
+ // 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
1680
+ const meta2 = this.metadata.find(prop.target) ?? this.metadata.getByClassName(prop.type);
1681
+ // If targetKey is specified, use that property instead of PKs for referencedPKs
1682
+ prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
1198
1683
  prop.targetMeta = meta2;
1684
+ if (meta2.view) {
1685
+ prop.createForeignKeyConstraint = false;
1686
+ }
1687
+ // Auto-generate formula for persist: false relations, but only for single-column FKs
1688
+ // Composite FK relations need standard JOIN conditions, not formula-based
1199
1689
  if (!prop.formula && prop.persist === false && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.embedded) {
1200
- prop.formula = a => `${a}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1690
+ this.initFieldName(prop);
1691
+ if (prop.fieldNames?.length === 1) {
1692
+ prop.formula = table => `${table}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1693
+ }
1201
1694
  }
1202
1695
  }
1203
1696
  initColumnType(prop) {
1204
1697
  this.initUnsigned(prop);
1205
- this.metadata.find(prop.type)?.getPrimaryProps().map(pk => {
1206
- prop.length ??= pk.length;
1207
- prop.precision ??= pk.precision;
1208
- prop.scale ??= pk.scale;
1698
+ // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
1699
+ const targetProps = prop.targetMeta
1700
+ ? (prop.targetKey ? [prop.targetMeta.properties[prop.targetKey]] : prop.targetMeta.getPrimaryProps())
1701
+ : [];
1702
+ targetProps.map(targetProp => {
1703
+ prop.length ??= targetProp.length;
1704
+ prop.precision ??= targetProp.precision;
1705
+ prop.scale ??= targetProp.scale;
1209
1706
  });
1210
1707
  if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
1211
1708
  delete prop.type;
@@ -1218,8 +1715,7 @@ export class MetadataDiscovery {
1218
1715
  if (prop.kind === ReferenceKind.SCALAR) {
1219
1716
  const mappedType = this.getMappedType(prop);
1220
1717
  const SCALAR_TYPES = ['string', 'number', 'boolean', 'bigint', 'Date', 'Buffer', 'RegExp', 'any', 'unknown'];
1221
- if (mappedType instanceof UnknownType
1222
- && !prop.columnTypes
1718
+ if (mappedType instanceof t.unknown
1223
1719
  // it could be a runtime type from reflect-metadata
1224
1720
  && !SCALAR_TYPES.includes(prop.type)
1225
1721
  // or it might be inferred via ts-morph to some generic type alias
@@ -1232,23 +1728,31 @@ export class MetadataDiscovery {
1232
1728
  }
1233
1729
  return;
1234
1730
  }
1235
- if (prop.kind === ReferenceKind.EMBEDDED && prop.object && !prop.columnTypes) {
1731
+ /* v8 ignore next */
1732
+ if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
1236
1733
  prop.columnTypes = [this.platform.getJsonDeclarationSQL()];
1237
1734
  return;
1238
1735
  }
1239
- const targetMeta = this.metadata.get(prop.type);
1736
+ const targetMeta = prop.targetMeta;
1240
1737
  prop.columnTypes = [];
1241
- for (const pk of targetMeta.getPrimaryProps()) {
1242
- this.initCustomType(targetMeta, pk);
1243
- this.initColumnType(pk);
1244
- const mappedType = this.getMappedType(pk);
1245
- let columnTypes = pk.columnTypes;
1246
- if (pk.autoincrement) {
1247
- columnTypes = [mappedType.getColumnType({ ...pk, autoincrement: false }, this.platform)];
1738
+ // Use targetKey property if specified, otherwise use primary key properties
1739
+ const referencedProps = prop.targetKey
1740
+ ? [targetMeta.properties[prop.targetKey]]
1741
+ : targetMeta.getPrimaryProps();
1742
+ if (prop.polymorphic && prop.polymorphTargets) {
1743
+ prop.columnTypes.push(this.platform.getVarcharTypeDeclarationSQL(prop));
1744
+ }
1745
+ for (const referencedProp of referencedProps) {
1746
+ this.initCustomType(targetMeta, referencedProp);
1747
+ this.initColumnType(referencedProp);
1748
+ const mappedType = this.getMappedType(referencedProp);
1749
+ let columnTypes = referencedProp.columnTypes;
1750
+ if (referencedProp.autoincrement) {
1751
+ columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.platform)];
1248
1752
  }
1249
1753
  prop.columnTypes.push(...columnTypes);
1250
- if (!targetMeta.compositePK) {
1251
- prop.customType = pk.customType;
1754
+ if (!targetMeta.compositePK || prop.targetKey) {
1755
+ prop.customType = referencedProp.customType;
1252
1756
  }
1253
1757
  }
1254
1758
  }
@@ -1262,7 +1766,7 @@ export class MetadataDiscovery {
1262
1766
  t = 'enum';
1263
1767
  }
1264
1768
  else if (prop.enum) {
1265
- t = prop.items?.every(item => Utils.isString(item)) ? 'enum' : 'tinyint';
1769
+ t = prop.items?.every(item => typeof item === 'string') ? 'enum' : 'tinyint';
1266
1770
  }
1267
1771
  if (t === 'Date') {
1268
1772
  t = 'datetime';
@@ -1286,8 +1790,7 @@ export class MetadataDiscovery {
1286
1790
  return;
1287
1791
  }
1288
1792
  if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1289
- const meta2 = this.metadata.get(prop.type);
1290
- prop.unsigned = meta2.getPrimaryProps().some(pk => {
1793
+ prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
1291
1794
  this.initUnsigned(pk);
1292
1795
  return pk.unsigned;
1293
1796
  });
@@ -1301,30 +1804,6 @@ export class MetadataDiscovery {
1301
1804
  prop.index ??= true;
1302
1805
  }
1303
1806
  }
1304
- async getEntityClassOrSchema(path, name) {
1305
- const exports = await Utils.dynamicImport(path);
1306
- const targets = Object.values(exports)
1307
- .filter(item => item instanceof EntitySchema || (item instanceof Function && MetadataStorage.isKnownEntity(item.name)));
1308
- // ignore class implementations that are linked from an EntitySchema
1309
- for (const item of targets) {
1310
- if (item instanceof EntitySchema) {
1311
- targets.forEach((item2, idx) => {
1312
- if (item.meta.class === item2) {
1313
- targets.splice(idx, 1);
1314
- }
1315
- });
1316
- }
1317
- }
1318
- if (targets.length > 0) {
1319
- return targets;
1320
- }
1321
- const target = exports.default ?? exports[name];
1322
- /* v8 ignore next 3 */
1323
- if (!target) {
1324
- throw MetadataError.entityNotFound(name, path.replace(this.config.get('baseDir'), '.'));
1325
- }
1326
- return [target];
1327
- }
1328
1807
  shouldForceConstructorUsage(meta) {
1329
1808
  const forceConstructor = this.config.get('forceEntityConstructor');
1330
1809
  if (Array.isArray(forceConstructor)) {