@mikro-orm/core 7.3.0-dev.2 → 7.3.0-dev.20

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 (49) hide show
  1. package/EntityManager.js +131 -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 +8 -0
  6. package/entity/EntityFactory.js +69 -3
  7. package/entity/EntityLoader.js +31 -15
  8. package/entity/WrappedEntity.js +6 -1
  9. package/entity/defineEntity.d.ts +32 -19
  10. package/entity/defineEntity.js +9 -0
  11. package/entity/validators.js +4 -1
  12. package/errors.d.ts +7 -0
  13. package/errors.js +15 -0
  14. package/hydration/ObjectHydrator.js +6 -2
  15. package/metadata/MetadataDiscovery.d.ts +10 -0
  16. package/metadata/MetadataDiscovery.js +178 -30
  17. package/metadata/MetadataStorage.d.ts +8 -0
  18. package/metadata/MetadataStorage.js +47 -3
  19. package/metadata/MetadataValidator.js +2 -1
  20. package/metadata/types.d.ts +12 -0
  21. package/package.json +1 -1
  22. package/platforms/Platform.d.ts +6 -0
  23. package/platforms/Platform.js +26 -0
  24. package/serialization/EntitySerializer.js +52 -12
  25. package/types/DecimalType.js +34 -1
  26. package/types/DoubleType.d.ts +7 -3
  27. package/types/DoubleType.js +8 -3
  28. package/typings.d.ts +26 -6
  29. package/typings.js +5 -2
  30. package/unit-of-work/ChangeSetComputer.js +22 -7
  31. package/unit-of-work/ChangeSetPersister.js +24 -5
  32. package/unit-of-work/CommitOrderCalculator.js +2 -1
  33. package/unit-of-work/IdentityMap.d.ts +3 -2
  34. package/unit-of-work/IdentityMap.js +30 -7
  35. package/unit-of-work/UnitOfWork.js +45 -7
  36. package/utils/Configuration.d.ts +12 -1
  37. package/utils/EntityComparator.d.ts +5 -0
  38. package/utils/EntityComparator.js +87 -19
  39. package/utils/QueryHelper.d.ts +14 -0
  40. package/utils/QueryHelper.js +90 -13
  41. package/utils/RawQueryFragment.d.ts +5 -1
  42. package/utils/RawQueryFragment.js +27 -19
  43. package/utils/TransactionManager.js +4 -0
  44. package/utils/Utils.d.ts +2 -0
  45. package/utils/Utils.js +24 -8
  46. package/utils/clone.js +5 -7
  47. package/utils/env-vars.js +2 -0
  48. package/utils/fs-utils.js +19 -15
  49. package/utils/upsert-utils.js +2 -1
@@ -4,20 +4,50 @@ import { ReferenceKind } from '../enums.js';
4
4
  import { Reference } from '../entity/Reference.js';
5
5
  import { SerializationContext } from './SerializationContext.js';
6
6
  import { isRaw } from '../utils/RawQueryFragment.js';
7
- /** Returns true when any entry in `items` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
8
- function matchesPath(items, propName) {
9
- return items.some(item => item === propName || item === '*' || item.startsWith(propName + '.'));
7
+ // Keyed by option objects created for a single `serialize()` call, so caller-owned options are never cached.
8
+ const preparedOptions = new WeakMap();
9
+ function createPathLookup(items) {
10
+ const heads = new Set();
11
+ let all = false;
12
+ for (const item of items) {
13
+ const dot = item.indexOf('.');
14
+ heads.add(dot === -1 ? item : item.substring(0, dot));
15
+ all ||= item === '*';
16
+ }
17
+ return { all, heads };
18
+ }
19
+ /**
20
+ * Copies caller options once per `serialize()` call, so the derived child options and path lookups can be cached
21
+ * for every entity of the call without ever caching an options object the caller might mutate later.
22
+ */
23
+ function prepareOptions(options) {
24
+ if (preparedOptions.has(options)) {
25
+ return options;
26
+ }
27
+ const prepared = {
28
+ ...options,
29
+ populate: options.populate ? Utils.asArray(options.populate) : options.populate,
30
+ };
31
+ preparedOptions.set(prepared, { children: new Map() });
32
+ return prepared;
33
+ }
34
+ function pathLookup(options, key) {
35
+ return (preparedOptions.get(options)[key] ??= createPathLookup(options[key]));
36
+ }
37
+ /** Returns true when any entry in `options[key]` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
38
+ function matchesPath(options, key, propName) {
39
+ const lookup = pathLookup(options, key);
40
+ return lookup.all || lookup.heads.has(propName);
10
41
  }
11
42
  function isVisible(meta, propName, options) {
12
43
  const prop = meta.properties[propName];
13
44
  if (options.groups && prop?.groups) {
14
45
  return prop.groups.some(g => options.groups.includes(g));
15
46
  }
16
- if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options.fields, propName)) {
47
+ if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options, 'fields', propName)) {
17
48
  return false;
18
49
  }
19
- if (Array.isArray(options.populate) &&
20
- options.populate.find(item => item === propName || item.startsWith(propName + '.'))) {
50
+ if (Array.isArray(options.populate) && pathLookup(options, 'populate').heads.has(propName)) {
21
51
  return true;
22
52
  }
23
53
  if (options.exclude?.find(item => item === propName)) {
@@ -30,7 +60,7 @@ function isVisible(meta, propName, options) {
30
60
  function isPopulated(propName, options) {
31
61
  if (typeof options.populate !== 'boolean' &&
32
62
  Array.isArray(options.populate) &&
33
- matchesPath(options.populate, propName)) {
63
+ matchesPath(options, 'populate', propName)) {
34
64
  return true;
35
65
  }
36
66
  if (typeof options.populate === 'boolean') {
@@ -45,10 +75,10 @@ export class EntitySerializer {
45
75
  const wrapped = helper(entity);
46
76
  const meta = wrapped.__meta;
47
77
  let contextCreated = false;
78
+ options = prepareOptions(options);
48
79
  if (!wrapped.__serializationContext.root) {
49
80
  const root = new SerializationContext();
50
81
  SerializationContext.propagate(root, entity, (meta, prop) => meta.properties[prop]?.kind !== ReferenceKind.SCALAR);
51
- options.populate = (options.populate ? Utils.asArray(options.populate) : options.populate);
52
82
  contextCreated = true;
53
83
  }
54
84
  const root = wrapped.__serializationContext.root;
@@ -145,7 +175,8 @@ export class EntitySerializer {
145
175
  if (entity[prop] instanceof Function) {
146
176
  const returnValue = entity[prop]();
147
177
  if (!options.ignoreSerializers && serializer) {
148
- return serializer(returnValue, this.extractChildOptions(options, prop));
178
+ // copy, so a serializer mutating its options cannot leak into the cached child options
179
+ return serializer(returnValue, { ...this.extractChildOptions(options, prop) });
149
180
  }
150
181
  return returnValue;
151
182
  }
@@ -186,7 +217,12 @@ export class EntitySerializer {
186
217
  return prop.customType.toJSON(value, platform);
187
218
  }
188
219
  static extractChildOptions(options, prop) {
189
- return {
220
+ const prepared = preparedOptions.get(options);
221
+ const cached = prepared.children.get(prop);
222
+ if (cached) {
223
+ return cached;
224
+ }
225
+ const child = {
190
226
  ...options,
191
227
  populate: Array.isArray(options.populate)
192
228
  ? Utils.extractChildElements(options.populate, prop, '*')
@@ -194,6 +230,9 @@ export class EntitySerializer {
194
230
  exclude: Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop) : options.exclude,
195
231
  fields: Array.isArray(options.fields) ? this.extractChildFields(options.fields, prop) : options.fields,
196
232
  };
233
+ prepared.children.set(prop, child);
234
+ preparedOptions.set(child, { children: new Map() });
235
+ return child;
197
236
  }
198
237
  /**
199
238
  * Extracts the nested `fields` whitelist for a child property. A bare parent name (`fields: ['books']`) or a
@@ -269,8 +308,9 @@ export class EntitySerializer {
269
308
  * ```
270
309
  */
271
310
  export function serialize(entities, options) {
311
+ const prepared = prepareOptions(options ?? {});
272
312
  if (Array.isArray(entities)) {
273
- return entities.map(e => EntitySerializer.serialize(e, options));
313
+ return entities.map(e => EntitySerializer.serialize(e, prepared));
274
314
  }
275
- return EntitySerializer.serialize(entities, options);
315
+ return EntitySerializer.serialize(entities, prepared);
276
316
  }
@@ -1,4 +1,24 @@
1
1
  import { Type } from './Type.js';
2
+ /** Scales a plain decimal string to an integer rounded half away from zero at `scale`, like the DB does. */
3
+ function toScaledInteger(value, scale) {
4
+ if (typeof value !== 'string') {
5
+ return undefined;
6
+ }
7
+ const negative = value.startsWith('-');
8
+ const [int, fraction = ''] = (negative ? value.slice(1) : value).split('.');
9
+ const digits = int + fraction;
10
+ for (let i = 0; i < digits.length; i++) {
11
+ const code = digits.charCodeAt(i);
12
+ if (code < 48 || code > 57) {
13
+ return undefined;
14
+ }
15
+ }
16
+ let scaled = BigInt('0' + int + fraction.slice(0, scale).padEnd(scale, '0'));
17
+ if (fraction.charCodeAt(scale) >= 53) {
18
+ scaled++;
19
+ }
20
+ return negative ? -scaled : scaled;
21
+ }
2
22
  /**
3
23
  * Type that maps an SQL DECIMAL to a JS string or number.
4
24
  */
@@ -16,7 +36,20 @@ export class DecimalType extends Type {
16
36
  return String(value);
17
37
  }
18
38
  compareValues(a, b) {
19
- return this.platform.formatDecimal(a, this.prop?.scale) === this.platform.formatDecimal(b, this.prop?.scale);
39
+ if (a === b) {
40
+ return true;
41
+ }
42
+ const scale = this.prop?.scale;
43
+ if (this.platform.formatDecimal(a, scale) !== this.platform.formatDecimal(b, scale)) {
44
+ return false;
45
+ }
46
+ // doubles are exact only up to 15 significant digits, so only higher precision strings can differ past the numeric check
47
+ if ((this.prop?.precision ?? 0) <= 15 || this.compareAsType() !== 'string') {
48
+ return true;
49
+ }
50
+ const scaledA = toScaledInteger(a, scale ?? 0);
51
+ const scaledB = toScaledInteger(b, scale ?? 0);
52
+ return scaledA == null || scaledB == null || scaledA === scaledB;
20
53
  }
21
54
  getColumnType(prop, platform) {
22
55
  return platform.getDecimalTypeDeclarationSQL(prop);
@@ -2,10 +2,14 @@ import { Type } from './Type.js';
2
2
  import type { Platform } from '../platforms/Platform.js';
3
3
  import type { EntityProperty } from '../typings.js';
4
4
  /**
5
- * Type that maps an SQL DOUBLE to a JS string or number.
5
+ * Type that maps an SQL DOUBLE to a JS number (default) or string.
6
6
  */
7
- export declare class DoubleType extends Type<number | string, string> {
8
- convertToJSValue(value: string): number | string;
7
+ export declare class DoubleType<Mode extends 'number' | 'string' = 'number'> extends Type<JSTypeByMode<Mode>, string> {
8
+ mode?: Mode | undefined;
9
+ constructor(mode?: Mode | undefined);
10
+ convertToJSValue(value: string): JSTypeByMode<Mode>;
9
11
  getColumnType(prop: EntityProperty, platform: Platform): string;
10
12
  compareAsType(): string;
11
13
  }
14
+ type JSTypeByMode<Mode extends 'number' | 'string'> = Mode extends 'number' ? number : string;
15
+ export {};
@@ -1,11 +1,16 @@
1
1
  import { Type } from './Type.js';
2
2
  /**
3
- * Type that maps an SQL DOUBLE to a JS string or number.
3
+ * Type that maps an SQL DOUBLE to a JS number (default) or string.
4
4
  */
5
5
  export class DoubleType extends Type {
6
+ mode;
7
+ constructor(mode) {
8
+ super();
9
+ this.mode = mode;
10
+ }
6
11
  /* v8 ignore next */
7
12
  convertToJSValue(value) {
8
- if (this.prop?.runtimeType === 'number') {
13
+ if ((this.mode ?? this.prop?.runtimeType) === 'number') {
9
14
  return +value;
10
15
  }
11
16
  return String(value);
@@ -14,6 +19,6 @@ export class DoubleType extends Type {
14
19
  return platform.getDoubleDeclarationSQL();
15
20
  }
16
21
  compareAsType() {
17
- return this.prop?.runtimeType ?? 'number';
22
+ return this.mode ?? this.prop?.runtimeType ?? 'number';
18
23
  }
19
24
  }
package/typings.d.ts CHANGED
@@ -12,7 +12,7 @@ import type { SerializeOptions } from './serialization/EntitySerializer.js';
12
12
  import type { MetadataStorage } from './metadata/MetadataStorage.js';
13
13
  import type { Routine } from './metadata/Routine.js';
14
14
  import type { EntitySchema } from './metadata/EntitySchema.js';
15
- import type { EntityPartitionBy, IndexColumnOptions } from './metadata/types.js';
15
+ import type { EntityPartitionBy, IndexColumnOptions, SequenceOptions } from './metadata/types.js';
16
16
  import type { Type, types } from './types/index.js';
17
17
  import type { Platform } from './platforms/Platform.js';
18
18
  import type { Configuration } from './utils/Configuration.js';
@@ -358,12 +358,15 @@ type ElemMatchCondition<T extends Record<string, any>> = {
358
358
  $and?: ElemMatchCondition<T>[];
359
359
  $not?: ElemMatchCondition<T>;
360
360
  };
361
- type ElemMatchFilter<T> = T extends readonly (infer E)[] ? E extends Record<string, any> ? {
361
+ type ArrayFilter<T> = T extends readonly (infer E)[] ? [E] extends [Scalar] ? {
362
+ $in?: readonly (readonly ExpandScalar<E>[])[];
363
+ $nin?: readonly (readonly ExpandScalar<E>[])[];
364
+ } : E extends Record<string, any> ? {
362
365
  $elemMatch: ElemMatchCondition<E>;
363
366
  } : never : never;
364
367
  /** Object form of a filter query, mapping entity keys to their filter conditions. */
365
368
  export type FilterObject<T> = {
366
- -readonly [K in EntityKey<T>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ElemMatchFilter<FilterObjectProp<T, K>> | null;
369
+ -readonly [K in EntityKey<T>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ArrayFilter<FilterObjectProp<T, K>> | null;
367
370
  };
368
371
  /** Recursively expands a type into its `FilterQuery` form for nested object filtering. */
369
372
  export type ExpandQuery<T> = T extends object ? (T extends Scalar ? never : FilterQuery<T>) : FilterValue<T>;
@@ -383,7 +386,7 @@ export type FilterQuery<T> = ObjectQuery<T> | NonNullable<ExpandScalar<Primary<T
383
386
  * Used when `using` option is set in `FindOptions` to enforce type-safe index usage.
384
387
  */
385
388
  export type IndexFilterQuery<T, Using extends string> = [Using] extends [never] ? FilterQuery<T> : (OperatorMap<T> & {
386
- -readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ElemMatchFilter<FilterObjectProp<T, K>> | null;
389
+ -readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ArrayFilter<FilterObjectProp<T, K>> | null;
387
390
  }) | NonNullable<ExpandScalar<Primary<T>>> | IndexFilterQuery<T, Using>[];
388
391
  /** Replaces `where` and `using` on an options type with index-aware variants when `Using` is specified. */
389
392
  export type WithUsingOptions<Opts, Entity, Using extends string> = Omit<Opts, 'where' | 'using'> & {
@@ -564,6 +567,13 @@ export declare namespace LazyRef {
564
567
  [__lazyRef]?: (arg: T) => T;
565
568
  }
566
569
  }
570
+ /**
571
+ * The brand's only key is optional, so an object type without named properties (like `Record<string, number>`)
572
+ * extends `LazyRef.Brand<infer U>` too, with `U` inferred as `unknown`. `LazyRef<T>` needs `T extends object`,
573
+ * so an `unknown` target means the type is not a `LazyRef` and resolves to `Fallback`. `any` stays `any`.
574
+ * Only checked once `unknown extends U` holds, so a real `LazyRef` skips it.
575
+ */
576
+ type NotLazyRef<U, Fallback> = IsAny<U> extends true ? U : Fallback;
567
577
  /** Alias for `Reference<T> & { id: number }` (see {@apilink Ref}). */
568
578
  export type EntityRef<T extends object> = true extends IsUnknown<PrimaryProperty<T>> ? Reference<T> : IsAny<T> extends true ? Reference<T> : {
569
579
  [K in PrimaryProperty<T> & keyof T]: T[K];
@@ -986,6 +996,7 @@ export interface EntityProperty<Owner = any, Target = any> {
986
996
  hasConvertToJSValueSQL: boolean;
987
997
  hasConvertToDatabaseValueSQL: boolean;
988
998
  autoincrement?: boolean;
999
+ sequence?: SequenceOptions;
989
1000
  returning?: boolean;
990
1001
  primary?: boolean;
991
1002
  serializedPrimaryKey: boolean;
@@ -1175,6 +1186,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1175
1186
  meta: EntityMetadata<Entity>;
1176
1187
  prop: EntityProperty<Entity>;
1177
1188
  }[];
1189
+ targetKeys?: EntityKey<Entity>[];
1178
1190
  comparableProps: EntityProperty<Entity>[];
1179
1191
  trackingProps: EntityProperty<Entity>[];
1180
1192
  hydrateProps: EntityProperty<Entity>[];
@@ -1258,6 +1270,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1258
1270
  orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
1259
1271
  /** @internal can be used for computed numeric cache keys */
1260
1272
  readonly _id: number;
1273
+ /** @internal name that does not depend on module evaluation order (unlike `_id`), used to key `compiledFunctions` */
1274
+ compiledName?: string;
1261
1275
  }
1262
1276
  /** Options for `ISchemaGenerator.create()`. */
1263
1277
  export interface CreateSchemaOptions {
@@ -1616,7 +1630,8 @@ export type UnboxArray<T> = T extends any[] ? ArrayElement<T> : T;
1616
1630
  /** Extracts the element type from an array type. */
1617
1631
  export type ArrayElement<ArrayType extends unknown[]> = ArrayType extends (infer ElementType)[] ? ElementType : never;
1618
1632
  /** Unwraps a property type from its wrapper (Reference, Collection, or array) to the inner entity type. */
1619
- export type ExpandProperty<T> = T extends LazyRef.Brand<infer U> ? NonNullable<U> : T extends ReferenceShape<infer U> ? NonNullable<U> : T extends CollectionShape<infer U> ? NonNullable<U> : T extends (infer U)[] ? NonNullable<U> : NonNullable<T>;
1633
+ export type ExpandProperty<T> = ExpandPropertyOf<T, T>;
1634
+ type ExpandPropertyOf<T, R> = T extends LazyRef.Brand<infer U> ? unknown extends U ? NotLazyRef<U, NonNullable<R>> : NonNullable<U> : T extends ReferenceShape<infer U> ? NonNullable<U> : T extends CollectionShape<infer U> ? NonNullable<U> : T extends (infer U)[] ? NonNullable<U> : NonNullable<T>;
1620
1635
  type LoadedLoadable<T, E extends object> = T extends CollectionShape ? LoadedCollection<E> : T extends ScalarReference<infer U> ? LoadedScalarReference<U> : T extends LazyRef.Brand<any> ? E : T extends ReferenceShape ? T & LoadedReference<E> : T extends Scalar ? T : T extends (infer U)[] ? U extends Scalar ? T : E[] : E;
1621
1636
  type IsTrue<T> = IsNever<T> extends true ? false : T extends boolean ? (T extends true ? true : false) : false;
1622
1637
  type StringLiteral<T> = T extends string ? (string extends T ? never : T) : never;
@@ -1651,7 +1666,12 @@ export type MergeSelected<T, U, F extends string> = IsLoadedType<T> extends true
1651
1666
  export type MergeLoaded<T, U, P extends string, F extends string, E extends string, R extends boolean = false> = IsLoadedType<T> extends true ? T & Loaded<U, P, F, E> : Loaded<T, P, F, E>;
1652
1667
  /** Extracts the nullability modifiers (`null`, `undefined`, or both) from a type `T`. */
1653
1668
  export type AddOptional<T> = undefined | null extends T ? null | undefined : null extends T ? null : undefined extends T ? undefined : never;
1654
- type LoadedProp<T, L extends string = never, F extends string = '*', E extends string = never> = LoadedLoadable<T, Loaded<ExtractTypeWithLazyRef<T>, L, F, E>>;
1669
+ /**
1670
+ * Like {@link ExtractTypeWithLazyRef}, but a record type that matched `LazyRef.Brand` resolves to itself (see
1671
+ * {@link NotLazyRef}). `AutoPath` keeps the unguarded version, as it is on its hot path.
1672
+ */
1673
+ type ExtractLoadedType<T> = T extends LazyRef.Brand<infer U> ? (unknown extends U ? NotLazyRef<U, ExtractType<T>> : U) : ExtractType<T>;
1674
+ type LoadedProp<T, L extends string = never, F extends string = '*', E extends string = never> = LoadedLoadable<T, Loaded<ExtractLoadedType<T>, L, F, E>>;
1655
1675
  /** Extracts the eager-loaded property names declared via `[EagerProps]` as a string union. */
1656
1676
  export type AddEager<T> = ExtractEagerProps<T> & string;
1657
1677
  /** Combines an explicit populate hint `L` with the entity's eagerly loaded properties. */
package/typings.js CHANGED
@@ -304,8 +304,11 @@ export class EntityMetadata {
304
304
  }, { __gettersDefined: { value: true, enumerable: false } });
305
305
  }
306
306
  initIndexes(prop) {
307
- const simpleIndex = this.indexes.find(index => index.properties === prop.name && !index.options && !index.type && !index.expression && index.where == null);
308
- const simpleUnique = this.uniques.find(index => index.properties === prop.name && !index.options && !index.expression && index.where == null);
307
+ // `prop.index`/`prop.unique` keep only the name, so an index with any other option must stay in `meta.indexes`
308
+ const isSimple = (index) => index.properties === prop.name &&
309
+ Object.entries(index).every(([key, value]) => key === 'name' || key === 'properties' || value == null);
310
+ const simpleIndex = this.indexes.find(isSimple);
311
+ const simpleUnique = this.uniques.find(isSimple);
309
312
  const owner = prop.kind === ReferenceKind.MANY_TO_ONE;
310
313
  if (!prop.index && simpleIndex) {
311
314
  Utils.defaultValue(simpleIndex, 'name', true);
@@ -7,6 +7,7 @@ import { Reference } from '../entity/Reference.js';
7
7
  import { PolymorphicRef } from '../entity/PolymorphicRef.js';
8
8
  import { ReferenceKind } from '../enums.js';
9
9
  import { isRaw } from '../utils/RawQueryFragment.js';
10
+ import { ValidationError } from '../errors.js';
10
11
  /** @internal Computes change sets by comparing entity state against original snapshots. */
11
12
  export class ChangeSetComputer {
12
13
  #comparator;
@@ -48,6 +49,10 @@ export class ChangeSetComputer {
48
49
  if (Utils.equals(data, wrapped.__originalEntityData)) {
49
50
  return null;
50
51
  }
52
+ // a reference known only by its `targetKey` has no PK, so the update would have no condition
53
+ if (!wrapped.hasPrimaryKey()) {
54
+ throw ValidationError.referenceWithoutPK(entity);
55
+ }
51
56
  }
52
57
  const changeSet = new ChangeSet(entity, type, this.computePayload(entity), meta);
53
58
  changeSet.originalEntity = wrapped.__originalEntityData;
@@ -160,23 +165,33 @@ export class ChangeSetComputer {
160
165
  const targets = Utils.unwrapProperty(changeSet.entity, changeSet.meta, prop);
161
166
  targets.forEach(([rawTarget, idx]) => {
162
167
  const target = Reference.unwrapReference(rawTarget);
163
- const needsProcessing = target != null && (prop.targetKey != null || !target.__helper.hasPrimaryKey());
168
+ // a same-named scalar of another polymorphic embeddable variant is not a reference
169
+ const needsProcessing = Utils.isEntity(target) && (prop.targetKey != null || !target.__helper.hasPrimaryKey());
164
170
  if (needsProcessing) {
171
+ // a reference known only by its `targetKey` has no PK, and is never inserted to generate one
172
+ if (!prop.targetKey && !target.__helper.__initialized) {
173
+ throw ValidationError.referenceWithoutPK(target);
174
+ }
165
175
  let value = prop.targetKey ? target[prop.targetKey] : target.__helper.__identifier;
166
- /* v8 ignore next */
167
- if (prop.targetKey && prop.targetMeta) {
168
- const targetProp = prop.targetMeta.properties[prop.targetKey];
176
+ // use the actual target's meta, a polymorphic relation has several
177
+ if (prop.targetKey) {
178
+ const targetProp = target.__meta.properties[prop.targetKey];
169
179
  if (targetProp?.customType) {
170
180
  value = targetProp.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' });
171
181
  }
172
182
  }
173
183
  if (prop.polymorphic) {
174
184
  const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, target.constructor);
175
- Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, new PolymorphicRef(discriminator, value), idx);
185
+ value = new PolymorphicRef(discriminator, value);
176
186
  }
177
- else {
178
- Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
187
+ // an unchanged `targetKey` FK is not part of the diff
188
+ if (prop.targetKey &&
189
+ changeSet.type === ChangeSetType.UPDATE &&
190
+ !(prop.name in changeSet.payload) &&
191
+ Utils.equals(changeSet.originalEntity?.[prop.name], value)) {
192
+ return;
179
193
  }
194
+ Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
180
195
  }
181
196
  });
182
197
  }
@@ -196,10 +196,17 @@ export class ChangeSetPersister {
196
196
  }
197
197
  async persistManagedEntities(meta, changeSets, options) {
198
198
  const size = this.#config.get('batchSize');
199
- for (let i = 0; i < changeSets.length; i += size) {
200
- const chunk = changeSets.slice(i, i + size);
201
- await this.persistManagedEntitiesBatch(meta, chunk, options);
202
- await this.reloadVersionValues(meta, chunk, options);
199
+ // a batch checks the version in all rows or none, so references without a loaded version go separately
200
+ const hasVersion = (cs) => cs.entity[meta.versionProperty] != null;
201
+ const groups = meta.ownsVersionProperty()
202
+ ? [changeSets.filter(hasVersion), changeSets.filter(cs => !hasVersion(cs))]
203
+ : [changeSets];
204
+ for (const group of groups) {
205
+ for (let i = 0; i < group.length; i += size) {
206
+ const chunk = group.slice(i, i + size);
207
+ await this.persistManagedEntitiesBatch(meta, chunk, options);
208
+ await this.reloadVersionValues(meta, chunk, options);
209
+ }
203
210
  }
204
211
  }
205
212
  checkConcurrencyKeys(meta, changeSet, cond) {
@@ -223,9 +230,14 @@ export class ChangeSetPersister {
223
230
  });
224
231
  const cond = [];
225
232
  const payload = [];
233
+ // like in `updateEntity()`, but the batch needs the version in all rows or none
234
+ const versioned = meta.ownsVersionProperty() && changeSets.every(cs => cs.entity[meta.versionProperty] != null);
226
235
  for (const changeSet of changeSets) {
227
236
  const where = changeSet.getPrimaryKey(true);
228
237
  this.checkConcurrencyKeys(meta, changeSet, where);
238
+ if (versioned) {
239
+ where[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
240
+ }
229
241
  cond.push(where);
230
242
  payload.push(changeSet.payload);
231
243
  }
@@ -235,6 +247,12 @@ export class ChangeSetPersister {
235
247
  // from those to be able to match them with `getSerializedPrimaryKey()` of the entity
236
248
  const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
237
249
  res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
250
+ if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) &&
251
+ res.affectedRows < changeSets.length) {
252
+ // without `returning`, the stale entity can't be identified, so we report the first one
253
+ const failed = changeSets.find(cs => !map.has(helper(cs.entity).getSerializedPrimaryKey())) ?? changeSets[0];
254
+ throw OptimisticLockError.lockFailed(failed.entity);
255
+ }
238
256
  for (const changeSet of changeSets) {
239
257
  if (res.rows) {
240
258
  const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
@@ -484,7 +502,8 @@ export class ChangeSetPersister {
484
502
  }
485
503
  if (upsert) {
486
504
  for (const prop of meta.props) {
487
- if (prop.customType && prop.name in mapped) {
505
+ // null is not converted, the same way the hydrator handles it
506
+ if (prop.customType && mapped[prop.name] != null) {
488
507
  mapped[prop.name] = prop.customType.convertToJSValue(mapped[prop.name], this.#platform);
489
508
  }
490
509
  }
@@ -44,7 +44,8 @@ export class CommitOrderCalculator {
44
44
  if (!toOneOwner && !toManyOwner) {
45
45
  return;
46
46
  }
47
- const propertyType = prop.targetMeta?.root._id;
47
+ // TPT tables are separate nodes and the FK references the target's own table, not the root
48
+ const propertyType = (prop.targetMeta?.inheritanceType === 'tpt' ? prop.targetMeta : prop.targetMeta?.root)?._id;
48
49
  if (propertyType == null || !this.hasNode(propertyType)) {
49
50
  return;
50
51
  }
@@ -1,9 +1,9 @@
1
1
  import type { AnyEntity, EntityMetadata } from '../typings.js';
2
- /** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
2
+ /** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
3
3
  export declare class IdentityMap {
4
4
  #private;
5
5
  constructor(defaultSchema?: string);
6
- /** Stores an entity in the identity map under its primary key hash. */
6
+ /** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
7
7
  store<T>(item: T): void;
8
8
  /**
9
9
  * Stores an entity under an alternate key (non-PK property).
@@ -25,6 +25,7 @@ export declare class IdentityMap {
25
25
  /** Returns all entities currently in the identity map. */
26
26
  values(): AnyEntity[];
27
27
  [Symbol.iterator](): IterableIterator<AnyEntity>;
28
+ private getEntities;
28
29
  /** Returns all hash keys currently in the identity map. */
29
30
  keys(): string[];
30
31
  /**
@@ -1,15 +1,29 @@
1
- /** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
1
+ /** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
2
2
  export class IdentityMap {
3
3
  #defaultSchema;
4
4
  #registry = new Map();
5
5
  /** Tracks alternate key hashes for each entity so we can clean them up on delete */
6
6
  #alternateKeys = new WeakMap();
7
+ #storesWithAlternateKeys = new WeakSet();
7
8
  constructor(defaultSchema) {
8
9
  this.#defaultSchema = defaultSchema;
9
10
  }
10
- /** Stores an entity in the identity map under its primary key hash. */
11
+ /** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
11
12
  store(item) {
12
- this.getStore(item.__meta.root).set(this.getPkHash(item), item);
13
+ const meta = item.__meta.root;
14
+ const wrapped = item.__helper;
15
+ // PK-less references (known only by a `targetKey`) would share one slot that outlives their later removal
16
+ if (!meta.targetKeys || wrapped.hasPrimaryKey()) {
17
+ this.getStore(meta).set(this.getPkHash(item), item);
18
+ }
19
+ // references resolved via `targetKey` look the entity up by that key instead of the PK
20
+ for (const key of meta.targetKeys ?? []) {
21
+ const value = item[key];
22
+ if (value != null) {
23
+ const schema = wrapped.__schema ?? meta.schema ?? this.#defaultSchema;
24
+ this.storeByKey(item, key, '' + value, schema);
25
+ }
26
+ }
13
27
  }
14
28
  /**
15
29
  * Stores an entity under an alternate key (non-PK property).
@@ -17,7 +31,9 @@ export class IdentityMap {
17
31
  */
18
32
  storeByKey(item, key, value, schema) {
19
33
  const hash = this.getKeyHash(key, value, schema);
20
- this.getStore(item.__meta.root).set(hash, item);
34
+ const store = this.getStore(item.__meta.root);
35
+ store.set(hash, item);
36
+ this.#storesWithAlternateKeys.add(store);
21
37
  // Track this alternate key so we can clean it up when the entity is deleted
22
38
  let keys = this.#alternateKeys.get(item);
23
39
  if (!keys) {
@@ -35,7 +51,10 @@ export class IdentityMap {
35
51
  const altKeys = this.#alternateKeys.get(item);
36
52
  if (altKeys) {
37
53
  for (const hash of altKeys) {
38
- store.delete(hash);
54
+ // the hash might be owned by another entity by now, e.g. after swapping unique values
55
+ if (store.get(hash) === item) {
56
+ store.delete(hash);
57
+ }
39
58
  }
40
59
  this.#alternateKeys.delete(item);
41
60
  }
@@ -69,7 +88,7 @@ export class IdentityMap {
69
88
  values() {
70
89
  const ret = [];
71
90
  for (const store of this.#registry.values()) {
72
- for (const item of store.values()) {
91
+ for (const item of this.getEntities(store)) {
73
92
  ret.push(item);
74
93
  }
75
94
  }
@@ -77,11 +96,15 @@ export class IdentityMap {
77
96
  }
78
97
  *[Symbol.iterator]() {
79
98
  for (const store of this.#registry.values()) {
80
- for (const item of store.values()) {
99
+ for (const item of this.getEntities(store)) {
81
100
  yield item;
82
101
  }
83
102
  }
84
103
  }
104
+ getEntities(store) {
105
+ // an entity indexed by alternate keys occupies several slots of its store
106
+ return this.#storesWithAlternateKeys.has(store) ? new Set(store.values()) : store.values();
107
+ }
85
108
  /** Returns all hash keys currently in the identity map. */
86
109
  keys() {
87
110
  const ret = [];