@mikro-orm/core 7.2.4-dev.9 → 7.2.4

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.
package/EntityManager.js CHANGED
@@ -1025,6 +1025,15 @@ export class EntityManager {
1025
1025
  where[meta.primaryKeys[0]] = ret.insertId;
1026
1026
  }
1027
1027
  }
1028
+ // a `null` unique value cannot identify the row, and as it never conflicts, the row was inserted
1029
+ if (!Utils.hasObjectKeys(where)) {
1030
+ const insertId = meta.simplePK && ret.insertId != null ? { [meta.primaryKeys[0]]: ret.insertId } : {};
1031
+ Object.assign(where, Utils.getPrimaryKeyCond(entity, meta.primaryKeys) ?? insertId);
1032
+ }
1033
+ // an empty condition would hydrate the entity from an arbitrary row, mongo still gets one for nested conflict fields
1034
+ if (!Utils.hasObjectKeys(where) && platform.usesPivotTable()) {
1035
+ throw new Error(`Cannot find the upserted ${meta.className} row, as neither its primary key nor a unique value is known`);
1036
+ }
1028
1037
  const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.findOne(meta.class, where, {
1029
1038
  fields: returning.concat(...(options.onConflictMergeFields ?? [])),
1030
1039
  ctx,
@@ -1032,6 +1041,9 @@ export class EntityManager {
1032
1041
  connectionType: 'write',
1033
1042
  schema: options.schema,
1034
1043
  }));
1044
+ if (!data2) {
1045
+ throw new Error(`Cannot find the upserted ${meta.className} row for condition ${JSON.stringify(where)}`);
1046
+ }
1035
1047
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
1036
1048
  }
1037
1049
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
@@ -1183,8 +1195,10 @@ export class EntityManager {
1183
1195
  unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
1184
1196
  const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
1185
1197
  propIndex = tmp.propIndex;
1198
+ // a partial composite PK cannot identify the row
1199
+ const partialPK = meta.compositePK && !Utils.isPlainObject(tmp.where) && meta.primaryKeys.some(pk => row[pk] == null);
1186
1200
  where = QueryHelper.processWhere({
1187
- where: tmp.where,
1201
+ where: partialPK ? {} : tmp.where,
1188
1202
  entityName,
1189
1203
  metadata: this.metadata,
1190
1204
  platform: this.getPlatform(),
@@ -1199,6 +1213,25 @@ export class EntityManager {
1199
1213
  allWhere.push(where);
1200
1214
  dataIndexes.push(i);
1201
1215
  }
1216
+ // mongo inserts the rows with an empty condition as part of the batch and reports their ids
1217
+ const positional = !this.getPlatform().usesPivotTable() ||
1218
+ ((this.getPlatform().usesReturningStatement() || this.getPlatform().usesOutputStatement()) &&
1219
+ options.onConflictAction !== 'ignore' &&
1220
+ !options.onConflictWhere);
1221
+ const hasUniqueKey = !!options.onConflictFields || meta.uniques.length > 0 || meta.props.some(p => p.unique && !p.primary);
1222
+ // only an autoincrement PK can be derived from the `insertId` of the batch
1223
+ const autoincrement = meta.getPrimaryProps().some(p => p.autoincrement);
1224
+ // a TPT child table does not return its rows, so they are never mapped by position
1225
+ const peel = !!meta.tptParent || (!positional && (hasUniqueKey || !autoincrement));
1226
+ // a row with a `null` unique value has nothing to be reloaded by, the single row path maps its PK from the insert
1227
+ for (let idx = 0; peel && idx < allWhere.length; idx++) {
1228
+ if (!Utils.hasObjectKeys(allWhere[idx])) {
1229
+ result[dataIndexes[idx]] = await em.upsert(entityName, data[dataIndexes[idx]], options);
1230
+ allData.splice(idx, 1);
1231
+ allWhere.splice(idx, 1);
1232
+ dataIndexes.splice(idx--, 1);
1233
+ }
1234
+ }
1202
1235
  if (allData.length === 0) {
1203
1236
  return [...new Set(result)];
1204
1237
  }
@@ -1248,7 +1281,11 @@ export class EntityManager {
1248
1281
  // skip the reload only when RETURNING brought back a row for every input
1249
1282
  // (onConflictWhere can suppress writes, leaving some rows out)
1250
1283
  // oxfmt-ignore
1251
- const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
1284
+ const getUniqueFields = (where) => options.onConflictFields ?? (Utils.isPlainObject(where) ? Object.keys(where).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
1285
+ // a row without a condition has no unique fields to derive
1286
+ const uniqueFields = getUniqueFields(allWhere.find(cond => Utils.hasObjectKeys(cond)) ?? allWhere[0]);
1287
+ // an empty condition identifies no row, it must not turn into a reload condition matching every row
1288
+ const getRowUniqueFields = (where) => Utils.isPlainObject(where) && !Utils.hasObjectKeys(where) ? uniqueFields : getUniqueFields(where);
1252
1289
  const platform = this.getPlatform();
1253
1290
  const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
1254
1291
  if (options.onConflictWhere && res.rows?.length !== allData.length) {
@@ -1265,10 +1302,14 @@ export class EntityManager {
1265
1302
  Utils.keys(cond).forEach(key => add.add(key));
1266
1303
  }
1267
1304
  const where = { $or: [] };
1305
+ // a row without a condition was inserted, only the primary key reported for it can identify it
1306
+ const insertedPKs = allData.map((row, idx) => Utils.hasObjectKeys(allWhere[idx]) ? null : Utils.getPrimaryKeyCond(entitiesByData.get(row), meta.primaryKeys));
1268
1307
  dataIndexes.forEach((index, idx) => {
1269
1308
  const item = data[index];
1270
- where.$or[idx] = {};
1271
- const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item);
1309
+ where.$or[idx] = { ...insertedPKs[idx] };
1310
+ // the inputs of one batch can be identified by different unique keys
1311
+ const fields = insertedPKs[idx] ? [] : getRowUniqueFields(allWhere[idx]);
1312
+ const props = Array.isArray(fields) ? fields : Object.keys(item);
1272
1313
  props.forEach(prop => {
1273
1314
  where.$or[idx][prop] = item[prop];
1274
1315
  });
@@ -1283,8 +1324,8 @@ export class EntityManager {
1283
1324
  connectionType: 'write',
1284
1325
  schema: options.schema,
1285
1326
  }));
1286
- for (const [entity, cond] of loadPK.entries()) {
1287
- const row = data2.find(row => {
1327
+ if (loadPK.size > 0) {
1328
+ const comparableRows = data2.map(row => {
1288
1329
  const tmp = {};
1289
1330
  add.forEach(k => {
1290
1331
  const prop = meta.properties[k];
@@ -1292,25 +1333,58 @@ export class EntityManager {
1292
1333
  tmp[k] = prop?.customType ? prop.customType.convertToDatabaseValue(row[k], this.getPlatform()) : row[k];
1293
1334
  }
1294
1335
  });
1295
- return this.#comparator.matching(entityName, cond, tmp);
1336
+ return tmp;
1296
1337
  });
1297
- /* v8 ignore next */
1298
- if (!row) {
1299
- throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
1338
+ // rows carry the keys of every condition in the batch, compare only this one's
1339
+ const isMatch = (cond, idx) => this.#comparator.matching(entityName, cond, Object.fromEntries(Object.entries(comparableRows[idx]).filter(([k]) => k in cond)));
1340
+ // When every condition key is a comparable prop checked with `!==`, a matching row has the same primitive
1341
+ // values, so rows can be grouped by those values once instead of comparing every row for every entity.
1342
+ // Props outside `comparableProps` are skipped by the comparator, so they never qualify for grouping.
1343
+ const groupableProps = new Set(meta.comparableProps.filter(prop => this.#comparator.isStrictlyCompared(prop)));
1344
+ const groups = new Map();
1345
+ // `null` and `undefined` share a key, which can only make a group wider than what `matching` accepts
1346
+ const groupKey = (data, keys) => keys.map(k => (data[k] == null ? 'null' : `${typeof data[k]}:${data[k]}`)).join('\0');
1347
+ const findRowIndex = (cond) => {
1348
+ const keys = Utils.keys(cond);
1349
+ if (!keys.every(k => groupableProps.has(meta.properties[k]))) {
1350
+ return comparableRows.findIndex((_, idx) => isMatch(cond, idx));
1351
+ }
1352
+ const signature = keys.join('\0');
1353
+ if (!groups.has(signature)) {
1354
+ const rowsByKey = new Map();
1355
+ comparableRows.forEach((tmp, idx) => {
1356
+ const rowKey = groupKey(tmp, keys);
1357
+ const indexes = rowsByKey.get(rowKey) ?? [];
1358
+ indexes.push(idx);
1359
+ rowsByKey.set(rowKey, indexes);
1360
+ });
1361
+ groups.set(signature, rowsByKey);
1362
+ }
1363
+ const group = groups.get(signature);
1364
+ return group.get(groupKey(cond, keys))?.find(idx => isMatch(cond, idx)) ?? -1;
1365
+ };
1366
+ for (const [entity, cond] of loadPK.entries()) {
1367
+ const row = data2[findRowIndex(cond)];
1368
+ /* v8 ignore next */
1369
+ if (!row) {
1370
+ throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
1371
+ }
1372
+ em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
1300
1373
  }
1301
- em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
1302
1374
  }
1303
1375
  if (loadPK.size !== data2.length && Array.isArray(uniqueFields)) {
1304
1376
  for (let i = 0; i < allData.length; i++) {
1305
1377
  const data = allData[i];
1306
- const cond = uniqueFields.reduce((a, b) => {
1307
- // @ts-ignore
1308
- a[b] = data[b];
1309
- return a;
1310
- }, {});
1378
+ const fields = (insertedPKs[i] ? meta.primaryKeys : getRowUniqueFields(allWhere[i]));
1379
+ const cond = insertedPKs[i] ??
1380
+ fields.reduce((a, b) => {
1381
+ // @ts-ignore
1382
+ a[b] = data[b];
1383
+ return a;
1384
+ }, {});
1311
1385
  const entity = entitiesByData.get(data);
1312
1386
  const row = data2.find(item => {
1313
- const pk = uniqueFields.reduce((a, b) => {
1387
+ const pk = fields.reduce((a, b) => {
1314
1388
  // @ts-ignore
1315
1389
  a[b] = item[b];
1316
1390
  return a;
@@ -325,11 +325,12 @@ export class EntityLoader {
325
325
  }
326
326
  else {
327
327
  // owning side of M:N without pivot table needs to be reordered
328
+ const childIndex = new Map(children.map((child, idx) => [child, idx]));
328
329
  for (const entity of filtered) {
329
- const order = !customOrder ? [...entity[prop.name].getItems(false)] : []; // copy order of references
330
- const items = children.filter(child => entity[prop.name].contains(child, false));
331
- if (!customOrder) {
332
- items.sort((a, b) => order.indexOf(a) - order.indexOf(b));
330
+ // keep the order of references, or the order of loaded children when a custom order is used
331
+ const items = entity[prop.name].getItems(false).filter(item => childIndex.has(item));
332
+ if (customOrder) {
333
+ items.sort((a, b) => childIndex.get(a) - childIndex.get(b));
333
334
  }
334
335
  entity[field].hydrate(items, true, partial, readonly);
335
336
  }
@@ -641,7 +641,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
641
641
  '~discriminatorColumn'?: TBaseDiscriminatorColumn;
642
642
  });
643
643
  properties: TProperties | ((properties: PropertyBuilders) => TProperties);
644
- primaryKeys?: TPK & InferPrimaryKeyConstraint<TProperties>[];
644
+ primaryKeys?: TPK & NoInfer<InferPrimaryKeyConstraint<TProperties>>[];
645
645
  hooks?: DefineEntityHooks;
646
646
  repository?: () => TRepository;
647
647
  filters?: Dictionary<{
@@ -667,16 +667,16 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
667
667
  /** Alias for `discriminator`. */
668
668
  discriminatorColumn?: TDiscriminatorColumn;
669
669
  discriminatorValue?: TDiscriminatorValue;
670
- versionProperty?: AllKeys<TProperties, TBase>;
671
- concurrencyCheckKeys?: Set<AllKeys<TProperties, TBase>>;
672
- serializedPrimaryKey?: AllKeys<TProperties, TBase>;
670
+ versionProperty?: NoInfer<AllKeys<TProperties, TBase>>;
671
+ concurrencyCheckKeys?: Set<NoInfer<AllKeys<TProperties, TBase>>>;
672
+ serializedPrimaryKey?: NoInfer<AllKeys<TProperties, TBase>>;
673
673
  indexes?: {
674
674
  properties?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
675
675
  name?: string;
676
676
  type?: string;
677
677
  options?: Dictionary;
678
678
  expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
679
- where?: PartialWhere<TProperties, TBase>;
679
+ where?: NoInfer<PartialWhere<TProperties, TBase>>;
680
680
  columns?: IndexColumnOptions[];
681
681
  include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
682
682
  fillFactor?: number;
@@ -689,7 +689,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
689
689
  name?: string;
690
690
  options?: Dictionary;
691
691
  expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
692
- where?: PartialWhere<TProperties, TBase>;
692
+ where?: NoInfer<PartialWhere<TProperties, TBase>>;
693
693
  deferMode?: DeferMode | `${DeferMode}`;
694
694
  columns?: IndexColumnOptions[];
695
695
  include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.4-dev.9",
3
+ "version": "7.2.4",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -19,6 +19,11 @@ export declare class EntityComparator {
19
19
  diffEntities<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>, options?: {
20
20
  includeInverseSides?: boolean;
21
21
  }): EntityData<T>;
22
+ /**
23
+ * Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
24
+ * @internal
25
+ */
26
+ isStrictlyCompared<T>(prop: EntityProperty<T>): boolean;
22
27
  /** Returns true if two entity snapshots are identical (no differences). */
23
28
  matching<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>): boolean;
24
29
  /**
@@ -29,6 +29,17 @@ export class EntityComparator {
29
29
  const comparator = this.getEntityComparator(entityName);
30
30
  return Utils.callCompiledFunction(comparator, a, b, options);
31
31
  }
32
+ /**
33
+ * Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
34
+ * @internal
35
+ */
36
+ isStrictlyCompared(prop) {
37
+ if (prop.kind !== ReferenceKind.SCALAR || prop.customType?.compareValues) {
38
+ return false;
39
+ }
40
+ const type = (prop.customType ? prop.customType.compareAsType() : prop.type).toLowerCase();
41
+ return ['string', 'number', 'bigint'].includes(type);
42
+ }
32
43
  /** Returns true if two entity snapshots are identical (no differences). */
33
44
  matching(entityName, a, b) {
34
45
  const diff = this.diffEntities(entityName, a, b);
package/utils/Utils.js CHANGED
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
153
153
  /** Collection of general-purpose utility methods used throughout the ORM. */
154
154
  export class Utils {
155
155
  static PK_SEPARATOR = '~~~';
156
- static #ORM_VERSION = '7.2.4-dev.9';
156
+ static #ORM_VERSION = '7.2.4';
157
157
  /** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
158
158
  static getRlsSettingName(filterName, argName) {
159
159
  return `mikro.${filterName}.${argName}`;
@@ -103,7 +103,8 @@ export function getOnConflictReturningFields(meta, data, uniqueFields, options,
103
103
  if (p.lazy || p.embeddable) {
104
104
  return false;
105
105
  }
106
- if (p.autoincrement) {
106
+ // a PK missing in the data is generated by the database
107
+ if (p.autoincrement || (p.primary && data[p.name] == null)) {
107
108
  return true;
108
109
  }
109
110
  // for a raw `onConflictFields` fragment we can't introspect which columns form the conflict