@mikro-orm/core 7.0.17 → 7.0.18-dev.0

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 (69) hide show
  1. package/EntityManager.d.ts +106 -12
  2. package/EntityManager.js +228 -19
  3. package/MikroORM.d.ts +3 -3
  4. package/README.md +2 -1
  5. package/connections/Connection.d.ts +67 -1
  6. package/connections/Connection.js +64 -0
  7. package/drivers/DatabaseDriver.d.ts +1 -0
  8. package/drivers/DatabaseDriver.js +3 -0
  9. package/drivers/IDatabaseDriver.d.ts +50 -7
  10. package/entity/BaseEntity.d.ts +70 -3
  11. package/entity/BaseEntity.js +18 -0
  12. package/entity/Collection.d.ts +6 -3
  13. package/entity/Collection.js +15 -4
  14. package/entity/EntityAssigner.js +8 -0
  15. package/entity/EntityFactory.js +20 -1
  16. package/entity/EntityLoader.d.ts +8 -1
  17. package/entity/EntityLoader.js +41 -16
  18. package/entity/EntityRepository.d.ts +27 -9
  19. package/entity/EntityRepository.js +12 -0
  20. package/entity/Reference.d.ts +42 -1
  21. package/entity/Reference.js +9 -0
  22. package/entity/WrappedEntity.d.ts +2 -2
  23. package/entity/defineEntity.d.ts +92 -17
  24. package/entity/defineEntity.js +17 -6
  25. package/entity/utils.js +4 -5
  26. package/enums.d.ts +8 -1
  27. package/errors.d.ts +2 -0
  28. package/errors.js +4 -0
  29. package/index.d.ts +2 -2
  30. package/index.js +1 -1
  31. package/metadata/EntitySchema.js +3 -0
  32. package/metadata/MetadataDiscovery.d.ts +11 -0
  33. package/metadata/MetadataDiscovery.js +135 -10
  34. package/metadata/MetadataValidator.d.ts +26 -0
  35. package/metadata/MetadataValidator.js +236 -0
  36. package/metadata/Routine.d.ts +74 -0
  37. package/metadata/Routine.js +140 -0
  38. package/metadata/index.d.ts +1 -0
  39. package/metadata/index.js +1 -0
  40. package/metadata/types.d.ts +77 -4
  41. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  42. package/naming-strategy/NamingStrategy.d.ts +1 -1
  43. package/package.json +1 -1
  44. package/platforms/Platform.d.ts +7 -0
  45. package/platforms/Platform.js +15 -1
  46. package/serialization/EntitySerializer.d.ts +18 -6
  47. package/serialization/EntitySerializer.js +31 -4
  48. package/typings.d.ts +389 -18
  49. package/typings.js +21 -4
  50. package/unit-of-work/UnitOfWork.js +11 -3
  51. package/utils/AbstractMigrator.d.ts +26 -3
  52. package/utils/AbstractMigrator.js +264 -15
  53. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  54. package/utils/AbstractSchemaGenerator.js +4 -1
  55. package/utils/Configuration.d.ts +40 -3
  56. package/utils/Configuration.js +46 -0
  57. package/utils/DataloaderUtils.d.ts +10 -1
  58. package/utils/DataloaderUtils.js +78 -0
  59. package/utils/QueryHelper.d.ts +16 -0
  60. package/utils/QueryHelper.js +15 -0
  61. package/utils/TransactionManager.js +2 -0
  62. package/utils/Utils.d.ts +1 -1
  63. package/utils/Utils.js +1 -1
  64. package/utils/fs-utils.d.ts +2 -0
  65. package/utils/fs-utils.js +7 -1
  66. package/utils/index.d.ts +1 -0
  67. package/utils/index.js +1 -0
  68. package/utils/partition-utils.d.ts +17 -0
  69. package/utils/partition-utils.js +79 -0
package/entity/utils.js CHANGED
@@ -94,11 +94,10 @@ export function applyPopulateHints(populate, hints) {
94
94
  if (!entry) {
95
95
  continue;
96
96
  }
97
- if (hint.strategy != null) {
98
- entry.strategy = hint.strategy;
99
- }
100
- if (hint.joinType != null) {
101
- entry.joinType = hint.joinType;
97
+ for (const key of Object.keys(hint)) {
98
+ if (hint[key] != null) {
99
+ entry[key] = hint[key];
100
+ }
102
101
  }
103
102
  }
104
103
  }
package/enums.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { EntityKey, ExpandProperty } from './typings.js';
2
- import type { Transaction } from './connections/Connection.js';
2
+ import type { InflightQueryAbortStrategy, Transaction } from './connections/Connection.js';
3
3
  import type { LogContext } from './logging/Logger.js';
4
4
  /** Controls when the `EntityManager` flushes pending changes to the database. */
5
5
  export declare enum FlushMode {
@@ -307,6 +307,13 @@ export interface TransactionOptions {
307
307
  flushMode?: FlushMode | `${FlushMode}`;
308
308
  ignoreNestedTransactions?: boolean;
309
309
  loggerContext?: LogContext;
310
+ /**
311
+ * `AbortSignal` cancelling every query within the transaction (including the implicit flush).
312
+ * Cancelling mid-transaction triggers a rollback once the in-flight query settles.
313
+ */
314
+ signal?: AbortSignal;
315
+ /** Cancellation strategy paired with {@link signal}. */
316
+ inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
310
317
  }
311
318
  export declare abstract class PlainObject {
312
319
  }
package/errors.d.ts CHANGED
@@ -74,6 +74,8 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
74
74
  static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
75
75
  static mixedInheritanceStrategies(root: EntityMetadata, child: EntityMetadata): MetadataError;
76
76
  static tptNotSupportedByDriver(meta: EntityMetadata): MetadataError;
77
+ /** Thrown when database triggers are defined on an entity using a driver that does not support them. */
78
+ static triggersNotSupportedByDriver(meta: EntityMetadata): MetadataError;
77
79
  private static fromMessage;
78
80
  }
79
81
  /** Error thrown when an entity lookup fails to find the expected result. */
package/errors.js CHANGED
@@ -243,6 +243,10 @@ export class MetadataError extends ValidationError {
243
243
  static tptNotSupportedByDriver(meta) {
244
244
  return new MetadataError(`Entity ${meta.className} uses TPT (Table-Per-Type) inheritance which is not supported by the current driver. TPT requires SQL JOINs and is only available with SQL drivers.`);
245
245
  }
246
+ /** Thrown when database triggers are defined on an entity using a driver that does not support them. */
247
+ static triggersNotSupportedByDriver(meta) {
248
+ return new MetadataError(`Entity ${meta.className} defines database triggers which are not supported by the current driver. Triggers are only available with SQL drivers.`);
249
+ }
246
250
  static fromMessage(meta, prop, message) {
247
251
  return new MetadataError(`${meta.className}.${prop.name} ${message}`);
248
252
  }
package/index.d.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * @packageDocumentation
3
3
  * @module core
4
4
  */
5
- export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, } from './typings.js';
6
- export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaColumns, FormulaTable, SchemaTable, SchemaColumns, SchemaColumnRef, EntityDataPropValue, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
5
+ export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
6
+ export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaColumns, FormulaTable, SchemaTable, SchemaColumns, SchemaColumnRef, EntityDataPropValue, SimpleColumnMeta, Rel, Ref, LazyRef, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, TriggerDef, RoutineReturns, RoutineBodyCallback, RoutineJsBody, RoutineIgnoreField, RoutineParamConfig, RoutineConfig, RoutineRuntimeType, RoutineArgs, RoutineReturn, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
7
7
  export * from './enums.js';
8
8
  export * from './errors.js';
9
9
  export * from './exceptions.js';
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @packageDocumentation
3
3
  * @module core
4
4
  */
5
- export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, } from './typings.js';
5
+ export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
6
6
  export * from './enums.js';
7
7
  export * from './errors.js';
8
8
  export * from './exceptions.js';
@@ -85,6 +85,9 @@ export class EntitySchema {
85
85
  if (options.items instanceof Function) {
86
86
  options.items = Utils.extractEnumValues(options.items());
87
87
  }
88
+ else if (options.items && !Array.isArray(options.items) && typeof options.items === 'object') {
89
+ options.items = Utils.extractEnumValues(options.items);
90
+ }
88
91
  // enum arrays are simple numeric/string arrays, the constraint is enforced in the custom type only
89
92
  if (options.array && !options.type) {
90
93
  options.type = new EnumArrayType(`${this._meta.className}.${name}`, options.items);
@@ -54,6 +54,16 @@ export declare class MetadataDiscovery {
54
54
  * Define properties for a polymorphic pivot table.
55
55
  */
56
56
  private definePolymorphicPivotProperties;
57
+ /**
58
+ * Mirror of definePolymorphicPivotProperties for union-target M:N
59
+ * (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
60
+ *
61
+ * Pivot shape:
62
+ * (owner_fk..., discriminator_column, target_fk...)
63
+ * - owner side is a normal M:1 to the single owner entity
64
+ * - target side is a discriminator column + per-target-type virtual M:1 relations
65
+ */
66
+ private defineUnionTargetPolymorphicPivotProperties;
57
67
  /**
58
68
  * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
59
69
  * This enables single-query join loading for inverse-side polymorphic M:N.
@@ -106,6 +116,7 @@ export declare class MetadataDiscovery {
106
116
  private initAutoincrement;
107
117
  private createSchemaTable;
108
118
  private initCheckConstraints;
119
+ private initTriggers;
109
120
  private initGeneratedColumn;
110
121
  private getDefaultVersionValue;
111
122
  private inferDefaultValue;
@@ -164,6 +164,7 @@ export class MetadataDiscovery {
164
164
  forEachProp((m, p) => this.initGeneratedColumn(m, p));
165
165
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
166
166
  filtered.forEach(meta => this.initCheckConstraints(meta));
167
+ filtered.forEach(meta => this.initTriggers(meta));
167
168
  forEachProp((_m, p) => {
168
169
  this.initDefaultValue(p);
169
170
  this.inferTypeFromDefault(p);
@@ -484,17 +485,28 @@ export class MetadataDiscovery {
484
485
  prop.polymorphic = prop2.polymorphic;
485
486
  prop.discriminator = prop2.discriminator;
486
487
  prop.discriminatorColumn = prop2.discriminatorColumn;
487
- prop.discriminatorValue = prop2.discriminatorValue;
488
+ // For a union-target pivot each inverse side sits on one specific target class, so its
489
+ // discriminator value is that class's tableName. For Rails-style, prop2 has a single fixed value.
490
+ prop.discriminatorValue = QueryHelper.isUnionTargetPolymorphic(prop2) ? meta.tableName : prop2.discriminatorValue;
488
491
  }
489
492
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
490
- // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
491
- if (prop.polymorphic && prop.discriminator) {
493
+ // Union-target polymorphic M:N: owner side is fixed (real FK), target side uses discriminator-derived names.
494
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
495
+ if (prop.polymorphic && prop.discriminator && !isUnionTargetMN) {
496
+ // Rails-style: owner side is polymorphic, uses discriminator base name (e.g. taggable_id instead of post_id)
492
497
  prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
493
498
  }
494
499
  else {
495
500
  prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
496
501
  }
497
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
502
+ if (isUnionTargetMN) {
503
+ // Target side uses discriminator base name (e.g. attachable_id — shared across Image/Video)
504
+ const targetPkCols = Utils.flatten(meta2.primaryKeys.map(pk => meta2.properties[pk].fieldNames));
505
+ prop.inverseJoinColumns ??= targetPkCols.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, targetPkCols.length > 1));
506
+ }
507
+ else {
508
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
509
+ }
498
510
  }
499
511
  isExplicitTableName(meta) {
500
512
  return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
@@ -561,6 +573,12 @@ export class MetadataDiscovery {
561
573
  }
562
574
  }
563
575
  meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
576
+ // Fail fast when the platform rejects the metadata, so users see the platform-specific
577
+ // error (e.g. "SqlitePlatform does not support partitioned tables") instead of a
578
+ // downstream core validator message that assumes partitioning is supported.
579
+ if (meta.partitionBy && !this.#platform.supportsPartitionedTables()) {
580
+ this.#platform.validateMetadata(meta);
581
+ }
564
582
  this.#validator.validateEntityDefinition(this.#metadata, meta.class, this.#config.get('discovery'));
565
583
  for (const prop of Object.values(meta.properties)) {
566
584
  this.initNullability(prop);
@@ -592,6 +610,24 @@ export class MetadataDiscovery {
592
610
  targetRoot.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
593
611
  }
594
612
  }
613
+ // Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
614
+ // owner prop. Covers three cases:
615
+ // - regular inverse (Tag.posts mappedBy Post.tags) — handled by inversedBy above
616
+ // - union-target inverse (Image.posts mappedBy Post.attachments) — on each polymorph target
617
+ // - merged inverse (Tag.owners mappedBy [Post,Video].tags) — union collection on the target
618
+ const inverseCandidates = QueryHelper.isUnionTargetPolymorphic(prop)
619
+ ? prop.polymorphTargets
620
+ : [prop.targetMeta];
621
+ for (const targetMeta of inverseCandidates) {
622
+ for (const inverseProp of Object.values(targetMeta.properties)) {
623
+ if (inverseProp.kind === ReferenceKind.MANY_TO_MANY &&
624
+ inverseProp.mappedBy === prop.name &&
625
+ !inverseProp.pivotEntity) {
626
+ inverseProp.pivotEntity = pivotMeta.class;
627
+ inverseProp.pivotTable = pivotMeta.tableName;
628
+ }
629
+ }
630
+ }
595
631
  return pivotMeta;
596
632
  });
597
633
  }
@@ -730,8 +766,12 @@ export class MetadataDiscovery {
730
766
  }
731
767
  }
732
768
  }
733
- // For polymorphic M:N, create discriminator column and polymorphic FK
734
- if (prop.polymorphic && prop.discriminatorColumn) {
769
+ // Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
770
+ if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
771
+ this.defineUnionTargetPolymorphicPivotProperties(pivotMeta2, meta, prop);
772
+ }
773
+ else if (prop.polymorphic && prop.discriminatorColumn) {
774
+ // Rails-style polymorphic M:N: multiple owners share the pivot, single target type
735
775
  this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
736
776
  }
737
777
  else {
@@ -818,6 +858,33 @@ export class MetadataDiscovery {
818
858
  pivotMeta.polymorphicDiscriminatorMap ??= {};
819
859
  pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
820
860
  }
861
+ /**
862
+ * Mirror of definePolymorphicPivotProperties for union-target M:N
863
+ * (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
864
+ *
865
+ * Pivot shape:
866
+ * (owner_fk..., discriminator_column, target_fk...)
867
+ * - owner side is a normal M:1 to the single owner entity
868
+ * - target side is a discriminator column + per-target-type virtual M:1 relations
869
+ */
870
+ defineUnionTargetPolymorphicPivotProperties(pivotMeta, meta, prop) {
871
+ const discriminatorColumn = prop.discriminatorColumn;
872
+ const targets = prop.polymorphTargets;
873
+ pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
874
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: true, nullable: false });
875
+ this.initFieldName(discriminatorProp);
876
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
877
+ const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
878
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: true, nullable: false });
879
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
880
+ for (const targetMeta of targets) {
881
+ const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
882
+ const relation = this.definePolymorphicOwnerRelation(prop, relationName, targetMeta);
883
+ relation.joinColumns = relation.fieldNames = relation.ownColumns = [...prop.inverseJoinColumns];
884
+ pivotMeta.properties[relationName] = relation;
885
+ pivotMeta.polymorphicDiscriminatorMap[targetMeta.tableName] = targetMeta.class;
886
+ }
887
+ }
821
888
  /**
822
889
  * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
823
890
  * This enables single-query join loading for inverse-side polymorphic M:N.
@@ -968,11 +1035,12 @@ export class MetadataDiscovery {
968
1035
  });
969
1036
  }
970
1037
  // TPT children have their own tables that don't contain the parent's columns,
971
- // so propagating parent indexes/uniques/checks would target missing columns.
1038
+ // so propagating parent indexes/uniques/checks/triggers would target missing columns.
972
1039
  if (meta.inheritanceType !== 'tpt' || !meta.tptParent) {
973
1040
  meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
974
1041
  meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
975
1042
  meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1043
+ meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
976
1044
  }
977
1045
  const pks = Object.values(meta.properties)
978
1046
  .filter(p => p.primary)
@@ -1058,11 +1126,15 @@ export class MetadataDiscovery {
1058
1126
  prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
1059
1127
  prop.createForeignKeyConstraint = false;
1060
1128
  const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1061
- if (isToOne) {
1129
+ const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
1130
+ if (isToOne || isUnionTargetMN) {
1062
1131
  const types = prop.type.split(/ ?\| ?/);
1063
1132
  prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1064
1133
  prop.targetMeta = prop.polymorphTargets[0];
1065
1134
  prop.referencedPKs = prop.targetMeta?.primaryKeys;
1135
+ if (isUnionTargetMN && prop.polymorphTargets.length < 2) {
1136
+ throw new MetadataError(`${meta.className}.${prop.name} union-target polymorphic M:N requires at least two target entity types; use a regular M:N relation for a single target.`);
1137
+ }
1066
1138
  }
1067
1139
  if (prop.discriminatorMap) {
1068
1140
  const normalizedMap = {};
@@ -1078,7 +1150,7 @@ export class MetadataDiscovery {
1078
1150
  }
1079
1151
  prop.discriminatorMap = normalizedMap;
1080
1152
  }
1081
- else if (isToOne) {
1153
+ else if (isToOne || isUnionTargetMN) {
1082
1154
  prop.discriminatorMap = {};
1083
1155
  const tableNameToTarget = new Map();
1084
1156
  for (const target of prop.polymorphTargets) {
@@ -1572,6 +1644,27 @@ export class MetadataDiscovery {
1572
1644
  }
1573
1645
  }
1574
1646
  }
1647
+ initTriggers(meta) {
1648
+ if (meta.triggers.length === 0) {
1649
+ return;
1650
+ }
1651
+ const columns = meta.createSchemaColumnMappingObject();
1652
+ const table = this.createSchemaTable(meta);
1653
+ for (const trigger of meta.triggers) {
1654
+ if (trigger.body && trigger.expression) {
1655
+ throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} defines both 'body' and 'expression'. Use one or the other.`);
1656
+ }
1657
+ if (!trigger.body && !trigger.expression) {
1658
+ throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} must define either 'body' or 'expression'.`);
1659
+ }
1660
+ trigger.name ??= this.#namingStrategy.indexName(meta.tableName, trigger.events, 'trigger');
1661
+ trigger.forEach ??= 'row';
1662
+ if (trigger.body instanceof Function) {
1663
+ trigger.body = trigger.body(columns, table);
1664
+ }
1665
+ }
1666
+ meta.hasTriggers = true;
1667
+ }
1575
1668
  initGeneratedColumn(meta, prop) {
1576
1669
  if (!prop.generated && prop.columnTypes) {
1577
1670
  const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
@@ -1698,7 +1791,14 @@ export class MetadataDiscovery {
1698
1791
  return (!Type.getType(t[type]).ensureComparable(meta, prop) && (prop.type === t[type] || brand === type));
1699
1792
  });
1700
1793
  if (type) {
1701
- prop.type = type === 'datetime' ? 'Date' : type;
1794
+ if (prop.array) {
1795
+ // built-in type + array: true — force-create instance for ArrayType wrapping
1796
+ prop.customType = new prop.type();
1797
+ prop.type = prop.customType.constructor.name;
1798
+ }
1799
+ else {
1800
+ prop.type = type === 'datetime' ? 'Date' : type;
1801
+ }
1702
1802
  }
1703
1803
  else {
1704
1804
  prop.customType = new prop.type();
@@ -1708,6 +1808,31 @@ export class MetadataDiscovery {
1708
1808
  if (simple) {
1709
1809
  return;
1710
1810
  }
1811
+ if (prop.array &&
1812
+ prop.customType &&
1813
+ !(prop.customType instanceof t.array) &&
1814
+ !(prop.customType instanceof t.enumArray) &&
1815
+ !(prop.customType instanceof t.json) &&
1816
+ prop.kind === ReferenceKind.SCALAR) {
1817
+ const innerType = prop.customType;
1818
+ innerType.platform = this.#platform;
1819
+ innerType.meta = meta;
1820
+ innerType.prop = prop;
1821
+ if (!prop.columnTypes) {
1822
+ const arrayDecl = this.#platform.getArrayDeclarationSQL();
1823
+ if (arrayDecl.endsWith('[]')) {
1824
+ // native array support (e.g. postgres) — use inner type's column type with [] suffix
1825
+ prop.columnTypes = [innerType.getColumnType(prop, this.#platform) + '[]'];
1826
+ }
1827
+ else {
1828
+ // non-native (e.g. mysql, sqlite) — store as text/clob
1829
+ prop.columnTypes = [arrayDecl];
1830
+ }
1831
+ }
1832
+ const platform = this.#platform;
1833
+ prop.customType = new t.array(v => innerType.convertToJSValue(v, platform), v => innerType.convertToDatabaseValue(v, platform));
1834
+ prop.type = prop.customType.constructor.name;
1835
+ }
1711
1836
  if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
1712
1837
  prop.customType = new t.json();
1713
1838
  }
@@ -1,4 +1,5 @@
1
1
  import type { EntityMetadata, EntityName } from '../typings.js';
2
+ import type { Routine } from './Routine.js';
2
3
  import { type MetadataDiscoveryOptions } from '../utils/Configuration.js';
3
4
  import type { MetadataStorage } from './MetadataStorage.js';
4
5
  /**
@@ -6,7 +7,32 @@ import type { MetadataStorage } from './MetadataStorage.js';
6
7
  */
7
8
  export declare class MetadataValidator {
8
9
  validateEntityDefinition<T>(metadata: MetadataStorage, name: EntityName<T>, options: MetadataDiscoveryOptions): void;
10
+ validateRoutineDefinition(routine: Routine): void;
9
11
  validateDiscovered(discovered: EntityMetadata[], options: MetadataDiscoveryOptions): void;
12
+ private validatePartitioning;
13
+ /**
14
+ * Find the first partition name whose normalized form (case-folded for unquoted segments,
15
+ * quoted segments preserved) has already been seen. Returns the offending name in its
16
+ * original form for the error message.
17
+ */
18
+ private findDuplicatePartitionName;
19
+ /**
20
+ * Partition names may be bare (`child`), schema-qualified (`schema.child`), or use quoted
21
+ * identifiers (`"my.schema"."child"`). Reject anything with more than one unquoted `.`.
22
+ */
23
+ private hasValidPartitionName;
24
+ private hasPartitionExpression;
25
+ private validatePartitionKeyConstraints;
26
+ /**
27
+ * Returns the list of physical field names that a partition expression references, or
28
+ * `undefined` when the expression is opaque (callback, or raw SQL like `date_trunc('day', x)`
29
+ * that we cannot statically parse). Opaque expressions intentionally bypass the primary-key /
30
+ * unique-constraint coverage checks — users are trusted to ensure the referenced columns are
31
+ * part of the partition key, since PostgreSQL will surface the violation at DDL execution.
32
+ */
33
+ private getPartitionKeyFields;
34
+ private resolvePartitionKeyField;
35
+ private getConstraintFields;
10
36
  private validateReference;
11
37
  private validateTargetKey;
12
38
  /**