From ec650013f3486a89a12c105ea49a8fc28b1f8072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Sun, 8 Oct 2023 20:01:13 +0200 Subject: [PATCH] feat(core): respect updates to M:N inverse sides and batch them (#4798) Implements diffing for inverse sides of M:N relations (for SQL drivers). This was previously working only if the items were initialized. ```ts const tag = await em.findOne(BookTag, 1); // tag.books in an inverse side tag.books.add(em.getReference(Book, 123)); await em.flush(); ``` The M:N updates are now also batched. Closes #4564 --- packages/core/src/EntityManager.ts | 8 +- packages/core/src/drivers/DatabaseDriver.ts | 18 +- packages/core/src/drivers/IDatabaseDriver.ts | 2 +- packages/core/src/entity/ArrayCollection.ts | 10 +- packages/core/src/entity/Collection.ts | 10 +- .../src/unit-of-work/ChangeSetComputer.ts | 6 +- packages/core/src/unit-of-work/UnitOfWork.ts | 3 +- packages/core/src/utils/Utils.ts | 17 +- packages/knex/src/AbstractSqlDriver.ts | 164 +++++++----------- packages/knex/src/PivotCollectionPersister.ts | 163 +++++++++++++++++ packages/knex/src/query/QueryBuilder.ts | 2 +- tests/EntityManager.mongo.test.ts | 30 +++- tests/EntityManager.mysql.test.ts | 28 +++ tests/EntityManager.postgre.test.ts | 7 +- tests/EntityManager.sqlite.test.ts | 1 - tests/EntityManager.sqlite2.test.ts | 1 - .../composite-keys.mysql.test.ts | 2 +- .../composite-keys.sqlite.test.ts | 4 +- ...pivot-entity-auto-discovery.sqlite.test.ts | 1 - ...om-pivot-entity-wrong-order.sqlite.test.ts | 1 - .../custom-pivot-entity.sqlite.test.ts | 13 +- tests/features/entity-assigner/GH1811.test.ts | 4 +- .../features/filters/filters.postgres.test.ts | 2 +- ...le-schemas-entity-manager.postgres.test.ts | 9 +- .../features/multiple-schemas/GH3177.test.ts | 2 +- .../multiple-schemas.postgres.test.ts | 9 +- .../result-cache/result-cache.postgre.test.ts | 36 ++-- .../sharing-column-in-composite-pk-fk.test.ts | 4 +- tests/issues/GH1041.test.ts | 4 +- tests/issues/GH4027.test.ts | 2 +- 30 files changed, 379 insertions(+), 184 deletions(-) create mode 100644 packages/knex/src/PivotCollectionPersister.ts diff --git a/packages/core/src/EntityManager.ts b/packages/core/src/EntityManager.ts index f8f81fa2b517..38cbddb92071 100644 --- a/packages/core/src/EntityManager.ts +++ b/packages/core/src/EntityManager.ts @@ -1486,7 +1486,7 @@ export class EntityManager { * Tells the EntityManager to make an instance managed and persistent. * The entity will be entered into the database at or before transaction commit or as a result of the flush operation. */ - persist(entity: Entity | Reference | (Entity | Reference)[]): this { + persist(entity: Entity | Reference | Iterable>): this { const em = this.getContext(); if (Utils.isEntity(entity)) { @@ -1515,7 +1515,7 @@ export class EntityManager { * Persists your entity immediately, flushing all not yet persisted changes to the database too. * Equivalent to `em.persist(e).flush()`. */ - async persistAndFlush(entity: AnyEntity | Reference | (AnyEntity | Reference)[]): Promise { + async persistAndFlush(entity: AnyEntity | Reference | Iterable>): Promise { await this.persist(entity).flush(); } @@ -1525,7 +1525,7 @@ export class EntityManager { * * To remove entities by condition, use `em.nativeDelete()`. */ - remove(entity: Entity | Reference | (Entity | Reference)[]): this { + remove(entity: Entity | Reference | Iterable>): this { const em = this.getContext(); if (Utils.isEntity(entity)) { @@ -1552,7 +1552,7 @@ export class EntityManager { * Removes an entity instance immediately, flushing all not yet persisted changes to the database too. * Equivalent to `em.remove(e).flush()` */ - async removeAndFlush(entity: AnyEntity | Reference): Promise { + async removeAndFlush(entity: AnyEntity | Reference | Iterable>): Promise { await this.remove(entity).flush(); } diff --git a/packages/core/src/drivers/DatabaseDriver.ts b/packages/core/src/drivers/DatabaseDriver.ts index 59774d2fdc7e..f32cf262be50 100644 --- a/packages/core/src/drivers/DatabaseDriver.ts +++ b/packages/core/src/drivers/DatabaseDriver.ts @@ -92,10 +92,20 @@ export abstract class DatabaseDriver implements IDatabaseD throw new Error(`${this.constructor.name} does not use pivot tables`); } - async syncCollection(coll: Collection, options?: DriverMethodOptions): Promise { - const pk = coll.property.targetMeta!.primaryKeys[0]; - const data = { [coll.property.name]: coll.getIdentifiers(pk) } as EntityData; - await this.nativeUpdate(coll.owner.constructor.name, helper(coll.owner).getPrimaryKey() as FilterQuery, data, options); + async syncCollections(collections: Iterable>, options?: DriverMethodOptions): Promise { + for (const coll of collections) { + if (!coll.property.owner) { + if (coll.getSnapshot() === undefined) { + throw ValidationError.cannotModifyInverseCollection(coll.owner, coll.property); + } + + continue; + } + + const pk = coll.property.targetMeta!.primaryKeys[0]; + const data = { [coll.property.name]: coll.getIdentifiers(pk) } as EntityData; + await this.nativeUpdate(coll.owner.constructor.name, helper(coll.owner).getPrimaryKey() as FilterQuery, data, options); + } } mapResult(result: EntityDictionary, meta?: EntityMetadata, populate: PopulateOptions[] = []): EntityData | null { diff --git a/packages/core/src/drivers/IDatabaseDriver.ts b/packages/core/src/drivers/IDatabaseDriver.ts index 93d8485aeb34..2a09e79b44b0 100644 --- a/packages/core/src/drivers/IDatabaseDriver.ts +++ b/packages/core/src/drivers/IDatabaseDriver.ts @@ -51,7 +51,7 @@ export interface IDatabaseDriver { nativeDelete(entityName: string, where: FilterQuery, options?: NativeDeleteOptions): Promise>; - syncCollection(collection: Collection, options?: DriverMethodOptions): Promise; + syncCollections(collections: Iterable>, options?: DriverMethodOptions): Promise; count(entityName: string, where: FilterQuery, options?: CountOptions): Promise; diff --git a/packages/core/src/entity/ArrayCollection.ts b/packages/core/src/entity/ArrayCollection.ts index 1f3981fc0a95..ee6450c2bd21 100644 --- a/packages/core/src/entity/ArrayCollection.ts +++ b/packages/core/src/entity/ArrayCollection.ts @@ -64,7 +64,7 @@ export class ArrayCollection { }) as unknown as U[]; } - add(entity: T | Reference | (T | Reference)[], ...entities: (T | Reference)[]): void { + add(entity: T | Reference | Iterable>, ...entities: (T | Reference)[]): void { entities = Utils.asArray(entity).concat(entities); for (const item of entities) { @@ -79,12 +79,12 @@ export class ArrayCollection { } } - set(items: (T | Reference)[]): void { - if (this.compare(items.map(item => Reference.unwrapReference(item)))) { + set(items: Iterable>): void { + if (this.compare(Utils.asArray(items).map(item => Reference.unwrapReference(item)))) { return; } - this.removeAll(); + this.remove(this.items); this.add(items); } @@ -123,7 +123,7 @@ export class ArrayCollection { * is not the same as `em.remove()`. If we want to delete the entity by removing it from collection, we need to enable `orphanRemoval: true`, * which tells the ORM we don't want orphaned entities to exist, so we know those should be removed. */ - remove(entity: T | Reference | (T | Reference)[], ...entities: (T | Reference)[]): void { + remove(entity: T | Reference | Iterable>, ...entities: (T | Reference)[]): void { entities = Utils.asArray(entity).concat(entities); let modified = false; diff --git a/packages/core/src/entity/Collection.ts b/packages/core/src/entity/Collection.ts index c51add263072..63759ac314d6 100644 --- a/packages/core/src/entity/Collection.ts +++ b/packages/core/src/entity/Collection.ts @@ -158,7 +158,7 @@ export class Collection extends Arr return super.toJSON() as unknown as EntityDTO[]; } - override add(entity: TT | Reference | (TT | Reference)[], ...entities: (TT | Reference)[]): void { + override add(entity: TT | Reference | Iterable>, ...entities: (TT | Reference)[]): void { entities = Utils.asArray(entity).concat(entities); const unwrapped = entities.map(i => Reference.unwrapReference(i)) as T[]; unwrapped.forEach(entity => this.validateItemType(entity)); @@ -166,13 +166,14 @@ export class Collection extends Arr this.cancelOrphanRemoval(unwrapped); } - override set(items: (TT | Reference)[]): void { + override set(items: Iterable>): void { if (!this.initialized) { this.initialized = true; this.snapshot = undefined; } super.set(items as T[]); + this.setDirty(); } /** @@ -187,7 +188,7 @@ export class Collection extends Arr /** * @inheritDoc */ - override remove(entity: TT | Reference | (TT | Reference)[] | ((item: TT) => boolean), ...entities: (TT | Reference)[]): void { + override remove(entity: TT | Reference | Iterable> | ((item: TT) => boolean), ...entities: (TT | Reference)[]): void { if (entity instanceof Function) { for (const item of this.items) { if (entity(item as TT)) { @@ -214,8 +215,7 @@ export class Collection extends Arr * @inheritDoc */ override removeAll(): void { - this.checkInitialized(); - super.removeAll(); + this.set([]); } override contains(item: TT | Reference, check = true): boolean { diff --git a/packages/core/src/unit-of-work/ChangeSetComputer.ts b/packages/core/src/unit-of-work/ChangeSetComputer.ts index cfe4ccfdcb79..75d43dd6611e 100644 --- a/packages/core/src/unit-of-work/ChangeSetComputer.ts +++ b/packages/core/src/unit-of-work/ChangeSetComputer.ts @@ -155,7 +155,7 @@ export class ChangeSetComputer { } private processToMany(prop: EntityProperty, changeSet: ChangeSet): void { - const target = changeSet.entity[prop.name] as unknown as Collection; + const target = changeSet.entity[prop.name] as Collection; if (!target.isDirty()) { return; @@ -169,8 +169,8 @@ export class ChangeSetComputer { } } else if (prop.kind === ReferenceKind.ONE_TO_MANY && target.getSnapshot() === undefined) { this.collectionUpdates.add(target); - } else { - target.setDirty(false); // inverse side with only populated items, nothing to persist + } else if (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner) { + this.collectionUpdates.add(target); } } diff --git a/packages/core/src/unit-of-work/UnitOfWork.ts b/packages/core/src/unit-of-work/UnitOfWork.ts index 4ab1dd9d3022..4b5b957fc430 100644 --- a/packages/core/src/unit-of-work/UnitOfWork.ts +++ b/packages/core/src/unit-of-work/UnitOfWork.ts @@ -880,8 +880,9 @@ export class UnitOfWork { await this.commitExtraUpdates(ctx); // 6. collection updates + await this.em.getDriver().syncCollections(this.collectionUpdates, { ctx }); + for (const coll of this.collectionUpdates) { - await this.em.getDriver().syncCollection(coll, { ctx }); coll.takeSnapshot(); } diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index bf74ecc3a1cf..7e4780ebff9e 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -399,16 +399,27 @@ export class Utils { /** * Normalize the argument to always be an array. */ - static asArray(data?: T | readonly T[], strict = false): T[] { + static asArray(data?: T | readonly T[] | Iterable, strict = false): T[] { if (typeof data === 'undefined' && !strict) { return []; } - if (data instanceof Set) { + if (this.isIterable(data)) { return Array.from(data); } - return Array.isArray(data!) ? data : [data as T]; + return [data as T]; + } + + /** + * Checks if the value is iterable, but considers strings and buffers as not iterable. + */ + static isIterable(value: unknown): value is Iterable { + if (value == null || typeof value === 'string' || ArrayBuffer.isView(value)) { + return false; + } + + return typeof Object(value)[Symbol.iterator] === 'function'; } /** diff --git a/packages/knex/src/AbstractSqlDriver.ts b/packages/knex/src/AbstractSqlDriver.ts index 0ee0ef845401..4126aaacdf42 100644 --- a/packages/knex/src/AbstractSqlDriver.ts +++ b/packages/knex/src/AbstractSqlDriver.ts @@ -20,11 +20,11 @@ import { type EntityName, type EntityProperty, type EntityValue, + type FilterKey, type FilterQuery, type FindByCursorOptions, type FindOneOptions, type FindOptions, - type FilterKey, getOnConflictFields, getOnConflictReturningFields, helper, @@ -34,28 +34,28 @@ import { type LoggingOptions, type NativeInsertUpdateManyOptions, type NativeInsertUpdateOptions, + type OrderDefinition, type PopulateOptions, type Primary, QueryFlag, QueryHelper, + QueryOrder, type QueryOrderMap, type QueryResult, raw, - sql, ReferenceKind, type RequiredEntityData, type Transaction, type UpsertManyOptions, type UpsertOptions, Utils, - type OrderDefinition, - QueryOrder, } from '@mikro-orm/core'; import type { AbstractSqlConnection } from './AbstractSqlConnection'; import type { AbstractSqlPlatform } from './AbstractSqlPlatform'; import { QueryBuilder, QueryType } from './query'; import { SqlEntityManager } from './SqlEntityManager'; import type { Field } from './typings'; +import { PivotCollectionPersister } from './PivotCollectionPersister'; export abstract class AbstractSqlDriver extends DatabaseDriver { @@ -649,66 +649,76 @@ export abstract class AbstractSqlDriver(coll: Collection, options?: DriverMethodOptions): Promise { - const wrapped = helper(coll.owner); - const meta = wrapped.__meta; - const pks = wrapped.getPrimaryKeys(true)!; - const snap = coll.getSnapshot(); - const includes = (arr: T[], item: T) => !!arr.find(i => Utils.equals(i, item)); - const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)!) : []; - const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true)!); - const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true; - const insertDiff = current.filter(item => !includes(snapshot, item)); - const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff); - const equals = Utils.equals(current, target); - const ctx = options?.ctx; - - // wrong order if we just delete and insert to the end (only owning sides can have fixed order) - if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) { - (deleteDiff as unknown[]).length = insertDiff.length = 0; - deleteDiff.push(...snapshot); - insertDiff.push(...current); - } + override async syncCollections(collections: Iterable>, options?: DriverMethodOptions): Promise { + const groups = {} as Dictionary>; + + for (const coll of collections) { + const wrapped = helper(coll.owner); + const meta = wrapped.__meta; + const pks = wrapped.getPrimaryKeys(true)!; + const snap = coll.getSnapshot(); + const includes = (arr: T[], item: T) => !!arr.find(i => Utils.equals(i, item)); + const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)!) : []; + const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true)!); + const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true; + const insertDiff = current.filter(item => !includes(snapshot, item)); + const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff); + const equals = Utils.equals(current, target); + + // wrong order if we just delete and insert to the end (only owning sides can have fixed order) + if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) { + deleteDiff.length = insertDiff.length = 0; + deleteDiff.push(...snapshot); + insertDiff.push(...current); + } - if (coll.property.kind === ReferenceKind.ONE_TO_MANY) { - const cols = coll.property.referencedColumnNames; - const qb = this.createQueryBuilder(coll.property.type, ctx, 'write') - .withSchema(this.getSchemaName(meta, options)); + if (coll.property.kind === ReferenceKind.ONE_TO_MANY) { + const cols = coll.property.referencedColumnNames; + const qb = this.createQueryBuilder(coll.property.type, options?.ctx, 'write') + .withSchema(this.getSchemaName(meta, options)); + + if (coll.getSnapshot() === undefined) { + if (coll.property.orphanRemoval) { + const kqb = qb.delete({ [coll.property.mappedBy]: pks }) + .getKnexQuery() + .whereNotIn(cols, insertDiff as string[][]); - if (coll.getSnapshot() === undefined) { - if (coll.property.orphanRemoval) { - const kqb = qb.delete({ [coll.property.mappedBy]: pks }) + return this.rethrow(this.execute(kqb)); + } + + const kqb = qb.update({ [coll.property.mappedBy]: null }) .getKnexQuery() .whereNotIn(cols, insertDiff as string[][]); return this.rethrow(this.execute(kqb)); } - const kqb = qb.update({ [coll.property.mappedBy]: null }) + const kqb = qb.update({ [coll.property.mappedBy]: pks }) .getKnexQuery() - .whereNotIn(cols, insertDiff as string[][]); + .whereIn(cols, insertDiff as string[][]); return this.rethrow(this.execute(kqb)); } - const kqb = qb.update({ [coll.property.mappedBy]: pks }) - .getKnexQuery() - .whereIn(cols, insertDiff as string[][]); + /* istanbul ignore next */ + const pivotMeta = this.metadata.find(coll.property.pivotEntity)!; + let schema = pivotMeta.schema; + + if (schema === '*') { + const ownerSchema = wrapped.getSchema() === '*' ? this.config.get('schema') : wrapped.getSchema(); + schema = coll.property.owner ? ownerSchema : this.config.get('schema'); + } else if (schema == null) { + schema = this.config.get('schema'); + } - return this.rethrow(this.execute(kqb)); + const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`; + const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema); + persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks); } - /* istanbul ignore next */ - const ownerSchema = wrapped.getSchema() === '*' ? this.config.get('schema') : wrapped.getSchema(); - const pivotMeta = this.metadata.find(coll.property.pivotEntity)!; - - if (pivotMeta.schema === '*') { - /* istanbul ignore next */ - options ??= {}; - options.schema = ownerSchema; + for (const persister of Utils.values(groups)) { + await this.rethrow(persister.execute()); } - - return this.rethrow(this.updateCollectionDiff(meta, coll.property, pks, deleteDiff, insertDiff, options)); } override async loadFromPivotTable(prop: EntityProperty, owners: Primary[][], where: FilterQuery = {} as FilterQuery, orderBy?: OrderDefinition, ctx?: Transaction, options?: FindOptions, pivotJoin?: boolean): Promise> { @@ -1022,66 +1032,14 @@ export abstract class AbstractSqlDriver[][], options)); + const pivotMeta = this.metadata.find(prop.pivotEntity)!; + const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema); + persister.enqueueUpdate(prop, collections[prop.name] as Primary[][], clear, pks); + await this.rethrow(persister.execute()); } } } - protected async updateCollectionDiff( - meta: EntityMetadata, - prop: EntityProperty, - pks: Primary[], - deleteDiff: Primary[][] | boolean, - insertDiff: Primary[][], - options?: DriverMethodOptions & { ownerSchema?: string }, - ): Promise { - if (!deleteDiff) { - deleteDiff = []; - } - - const pivotMeta = this.metadata.find(prop.pivotEntity)!; - - if (deleteDiff === true || deleteDiff.length > 0) { - const qb1 = this.createQueryBuilder(prop.pivotEntity, options?.ctx, 'write').withSchema(this.getSchemaName(pivotMeta, options)); - const knex = qb1.getKnex(); - - if (Array.isArray(deleteDiff)) { - knex.whereIn(prop.inverseJoinColumns, deleteDiff as Knex.Value[][]); - } - - prop.joinColumns.forEach((joinColumn, idx) => knex.andWhere(joinColumn, pks[idx] as Knex.Value[][])); - await this.execute(knex.delete()); - } - - if (insertDiff.length === 0) { - return; - } - - const items = insertDiff.map(item => { - const cond = {} as Dictionary>; - prop.joinColumns.forEach((joinColumn, idx) => cond[joinColumn] = pks[idx]); - prop.inverseJoinColumns.forEach((inverseJoinColumn, idx) => cond[inverseJoinColumn] = item[idx]); - - return cond; - }); - - /* istanbul ignore else */ - if (this.platform.allowsMultiInsert()) { - await this.nativeInsertMany(prop.pivotEntity, items as EntityData[], { - ...options, - convertCustomTypes: false, - processCollections: false, - }); - } else { - await Utils.runSerial(items, item => { - return this.createQueryBuilder(prop.pivotEntity, options?.ctx, 'write') - .withSchema(this.getSchemaName(pivotMeta, options)) - .insert(item) - .execute('run', false); - }); - } - } - override async lockPessimistic(entity: T, options: LockOptions): Promise { const meta = helper(entity).__meta; const qb = this.createQueryBuilder((entity as object).constructor.name, options.ctx).withSchema(options.schema ?? meta.schema); diff --git a/packages/knex/src/PivotCollectionPersister.ts b/packages/knex/src/PivotCollectionPersister.ts new file mode 100644 index 000000000000..ec14aba1a825 --- /dev/null +++ b/packages/knex/src/PivotCollectionPersister.ts @@ -0,0 +1,163 @@ +import { + type Dictionary, + type EntityData, + type EntityKey, + type EntityMetadata, + type EntityProperty, + type FilterQuery, + type Primary, + type Transaction, + Utils, +} from '@mikro-orm/core'; +import { type AbstractSqlDriver } from './AbstractSqlDriver'; +import { type AbstractSqlPlatform } from './AbstractSqlPlatform'; + +class InsertStatement { + + constructor( + private readonly keys: string[], + private readonly data: EntityData, + readonly order: number, + ) {} + + getHash(): string { + return JSON.stringify(this.data); + } + + getData(): EntityData { + const data = {} as Dictionary; + this.keys.forEach((key, idx) => data[key] = (this.data as Dictionary)[idx]); + return data as EntityData; + } + +} + +class DeleteStatement { + + constructor( + private readonly keys: EntityKey[], + private readonly cond: FilterQuery, + ) {} + + getHash(): string { + return JSON.stringify(this.cond); + } + + getCondition(): FilterQuery { + const cond = {} as Dictionary; + this.keys.forEach((key, idx) => cond[key] = (this.cond as Dictionary)[idx]); + return cond as FilterQuery; + } + +} + +export class PivotCollectionPersister { + + private readonly platform: AbstractSqlPlatform; + private readonly inserts = new Map>(); + private readonly deletes = new Map>(); + private order = 0; + + constructor( + private readonly meta: EntityMetadata, + private readonly driver: AbstractSqlDriver, + private readonly ctx?: Transaction, + private readonly schema?: string, + ) { + this.platform = this.driver.getPlatform(); + } + + enqueueUpdate( + prop: EntityProperty, + insertDiff: Primary[][], + deleteDiff: Primary[][] | boolean, + pks: Primary[], + ) { + if (insertDiff.length) { + this.enqueueInsert(prop, insertDiff, pks); + } + + if (deleteDiff === true || (Array.isArray(deleteDiff) && deleteDiff.length)) { + this.enqueueDelete(prop, deleteDiff, pks); + } + } + + private enqueueInsert(prop: EntityProperty, insertDiff: Primary[][], pks: Primary[]) { + for (const fks of insertDiff) { + const data = prop.owner ? [...fks, ...pks] : [...pks, ...fks]; + const keys = prop.owner + ? [...prop.inverseJoinColumns, ...prop.joinColumns] + : [...prop.joinColumns, ...prop.inverseJoinColumns]; + + const statement = new InsertStatement(keys, data, this.order++); + const hash = statement.getHash(); + + if (prop.owner || !this.inserts.has(hash)) { + this.inserts.set(hash, statement); + } + } + } + + private enqueueDelete(prop: EntityProperty, deleteDiff: Primary[][] | true, pks: Primary[]) { + if (deleteDiff === true) { + const statement = new DeleteStatement(prop.joinColumns as EntityKey[], pks as FilterQuery); + this.deletes.set(statement.getHash(), statement); + + return; + } + + for (const fks of deleteDiff) { + const data = prop.owner ? [...fks, ...pks] : [...pks, ...fks]; + const keys = prop.owner + ? [...prop.inverseJoinColumns, ...prop.joinColumns] + : [...prop.joinColumns, ...prop.inverseJoinColumns]; + + const statement = new DeleteStatement(keys as EntityKey[], data as FilterQuery); + this.deletes.set(statement.getHash(), statement); + } + } + + async execute(): Promise { + if (this.deletes.size > 0) { + const knex = this.driver.createQueryBuilder(this.meta.className, this.ctx, 'write') + .withSchema(this.schema) + .getKnex(); + + for (const item of this.deletes.values()) { + knex.orWhere(item.getCondition()); + } + + await this.driver.execute(knex.delete()); + } + + if (this.inserts.size === 0) { + return; + } + + let items: EntityData[] = []; + + for (const insert of this.inserts.values()) { + items[insert.order] = insert.getData(); + } + + items = items.filter(i => i); + + /* istanbul ignore else */ + if (this.platform.allowsMultiInsert()) { + await this.driver.nativeInsertMany(this.meta.className, items as EntityData[], { + ctx: this.ctx, + schema: this.schema, + convertCustomTypes: false, + processCollections: false, + }); + } else { + await Utils.runSerial(items, item => { + return this.driver.createQueryBuilder(this.meta.className, this.ctx, 'write') + .withSchema(this.schema) + .insert(item) + .execute('run', false); + }); + } + } + +} diff --git a/packages/knex/src/query/QueryBuilder.ts b/packages/knex/src/query/QueryBuilder.ts index 5bed7dae8ee8..e15f7784f6e8 100644 --- a/packages/knex/src/query/QueryBuilder.ts +++ b/packages/knex/src/query/QueryBuilder.ts @@ -351,7 +351,7 @@ export class QueryBuilder { orderBy(orderBy: QBQueryOrderMap | QBQueryOrderMap[]): this { this.ensureNotFinalized(); this._orderBy = []; - Utils.asArray(orderBy).forEach(o => { + Utils.asArray>(orderBy).forEach(o => { const processed = QueryHelper.processWhere({ where: o as Dictionary, entityName: this.mainAlias.entityName, diff --git a/tests/EntityManager.mongo.test.ts b/tests/EntityManager.mongo.test.ts index 35180afebae6..4fdf2f7106e1 100644 --- a/tests/EntityManager.mongo.test.ts +++ b/tests/EntityManager.mongo.test.ts @@ -787,9 +787,6 @@ describe('EntityManagerMongo', () => { expect(mock.mock.calls[3][0]).toMatch(`db.getCollection('books-table').insertMany(`); orm.em.clear(); - // just to raise coverage, that method is no longer used internally - await orm.em.getDriver().syncCollection(book1.tags); - // test inverse side const tagRepository = orm.em.getRepository(BookTag); { @@ -811,7 +808,6 @@ describe('EntityManagerMongo', () => { expect(tags[0].books.isDirty()).toBe(false); expect(() => tags[0].books.getItems()).toThrowError(/Collection of entity BookTag\[\w{24}] not initialized/); expect(() => tags[0].books.remove(book1, book2)).toThrowError(/Collection of entity BookTag\[\w{24}] not initialized/); - expect(() => tags[0].books.removeAll()).toThrowError(/Collection of entity BookTag\[\w{24}] not initialized/); expect(() => tags[0].books.contains(book1)).toThrowError(/Collection of entity BookTag\[\w{24}] not initialized/); // test M:N lazy load @@ -1285,7 +1281,7 @@ describe('EntityManagerMongo', () => { const repo = orm.em.getRepository(BookTag); orm.em.clear(); - const tags = await repo.findAll({ populate: ['books.publisher.tests', 'books.author'] }); + const tags = await repo.findAll({ populate: ['books.publisher.tests', 'books.author'], logging: { enabled: true } }); expect(tags.length).toBe(5); expect(tags[0]).toBeInstanceOf(BookTag); expect(tags[0].books.isInitialized()).toBe(true); @@ -2092,6 +2088,30 @@ describe('EntityManagerMongo', () => { expect(tag.books.count()).toBe(4); }); + test('many to many working with inverse side persistence', async () => { + const author = new Author('Jon Snow', 'snow@wall.st'); + const book1 = new Book('My Life on The Wall, part 1', author); + const book2 = new Book('My Life on The Wall, part 2', author); + const book3 = new Book('My Life on The Wall, part 3', author); + const book4 = new Book('Another Book', author); + const tag1 = new BookTag('silly'); + const tag2 = new BookTag('funny'); + const tag3 = new BookTag('sick'); + const tag4 = new BookTag('strange'); + const tag5 = new BookTag('sexy'); + book1.tags.add(tag1, tag3); + book2.tags.add(tag1, tag2, tag5); + book3.tags.add(tag2, tag4, tag5); + + orm.em.persist([book1, book2, book3, book4]); + await orm.em.flush(); + orm.em.clear(); + + const tag = await orm.em.findOneOrFail(BookTag, tag1.id); + tag.books.removeAll(); + await expect(orm.em.flush()).rejects.toThrow('You cannot modify inverse side of M:N collection BookTag.books when the owning side is not initialized. Consider working with the owning side instead (Book.tags)'); + }); + test('transactions with embedded transaction', async () => { try { await orm.em.transactional(async em => { diff --git a/tests/EntityManager.mysql.test.ts b/tests/EntityManager.mysql.test.ts index d4a8903589b0..e7df8a5cec26 100644 --- a/tests/EntityManager.mysql.test.ts +++ b/tests/EntityManager.mysql.test.ts @@ -1283,6 +1283,34 @@ describe('EntityManagerMySql', () => { expect(tag.books.count()).toBe(4); }); + test('many to many working with inverse side persistence', async () => { + const author = new Author2('Jon Snow', 'snow@wall.st'); + const book1 = new Book2('My Life on The Wall, part 1', author); + const book2 = new Book2('My Life on The Wall, part 2', author); + const book3 = new Book2('My Life on The Wall, part 3', author); + const book4 = new Book2('Another Book', author); + const tag1 = new BookTag2('silly'); + const tag2 = new BookTag2('funny'); + const tag3 = new BookTag2('sick'); + const tag4 = new BookTag2('strange'); + const tag5 = new BookTag2('sexy'); + book1.tags.add(tag1, tag3); + book2.tags.add(tag1, tag2, tag5); + book3.tags.add(tag2, tag4, tag5); + + orm.em.persist([book1, book2, book3, book4]); + await orm.em.flush(); + orm.em.clear(); + + let tag = await orm.em.findOneOrFail(BookTag2, tag1.id); + tag.books.removeAll(); + await orm.em.flush(); + orm.em.clear(); + + tag = await orm.em.findOneOrFail(BookTag2, tag1.id, { populate: ['books'] }); + expect(tag.books.count()).toBe(0); + }); + test('populating many to many relation', async () => { const p1 = new Publisher2('foo'); expect(p1.tests).toBeInstanceOf(Collection); diff --git a/tests/EntityManager.postgre.test.ts b/tests/EntityManager.postgre.test.ts index 0e568af977e1..bde358caf80f 100644 --- a/tests/EntityManager.postgre.test.ts +++ b/tests/EntityManager.postgre.test.ts @@ -155,9 +155,9 @@ describe('EntityManagerPostgre', () => { ]); expect(mock.mock.calls[0][0]).toMatch('insert into "publisher2" ("name", "type", "type2") values ($1, $2, $3), ($4, $5, $6), ($7, $8, $9) returning "id"'); - expect(mock.mock.calls[1][0]).toMatch('insert into "publisher2_tests" ("publisher2_id", "test2_id") values ($1, $2), ($3, $4), ($5, $6)'); - expect(mock.mock.calls[2][0]).toMatch('insert into "publisher2_tests" ("publisher2_id", "test2_id") values ($1, $2), ($3, $4)'); - expect(mock.mock.calls[3][0]).toMatch('insert into "publisher2_tests" ("publisher2_id", "test2_id") values ($1, $2), ($3, $4), ($5, $6)'); + expect(mock.mock.calls[1][0]).toMatch('insert into "publisher2_tests" ("test2_id", "publisher2_id") values ($1, $2), ($3, $4), ($5, $6)'); + expect(mock.mock.calls[2][0]).toMatch('insert into "publisher2_tests" ("test2_id", "publisher2_id") values ($1, $2), ($3, $4)'); + expect(mock.mock.calls[3][0]).toMatch('insert into "publisher2_tests" ("test2_id", "publisher2_id") values ($1, $2), ($3, $4), ($5, $6)'); // postgres returns all the ids based on returning clause expect(res).toMatchObject({ insertId: 1, affectedRows: 3, row: { id: 1 }, rows: [ { id: 1 }, { id: 2 }, { id: 3 } ] }); @@ -1126,7 +1126,6 @@ describe('EntityManagerPostgre', () => { expect(tags[0].books.isDirty()).toBe(false); expect(() => tags[0].books.getItems()).toThrowError(/Collection of entity BookTag2\[\d+] not initialized/); expect(() => tags[0].books.remove(book1, book2)).toThrowError(/Collection of entity BookTag2\[\d+] not initialized/); - expect(() => tags[0].books.removeAll()).toThrowError(/Collection of entity BookTag2\[\d+] not initialized/); expect(() => tags[0].books.contains(book1)).toThrowError(/Collection of entity BookTag2\[\d+] not initialized/); // test M:N lazy load diff --git a/tests/EntityManager.sqlite.test.ts b/tests/EntityManager.sqlite.test.ts index 7eb888a6860b..83aa824dcc81 100644 --- a/tests/EntityManager.sqlite.test.ts +++ b/tests/EntityManager.sqlite.test.ts @@ -625,7 +625,6 @@ describe('EntityManagerSqlite', () => { expect(tags[0].books.isDirty()).toBe(false); expect(() => tags[0].books.getItems()).toThrowError(/Collection of entity BookTag3\[\d+] not initialized/); expect(() => tags[0].books.remove(book1, book2)).toThrowError(/Collection of entity BookTag3\[\d+] not initialized/); - expect(() => tags[0].books.removeAll()).toThrowError(/Collection of entity BookTag3\[\d+] not initialized/); expect(() => tags[0].books.contains(book1)).toThrowError(/Collection of entity BookTag3\[\d+] not initialized/); // test M:N lazy load diff --git a/tests/EntityManager.sqlite2.test.ts b/tests/EntityManager.sqlite2.test.ts index 5f0eac21a9a1..1e4b0a891aff 100644 --- a/tests/EntityManager.sqlite2.test.ts +++ b/tests/EntityManager.sqlite2.test.ts @@ -636,7 +636,6 @@ describe.each(['sqlite', 'better-sqlite'] as const)('EntityManager (%s)', driver expect(tags[0].books.isDirty()).toBe(false); expect(() => tags[0].books.getItems()).toThrowError(/Collection of entity BookTag4\[\d+] not initialized/); expect(() => tags[0].books.remove(book1, book2)).toThrowError(/Collection of entity BookTag4\[\d+] not initialized/); - expect(() => tags[0].books.removeAll()).toThrowError(/Collection of entity BookTag4\[\d+] not initialized/); expect(() => tags[0].books.contains(book1)).toThrowError(/Collection of entity BookTag4\[\d+] not initialized/); // test M:N lazy load diff --git a/tests/features/composite-keys/composite-keys.mysql.test.ts b/tests/features/composite-keys/composite-keys.mysql.test.ts index ca16cda14a95..3079cfef6eaa 100644 --- a/tests/features/composite-keys/composite-keys.mysql.test.ts +++ b/tests/features/composite-keys/composite-keys.mysql.test.ts @@ -303,7 +303,7 @@ describe('composite keys in mysql', () => { expect(mock.mock.calls[0][0]).toMatch('begin'); expect(mock.mock.calls[1][0]).toMatch('insert into `car2` (`name`, `year`, `price`) values (?, ?, ?), (?, ?, ?)'); // c1, c2 expect(mock.mock.calls[2][0]).toMatch('insert into `user2` (`first_name`, `last_name`) values (?, ?)'); // u1 - expect(mock.mock.calls[3][0]).toMatch('insert into `user2_cars` (`user2_first_name`, `user2_last_name`, `car2_name`, `car2_year`) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)'); + expect(mock.mock.calls[3][0]).toMatch('insert into `user2_cars` (`car2_name`, `car2_year`, `user2_first_name`, `user2_last_name`) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)'); expect(mock.mock.calls[4][0]).toMatch('commit'); }); diff --git a/tests/features/composite-keys/composite-keys.sqlite.test.ts b/tests/features/composite-keys/composite-keys.sqlite.test.ts index e53f91e9b3b1..6518f34c165a 100644 --- a/tests/features/composite-keys/composite-keys.sqlite.test.ts +++ b/tests/features/composite-keys/composite-keys.sqlite.test.ts @@ -493,7 +493,7 @@ describe('composite keys in sqlite', () => { await orm.em.flush(); expect(mock.mock.calls[0][0]).toMatch('begin'); - expect(mock.mock.calls[1][0]).toMatch('delete from `user2_sandwiches` where (`sandwich_id`) in ( values (2), (3)) and `user2_first_name` = \'Henry\' and `user2_last_name` = \'Doe 2\''); + expect(mock.mock.calls[1][0]).toMatch('delete from `user2_sandwiches` where (`sandwich_id` = 2 and `user2_first_name` = \'Henry\' and `user2_last_name` = \'Doe 2\') or (`sandwich_id` = 3 and `user2_first_name` = \'Henry\' and `user2_last_name` = \'Doe 2\')'); expect(mock.mock.calls[2][0]).toMatch('commit'); }); @@ -573,7 +573,7 @@ describe('composite keys in sqlite', () => { expect(mock.mock.calls[0][0]).toMatch('begin'); expect(mock.mock.calls[1][0]).toMatch('insert into `car2` (`name`, `year`, `price`) values (?, ?, ?), (?, ?, ?)'); // c1, c2 expect(mock.mock.calls[2][0]).toMatch('insert into `user2` (`first_name`, `last_name`) values (?, ?)'); // u1 - expect(mock.mock.calls[3][0]).toMatch('insert into `user2_cars` (`user2_first_name`, `user2_last_name`, `car2_name`, `car2_year`) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)'); + expect(mock.mock.calls[3][0]).toMatch('insert into `user2_cars` (`car2_name`, `car2_year`, `user2_first_name`, `user2_last_name`) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)'); expect(mock.mock.calls[4][0]).toMatch('commit'); }); diff --git a/tests/features/composite-keys/custom-pivot-entity-auto-discovery.sqlite.test.ts b/tests/features/composite-keys/custom-pivot-entity-auto-discovery.sqlite.test.ts index a0c966fc57f3..4d3d496d318c 100644 --- a/tests/features/composite-keys/custom-pivot-entity-auto-discovery.sqlite.test.ts +++ b/tests/features/composite-keys/custom-pivot-entity-auto-discovery.sqlite.test.ts @@ -152,7 +152,6 @@ describe('custom pivot entity for m:n with additional properties (auto-discovere expect(products[0].orders.isDirty()).toBe(false); expect(() => products[0].orders.getItems()).toThrowError(/Collection of entity Product\[\d+] not initialized/); expect(() => products[0].orders.remove(order1, order2)).toThrowError(/Collection of entity Product\[\d+] not initialized/); - expect(() => products[0].orders.removeAll()).toThrowError(/Collection of entity Product\[\d+] not initialized/); expect(() => products[0].orders.contains(order1)).toThrowError(/Collection of entity Product\[\d+] not initialized/); // test M:N lazy load diff --git a/tests/features/composite-keys/custom-pivot-entity-wrong-order.sqlite.test.ts b/tests/features/composite-keys/custom-pivot-entity-wrong-order.sqlite.test.ts index 459f4f174201..2f01aec449fb 100644 --- a/tests/features/composite-keys/custom-pivot-entity-wrong-order.sqlite.test.ts +++ b/tests/features/composite-keys/custom-pivot-entity-wrong-order.sqlite.test.ts @@ -159,7 +159,6 @@ describe('custom pivot entity for m:n with additional properties (bidirectional, expect(products[0].orders.isDirty()).toBe(false); expect(() => products[0].orders.getItems()).toThrowError(/Collection of entity Product\[\d+] not initialized/); expect(() => products[0].orders.remove(order1, order2)).toThrowError(/Collection of entity Product\[\d+] not initialized/); - expect(() => products[0].orders.removeAll()).toThrowError(/Collection of entity Product\[\d+] not initialized/); expect(() => products[0].orders.contains(order1)).toThrowError(/Collection of entity Product\[\d+] not initialized/); // test M:N lazy load diff --git a/tests/features/composite-keys/custom-pivot-entity.sqlite.test.ts b/tests/features/composite-keys/custom-pivot-entity.sqlite.test.ts index dd3f3b5a082c..a054d833ef1d 100644 --- a/tests/features/composite-keys/custom-pivot-entity.sqlite.test.ts +++ b/tests/features/composite-keys/custom-pivot-entity.sqlite.test.ts @@ -13,6 +13,18 @@ import { } from '@mikro-orm/core'; import { SqliteDriver } from '@mikro-orm/sqlite'; +function property(target: T, propertyName: keyof T) { + // decorator implementation can be even empty, it's enough to extract the metadata +} + +class User { + + @property + name?: string; + +} + + @Entity() export class Order { @@ -154,7 +166,6 @@ describe('custom pivot entity for m:n with additional properties (bidirectional) expect(products[0].orders.isDirty()).toBe(false); expect(() => products[0].orders.getItems()).toThrowError(/Collection of entity Product\[\d+] not initialized/); expect(() => products[0].orders.remove(order1, order2)).toThrowError(/Collection of entity Product\[\d+] not initialized/); - expect(() => products[0].orders.removeAll()).toThrowError(/Collection of entity Product\[\d+] not initialized/); expect(() => products[0].orders.contains(order1)).toThrowError(/Collection of entity Product\[\d+] not initialized/); // test M:N lazy load diff --git a/tests/features/entity-assigner/GH1811.test.ts b/tests/features/entity-assigner/GH1811.test.ts index 2304545b4921..236d13d84ca1 100644 --- a/tests/features/entity-assigner/GH1811.test.ts +++ b/tests/features/entity-assigner/GH1811.test.ts @@ -188,8 +188,8 @@ describe('GH issue 1811', () => { expect(mock.mock.calls[0][0]).toMatch('begin'); expect(mock.mock.calls[1][0]).toMatch('insert into `user` (`id`, `name`) values (?, ?)'); expect(mock.mock.calls[2][0]).toMatch('update `user` set `name` = ? where `id` = ?'); - expect(mock.mock.calls[3][0]).toMatch('delete from `recipe_authors` where (`user_id`) in ( values (?)) and `recipe_id` = ?'); - expect(mock.mock.calls[4][0]).toMatch('insert into `recipe_authors` (`recipe_id`, `user_id`) values (?, ?)'); + expect(mock.mock.calls[3][0]).toMatch('delete from `recipe_authors` where (`user_id` = ? and `recipe_id` = ?)'); + expect(mock.mock.calls[4][0]).toMatch('insert into `recipe_authors` (`user_id`, `recipe_id`) values (?, ?)'); expect(mock.mock.calls[5][0]).toMatch('commit'); const r1 = await orm.em.fork().findOneOrFail(Recipe, recipe, { populate: ['authors'], orderBy: { authors: { name: 'asc' } } }); diff --git a/tests/features/filters/filters.postgres.test.ts b/tests/features/filters/filters.postgres.test.ts index f2491a44c999..2892adffbf5b 100644 --- a/tests/features/filters/filters.postgres.test.ts +++ b/tests/features/filters/filters.postgres.test.ts @@ -155,7 +155,7 @@ describe('filters [postgres]', () => { expect(mock.mock.calls[1][0]).toMatch(`insert into "employee" ("id") values (default) returning "id"`); expect(mock.mock.calls[2][0]).toMatch(`insert into "benefit" ("benefit_status", "name") values ($1, $2), ($3, $4) returning "id"`); expect(mock.mock.calls[3][0]).toMatch(`insert into "benefit_detail" ("description", "benefit_id", "active") values ($1, $2, $3), ($4, $5, $6), ($7, $8, $9), ($10, $11, $12), ($13, $14, $15), ($16, $17, $18) returning "id"`); - expect(mock.mock.calls[4][0]).toMatch(`insert into "employee_benefits" ("employee_id", "benefit_id") values ($1, $2)`); + expect(mock.mock.calls[4][0]).toMatch(`insert into "employee_benefits" ("benefit_id", "employee_id") values ($1, $2), ($3, $4)`); expect(mock.mock.calls[5][0]).toMatch(`commit`); orm.em.clear(); mock.mockReset(); diff --git a/tests/features/multiple-schemas-entity-manager/multiple-schemas-entity-manager.postgres.test.ts b/tests/features/multiple-schemas-entity-manager/multiple-schemas-entity-manager.postgres.test.ts index 5ad539993927..4d9196943f03 100644 --- a/tests/features/multiple-schemas-entity-manager/multiple-schemas-entity-manager.postgres.test.ts +++ b/tests/features/multiple-schemas-entity-manager/multiple-schemas-entity-manager.postgres.test.ts @@ -468,11 +468,10 @@ describe('multiple connected schemas in postgres', () => { expect(mock.mock.calls[7][0]).toMatch(`insert into "n4"."book" ("author_id") values (1) returning "id"`); expect(mock.mock.calls[8][0]).toMatch(`update "n5"."book" set "based_on_id" = 1 where "id" = 1`); expect(mock.mock.calls[9][0]).toMatch(`update "n4"."book" set "based_on_id" = 1 where "id" = 1`); - expect(mock.mock.calls[10][0]).toMatch(`insert into "n3"."book_tags" ("book_id", "book_tag_id") values (1, 1), (1, 2), (1, 3)`); - expect(mock.mock.calls[11][0]).toMatch(`insert into "n5"."book_tags" ("book_id", "book_tag_id") values (1, 1), (1, 2), (1, 3)`); - expect(mock.mock.calls[12][0]).toMatch(`insert into "n5"."book_tags" ("book_id", "book_tag_id") values (2, 4), (2, 5), (2, 6)`); - expect(mock.mock.calls[13][0]).toMatch(`insert into "n4"."book_tags" ("book_id", "book_tag_id") values (1, 1), (1, 2), (1, 3)`); - expect(mock.mock.calls[14][0]).toMatch(`commit`); + expect(mock.mock.calls[10][0]).toMatch(`insert into "n3"."book_tags" ("book_tag_id", "book_id") values (1, 1), (2, 1), (3, 1)`); + expect(mock.mock.calls[11][0]).toMatch(`insert into "n5"."book_tags" ("book_tag_id", "book_id") values (1, 1), (2, 1), (3, 1), (4, 2), (5, 2), (6, 2)`); + expect(mock.mock.calls[12][0]).toMatch(`insert into "n4"."book_tags" ("book_tag_id", "book_id") values (1, 1), (2, 1), (3, 1)`); + expect(mock.mock.calls[13][0]).toMatch(`commit`); mock.mockReset(); // schema is saved after flush as if the entity was loaded from db diff --git a/tests/features/multiple-schemas/GH3177.test.ts b/tests/features/multiple-schemas/GH3177.test.ts index e66fa2a279fd..fe773f73dee5 100644 --- a/tests/features/multiple-schemas/GH3177.test.ts +++ b/tests/features/multiple-schemas/GH3177.test.ts @@ -91,7 +91,7 @@ test(`GH issue 3177`, async () => { expect(mock.mock.calls[1][0]).toMatch(`insert into "tenant_01"."user_access_profile" ("id") values (default) returning "id"`); expect(mock.mock.calls[2][0]).toMatch(`insert into "tenant_01"."user" ("id", "access_profile_id") values (1, 1)`); expect(mock.mock.calls[3][0]).toMatch(`insert into "public"."permission" ("id") values (default), (default), (default) returning "id"`); - expect(mock.mock.calls[4][0]).toMatch(`insert into "tenant_01"."access_profile_permission" ("access_profile_id", "permission_id") values (1, 1), (1, 2), (1, 3)`); + expect(mock.mock.calls[4][0]).toMatch(`insert into "tenant_01"."access_profile_permission" ("permission_id", "access_profile_id") values (1, 1), (2, 1), (3, 1)`); expect(mock.mock.calls[5][0]).toMatch(`commit`); expect(mock.mock.calls[6][0]).toMatch(`select "u0".* from "tenant_01"."user" as "u0" where "u0"."id" = 1 limit 1`); expect(mock.mock.calls[7][0]).toMatch(`select "p1".*, "a0"."permission_id" as "fk__permission_id", "a0"."access_profile_id" as "fk__access_profile_id" from "tenant_01"."access_profile_permission" as "a0" inner join "public"."permission" as "p1" on "a0"."permission_id" = "p1"."id" where "a0"."access_profile_id" in (1)`); diff --git a/tests/features/multiple-schemas/multiple-schemas.postgres.test.ts b/tests/features/multiple-schemas/multiple-schemas.postgres.test.ts index a191ca5d3672..cd60a41138c3 100644 --- a/tests/features/multiple-schemas/multiple-schemas.postgres.test.ts +++ b/tests/features/multiple-schemas/multiple-schemas.postgres.test.ts @@ -252,11 +252,10 @@ describe('multiple connected schemas in postgres', () => { expect(mock.mock.calls[7][0]).toMatch(`insert into "n4"."book" ("author_id") values (1) returning "id"`); expect(mock.mock.calls[8][0]).toMatch(`update "n5"."book" set "based_on_id" = 1 where "id" = 1`); expect(mock.mock.calls[9][0]).toMatch(`update "n4"."book" set "based_on_id" = 1 where "id" = 1`); - expect(mock.mock.calls[10][0]).toMatch(`insert into "n3"."book_tags" ("book_id", "book_tag_id") values (1, 1), (1, 2), (1, 3)`); - expect(mock.mock.calls[11][0]).toMatch(`insert into "n5"."book_tags" ("book_id", "book_tag_id") values (1, 1), (1, 2), (1, 3)`); - expect(mock.mock.calls[12][0]).toMatch(`insert into "n5"."book_tags" ("book_id", "book_tag_id") values (2, 4), (2, 5), (2, 6)`); - expect(mock.mock.calls[13][0]).toMatch(`insert into "n4"."book_tags" ("book_id", "book_tag_id") values (1, 1), (1, 2), (1, 3)`); - expect(mock.mock.calls[14][0]).toMatch(`commit`); + expect(mock.mock.calls[10][0]).toMatch(`insert into "n3"."book_tags" ("book_tag_id", "book_id") values (1, 1), (2, 1), (3, 1)`); + expect(mock.mock.calls[11][0]).toMatch(`insert into "n5"."book_tags" ("book_tag_id", "book_id") values (1, 1), (2, 1), (3, 1), (4, 2), (5, 2), (6, 2)`); + expect(mock.mock.calls[12][0]).toMatch(`insert into "n4"."book_tags" ("book_tag_id", "book_id") values (1, 1), (2, 1), (3, 1)`); + expect(mock.mock.calls[13][0]).toMatch(`commit`); mock.mockReset(); // schema is saved after flush as if the entity was loaded from db diff --git a/tests/features/result-cache/result-cache.postgre.test.ts b/tests/features/result-cache/result-cache.postgre.test.ts index a084ebd716a0..10fbcf445781 100644 --- a/tests/features/result-cache/result-cache.postgre.test.ts +++ b/tests/features/result-cache/result-cache.postgre.test.ts @@ -41,22 +41,22 @@ describe('result cache (postgres)', () => { const mock = mockLogger(orm, ['query']); - const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 50, strategy: LoadStrategy.JOINED }); + const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 100, strategy: LoadStrategy.JOINED }); expect(mock.mock.calls).toHaveLength(1); orm.em.clear(); - const res2 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 50, strategy: LoadStrategy.JOINED }); + const res2 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 100, strategy: LoadStrategy.JOINED }); expect(mock.mock.calls).toHaveLength(1); // cache hit, no new query fired expect(res1.map(e => wrap(e).toObject())).toEqual(res2.map(e => wrap(e).toObject())); orm.em.clear(); - const res3 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 50, strategy: LoadStrategy.JOINED }); + const res3 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 100, strategy: LoadStrategy.JOINED }); expect(mock.mock.calls).toHaveLength(1); // cache hit, no new query fired expect(res1.map(e => wrap(e).toObject())).toEqual(res3.map(e => wrap(e).toObject())); orm.em.clear(); - await new Promise(r => setTimeout(r, 100)); // wait for cache to expire - const res4 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 50, strategy: LoadStrategy.JOINED }); + await new Promise(r => setTimeout(r, 200)); // wait for cache to expire + const res4 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], cache: 100, strategy: LoadStrategy.JOINED }); expect(mock.mock.calls).toHaveLength(2); // cache miss, new query fired expect(res1.map(e => wrap(e).toObject())).toEqual(res4.map(e => wrap(e).toObject())); }); @@ -65,7 +65,7 @@ describe('result cache (postgres)', () => { await createBooksWithTags(); const mock = mockLogger(orm, ['query']); - orm.config.get('resultCache').global = 50; + orm.config.get('resultCache').global = 100; const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], strategy: LoadStrategy.JOINED }); expect(mock.mock.calls).toHaveLength(1); @@ -81,7 +81,7 @@ describe('result cache (postgres)', () => { expect(res1.map(e => wrap(e).toObject())).toEqual(res3.map(e => wrap(e).toObject())); orm.em.clear(); - await new Promise(r => setTimeout(r, 100)); // wait for cache to expire + await new Promise(r => setTimeout(r, 200)); // wait for cache to expire const res4 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: ['author', 'tags', 'publisher'], strategy: LoadStrategy.JOINED }); expect(mock.mock.calls).toHaveLength(2); // cache miss, new query fired expect(res1.map(e => wrap(e).toObject())).toEqual(res4.map(e => wrap(e).toObject())); @@ -97,7 +97,7 @@ describe('result cache (postgres)', () => { author: { name: 'Jon Snow' }, }, { populate: ['author', 'tags'], - cache: ['abc', 50], + cache: ['abc', 100], strategy: LoadStrategy.JOINED, }); @@ -115,7 +115,7 @@ describe('result cache (postgres)', () => { expect(wrap(res1).toObject()).toEqual(wrap(res3).toObject()); orm.em.clear(); - await new Promise(r => setTimeout(r, 100)); // wait for cache to expire + await new Promise(r => setTimeout(r, 200)); // wait for cache to expire const res4 = await call(); expect(mock.mock.calls).toHaveLength(2); // cache miss, new query fired expect(wrap(res1).toObject()).toEqual(wrap(res4).toObject()); @@ -137,22 +137,22 @@ describe('result cache (postgres)', () => { const mock = mockLogger(orm, ['query']); - const res1 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 50 }); + const res1 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 100 }); expect(mock.mock.calls).toHaveLength(1); orm.em.clear(); - const res2 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 50 }); + const res2 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 100 }); expect(mock.mock.calls).toHaveLength(1); // cache hit, no new query fired expect(res1).toEqual(res2); orm.em.clear(); - const res3 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 50 }); + const res3 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 100 }); expect(mock.mock.calls).toHaveLength(1); // cache hit, no new query fired expect(res1).toEqual(res3); orm.em.clear(); - await new Promise(r => setTimeout(r, 100)); // wait for cache to expire - const res4 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 50 }); + await new Promise(r => setTimeout(r, 200)); // wait for cache to expire + const res4 = await orm.em.count(Book2, { author: { name: 'Jon Snow' } }, { cache: 100 }); expect(mock.mock.calls).toHaveLength(2); // cache miss, new query fired expect(res1).toEqual(res4); }); @@ -162,21 +162,21 @@ describe('result cache (postgres)', () => { const mock = mockLogger(orm, ['query']); - const res1 = await orm.em.createQueryBuilder(Book2).where({ author: { name: 'Jon Snow' } }).cache(50).getResultList(); + const res1 = await orm.em.createQueryBuilder(Book2).where({ author: { name: 'Jon Snow' } }).cache(100).getResultList(); expect(mock.mock.calls).toHaveLength(1); orm.em.clear(); - const res2 = await orm.em.createQueryBuilder(Book2).where({ author: { name: 'Jon Snow' } }).cache(50).getResultList(); + const res2 = await orm.em.createQueryBuilder(Book2).where({ author: { name: 'Jon Snow' } }).cache(100).getResultList(); expect(mock.mock.calls).toHaveLength(1); // cache hit, no new query fired expect(res1).toEqual(res2); orm.em.clear(); - const res3 = await orm.em.createQueryBuilder(Book2).where({ author: { name: 'Jon Snow' } }).cache(50).getResultList(); + const res3 = await orm.em.createQueryBuilder(Book2).where({ author: { name: 'Jon Snow' } }).cache(100).getResultList(); expect(mock.mock.calls).toHaveLength(1); // cache hit, no new query fired expect(res1).toEqual(res3); orm.em.clear(); - await new Promise(r => setTimeout(r, 100)); // wait for cache to expire + await new Promise(r => setTimeout(r, 200)); // wait for cache to expire const res4 = await orm.em.createQueryBuilder(Book2).where({ author: { name: 'Jon Snow' } }).cache().getResultList(); expect(mock.mock.calls).toHaveLength(2); // cache miss, new query fired expect(res1).toEqual(res4); diff --git a/tests/features/sharing-column-in-composite-pk-fk.test.ts b/tests/features/sharing-column-in-composite-pk-fk.test.ts index 5d07d8728e2d..4a0ec155cf77 100644 --- a/tests/features/sharing-column-in-composite-pk-fk.test.ts +++ b/tests/features/sharing-column-in-composite-pk-fk.test.ts @@ -215,7 +215,7 @@ test('shared column as composite PK and FK in M:N', async () => { [`[query] insert into "organization" ("id", "name") values ('a900a4da-c464-4bd4-88a3-e41e1d33dc2e', 'Tenant 1')`], [`[query] insert into "order" ("id", "organization_id", "number") values ('d09f1159-c5b0-4336-bfed-2543b5422ba7', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e', 123)`], [`[query] insert into "product" ("id", "organization_id") values ('bb9efb3e-7c23-421c-9ae2-9d989630159a', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e')`], - [`[query] insert into "order_item" ("order_id", "organization_id", "product_id") values ('d09f1159-c5b0-4336-bfed-2543b5422ba7', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e', 'bb9efb3e-7c23-421c-9ae2-9d989630159a') returning "amount"`], + [`[query] insert into "order_item" ("product_id", "organization_id", "order_id") values ('bb9efb3e-7c23-421c-9ae2-9d989630159a', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e', 'd09f1159-c5b0-4336-bfed-2543b5422ba7') returning "amount"`], ['[query] commit'], ]); @@ -238,7 +238,7 @@ test('shared column as composite PK and FK in M:N', async () => { ['[query] begin'], [`[query] insert into "product" ("id", "organization_id") values ('ffffffff-7c23-421c-9ae2-9d989630159a', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e')`], [`[query] update "order" set "number" = 321 where "id" = 'd09f1159-c5b0-4336-bfed-2543b5422ba7' and "organization_id" = 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e'`], - [`[query] insert into "order_item" ("order_id", "organization_id", "product_id") values ('d09f1159-c5b0-4336-bfed-2543b5422ba7', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e', 'ffffffff-7c23-421c-9ae2-9d989630159a') returning "amount"`], + [`[query] insert into "order_item" ("product_id", "organization_id", "order_id") values ('ffffffff-7c23-421c-9ae2-9d989630159a', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e', 'd09f1159-c5b0-4336-bfed-2543b5422ba7') returning "amount"`], ['[query] commit'], ]); }); diff --git a/tests/issues/GH1041.test.ts b/tests/issues/GH1041.test.ts index c24d3ff26b64..7fbc6882a47d 100644 --- a/tests/issues/GH1041.test.ts +++ b/tests/issues/GH1041.test.ts @@ -66,7 +66,7 @@ describe('GH issue 1041, 1043', () => { await expect(orm.em.flush()).resolves.toBeUndefined(); expect(log.mock.calls[0][0]).toMatch('begin'); - expect(log.mock.calls[1][0]).toMatch('delete from `user_apps` where (`app_id`) in ( values (2)) and `user_id` = 123'); + expect(log.mock.calls[1][0]).toMatch('delete from `user_apps` where (`app_id` = 2 and `user_id` = 123)'); expect(log.mock.calls[2][0]).toMatch('commit'); }); @@ -79,7 +79,7 @@ describe('GH issue 1041, 1043', () => { await expect(orm.em.flush()).resolves.toBeUndefined(); expect(log.mock.calls[0][0]).toMatch('begin'); - expect(log.mock.calls[1][0]).toMatch('delete from `user_apps` where (`app_id`) in ( values (3)) and `user_id` = 123'); + expect(log.mock.calls[1][0]).toMatch('delete from `user_apps` where (`app_id` = 3 and `user_id` = 123)'); expect(log.mock.calls[2][0]).toMatch('commit'); }); diff --git a/tests/issues/GH4027.test.ts b/tests/issues/GH4027.test.ts index 4d477ac8ae8a..72a92346ba40 100644 --- a/tests/issues/GH4027.test.ts +++ b/tests/issues/GH4027.test.ts @@ -70,7 +70,7 @@ test('GH 4027', async () => { ['[query] commit'], ['[query] begin'], ["[query] insert into `parent` (`id`, `created_at`) values ('9a061473-4a98-477d-ad03-fd7bcba3ec4f', 1676050010441)"], - ["[query] insert into `parent_refs` (`parent_id`, `child_id`) values ('9a061473-4a98-477d-ad03-fd7bcba3ec4f', 'e80ccf60-5cb2-4972-9227-7a4b9138c845')"], + ["[query] insert into `parent_refs` (`child_id`, `parent_id`) values ('e80ccf60-5cb2-4972-9227-7a4b9138c845', '9a061473-4a98-477d-ad03-fd7bcba3ec4f')"], ['[query] commit'], ]); });