@mikro-orm/core 7.1.0-dev.8 → 7.1.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 (57) hide show
  1. package/EntityManager.d.ts +70 -5
  2. package/EntityManager.js +175 -23
  3. package/README.md +2 -1
  4. package/connections/Connection.d.ts +67 -1
  5. package/connections/Connection.js +64 -0
  6. package/drivers/IDatabaseDriver.d.ts +30 -6
  7. package/entity/BaseEntity.d.ts +68 -1
  8. package/entity/BaseEntity.js +18 -0
  9. package/entity/Collection.d.ts +4 -1
  10. package/entity/Collection.js +15 -4
  11. package/entity/EntityAssigner.js +8 -0
  12. package/entity/EntityFactory.js +5 -3
  13. package/entity/EntityLoader.d.ts +8 -1
  14. package/entity/EntityLoader.js +97 -29
  15. package/entity/EntityRepository.d.ts +12 -2
  16. package/entity/EntityRepository.js +12 -0
  17. package/entity/Reference.d.ts +42 -1
  18. package/entity/Reference.js +9 -0
  19. package/entity/defineEntity.d.ts +69 -16
  20. package/entity/defineEntity.js +17 -0
  21. package/entity/utils.js +4 -5
  22. package/enums.d.ts +8 -1
  23. package/index.d.ts +1 -1
  24. package/metadata/EntitySchema.js +18 -0
  25. package/metadata/MetadataDiscovery.d.ts +1 -0
  26. package/metadata/MetadataDiscovery.js +79 -15
  27. package/metadata/MetadataValidator.d.ts +26 -0
  28. package/metadata/MetadataValidator.js +228 -1
  29. package/metadata/Routine.d.ts +74 -0
  30. package/metadata/Routine.js +140 -0
  31. package/metadata/index.d.ts +1 -0
  32. package/metadata/index.js +1 -0
  33. package/metadata/types.d.ts +75 -8
  34. package/package.json +1 -1
  35. package/platforms/Platform.d.ts +13 -3
  36. package/platforms/Platform.js +51 -6
  37. package/serialization/EntitySerializer.js +4 -3
  38. package/typings.d.ts +323 -21
  39. package/typings.js +32 -12
  40. package/unit-of-work/UnitOfWork.js +43 -5
  41. package/utils/AbstractMigrator.d.ts +9 -4
  42. package/utils/AbstractMigrator.js +60 -28
  43. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  44. package/utils/AbstractSchemaGenerator.js +4 -1
  45. package/utils/Configuration.d.ts +30 -0
  46. package/utils/Configuration.js +45 -0
  47. package/utils/DataloaderUtils.d.ts +10 -1
  48. package/utils/DataloaderUtils.js +78 -0
  49. package/utils/EntityComparator.js +1 -1
  50. package/utils/TransactionManager.js +2 -0
  51. package/utils/Utils.js +1 -1
  52. package/utils/index.d.ts +1 -0
  53. package/utils/index.js +1 -0
  54. package/utils/partition-utils.d.ts +17 -0
  55. package/utils/partition-utils.js +79 -0
  56. package/utils/upsert-utils.d.ts +2 -0
  57. package/utils/upsert-utils.js +26 -1
@@ -173,6 +173,16 @@ export class UniversalPropertyOptionsBuilder {
173
173
  ref() {
174
174
  return this.assignOptions({ ref: true });
175
175
  }
176
+ /**
177
+ * Mark a to-one relation (`m:1`/`1:1`) as a {@apilink LazyRef} — the runtime value stays a plain entity
178
+ * (no `Reference` wrapper) but the TypeScript type restricts access to the primary key until `Loaded<>`
179
+ * narrows it. Type-level only; does not set `ref: true`. Not compatible with `.ref()` or `.mapToPk()` —
180
+ * those chains are rejected in both directions at compile time.
181
+ */
182
+ lazyRef() {
183
+ // purely type-level — no runtime metadata change
184
+ return this;
185
+ }
176
186
  /**
177
187
  * Set to true to omit the property when {@link https://mikro-orm.io/docs/serializing Serializing}.
178
188
  */
@@ -306,6 +316,13 @@ export class UniversalPropertyOptionsBuilder {
306
316
  comment(comment) {
307
317
  return this.assignOptions({ comment });
308
318
  }
319
+ /**
320
+ * Specify column-level collation for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}. (SQL only)
321
+ * Emits a `COLLATE` clause on the column definition; the accepted collation names are dialect-specific.
322
+ */
323
+ collation(collation) {
324
+ return this.assignOptions({ collation });
325
+ }
309
326
  /** mysql only */
310
327
  extra(extra) {
311
328
  return this.assignOptions({ extra });
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/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module core
4
4
  */
5
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, FormulaTable, SchemaTable, SchemaColumns, 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, TriggerDef, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } 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';
@@ -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);
@@ -251,6 +254,21 @@ export class EntitySchema {
251
254
  return this;
252
255
  }
253
256
  this.setClass(this._meta.class);
257
+ // `discriminator` is the property name on the entity / embeddable; `discriminatorColumn` is an
258
+ // optional column-name override on that property. For backward compat, when only
259
+ // `discriminatorColumn` is given (legacy), it is treated as the property name. Once normalized,
260
+ // `meta.discriminatorColumn` always holds the property name (so existing readers keep working),
261
+ // and `meta.discriminatorFieldName` carries the explicit column override (if any).
262
+ if (this._meta.discriminator &&
263
+ this._meta.discriminatorColumn &&
264
+ this._meta.discriminator !== this._meta.discriminatorColumn) {
265
+ this._meta.discriminatorFieldName = this._meta.discriminatorColumn;
266
+ this._meta.discriminatorColumn = this._meta.discriminator;
267
+ }
268
+ else {
269
+ this._meta.discriminatorColumn ??= this._meta.discriminator;
270
+ this._meta.discriminator ??= this._meta.discriminatorColumn;
271
+ }
254
272
  // Abstract TPT entities keep their name because they have their own table
255
273
  const isTPT = this._meta.inheritance === 'tpt' || this.isPartOfTPTHierarchy();
256
274
  if (this._meta.abstract && !this._meta.discriminatorColumn && !isTPT) {
@@ -76,6 +76,7 @@ export declare class MetadataDiscovery {
76
76
  private initPolyEmbeddables;
77
77
  private initPolymorphicRelation;
78
78
  private initEmbeddables;
79
+ private sameRelationTargetRoot;
79
80
  private initSingleTableInheritance;
80
81
  /**
81
82
  * First pass of TPT initialization: sets up hierarchy relationships
@@ -114,7 +114,12 @@ export class MetadataDiscovery {
114
114
  }
115
115
  else {
116
116
  const name = prop.name;
117
- if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
117
+ // For to-one relations, only swap to the accessor for the documented
118
+ // backing-field convention (a `_`-prefixed property like `_draft` paired
119
+ // with `accessor: 'draft'`). The non-prefixed sibling-helper pattern
120
+ // (`author` + `accessor: 'authorId'`) keeps the property name as the
121
+ // canonical FK column source, so this stays non-breaking.
122
+ if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED || name.startsWith('_')) {
118
123
  prop.name = prop.accessor;
119
124
  }
120
125
  this.initRelation(prop);
@@ -568,6 +573,12 @@ export class MetadataDiscovery {
568
573
  }
569
574
  }
570
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
+ }
571
582
  this.#validator.validateEntityDefinition(this.#metadata, meta.class, this.#config.get('discovery'));
572
583
  for (const prop of Object.values(meta.properties)) {
573
584
  this.initNullability(prop);
@@ -594,6 +605,10 @@ export class MetadataDiscovery {
594
605
  prop.pivotEntity = pivotMeta.class;
595
606
  if (prop.inversedBy) {
596
607
  prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
608
+ const targetRoot = prop.targetMeta.root;
609
+ if (targetRoot !== prop.targetMeta && targetRoot.properties[prop.inversedBy]) {
610
+ targetRoot.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
611
+ }
597
612
  }
598
613
  // Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
599
614
  // owner prop. Covers three cases:
@@ -1019,10 +1034,14 @@ export class MetadataDiscovery {
1019
1034
  meta.propertyOrder.set(prop.name, (order += 0.01));
1020
1035
  });
1021
1036
  }
1022
- meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1023
- meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1024
- meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1025
- meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
1037
+ // TPT children have their own tables that don't contain the parent's columns,
1038
+ // so propagating parent indexes/uniques/checks/triggers would target missing columns.
1039
+ if (meta.inheritanceType !== 'tpt' || !meta.tptParent) {
1040
+ meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1041
+ meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1042
+ meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1043
+ meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
1044
+ }
1026
1045
  const pks = Object.values(meta.properties)
1027
1046
  .filter(p => p.primary)
1028
1047
  .map(p => p.name);
@@ -1253,6 +1272,19 @@ export class MetadataDiscovery {
1253
1272
  });
1254
1273
  }
1255
1274
  }
1275
+ sameRelationTargetRoot(rootProp, prop) {
1276
+ if (!rootProp || rootProp.kind !== prop.kind) {
1277
+ return false;
1278
+ }
1279
+ if (prop.kind !== ReferenceKind.MANY_TO_ONE &&
1280
+ prop.kind !== ReferenceKind.ONE_TO_ONE &&
1281
+ prop.kind !== ReferenceKind.ONE_TO_MANY &&
1282
+ prop.kind !== ReferenceKind.MANY_TO_MANY) {
1283
+ return false;
1284
+ }
1285
+ const aRoot = this.#metadata.getByClassName(prop.type, false)?.root;
1286
+ return aRoot != null && aRoot === this.#metadata.getByClassName(rootProp.type, false)?.root;
1287
+ }
1256
1288
  initSingleTableInheritance(meta, metadata) {
1257
1289
  if (meta.root !== meta && !meta.__processed) {
1258
1290
  meta.root = metadata.find(m => m.class === meta.root.class);
@@ -1293,12 +1325,24 @@ export class MetadataDiscovery {
1293
1325
  Object.values(meta.properties).forEach(prop => {
1294
1326
  const newProp = { ...prop };
1295
1327
  const rootProp = meta.root.properties[prop.name];
1328
+ // A child that narrows a relation to a subclass of the root's declared
1329
+ // target (same STI hierarchy) shares the FK column with the root; treat
1330
+ // that as matching so the rename branch below doesn't run (which would
1331
+ // crash — `targetMeta` is only populated later, in `initRelation`).
1332
+ const narrowedRelationOverride = rootProp != null && rootProp.type !== prop.type && this.sameRelationTargetRoot(rootProp, prop);
1296
1333
  // stiMerged is set during inlineProperties when a property was merged
1297
1334
  // from multiple polymorphic variants with different types. The flag is
1298
1335
  // cleared implicitly when the first child claims the root property via
1299
1336
  // addProperty below, so subsequent children correctly trigger renaming.
1300
- const typesMatch = rootProp?.type === prop.type || rootProp?.stiMerged === true;
1337
+ const typesMatch = rootProp?.type === prop.type || rootProp?.stiMerged === true || narrowedRelationOverride;
1338
+ // Inverse-side relations (1:m, inverse m:n) have no FK column on this
1339
+ // entity, so the rename branch — which exists to disambiguate physical
1340
+ // columns shared across STI children — doesn't apply. Skipping it also
1341
+ // avoids crashing on `[...rootProp.fieldNames]` since fieldNames is
1342
+ // never populated for inverse-side properties.
1343
+ const isInverseSideRelation = prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner);
1301
1344
  if (rootProp &&
1345
+ !isInverseSideRelation &&
1302
1346
  (!typesMatch ||
1303
1347
  (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))) {
1304
1348
  const name = newProp.name;
@@ -1347,6 +1391,20 @@ export class MetadataDiscovery {
1347
1391
  }
1348
1392
  newProp.nullable = true;
1349
1393
  newProp.inherited = !rootProp;
1394
+ // For narrowed relation overrides, keep the root's declaration intact so
1395
+ // the full target union (e.g. `Food`) is preserved for populates from the
1396
+ // abstract root — otherwise the last child processed would win.
1397
+ if (narrowedRelationOverride) {
1398
+ return;
1399
+ }
1400
+ // STI siblings can declare the same property name on inverse-side relations
1401
+ // pointing to disjoint targets (e.g. `Dog.items → DogItem` and
1402
+ // `Cat.items → CatItem` with no shared base). There's no physical column
1403
+ // to manage at root, and propagating would clobber the first child's
1404
+ // declaration; each child keeps its local typed property instead.
1405
+ if (isInverseSideRelation && rootProp && rootProp.type !== prop.type) {
1406
+ return;
1407
+ }
1350
1408
  meta.root.addProperty(newProp);
1351
1409
  });
1352
1410
  meta.tableName = meta.root.tableName;
@@ -1362,10 +1420,10 @@ export class MetadataDiscovery {
1362
1420
  if (meta.root !== meta) {
1363
1421
  meta.root = metadata.find(m => m.class === meta.root.class);
1364
1422
  }
1365
- const inheritance = meta.inheritance;
1366
- if (inheritance === 'tpt' && meta.root === meta) {
1367
- meta.inheritanceType = 'tpt';
1368
- meta.tptChildren = [];
1423
+ // init the root eagerly so children iterated before the root (e.g. alphabetical glob order) still get linked
1424
+ if (meta.root.inheritance === 'tpt' && meta.root.inheritanceType !== 'tpt') {
1425
+ meta.root.inheritanceType = 'tpt';
1426
+ meta.root.tptChildren ??= [];
1369
1427
  }
1370
1428
  if (meta.root.inheritanceType !== 'tpt') {
1371
1429
  return;
@@ -1524,13 +1582,18 @@ export class MetadataDiscovery {
1524
1582
  });
1525
1583
  }
1526
1584
  createDiscriminatorProperty(meta) {
1527
- meta.addProperty({
1585
+ const prop = {
1528
1586
  name: meta.discriminatorColumn,
1529
1587
  type: 'string',
1530
1588
  enum: true,
1531
1589
  kind: ReferenceKind.SCALAR,
1532
1590
  userDefined: false,
1533
- });
1591
+ };
1592
+ // honor the column-name override set via `discriminatorColumn` when `discriminator` is also given
1593
+ if (meta.discriminatorFieldName) {
1594
+ prop.fieldName = meta.discriminatorFieldName;
1595
+ }
1596
+ meta.addProperty(prop);
1534
1597
  }
1535
1598
  initAutoincrement(meta) {
1536
1599
  const pks = meta.getPrimaryProps();
@@ -1553,7 +1616,7 @@ export class MetadataDiscovery {
1553
1616
  const table = this.createSchemaTable(meta);
1554
1617
  for (const check of meta.checks) {
1555
1618
  const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
1556
- check.name ??= this.#namingStrategy.indexName(meta.tableName, fieldNames, 'check');
1619
+ check.name ??= this.#platform.getIndexName(meta.tableName, fieldNames, 'check');
1557
1620
  if (check.expression instanceof Function) {
1558
1621
  check.expression = check.expression(columns, table);
1559
1622
  }
@@ -1578,7 +1641,7 @@ export class MetadataDiscovery {
1578
1641
  }
1579
1642
  if (expression) {
1580
1643
  meta.checks.push({
1581
- name: this.#namingStrategy.indexName(meta.tableName, prop.fieldNames, 'check'),
1644
+ name: this.#platform.getIndexName(meta.tableName, prop.fieldNames, 'check'),
1582
1645
  property: prop.name,
1583
1646
  expression,
1584
1647
  });
@@ -1754,6 +1817,7 @@ export class MetadataDiscovery {
1754
1817
  prop.customType &&
1755
1818
  !(prop.customType instanceof t.array) &&
1756
1819
  !(prop.customType instanceof t.enumArray) &&
1820
+ !(prop.customType instanceof t.json) &&
1757
1821
  prop.kind === ReferenceKind.SCALAR) {
1758
1822
  const innerType = prop.customType;
1759
1823
  innerType.platform = this.#platform;
@@ -1789,7 +1853,7 @@ export class MetadataDiscovery {
1789
1853
  if (!prop.customType && prop.array && prop.items) {
1790
1854
  prop.customType = new t.enumArray(`${meta.className}.${prop.name}`, prop.items);
1791
1855
  }
1792
- const isArray = prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]');
1856
+ const isArray = prop.array || prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]');
1793
1857
  if (objectEmbeddable && !prop.customType && isArray) {
1794
1858
  prop.customType = new t.json();
1795
1859
  }
@@ -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
  /**
@@ -1,4 +1,5 @@
1
1
  import { Utils } from '../utils/Utils.js';
2
+ import { normalizePartitionNameForComparison, splitCommaSeparatedIdentifiers } from '../utils/partition-utils.js';
2
3
  import { MetadataError } from '../errors.js';
3
4
  import { ReferenceKind } from '../enums.js';
4
5
  /**
@@ -28,6 +29,9 @@ export class MetadataValidator {
28
29
  // Virtual entities (expression without view flag) have restrictions - no PKs, limited relation types
29
30
  // Note: meta.virtual is set later in sync(), so we check for expression && !view here
30
31
  if (meta.virtual || (meta.expression && !meta.view)) {
32
+ if (meta.partitionBy) {
33
+ throw new MetadataError(`Virtual entity ${meta.className} cannot define partitionBy`);
34
+ }
31
35
  for (const prop of Utils.values(meta.properties)) {
32
36
  if (![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED, ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
33
37
  throw new MetadataError(`Only scalars, embedded properties and to-many relations are allowed inside virtual entity. Found '${prop.kind}' in ${meta.className}.${prop.name}`);
@@ -46,6 +50,7 @@ export class MetadataValidator {
46
50
  this.validateDuplicateFieldNames(meta, options);
47
51
  this.validateIndexes(meta, meta.indexes ?? [], 'index');
48
52
  this.validateIndexes(meta, meta.uniques ?? [], 'unique');
53
+ this.validatePartitioning(meta);
49
54
  this.validatePropertyNames(meta);
50
55
  for (const prop of Utils.values(meta.properties)) {
51
56
  if (prop.kind !== ReferenceKind.SCALAR) {
@@ -57,6 +62,41 @@ export class MetadataValidator {
57
62
  }
58
63
  }
59
64
  }
65
+ validateRoutineDefinition(routine) {
66
+ if (typeof routine.name !== 'string' || routine.name.trim() === '') {
67
+ throw new MetadataError(`Routine is missing the required 'name' option.`);
68
+ }
69
+ if (!routine.type) {
70
+ throw new MetadataError(`Routine ${routine.name} is missing the required 'type' option ('procedure' | 'function').`);
71
+ }
72
+ if (routine.body != null && routine.expression != null) {
73
+ throw new MetadataError(`Routine ${routine.name} defines both 'body' and 'expression'. Use one or the other.`);
74
+ }
75
+ if (routine.body == null && routine.expression == null && routine.bodyJs == null) {
76
+ throw new MetadataError(`Routine ${routine.name} must define a 'body', 'expression', or 'bodyJs'.`);
77
+ }
78
+ if (routine.type === 'function' && routine.returns == null) {
79
+ throw new MetadataError(`Function routine ${routine.name} must declare a 'returns' option.`);
80
+ }
81
+ if (routine.type === 'procedure' && routine.bodyJs != null) {
82
+ throw new MetadataError(`Routine ${routine.name} declares 'bodyJs' on a procedure. JS fallbacks are only supported for functions — SQLite has no analog for stored procedures.`);
83
+ }
84
+ for (const param of routine.params) {
85
+ const dir = param.direction;
86
+ if (dir !== 'in' && dir !== 'out' && dir !== 'inout') {
87
+ throw new MetadataError(`Routine ${routine.name}.${param.name} has invalid direction '${dir}'. Expected 'in', 'out', or 'inout'.`);
88
+ }
89
+ if ((dir === 'out' || dir === 'inout') && !param.ref) {
90
+ throw new MetadataError(`Routine ${routine.name}.${param.name} is declared as '${dir}' but missing 'ref: true'. OUT/INOUT parameters must be passed as ScalarReference.`);
91
+ }
92
+ if (dir === 'in' && param.ref) {
93
+ throw new MetadataError(`Routine ${routine.name}.${param.name} declares 'ref: true' on an IN parameter. ScalarReference wrapping is only meaningful for OUT/INOUT parameters.`);
94
+ }
95
+ if (routine.type === 'function' && dir !== 'in') {
96
+ throw new MetadataError(`Function routine ${routine.name}.${param.name} declares direction '${dir}'. Functions only support IN parameters — use a procedure for OUT/INOUT semantics.`);
97
+ }
98
+ }
99
+ }
60
100
  validateDiscovered(discovered, options) {
61
101
  if (discovered.length === 0 && options.warnWhenNoEntities) {
62
102
  throw MetadataError.noEntityDiscovered();
@@ -111,6 +151,189 @@ export class MetadataValidator {
111
151
  }
112
152
  });
113
153
  }
154
+ validatePartitioning(meta) {
155
+ if (!meta.partitionBy) {
156
+ return;
157
+ }
158
+ if (!this.hasPartitionExpression(meta.partitionBy.expression)) {
159
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: missing expression`);
160
+ }
161
+ // Inheritance (STI/TPT) and partitioning both drive table layout, so combining them would
162
+ // require non-trivial DDL coordination that the schema generator does not produce today.
163
+ const hasInheritance = !!meta.root.discriminatorColumn || meta.root.inheritanceType === 'tpt' || meta.root.inheritance === 'tpt';
164
+ if (hasInheritance) {
165
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: combining partitioning with inheritance is not supported`);
166
+ }
167
+ this.validatePartitionKeyConstraints(meta);
168
+ if (meta.partitionBy.type === 'hash') {
169
+ const { partitions } = meta.partitionBy;
170
+ if (Array.isArray(partitions)) {
171
+ if (partitions.length === 0) {
172
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition name list must not be empty`);
173
+ }
174
+ const blank = partitions.find(name => typeof name !== 'string' || !name.trim());
175
+ if (blank !== undefined) {
176
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition names must be non-empty strings`);
177
+ }
178
+ const ambiguous = partitions.find(name => !this.hasValidPartitionName(name));
179
+ if (ambiguous) {
180
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguous}' contains more than one '.' — use at most one '.' to separate schema from table`);
181
+ }
182
+ const duplicate = this.findDuplicatePartitionName(partitions);
183
+ if (duplicate !== undefined) {
184
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate hash partition name '${duplicate}'`);
185
+ }
186
+ return;
187
+ }
188
+ if (typeof partitions !== 'number' || !Number.isInteger(partitions) || partitions < 1) {
189
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition count must be a positive integer`);
190
+ }
191
+ return;
192
+ }
193
+ if (!Array.isArray(meta.partitionBy.partitions) || meta.partitionBy.partitions.length === 0) {
194
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: list/range partitions must be a non-empty array`);
195
+ }
196
+ if (meta.partitionBy.partitions.some(partition => !partition.values?.trim())) {
197
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: every partition must define values`);
198
+ }
199
+ const ambiguousName = meta.partitionBy.partitions.find(partition => partition.name != null && !this.hasValidPartitionName(partition.name));
200
+ if (ambiguousName) {
201
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguousName.name}' contains more than one '.' — use at most one '.' to separate schema from table`);
202
+ }
203
+ // Include auto-generated default names (`${tableName}_${index}`, matching
204
+ // `createExplicitPartitions` in the sql package) so an explicit name that collides with
205
+ // an unnamed peer's default is caught here rather than at DDL execution time.
206
+ const resolvedNames = meta.partitionBy.partitions.map((partition, index) => partition.name ?? `${meta.tableName}_${index}`);
207
+ const duplicate = this.findDuplicatePartitionName(resolvedNames);
208
+ if (duplicate !== undefined) {
209
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate partition name '${duplicate}'`);
210
+ }
211
+ }
212
+ /**
213
+ * Find the first partition name whose normalized form (case-folded for unquoted segments,
214
+ * quoted segments preserved) has already been seen. Returns the offending name in its
215
+ * original form for the error message.
216
+ */
217
+ findDuplicatePartitionName(names) {
218
+ const seen = new Set();
219
+ for (const name of names) {
220
+ const normalized = normalizePartitionNameForComparison(name);
221
+ if (seen.has(normalized)) {
222
+ return name;
223
+ }
224
+ seen.add(normalized);
225
+ }
226
+ return undefined;
227
+ }
228
+ /**
229
+ * Partition names may be bare (`child`), schema-qualified (`schema.child`), or use quoted
230
+ * identifiers (`"my.schema"."child"`). Reject anything with more than one unquoted `.`.
231
+ */
232
+ hasValidPartitionName(name) {
233
+ let depth = 0;
234
+ let dots = 0;
235
+ for (let i = 0; i < name.length; i++) {
236
+ const ch = name[i];
237
+ if (ch === '"') {
238
+ if (name[i + 1] === '"') {
239
+ i++;
240
+ continue;
241
+ }
242
+ depth = depth === 0 ? 1 : 0;
243
+ continue;
244
+ }
245
+ if (ch === '.' && depth === 0) {
246
+ dots++;
247
+ }
248
+ }
249
+ return dots <= 1;
250
+ }
251
+ hasPartitionExpression(expression) {
252
+ if (expression == null) {
253
+ return false;
254
+ }
255
+ if (typeof expression === 'function') {
256
+ return true;
257
+ }
258
+ if (Array.isArray(expression)) {
259
+ return expression.length > 0 && expression.every(key => typeof key === 'string' && key.trim().length > 0);
260
+ }
261
+ return String(expression).trim().length > 0;
262
+ }
263
+ validatePartitionKeyConstraints(meta) {
264
+ const partitionFields = this.getPartitionKeyFields(meta);
265
+ if (!partitionFields?.length) {
266
+ return;
267
+ }
268
+ const primaryKeyFields = meta.root.getPrimaryProps().flatMap(prop => prop.fieldNames);
269
+ if (partitionFields.some(field => !primaryKeyFields.includes(field))) {
270
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: primary key must include partition key columns '${partitionFields.join("', '")}'`);
271
+ }
272
+ for (const prop of Object.values(meta.root.properties)) {
273
+ if (!prop.unique || !prop.fieldNames?.length) {
274
+ continue;
275
+ }
276
+ if (partitionFields.some(field => !prop.fieldNames.includes(field))) {
277
+ throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: unique property ${meta.root.className}.${prop.name} must include partition key columns '${partitionFields.join("', '")}'`);
278
+ }
279
+ }
280
+ for (const unique of meta.root.uniques ?? []) {
281
+ const fields = this.getConstraintFields(meta.root, unique.properties);
282
+ if (!fields?.length) {
283
+ continue;
284
+ }
285
+ if (partitionFields.some(field => !fields.includes(field))) {
286
+ const constraint = unique.name ? `unique constraint '${unique.name}'` : 'unique constraint';
287
+ throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: ${constraint} must include partition key columns '${partitionFields.join("', '")}'`);
288
+ }
289
+ }
290
+ }
291
+ /**
292
+ * Returns the list of physical field names that a partition expression references, or
293
+ * `undefined` when the expression is opaque (callback, or raw SQL like `date_trunc('day', x)`
294
+ * that we cannot statically parse). Opaque expressions intentionally bypass the primary-key /
295
+ * unique-constraint coverage checks — users are trusted to ensure the referenced columns are
296
+ * part of the partition key, since PostgreSQL will surface the violation at DDL execution.
297
+ */
298
+ getPartitionKeyFields(meta) {
299
+ const expression = meta.partitionBy?.expression;
300
+ if (!expression || typeof expression === 'function') {
301
+ return undefined;
302
+ }
303
+ if (Array.isArray(expression)) {
304
+ return expression.map(key => this.resolvePartitionKeyField(meta, key));
305
+ }
306
+ const keys = splitCommaSeparatedIdentifiers(String(expression).trim());
307
+ if (!keys) {
308
+ return undefined;
309
+ }
310
+ return keys.map(key => this.resolvePartitionKeyField(meta, key));
311
+ }
312
+ resolvePartitionKeyField(meta, key) {
313
+ const trimmed = key.trim().replaceAll('"', '');
314
+ if (!trimmed) {
315
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: empty partition key`);
316
+ }
317
+ const prop = meta.root.properties[trimmed] ??
318
+ Object.values(meta.root.properties).find(candidate => candidate.fieldNames?.length === 1 && candidate.fieldNames[0] === trimmed);
319
+ if (!prop) {
320
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: unknown partition key '${key.trim()}'`);
321
+ }
322
+ if (prop.fieldNames?.length !== 1) {
323
+ throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition key '${key.trim()}' maps to multiple columns ('${prop.fieldNames?.join("', '")}'); list them explicitly as partition keys`);
324
+ }
325
+ return prop.fieldNames[0];
326
+ }
327
+ getConstraintFields(meta, properties) {
328
+ if (!properties) {
329
+ return undefined;
330
+ }
331
+ const fields = Utils.asArray(properties).flatMap(propName => {
332
+ const prop = meta.root.properties[propName];
333
+ return prop?.fieldNames ?? [];
334
+ });
335
+ return fields.length > 0 ? fields : undefined;
336
+ }
114
337
  validateReference(meta, prop, options) {
115
338
  // references do have types
116
339
  if (!prop.type) {
@@ -160,7 +383,7 @@ export class MetadataValidator {
160
383
  * Composite unique constraints are not sufficient for targetKey.
161
384
  */
162
385
  isPropertyUnique(prop, meta) {
163
- if (prop.unique) {
386
+ if (prop.unique || (prop.primary && meta.primaryKeys.length === 1)) {
164
387
  return true;
165
388
  }
166
389
  // Check for single-property unique constraint via @Unique decorator
@@ -358,6 +581,10 @@ export class MetadataValidator {
358
581
  if (!meta.expression) {
359
582
  throw MetadataError.viewEntityWithoutExpression(meta);
360
583
  }
584
+ // Views are not partitionable - reject explicitly instead of silently ignoring
585
+ if (meta.partitionBy) {
586
+ throw new MetadataError(`View entity ${meta.className} cannot define partitionBy`);
587
+ }
361
588
  // Validate indexes if present
362
589
  this.validateIndexes(meta, meta.indexes ?? [], 'index');
363
590
  this.validateIndexes(meta, meta.uniques ?? [], 'unique');