From 0435faf712783c89ed8b9de456c8da1c4f551c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Fri, 1 Mar 2024 17:08:08 +0100 Subject: [PATCH] fix(core): fix aliasing of queries with collection operators Closes #5301 --- packages/knex/src/query/ObjectCriteriaNode.ts | 6 +- packages/knex/src/query/QueryBuilder.ts | 2 +- tests/features/collection/GH5301.test.ts | 94 +++++++++++++++++++ .../collection/collection-operators.test.ts | 24 ++--- 4 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 tests/features/collection/GH5301.test.ts diff --git a/packages/knex/src/query/ObjectCriteriaNode.ts b/packages/knex/src/query/ObjectCriteriaNode.ts index 6d046486e027..af06deab1877 100644 --- a/packages/knex/src/query/ObjectCriteriaNode.ts +++ b/packages/knex/src/query/ObjectCriteriaNode.ts @@ -40,10 +40,10 @@ export class ObjectCriteriaNode extends CriteriaNode { } const payload = (this.payload[key] as CriteriaNode).unwrap(); - const sub = qb - .clone(true) + const qb2 = qb.clone(true); + const sub = qb2 .from(this.parent!.entityName) - .innerJoin(this.key!, qb.getNextAlias(this.prop!.type)) + .innerJoin(this.key!, qb2.getNextAlias(this.prop!.type)) .select(this.prop!.targetMeta!.primaryKeys); if (key === '$every') { diff --git a/packages/knex/src/query/QueryBuilder.ts b/packages/knex/src/query/QueryBuilder.ts index 07badd63fbb7..5394ed0fac92 100644 --- a/packages/knex/src/query/QueryBuilder.ts +++ b/packages/knex/src/query/QueryBuilder.ts @@ -951,7 +951,7 @@ export class QueryBuilder { const properties = [ 'flags', '_populate', '_populateWhere', '__populateWhere', '_populateMap', '_joins', '_joinedProps', '_cond', '_data', '_orderBy', '_schema', '_indexHint', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning', - '_comments', '_hintComments', 'rawFragments', + '_comments', '_hintComments', 'rawFragments', 'aliasCounter', ]; RawQueryFragment.cloneRegistry = this.rawFragments; diff --git a/tests/features/collection/GH5301.test.ts b/tests/features/collection/GH5301.test.ts new file mode 100644 index 000000000000..f5afe2bd909b --- /dev/null +++ b/tests/features/collection/GH5301.test.ts @@ -0,0 +1,94 @@ +import { Collection, Entity, ManyToMany, MikroORM, PrimaryKey, Property } from '@mikro-orm/sqlite'; +import { mockLogger } from '../../helpers'; + +@Entity() +class UserLabel { + + @PrimaryKey() + id!: number; + + @Property() + name: string; + + @ManyToMany(() => User, user => user.labels) + users = new Collection(this); + + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } + +} + +@Entity() +class User { + + @PrimaryKey() + id!: number; + + @Property() + name: string; + + @ManyToMany(() => UserLabel) + labels = new Collection(this); + + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } + +} + +let orm: MikroORM; + +beforeAll(async () => { + orm = await MikroORM.init({ + dbName: ':memory:', + entities: [User, UserLabel], + }); + await orm.schema.createSchema(); +}); + +afterAll(async () => { + await orm.close(true); +}); + +test('basic CRUD example', async () => { + const user1 = orm.em.create(User, { + id: 1, + name: 'User 1', + }); + + const user2 = orm.em.create(User, { + id: 2, + name: 'User 2', + }); + + orm.em.create(User, { + id: 3, + name: 'User 3', + }); + + const label1 = orm.em.create(UserLabel, { + id: 1, + name: 'Label 1', + }); + + const label2 = orm.em.create(UserLabel, { + id: 2, + name: 'Label 2', + }); + + user1.labels.add(label1, label2); + user2.labels.add(label1); + + await orm.em.flush(); + orm.em.clear(); + + const mock = mockLogger(orm); + const count = await orm.em.count(User, { + labels: { $some: { id: 1 } }, + }); + expect(count).toBe(2); + expect(mock.mock.calls[0][0]).toMatch('select count(*) as `count` from `user` as `u0` where `u0`.`id` in (select `u0`.`id` from `user` as `u0` left join `user_labels` as `u2` on `u0`.`id` = `u2`.`user_id` inner join `user_label` as `u1` on `u2`.`user_label_id` = `u1`.`id` where `u2`.`user_label_id` = 1)'); +}); diff --git a/tests/features/collection/collection-operators.test.ts b/tests/features/collection/collection-operators.test.ts index 71c2f134f861..750ff86615c6 100644 --- a/tests/features/collection/collection-operators.test.ts +++ b/tests/features/collection/collection-operators.test.ts @@ -159,7 +159,7 @@ test('m:n sub-query operators $some, $none and $every (select-in)', async () => ['t2'], ['t2'], ]); - expect(mock.mock.calls[0][0]).toBe("[query] select `b0`.* from `book` as `b0` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b1` on `b1`.`book_tag_id` = `b1`.`id` where `b1`.`name` in ('t1', 't2'))"); + expect(mock.mock.calls[0][0]).toBe("[query] select `b0`.* from `book` as `b0` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b1`.`name` in ('t1', 't2'))"); expect(mock.mock.calls[1][0]).toBe('[query] select `b1`.*, `b0`.`book_tag_id` as `fk__book_tag_id`, `b0`.`book_id` as `fk__book_id` from `book_tags` as `b0` inner join `book_tag` as `b1` on `b0`.`book_tag_id` = `b1`.`id` where `b0`.`book_id` in (1, 2, 3, 4, 5, 6, 7, 8)'); results = await orm.em.fork().find(Book, { @@ -168,7 +168,7 @@ test('m:n sub-query operators $some, $none and $every (select-in)', async () => expect(results.map(res => res.tags.getIdentifiers('name'))).toEqual([ ['t4', 't5'], ]); - expect(mock.mock.calls[2][0]).toBe("[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b3` on `b0`.`id` = `b3`.`book_id` left join `book_tag` as `b2` on `b3`.`book_tag_id` = `b2`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b1` on `b1`.`book_tag_id` = `b1`.`id` where `b1`.`name` in ('t1', 't2')) order by `b2`.`name` asc"); + expect(mock.mock.calls[2][0]).toBe("[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b1`.`name` in ('t1', 't2')) order by `b1`.`name` asc"); expect(mock.mock.calls[3][0]).toBe('[query] select `b1`.*, `b0`.`book_tag_id` as `fk__book_tag_id`, `b0`.`book_id` as `fk__book_id` from `book_tags` as `b0` inner join `book_tag` as `b1` on `b0`.`book_tag_id` = `b1`.`id` where `b0`.`book_id` in (9) order by `b1`.`name` asc'); results = await orm.em.fork().find(Book, { @@ -181,7 +181,7 @@ test('m:n sub-query operators $some, $none and $every (select-in)', async () => ['t2'], ['t2'], ]); - expect(mock.mock.calls[4][0]).toBe("[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b3` on `b0`.`id` = `b3`.`book_id` left join `book_tag` as `b2` on `b3`.`book_tag_id` = `b2`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b1` on `b1`.`book_tag_id` = `b1`.`id` where not (`b1`.`name` in ('t1', 't2'))) order by `b2`.`name` asc"); + expect(mock.mock.calls[4][0]).toBe("[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where not (`b1`.`name` in ('t1', 't2'))) order by `b1`.`name` asc"); expect(mock.mock.calls[5][0]).toBe('[query] select `b1`.*, `b0`.`book_tag_id` as `fk__book_tag_id`, `b0`.`book_id` as `fk__book_id` from `book_tags` as `b0` inner join `book_tag` as `b1` on `b0`.`book_tag_id` = `b1`.`id` where `b0`.`book_id` in (3, 5, 6, 7, 8) order by `b1`.`name` asc'); results = await orm.em.fork().find(Book, { @@ -198,14 +198,14 @@ test('m:n sub-query operators $some, $none and $every (select-in)', async () => ['t2'], ['t4', 't5'], ]); - expect(mock.mock.calls[6][0]).toBe('[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b3` on `b0`.`id` = `b3`.`book_id` left join `book_tag` as `b2` on `b3`.`book_tag_id` = `b2`.`id` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b1` on `b1`.`book_tag_id` = `b1`.`id`) order by `b2`.`name` asc'); + expect(mock.mock.calls[6][0]).toBe('[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id`) order by `b1`.`name` asc'); expect(mock.mock.calls[7][0]).toBe('[query] select `b1`.*, `b0`.`book_tag_id` as `fk__book_tag_id`, `b0`.`book_id` as `fk__book_id` from `book_tags` as `b0` inner join `book_tag` as `b1` on `b0`.`book_tag_id` = `b1`.`id` where `b0`.`book_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9) order by `b1`.`name` asc'); results = await orm.em.fork().find(Book, { tags: { $none: {} }, }, { populate: ['tags'], orderBy: { tags: { name: 1 } } }); expect(results.map(res => res.tags.getIdentifiers('name'))).toEqual([]); - expect(mock.mock.calls[8][0]).toBe('[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b3` on `b0`.`id` = `b3`.`book_id` left join `book_tag` as `b2` on `b3`.`book_tag_id` = `b2`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b1` on `b1`.`book_tag_id` = `b1`.`id`) order by `b2`.`name` asc'); + expect(mock.mock.calls[8][0]).toBe('[query] select `b0`.* from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id`) order by `b1`.`name` asc'); }); test('m:n sub-query operators $some, $none and $every (joined)', async () => { @@ -225,7 +225,7 @@ test('m:n sub-query operators $some, $none and $every (joined)', async () => { ['t2'], ['t2'], ]); - expect(mock.mock.calls[0][0]).toBe("[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b3` on `b1`.`book_tag_id` = `b3`.`id` where `b3`.`name` in ('t1', 't2'))"); + expect(mock.mock.calls[0][0]).toBe("[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b1`.`name` in ('t1', 't2'))"); results = await orm.em.fork().find(Book, { tags: { $none: { name: ['t1', 't2'] } }, @@ -233,7 +233,7 @@ test('m:n sub-query operators $some, $none and $every (joined)', async () => { expect(results.map(res => res.tags.getIdentifiers('name'))).toEqual([ ['t4', 't5'], ]); - expect(mock.mock.calls[1][0]).toBe("[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b3` on `b1`.`book_tag_id` = `b3`.`id` where `b3`.`name` in ('t1', 't2')) order by `t1`.`name` asc"); + expect(mock.mock.calls[1][0]).toBe("[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where `b1`.`name` in ('t1', 't2')) order by `t1`.`name` asc"); results = await orm.em.fork().find(Book, { tags: { $every: { name: ['t1', 't2'] } }, @@ -245,7 +245,7 @@ test('m:n sub-query operators $some, $none and $every (joined)', async () => { ['t2'], ['t2'], ]); - expect(mock.mock.calls[2][0]).toBe("[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b3` on `b1`.`book_tag_id` = `b3`.`id` where not (`b3`.`name` in ('t1', 't2'))) order by `t1`.`name` asc"); + expect(mock.mock.calls[2][0]).toBe("[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id` where not (`b1`.`name` in ('t1', 't2'))) order by `t1`.`name` asc"); results = await orm.em.fork().find(Book, { tags: { $some: {} }, @@ -261,13 +261,13 @@ test('m:n sub-query operators $some, $none and $every (joined)', async () => { ['t2'], ['t4', 't5'], ]); - expect(mock.mock.calls[3][0]).toBe('[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b3` on `b1`.`book_tag_id` = `b3`.`id`) order by `t1`.`name` asc'); + expect(mock.mock.calls[3][0]).toBe('[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id`) order by `t1`.`name` asc'); results = await orm.em.fork().find(Book, { tags: { $none: {} }, }, { populate: ['tags'], orderBy: { tags: { name: 1 } } }); expect(results.map(res => res.tags.getIdentifiers('name'))).toEqual([]); - expect(mock.mock.calls[4][0]).toBe('[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b1` on `b0`.`id` = `b1`.`book_id` inner join `book_tag` as `b3` on `b1`.`book_tag_id` = `b3`.`id`) order by `t1`.`name` asc'); + expect(mock.mock.calls[4][0]).toBe('[query] select `b0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `book` as `b0` left join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` left join `book_tag` as `t1` on `b2`.`book_tag_id` = `t1`.`id` where `b0`.`id` not in (select `b0`.`id` from `book` as `b0` inner join `book_tags` as `b2` on `b0`.`id` = `b2`.`book_id` inner join `book_tag` as `b1` on `b2`.`book_tag_id` = `b1`.`id`) order by `t1`.`name` asc'); }); test('allows only one of $some, $none and $every on the given level', async () => { @@ -278,7 +278,7 @@ test('allows only one of $some, $none and $every on the given level', async () = $none: { title: 'Foo' }, }, }); - expect(mock.mock.calls[0][0]).toBe("[query] select `a0`.* from `author` as `a0` where `a0`.`id` in (select `a0`.`id` from `author` as `a0` inner join `book` as `b1` on `a0`.`id` = `b1`.`author_id` where `b1`.`title` = 'Foo') and `a0`.`id` not in (select `a0`.`id` from `author` as `a0` inner join `book` as `b2` on `a0`.`id` = `b2`.`author_id` where `b2`.`title` = 'Foo')"); + expect(mock.mock.calls[0][0]).toBe("[query] select `a0`.* from `author` as `a0` where `a0`.`id` in (select `a0`.`id` from `author` as `a0` inner join `book` as `b1` on `a0`.`id` = `b1`.`author_id` where `b1`.`title` = 'Foo') and `a0`.`id` not in (select `a0`.`id` from `author` as `a0` inner join `book` as `b1` on `a0`.`id` = `b1`.`author_id` where `b1`.`title` = 'Foo')"); expect(results).toHaveLength(0); results = await orm.em.fork().find(Author, { @@ -287,7 +287,7 @@ test('allows only one of $some, $none and $every on the given level', async () = $none: { title: 'Foo 123' }, }, }); - expect(mock.mock.calls[1][0]).toBe("[query] select `a0`.* from `author` as `a0` where `a0`.`id` in (select `a0`.`id` from `author` as `a0` inner join `book` as `b1` on `a0`.`id` = `b1`.`author_id` where `b1`.`title` = 'Foo') and `a0`.`id` not in (select `a0`.`id` from `author` as `a0` inner join `book` as `b2` on `a0`.`id` = `b2`.`author_id` where `b2`.`title` = 'Foo 123')"); + expect(mock.mock.calls[1][0]).toBe("[query] select `a0`.* from `author` as `a0` where `a0`.`id` in (select `a0`.`id` from `author` as `a0` inner join `book` as `b1` on `a0`.`id` = `b1`.`author_id` where `b1`.`title` = 'Foo') and `a0`.`id` not in (select `a0`.`id` from `author` as `a0` inner join `book` as `b1` on `a0`.`id` = `b1`.`author_id` where `b1`.`title` = 'Foo 123')"); expect(results.map(res => res.name)).toEqual([ 'Author 2', 'Author 3',