@mikro-orm/core 7.3.0-dev.16 → 7.3.0-dev.18

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 (38) hide show
  1. package/EntityManager.js +127 -32
  2. package/drivers/DatabaseDriver.d.ts +2 -0
  3. package/drivers/DatabaseDriver.js +16 -3
  4. package/entity/EntityAssigner.js +15 -3
  5. package/entity/EntityFactory.d.ts +7 -0
  6. package/entity/EntityFactory.js +37 -0
  7. package/entity/EntityLoader.js +24 -9
  8. package/entity/defineEntity.d.ts +24 -18
  9. package/entity/defineEntity.js +1 -0
  10. package/entity/validators.js +4 -1
  11. package/hydration/ObjectHydrator.js +4 -0
  12. package/metadata/MetadataDiscovery.d.ts +5 -0
  13. package/metadata/MetadataDiscovery.js +96 -25
  14. package/metadata/MetadataStorage.d.ts +1 -0
  15. package/metadata/MetadataStorage.js +20 -3
  16. package/metadata/MetadataValidator.js +2 -1
  17. package/package.json +1 -1
  18. package/platforms/Platform.d.ts +4 -0
  19. package/platforms/Platform.js +8 -0
  20. package/serialization/EntitySerializer.js +52 -12
  21. package/types/DoubleType.d.ts +7 -3
  22. package/types/DoubleType.js +8 -3
  23. package/typings.d.ts +21 -5
  24. package/typings.js +5 -2
  25. package/unit-of-work/ChangeSetComputer.js +2 -1
  26. package/unit-of-work/ChangeSetPersister.js +24 -5
  27. package/unit-of-work/CommitOrderCalculator.js +2 -1
  28. package/unit-of-work/UnitOfWork.js +20 -5
  29. package/utils/EntityComparator.d.ts +5 -0
  30. package/utils/EntityComparator.js +46 -6
  31. package/utils/QueryHelper.js +10 -2
  32. package/utils/RawQueryFragment.d.ts +5 -1
  33. package/utils/RawQueryFragment.js +27 -19
  34. package/utils/Utils.d.ts +2 -0
  35. package/utils/Utils.js +24 -8
  36. package/utils/clone.js +5 -7
  37. package/utils/fs-utils.js +19 -15
  38. package/utils/upsert-utils.js +2 -1
package/EntityManager.js CHANGED
@@ -743,10 +743,15 @@ export class EntityManager {
743
743
  if (Utils.isEmpty(options.orderBy) && !Raw.hasObjectFragments(options.orderBy)) {
744
744
  throw new Error('Explicit `orderBy` option required');
745
745
  }
746
+ const meta = this.metadata.get(entityName);
747
+ // same merge as em.find, so the cursor and the query see the same orderBy
748
+ if (meta.orderBy) {
749
+ options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
750
+ }
746
751
  const [entities, count] = options.includeCount !== false
747
752
  ? await em.findAndCount(entityName, options.where, options)
748
753
  : [await em.find(entityName, options.where, options)];
749
- return new Cursor(entities, count, options, this.metadata.get(entityName));
754
+ return new Cursor(entities, count, options, meta);
750
755
  }
751
756
  /**
752
757
  * Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
@@ -781,14 +786,38 @@ export class EntityManager {
781
786
  em.#unitOfWork.unsetIdentity(entity);
782
787
  return null;
783
788
  }
789
+ // `serialize()` applies `Type.toJSON()` and `serializedName`, take only relations and embeddables from it, scalars from the entity
790
+ const getData = (e, data = helper(e).serialize({
791
+ ignoreSerializers: true,
792
+ includeHidden: true,
793
+ convertCustomTypes: false,
794
+ })) => {
795
+ for (const prop of helper(e).__meta.props) {
796
+ if (prop.kind === ReferenceKind.SCALAR && !prop.ref) {
797
+ if (helper(e).__initialized || prop.primary) {
798
+ data[prop.name] = e[prop.name];
799
+ }
800
+ continue;
801
+ }
802
+ if (prop.serializedName && prop.serializedName in data) {
803
+ data[prop.name] = data[prop.serializedName];
804
+ }
805
+ if (prop.kind === ReferenceKind.EMBEDDED && e[prop.name] != null) {
806
+ Utils.asArray(e[prop.name]).forEach((item, i) => getData(item, prop.array ? data[prop.name][i] : data[prop.name]));
807
+ }
808
+ }
809
+ return data;
810
+ };
811
+ const hydrate = (target, source) => {
812
+ em.config
813
+ .getHydrator(this.metadata)
814
+ .hydrate(target, helper(target).__meta, getData(source), em.#entityFactory, 'full', false, false);
815
+ Utils.merge(helper(target).__originalEntityData, this.#comparator.prepareEntity(source));
816
+ };
784
817
  let found = false;
785
818
  for (const e of fork.#unitOfWork.getIdentityMap()) {
786
819
  const ref = em.getReference(e.constructor, helper(e).getPrimaryKey());
787
- const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true, convertCustomTypes: false });
788
- em.config
789
- .getHydrator(this.metadata)
790
- .hydrate(ref, helper(ref).__meta, data, em.#entityFactory, 'full', false, false);
791
- Utils.merge(helper(ref).__originalEntityData, this.#comparator.prepareEntity(e));
820
+ hydrate(ref, e);
792
821
  // index a refreshed `targetKey` value, `getByKey` ignores the old one as stale
793
822
  if (helper(ref).__meta.root.targetKeys) {
794
823
  em.#unitOfWork.getIdentityMap().store(ref);
@@ -796,15 +825,7 @@ export class EntityManager {
796
825
  found ||= ref === entity;
797
826
  }
798
827
  if (!found) {
799
- const data = helper(reloaded).serialize({
800
- ignoreSerializers: true,
801
- includeHidden: true,
802
- convertCustomTypes: false,
803
- });
804
- em.config
805
- .getHydrator(this.metadata)
806
- .hydrate(entity, wrapped.__meta, data, em.#entityFactory, 'full', false, false);
807
- Utils.merge(wrapped.__originalEntityData, this.#comparator.prepareEntity(reloaded));
828
+ hydrate(entity, reloaded);
808
829
  }
809
830
  return entity;
810
831
  }
@@ -1025,6 +1046,15 @@ export class EntityManager {
1025
1046
  where[meta.primaryKeys[0]] = ret.insertId;
1026
1047
  }
1027
1048
  }
1049
+ // a `null` unique value cannot identify the row, and as it never conflicts, the row was inserted
1050
+ if (!Utils.hasObjectKeys(where)) {
1051
+ const insertId = meta.simplePK && ret.insertId != null ? { [meta.primaryKeys[0]]: ret.insertId } : {};
1052
+ Object.assign(where, Utils.getPrimaryKeyCond(entity, meta.primaryKeys) ?? insertId);
1053
+ }
1054
+ // an empty condition would hydrate the entity from an arbitrary row, mongo still gets one for nested conflict fields
1055
+ if (!Utils.hasObjectKeys(where) && platform.usesPivotTable()) {
1056
+ throw new Error(`Cannot find the upserted ${meta.className} row, as neither its primary key nor a unique value is known`);
1057
+ }
1028
1058
  const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.findOne(meta.class, where, {
1029
1059
  fields: returning.concat(...(options.onConflictMergeFields ?? [])),
1030
1060
  ctx,
@@ -1032,6 +1062,9 @@ export class EntityManager {
1032
1062
  connectionType: 'write',
1033
1063
  schema: options.schema,
1034
1064
  }));
1065
+ if (!data2) {
1066
+ throw new Error(`Cannot find the upserted ${meta.className} row for condition ${JSON.stringify(where)}`);
1067
+ }
1035
1068
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
1036
1069
  }
1037
1070
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
@@ -1183,8 +1216,10 @@ export class EntityManager {
1183
1216
  unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
1184
1217
  const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
1185
1218
  propIndex = tmp.propIndex;
1219
+ // a partial composite PK cannot identify the row
1220
+ const partialPK = meta.compositePK && !Utils.isPlainObject(tmp.where) && meta.primaryKeys.some(pk => row[pk] == null);
1186
1221
  where = QueryHelper.processWhere({
1187
- where: tmp.where,
1222
+ where: partialPK ? {} : tmp.where,
1188
1223
  entityName,
1189
1224
  metadata: this.metadata,
1190
1225
  platform: this.getPlatform(),
@@ -1199,6 +1234,25 @@ export class EntityManager {
1199
1234
  allWhere.push(where);
1200
1235
  dataIndexes.push(i);
1201
1236
  }
1237
+ // mongo inserts the rows with an empty condition as part of the batch and reports their ids
1238
+ const positional = !this.getPlatform().usesPivotTable() ||
1239
+ ((this.getPlatform().usesReturningStatement() || this.getPlatform().usesOutputStatement()) &&
1240
+ options.onConflictAction !== 'ignore' &&
1241
+ !options.onConflictWhere);
1242
+ const hasUniqueKey = !!options.onConflictFields || meta.uniques.length > 0 || meta.props.some(p => p.unique && !p.primary);
1243
+ // only an autoincrement PK can be derived from the `insertId` of the batch
1244
+ const autoincrement = meta.getPrimaryProps().some(p => p.autoincrement);
1245
+ // a TPT child table does not return its rows, so they are never mapped by position
1246
+ const peel = !!meta.tptParent || (!positional && (hasUniqueKey || !autoincrement));
1247
+ // a row with a `null` unique value has nothing to be reloaded by, the single row path maps its PK from the insert
1248
+ for (let idx = 0; peel && idx < allWhere.length; idx++) {
1249
+ if (!Utils.hasObjectKeys(allWhere[idx])) {
1250
+ result[dataIndexes[idx]] = await em.upsert(entityName, data[dataIndexes[idx]], options);
1251
+ allData.splice(idx, 1);
1252
+ allWhere.splice(idx, 1);
1253
+ dataIndexes.splice(idx--, 1);
1254
+ }
1255
+ }
1202
1256
  if (allData.length === 0) {
1203
1257
  return [...new Set(result)];
1204
1258
  }
@@ -1248,7 +1302,11 @@ export class EntityManager {
1248
1302
  // skip the reload only when RETURNING brought back a row for every input
1249
1303
  // (onConflictWhere can suppress writes, leaving some rows out)
1250
1304
  // oxfmt-ignore
1251
- const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
1305
+ const getUniqueFields = (where) => options.onConflictFields ?? (Utils.isPlainObject(where) ? Object.keys(where).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
1306
+ // a row without a condition has no unique fields to derive
1307
+ const uniqueFields = getUniqueFields(allWhere.find(cond => Utils.hasObjectKeys(cond)) ?? allWhere[0]);
1308
+ // an empty condition identifies no row, it must not turn into a reload condition matching every row
1309
+ const getRowUniqueFields = (where) => Utils.isPlainObject(where) && !Utils.hasObjectKeys(where) ? uniqueFields : getUniqueFields(where);
1252
1310
  const platform = this.getPlatform();
1253
1311
  const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
1254
1312
  if (options.onConflictWhere && res.rows?.length !== allData.length) {
@@ -1265,10 +1323,14 @@ export class EntityManager {
1265
1323
  Utils.keys(cond).forEach(key => add.add(key));
1266
1324
  }
1267
1325
  const where = { $or: [] };
1326
+ // a row without a condition was inserted, only the primary key reported for it can identify it
1327
+ const insertedPKs = allData.map((row, idx) => Utils.hasObjectKeys(allWhere[idx]) ? null : Utils.getPrimaryKeyCond(entitiesByData.get(row), meta.primaryKeys));
1268
1328
  dataIndexes.forEach((index, idx) => {
1269
1329
  const item = data[index];
1270
- where.$or[idx] = {};
1271
- const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item);
1330
+ where.$or[idx] = { ...insertedPKs[idx] };
1331
+ // the inputs of one batch can be identified by different unique keys
1332
+ const fields = insertedPKs[idx] ? [] : getRowUniqueFields(allWhere[idx]);
1333
+ const props = Array.isArray(fields) ? fields : Object.keys(item);
1272
1334
  props.forEach(prop => {
1273
1335
  where.$or[idx][prop] = item[prop];
1274
1336
  });
@@ -1283,8 +1345,8 @@ export class EntityManager {
1283
1345
  connectionType: 'write',
1284
1346
  schema: options.schema,
1285
1347
  }));
1286
- for (const [entity, cond] of loadPK.entries()) {
1287
- const row = data2.find(row => {
1348
+ if (loadPK.size > 0) {
1349
+ const comparableRows = data2.map(row => {
1288
1350
  const tmp = {};
1289
1351
  add.forEach(k => {
1290
1352
  const prop = meta.properties[k];
@@ -1292,25 +1354,58 @@ export class EntityManager {
1292
1354
  tmp[k] = prop?.customType ? prop.customType.convertToDatabaseValue(row[k], this.getPlatform()) : row[k];
1293
1355
  }
1294
1356
  });
1295
- return this.#comparator.matching(entityName, cond, tmp);
1357
+ return tmp;
1296
1358
  });
1297
- /* v8 ignore next */
1298
- if (!row) {
1299
- throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
1359
+ // rows carry the keys of every condition in the batch, compare only this one's
1360
+ const isMatch = (cond, idx) => this.#comparator.matching(entityName, cond, Object.fromEntries(Object.entries(comparableRows[idx]).filter(([k]) => k in cond)));
1361
+ // When every condition key is a comparable prop checked with `!==`, a matching row has the same primitive
1362
+ // values, so rows can be grouped by those values once instead of comparing every row for every entity.
1363
+ // Props outside `comparableProps` are skipped by the comparator, so they never qualify for grouping.
1364
+ const groupableProps = new Set(meta.comparableProps.filter(prop => this.#comparator.isStrictlyCompared(prop)));
1365
+ const groups = new Map();
1366
+ // `null` and `undefined` share a key, which can only make a group wider than what `matching` accepts
1367
+ const groupKey = (data, keys) => keys.map(k => (data[k] == null ? 'null' : `${typeof data[k]}:${data[k]}`)).join('\0');
1368
+ const findRowIndex = (cond) => {
1369
+ const keys = Utils.keys(cond);
1370
+ if (!keys.every(k => groupableProps.has(meta.properties[k]))) {
1371
+ return comparableRows.findIndex((_, idx) => isMatch(cond, idx));
1372
+ }
1373
+ const signature = keys.join('\0');
1374
+ if (!groups.has(signature)) {
1375
+ const rowsByKey = new Map();
1376
+ comparableRows.forEach((tmp, idx) => {
1377
+ const rowKey = groupKey(tmp, keys);
1378
+ const indexes = rowsByKey.get(rowKey) ?? [];
1379
+ indexes.push(idx);
1380
+ rowsByKey.set(rowKey, indexes);
1381
+ });
1382
+ groups.set(signature, rowsByKey);
1383
+ }
1384
+ const group = groups.get(signature);
1385
+ return group.get(groupKey(cond, keys))?.find(idx => isMatch(cond, idx)) ?? -1;
1386
+ };
1387
+ for (const [entity, cond] of loadPK.entries()) {
1388
+ const row = data2[findRowIndex(cond)];
1389
+ /* v8 ignore next */
1390
+ if (!row) {
1391
+ throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
1392
+ }
1393
+ em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
1300
1394
  }
1301
- em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
1302
1395
  }
1303
1396
  if (loadPK.size !== data2.length && Array.isArray(uniqueFields)) {
1304
1397
  for (let i = 0; i < allData.length; i++) {
1305
1398
  const data = allData[i];
1306
- const cond = uniqueFields.reduce((a, b) => {
1307
- // @ts-ignore
1308
- a[b] = data[b];
1309
- return a;
1310
- }, {});
1399
+ const fields = (insertedPKs[i] ? meta.primaryKeys : getRowUniqueFields(allWhere[i]));
1400
+ const cond = insertedPKs[i] ??
1401
+ fields.reduce((a, b) => {
1402
+ // @ts-ignore
1403
+ a[b] = data[b];
1404
+ return a;
1405
+ }, {});
1311
1406
  const entity = entitiesByData.get(data);
1312
1407
  const row = data2.find(item => {
1313
- const pk = uniqueFields.reduce((a, b) => {
1408
+ const pk = fields.reduce((a, b) => {
1314
1409
  // @ts-ignore
1315
1410
  a[b] = item[b];
1316
1411
  return a;
@@ -81,6 +81,8 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
81
81
  */
82
82
  private mapCursorOffset;
83
83
  protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>, fromJson?: boolean): FilterQuery<T>;
84
+ /** @internal Polymorphic embeddable variants can declare same-named properties with different field names. */
85
+ protected variantProps(prop: EntityProperty, data?: Dictionary): Dictionary<EntityProperty>;
84
86
  /** @internal */
85
87
  mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
86
88
  protected inlineEmbeddables<T extends object>(meta: EntityMetadata<T>, data: T, where?: boolean): void;
@@ -403,6 +403,19 @@ export class DatabaseDriver {
403
403
  }
404
404
  return { ...atOrPast, ...past };
405
405
  }
406
+ /** @internal Polymorphic embeddable variants can declare same-named properties with different field names. */
407
+ variantProps(prop, data) {
408
+ const meta = prop.targetMeta;
409
+ // eslint-disable-next-line eqeqeq
410
+ const variant = meta?.polymorphs?.find(m => m.discriminatorValue == data?.[meta.discriminatorColumn]);
411
+ const props = variant ? { ...prop.embeddedProps } : prop.embeddedProps;
412
+ for (const p of variant ? Object.values(props) : []) {
413
+ if (variant.properties[p.embedded[1]] && Utils.isSameKind(variant.properties[p.embedded[1]], p)) {
414
+ props[p.embedded[1]] = p;
415
+ }
416
+ }
417
+ return props;
418
+ }
406
419
  /** @internal */
407
420
  mapDataToFieldNames(data, stringifyJsonArrays, properties, convertCustomTypes, object) {
408
421
  if (!properties || data == null) {
@@ -417,17 +430,17 @@ export class DatabaseDriver {
417
430
  if (prop.embeddedProps && !prop.object && !object) {
418
431
  const copy = data[k];
419
432
  delete data[k];
420
- Object.assign(data, this.mapDataToFieldNames(copy, stringifyJsonArrays, prop.embeddedProps, convertCustomTypes));
433
+ Object.assign(data, this.mapDataToFieldNames(copy, stringifyJsonArrays, this.variantProps(prop, copy), convertCustomTypes));
421
434
  return;
422
435
  }
423
436
  if (prop.embeddedProps && (object || prop.object)) {
424
437
  const copy = data[k];
425
438
  delete data[k];
426
439
  if (prop.array) {
427
- data[prop.fieldNames[0]] = copy?.map((item) => this.mapDataToFieldNames(item, stringifyJsonArrays, prop.embeddedProps, convertCustomTypes, true));
440
+ data[prop.fieldNames[0]] = copy?.map((item) => this.mapDataToFieldNames(item, stringifyJsonArrays, this.variantProps(prop, item), convertCustomTypes, true));
428
441
  }
429
442
  else {
430
- data[prop.fieldNames[0]] = this.mapDataToFieldNames(copy, stringifyJsonArrays, prop.embeddedProps, convertCustomTypes, true);
443
+ data[prop.fieldNames[0]] = this.mapDataToFieldNames(copy, stringifyJsonArrays, this.variantProps(prop, copy), convertCustomTypes, true);
431
444
  }
432
445
  if (stringifyJsonArrays && prop.array && !object) {
433
446
  data[prop.fieldNames[0]] = this.platform.convertJsonToDatabaseValue(data[prop.fieldNames[0]]);
@@ -234,14 +234,26 @@ export class EntityAssigner {
234
234
  entity[propName].push(...Object.values(tmp));
235
235
  });
236
236
  }
237
+ const polymorphs = prop.targetMeta?.polymorphs;
238
+ // eslint-disable-next-line eqeqeq
239
+ const variant = polymorphs?.find(m => m.discriminatorValue == value[prop.targetMeta.discriminatorColumn]);
237
240
  const create = () => EntityAssigner.validateEM(em) &&
238
- em.getEntityFactory().createEmbeddable(prop.targetMeta.class, value, {
241
+ em.getEntityFactory().createEmbeddable((variant ?? prop.targetMeta).class, value, {
239
242
  convertCustomTypes: options.convertCustomTypes,
240
243
  newEntity: options.mergeEmbeddedProperties ? !('propName' in entity) : true,
241
244
  });
242
- entity[propName] = (options.mergeEmbeddedProperties ? entity[propName] || create() : create());
245
+ // a different polymorphic variant cannot be merged into the current one
246
+ const merge = options.mergeEmbeddedProperties && (!variant || entity[propName]?.constructor === variant.class);
247
+ entity[propName] = (merge ? entity[propName] || create() : create());
248
+ const props = polymorphs ? { ...prop.embeddedProps } : prop.embeddedProps;
249
+ // polymorphic variants can declare same-named properties of different kinds or relation targets
250
+ for (const p of polymorphs ? entity[propName].__meta.props : []) {
251
+ if (props[p.name] && props[p.name].targetMeta?.class !== p.targetMeta?.class) {
252
+ props[p.name] = { ...p, nullable: prop.nullable || p.nullable };
253
+ }
254
+ }
243
255
  Object.keys(value).forEach(key => {
244
- EntityAssigner.assignProperty(entity[propName], key, prop.embeddedProps, value, options);
256
+ EntityAssigner.assignProperty(entity[propName], key, props, value, options);
245
257
  });
246
258
  }
247
259
  static createCollectionItem(item, em, prop, invalid, options) {
@@ -38,6 +38,13 @@ export declare class EntityFactory {
38
38
  constructor(em: EntityManager);
39
39
  /** Creates a new entity instance or returns an existing one from the identity map, hydrating it with the provided data. */
40
40
  create<T extends object, P extends string = string>(entityName: EntityName<T>, data: EntityData<T>, options?: FactoryOptions): New<T, P>;
41
+ /**
42
+ * Whether merging `data` into an initialized entity would change nothing: every key is a scalar the entity already
43
+ * holds by identity, and keys `mergeData` always re-applies (formula, version, generated, discriminator) match the snapshot.
44
+ */
45
+ private isNoopMerge;
46
+ /** Whether hydrating `value` into `prop` would leave `current` as is, mirroring the hydrator's Date and boolean coercion. */
47
+ private holdsValue;
41
48
  /** Merges new data into an existing entity, preserving user-modified properties. */
42
49
  mergeData<T extends object>(meta: EntityMetadata<T>, entity: T, data: EntityData<T>, options?: FactoryOptions): void;
43
50
  /** Creates or retrieves an uninitialized entity reference by its primary key or alternate key. */
@@ -149,8 +149,45 @@ export class EntityFactory {
149
149
  wrapped.__processing = false;
150
150
  return entity;
151
151
  }
152
+ /**
153
+ * Whether merging `data` into an initialized entity would change nothing: every key is a scalar the entity already
154
+ * holds by identity, and keys `mergeData` always re-applies (formula, version, generated, discriminator) match the snapshot.
155
+ */
156
+ isNoopMerge(meta, entity, data, options) {
157
+ const wrapped = helper(entity);
158
+ if (!wrapped.__initialized || options.normalizeAccessors) {
159
+ return false;
160
+ }
161
+ for (const key of Utils.keys(data)) {
162
+ const prop = meta.properties[key];
163
+ const value = data[key];
164
+ // custom types are left to the full merge, as their JS and database values can differ
165
+ if (!prop || prop.customType || value === undefined || !this.holdsValue(prop, entity[key], value)) {
166
+ return false;
167
+ }
168
+ const reapplied = prop.formula || prop.version || prop.generated || key === meta.root.discriminatorColumn;
169
+ if (reapplied && (wrapped.__originalEntityData?.[key] !== value || !wrapped.__loadedProperties.has(key))) {
170
+ return false;
171
+ }
172
+ }
173
+ return true;
174
+ }
175
+ /** Whether hydrating `value` into `prop` would leave `current` as is, mirroring the hydrator's Date and boolean coercion. */
176
+ holdsValue(prop, current, value) {
177
+ if (current instanceof Date && value instanceof Date) {
178
+ return current.getTime() === value.getTime();
179
+ }
180
+ if (prop.runtimeType === 'boolean' && value != null) {
181
+ return current === !!value;
182
+ }
183
+ return current === value;
184
+ }
152
185
  /** Merges new data into an existing entity, preserving user-modified properties. */
153
186
  mergeData(meta, entity, data, options = {}) {
187
+ // joined rows often repeat exactly what the entity already holds, skip the snapshot and diffing then
188
+ if (this.isNoopMerge(meta, entity, data, options)) {
189
+ return;
190
+ }
154
191
  // merge unchanged properties automatically
155
192
  data = QueryHelper.processParams(data);
156
193
  const existsData = this.#comparator.prepareEntity(entity);
@@ -312,18 +312,25 @@ export class EntityLoader {
312
312
  }
313
313
  initializeManyToMany(filtered, children, prop, field, customOrder, partial, readonly) {
314
314
  if (prop.mappedBy) {
315
+ // group children by owner in one pass
316
+ const map = new Map(filtered.map(entity => [entity, []]));
317
+ for (const child of children) {
318
+ for (const owner of child[prop.mappedBy].getItems(false)) {
319
+ map.get(owner)?.push(child);
320
+ }
321
+ }
315
322
  for (const entity of filtered) {
316
- const items = children.filter(child => child[prop.mappedBy].contains(entity, false));
317
- entity[field].hydrate(items, true, partial, readonly);
323
+ entity[field].hydrate(map.get(entity), true, partial, readonly);
318
324
  }
319
325
  }
320
326
  else {
321
327
  // owning side of M:N without pivot table needs to be reordered
328
+ const childIndex = new Map(children.map((child, idx) => [child, idx]));
322
329
  for (const entity of filtered) {
323
- const order = !customOrder ? [...entity[prop.name].getItems(false)] : []; // copy order of references
324
- const items = children.filter(child => entity[prop.name].contains(child, false));
325
- if (!customOrder) {
326
- 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));
327
334
  }
328
335
  entity[field].hydrate(items, true, partial, readonly);
329
336
  }
@@ -673,12 +680,20 @@ export class EntityLoader {
673
680
  const subCond = Utils.isPlainObject(where[prop.name]) ? { ...where[prop.name] } : {};
674
681
  const meta2 = prop.targetMeta;
675
682
  const pk = Utils.getPrimaryKeyHash(meta2.primaryKeys);
683
+ // `$not` wrapping entity conditions stays on the target entity, only operators on the relation itself map to its PK
684
+ const isRelationOperator = (key, value) => {
685
+ if (key === '$not' && Utils.isPlainObject(value)) {
686
+ return Utils.getObjectQueryKeys(value).every(k => Utils.isOperator(k, false));
687
+ }
688
+ return Utils.isOperator(key, false);
689
+ };
676
690
  ['$and', '$or'].forEach(op => {
677
691
  if (where[op]) {
678
692
  const child = where[op]
679
693
  .map((cond) => cond[prop.name])
680
694
  .filter((sub) => sub != null &&
681
- !(Utils.isPlainObject(sub) && Utils.getObjectQueryKeys(sub).every(key => Utils.isOperator(key, false))))
695
+ !(Utils.isPlainObject(sub) &&
696
+ Utils.getObjectQueryKeys(sub).every(key => isRelationOperator(key, sub[key]))))
682
697
  .map((cond) => {
683
698
  if (Utils.isPrimaryKey(cond)) {
684
699
  return { [pk]: cond };
@@ -691,7 +706,7 @@ export class EntityLoader {
691
706
  }
692
707
  }
693
708
  });
694
- const operators = Object.keys(subCond).filter(key => Utils.isOperator(key, false));
709
+ const operators = Object.keys(subCond).filter(key => isRelationOperator(key, subCond[key]));
695
710
  if (operators.length > 0) {
696
711
  subCond[pk] = Utils.isPlainObject(subCond[pk]) ? { ...subCond[pk] } : (subCond[pk] ?? {});
697
712
  operators.forEach(op => {
@@ -809,7 +824,7 @@ export class EntityLoader {
809
824
  if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(wrapped.__meta.properties[f].kind)) {
810
825
  return entity[f].getItems(false).every((item) => this.isPropertyLoaded(item, r.join('.')));
811
826
  }
812
- return this.isPropertyLoaded(entity[f], r.join('.'));
827
+ return this.isPropertyLoaded(Reference.unwrapReference(entity[f]), r.join('.'));
813
828
  }
814
829
  filterReferences(entities, field, options, ref, populate) {
815
830
  if (ref) {
@@ -600,7 +600,7 @@ type EntityTarget = {
600
600
  '~entity': any;
601
601
  } | EntityClass;
602
602
  declare const propertyBuilders: PropertyBuilders;
603
- type PropertyBuildersOverrideKeys = 'bigint' | 'array' | 'decimal' | 'json' | 'string' | 'text' | 'datetime' | 'time' | 'enum';
603
+ type PropertyBuildersOverrideKeys = 'bigint' | 'array' | 'decimal' | 'double' | 'json' | 'string' | 'text' | 'datetime' | 'time' | 'enum';
604
604
  /** Map of factory functions for creating type-safe property builders (scalars, enums, embeddables, and relations). */
605
605
  export type PropertyBuilders = {
606
606
  [K in Exclude<keyof typeof types, PropertyBuildersOverrideKeys>]: () => UniversalPropertyOptionsBuilder<InferPropertyValueType<(typeof types)[K]>, EmptyOptions, IncludeKeysForProperty>;
@@ -608,6 +608,7 @@ export type PropertyBuilders = {
608
608
  bigint: <Mode extends 'bigint' | 'number' | 'string' = 'bigint'>(mode?: Mode) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.bigint<Mode>>, EmptyOptions, IncludeKeysForProperty>;
609
609
  array: <T = string>(toJsValue?: (i: string) => T, toDbValue?: (i: T) => string) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.array<T>>, EmptyOptions, IncludeKeysForProperty>;
610
610
  decimal: <Mode extends 'number' | 'string' = 'string'>(mode?: Mode) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.decimal<Mode>>, EmptyOptions, IncludeKeysForProperty>;
611
+ double: <Mode extends 'number' | 'string' = 'number'>(mode?: Mode) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.double<Mode>>, EmptyOptions, IncludeKeysForProperty>;
611
612
  json: <T>() => UniversalPropertyOptionsBuilder<T, EmptyOptions, IncludeKeysForProperty>;
612
613
  string: () => StringPropertyOptionsBuilder<InferPropertyValueType<typeof types.string>, EmptyOptions>;
613
614
  text: () => StringPropertyOptionsBuilder<InferPropertyValueType<typeof types.text>, EmptyOptions>;
@@ -637,6 +638,21 @@ type AllKeys<TProperties, TBase> = keyof TProperties | (IsNever<TBase> extends t
637
638
  type PartialWhere<TProperties, TBase> = string | FilterQuery<{
638
639
  [K in AllKeys<TProperties, TBase> & string]?: unknown;
639
640
  }>;
641
+ type DefineEntityFilterMap = Dictionary<{
642
+ name: string;
643
+ cond: Dictionary | ((args: Dictionary, type: 'read' | 'update' | 'delete', em: any, options?: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any>, entityName?: string) => MaybePromise<FilterQuery<any>>);
644
+ default?: boolean;
645
+ entity?: EntityName<any> | EntityName<any>[];
646
+ args?: boolean;
647
+ strict?: boolean;
648
+ rls?: boolean | {
649
+ setting?: string;
650
+ };
651
+ }>;
652
+ type DefineEntityFilterMapLoose = Dictionary<{
653
+ name: string;
654
+ cond: Dictionary;
655
+ }>;
640
656
  /** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
641
657
  export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false, TDiscriminatorColumn extends string | undefined = undefined, TDiscriminatorValue extends string | number | undefined = undefined, TBaseDiscriminatorColumn extends string | undefined = undefined, TEmbeddable extends boolean = false, TForceUndefined extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminator' | 'discriminatorColumn' | 'discriminatorValue' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
642
658
  name: TName;
@@ -648,20 +664,10 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
648
664
  '~discriminatorColumn'?: TBaseDiscriminatorColumn;
649
665
  });
650
666
  properties: TProperties | ((properties: PropertyBuilders) => TProperties);
651
- primaryKeys?: TPK & InferPrimaryKeyConstraint<TProperties>[];
667
+ primaryKeys?: TPK & NoInfer<InferPrimaryKeyConstraint<TProperties>>[];
652
668
  hooks?: DefineEntityHooks;
653
669
  repository?: () => TRepository;
654
- filters?: Dictionary<{
655
- name: string;
656
- cond: Dictionary | ((args: Dictionary, type: 'read' | 'update' | 'delete', em: any, options?: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any>, entityName?: string) => MaybePromise<FilterQuery<any>>);
657
- default?: boolean;
658
- entity?: EntityName<any> | EntityName<any>[];
659
- args?: boolean;
660
- strict?: boolean;
661
- rls?: boolean | {
662
- setting?: string;
663
- };
664
- }>;
670
+ filters?: DefineEntityFilterMap | IsNever<TRepository, never, DefineEntityFilterMapLoose>;
665
671
  forceObject?: TForceObject;
666
672
  /** Hydrate `null` as `undefined` for this entity and infer nullable properties as `T | undefined`. */
667
673
  forceUndefined?: TForceUndefined;
@@ -676,16 +682,16 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
676
682
  /** Alias for `discriminator`. */
677
683
  discriminatorColumn?: TDiscriminatorColumn;
678
684
  discriminatorValue?: TDiscriminatorValue;
679
- versionProperty?: AllKeys<TProperties, TBase>;
680
- concurrencyCheckKeys?: Set<AllKeys<TProperties, TBase>>;
681
- serializedPrimaryKey?: AllKeys<TProperties, TBase>;
685
+ versionProperty?: NoInfer<AllKeys<TProperties, TBase>>;
686
+ concurrencyCheckKeys?: Set<NoInfer<AllKeys<TProperties, TBase>>>;
687
+ serializedPrimaryKey?: NoInfer<AllKeys<TProperties, TBase>>;
682
688
  indexes?: {
683
689
  properties?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
684
690
  name?: string;
685
691
  type?: string;
686
692
  options?: Dictionary;
687
693
  expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
688
- where?: PartialWhere<TProperties, TBase>;
694
+ where?: NoInfer<PartialWhere<TProperties, TBase>>;
689
695
  columns?: IndexColumnOptions[];
690
696
  include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
691
697
  fillFactor?: number;
@@ -698,7 +704,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
698
704
  name?: string;
699
705
  options?: Dictionary;
700
706
  expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
701
- where?: PartialWhere<TProperties, TBase>;
707
+ where?: NoInfer<PartialWhere<TProperties, TBase>>;
702
708
  deferMode?: DeferMode | `${DeferMode}`;
703
709
  columns?: IndexColumnOptions[];
704
710
  include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
@@ -519,6 +519,7 @@ const propertyBuilders = {
519
519
  bigint: (mode) => new UniversalPropertyOptionsBuilder({ type: new types.bigint(mode) }),
520
520
  array: (toJsValue = i => i, toDbValue = i => i) => new UniversalPropertyOptionsBuilder({ type: new types.array(toJsValue, toDbValue) }),
521
521
  decimal: (mode) => new UniversalPropertyOptionsBuilder({ type: new types.decimal(mode) }),
522
+ double: (mode) => new UniversalPropertyOptionsBuilder({ type: new types.double(mode) }),
522
523
  json: () => new UniversalPropertyOptionsBuilder({ type: types.json }),
523
524
  string: () => new StringPropertyOptionsBuilder({
524
525
  type: types.string,
@@ -24,7 +24,10 @@ export function validateProperty(prop, givenValue, entity) {
24
24
  }
25
25
  function getValue(o, prop) {
26
26
  if (prop.embedded && prop.embedded[0] in o) {
27
- return o[prop.embedded[0]]?.[prop.embedded[1]];
27
+ const embeddable = o[prop.embedded[0]];
28
+ const variantProp = embeddable?.__meta?.properties[prop.embedded[1]];
29
+ // a same-named property of a different kind in another polymorphic variant
30
+ return variantProp && !Utils.isSameKind(variantProp, prop) ? undefined : embeddable?.[prop.embedded[1]];
28
31
  }
29
32
  /* v8 ignore next */
30
33
  if (prop.ref) {
@@ -313,6 +313,10 @@ export class ObjectHydrator extends Hydrator {
313
313
  ? dataKey + this.wrap(childProp.embedded[1])
314
314
  : this.wrap(childProp.name);
315
315
  const prop2 = childMeta.properties[childProp.embedded[1]];
316
+ // a same-named property of a different kind in another variant
317
+ if (prop2 && !Utils.isSameKind(prop2, childProp)) {
318
+ return;
319
+ }
316
320
  const prop3 = {
317
321
  ...prop2,
318
322
  name: childProp.name,
@@ -34,6 +34,11 @@ export declare class MetadataDiscovery {
34
34
  private initManyToOneFields;
35
35
  private initOneToManyFields;
36
36
  private processEntity;
37
+ /**
38
+ * Groups the relations of all entities by target class name in one pass, so `findReferencingProperties()`
39
+ * does not go over the relations of every entity for each entity. Lists keep the order of `metadata` and `relations`.
40
+ */
41
+ private indexReferencingProperties;
37
42
  private findReferencingProperties;
38
43
  private initFactoryField;
39
44
  private ensureCorrectFKOrderInPivotEntity;