@mikro-orm/core 7.1.0-dev.4 → 7.1.0-dev.40

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 (60) hide show
  1. package/EntityManager.d.ts +63 -12
  2. package/EntityManager.js +221 -40
  3. package/README.md +2 -1
  4. package/connections/Connection.d.ts +29 -0
  5. package/drivers/IDatabaseDriver.d.ts +45 -7
  6. package/entity/BaseEntity.d.ts +68 -1
  7. package/entity/BaseEntity.js +18 -0
  8. package/entity/Collection.d.ts +6 -3
  9. package/entity/Collection.js +15 -4
  10. package/entity/EntityFactory.js +20 -1
  11. package/entity/EntityLoader.d.ts +8 -1
  12. package/entity/EntityLoader.js +89 -28
  13. package/entity/EntityRepository.d.ts +27 -9
  14. package/entity/EntityRepository.js +12 -0
  15. package/entity/Reference.d.ts +42 -1
  16. package/entity/Reference.js +9 -0
  17. package/entity/defineEntity.d.ts +99 -21
  18. package/entity/defineEntity.js +17 -6
  19. package/entity/utils.js +4 -5
  20. package/enums.d.ts +8 -1
  21. package/errors.d.ts +2 -0
  22. package/errors.js +4 -0
  23. package/index.d.ts +2 -2
  24. package/index.js +1 -1
  25. package/metadata/EntitySchema.js +3 -0
  26. package/metadata/MetadataDiscovery.d.ts +12 -0
  27. package/metadata/MetadataDiscovery.js +166 -20
  28. package/metadata/MetadataValidator.d.ts +24 -0
  29. package/metadata/MetadataValidator.js +202 -1
  30. package/metadata/types.d.ts +71 -4
  31. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  32. package/naming-strategy/NamingStrategy.d.ts +1 -1
  33. package/package.json +1 -1
  34. package/platforms/Platform.d.ts +18 -3
  35. package/platforms/Platform.js +58 -6
  36. package/serialization/EntitySerializer.js +2 -1
  37. package/typings.d.ts +202 -22
  38. package/typings.js +51 -14
  39. package/unit-of-work/UnitOfWork.js +15 -4
  40. package/utils/AbstractMigrator.d.ts +20 -5
  41. package/utils/AbstractMigrator.js +263 -28
  42. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  43. package/utils/AbstractSchemaGenerator.js +4 -1
  44. package/utils/Configuration.d.ts +25 -0
  45. package/utils/Configuration.js +1 -0
  46. package/utils/DataloaderUtils.d.ts +10 -1
  47. package/utils/DataloaderUtils.js +78 -0
  48. package/utils/EntityComparator.js +1 -1
  49. package/utils/QueryHelper.d.ts +16 -0
  50. package/utils/QueryHelper.js +15 -0
  51. package/utils/TransactionManager.js +2 -0
  52. package/utils/Utils.js +1 -1
  53. package/utils/fs-utils.d.ts +2 -0
  54. package/utils/fs-utils.js +7 -1
  55. package/utils/index.d.ts +1 -0
  56. package/utils/index.js +1 -0
  57. package/utils/partition-utils.d.ts +17 -0
  58. package/utils/partition-utils.js +79 -0
  59. package/utils/upsert-utils.d.ts +2 -0
  60. package/utils/upsert-utils.js +26 -1
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
@@ -103,9 +107,6 @@ export class EntityManager {
103
107
  repo(entityName) {
104
108
  return this.getRepository(entityName);
105
109
  }
106
- /**
107
- * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
108
- */
109
110
  async find(entityName, where, options = {}) {
110
111
  if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
111
112
  const em = this.getContext(false);
@@ -116,10 +117,11 @@ export class EntityManager {
116
117
  }
117
118
  const em = this.getContext();
118
119
  em.prepareOptions(options);
120
+ const meta = this.metadata.get(entityName);
121
+ em.validateIndexUsage(meta, where, options);
119
122
  await em.tryFlush(entityName, options);
120
123
  where = await em.processWhere(entityName, where, options, 'read');
121
124
  validateParams(where);
122
- const meta = this.metadata.get(entityName);
123
125
  if (meta.orderBy) {
124
126
  options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
125
127
  }
@@ -203,6 +205,7 @@ export class EntityManager {
203
205
  options.orderBy = options.orderBy || {};
204
206
  options.populate = (await em.preparePopulate(entityName, options));
205
207
  const meta = this.metadata.get(entityName);
208
+ em.validateIndexUsage(meta, options.where ?? {}, options);
206
209
  options = { ...options };
207
210
  // save the original hint value so we know it was infer/all
208
211
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
@@ -319,13 +322,13 @@ export class EntityManager {
319
322
  }
320
323
  const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
321
324
  const children = [];
322
- const lookUpChildren = (ret, type) => {
323
- const children = types.filter(meta2 => meta2.extends === type);
324
- 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));
325
328
  ret.push(...children.filter(c => c.discriminatorValue));
326
329
  return children;
327
330
  };
328
- lookUpChildren(children, meta.class);
331
+ lookUpChildren(children, meta);
329
332
  /* v8 ignore next */
330
333
  where[meta.root.discriminatorColumn] =
331
334
  children.length > 0
@@ -355,10 +358,18 @@ export class EntityManager {
355
358
  const field = hint.field.split(':')[0];
356
359
  const prop = meta.properties[field];
357
360
  const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
358
- 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;
359
363
  if (!joined && !hint.filter) {
360
364
  continue;
361
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
+ }
362
373
  const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
363
374
  const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
364
375
  ...options,
@@ -424,11 +435,13 @@ export class EntityManager {
424
435
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
425
436
  let found = false;
426
437
  for (const hint of populated) {
427
- 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) {
428
442
  hint.filter = true;
429
443
  }
430
- const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
431
- if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
444
+ if (hint.field === `${prop.name}:ref` || (hint.filter && willJoin)) {
432
445
  found = true;
433
446
  }
434
447
  }
@@ -442,7 +455,23 @@ export class EntityManager {
442
455
  const prop = meta?.properties[field];
443
456
  if (prop && !ref) {
444
457
  hint.children ??= [];
445
- 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
+ }
446
475
  }
447
476
  }
448
477
  }
@@ -494,10 +523,6 @@ export class EntityManager {
494
523
  const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c));
495
524
  return conds.length > 1 ? { $and: conds } : conds[0];
496
525
  }
497
- /**
498
- * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
499
- * where the first element is the array of entities, and the second is the count.
500
- */
501
526
  async findAndCount(entityName, where, options = {}) {
502
527
  const em = this.getContext(false);
503
528
  await em.tryFlush(entityName, options);
@@ -631,9 +656,6 @@ export class EntityManager {
631
656
  }
632
657
  return entity;
633
658
  }
634
- /**
635
- * Finds first entity matching your `where` query.
636
- */
637
659
  async findOne(entityName, where, options = {}) {
638
660
  if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
639
661
  const em = this.getContext(false);
@@ -653,10 +675,11 @@ export class EntityManager {
653
675
  }
654
676
  await em.tryFlush(entityName, options);
655
677
  const meta = em.metadata.get(entityName);
678
+ em.validateIndexUsage(meta, where, options);
656
679
  where = await em.processWhere(entityName, where, options, 'read');
657
680
  validateEmptyWhere(where);
658
681
  em.checkLockRequirements(options.lockMode, meta);
659
- const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC;
682
+ const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.NONE || options.lockMode === LockMode.OPTIMISTIC;
660
683
  if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) {
661
684
  return em.lockAndPopulate(meta, entity, where, options);
662
685
  }
@@ -701,12 +724,6 @@ export class EntityManager {
701
724
  await em.storeCache(options.cache, cached, () => helper(entity).toPOJO());
702
725
  return entity;
703
726
  }
704
- /**
705
- * Finds first entity matching your `where` query. If nothing found, it will throw an error.
706
- * If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
707
- * You can override the factory for creating this method via `options.failHandler` locally
708
- * or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
709
- */
710
727
  async findOneOrFail(entityName, where, options = {}) {
711
728
  let entity;
712
729
  let isStrictViolation = false;
@@ -803,7 +820,10 @@ export class EntityManager {
803
820
  data = QueryHelper.processObjectParams(data);
804
821
  validateParams(data, 'insert data');
805
822
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
806
- 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
+ }
807
827
  }
808
828
  const ret = await em.driver.nativeUpdate(entityName, where, data, {
809
829
  ctx: em.#transactionContext,
@@ -852,6 +872,7 @@ export class EntityManager {
852
872
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
853
873
  }
854
874
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
875
+ resetUntouchedCollections(meta, entity);
855
876
  const snapshot = this.#comparator.prepareEntity(entity);
856
877
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
857
878
  if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
@@ -918,6 +939,7 @@ export class EntityManager {
918
939
  const allWhere = [];
919
940
  const entities = new Map();
920
941
  const entitiesByData = new Map();
942
+ const entitiesByAllDataIdx = new Map();
921
943
  for (let i = 0; i < data.length; i++) {
922
944
  let row = data[i];
923
945
  let where;
@@ -931,6 +953,7 @@ export class EntityManager {
931
953
  }
932
954
  where = helper(entity).getPrimaryKey();
933
955
  em.#entityFactory.assignDefaultValues(entity, meta);
956
+ entitiesByAllDataIdx.set(allData.length, entity);
934
957
  row = em.#comparator.prepareEntity(entity);
935
958
  }
936
959
  else {
@@ -978,6 +1001,9 @@ export class EntityManager {
978
1001
  const entity = entitiesByData.get(dto) ?? dto;
979
1002
  await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
980
1003
  }
1004
+ for (const [idx, entity] of entitiesByAllDataIdx) {
1005
+ allData[idx] = em.#comparator.prepareEntity(entity);
1006
+ }
981
1007
  }
982
1008
  const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
983
1009
  ctx: em.#transactionContext,
@@ -1078,6 +1104,7 @@ export class EntityManager {
1078
1104
  }
1079
1105
  for (const [entity] of entities) {
1080
1106
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
1107
+ resetUntouchedCollections(meta, entity);
1081
1108
  const snapshot = this.#comparator.prepareEntity(entity);
1082
1109
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
1083
1110
  }
@@ -1171,6 +1198,7 @@ export class EntityManager {
1171
1198
  */
1172
1199
  async lock(entity, lockMode, options = {}) {
1173
1200
  options = Utils.isPlainObject(options) ? options : { lockVersion: options };
1201
+ this.getContext(false).prepareOptions(options);
1174
1202
  await this.getUnitOfWork().lock(entity, { lockMode, ...options });
1175
1203
  }
1176
1204
  /**
@@ -1192,9 +1220,6 @@ export class EntityManager {
1192
1220
  helper(data).setSchema(options.schema);
1193
1221
  }
1194
1222
  if (!helper(data).__managed) {
1195
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1196
- em.#unitOfWork.getPersistStack().delete(data);
1197
- // it can be also in the identity map if it had a PK value already
1198
1223
  em.#unitOfWork.unsetIdentity(data);
1199
1224
  }
1200
1225
  const meta = helper(data).__meta;
@@ -1283,9 +1308,6 @@ export class EntityManager {
1283
1308
  helper(row).setSchema(options.schema);
1284
1309
  }
1285
1310
  if (!helper(row).__managed) {
1286
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1287
- em.#unitOfWork.getPersistStack().delete(row);
1288
- // it can be also in the identity map if it had a PK value already
1289
1311
  em.#unitOfWork.unsetIdentity(row);
1290
1312
  }
1291
1313
  const payload = em.#comparator.prepareEntity(row);
@@ -1471,6 +1493,29 @@ export class EntityManager {
1471
1493
  await em.storeCache(options.cache, cached, () => +count);
1472
1494
  return +count;
1473
1495
  }
1496
+ /**
1497
+ * Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
1498
+ * field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
1499
+ *
1500
+ * SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
1501
+ *
1502
+ * @example
1503
+ * ```ts
1504
+ * // Count books per author
1505
+ * const counts = await em.countBy(Book, 'author');
1506
+ * // { '1': 2, '2': 1, '3': 3 }
1507
+ *
1508
+ * // Count with a filter
1509
+ * const counts = await em.countBy(Book, 'author', { where: { active: true } });
1510
+ *
1511
+ * // Composite groupBy — keys joined with ~~~
1512
+ * const counts = await em.countBy(Order, ['status', 'country']);
1513
+ * // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
1514
+ * ```
1515
+ */
1516
+ async countBy(entityName, groupBy, options) {
1517
+ throw new Error(`${this.constructor.name}.countBy() is not supported by the current driver`);
1518
+ }
1474
1519
  /**
1475
1520
  * Tells the EntityManager to make an instance managed and persistent.
1476
1521
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -1576,7 +1621,7 @@ export class EntityManager {
1576
1621
  const em = this.getContext();
1577
1622
  em.prepareOptions(options);
1578
1623
  const entityName = arr[0].constructor;
1579
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1624
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
1580
1625
  await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
1581
1626
  return entities;
1582
1627
  }
@@ -1609,6 +1654,8 @@ export class EntityManager {
1609
1654
  fork.#filterParams = Utils.copy(em.#filterParams);
1610
1655
  fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
1611
1656
  fork.#schema = options.schema ?? em.#schema;
1657
+ fork.signal = options.signal ?? em.signal;
1658
+ fork.inflightQueryAbortStrategy = options.inflightQueryAbortStrategy ?? em.inflightQueryAbortStrategy;
1612
1659
  if (!options.clear) {
1613
1660
  for (const entity of em.#unitOfWork.getIdentityMap()) {
1614
1661
  fork.#unitOfWork.register(entity);
@@ -1684,6 +1731,22 @@ export class EntityManager {
1684
1731
  getTransactionContext() {
1685
1732
  return this.getContext(false).#transactionContext;
1686
1733
  }
1734
+ /**
1735
+ * Returns the cancellation defaults configured on this EntityManager (via `em.fork({ signal })`
1736
+ * or inherited from a transactional fork). Returns `undefined` when no signal is set.
1737
+ *
1738
+ * @internal — exposed for subclass drivers and `UnitOfWork`; not part of the public API.
1739
+ */
1740
+ getAbortOptions() {
1741
+ const em = this.getContext(false);
1742
+ if (em.signal == null && em.inflightQueryAbortStrategy == null) {
1743
+ return undefined;
1744
+ }
1745
+ return {
1746
+ signal: em.signal,
1747
+ inflightQueryAbortStrategy: em.inflightQueryAbortStrategy,
1748
+ };
1749
+ }
1687
1750
  /**
1688
1751
  * Sets the transaction context.
1689
1752
  */
@@ -1727,6 +1790,79 @@ export class EntityManager {
1727
1790
  throw ValidationError.transactionRequired();
1728
1791
  }
1729
1792
  }
1793
+ validateIndexUsage(meta, where, options) {
1794
+ if (!options.using) {
1795
+ return;
1796
+ }
1797
+ const indexNames = Utils.asArray(options.using);
1798
+ const allIndexes = [...meta.indexes, ...meta.uniques];
1799
+ const indexMap = new Map();
1800
+ for (const idx of allIndexes) {
1801
+ if (idx.name) {
1802
+ indexMap.set(idx.name, Utils.asArray(idx.properties ?? []));
1803
+ }
1804
+ }
1805
+ for (const prop of meta.props) {
1806
+ if (typeof prop.index === 'string') {
1807
+ indexMap.set(prop.index, [prop.name]);
1808
+ }
1809
+ if (typeof prop.unique === 'string') {
1810
+ indexMap.set(prop.unique, [prop.name]);
1811
+ }
1812
+ }
1813
+ const allowedProps = new Set();
1814
+ for (const name of indexNames) {
1815
+ const props = indexMap.get(name);
1816
+ if (!props) {
1817
+ const available = [...indexMap.keys()];
1818
+ throw new Error(`Index '${name}' not found on entity '${meta.className}'. ` +
1819
+ (available.length > 0 ? `Available indexes: ${available.join(', ')}` : 'No named indexes defined.'));
1820
+ }
1821
+ for (const prop of props) {
1822
+ allowedProps.add(prop);
1823
+ }
1824
+ }
1825
+ this.validateWhereKeysForIndex(where, allowedProps, indexNames);
1826
+ if (options.orderBy) {
1827
+ const orderMaps = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
1828
+ for (const orderMap of orderMaps) {
1829
+ for (const key of Object.keys(orderMap)) {
1830
+ if (!allowedProps.has(key)) {
1831
+ throw new Error(`Property '${key}' in orderBy is not covered by index '${indexNames.join("', '")}'. ` +
1832
+ `Allowed properties: ${[...allowedProps].join(', ')}`);
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ }
1838
+ validateWhereKeysForIndex(where, allowedProps, indexNames) {
1839
+ if (typeof where !== 'object' || where === null) {
1840
+ return;
1841
+ }
1842
+ if (Array.isArray(where)) {
1843
+ for (const item of where) {
1844
+ this.validateWhereKeysForIndex(item, allowedProps, indexNames);
1845
+ }
1846
+ return;
1847
+ }
1848
+ for (const key of Object.keys(where)) {
1849
+ if (key === '$and' || key === '$or') {
1850
+ for (const item of Utils.asArray(where[key])) {
1851
+ this.validateWhereKeysForIndex(item, allowedProps, indexNames);
1852
+ }
1853
+ }
1854
+ else if (key === '$not') {
1855
+ this.validateWhereKeysForIndex(where[key], allowedProps, indexNames);
1856
+ }
1857
+ else if (key.startsWith('$')) {
1858
+ continue;
1859
+ }
1860
+ else if (!allowedProps.has(key)) {
1861
+ throw new Error(`Property '${key}' in where clause is not covered by index '${indexNames.join("', '")}'. ` +
1862
+ `Allowed properties: ${[...allowedProps].join(', ')}`);
1863
+ }
1864
+ }
1865
+ }
1730
1866
  async lockAndPopulate(meta, entity, where, options) {
1731
1867
  if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) {
1732
1868
  await this.lock(entity, options.lockMode, {
@@ -1761,8 +1897,17 @@ export class EntityManager {
1761
1897
  return [];
1762
1898
  }
1763
1899
  const meta = this.metadata.find(entityName);
1764
- // infer populate hint if only `fields` are available
1765
- if (!options.populate && options.fields) {
1900
+ // infer populate hints from `fields` when present; merge with explicit `populate` if both are set.
1901
+ // `populateArray` is only kept when the entries are user-provided strings — when this method runs a
1902
+ // second time (e.g. from `lockAndPopulate`) the populate is already normalized and merging would be a no-op.
1903
+ const fields = options.fields ? this.buildFields(options.fields) : undefined;
1904
+ const populateArray = Array.isArray(options.populate) && options.populate.every(p => typeof p === 'string')
1905
+ ? options.populate
1906
+ : undefined;
1907
+ const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
1908
+ const shouldInfer = fields !== undefined && options.populate === undefined;
1909
+ const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
1910
+ if (shouldInfer || shouldMerge) {
1766
1911
  // 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
1767
1912
  const pruneToOneRelations = (meta, fields) => {
1768
1913
  const ret = [];
@@ -1796,7 +1941,19 @@ export class EntityManager {
1796
1941
  }
1797
1942
  return Utils.unique(ret);
1798
1943
  };
1799
- options.populate = pruneToOneRelations(meta, this.buildFields(options.fields));
1944
+ const fromFields = pruneToOneRelations(meta, fields);
1945
+ if (shouldInfer) {
1946
+ options.populate = fromFields;
1947
+ }
1948
+ else {
1949
+ // with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
1950
+ // require a JOIN to access sub-fields, whereas bare names are already handled by the driver.
1951
+ // skip paths already in user's `populate` (regardless of `:ref` modifier) to avoid producing
1952
+ // both a `:ref` and a non-`:ref` entry for the same relation
1953
+ const existing = new Set(populateArray.map(p => p.split(':')[0]));
1954
+ const extras = fromFields.filter(f => f.includes('.') && !existing.has(f));
1955
+ options.populate = [...populateArray, ...extras];
1956
+ }
1800
1957
  }
1801
1958
  if (!options.populate) {
1802
1959
  const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude);
@@ -1836,9 +1993,21 @@ export class EntityManager {
1836
1993
  }
1837
1994
  if (options.populateHints) {
1838
1995
  applyPopulateHints(populate, options.populateHints);
1996
+ this.forceSelectInForLimitedPopulate(populate);
1839
1997
  }
1840
1998
  return populate;
1841
1999
  }
2000
+ /** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
2001
+ forceSelectInForLimitedPopulate(populate) {
2002
+ for (const entry of populate) {
2003
+ if (entry.limit != null) {
2004
+ entry.strategy = LoadStrategy.SELECT_IN;
2005
+ }
2006
+ if (entry.children?.length) {
2007
+ this.forceSelectInForLimitedPopulate(entry.children);
2008
+ }
2009
+ }
2010
+ }
1842
2011
  /**
1843
2012
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
1844
2013
  * some additional lazy properties, if so, we reload and merge the data from database
@@ -1870,6 +2039,8 @@ export class EntityManager {
1870
2039
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1871
2040
  }
1872
2041
  options.schema ??= this.#schema;
2042
+ options.signal ??= this.signal;
2043
+ options.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
1873
2044
  options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1874
2045
  }
1875
2046
  /**
@@ -1878,7 +2049,15 @@ export class EntityManager {
1878
2049
  cacheKey(entityName, options, method, where) {
1879
2050
  const { ...opts } = options;
1880
2051
  // ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query
1881
- for (const k of ['ctx', 'strategy', 'flushMode', 'logging', 'loggerContext']) {
2052
+ for (const k of [
2053
+ 'ctx',
2054
+ 'strategy',
2055
+ 'flushMode',
2056
+ 'logging',
2057
+ 'loggerContext',
2058
+ 'signal',
2059
+ 'inflightQueryAbortStrategy',
2060
+ ]) {
1882
2061
  delete opts[k];
1883
2062
  }
1884
2063
  return [Utils.className(entityName), method, opts, where];
@@ -1971,6 +2150,8 @@ export class EntityManager {
1971
2150
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
1972
2151
  case 'm:n':
1973
2152
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
2153
+ case 'count':
2154
+ return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getCountBatchLoadFn(em)));
1974
2155
  }
1975
2156
  }
1976
2157
  /**
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
@@ -104,3 +104,32 @@ export interface ConnectionConfig {
104
104
  }
105
105
  /** Opaque transaction context type, wrapping the driver-specific transaction object. */
106
106
  export type Transaction<T = any> = T & {};
107
+ /**
108
+ * Strategy applied when an `AbortSignal` fires while a query is in flight.
109
+ *
110
+ * - `'ignore query'` — stop awaiting; the query keeps running on the server until it settles
111
+ * (the connection returns to the pool only when the database replies).
112
+ * - `'cancel query'` — ask the database to cancel the running query (e.g. `pg_cancel_backend`,
113
+ * `KILL QUERY`). Falls back to `'ignore query'` if the dialect cannot cancel.
114
+ * Most engines do not cancel writes; partial commits are possible.
115
+ * - `'kill session'` — terminate the database session/process the query runs in
116
+ * (`pg_terminate_backend` etc.). Falls back to `'cancel query'` if not supported.
117
+ *
118
+ * Default: `'ignore query'`.
119
+ *
120
+ * **Streaming queries (`em.stream()` / `qb.stream()`):** the strategy is silently treated as
121
+ * `'ignore query'` because the underlying driver only accepts a plain `AbortSignal` for
122
+ * streamed reads — there is no server-side cancel for an open cursor. The MongoDB driver also
123
+ * has no notion of strategies; only the signal is honored there.
124
+ */
125
+ export type InflightQueryAbortStrategy = 'ignore query' | 'cancel query' | 'kill session';
126
+ /** Per-query cancellation controls forwarded to the underlying driver. */
127
+ export interface AbortQueryOptions {
128
+ /** AbortSignal that cancels the query when fired. */
129
+ signal?: AbortSignal;
130
+ /**
131
+ * Strategy used when the signal fires while the query is in flight. See
132
+ * {@apilink InflightQueryAbortStrategy} for caveats around streams and MongoDB.
133
+ */
134
+ inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
135
+ }
@@ -1,5 +1,5 @@
1
- import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes } from '../typings.js';
2
- import type { Connection, QueryResult, Transaction } from '../connections/Connection.js';
1
+ import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes, IndexName } from '../typings.js';
2
+ import type { AbortQueryOptions, Connection, QueryResult, Transaction } from '../connections/Connection.js';
3
3
  import type { FlushMode, LockMode, QueryOrderMap, QueryFlag, LoadStrategy, PopulateHint, PopulatePath } from '../enums.js';
4
4
  import type { Platform } from '../platforms/Platform.js';
5
5
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
@@ -110,6 +110,20 @@ export interface FindAllOptions<T, P extends string = never, F extends string =
110
110
  }
111
111
  /** Options for streaming query results via `em.stream()`. */
112
112
  export interface StreamOptions<Entity, Populate extends string = never, Fields extends string = never, Exclude extends string = never> extends Omit<FindAllOptions<Entity, Populate, Fields, Exclude>, 'cache' | 'before' | 'after' | 'first' | 'last' | 'overfetch' | 'strategy'> {
113
+ /**
114
+ * How many rows to fetch in one round-trip.
115
+ * Lower values will result in more queries and network bandwidth, but less memory usage.
116
+ * Higher values will result in fewer queries and network bandwidth, but higher memory usage.
117
+ * Note that the results are iterated one row at a time regardless of this value.
118
+ *
119
+ * Honored on PostgreSQL (cursor-based fetch), MSSQL (tedious stream chunk size),
120
+ * Oracle (mapped to `fetchArraySize`) and MongoDB (mapped to `batchSize`). Ignored
121
+ * on MySQL, MariaDB, SQLite and libSQL, where the underlying driver already streams
122
+ * row-by-row with no batching knob.
123
+ *
124
+ * @default 100 (on dialects that honor it)
125
+ */
126
+ chunkSize?: number;
113
127
  /**
114
128
  * When populating to-many relations, the ORM streams fully merged entities instead of yielding every row.
115
129
  * You can opt out of this behavior by specifying `mergeResults: false`. This will yield every row from
@@ -130,7 +144,7 @@ export interface LoadHint<Entity, Hint extends string = never, Fields extends st
130
144
  exclude?: readonly AutoPath<Entity, Excludes>[];
131
145
  }
132
146
  /** Options for `em.find()` queries, including population, ordering, pagination, and locking. */
133
- export interface FindOptions<Entity, Hint extends string = never, Fields extends string = never, Excludes extends string = never> extends LoadHint<Entity, Hint, Fields, Excludes> {
147
+ export interface FindOptions<Entity, Hint extends string = never, Fields extends string = never, Excludes extends string = never> extends LoadHint<Entity, Hint, Fields, Excludes>, AbortQueryOptions {
134
148
  /**
135
149
  * Where condition for populated relations. This will have no effect on the root entity.
136
150
  * With `select-in` strategy, this is applied only to the populate queries.
@@ -216,6 +230,20 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
216
230
  connectionType?: ConnectionType;
217
231
  /** SQL: appended to FROM clause (e.g. `'force index(my_index)'`); MongoDB: index name or spec passed as `hint`. */
218
232
  indexHint?: string | Dictionary;
233
+ /**
234
+ * Named index(es) for this query. When provided:
235
+ * - Validates that `where` and `orderBy` only reference columns covered by the specified index(es).
236
+ * - Emits SQL index hints where supported (MySQL/MariaDB: `USE INDEX`, MSSQL: `WITH (INDEX(...))`).
237
+ * - If `indexHint` is also set, `indexHint` takes precedence for SQL generation.
238
+ *
239
+ * Accepts a single index name or an array. For `defineEntity` entities with named indexes
240
+ * and decorator entities with `[IndexHints]`, index names are autocompleted.
241
+ *
242
+ * @example
243
+ * await em.find(Book, { title: 'foo' }, { using: 'idx_book_title' });
244
+ * await em.find(Book, { title: 'foo', author: 1 }, { using: ['idx_book_title', 'idx_book_author'] });
245
+ */
246
+ using?: IndexName<Entity> | IndexName<Entity>[];
219
247
  /** sql only */
220
248
  comments?: string | string[];
221
249
  /** sql only */
@@ -246,7 +274,7 @@ export interface FindOneOrFailOptions<T extends object, P extends string = never
246
274
  strict?: boolean;
247
275
  }
248
276
  /** Options for native insert and update operations. */
249
- export interface NativeInsertUpdateOptions<T> {
277
+ export interface NativeInsertUpdateOptions<T> extends AbortQueryOptions {
250
278
  convertCustomTypes?: boolean;
251
279
  ctx?: Transaction;
252
280
  schema?: string;
@@ -280,7 +308,7 @@ export interface UpsertManyOptions<Entity, Fields extends string = never> extend
280
308
  batchSize?: number;
281
309
  }
282
310
  /** Options for `em.count()` queries. */
283
- export interface CountOptions<T extends object, P extends string = never> {
311
+ export interface CountOptions<T extends object, P extends string = never> extends AbortQueryOptions {
284
312
  filters?: FilterOptions;
285
313
  schema?: string;
286
314
  groupBy?: string | readonly string[];
@@ -311,8 +339,18 @@ export interface CountOptions<T extends object, P extends string = never> {
311
339
  /** @internal used to apply filters to the auto-joined relations */
312
340
  em?: EntityManager;
313
341
  }
342
+ /** Options for `em.countBy()` queries. */
343
+ export interface CountByOptions<T extends object> {
344
+ where?: FilterQuery<T>;
345
+ filters?: FilterOptions;
346
+ having?: FilterQuery<T>;
347
+ schema?: string;
348
+ flushMode?: FlushMode | `${FlushMode}`;
349
+ loggerContext?: LogContext;
350
+ logging?: LoggingOptions;
351
+ }
314
352
  /** Options for `em.qb().update()` operations. */
315
- export interface UpdateOptions<T> {
353
+ export interface UpdateOptions<T> extends AbortQueryOptions {
316
354
  filters?: FilterOptions;
317
355
  schema?: string;
318
356
  ctx?: Transaction;
@@ -351,7 +389,7 @@ export interface LockOptions extends DriverMethodOptions {
351
389
  logging?: LoggingOptions;
352
390
  }
353
391
  /** Base options shared by all driver methods (transaction context, schema, logging). */
354
- export interface DriverMethodOptions {
392
+ export interface DriverMethodOptions extends AbortQueryOptions {
355
393
  ctx?: Transaction;
356
394
  schema?: string;
357
395
  loggerContext?: LogContext;