From b99e7bb4d6dd1c0657ea0ddf28fb0c3a1986e5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Tue, 1 Nov 2022 16:19:00 +0100 Subject: [PATCH] fix(core): fix querying for a complex composite key via inverse side Closes #3669 --- packages/core/src/metadata/EntitySchema.ts | 1 + packages/knex/src/query/CriteriaNode.ts | 2 +- tests/issues/GH3669.test.ts | 102 +++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/issues/GH3669.test.ts diff --git a/packages/core/src/metadata/EntitySchema.ts b/packages/core/src/metadata/EntitySchema.ts index 8827e93adb3e..c659adf922ae 100644 --- a/packages/core/src/metadata/EntitySchema.ts +++ b/packages/core/src/metadata/EntitySchema.ts @@ -137,6 +137,7 @@ export class EntitySchema { addManyToOne(name: string & keyof T, type: TypeType, options: ManyToOneOptions): void { const prop = this.createProperty(ReferenceType.MANY_TO_ONE, options); + prop.owner = true; if (prop.joinColumns && !prop.fieldNames) { prop.fieldNames = prop.joinColumns; diff --git a/packages/knex/src/query/CriteriaNode.ts b/packages/knex/src/query/CriteriaNode.ts index c0ede2eb2fd8..6c4ae19da735 100644 --- a/packages/knex/src/query/CriteriaNode.ts +++ b/packages/knex/src/query/CriteriaNode.ts @@ -79,7 +79,7 @@ export class CriteriaNode implements ICriteriaNode { return Utils.getPrimaryKeyHash(this.prop!.inverseJoinColumns.map(col => `${alias}.${col}`)); } - if (this.prop!.joinColumns.length > 1) { + if (this.prop!.owner && this.prop!.joinColumns.length > 1) { return Utils.getPrimaryKeyHash(this.prop!.joinColumns.map(col => `${alias}.${col}`)); } diff --git a/tests/issues/GH3669.test.ts b/tests/issues/GH3669.test.ts new file mode 100644 index 000000000000..2d51be9437f9 --- /dev/null +++ b/tests/issues/GH3669.test.ts @@ -0,0 +1,102 @@ +import { Entity, ManyToOne, OneToOne, PrimaryKey, PrimaryKeyType, Property, Rel, SimpleLogger } from '@mikro-orm/core'; +import { MikroORM } from '@mikro-orm/postgresql'; +import { mockLogger } from '../helpers'; + +@Entity() +class Vendor { + + @PrimaryKey() + id!: number; + + @Property() + prop!: string; + +} + +@Entity() +class TechnicianManager { + + [PrimaryKeyType]?: [number, number]; + + @ManyToOne({ entity: () => Vendor, primary: true }) + vendor!: Vendor; + + @OneToOne({ entity: () => User, primary: true }) + user!: Rel; + +} + +@Entity() +class User { + + @PrimaryKey() + id!: number; + + @OneToOne(() => TechnicianManager, tm => tm.user) + technicianManager!: TechnicianManager; + +} + +describe('GH issue 3669', () => { + + let orm: MikroORM; + let loggerMock: jest.Mock; + + beforeAll(async () => { + orm = await MikroORM.init({ + entities: [TechnicianManager], + dbName: 'mikro_orm_test_3669', + loggerFactory: options => new SimpleLogger(options), + }); + await orm.schema.refreshDatabase(); + orm.em.create(User, { + id: 1, + technicianManager: { + user: 1, + vendor: { id: 2, prop: 'p' }, + }, + }); + await orm.em.flush(); + loggerMock = mockLogger(orm); + }); + + beforeEach(() => loggerMock.mockReset()); + + afterAll(() => orm.close(true)); + + test('$not operator on 1:1 inverse side', async () => { + await expect(orm.em.find(User, { $not: { technicianManager: null } })).resolves.toHaveLength(1); + expect(loggerMock.mock.calls).toEqual([[ + '[query] select "u0".*, "t1"."vendor_id" as "technician_manager_vendor_id", "t1"."user_id" as "technician_manager_user_id" from "user" as "u0" left join "technician_manager" as "t1" on "u0"."id" = "t1"."user_id" where not (("t1"."vendor_id", "t1"."user_id") is null)', + ]]); + }); + + test('$ne operator on 1:1 inverse side', async () => { + await expect(orm.em.find(User, { technicianManager: { $ne: null } })).resolves.toHaveLength(1); + expect(loggerMock.mock.calls).toEqual([[ + '[query] select "u0".*, "t1"."vendor_id" as "technician_manager_vendor_id", "t1"."user_id" as "technician_manager_user_id" from "user" as "u0" left join "technician_manager" as "t1" on "u0"."id" = "t1"."user_id" where ("t1"."vendor_id", "t1"."user_id") is not null', + ]]); + }); + + test('$eq operator on 1:1 inverse side', async () => { + await expect(orm.em.find(User, { technicianManager: { $eq: null } })).resolves.toEqual([]); + expect(loggerMock.mock.calls).toEqual([[ + '[query] select "u0".*, "t1"."vendor_id" as "technician_manager_vendor_id", "t1"."user_id" as "technician_manager_user_id" from "user" as "u0" left join "technician_manager" as "t1" on "u0"."id" = "t1"."user_id" where ("t1"."vendor_id", "t1"."user_id") is null', + ]]); + }); + + test('compare null to 1:1 inverse side', async () => { + await expect(orm.em.find(User, { technicianManager: null })).resolves.toEqual([]); + expect(loggerMock.mock.calls).toEqual([[ + '[query] select "u0".*, "t1"."vendor_id" as "technician_manager_vendor_id", "t1"."user_id" as "technician_manager_user_id" from "user" as "u0" left join "technician_manager" as "t1" on "u0"."id" = "t1"."user_id" where ("t1"."vendor_id", "t1"."user_id") is null', + ]]); + }); + + test('compare array PK to 1:1 inverse side', async () => { + await expect(orm.em.find(User, { technicianManager: [2, 1] })).resolves.toHaveLength(1); + expect(loggerMock.mock.calls).toEqual([[ + '[query] select "u0".*, "t1"."vendor_id" as "technician_manager_vendor_id", "t1"."user_id" as "technician_manager_user_id" from "user" as "u0" left join "technician_manager" as "t1" on "u0"."id" = "t1"."user_id" where ("t1"."vendor_id", "t1"."user_id") = (2, 1)', + ]]); + }); + +});