@mikro-orm/core 7.1.0-dev.9 → 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 (53) hide show
  1. package/EntityManager.d.ts +47 -3
  2. package/EntityManager.js +150 -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 +20 -6
  7. package/entity/BaseEntity.d.ts +68 -1
  8. package/entity/BaseEntity.js +18 -0
  9. package/entity/Collection.d.ts +1 -1
  10. package/entity/Collection.js +4 -1
  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/Reference.d.ts +42 -1
  16. package/entity/Reference.js +9 -0
  17. package/entity/defineEntity.d.ts +69 -16
  18. package/entity/defineEntity.js +17 -0
  19. package/entity/utils.js +4 -5
  20. package/enums.d.ts +8 -1
  21. package/index.d.ts +1 -1
  22. package/metadata/EntitySchema.js +18 -0
  23. package/metadata/MetadataDiscovery.d.ts +1 -0
  24. package/metadata/MetadataDiscovery.js +79 -15
  25. package/metadata/MetadataValidator.d.ts +26 -0
  26. package/metadata/MetadataValidator.js +228 -1
  27. package/metadata/Routine.d.ts +74 -0
  28. package/metadata/Routine.js +140 -0
  29. package/metadata/index.d.ts +1 -0
  30. package/metadata/index.js +1 -0
  31. package/metadata/types.d.ts +75 -8
  32. package/package.json +1 -1
  33. package/platforms/Platform.d.ts +13 -3
  34. package/platforms/Platform.js +51 -6
  35. package/serialization/EntitySerializer.js +4 -3
  36. package/typings.d.ts +323 -21
  37. package/typings.js +32 -12
  38. package/unit-of-work/UnitOfWork.js +43 -5
  39. package/utils/AbstractMigrator.d.ts +9 -4
  40. package/utils/AbstractMigrator.js +60 -28
  41. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  42. package/utils/AbstractSchemaGenerator.js +4 -1
  43. package/utils/Configuration.d.ts +30 -0
  44. package/utils/Configuration.js +45 -0
  45. package/utils/EntityComparator.js +1 -1
  46. package/utils/TransactionManager.js +2 -0
  47. package/utils/Utils.js +1 -1
  48. package/utils/index.d.ts +1 -0
  49. package/utils/index.js +1 -0
  50. package/utils/partition-utils.d.ts +17 -0
  51. package/utils/partition-utils.js +79 -0
  52. package/utils/upsert-utils.d.ts +2 -0
  53. package/utils/upsert-utils.js +26 -1
@@ -7,10 +7,11 @@ import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js
7
7
  import { Reference } from './entity/Reference.js';
8
8
  import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
9
9
  import type { CountByOptions, CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
10
- import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
10
+ import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, RoutineArgs, RoutineReturn, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
11
+ import type { Routine } from './metadata/Routine.js';
11
12
  import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
12
13
  import type { MetadataStorage } from './metadata/MetadataStorage.js';
13
- import type { Transaction } from './connections/Connection.js';
14
+ import type { AbortQueryOptions, InflightQueryAbortStrategy, Transaction } from './connections/Connection.js';
14
15
  import { EventManager } from './events/EventManager.js';
15
16
  import type { EntityComparator } from './utils/EntityComparator.js';
16
17
  /**
@@ -33,6 +34,10 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
33
34
  /** The context name of this EntityManager, derived from the ORM configuration. */
34
35
  readonly name: string;
35
36
  protected loggerContext?: Dictionary;
37
+ /** @internal */
38
+ protected signal?: AbortSignal;
39
+ /** @internal */
40
+ protected inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
36
41
  /**
37
42
  * @internal
38
43
  */
@@ -342,6 +347,29 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
342
347
  * Fires native update query. Calling this has no side effects on the context (identity map).
343
348
  */
344
349
  nativeUpdate<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, data: EntityData<Entity>, options?: UpdateOptions<Entity>): Promise<number>;
350
+ /**
351
+ * Invokes a stored procedure or function declared via the {@link Routine} class. Arg and return
352
+ * types are inferred from the literal config; use {@link Routine.create} to refine when needed.
353
+ *
354
+ * Procedures emitting result sets (MySQL `SELECT`s, PG/Oracle refcursor OUT params) return
355
+ * `Dictionary[][]`; MSSQL multi-result procs aren't exposed here, use `em.getConnection().execute()`.
356
+ *
357
+ * Mongo throws. SQLite bridges `bodyJs` functions via better-sqlite3 UDFs (procedures and
358
+ * function-without-bodyJs throw); libSQL throws unconditionally. Oracle calls run on their own
359
+ * pool connection with `autoCommit: true`, so wrapping in `em.transactional(...)` throws.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
364
+ *
365
+ * const hashRef = new ScalarReference<string>();
366
+ * await em.callRoutine(AddRecord, { p_name: 'jon', p_age: 30, p_hash: hashRef });
367
+ *
368
+ * const TwoCursors = Routine.create<Record<string, never>, unknown[][]>({ ... });
369
+ * const [users, books] = await em.transactional(em => em.callRoutine(TwoCursors, {}));
370
+ * ```
371
+ */
372
+ callRoutine<R extends Routine>(routine: R, args: RoutineArgs<R>): Promise<RoutineReturn<R>>;
345
373
  /**
346
374
  * Fires native delete query. Calling this has no side effects on the context (identity map).
347
375
  */
@@ -532,6 +560,13 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
532
560
  * Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
533
561
  */
534
562
  getTransactionContext<T extends Transaction = Transaction>(): T | undefined;
563
+ /**
564
+ * Returns the cancellation defaults configured on this EntityManager (via `em.fork({ signal })`
565
+ * or inherited from a transactional fork). Returns `undefined` when no signal is set.
566
+ *
567
+ * @internal — exposed for subclass drivers and `UnitOfWork`; not part of the public API.
568
+ */
569
+ protected getAbortOptions(): AbortQueryOptions | undefined;
535
570
  /**
536
571
  * Sets the transaction context.
537
572
  */
@@ -559,12 +594,14 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
559
594
  private buildFields;
560
595
  /** @internal */
561
596
  preparePopulate<Entity extends object>(entityName: EntityName<Entity>, options: Pick<FindOptions<Entity, any, any, any>, 'populate' | 'strategy' | 'fields' | 'flags' | 'filters' | 'exclude' | 'populateHints'>, validate?: boolean): Promise<PopulateOptions<Entity>[]>;
597
+ /** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
598
+ private forceSelectInForLimitedPopulate;
562
599
  /**
563
600
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
564
601
  * some additional lazy properties, if so, we reload and merge the data from database
565
602
  */
566
603
  protected shouldRefresh<T extends object, P extends string = never, F extends string = never, E extends string = never>(meta: EntityMetadata<T>, entity: T, options: FindOneOptions<T, P, F, E>): boolean;
567
- protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>): void;
604
+ protected prepareOptions(options: (FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>) & AbortQueryOptions): void;
568
605
  /**
569
606
  * @internal
570
607
  */
@@ -659,4 +696,11 @@ export interface ForkOptions {
659
696
  schema?: string;
660
697
  /** default logger context, can be overridden via {@apilink FindOptions} */
661
698
  loggerContext?: Dictionary;
699
+ /**
700
+ * Default `AbortSignal` applied to every operation on this fork (queries and UoW flushes).
701
+ * Per-call options.signal still takes precedence.
702
+ */
703
+ signal?: AbortSignal;
704
+ /** Default cancellation strategy paired with {@link signal}. */
705
+ inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
662
706
  }
package/EntityManager.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
1
+ import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } from './utils/upsert-utils.js';
2
2
  import { Utils } from './utils/Utils.js';
3
3
  import { Cursor } from './utils/Cursor.js';
4
4
  import { QueryHelper } from './utils/QueryHelper.js';
@@ -49,6 +49,10 @@ export class EntityManager {
49
49
  #disableTransactions;
50
50
  #flushMode;
51
51
  #schema;
52
+ /** @internal */
53
+ signal;
54
+ /** @internal */
55
+ inflightQueryAbortStrategy;
52
56
  #useContext;
53
57
  /**
54
58
  * @internal
@@ -318,13 +322,13 @@ export class EntityManager {
318
322
  }
319
323
  const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
320
324
  const children = [];
321
- const lookUpChildren = (ret, type) => {
322
- const children = types.filter(meta2 => meta2.extends === type);
323
- children.forEach(m => lookUpChildren(ret, m.class));
325
+ const lookUpChildren = (ret, parent) => {
326
+ const children = types.filter(meta2 => meta2.extends && this.metadata.find(meta2.extends) === parent);
327
+ children.forEach(m => lookUpChildren(ret, m));
324
328
  ret.push(...children.filter(c => c.discriminatorValue));
325
329
  return children;
326
330
  };
327
- lookUpChildren(children, meta.class);
331
+ lookUpChildren(children, meta);
328
332
  /* v8 ignore next */
329
333
  where[meta.root.discriminatorColumn] =
330
334
  children.length > 0
@@ -354,10 +358,18 @@ export class EntityManager {
354
358
  const field = hint.field.split(':')[0];
355
359
  const prop = meta.properties[field];
356
360
  const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
357
- const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
361
+ const joined = (strategy === LoadStrategy.JOINED || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) &&
362
+ prop.kind !== ReferenceKind.SCALAR;
358
363
  if (!joined && !hint.filter) {
359
364
  continue;
360
365
  }
366
+ // Polymorphic to-one relations are already filtered via per-target LEFT JOINs created
367
+ // for the `:ref filter` hint; emitting a populate-filter entry here would auto-join only
368
+ // the first target meta and either drop rows pointing to other targets or reference
369
+ // parent-table-only columns on the child sub-table alias for TPT children.
370
+ if (prop.polymorphic && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
371
+ continue;
372
+ }
361
373
  const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
362
374
  const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
363
375
  ...options,
@@ -423,11 +435,13 @@ export class EntityManager {
423
435
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
424
436
  let found = false;
425
437
  for (const hint of populated) {
426
- if (!hint.all) {
438
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
439
+ // inverse 1:1 always produces a JOIN (forced by `joinedProps()`), regardless of strategy
440
+ const willJoin = strategy === LoadStrategy.JOINED || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner);
441
+ if (!hint.all || willJoin) {
427
442
  hint.filter = true;
428
443
  }
429
- const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
430
- if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
444
+ if (hint.field === `${prop.name}:ref` || (hint.filter && willJoin)) {
431
445
  found = true;
432
446
  }
433
447
  }
@@ -441,7 +455,23 @@ export class EntityManager {
441
455
  const prop = meta?.properties[field];
442
456
  if (prop && !ref) {
443
457
  hint.children ??= [];
444
- await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
458
+ const targets = prop.polymorphic && prop.polymorphTargets?.length ? prop.polymorphTargets : [prop.targetMeta];
459
+ for (const targetMeta of targets) {
460
+ const before = hint.children.length;
461
+ await this.autoJoinRefsForFilters(targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
462
+ // For polymorphic relations, `hint.children` is applied to every polymorph target during
463
+ // population, so drop any auto-added hint whose field does not exist on every target —
464
+ // otherwise the shared `:ref` would crash when applied to a target lacking it (GH #7722).
465
+ if (targets.length > 1 && hint.children.length > before) {
466
+ const added = hint.children.splice(before);
467
+ for (const h of added) {
468
+ const f = h.field.split(':')[0];
469
+ if (targets.every(t => f in t.properties)) {
470
+ hint.children.push(h);
471
+ }
472
+ }
473
+ }
474
+ }
445
475
  }
446
476
  }
447
477
  }
@@ -649,7 +679,7 @@ export class EntityManager {
649
679
  where = await em.processWhere(entityName, where, options, 'read');
650
680
  validateEmptyWhere(where);
651
681
  em.checkLockRequirements(options.lockMode, meta);
652
- const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC;
682
+ const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.NONE || options.lockMode === LockMode.OPTIMISTIC;
653
683
  if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) {
654
684
  return em.lockAndPopulate(meta, entity, where, options);
655
685
  }
@@ -790,7 +820,10 @@ export class EntityManager {
790
820
  data = QueryHelper.processObjectParams(data);
791
821
  validateParams(data, 'insert data');
792
822
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
793
- await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: data, em, meta }, meta);
823
+ await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: entity ?? data, em, meta }, meta);
824
+ if (entity) {
825
+ data = em.#comparator.prepareEntity(entity);
826
+ }
794
827
  }
795
828
  const ret = await em.driver.nativeUpdate(entityName, where, data, {
796
829
  ctx: em.#transactionContext,
@@ -839,6 +872,7 @@ export class EntityManager {
839
872
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
840
873
  }
841
874
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
875
+ resetUntouchedCollections(meta, entity);
842
876
  const snapshot = this.#comparator.prepareEntity(entity);
843
877
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
844
878
  if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
@@ -905,6 +939,7 @@ export class EntityManager {
905
939
  const allWhere = [];
906
940
  const entities = new Map();
907
941
  const entitiesByData = new Map();
942
+ const entitiesByAllDataIdx = new Map();
908
943
  for (let i = 0; i < data.length; i++) {
909
944
  let row = data[i];
910
945
  let where;
@@ -918,6 +953,7 @@ export class EntityManager {
918
953
  }
919
954
  where = helper(entity).getPrimaryKey();
920
955
  em.#entityFactory.assignDefaultValues(entity, meta);
956
+ entitiesByAllDataIdx.set(allData.length, entity);
921
957
  row = em.#comparator.prepareEntity(entity);
922
958
  }
923
959
  else {
@@ -965,6 +1001,9 @@ export class EntityManager {
965
1001
  const entity = entitiesByData.get(dto) ?? dto;
966
1002
  await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
967
1003
  }
1004
+ for (const [idx, entity] of entitiesByAllDataIdx) {
1005
+ allData[idx] = em.#comparator.prepareEntity(entity);
1006
+ }
968
1007
  }
969
1008
  const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
970
1009
  ctx: em.#transactionContext,
@@ -1065,6 +1104,7 @@ export class EntityManager {
1065
1104
  }
1066
1105
  for (const [entity] of entities) {
1067
1106
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
1107
+ resetUntouchedCollections(meta, entity);
1068
1108
  const snapshot = this.#comparator.prepareEntity(entity);
1069
1109
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
1070
1110
  }
@@ -1158,6 +1198,7 @@ export class EntityManager {
1158
1198
  */
1159
1199
  async lock(entity, lockMode, options = {}) {
1160
1200
  options = Utils.isPlainObject(options) ? options : { lockVersion: options };
1201
+ this.getContext(false).prepareOptions(options);
1161
1202
  await this.getUnitOfWork().lock(entity, { lockMode, ...options });
1162
1203
  }
1163
1204
  /**
@@ -1179,9 +1220,6 @@ export class EntityManager {
1179
1220
  helper(data).setSchema(options.schema);
1180
1221
  }
1181
1222
  if (!helper(data).__managed) {
1182
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1183
- em.#unitOfWork.getPersistStack().delete(data);
1184
- // it can be also in the identity map if it had a PK value already
1185
1223
  em.#unitOfWork.unsetIdentity(data);
1186
1224
  }
1187
1225
  const meta = helper(data).__meta;
@@ -1270,9 +1308,6 @@ export class EntityManager {
1270
1308
  helper(row).setSchema(options.schema);
1271
1309
  }
1272
1310
  if (!helper(row).__managed) {
1273
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1274
- em.#unitOfWork.getPersistStack().delete(row);
1275
- // it can be also in the identity map if it had a PK value already
1276
1311
  em.#unitOfWork.unsetIdentity(row);
1277
1312
  }
1278
1313
  const payload = em.#comparator.prepareEntity(row);
@@ -1310,6 +1345,37 @@ export class EntityManager {
1310
1345
  });
1311
1346
  return res.affectedRows;
1312
1347
  }
1348
+ /**
1349
+ * Invokes a stored procedure or function declared via the {@link Routine} class. Arg and return
1350
+ * types are inferred from the literal config; use {@link Routine.create} to refine when needed.
1351
+ *
1352
+ * Procedures emitting result sets (MySQL `SELECT`s, PG/Oracle refcursor OUT params) return
1353
+ * `Dictionary[][]`; MSSQL multi-result procs aren't exposed here, use `em.getConnection().execute()`.
1354
+ *
1355
+ * Mongo throws. SQLite bridges `bodyJs` functions via better-sqlite3 UDFs (procedures and
1356
+ * function-without-bodyJs throw); libSQL throws unconditionally. Oracle calls run on their own
1357
+ * pool connection with `autoCommit: true`, so wrapping in `em.transactional(...)` throws.
1358
+ *
1359
+ * @example
1360
+ * ```ts
1361
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
1362
+ *
1363
+ * const hashRef = new ScalarReference<string>();
1364
+ * await em.callRoutine(AddRecord, { p_name: 'jon', p_age: 30, p_hash: hashRef });
1365
+ *
1366
+ * const TwoCursors = Routine.create<Record<string, never>, unknown[][]>({ ... });
1367
+ * const [users, books] = await em.transactional(em => em.callRoutine(TwoCursors, {}));
1368
+ * ```
1369
+ */
1370
+ async callRoutine(routine, args) {
1371
+ const em = this.getContext(false);
1372
+ // Identity by reference so the user-held import survives bundling and tree-shakes unused routines.
1373
+ if (!em.config.hasRoutine(routine)) {
1374
+ throw new Error(`Routine '${routine.name}' is not registered in the 'routines' config option.`);
1375
+ }
1376
+ const conn = em.driver.getConnection('write');
1377
+ return conn.callRoutine(routine, args, em.#transactionContext);
1378
+ }
1313
1379
  /**
1314
1380
  * Fires native delete query. Calling this has no side effects on the context (identity map).
1315
1381
  */
@@ -1586,7 +1652,7 @@ export class EntityManager {
1586
1652
  const em = this.getContext();
1587
1653
  em.prepareOptions(options);
1588
1654
  const entityName = arr[0].constructor;
1589
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1655
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
1590
1656
  await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
1591
1657
  return entities;
1592
1658
  }
@@ -1619,6 +1685,8 @@ export class EntityManager {
1619
1685
  fork.#filterParams = Utils.copy(em.#filterParams);
1620
1686
  fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
1621
1687
  fork.#schema = options.schema ?? em.#schema;
1688
+ fork.signal = options.signal ?? em.signal;
1689
+ fork.inflightQueryAbortStrategy = options.inflightQueryAbortStrategy ?? em.inflightQueryAbortStrategy;
1622
1690
  if (!options.clear) {
1623
1691
  for (const entity of em.#unitOfWork.getIdentityMap()) {
1624
1692
  fork.#unitOfWork.register(entity);
@@ -1694,6 +1762,22 @@ export class EntityManager {
1694
1762
  getTransactionContext() {
1695
1763
  return this.getContext(false).#transactionContext;
1696
1764
  }
1765
+ /**
1766
+ * Returns the cancellation defaults configured on this EntityManager (via `em.fork({ signal })`
1767
+ * or inherited from a transactional fork). Returns `undefined` when no signal is set.
1768
+ *
1769
+ * @internal — exposed for subclass drivers and `UnitOfWork`; not part of the public API.
1770
+ */
1771
+ getAbortOptions() {
1772
+ const em = this.getContext(false);
1773
+ if (em.signal == null && em.inflightQueryAbortStrategy == null) {
1774
+ return undefined;
1775
+ }
1776
+ return {
1777
+ signal: em.signal,
1778
+ inflightQueryAbortStrategy: em.inflightQueryAbortStrategy,
1779
+ };
1780
+ }
1697
1781
  /**
1698
1782
  * Sets the transaction context.
1699
1783
  */
@@ -1844,8 +1928,17 @@ export class EntityManager {
1844
1928
  return [];
1845
1929
  }
1846
1930
  const meta = this.metadata.find(entityName);
1847
- // infer populate hint if only `fields` are available
1848
- if (!options.populate && options.fields) {
1931
+ // infer populate hints from `fields` when present; merge with explicit `populate` if both are set.
1932
+ // `populateArray` is only kept when the entries are user-provided strings — when this method runs a
1933
+ // second time (e.g. from `lockAndPopulate`) the populate is already normalized and merging would be a no-op.
1934
+ const fields = options.fields ? this.buildFields(options.fields) : undefined;
1935
+ const populateArray = Array.isArray(options.populate) && options.populate.every(p => typeof p === 'string')
1936
+ ? options.populate
1937
+ : undefined;
1938
+ const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
1939
+ const shouldInfer = fields !== undefined && options.populate === undefined;
1940
+ const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
1941
+ if (shouldInfer || shouldMerge) {
1849
1942
  // we need to prune the `populate` hint from to-one relations, as partially loading them does not require their population, we want just the FK
1850
1943
  const pruneToOneRelations = (meta, fields) => {
1851
1944
  const ret = [];
@@ -1879,7 +1972,19 @@ export class EntityManager {
1879
1972
  }
1880
1973
  return Utils.unique(ret);
1881
1974
  };
1882
- options.populate = pruneToOneRelations(meta, this.buildFields(options.fields));
1975
+ const fromFields = pruneToOneRelations(meta, fields);
1976
+ if (shouldInfer) {
1977
+ options.populate = fromFields;
1978
+ }
1979
+ else {
1980
+ // with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
1981
+ // require a JOIN to access sub-fields, whereas bare names are already handled by the driver.
1982
+ // skip paths already in user's `populate` (regardless of `:ref` modifier) to avoid producing
1983
+ // both a `:ref` and a non-`:ref` entry for the same relation
1984
+ const existing = new Set(populateArray.map(p => p.split(':')[0]));
1985
+ const extras = fromFields.filter(f => f.includes('.') && !existing.has(f));
1986
+ options.populate = [...populateArray, ...extras];
1987
+ }
1883
1988
  }
1884
1989
  if (!options.populate) {
1885
1990
  const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude);
@@ -1919,9 +2024,21 @@ export class EntityManager {
1919
2024
  }
1920
2025
  if (options.populateHints) {
1921
2026
  applyPopulateHints(populate, options.populateHints);
2027
+ this.forceSelectInForLimitedPopulate(populate);
1922
2028
  }
1923
2029
  return populate;
1924
2030
  }
2031
+ /** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
2032
+ forceSelectInForLimitedPopulate(populate) {
2033
+ for (const entry of populate) {
2034
+ if (entry.limit != null) {
2035
+ entry.strategy = LoadStrategy.SELECT_IN;
2036
+ }
2037
+ if (entry.children?.length) {
2038
+ this.forceSelectInForLimitedPopulate(entry.children);
2039
+ }
2040
+ }
2041
+ }
1925
2042
  /**
1926
2043
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
1927
2044
  * some additional lazy properties, if so, we reload and merge the data from database
@@ -1953,6 +2070,8 @@ export class EntityManager {
1953
2070
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1954
2071
  }
1955
2072
  options.schema ??= this.#schema;
2073
+ options.signal ??= this.signal;
2074
+ options.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
1956
2075
  options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1957
2076
  }
1958
2077
  /**
@@ -1961,7 +2080,15 @@ export class EntityManager {
1961
2080
  cacheKey(entityName, options, method, where) {
1962
2081
  const { ...opts } = options;
1963
2082
  // ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query
1964
- for (const k of ['ctx', 'strategy', 'flushMode', 'logging', 'loggerContext']) {
2083
+ for (const k of [
2084
+ 'ctx',
2085
+ 'strategy',
2086
+ 'flushMode',
2087
+ 'logging',
2088
+ 'loggerContext',
2089
+ 'signal',
2090
+ 'inflightQueryAbortStrategy',
2091
+ ]) {
1965
2092
  delete opts[k];
1966
2093
  }
1967
2094
  return [Utils.className(entityName), method, opts, where];
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
3
3
  </h1>
4
4
 
5
- TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
5
+ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
6
6
 
7
7
  > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
8
8
 
@@ -19,6 +19,7 @@ Install a driver package for your database:
19
19
 
20
20
  ```sh
21
21
  npm install @mikro-orm/postgresql # PostgreSQL
22
+ npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
22
23
  npm install @mikro-orm/mysql # MySQL
23
24
  npm install @mikro-orm/mariadb # MariaDB
24
25
  npm install @mikro-orm/sqlite # SQLite
@@ -1,8 +1,10 @@
1
1
  import { type Configuration, type ConnectionOptions } from '../utils/Configuration.js';
2
2
  import type { LogContext, Logger } from '../logging/Logger.js';
3
3
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
4
- import type { ConnectionType, Dictionary, MaybePromise, Primary } from '../typings.js';
4
+ import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty } from '../typings.js';
5
+ import type { Routine } from '../metadata/Routine.js';
5
6
  import type { Platform } from '../platforms/Platform.js';
7
+ import type { Type } from '../types/Type.js';
6
8
  import type { TransactionEventBroadcaster } from '../events/TransactionEventBroadcaster.js';
7
9
  import type { IsolationLevel } from '../enums.js';
8
10
  /** Abstract base class for database connections, providing transaction and query execution support. */
@@ -72,6 +74,41 @@ export declare abstract class Connection {
72
74
  rollback(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
73
75
  /** Executes a raw query and returns the result. */
74
76
  abstract execute<T>(query: string, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction): Promise<QueryResult<T> | any | any[]>;
77
+ /** @internal — public callers go through {@link EntityManager.callRoutine}. */
78
+ callRoutine<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
79
+ /**
80
+ * Unwraps a routine argument (resolving any `ScalarReference` wrapper) and, when the param
81
+ * declares a `customType`, marshals it through `convertToDatabaseValue`. `undefined` is
82
+ * normalised to `null` so every driver sees the same shape.
83
+ *
84
+ * @internal
85
+ */
86
+ protected convertRoutineInbound(value: unknown, param: RoutineProperty | undefined): unknown;
87
+ /**
88
+ * Converts a raw database value to its JS representation via the supplied `customType`, when
89
+ * one is declared. Used to marshal scalar function returns and OUT/INOUT values back to the
90
+ * caller before they land in a `ScalarReference` or `em.callRoutine`'s return value.
91
+ *
92
+ * @internal
93
+ */
94
+ protected convertRoutineOutbound<T>(value: unknown, customType: Type<unknown> | undefined): T;
95
+ /**
96
+ * Executes a scalar function routine as `select <qualified>(?, ?, ...) as value`, marshalling
97
+ * IN params on the way in and the return value on the way out. The qualified name is built
98
+ * from `routine.schema`, falling back to the platform's default schema (e.g. `dbo` on MSSQL),
99
+ * which gives MySQL/SQLite a bare name and MSSQL/Oracle the mandatory `schema.name` form.
100
+ *
101
+ * @internal
102
+ */
103
+ protected callRoutineFunction<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
104
+ /**
105
+ * Walks a result row produced by an OUT/INOUT-param SELECT and writes each value into the
106
+ * caller's `ScalarReference` slot. Non-reference args are ignored (the user opted out of
107
+ * receiving the OUT value).
108
+ *
109
+ * @internal
110
+ */
111
+ protected applyRoutineOutParams(row: Dictionary, outParams: RoutineProperty[], args: Record<string, unknown>): void;
75
112
  /** Parses and returns the resolved connection configuration (host, port, user, etc.). */
76
113
  getConnectionOptions(): ConnectionConfig;
77
114
  /** Sets the metadata storage on this connection. */
@@ -104,3 +141,32 @@ export interface ConnectionConfig {
104
141
  }
105
142
  /** Opaque transaction context type, wrapping the driver-specific transaction object. */
106
143
  export type Transaction<T = any> = T & {};
144
+ /**
145
+ * Strategy applied when an `AbortSignal` fires while a query is in flight.
146
+ *
147
+ * - `'ignore query'` — stop awaiting; the query keeps running on the server until it settles
148
+ * (the connection returns to the pool only when the database replies).
149
+ * - `'cancel query'` — ask the database to cancel the running query (e.g. `pg_cancel_backend`,
150
+ * `KILL QUERY`). Falls back to `'ignore query'` if the dialect cannot cancel.
151
+ * Most engines do not cancel writes; partial commits are possible.
152
+ * - `'kill session'` — terminate the database session/process the query runs in
153
+ * (`pg_terminate_backend` etc.). Falls back to `'cancel query'` if not supported.
154
+ *
155
+ * Default: `'ignore query'`.
156
+ *
157
+ * **Streaming queries (`em.stream()` / `qb.stream()`):** the strategy is silently treated as
158
+ * `'ignore query'` because the underlying driver only accepts a plain `AbortSignal` for
159
+ * streamed reads — there is no server-side cancel for an open cursor. The MongoDB driver also
160
+ * has no notion of strategies; only the signal is honored there.
161
+ */
162
+ export type InflightQueryAbortStrategy = 'ignore query' | 'cancel query' | 'kill session';
163
+ /** Per-query cancellation controls forwarded to the underlying driver. */
164
+ export interface AbortQueryOptions {
165
+ /** AbortSignal that cancels the query when fired. */
166
+ signal?: AbortSignal;
167
+ /**
168
+ * Strategy used when the signal fires while the query is in flight. See
169
+ * {@apilink InflightQueryAbortStrategy} for caveats around streams and MongoDB.
170
+ */
171
+ inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
172
+ }
@@ -1,4 +1,5 @@
1
1
  import { Utils } from '../utils/Utils.js';
2
+ import { ScalarReference } from '../entity/Reference.js';
2
3
  /** Abstract base class for database connections, providing transaction and query execution support. */
3
4
  export class Connection {
4
5
  config;
@@ -92,6 +93,69 @@ export class Connection {
92
93
  async rollback(ctx, eventBroadcaster, loggerContext) {
93
94
  throw new Error(`Transactions are not supported by current driver`);
94
95
  }
96
+ /** @internal — public callers go through {@link EntityManager.callRoutine}. */
97
+ async callRoutine(routine, args, ctx) {
98
+ throw new Error(`Stored routines are not supported by the current driver`);
99
+ }
100
+ /**
101
+ * Unwraps a routine argument (resolving any `ScalarReference` wrapper) and, when the param
102
+ * declares a `customType`, marshals it through `convertToDatabaseValue`. `undefined` is
103
+ * normalised to `null` so every driver sees the same shape.
104
+ *
105
+ * @internal
106
+ */
107
+ convertRoutineInbound(value, param) {
108
+ const resolved = value instanceof ScalarReference ? value.unwrap() : value;
109
+ const coerced = resolved === undefined ? null : resolved;
110
+ if (coerced === null || !param?.customType) {
111
+ return coerced;
112
+ }
113
+ return param.customType.convertToDatabaseValue(coerced, this.platform);
114
+ }
115
+ /**
116
+ * Converts a raw database value to its JS representation via the supplied `customType`, when
117
+ * one is declared. Used to marshal scalar function returns and OUT/INOUT values back to the
118
+ * caller before they land in a `ScalarReference` or `em.callRoutine`'s return value.
119
+ *
120
+ * @internal
121
+ */
122
+ convertRoutineOutbound(value, customType) {
123
+ if (value === null || value === undefined || !customType) {
124
+ return value;
125
+ }
126
+ return customType.convertToJSValue(value, this.platform);
127
+ }
128
+ /**
129
+ * Executes a scalar function routine as `select <qualified>(?, ?, ...) as value`, marshalling
130
+ * IN params on the way in and the return value on the way out. The qualified name is built
131
+ * from `routine.schema`, falling back to the platform's default schema (e.g. `dbo` on MSSQL),
132
+ * which gives MySQL/SQLite a bare name and MSSQL/Oracle the mandatory `schema.name` form.
133
+ *
134
+ * @internal
135
+ */
136
+ async callRoutineFunction(routine, args, ctx) {
137
+ const schema = routine.schema ?? this.platform.getDefaultSchemaName();
138
+ const qualified = (schema ? `${this.platform.quoteIdentifier(schema)}.` : '') + this.platform.quoteIdentifier(routine.name);
139
+ const placeholders = routine.params.map(() => '?').join(', ');
140
+ const positional = routine.params.map(p => this.convertRoutineInbound(args[p.name], p));
141
+ const rows = (await this.execute(`select ${qualified}(${placeholders}) as value`, positional, 'all', ctx));
142
+ return this.convertRoutineOutbound(rows[0]?.value, routine.returnCustomType);
143
+ }
144
+ /**
145
+ * Walks a result row produced by an OUT/INOUT-param SELECT and writes each value into the
146
+ * caller's `ScalarReference` slot. Non-reference args are ignored (the user opted out of
147
+ * receiving the OUT value).
148
+ *
149
+ * @internal
150
+ */
151
+ applyRoutineOutParams(row, outParams, args) {
152
+ for (const param of outParams) {
153
+ const ref = args[param.name];
154
+ if (ref instanceof ScalarReference) {
155
+ ref.set(this.convertRoutineOutbound(row[param.name], param.customType));
156
+ }
157
+ }
158
+ }
95
159
  /** Parses and returns the resolved connection configuration (host, port, user, etc.). */
96
160
  getConnectionOptions() {
97
161
  const ret = {};