From 90ec7663d01ea0bd577b15051f7bfb02afc687e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Sat, 25 Nov 2023 22:19:25 +0100 Subject: [PATCH] feat(sql): use joined strategy as default for SQL drivers (#4958) This builds on the foundation of #4957, which resolved the biggest difference of the strategies. As part of the switch, few other smaller bugs were also fixed, as most of the test suite is now using the joined strategy too, so it uncovered some bugs that were fixed only with the select-in. To keep the old behaviour, you can override the default loading strategy in your ORM config. --- docs/docs/loading-strategies.md | 100 ++++++++++------- packages/core/src/EntityManager.ts | 2 +- packages/core/src/entity/ArrayCollection.ts | 2 +- packages/core/src/entity/EntityFactory.ts | 3 +- packages/core/src/hydration/ObjectHydrator.ts | 5 +- packages/core/src/utils/Configuration.ts | 2 +- packages/core/src/utils/Utils.ts | 22 ++++ packages/knex/src/AbstractSqlDriver.ts | 78 +++++++++----- packages/mongodb/src/MongoPlatform.ts | 1 + tests/EntityHelper.mysql.test.ts | 45 +++++++- tests/EntityManager.mysql.test.ts | 32 +++--- .../__snapshots__/dataloader.test.ts.snap | 15 +-- .../composite-keys.mysql.test.ts | 54 ++++++++++ .../__snapshots__/complex-cursor.test.ts.snap | 20 ++-- .../custom-order/custom-order.mysql.test.ts | 15 +++ .../custom-order.postgres.test.ts | 21 +++- .../custom-order/custom-order.sqlite.test.ts | 16 +++ tests/features/embeddables/GH4824.test.ts | 3 + tests/features/events/events.mysql.test.ts | 4 +- tests/features/filters/filters.mysql.test.ts | 102 +++++++++++++++++- .../features/filters/filters.postgres.test.ts | 4 +- .../lazy-scalar-properties.mysql.test.ts | 53 ++++++++- .../features/multiple-schemas/GH3177.test.ts | 7 +- .../partial-loading.mysql.test.ts | 34 +++--- .../sharing-column-in-composite-pk-fk.test.ts | 3 +- .../single-table-inheritance/GH4423.test.ts | 30 ++---- tests/issues/GH1657.test.ts | 2 +- tests/issues/GH1882.test.ts | 78 +++++++++++++- tests/issues/GH234.test.ts | 59 +++++++--- tests/issues/GH2647.test.ts | 48 ++++++++- tests/issues/GH2916.test.ts | 65 ++++++++++- tests/issues/GH4843.test.ts | 20 +++- tests/issues/GH572.test.ts | 3 +- 33 files changed, 742 insertions(+), 206 deletions(-) diff --git a/docs/docs/loading-strategies.md b/docs/docs/loading-strategies.md index 5fabca7cc29d..1ffb562a0e2d 100644 --- a/docs/docs/loading-strategies.md +++ b/docs/docs/loading-strategies.md @@ -3,66 +3,85 @@ title: Relationship Loading Strategies sidebar_label: Loading Strategies --- -> `JOINED` loading strategy is SQL only feature. +MikroORM supports two loading strategies: -Controls how relationships get loaded when querying. By default, populated relationships are loaded via the `select-in` strategy. This strategy issues one additional `SELECT` statement per relation being loaded. +- `select-in` which uses separate queries for each relation - it you populate two relations, you will get three queries - one for the root entity, and one for each populated relation. +- `joined` which uses a single query and joins the relations instead. -The loading strategy can be specified both at mapping time and when loading entities. +> `joined` strategy is supported **only in SQL drivers** and is the **default** for those since v6. -For example, given the following entities: +## Configuring the strategy + +The loading strategy can be specified both at mapping time and when loading entities, as well as globally in your ORM config. + +Given the following entities: ```ts -import { Entity, LoadStrategy, OneToMany, ManyToOne } from '@mikro-orm/core'; +import { Entity, LoadStrategy, OneToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; @Entity() export class Author { + + @PrimaryKey() + id!: number; + @OneToMany(() => Book, b => b.author) books = new Collection(this); + } @Entity() export class Book { + + @PrimaryKey() + id!: number; + @ManyToOne() author: Author; + } ``` -The following will issue two SQL statements. One to load the author and another to load all the books belonging to that author: +With the default `joined` strategy, this will issue a single query both the `Author` entity and its `books` relation. ```ts -const author = await orm.em.findOne(Author, 1, { populate: ['books'] }); +const author = await orm.em.findOne(Author, 1, { + populate: ['books'], +}); +``` + +To override the strategy, you can use the `strategy` option: + +```ts +const author = await orm.em.findOne(Author, 1, { + populate: ['books'], + strategy: 'select-in', +}); ``` -If we update the `Author.books` mapping to the following: +This will issue two SQL statements, one to load the author and another to load all the books belonging to that author: + +Alternatively, you can control the strategy in your entity definition. ```ts import { Entity, LoadStrategy, OneToMany } from '@mikro-orm/core'; @Entity() export class Author { + + // ... + @OneToMany({ entity: () => Book, mappedBy: b => b.author, - strategy: LoadStrategy.JOINED, + strategy: 'select-in', // force select-in strategy for this relation }) books = new Collection(this); -} -``` - -The following will issue **one** SQL statement: -```ts -const author = await orm.em.findOne(Author, 1, { populate: ['books'] }); +} ``` -You can also specify the load strategy as needed. This will override whatever strategy is declared in the mapping. This also works for nested populates: - -```ts -const author = await orm.em.findOne(Author, 1, { - populate: ['books.publisher'], - strategy: LoadStrategy.JOINED -}); -``` +The strategy defined on property level will always take a precedence. ## Changing the loading strategy globally @@ -70,38 +89,37 @@ You can use `loadStrategy` option in the ORM config: ```ts MikroORM.init({ - loadStrategy: LoadStrategy.JOINED, + // ... + populate: ['books'], + loadStrategy: 'select-in', // 'joined' is the default for SQL drivers }); ``` This value will be used as the default, specifying the loading strategy on property level has precedence, as well as specifying it in the `FindOptions`. -## Population where condition - -> This applies only to SELECT_IN strategy, as JOINED strategy implies the inference. +## Population `where` condition -In v4, when we used populate hints in `em.find()` and similar methods, the query for our entity would be analysed and parts of it extracted and used for the population. Following example would find all authors that have books with given IDs, and populate their books collection, again using this PK condition, resulting in only such books being in those collections. +The where condition is by default applied only to the root entity. This can be controlled via `populateWhere` option. It accepts one of `all` (default), `infer` (use same condition as the `where` query) or an explicit filter query. ```ts -// this would end up with `Author.books` collections having only books of PK 1, 2, 3 -const a = await em.find(Author, { books: [1, 2, 3] }, { populate: ['books'] }); +await em.find(Author, { ... }, { + populate: ['books'], + populateWhere: 'infer', // defaults to `all` + + // or specify custom query, will be used via `join on` conditions + // populateWhere: { age: { $gte: 18 } }, +}); ``` -Following this example, if we wanted to load all books, we would need a separate `em.populate()` call: +> `populateWhere` can be also set globally, the default is `all`. -```ts -const a = await em.find(Author, { books: [1, 2, 3] }); -await em.populate(a, ['books']); -``` +## Population `order by` clause -This behaviour changed and is now configurable both globally and locally, via `populateWhere` option. Globally we can specify one of `PopulateHint.ALL` and `PopulateHint.INFER`, the former being the default in v5, the latter being the default behaviour in v4. Locally (via `FindOptions`) we can also specify custom where condition that will be passed to `em.populate()` call. +Similarly to the `populateWhere`, you can also control the `order by` clause used for the populate queries. The default behaviour is to use the same ordering as for the root entity, and you can use `populateOrderBy` option to add a different ordering: ```ts await em.find(Author, { ... }, { - // defaults to PopulateHint.ALL in v5 - populateWhere: PopulateHint.INFER, // revert to v4 behaviour - - // or we can specify custom condition for the population: - // populateWhere: { ... }, + populate: ['books'], + populateOrderBy: { books: { publishedAt: 'desc' } }, }); ``` diff --git a/packages/core/src/EntityManager.ts b/packages/core/src/EntityManager.ts index cbea039a5652..7e1e943a64d1 100644 --- a/packages/core/src/EntityManager.ts +++ b/packages/core/src/EntityManager.ts @@ -404,7 +404,7 @@ export class EntityManager { for (const hint of (options.populate as unknown as PopulateOptions[])) { const field = hint.field.split(':')[0] as EntityKey; const prop = meta.properties[field]; - const joined = (options.strategy || hint.strategy || prop.strategy || this.config.get('loadStrategy')) === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR; + const joined = (prop.strategy || options.strategy || hint.strategy || this.config.get('loadStrategy')) === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR; if (!joined) { continue; diff --git a/packages/core/src/entity/ArrayCollection.ts b/packages/core/src/entity/ArrayCollection.ts index fa833ed4f28f..dad721bfada5 100644 --- a/packages/core/src/entity/ArrayCollection.ts +++ b/packages/core/src/entity/ArrayCollection.ts @@ -39,7 +39,7 @@ export class ArrayCollection { const meta = this.property.targetMeta!; const args = [...meta.toJsonParams.map(() => undefined)]; - return this.getItems().map(item => wrap(item as TT).toJSON(...args)); + return this.map(item => wrap(item as TT).toJSON(...args)); } toJSON(): EntityDTO[] { diff --git a/packages/core/src/entity/EntityFactory.ts b/packages/core/src/entity/EntityFactory.ts index 14f2382655dd..ea12ba884e77 100644 --- a/packages/core/src/entity/EntityFactory.ts +++ b/packages/core/src/entity/EntityFactory.ts @@ -210,7 +210,6 @@ export class EntityFactory { } const pks = Utils.getOrderedPrimaryKeys(id, meta); - const exists = this.unitOfWork.getById(entityName, pks as Primary, schema); if (exists) { @@ -305,7 +304,7 @@ export class EntityFactory { return this.unitOfWork.getById(meta.name!, data[meta.primaryKeys[0]] as Primary, schema); } - if (meta.primaryKeys.some(pk => data[pk] == null)) { + if (!Array.isArray(data) && meta.primaryKeys.some(pk => data[pk] == null)) { return undefined; } diff --git a/packages/core/src/hydration/ObjectHydrator.ts b/packages/core/src/hydration/ObjectHydrator.ts index 1a8ca6e8b423..3c4bbe0bde1e 100644 --- a/packages/core/src/hydration/ObjectHydrator.ts +++ b/packages/core/src/hydration/ObjectHydrator.ts @@ -137,6 +137,7 @@ export class ObjectHydrator extends Hydrator { const hydrateToOne = (prop: EntityProperty, dataKey: string, entityKey: string) => { const ret: string[] = []; + const method = type === 'reference' ? 'createReference' : 'create'; const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null'; ret.push(` if (data${dataKey} === null) {\n entity${entityKey} = ${nullVal};`); ret.push(` } else if (typeof data${dataKey} !== 'undefined') {`); @@ -151,9 +152,9 @@ export class ObjectHydrator extends Hydrator { ret.push(` } else if (data${dataKey} && typeof data${dataKey} === 'object') {`); if (prop.ref) { - ret.push(` entity${entityKey} = Reference.create(factory.create('${prop.type}', data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, schema }));`); + ret.push(` entity${entityKey} = Reference.create(factory.${method}('${prop.type}', data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, schema }));`); } else { - ret.push(` entity${entityKey} = factory.create('${prop.type}', data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, schema });`); + ret.push(` entity${entityKey} = factory.${method}('${prop.type}', data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, schema });`); } ret.push(` }`); diff --git a/packages/core/src/utils/Configuration.ts b/packages/core/src/utils/Configuration.ts index 3f9b1b4b2ffd..b743019089a3 100644 --- a/packages/core/src/utils/Configuration.ts +++ b/packages/core/src/utils/Configuration.ts @@ -70,7 +70,7 @@ export class Configuration { baseDir: process.cwd(), hydrator: ObjectHydrator, flushMode: FlushMode.AUTO, - loadStrategy: LoadStrategy.SELECT_IN, + loadStrategy: LoadStrategy.JOINED, dataloader: Dataloader.OFF, populateWhere: PopulateHint.ALL, connect: true, diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index 043e5274830b..beacf00e3697 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -632,6 +632,28 @@ export class Utils { return cond; } + /** + * Maps nested FKs from `[1, 2, 3]` to `[1, [2, 3]]`. + */ + static mapFlatCompositePrimaryKey(fk: Primary[], prop: EntityProperty, fieldNames = prop.fieldNames, idx = 0): Primary | Primary[] { + if (!prop.targetMeta) { + return fk[idx++]; + } + + const parts: Primary[] = []; + + for (const pk of prop.targetMeta.getPrimaryProps()) { + parts.push(this.mapFlatCompositePrimaryKey(fk, pk, fieldNames, idx)); + idx += pk.fieldNames.length; + } + + if (parts.length < 2) { + return parts[0]; + } + + return parts; + } + static getPrimaryKeyCondFromArray(pks: Primary[], meta: EntityMetadata): Record> { return meta.getPrimaryProps().reduce((o, pk, idx) => { if (Array.isArray(pks[idx]) && pk.targetMeta) { diff --git a/packages/knex/src/AbstractSqlDriver.ts b/packages/knex/src/AbstractSqlDriver.ts index 3d2a7af91049..59dc7b6b8df4 100644 --- a/packages/knex/src/AbstractSqlDriver.ts +++ b/packages/knex/src/AbstractSqlDriver.ts @@ -270,18 +270,18 @@ export abstract class AbstractSqlDriver(result: EntityData, meta: EntityMetadata, populate: PopulateOptions[], qb: QueryBuilder, root: EntityData, map: Dictionary, parentJoinPath?: string) { const joinedProps = this.joinedProps(meta, populate); - joinedProps.forEach(p => { - const [propName, ref] = p.field.split(':', 2) as [EntityKey, string | undefined]; - const relation = meta.properties[propName]; + joinedProps.forEach(hint => { + const [propName, ref] = hint.field.split(':', 2) as [EntityKey, string | undefined]; + const prop = meta.properties[propName]; /* istanbul ignore next */ - if (!relation) { + if (!prop) { return; } - const pivotRefJoin = relation.kind === ReferenceKind.MANY_TO_MANY && ref; - const meta2 = this.metadata.find(relation.type)!; - let path = parentJoinPath ? `${parentJoinPath}.${relation.name}` : `${meta.name}.${relation.name}`; + const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref; + const meta2 = this.metadata.find(prop.type)!; + let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`; if (!parentJoinPath) { path = '[populate]' + path; @@ -297,20 +297,20 @@ export abstract class AbstractSqlDriver 1) { // composite keys - item = relation.inverseJoinColumns.map(name => root![`${relationAlias}__${name}` as EntityKey]) as EntityValue; + if (prop.inverseJoinColumns.length > 1) { // composite keys + item = prop.inverseJoinColumns.map(name => root![`${relationAlias}__${name}` as EntityKey]) as EntityValue; } else { - const alias = `${relationAlias}__${relation.inverseJoinColumns[0]}` as EntityKey; + const alias = `${relationAlias}__${prop.inverseJoinColumns[0]}` as EntityKey; item = root![alias] as EntityValue; } - relation.joinColumns.forEach(name => delete root![`${relationAlias}__${name}` as EntityKey]); - relation.inverseJoinColumns.forEach(name => delete root![`${relationAlias}__${name}` as EntityKey]); + prop.joinColumns.forEach(name => delete root![`${relationAlias}__${name}` as EntityKey]); + prop.inverseJoinColumns.forEach(name => delete root![`${relationAlias}__${name}` as EntityKey]); - result[relation.name] ??= [] as EntityValue; + result[prop.name] ??= [] as EntityValue; if (item) { - (result[relation.name] as EntityData[]).push(item); + (result[prop.name] as EntityData[]).push(item); } return; @@ -323,12 +323,12 @@ export abstract class AbstractSqlDriver; + if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) { + result[prop.name] ??= [] as EntityValue; } - if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(relation.kind)) { - result[relation.name] ??= null; + if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) { + result[prop.name] ??= null; } return; @@ -338,7 +338,9 @@ export abstract class AbstractSqlDriver prop.persist === false && prop.fieldNames) + .filter(prop => !prop.lazy || populate.some(p => p.field === prop.name || p.all)) .forEach(prop => { + /* istanbul ignore if */ if (prop.fieldNames.length > 1) { // composite keys relationPojo[prop.name as EntityKey] = prop.fieldNames.map(name => root![`${relationAlias}__${name}` as EntityKey]) as EntityValue; } else { @@ -348,11 +350,12 @@ export abstract class AbstractSqlDriver this.platform.shouldHaveColumn(prop, p.children as any || [])) + .filter(prop => this.platform.shouldHaveColumn(prop, hint.children as any || [])) .forEach(prop => { if (prop.fieldNames.length > 1) { // composite keys - relationPojo[prop.name] = prop.fieldNames.map(name => root![`${relationAlias}__${name}` as EntityKey]) as EntityValue; + const fk = prop.fieldNames.map(name => root![`${relationAlias}__${name}` as EntityKey]) as Primary[]; prop.fieldNames.map(name => delete root![`${relationAlias}__${name}` as EntityKey]); + relationPojo[prop.name] = Utils.mapFlatCompositePrimaryKey(fk, prop) as EntityValue; } else if (prop.runtimeType === 'Date') { const alias = `${relationAlias}__${prop.fieldNames[0]}` as EntityKey; relationPojo[prop.name] = (typeof root![alias] === 'string' ? new Date(root![alias] as string) : root![alias]) as EntityValue; @@ -364,14 +367,14 @@ export abstract class AbstractSqlDriver; - (result[relation.name] as EntityData[]).push(relationPojo); + if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) { + result[prop.name] ??= [] as EntityValue; + (result[prop.name] as EntityData[]).push(relationPojo); } else { - result[relation.name] = relationPojo as EntityValue; + result[prop.name] = relationPojo as EntityValue; } - const populateChildren = p.children as any || []; + const populateChildren = hint.children as any || []; this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path); }); } @@ -959,7 +962,12 @@ export abstract class AbstractSqlDriver type.convertToJSValueSQL)) { + return prop.fieldNames.map((col, idx) => { + if (!prop.customTypes[idx].convertToJSValueSQL) { + return col; + } + + const prefixed = knex.ref(col).withSchema(tableAlias).toString(); + const aliased = knex.ref(`${tableAlias}__${col}`).toString(); + + return raw(`${prop.customTypes[idx].convertToJSValueSQL!(prefixed, this.platform)} as ${aliased}`); + }); + } + + if (tableAlias && prop.customType?.convertToJSValueSQL) { const prefixed = knex.ref(prop.fieldNames[0]).withSchema(tableAlias).toString(); return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)]; } @@ -1254,7 +1275,8 @@ export abstract class AbstractSqlDriver(prop.type)!; if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) { - orderBy.push({ [`${(join.ownerAlias)}.${prop.fixedOrderColumn}`]: QueryOrder.ASC } as QueryOrderMap); + const alias = ref ? propAlias : join.ownerAlias; + orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC } as QueryOrderMap); } if (propOrderBy) { diff --git a/packages/mongodb/src/MongoPlatform.ts b/packages/mongodb/src/MongoPlatform.ts index 9324f330b905..94e6905f23dc 100644 --- a/packages/mongodb/src/MongoPlatform.ts +++ b/packages/mongodb/src/MongoPlatform.ts @@ -13,6 +13,7 @@ export class MongoPlatform extends Platform { override setConfig(config: Configuration) { config.set('autoJoinOneToOneOwner', false); + config.set('loadStrategy', 'select-in'); config.get('discovery').inferDefaultValues = false; super.setConfig(config); } diff --git a/tests/EntityHelper.mysql.test.ts b/tests/EntityHelper.mysql.test.ts index 4d41e6169d98..468f34248062 100644 --- a/tests/EntityHelper.mysql.test.ts +++ b/tests/EntityHelper.mysql.test.ts @@ -24,14 +24,55 @@ describe('EntityHelperMySql', () => { expect(dto.id).toBeUndefined(); }); - test(`toObject handles recursion in 1:1`, async () => { + test(`toObject handles recursion in 1:1 (select-in)`, async () => { const bar = FooBar2.create('fb'); bar.baz = new FooBaz2('fz'); await orm.em.persistAndFlush(bar); orm.em.clear(); const repo = orm.em.getRepository(FooBar2); - const a = await repo.findOneOrFail(bar.id, { populate: ['baz.bar'] }); + const a = await repo.findOneOrFail(bar.id, { populate: ['baz.bar'], strategy: 'select-in' }); + expect(wrap(a.baz!).isInitialized()).toBe(true); + expect(wrap(a.baz!.bar!).isInitialized()).toBe(true); + expect(wrap(a).toJSON()).toEqual({ + baz: { + bar: { + id: 1, + name: 'fb', + nameWithSpace: null, + objectProperty: null, + random: 123, + version: a.version, + array: null, + blob: null, + blob2: null, + fooBar: null, + }, // circular reference breaks the cycle + id: 1, + name: 'fz', + version: a.baz!.version, + }, + fooBar: null, + id: 1, + name: 'fb', + nameWithSpace: null, + random: 123, + version: a.version, + array: null, + objectProperty: null, + blob: null, + blob2: null, + }); + }); + + test(`toObject handles recursion in 1:1 (joined)`, async () => { + const bar = FooBar2.create('fb'); + bar.baz = new FooBaz2('fz'); + await orm.em.persistAndFlush(bar); + orm.em.clear(); + + const repo = orm.em.getRepository(FooBar2); + const a = await repo.findOneOrFail(bar.id, { populate: ['baz.bar'], strategy: 'joined' }); expect(wrap(a.baz!).isInitialized()).toBe(true); expect(wrap(a.baz!.bar!).isInitialized()).toBe(true); expect(wrap(a).toJSON()).toEqual({ diff --git a/tests/EntityManager.mysql.test.ts b/tests/EntityManager.mysql.test.ts index 631fcdea2194..1f8583c50dd0 100644 --- a/tests/EntityManager.mysql.test.ts +++ b/tests/EntityManager.mysql.test.ts @@ -36,7 +36,7 @@ describe('EntityManagerMySql', () => { let orm: MikroORM; - beforeAll(async () => orm = await initORMMySql('mysql', { loadStrategy: 'joined' }, true)); + beforeAll(async () => orm = await initORMMySql('mysql', {}, true)); beforeEach(async () => orm.schema.clearDatabase()); afterEach(() => { orm.config.set('debug', false); @@ -1728,14 +1728,14 @@ describe('EntityManagerMySql', () => { orderBy: { title: QueryOrder.DESC }, strategy: 'joined', }); - expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name`, `t3`.`id` as `test_id` ' + + expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name`, `t5`.`id` as `test_id` ' + 'from `book2` as `b0` ' + 'left join `book_to_tag_unordered` as `b2` on `b0`.`uuid_pk` = `b2`.`book2_uuid_pk` ' + 'left join `book_tag2` as `t1` on `b2`.`book_tag2_id` = `t1`.`id` ' + - 'left join `test2` as `t3` on `b0`.`uuid_pk` = `t3`.`book_uuid_pk` ' + - 'left join `book_to_tag_unordered` as `b5` on `b0`.`uuid_pk` = `b5`.`book2_uuid_pk` ' + - 'left join `book_tag2` as `b4` on `b5`.`book_tag2_id` = `b4`.`id` ' + - 'where `b0`.`author_id` is not null and `b4`.`name` != ? ' + + 'left join `book_to_tag_unordered` as `b4` on `b0`.`uuid_pk` = `b4`.`book2_uuid_pk` ' + + 'left join `book_tag2` as `b3` on `b4`.`book_tag2_id` = `b3`.`id` ' + + 'left join `test2` as `t5` on `b0`.`uuid_pk` = `t5`.`book_uuid_pk` ' + + 'where `b0`.`author_id` is not null and `b3`.`name` != ? ' + 'order by `b0`.`title` desc, `t1`.`name` asc'); expect(books3.length).toBe(3); expect(books3[0].perex).toBeInstanceOf(ScalarReference); @@ -1755,14 +1755,14 @@ describe('EntityManagerMySql', () => { orderBy: { title: QueryOrder.DESC }, strategy: 'joined', }); - expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name`, `t3`.`id` as `test_id` ' + + expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name`, `t5`.`id` as `test_id` ' + 'from `book2` as `b0` ' + 'left join `book_to_tag_unordered` as `b2` on `b0`.`uuid_pk` = `b2`.`book2_uuid_pk` ' + 'left join `book_tag2` as `t1` on `b2`.`book_tag2_id` = `t1`.`id` and `t1`.`name` != ? ' + - 'left join `test2` as `t3` on `b0`.`uuid_pk` = `t3`.`book_uuid_pk` ' + - 'left join `book_to_tag_unordered` as `b5` on `b0`.`uuid_pk` = `b5`.`book2_uuid_pk` ' + - 'left join `book_tag2` as `b4` on `b5`.`book_tag2_id` = `b4`.`id` ' + - 'where `b0`.`author_id` is not null and `b4`.`name` != ? ' + + 'left join `book_to_tag_unordered` as `b4` on `b0`.`uuid_pk` = `b4`.`book2_uuid_pk` ' + + 'left join `book_tag2` as `b3` on `b4`.`book_tag2_id` = `b3`.`id` ' + + 'left join `test2` as `t5` on `b0`.`uuid_pk` = `t5`.`book_uuid_pk` ' + + 'where `b0`.`author_id` is not null and `b3`.`name` != ? ' + 'order by `b0`.`title` desc, `t1`.`name` asc'); expect(books4.length).toBe(3); @@ -1784,14 +1784,14 @@ describe('EntityManagerMySql', () => { strategy: 'joined', }); - expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name`, `t3`.`id` as `test_id` ' + + expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name`, `t5`.`id` as `test_id` ' + 'from `book2` as `b0` ' + 'left join `book_to_tag_unordered` as `b2` on `b0`.`uuid_pk` = `b2`.`book2_uuid_pk` ' + 'left join `book_tag2` as `t1` on `b2`.`book_tag2_id` = `t1`.`id` and `t1`.`name` != ? ' + - 'left join `test2` as `t3` on `b0`.`uuid_pk` = `t3`.`book_uuid_pk` ' + - 'left join `book_to_tag_unordered` as `b5` on `b0`.`uuid_pk` = `b5`.`book2_uuid_pk` ' + - 'left join `book_tag2` as `b4` on `b5`.`book_tag2_id` = `b4`.`id` ' + - 'where `b0`.`author_id` is not null and `b4`.`name` != ? ' + + 'left join `book_to_tag_unordered` as `b4` on `b0`.`uuid_pk` = `b4`.`book2_uuid_pk` ' + + 'left join `book_tag2` as `b3` on `b4`.`book_tag2_id` = `b3`.`id` ' + + 'left join `test2` as `t5` on `b0`.`uuid_pk` = `t5`.`book_uuid_pk` ' + + 'where `b0`.`author_id` is not null and `b3`.`name` != ? ' + 'order by `b0`.`title` desc, `t1`.`name` asc'); expect(books5.length).toBe(3); diff --git a/tests/features/__snapshots__/dataloader.test.ts.snap b/tests/features/__snapshots__/dataloader.test.ts.snap index 50ead8830bb4..874bb4278753 100644 --- a/tests/features/__snapshots__/dataloader.test.ts.snap +++ b/tests/features/__snapshots__/dataloader.test.ts.snap @@ -49,9 +49,6 @@ exports[`Dataloader Collection.load 1`] = ` [ "[query] select \`b0\`.* from \`book\` as \`b0\` where (\`b0\`.\`author_id\` in (1, 2, 3) or \`b0\`.\`publisher_id\` in (1, 2))", ], - [ - "[query] select \`a0\`.* from \`author\` as \`a0\` left join \`author_buddies\` as \`a1\` on \`a0\`.\`id\` = \`a1\`.\`author_2_id\` where \`a1\`.\`author_1_id\` in (1, 2, 3)", - ], [ "[query] select \`c0\`.* from \`chat\` as \`c0\` where \`c0\`.\`owner_id\` in (1, 2, 3)", ], @@ -59,7 +56,7 @@ exports[`Dataloader Collection.load 1`] = ` "[query] select \`m0\`.* from \`message\` as \`m0\` where (\`m0\`.\`chat_owner_id\`, \`m0\`.\`chat_recipient_id\`) in ( values (1, 2), (1, 3))", ], [ - "[query] select \`a1\`.*, \`a0\`.\`author_2_id\` as \`fk__author_2_id\`, \`a0\`.\`author_1_id\` as \`fk__author_1_id\` from \`author_buddies\` as \`a0\` inner join \`author\` as \`a1\` on \`a0\`.\`author_1_id\` = \`a1\`.\`id\` where \`a0\`.\`author_2_id\` in (1, 2, 4, 5)", + "[query] select \`a0\`.*, \`b1\`.\`id\` as \`b1__id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\` from \`author\` as \`a0\` left join \`author_buddies\` as \`a2\` on \`a0\`.\`id\` = \`a2\`.\`author_2_id\` left join \`author\` as \`b1\` on \`a2\`.\`author_1_id\` = \`b1\`.\`id\` left join \`author_buddies\` as \`a3\` on \`a0\`.\`id\` = \`a3\`.\`author_2_id\` where \`a3\`.\`author_1_id\` in (1, 2, 3)", ], ] `; @@ -69,9 +66,6 @@ exports[`Dataloader Dataloader can be globally enabled for Collections 1`] = ` [ "[query] select \`b0\`.* from \`book\` as \`b0\` where (\`b0\`.\`author_id\` in (1, 2, 3) or \`b0\`.\`publisher_id\` in (1, 2))", ], - [ - "[query] select \`a0\`.* from \`author\` as \`a0\` left join \`author_buddies\` as \`a1\` on \`a0\`.\`id\` = \`a1\`.\`author_2_id\` where \`a1\`.\`author_1_id\` in (1, 2, 3)", - ], [ "[query] select \`c0\`.* from \`chat\` as \`c0\` where \`c0\`.\`owner_id\` in (1, 2, 3)", ], @@ -79,7 +73,7 @@ exports[`Dataloader Dataloader can be globally enabled for Collections 1`] = ` "[query] select \`m0\`.* from \`message\` as \`m0\` where (\`m0\`.\`chat_owner_id\`, \`m0\`.\`chat_recipient_id\`) in ( values (1, 2), (1, 3))", ], [ - "[query] select \`a1\`.*, \`a0\`.\`author_2_id\` as \`fk__author_2_id\`, \`a0\`.\`author_1_id\` as \`fk__author_1_id\` from \`author_buddies\` as \`a0\` inner join \`author\` as \`a1\` on \`a0\`.\`author_1_id\` = \`a1\`.\`id\` where \`a0\`.\`author_2_id\` in (1, 2, 4, 5)", + "[query] select \`a0\`.*, \`b1\`.\`id\` as \`b1__id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\` from \`author\` as \`a0\` left join \`author_buddies\` as \`a2\` on \`a0\`.\`id\` = \`a2\`.\`author_2_id\` left join \`author\` as \`b1\` on \`a2\`.\`author_1_id\` = \`b1\`.\`id\` left join \`author_buddies\` as \`a3\` on \`a0\`.\`id\` = \`a3\`.\`author_2_id\` where \`a3\`.\`author_1_id\` in (1, 2, 3)", ], ] `; @@ -146,9 +140,6 @@ exports[`Dataloader getColBatchLoadFn 1`] = ` [ "[query] select \`b0\`.* from \`book\` as \`b0\` where (\`b0\`.\`author_id\` in (1, 2, 3) or \`b0\`.\`publisher_id\` in (1, 2))", ], - [ - "[query] select \`a0\`.* from \`author\` as \`a0\` left join \`author_buddies\` as \`a1\` on \`a0\`.\`id\` = \`a1\`.\`author_2_id\` where \`a1\`.\`author_1_id\` in (1, 2, 3)", - ], [ "[query] select \`c0\`.* from \`chat\` as \`c0\` where \`c0\`.\`owner_id\` in (1, 2, 3)", ], @@ -156,7 +147,7 @@ exports[`Dataloader getColBatchLoadFn 1`] = ` "[query] select \`m0\`.* from \`message\` as \`m0\` where (\`m0\`.\`chat_owner_id\`, \`m0\`.\`chat_recipient_id\`) in ( values (1, 2), (1, 3))", ], [ - "[query] select \`a1\`.*, \`a0\`.\`author_2_id\` as \`fk__author_2_id\`, \`a0\`.\`author_1_id\` as \`fk__author_1_id\` from \`author_buddies\` as \`a0\` inner join \`author\` as \`a1\` on \`a0\`.\`author_1_id\` = \`a1\`.\`id\` where \`a0\`.\`author_2_id\` in (1, 2, 4, 5)", + "[query] select \`a0\`.*, \`b1\`.\`id\` as \`b1__id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\` from \`author\` as \`a0\` left join \`author_buddies\` as \`a2\` on \`a0\`.\`id\` = \`a2\`.\`author_2_id\` left join \`author\` as \`b1\` on \`a2\`.\`author_1_id\` = \`b1\`.\`id\` left join \`author_buddies\` as \`a3\` on \`a0\`.\`id\` = \`a3\`.\`author_2_id\` where \`a3\`.\`author_1_id\` in (1, 2, 3)", ], ] `; diff --git a/tests/features/composite-keys/composite-keys.mysql.test.ts b/tests/features/composite-keys/composite-keys.mysql.test.ts index 16a63e8f7958..8a18e097da2f 100644 --- a/tests/features/composite-keys/composite-keys.mysql.test.ts +++ b/tests/features/composite-keys/composite-keys.mysql.test.ts @@ -227,6 +227,60 @@ describe('composite keys in mysql', () => { expect(c3).toBeNull(); }); + test('populate pivot reference with composite FK', async () => { + const car1 = new Car2('Audi A8', 2011, 100_000); + const car2 = new Car2('Audi A8', 2012, 150_000); + const car3 = new Car2('Audi A8', 2013, 200_000); + const user1 = new User2('John', 'Doe 1'); + const user2 = new User2('John', 'Doe 2'); + const user3 = new User2('John', 'Doe 3'); + user1.cars.add(car1, car3); + user2.cars.add(car3); + user2.cars.add(car2, car3); + await orm.em.persistAndFlush([user1, user2, user3]); + orm.em.clear(); + + const u1 = await orm.em.findOneOrFail(User2, user1, { + populate: ['cars:ref'], + }); + expect(u1.cars.isDirty()).toBe(false); + expect(u1.cars.isInitialized()).toBe(true); + expect(u1.cars.isInitialized(true)).toBe(false); + expect(u1.cars.getItems()).toMatchObject([ + { name: 'Audi A8', price: undefined, year: 2011 }, + { name: 'Audi A8', price: undefined, year: 2013 }, + ]); + expect(wrap(u1).toJSON()).toEqual({ + firstName: 'John', + lastName: 'Doe 1', + favouriteCar: null, + foo: null, + cars: [ + { name: 'Audi A8', price: undefined, year: 2011 }, + { name: 'Audi A8', price: undefined, year: 2013 }, + ], + }); + + u1.foo = 321; + u1.cars[0].price = 350_000; + await orm.em.flush(); + orm.em.clear(); + + const u2 = await orm.em.findOneOrFail(User2, u1, { populate: ['cars'] }); + expect(u2.cars[0].price).toBe(350_000); + + const c1 = await orm.em.findOneOrFail(Car2, { name: car1.name, year: car1.year }); + expect(c1).toBe(u2.cars[0]); + + await orm.em.remove(u2).flush(); + const o3 = await orm.em.findOne(User2, u1); + expect(o3).toBeNull(); + const c2 = await orm.em.findOneOrFail(Car2, car1); + await orm.em.remove(c2).flush(); + const c3 = await orm.em.findOne(Car2, car1); + expect(c3).toBeNull(); + }); + test('composite entity in m:n relationship, one of entity is composite', async () => { const sandwich1 = new Sandwich('Fish Sandwich', 100); const sandwich2 = new Sandwich('Fried Egg Sandwich', 200); diff --git a/tests/features/cursor-based-pagination/__snapshots__/complex-cursor.test.ts.snap b/tests/features/cursor-based-pagination/__snapshots__/complex-cursor.test.ts.snap index 35dc371c9841..cd38777a4683 100644 --- a/tests/features/cursor-based-pagination/__snapshots__/complex-cursor.test.ts.snap +++ b/tests/features/cursor-based-pagination/__snapshots__/complex-cursor.test.ts.snap @@ -16,15 +16,14 @@ exports[`simple cursor based pagination (better-sqlite) complex cursor based pag exports[`simple cursor based pagination (better-sqlite) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 1`] = ` [ - "[query] select \`u0\`.* from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3' order by \`u1\`.\`email\` desc, \`u1\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", + "[query] select \`u0\`.*, \`b1\`.\`_id\` as \`b1___id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\`, \`b1\`.\`age\` as \`b1__age\`, \`b1\`.\`terms_accepted\` as \`b1__terms_accepted\`, \`b1\`.\`best_friend__id\` as \`b1__best_friend__id\` from \`user\` as \`u0\` left join \`user\` as \`b1\` on \`u0\`.\`best_friend__id\` = \`b1\`.\`_id\` left join \`user\` as \`u2\` on \`u0\`.\`best_friend__id\` = \`u2\`.\`_id\` where \`u2\`.\`name\` = 'User 3' order by \`u2\`.\`email\` desc, \`u2\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", "[query] select count(*) as \`count\` from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3'", ] `; exports[`simple cursor based pagination (better-sqlite) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 2`] = ` [ - "[query] select \`u0\`.* from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3' and \`u1\`.\`email\` <= 'email-96' and \`u1\`.\`name\` <= 'User 3' and ((\`u1\`.\`email\` < 'email-96' and \`u1\`.\`name\` < 'User 3') or (\`u0\`.\`name\` >= 'User 3' and (\`u0\`.\`name\` > 'User 3' or (\`u0\`.\`age\` <= 38 and (\`u0\`.\`age\` < 38 or \`u0\`.\`email\` < 'email-76'))))) order by \`u1\`.\`email\` desc, \`u1\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", - "[query] select \`u0\`.* from \`user\` as \`u0\` where \`u0\`.\`_id\` in (5) order by \`u0\`.\`email\` asc, \`u0\`.\`name\` asc", + "[query] select \`u0\`.*, \`b1\`.\`_id\` as \`b1___id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\`, \`b1\`.\`age\` as \`b1__age\`, \`b1\`.\`terms_accepted\` as \`b1__terms_accepted\`, \`b1\`.\`best_friend__id\` as \`b1__best_friend__id\` from \`user\` as \`u0\` left join \`user\` as \`b1\` on \`u0\`.\`best_friend__id\` = \`b1\`.\`_id\` left join \`user\` as \`u2\` on \`u0\`.\`best_friend__id\` = \`u2\`.\`_id\` where \`u2\`.\`name\` = 'User 3' and \`u2\`.\`email\` <= 'email-96' and \`u2\`.\`name\` <= 'User 3' and ((\`u2\`.\`email\` < 'email-96' and \`u2\`.\`name\` < 'User 3') or (\`u0\`.\`name\` >= 'User 3' and (\`u0\`.\`name\` > 'User 3' or (\`u0\`.\`age\` <= 38 and (\`u0\`.\`age\` < 38 or \`u0\`.\`email\` < 'email-76'))))) order by \`u2\`.\`email\` desc, \`u2\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", "[query] select count(*) as \`count\` from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3'", ] `; @@ -59,15 +58,14 @@ exports[`simple cursor based pagination (mysql) complex cursor based pagination exports[`simple cursor based pagination (mysql) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 1`] = ` [ - "[query] select \`u0\`.* from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3' order by \`u1\`.\`email\` desc, \`u1\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", + "[query] select \`u0\`.*, \`b1\`.\`_id\` as \`b1___id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\`, \`b1\`.\`age\` as \`b1__age\`, \`b1\`.\`terms_accepted\` as \`b1__terms_accepted\`, \`b1\`.\`best_friend__id\` as \`b1__best_friend__id\` from \`user\` as \`u0\` left join \`user\` as \`b1\` on \`u0\`.\`best_friend__id\` = \`b1\`.\`_id\` left join \`user\` as \`u2\` on \`u0\`.\`best_friend__id\` = \`u2\`.\`_id\` where \`u2\`.\`name\` = 'User 3' order by \`u2\`.\`email\` desc, \`u2\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", "[query] select count(*) as \`count\` from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3'", ] `; exports[`simple cursor based pagination (mysql) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 2`] = ` [ - "[query] select \`u0\`.* from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3' and \`u1\`.\`email\` <= 'email-96' and \`u1\`.\`name\` <= 'User 3' and ((\`u1\`.\`email\` < 'email-96' and \`u1\`.\`name\` < 'User 3') or (\`u0\`.\`name\` >= 'User 3' and (\`u0\`.\`name\` > 'User 3' or (\`u0\`.\`age\` <= 38 and (\`u0\`.\`age\` < 38 or \`u0\`.\`email\` < 'email-76'))))) order by \`u1\`.\`email\` desc, \`u1\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", - "[query] select \`u0\`.* from \`user\` as \`u0\` where \`u0\`.\`_id\` in (5) order by \`u0\`.\`email\` asc, \`u0\`.\`name\` asc", + "[query] select \`u0\`.*, \`b1\`.\`_id\` as \`b1___id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\`, \`b1\`.\`age\` as \`b1__age\`, \`b1\`.\`terms_accepted\` as \`b1__terms_accepted\`, \`b1\`.\`best_friend__id\` as \`b1__best_friend__id\` from \`user\` as \`u0\` left join \`user\` as \`b1\` on \`u0\`.\`best_friend__id\` = \`b1\`.\`_id\` left join \`user\` as \`u2\` on \`u0\`.\`best_friend__id\` = \`u2\`.\`_id\` where \`u2\`.\`name\` = 'User 3' and \`u2\`.\`email\` <= 'email-96' and \`u2\`.\`name\` <= 'User 3' and ((\`u2\`.\`email\` < 'email-96' and \`u2\`.\`name\` < 'User 3') or (\`u0\`.\`name\` >= 'User 3' and (\`u0\`.\`name\` > 'User 3' or (\`u0\`.\`age\` <= 38 and (\`u0\`.\`age\` < 38 or \`u0\`.\`email\` < 'email-76'))))) order by \`u2\`.\`email\` desc, \`u2\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", "[query] select count(*) as \`count\` from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3'", ] `; @@ -88,15 +86,14 @@ exports[`simple cursor based pagination (postgresql) complex cursor based pagina exports[`simple cursor based pagination (postgresql) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 1`] = ` [ - "[query] select "u0".* from "user" as "u0" left join "user" as "u1" on "u0"."best_friend__id" = "u1"."_id" where "u1"."name" = 'User 3' order by "u1"."email" desc, "u1"."name" desc, "u0"."name" asc, "u0"."age" desc, "u0"."email" desc limit 6", + "[query] select "u0".*, "b1"."_id" as "b1___id", "b1"."name" as "b1__name", "b1"."email" as "b1__email", "b1"."age" as "b1__age", "b1"."terms_accepted" as "b1__terms_accepted", "b1"."best_friend__id" as "b1__best_friend__id" from "user" as "u0" left join "user" as "b1" on "u0"."best_friend__id" = "b1"."_id" left join "user" as "u2" on "u0"."best_friend__id" = "u2"."_id" where "u2"."name" = 'User 3' order by "u2"."email" desc, "u2"."name" desc, "u0"."name" asc, "u0"."age" desc, "u0"."email" desc limit 6", "[query] select count(*) as "count" from "user" as "u0" left join "user" as "u1" on "u0"."best_friend__id" = "u1"."_id" where "u1"."name" = 'User 3'", ] `; exports[`simple cursor based pagination (postgresql) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 2`] = ` [ - "[query] select "u0".* from "user" as "u0" left join "user" as "u1" on "u0"."best_friend__id" = "u1"."_id" where "u1"."name" = 'User 3' and "u1"."email" <= 'email-96' and "u1"."name" <= 'User 3' and (("u1"."email" < 'email-96' and "u1"."name" < 'User 3') or ("u0"."name" >= 'User 3' and ("u0"."name" > 'User 3' or ("u0"."age" <= 38 and ("u0"."age" < 38 or "u0"."email" < 'email-76'))))) order by "u1"."email" desc, "u1"."name" desc, "u0"."name" asc, "u0"."age" desc, "u0"."email" desc limit 6", - "[query] select "u0".* from "user" as "u0" where "u0"."_id" in (5) order by "u0"."email" asc, "u0"."name" asc", + "[query] select "u0".*, "b1"."_id" as "b1___id", "b1"."name" as "b1__name", "b1"."email" as "b1__email", "b1"."age" as "b1__age", "b1"."terms_accepted" as "b1__terms_accepted", "b1"."best_friend__id" as "b1__best_friend__id" from "user" as "u0" left join "user" as "b1" on "u0"."best_friend__id" = "b1"."_id" left join "user" as "u2" on "u0"."best_friend__id" = "u2"."_id" where "u2"."name" = 'User 3' and "u2"."email" <= 'email-96' and "u2"."name" <= 'User 3' and (("u2"."email" < 'email-96' and "u2"."name" < 'User 3') or ("u0"."name" >= 'User 3' and ("u0"."name" > 'User 3' or ("u0"."age" <= 38 and ("u0"."age" < 38 or "u0"."email" < 'email-76'))))) order by "u2"."email" desc, "u2"."name" desc, "u0"."name" asc, "u0"."age" desc, "u0"."email" desc limit 6", "[query] select count(*) as "count" from "user" as "u0" left join "user" as "u1" on "u0"."best_friend__id" = "u1"."_id" where "u1"."name" = 'User 3'", ] `; @@ -117,15 +114,14 @@ exports[`simple cursor based pagination (sqlite) complex cursor based pagination exports[`simple cursor based pagination (sqlite) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 1`] = ` [ - "[query] select \`u0\`.* from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3' order by \`u1\`.\`email\` desc, \`u1\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", + "[query] select \`u0\`.*, \`b1\`.\`_id\` as \`b1___id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\`, \`b1\`.\`age\` as \`b1__age\`, \`b1\`.\`terms_accepted\` as \`b1__terms_accepted\`, \`b1\`.\`best_friend__id\` as \`b1__best_friend__id\` from \`user\` as \`u0\` left join \`user\` as \`b1\` on \`u0\`.\`best_friend__id\` = \`b1\`.\`_id\` left join \`user\` as \`u2\` on \`u0\`.\`best_friend__id\` = \`u2\`.\`_id\` where \`u2\`.\`name\` = 'User 3' order by \`u2\`.\`email\` desc, \`u2\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", "[query] select count(*) as \`count\` from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3'", ] `; exports[`simple cursor based pagination (sqlite) complex joined cursor based pagination using \`last\` and \`before\` (id asc) 2`] = ` [ - "[query] select \`u0\`.* from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3' and \`u1\`.\`email\` <= 'email-96' and \`u1\`.\`name\` <= 'User 3' and ((\`u1\`.\`email\` < 'email-96' and \`u1\`.\`name\` < 'User 3') or (\`u0\`.\`name\` >= 'User 3' and (\`u0\`.\`name\` > 'User 3' or (\`u0\`.\`age\` <= 38 and (\`u0\`.\`age\` < 38 or \`u0\`.\`email\` < 'email-76'))))) order by \`u1\`.\`email\` desc, \`u1\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", - "[query] select \`u0\`.* from \`user\` as \`u0\` where \`u0\`.\`_id\` in (5) order by \`u0\`.\`email\` asc, \`u0\`.\`name\` asc", + "[query] select \`u0\`.*, \`b1\`.\`_id\` as \`b1___id\`, \`b1\`.\`name\` as \`b1__name\`, \`b1\`.\`email\` as \`b1__email\`, \`b1\`.\`age\` as \`b1__age\`, \`b1\`.\`terms_accepted\` as \`b1__terms_accepted\`, \`b1\`.\`best_friend__id\` as \`b1__best_friend__id\` from \`user\` as \`u0\` left join \`user\` as \`b1\` on \`u0\`.\`best_friend__id\` = \`b1\`.\`_id\` left join \`user\` as \`u2\` on \`u0\`.\`best_friend__id\` = \`u2\`.\`_id\` where \`u2\`.\`name\` = 'User 3' and \`u2\`.\`email\` <= 'email-96' and \`u2\`.\`name\` <= 'User 3' and ((\`u2\`.\`email\` < 'email-96' and \`u2\`.\`name\` < 'User 3') or (\`u0\`.\`name\` >= 'User 3' and (\`u0\`.\`name\` > 'User 3' or (\`u0\`.\`age\` <= 38 and (\`u0\`.\`age\` < 38 or \`u0\`.\`email\` < 'email-76'))))) order by \`u2\`.\`email\` desc, \`u2\`.\`name\` desc, \`u0\`.\`name\` asc, \`u0\`.\`age\` desc, \`u0\`.\`email\` desc limit 6", "[query] select count(*) as \`count\` from \`user\` as \`u0\` left join \`user\` as \`u1\` on \`u0\`.\`best_friend__id\` = \`u1\`.\`_id\` where \`u1\`.\`name\` = 'User 3'", ] `; diff --git a/tests/features/custom-order/custom-order.mysql.test.ts b/tests/features/custom-order/custom-order.mysql.test.ts index 4e2fdd62f028..3c78ccfca38f 100644 --- a/tests/features/custom-order/custom-order.mysql.test.ts +++ b/tests/features/custom-order/custom-order.mysql.test.ts @@ -238,6 +238,7 @@ describe('custom order [mysql]', () => { em.clear(); const users = await em.find(User, {}, { + strategy: 'select-in', populate: ['tasks'], orderBy: { name: QueryOrder.ASC, @@ -251,5 +252,19 @@ describe('custom order [mysql]', () => { expect(ret).toEqual(['u1-1c', 'u1-1b', 'u1-1a', 'u2-2b', 'u2-2a', 'u2-2c']); expect(mock.mock.calls[4][0]).toMatch('select `u0`.* from `user` as `u0` left join `task` as `t1` on `u0`.`id` = `t1`.`owner_id` order by `u0`.`name` asc, (case when `t1`.`priority` = \'low\' then 0 when `t1`.`priority` = \'medium\' then 1 when `t1`.`priority` = \'high\' then 2 else null end) asc'); expect(mock.mock.calls[5][0]).toMatch('select `t0`.* from `task` as `t0` where `t0`.`owner_id` in (1, 2) order by (case when `t0`.`priority` = \'low\' then 0 when `t0`.`priority` = \'medium\' then 1 when `t0`.`priority` = \'high\' then 2 else null end) asc'); + + const users2 = await em.find(User, {}, { + populate: ['tasks'], + orderBy: { + name: QueryOrder.ASC, + tasks: { + priority: QueryOrder.ASC, + }, + }, + }); + + const ret2 = users2.flatMap(u => u.tasks.getItems()).map(({ owner, label }) => `${owner?.name}-${label}`); + expect(ret2).toEqual(['u1-1c', 'u1-1b', 'u1-1a', 'u2-2b', 'u2-2a', 'u2-2c']); + expect(mock.mock.calls[6][0]).toMatch("select `u0`.*, `t1`.`id` as `t1__id`, `t1`.`label` as `t1__label`, `t1`.`owner_id` as `t1__owner_id`, `t1`.`priority` as `t1__priority`, `t1`.`rating` as `t1__rating`, `t1`.`difficulty` as `t1__difficulty` from `user` as `u0` left join `task` as `t1` on `u0`.`id` = `t1`.`owner_id` order by `u0`.`name` asc, (case when `t1`.`priority` = 'low' then 0 when `t1`.`priority` = 'medium' then 1 when `t1`.`priority` = 'high' then 2 else null end) asc"); }); }); diff --git a/tests/features/custom-order/custom-order.postgres.test.ts b/tests/features/custom-order/custom-order.postgres.test.ts index 0e1e8e89119d..81c7cd671f48 100644 --- a/tests/features/custom-order/custom-order.postgres.test.ts +++ b/tests/features/custom-order/custom-order.postgres.test.ts @@ -239,7 +239,8 @@ describe('custom order [postgres]', () => { await em.persistAndFlush([user1, user2]); em.clear(); - const users = await em.find(User, {}, { + const users1 = await em.find(User, {}, { + strategy: 'select-in', populate: ['tasks'], orderBy: { name: QueryOrder.ASC, @@ -249,9 +250,23 @@ describe('custom order [postgres]', () => { }, }); - const ret = users.flatMap(u => u.tasks.getItems()).map(({ owner, label }) => `${owner?.name}-${label}`); - expect(ret).toEqual(['u1-1c', 'u1-1b', 'u1-1a', 'u2-2b', 'u2-2a', 'u2-2c']); + const ret1 = users1.flatMap(u => u.tasks.getItems()).map(({ owner, label }) => `${owner?.name}-${label}`); + expect(ret1).toEqual(['u1-1c', 'u1-1b', 'u1-1a', 'u2-2b', 'u2-2a', 'u2-2c']); expect(mock.mock.calls[4][0]).toMatch(`select "u0".* from "user" as "u0" left join "task" as "t1" on "u0"."id" = "t1"."owner_id" order by "u0"."name" asc, (case when "t1"."priority" = 'low' then 0 when "t1"."priority" = 'medium' then 1 when "t1"."priority" = 'high' then 2 else null end) asc`); expect(mock.mock.calls[5][0]).toMatch(`select "t0".* from "task" as "t0" where "t0"."owner_id" in (1, 2) order by (case when "t0"."priority" = 'low' then 0 when "t0"."priority" = 'medium' then 1 when "t0"."priority" = 'high' then 2 else null end) asc`); + + const users2 = await em.find(User, {}, { + populate: ['tasks'], + orderBy: { + name: QueryOrder.ASC, + tasks: { + priority: QueryOrder.ASC, + }, + }, + }); + + const ret2 = users2.flatMap(u => u.tasks.getItems()).map(({ owner, label }) => `${owner?.name}-${label}`); + expect(ret2).toEqual(['u1-1c', 'u1-1b', 'u1-1a', 'u2-2b', 'u2-2a', 'u2-2c']); + expect(mock.mock.calls[6][0]).toMatch(`select "u0".*, "t1"."id" as "t1__id", "t1"."label" as "t1__label", "t1"."owner_id" as "t1__owner_id", "t1"."priority" as "t1__priority", "t1"."rating" as "t1__rating", "t1"."difficulty" as "t1__difficulty" from "user" as "u0" left join "task" as "t1" on "u0"."id" = "t1"."owner_id" order by "u0"."name" asc, (case when "t1"."priority" = 'low' then 0 when "t1"."priority" = 'medium' then 1 when "t1"."priority" = 'high' then 2 else null end) asc`); }); }); diff --git a/tests/features/custom-order/custom-order.sqlite.test.ts b/tests/features/custom-order/custom-order.sqlite.test.ts index d733babeb7a5..48a1c3cc9832 100644 --- a/tests/features/custom-order/custom-order.sqlite.test.ts +++ b/tests/features/custom-order/custom-order.sqlite.test.ts @@ -240,6 +240,7 @@ describe('custom order [sqlite]', () => { em.clear(); const users = await em.find(User, {}, { + strategy: 'select-in', populate: ['tasks'], orderBy: { name: QueryOrder.ASC, @@ -253,6 +254,21 @@ describe('custom order [sqlite]', () => { expect(ret).toEqual(['u1-1c', 'u1-1b', 'u1-1a', 'u2-2b', 'u2-2a', 'u2-2c']); expect(mock.mock.calls[4][0]).toMatch('select `u0`.* from `user` as `u0` left join `task` as `t1` on `u0`.`id` = `t1`.`owner_id` order by `u0`.`name` asc, (case when `t1`.`priority` = \'low\' then 0 when `t1`.`priority` = \'medium\' then 1 when `t1`.`priority` = \'high\' then 2 else null end) asc'); expect(mock.mock.calls[5][0]).toMatch('select `t0`.* from `task` as `t0` where `t0`.`owner_id` in (1, 2) order by (case when `t0`.`priority` = \'low\' then 0 when `t0`.`priority` = \'medium\' then 1 when `t0`.`priority` = \'high\' then 2 else null end) asc'); + + + const users2 = await em.find(User, {}, { + populate: ['tasks'], + orderBy: { + name: QueryOrder.ASC, + tasks: { + priority: QueryOrder.ASC, + }, + }, + }); + + const ret2 = users2.flatMap(u => u.tasks.getItems()).map(({ owner, label }) => `${owner?.name}-${label}`); + expect(ret2).toEqual(['u1-1c', 'u1-1b', 'u1-1a', 'u2-2b', 'u2-2a', 'u2-2c']); + expect(mock.mock.calls[6][0]).toMatch(`select \`u0\`.*, \`t1\`.\`id\` as \`t1__id\`, \`t1\`.\`label\` as \`t1__label\`, \`t1\`.\`owner_id\` as \`t1__owner_id\`, \`t1\`.\`priority\` as \`t1__priority\`, \`t1\`.\`rating\` as \`t1__rating\`, \`t1\`.\`difficulty\` as \`t1__difficulty\` from \`user\` as \`u0\` left join \`task\` as \`t1\` on \`u0\`.\`id\` = \`t1\`.\`owner_id\` order by \`u0\`.\`name\` asc, (case when \`t1\`.\`priority\` = 'low' then 0 when \`t1\`.\`priority\` = 'medium' then 1 when \`t1\`.\`priority\` = 'high' then 2 else null end) asc`); }); }); diff --git a/tests/features/embeddables/GH4824.test.ts b/tests/features/embeddables/GH4824.test.ts index 433d089e609e..0a5ee8e5172c 100644 --- a/tests/features/embeddables/GH4824.test.ts +++ b/tests/features/embeddables/GH4824.test.ts @@ -1,5 +1,6 @@ import { Embeddable, Embedded, Entity, PrimaryKey, Property, Type } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/sqlite'; +import { mockLogger } from '../../helpers'; class Point { @@ -84,8 +85,10 @@ afterAll(async () => { }); test('GH #4824', async () => { + const mock = mockLogger(orm); const user = await orm.em.findOne(User, { id: 1, }); expect(user).toBeNull(); + expect(mock.mock.calls[0][0]).toMatch('select `u0`.* from `user` as `u0` where `u0`.`id` = 1 limit 1'); }); diff --git a/tests/features/events/events.mysql.test.ts b/tests/features/events/events.mysql.test.ts index 34796eb1c6bd..a1d4938ae6a2 100644 --- a/tests/features/events/events.mysql.test.ts +++ b/tests/features/events/events.mysql.test.ts @@ -210,20 +210,20 @@ describe('events (mysql)', () => { const authors2 = await orm.em.fork().find(Author2, {}, { populate: ['books'] }); expect(authors2).toHaveLength(1); expect(EverythingSubscriber.log.map(l => [l[0], l[1].entity.constructor.name]).filter(a => a[0] === EventType.onLoad)).toEqual([ - ['onLoad', 'Author2'], ['onLoad', 'Book2'], ['onLoad', 'Book2'], ['onLoad', 'Book2'], + ['onLoad', 'Author2'], ]); EverythingSubscriber.log.length = 0; const authors3 = await orm.em.fork().find(Author2, {}, { populate: ['*'] }); expect(authors3).toHaveLength(1); expect(EverythingSubscriber.log.map(l => [l[0], l[1].entity.constructor.name]).filter(a => a[0] === EventType.onLoad)).toEqual([ - ['onLoad', 'Author2'], ['onLoad', 'Book2'], ['onLoad', 'Book2'], ['onLoad', 'Book2'], + ['onLoad', 'Author2'], ['onLoad', 'Publisher2'], ['onLoad', 'BookTag2'], ['onLoad', 'BookTag2'], diff --git a/tests/features/filters/filters.mysql.test.ts b/tests/features/filters/filters.mysql.test.ts index 5ad47879cffa..960948a10521 100644 --- a/tests/features/filters/filters.mysql.test.ts +++ b/tests/features/filters/filters.mysql.test.ts @@ -15,7 +15,9 @@ describe('filters [mysql]', () => { await orm.close(true); }); - test('filters', async () => { + test('filters (select-in)', async () => { + orm.config.set('loadStrategy', 'select-in'); + 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); @@ -112,4 +114,102 @@ describe('filters [mysql]', () => { })).rejects.toThrow(`No arguments provided for filter 'writtenBy'`); }); + test('filters (joined)', async () => { + orm.config.set('loadStrategy', 'joined'); + 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); + author.books.add(book1, book2, book3); + const god = new Author2('God', 'hello@heaven.god'); + const bible1 = new Book2('Bible', god); + const bible2 = new Book2('Bible pt. 2', god); + const bible3 = new Book2('Bible pt. 3', new Author2('Lol', 'lol@lol.lol')); + god.books.add(bible1, bible2, bible3); + await orm.em.persistAndFlush([author, god]); + orm.em.clear(); + + const mock = mockLogger(orm); + const books1 = await orm.em.find(Book2, { title: '123' }, { + populate: ['perex'], + orderBy: { title: QueryOrder.DESC }, + filters: ['long', 'expensive'], + }); + expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `test_id` ' + + 'from `book2` as `b0` ' + + 'left join `test2` as `t1` on `b0`.`uuid_pk` = `t1`.`book_uuid_pk` ' + + 'where `b0`.`author_id` is not null and length(perex) > 10000 and `b0`.`price` > 1000 and `b0`.`title` = \'123\' ' + + 'order by `b0`.`title` desc'); + + const books2 = await orm.em.find(Book2, { title: '123' }, { + filters: { hasAuthor: false, long: true, writtenBy: { name: 'God' } }, + }); + expect(mock.mock.calls[1][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`created_at`, `b0`.`title`, `b0`.`price`, `b0`.`double`, `b0`.`meta`, `b0`.`author_id`, `b0`.`publisher_id`, `b0`.price * 1.19 as `price_taxed`, `t2`.`id` as `test_id` ' + + 'from `book2` as `b0` ' + + 'left join `author2` as `a1` on `b0`.`author_id` = `a1`.`id` ' + + 'left join `test2` as `t2` on `b0`.`uuid_pk` = `t2`.`book_uuid_pk` ' + + "where `a1`.`name` = 'God' and length(perex) > 10000 and `b0`.`title` = '123'"); + + const books3 = await orm.em.find(Book2, { title: '123' }, { + filters: false, + }); + expect(mock.mock.calls[2][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`created_at`, `b0`.`title`, `b0`.`price`, `b0`.`double`, `b0`.`meta`, `b0`.`author_id`, `b0`.`publisher_id`, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `test_id` ' + + 'from `book2` as `b0` ' + + 'left join `test2` as `t1` on `b0`.`uuid_pk` = `t1`.`book_uuid_pk` ' + + 'where `b0`.`title` = \'123\''); + + const books4 = await orm.em.find(Book2, { title: '123' }, { + filters: true, + }); + expect(mock.mock.calls[3][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`created_at`, `b0`.`title`, `b0`.`price`, `b0`.`double`, `b0`.`meta`, `b0`.`author_id`, `b0`.`publisher_id`, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `test_id` ' + + 'from `book2` as `b0` ' + + 'left join `test2` as `t1` on `b0`.`uuid_pk` = `t1`.`book_uuid_pk` ' + + 'where `b0`.`author_id` is not null and `b0`.`title` = \'123\''); + + const b1 = await orm.em.findOne(Book2, '123', { + filters: { long: true }, + }); + expect(mock.mock.calls[4][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`created_at`, `b0`.`title`, `b0`.`price`, `b0`.`double`, `b0`.`meta`, `b0`.`author_id`, `b0`.`publisher_id`, `b0`.price * 1.19 as `price_taxed`, `t1`.`id` as `test_id` ' + + 'from `book2` as `b0` ' + + 'left join `test2` as `t1` on `b0`.`uuid_pk` = `t1`.`book_uuid_pk` ' + + 'where `b0`.`author_id` is not null and length(perex) > 10000 and `b0`.`uuid_pk` = \'123\' limit 1'); + + const b2 = await orm.em.findOne(Book2, { author: { name: 'Jon' } }, { + filters: { hasAuthor: false, long: true }, + }); + expect(mock.mock.calls[5][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`created_at`, `b0`.`title`, `b0`.`price`, `b0`.`double`, `b0`.`meta`, `b0`.`author_id`, `b0`.`publisher_id`, `b0`.price * 1.19 as `price_taxed`, `t2`.`id` as `test_id` ' + + 'from `book2` as `b0` ' + + 'left join `author2` as `a1` on `b0`.`author_id` = `a1`.`id` ' + + 'left join `test2` as `t2` on `b0`.`uuid_pk` = `t2`.`book_uuid_pk` ' + + "where length(perex) > 10000 and `a1`.`name` = 'Jon' " + + 'limit 1'); + + await orm.em.count(Book2, { author: { name: 'Jon' } }, { + filters: { hasAuthor: false, long: true }, + }); + expect(mock.mock.calls[6][0]).toMatch('select count(*) as `count` ' + + 'from `book2` as `b0` ' + + 'left join `author2` as `a1` on `b0`.`author_id` = `a1`.`id` ' + + 'where length(perex) > 10000 and `a1`.`name` = \'Jon\''); + + await orm.em.nativeUpdate(Book2, '123', { title: 'b123' }, { + filters: { hasAuthor: false, long: true }, + }); + expect(mock.mock.calls[7][0]).toMatch('update `book2` set `title` = \'b123\''); + + await orm.em.nativeUpdate(Book2, '123', { title: 'b123' }, { + filters: { hasAuthor: false, long: true }, + }); + expect(mock.mock.calls[8][0]).toMatch('update `book2` set `title` = \'b123\' where length(perex) > 10000 and `uuid_pk` = \'123\''); + + await orm.em.nativeDelete(Book2, '321', { + filters: { hasAuthor: false, long: true }, + }); + expect(mock.mock.calls[9][0]).toMatch('delete from `book2` where length(perex) > 10000 and `uuid_pk` = \'321\''); + + await expect(orm.em.find(Book2, {}, { + filters: { hasAuthor: false, long: true, writtenBy: true }, + })).rejects.toThrow(`No arguments provided for filter 'writtenBy'`); + }); + }); diff --git a/tests/features/filters/filters.postgres.test.ts b/tests/features/filters/filters.postgres.test.ts index f7545c1eef8b..d74ae6a05916 100644 --- a/tests/features/filters/filters.postgres.test.ts +++ b/tests/features/filters/filters.postgres.test.ts @@ -166,7 +166,7 @@ describe('filters [postgres]', () => { orm.em.clear(); mock.mockReset(); - const e1 = await orm.em.findOneOrFail(Employee, employee.id, { populate: ['benefits.details'] }); + const e1 = await orm.em.findOneOrFail(Employee, employee.id, { populate: ['benefits.details'], strategy: 'select-in' }); expect(mock.mock.calls[0][0]).toMatch(`select "e0".* from "employee" as "e0" where "e0"."id" = $1 limit $2`); expect(mock.mock.calls[1][0]).toMatch(`select "b1".*, "e0"."benefit_id" as "fk__benefit_id", "e0"."employee_id" as "fk__employee_id" from "employee_benefits" as "e0" inner join "public"."benefit" as "b1" on "e0"."benefit_id" = "b1"."id" where "b1"."benefit_status" = $1 and "e0"."employee_id" in ($2)`); expect(mock.mock.calls[2][0]).toMatch(`select "b0".* from "benefit_detail" as "b0" where "b0"."active" = $1 and "b0"."benefit_id" in ($2)`); @@ -176,7 +176,7 @@ describe('filters [postgres]', () => { orm.em.clear(); mock.mockReset(); - const e2 = await orm.em.findOneOrFail(Employee, employee.id, { populate: ['benefits.details'], strategy: LoadStrategy.JOINED }); + const e2 = await orm.em.findOneOrFail(Employee, employee.id, { populate: ['benefits.details'], strategy: 'joined' }); expect(mock.mock.calls[0][0]).toMatch('select "e0".*, "b1"."id" as "b1__id", "b1"."benefit_status" as "b1__benefit_status", "b1"."name" as "b1__name", "d3"."id" as "d3__id", "d3"."description" as "d3__description", "d3"."benefit_id" as "d3__benefit_id", "d3"."active" as "d3__active" ' + 'from "employee" as "e0" ' + 'left join "employee_benefits" as "e2" on "e0"."id" = "e2"."employee_id" ' + diff --git a/tests/features/lazy-scalar-properties/lazy-scalar-properties.mysql.test.ts b/tests/features/lazy-scalar-properties/lazy-scalar-properties.mysql.test.ts index a945e8d50cea..be5ca6494f60 100644 --- a/tests/features/lazy-scalar-properties/lazy-scalar-properties.mysql.test.ts +++ b/tests/features/lazy-scalar-properties/lazy-scalar-properties.mysql.test.ts @@ -15,7 +15,9 @@ describe('lazy scalar properties (mysql)', () => { await orm.close(true); }); - test('lazy scalar properties', async () => { + test('lazy scalar properties (select-in)', async () => { + orm.config.set('loadStrategy', 'select-in'); + const book = new Book2('b', new Author2('n', 'e')); book.perex = ref('123'); await orm.em.persistAndFlush(book); @@ -75,6 +77,55 @@ describe('lazy scalar properties (mysql)', () => { 'order by `b0`.`title` asc'); }); + test('lazy scalar properties (joined)', async () => { + orm.config.set('loadStrategy', 'joined'); + + const book = new Book2('b', new Author2('n', 'e')); + book.perex = ref('123'); + await orm.em.persistAndFlush(book); + orm.em.clear(); + + const mock = mockLogger(orm, ['query']); + + const r1 = await orm.em.find(Author2, {}, { populate: ['books'] }); + expect(r1[0].books[0].perex?.unwrap()).not.toBe('123'); + await wrap(r1[0]).populate(['books.perex']); + expect(r1[0].books[0].perex?.unwrap()).toBe('123'); + expect(mock.mock.calls).toHaveLength(2); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`created_at` as `b1__created_at`, `b1`.`title` as `b1__title`, `b1`.`price` as `b1__price`, `b1`.price * 1.19 as `b1__price_taxed`, `b1`.`double` as `b1__double`, `b1`.`meta` as `b1__meta`, `b1`.`author_id` as `b1__author_id`, `b1`.`publisher_id` as `b1__publisher_id`, `a2`.`author_id` as `address_author_id` ' + + 'from `author2` as `a0` ' + + 'left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null ' + + 'left join `address2` as `a2` on `a0`.`id` = `a2`.`author_id` ' + + 'order by `b1`.`title` asc'); + + orm.em.clear(); + mock.mock.calls.length = 0; + const r2 = await orm.em.find(Author2, {}, { populate: ['books.perex'] }); + expect(r2[0].books[0].perex?.get()).toBe('123'); + expect(mock.mock.calls).toHaveLength(1); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`created_at` as `b1__created_at`, `b1`.`title` as `b1__title`, `b1`.`perex` as `b1__perex`, `b1`.`price` as `b1__price`, `b1`.price * 1.19 as `b1__price_taxed`, `b1`.`double` as `b1__double`, `b1`.`meta` as `b1__meta`, `b1`.`author_id` as `b1__author_id`, `b1`.`publisher_id` as `b1__publisher_id`, `a2`.`author_id` as `address_author_id` ' + + 'from `author2` as `a0` ' + + 'left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null ' + + 'left join `address2` as `a2` on `a0`.`id` = `a2`.`author_id` ' + + 'order by `b1`.`title` asc'); + + orm.em.clear(); + mock.mock.calls.length = 0; + const r3 = await orm.em.findOne(Author2, book.author, { populate: ['books'] }); + expect(r3!.books[0].perex?.unwrap()).not.toBe('123'); + await expect(r3!.books[0].perex?.load()).resolves.toBe('123'); + expect(mock.mock.calls).toHaveLength(2); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`created_at` as `b1__created_at`, `b1`.`title` as `b1__title`, `b1`.`price` as `b1__price`, `b1`.price * 1.19 as `b1__price_taxed`, `b1`.`double` as `b1__double`, `b1`.`meta` as `b1__meta`, `b1`.`author_id` as `b1__author_id`, `b1`.`publisher_id` as `b1__publisher_id`, `a2`.`author_id` as `address_author_id` from `author2` as `a0` left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null left join `address2` as `a2` on `a0`.`id` = `a2`.`author_id` where `a0`.`id` = ? order by `b1`.`title` asc'); + expect(mock.mock.calls[1][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`perex` from `book2` as `b0` where `b0`.`author_id` is not null and `b0`.`uuid_pk` in (?)'); + + orm.em.clear(); + mock.mock.calls.length = 0; + const r4 = await orm.em.findOne(Author2, book.author, { populate: ['books.perex'] }); + expect(r4!.books[0].perex?.$).toBe('123'); + expect(mock.mock.calls).toHaveLength(1); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`created_at` as `b1__created_at`, `b1`.`title` as `b1__title`, `b1`.`perex` as `b1__perex`, `b1`.`price` as `b1__price`, `b1`.price * 1.19 as `b1__price_taxed`, `b1`.`double` as `b1__double`, `b1`.`meta` as `b1__meta`, `b1`.`author_id` as `b1__author_id`, `b1`.`publisher_id` as `b1__publisher_id`, `a2`.`author_id` as `address_author_id` from `author2` as `a0` left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null left join `address2` as `a2` on `a0`.`id` = `a2`.`author_id` where `a0`.`id` = ? order by `b1`.`title` asc'); + }); + test('em.populate() respects lazy scalar properties', async () => { const book = new Book2('b', new Author2('n', 'e')); book.perex = ref('123'); diff --git a/tests/features/multiple-schemas/GH3177.test.ts b/tests/features/multiple-schemas/GH3177.test.ts index acb2916bedb9..862f3799eb3e 100644 --- a/tests/features/multiple-schemas/GH3177.test.ts +++ b/tests/features/multiple-schemas/GH3177.test.ts @@ -1,4 +1,4 @@ -import { Collection, Entity, ManyToMany, ManyToOne, MikroORM, OneToMany, PrimaryKey, Property, Reference, t } from '@mikro-orm/core'; +import { Collection, Entity, ManyToMany, ManyToOne, MikroORM, OneToMany, PrimaryKey } from '@mikro-orm/core'; import { mockLogger } from '../../helpers'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; @@ -87,6 +87,7 @@ test(`GH issue 3177`, async () => { const u2 = await orm.em.findOneOrFail(User, 1, { populate: ['accessProfile.permissions'] }); expect(u2.accessProfile.permissions.getItems()).toHaveLength(3); + expect(mock).toHaveBeenCalledTimes(8); expect(mock.mock.calls[0][0]).toMatch(`begin`); 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)`); @@ -94,7 +95,5 @@ test(`GH issue 3177`, async () => { 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 "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)`); - expect(mock.mock.calls[7][0]).toMatch(`select "u0".* from "tenant_01"."user" as "u0" where "u0"."id" = 1 limit 1`); - expect(mock.mock.calls[8][0]).toMatch(`select "u0".* from "tenant_01"."user_access_profile" as "u0" where "u0"."id" in (1)`); - expect(mock.mock.calls[9][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)`); + expect(mock.mock.calls[7][0]).toMatch(`select "u0".*, "a1"."id" as "a1__id", "p2"."id" as "p2__id" from "tenant_01"."user" as "u0" left join "tenant_01"."user_access_profile" as "a1" on "u0"."access_profile_id" = "a1"."id" left join "tenant_01"."access_profile_permission" as "a3" on "a1"."id" = "a3"."access_profile_id" left join "public"."permission" as "p2" on "a3"."permission_id" = "p2"."id" where "u0"."id" = 1`); }); diff --git a/tests/features/partial-loading/partial-loading.mysql.test.ts b/tests/features/partial-loading/partial-loading.mysql.test.ts index 60b63e3e0248..7c69dc59d2a6 100644 --- a/tests/features/partial-loading/partial-loading.mysql.test.ts +++ b/tests/features/partial-loading/partial-loading.mysql.test.ts @@ -115,8 +115,7 @@ describe('partial loading (mysql)', () => { // @ts-expect-error expect(r1[0].books[0].price).toBeUndefined(); expect(r1[0].books[0].author).toBeDefined(); - expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id` from `author2` as `a0` where `a0`.`id` = ?'); - expect(mock.mock.calls[1][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`author_id`, `b0`.`title` from `book2` as `b0` where `b0`.`author_id` is not null and `b0`.`author_id` in (?) order by `b0`.`title` asc'); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title`, `b1`.`author_id` as `b1__author_id` from `author2` as `a0` left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null where `a0`.`id` = ? order by `b1`.`title` asc'); orm.em.clear(); mock.mock.calls.length = 0; @@ -131,8 +130,7 @@ describe('partial loading (mysql)', () => { // @ts-expect-error expect(r2[0].books[0].price).toBeUndefined(); expect(r2[0].books[0].author).toBeDefined(); - expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id` from `author2` as `a0` where `a0`.`id` = ?'); - expect(mock.mock.calls[1][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`author_id`, `b0`.`title` from `book2` as `b0` where `b0`.`author_id` is not null and `b0`.`author_id` in (?) order by `b0`.`title` asc'); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title`, `b1`.`author_id` as `b1__author_id` from `author2` as `a0` left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null where `a0`.`id` = ? order by `b1`.`title` asc'); orm.em.clear(); mock.mock.calls.length = 0; @@ -147,8 +145,7 @@ describe('partial loading (mysql)', () => { // @ts-expect-error expect(r0[0].books[0].price).toBeUndefined(); expect(r0[0].books[0].author).toBeDefined(); - expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id` from `author2` as `a0` where `a0`.`id` = ?'); - expect(mock.mock.calls[1][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`author_id`, `b0`.`title` from `book2` as `b0` where `b0`.`author_id` is not null and `b0`.`author_id` in (?) order by `b0`.`title` asc'); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title`, `b1`.`author_id` as `b1__author_id` from `author2` as `a0` left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null where `a0`.`id` = ? order by `b1`.`title` asc'); orm.em.clear(); mock.mock.calls.length = 0; @@ -181,8 +178,7 @@ describe('partial loading (mysql)', () => { expect(r00[0].books[0].price).toBeUndefined(); // @ts-expect-error expect(r00[0].books[0].author).toBeDefined(); - expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id` from `author2` as `a0` where `a0`.`id` = ?'); - expect(mock.mock.calls[1][0]).toMatch('select `b0`.`uuid_pk`, `b0`.`title`, `b0`.`author_id` from `book2` as `b0` where `b0`.`author_id` is not null and `b0`.`author_id` in (?) order by `b0`.`title` asc'); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.`id`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title` from `author2` as `a0` left join `book2` as `b1` on `a0`.`id` = `b1`.`author_id` and `b1`.`author_id` is not null where `a0`.`id` = ? order by `b1`.`title` asc'); }); test('partial nested loading (m:1)', async () => { @@ -194,6 +190,7 @@ describe('partial loading (mysql)', () => { fields: ['uuid', 'title', 'author.email'], populate: ['author'], filters: false, + strategy: LoadStrategy.SELECT_IN, }); expect(r1).toHaveLength(1); expect(r1[0].uuid).toBe(b1.uuid); @@ -214,6 +211,7 @@ describe('partial loading (mysql)', () => { fields: ['uuid', 'title', 'author.email'], populate: ['author'], filters: false, + strategy: LoadStrategy.SELECT_IN, }); expect(r2).toHaveLength(1); expect(r2[0].uuid).toBe(b1.uuid); @@ -241,8 +239,7 @@ describe('partial loading (mysql)', () => { expect(r1[0].books[0].price).toBeUndefined(); // @ts-expect-error expect(r1[0].books[0].author).toBeUndefined(); - expect(mock.mock.calls[0][0]).toMatch('select `b0`.`id`, `b0`.`name` from `book_tag2` as `b0`'); - expect(mock.mock.calls[1][0]).toMatch('select `b1`.`title`, `b1`.`uuid_pk`, `b0`.`book_tag2_id` as `fk__book_tag2_id`, `b0`.`book2_uuid_pk` as `fk__book2_uuid_pk`, `b2`.`id` as `test_id` from `book2_tags` as `b0` inner join `book2` as `b1` on `b0`.`book2_uuid_pk` = `b1`.`uuid_pk` left join `test2` as `b2` on `b1`.`uuid_pk` = `b2`.`book_uuid_pk` where `b0`.`book_tag2_id` in (?, ?, ?, ?, ?, ?) order by `b0`.`order` asc'); + expect(mock.mock.calls[0][0]).toMatch('select `b0`.`id`, `b0`.`name`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title` from `book_tag2` as `b0` left join `book2_tags` as `b2` on `b0`.`id` = `b2`.`book_tag2_id` left join `book2` as `b1` on `b2`.`book2_uuid_pk` = `b1`.`uuid_pk` order by `b2`.`order` asc'); orm.em.clear(); mock.mock.calls.length = 0; @@ -254,11 +251,10 @@ describe('partial loading (mysql)', () => { expect(r2[0].books[0].price).toBeUndefined(); // @ts-expect-error expect(r2[0].books[0].author).toBeUndefined(); - expect(mock.mock.calls[0][0]).toMatch('select `b0`.`id`, `b0`.`name` from `book_tag2` as `b0` where `b0`.`name` = ?'); - expect(mock.mock.calls[1][0]).toMatch('select `b1`.`title`, `b1`.`uuid_pk`, `b0`.`book_tag2_id` as `fk__book_tag2_id`, `b0`.`book2_uuid_pk` as `fk__book2_uuid_pk`, `b2`.`id` as `test_id` from `book2_tags` as `b0` inner join `book2` as `b1` on `b0`.`book2_uuid_pk` = `b1`.`uuid_pk` left join `test2` as `b2` on `b1`.`uuid_pk` = `b2`.`book_uuid_pk` where `b0`.`book_tag2_id` in (?) order by `b0`.`order` asc'); + expect(mock.mock.calls[0][0]).toMatch('select `b0`.`id`, `b0`.`name`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title` from `book_tag2` as `b0` left join `book2_tags` as `b2` on `b0`.`id` = `b2`.`book_tag2_id` left join `book2` as `b1` on `b2`.`book2_uuid_pk` = `b1`.`uuid_pk` where `b0`.`name` = ? order by `b2`.`order` asc'); }); - test('partial nested loading (dot notation)', async () => { + test('partial nested loading (dot notation, select-in)', async () => { const god = await createEntities(); const mock = mockLogger(orm, ['query']); @@ -266,6 +262,7 @@ describe('partial loading (mysql)', () => { fields: ['name', 'books.title', 'books.author', 'books.author.email'], populate: ['books.author'], filters: false, + strategy: LoadStrategy.SELECT_IN, }); expect(r1).toHaveLength(6); expect(r1[0].name).toBe('t1'); @@ -290,7 +287,6 @@ describe('partial loading (mysql)', () => { fields: ['name', 'books.title', 'books.author', 'books.author.email'], populate: ['books.author'], filters: false, - strategy: LoadStrategy.JOINED, }); expect(r3).toHaveLength(6); expect(r3[0].name).toBe('t1'); @@ -317,7 +313,6 @@ describe('partial loading (mysql)', () => { fields: ['*', 'books.title', 'books.author', 'books.author.email'], populate: ['books.author'], filters: false, - strategy: LoadStrategy.JOINED, }); expect(r2).toHaveLength(6); expect(mock.mock.calls).toHaveLength(1); @@ -331,13 +326,14 @@ describe('partial loading (mysql)', () => { 'order by `b2`.`order` asc'); }); - test('partial nested loading (object notation)', async () => { + test('partial nested loading (object notation, select-in)', async () => { const god = await createEntities(); const mock = mockLogger(orm, ['query']); const r2 = await orm.em.find(BookTag2, {}, { fields: ['name', 'books.title', 'books.author', 'books.author.email'], filters: false, + strategy: LoadStrategy.SELECT_IN, }); expect(r2).toHaveLength(6); expect(r2[0].name).toBe('t1'); @@ -361,7 +357,6 @@ describe('partial loading (mysql)', () => { const r3 = await orm.em.find(BookTag2, {}, { fields: ['name', 'books.title', 'books.author.email'], filters: false, - strategy: LoadStrategy.JOINED, }); expect(r3).toHaveLength(6); expect(r3[0].name).toBe('t1'); @@ -381,11 +376,6 @@ describe('partial loading (mysql)', () => { 'left join `book2_tags` as `b2` on `b0`.`id` = `b2`.`book_tag2_id` ' + 'left join `book2` as `b1` on `b2`.`book2_uuid_pk` = `b1`.`uuid_pk` ' + 'left join `author2` as `a3` on `b1`.`author_id` = `a3`.`id`'); - - // Expected substring: " - // select `b0`.`id`, `b0`.`name`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title`, `b1`.`author_id` as `b1__author_id`, `a3`.`id` as `a3__id`, `a3`.`email` as `a3__email` from `book_tag2` as `b0` left join `book2_tags` as `b2` on `b0`.`id` = `b2`.`book_tag2_id` left join `book2` as `b1` on `b2`.`book2_uuid_pk` = `b1`.`uuid_pk` left join `author2` as `a3` on `b1`.`author_id` = `a3`.`id`" - // Received string: " - // select `b0`.`id`, `b0`.`name`, `b1`.`uuid_pk` as `b1__uuid_pk`, `b1`.`title` as `b1__title`, `a3`.`id` as `a3__id`, `a3`.`email` as `a3__email` from `book_tag2` as `b0` left join `book2_tags` as `b2` on `b0`.`id` = `b2`.`book_tag2_id` left join `book2` as `b1` on `b2`.`book2_uuid_pk` = `b1`.`uuid_pk` left join `author2` as `a3` on `b1`.`author_id` = `a3`.`id` [took 7 ms] (via read connection 'read-1')" }); }); 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 4a0ec155cf77..55e2e4a4ec5e 100644 --- a/tests/features/sharing-column-in-composite-pk-fk.test.ts +++ b/tests/features/sharing-column-in-composite-pk-fk.test.ts @@ -233,8 +233,7 @@ test('shared column as composite PK and FK in M:N', async () => { await orm.em.flush(); expect(mock.mock.calls).toEqual([ - [`[query] select "o0".* from "order" as "o0" where "o0"."id" = 'd09f1159-c5b0-4336-bfed-2543b5422ba7' limit 1`], - [`[query] select "p1".*, "o0"."product_id" as "fk__product_id", "o0"."organization_id" as "fk__organization_id", "o0"."order_id" as "fk__order_id", "o0"."organization_id" as "fk__organization_id" from "order_item" as "o0" inner join "public"."product" as "p1" on "o0"."product_id" = "p1"."id" and "o0"."organization_id" = "p1"."organization_id" where ("o0"."order_id", "o0"."organization_id") in (('d09f1159-c5b0-4336-bfed-2543b5422ba7', 'a900a4da-c464-4bd4-88a3-e41e1d33dc2e'))`], + [`[query] select "o0".*, "p1"."id" as "p1__id", "p1"."organization_id" as "p1__organization_id" from "order" as "o0" left join "order_item" as "o2" on "o0"."id" = "o2"."order_id" and "o0"."organization_id" = "o2"."organization_id" left join "product" as "p1" on "o2"."product_id" = "p1"."id" and "o2"."organization_id" = "p1"."organization_id" where "o0"."id" = 'd09f1159-c5b0-4336-bfed-2543b5422ba7'`], ['[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'`], diff --git a/tests/features/single-table-inheritance/GH4423.test.ts b/tests/features/single-table-inheritance/GH4423.test.ts index 4dde9a3fcd24..7e76b094ba03 100644 --- a/tests/features/single-table-inheritance/GH4423.test.ts +++ b/tests/features/single-table-inheritance/GH4423.test.ts @@ -83,30 +83,18 @@ describe('GH issue 4423', () => { test('The owning side is in the main entity, This one chooses the wrong column of the pivot table', async () => { const mock = mockLogger(orm); - await orm.em.find( - Task, - {}, - { - populate: ['managers'], - }, - ); - expect(mock.mock.calls[1][0]).toMatch( - "select `u1`.*, `t0`.`manager_id` as `fk__manager_id`, `t0`.`task_id` as `fk__task_id` from `task_managers` as `t0` inner join `user` as `u1` on `t0`.`manager_id` = `u1`.`id` and `u1`.`type` = 'manager' where `t0`.`task_id` in (1)", - ); + await orm.em.findAll(Task, { + populate: ['managers'], + }); + expect(mock.mock.calls[0][0]).toMatch("select `t0`.*, `m1`.`id` as `m1__id`, `m1`.`name` as `m1__name`, `m1`.`type` as `m1__type`, `m1`.`favorite_task_id` as `m1__favorite_task_id` from `task` as `t0` left join `task_managers` as `t2` on `t0`.`id` = `t2`.`task_id` left join `user` as `m1` on `t2`.`manager_id` = `m1`.`id` and `m1`.`type` = 'manager'"); }); test('The owning side is in the relation, This one works normally', async () => { const mock = mockLogger(orm); - await orm.em.find( - Manager, - {}, - { - populate: ['tasks'], - }, - ); - - expect(mock.mock.calls[1][0]).toMatch( - 'select `t1`.*, `t0`.`manager_id` as `fk__manager_id`, `t0`.`task_id` as `fk__task_id` from `task_managers` as `t0` inner join `task` as `t1` on `t0`.`task_id` = `t1`.`id` where `t0`.`manager_id` in (1)', - ); + await orm.em.findAll(Manager, { + populate: ['tasks'], + }); + + expect(mock.mock.calls[0][0]).toMatch("select `m0`.*, `t1`.`id` as `t1__id`, `t1`.`name` as `t1__name` from `user` as `m0` left join `task_managers` as `t2` on `m0`.`id` = `t2`.`manager_id` left join `task` as `t1` on `t2`.`task_id` = `t1`.`id` where `m0`.`type` = 'manager'"); }); }); diff --git a/tests/issues/GH1657.test.ts b/tests/issues/GH1657.test.ts index e85d22bc3158..7e8b886580b2 100644 --- a/tests/issues/GH1657.test.ts +++ b/tests/issues/GH1657.test.ts @@ -27,6 +27,7 @@ class OrderItem { @ManyToOne({ entity: () => Order, + strategy: LoadStrategy.SELECT_IN, eager: true, nullable: true, }) @@ -34,7 +35,6 @@ class OrderItem { @ManyToOne({ entity: () => Order, - strategy: LoadStrategy.JOINED, eager: true, nullable: true, }) diff --git a/tests/issues/GH1882.test.ts b/tests/issues/GH1882.test.ts index 3879a38f2d3e..035df8cae870 100644 --- a/tests/issues/GH1882.test.ts +++ b/tests/issues/GH1882.test.ts @@ -59,12 +59,12 @@ describe('GH issue 1882', () => { const mock = mockLogger(orm, ['query']); const cond = { $or: [{ barItems: '5678' }, { name: 'fooName' }] }; - await orm.em.fork().find(Foo, cond, { populate: ['barItems'], populateWhere: PopulateHint.INFER }); + await orm.em.fork().find(Foo, cond, { populate: ['barItems'], populateWhere: PopulateHint.INFER, strategy: 'select-in' }); expect(mock.mock.calls[0][0]).toMatch('select `f0`.* from `foo` as `f0` left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` where (`b1`.`id` = ? or `f0`.`name` = ?)'); expect(mock.mock.calls[1][0]).toMatch('select `b0`.* from `bar` as `b0` where `b0`.`foo_id` in (?) and `b0`.`id` = ?'); mock.mockReset(); - await orm.em.fork().find(Foo, cond, { populate: ['barItems'] }); + await orm.em.fork().find(Foo, cond, { populate: ['barItems'], strategy: 'select-in' }); expect(mock.mock.calls[0][0]).toMatch('select `f0`.* from `foo` as `f0` left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` where (`b1`.`id` = ? or `f0`.`name` = ?)'); expect(mock.mock.calls[1][0]).toMatch('select `b0`.* from `bar` as `b0` where `b0`.`foo_id` in (?)'); }); @@ -85,18 +85,88 @@ describe('GH issue 1882', () => { const mock = mockLogger(orm, ['query']); const cond = { $and: [{ barItems: '3456' }] }; - await orm.em.find(Foo, cond, { populate: ['barItems'], populateWhere: PopulateHint.INFER }); + await orm.em.find(Foo, cond, { populate: ['barItems'], populateWhere: PopulateHint.INFER, strategy: 'select-in' }); orm.em.clear(); expect(mock.mock.calls[0][0]).toMatch('select `f0`.* from `foo` as `f0` left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` where `b1`.`id` = ?'); expect(mock.mock.calls[1][0]).toMatch('select `b0`.* from `bar` as `b0` where `b0`.`foo_id` in (?) and `b0`.`id` = ?'); mock.mockReset(); - await orm.em.find(Foo, cond, { populate: ['barItems'] }); + await orm.em.find(Foo, cond, { populate: ['barItems'], strategy: 'select-in' }); orm.em.clear(); expect(mock.mock.calls[0][0]).toMatch('select `f0`.* from `foo` as `f0` left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` where `b1`.`id` = ?'); expect(mock.mock.calls[1][0]).toMatch('select `b0`.* from `bar` as `b0` where `b0`.`foo_id` in (?)'); }); + test(`GH issue 1882-3`, async () => { + const fooItem = new Foo(); + fooItem.id = 12345n; + fooItem.name = 'fooName'; + + const barItem = new Bar(); + barItem.id = 56789n; + barItem.name = 'barName1'; + barItem.foo = fooItem; + + await orm.em.persistAndFlush([barItem]); + orm.em.clear(); + + const mock = mockLogger(orm, ['query']); + const cond = { $or: [{ barItems: '5678' }, { name: 'fooName' }] }; + + await orm.em.fork().find(Foo, cond, { populate: ['barItems'], populateWhere: PopulateHint.INFER, strategy: 'joined' }); + expect(mock.mock.calls[0][0]).toMatch('select `f0`.*, ' + + '`b1`.`id` as `b1__id`, `b1`.`foo_id` as `b1__foo_id`, `b1`.`name` as `b1__name` ' + + 'from `foo` as `f0` ' + + 'left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` ' + + 'where (`b1`.`id` = ? or `f0`.`name` = ?)'); + mock.mockReset(); + + await orm.em.fork().find(Foo, cond, { populate: ['barItems'], strategy: 'joined' }); + expect(mock.mock.calls[0][0]).toMatch('select `f0`.*, ' + + '`b1`.`id` as `b1__id`, `b1`.`foo_id` as `b1__foo_id`, `b1`.`name` as `b1__name` ' + + 'from `foo` as `f0` ' + + 'left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` ' + + 'left join `bar` as `b2` on `f0`.`id` = `b2`.`foo_id` ' + + 'where (`b2`.`id` = ? or `f0`.`name` = ?)'); + }); + + test(`GH issue 1882-4`, async () => { + const fooItem = new Foo(); + fooItem.id = 90121n; + fooItem.name = 'fooName'; + + const barItem = new Bar(); + barItem.id = 34561n; + barItem.name = 'barName'; + barItem.foo = fooItem; + + await orm.em.persistAndFlush([barItem]); + orm.em.clear(); + + const mock = mockLogger(orm, ['query']); + const cond = { $and: [{ barItems: '3456' }] }; + + await orm.em.find(Foo, cond, { populate: ['barItems'], populateWhere: PopulateHint.INFER, strategy: 'joined' }); + orm.em.clear(); + + expect(mock.mock.calls[0][0]).toMatch('select `f0`.*, ' + + '`b1`.`id` as `b1__id`, `b1`.`foo_id` as `b1__foo_id`, `b1`.`name` as `b1__name` ' + + 'from `foo` as `f0` ' + + 'left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` ' + + 'where `b1`.`id` = ?'); + mock.mockReset(); + + await orm.em.find(Foo, cond, { populate: ['barItems'], strategy: 'joined' }); + orm.em.clear(); + + expect(mock.mock.calls[0][0]).toMatch('select `f0`.*, ' + + '`b1`.`id` as `b1__id`, `b1`.`foo_id` as `b1__foo_id`, `b1`.`name` as `b1__name` ' + + 'from `foo` as `f0` ' + + 'left join `bar` as `b1` on `f0`.`id` = `b1`.`foo_id` ' + + 'left join `bar` as `b2` on `f0`.`id` = `b2`.`foo_id` ' + + 'where `b2`.`id` = ?'); + }); + }); diff --git a/tests/issues/GH234.test.ts b/tests/issues/GH234.test.ts index c2692392ac50..0de015e3bda2 100644 --- a/tests/issues/GH234.test.ts +++ b/tests/issues/GH234.test.ts @@ -38,51 +38,80 @@ describe('GH issue 234', () => { entities: [A, B], dbName: ':memory:', }); - await orm.schema.dropSchema(); await orm.schema.createSchema(); - }); - - afterAll(() => orm.close(true)); - test('search by m:n', async () => { const a1 = new A(); + a1.id = 1; a1.name = 'a1'; const a2 = new A(); + a2.id = 2; a2.name = 'a2'; const a3 = new A(); + a3.id = 3; a3.name = 'a3'; const b = new B(); + b.id = 1; b.name = 'b'; b.aCollection.add(a1, a2, a3); - await orm.em.persistAndFlush(b); - orm.em.clear(); + await orm.em.fork().persistAndFlush(b); + }); + + afterAll(() => orm.close(true)); + test('search by m:n (select-in)', async () => { const mock = mockLogger(orm, ['query']); - const res1 = await orm.em.find(B, { aCollection: [1, 2, 3] }, { populate: ['aCollection'], populateWhere: PopulateHint.INFER }); + const res1 = await orm.em.find(B, { aCollection: [1, 2, 3] }, { populate: ['aCollection'], strategy: 'select-in', populateWhere: PopulateHint.INFER }); expect(mock.mock.calls[0][0]).toMatch('select `b0`.* from `b` as `b0` left join `b_a_collection` as `b1` on `b0`.`id` = `b1`.`b_id` where `b1`.`a_id` in (?, ?, ?)'); expect(mock.mock.calls[1][0]).toMatch('select `a1`.*, `b0`.`a_id` as `fk__a_id`, `b0`.`b_id` as `fk__b_id` from `b_a_collection` as `b0` inner join `a` as `a1` on `b0`.`a_id` = `a1`.`id` where `a1`.`id` in (?, ?, ?) and `b0`.`b_id` in (?) order by `b0`.`id` asc'); - expect(res1.map(b => b.id)).toEqual([b.id]); + expect(res1.map(b => b.id)).toEqual([1]); orm.em.clear(); mock.mock.calls.length = 0; - const res2 = await orm.em.find(A, { bCollection: [1, 2, 3] }, { populate: ['bCollection'], populateWhere: PopulateHint.INFER }); + const res2 = await orm.em.find(A, { bCollection: [1, 2, 3] }, { populate: ['bCollection'], strategy: 'select-in', populateWhere: PopulateHint.INFER }); expect(mock.mock.calls[0][0]).toMatch('select `a0`.* from `a` as `a0` left join `b_a_collection` as `b1` on `a0`.`id` = `b1`.`a_id` where `b1`.`b_id` in (?, ?, ?)'); expect(mock.mock.calls[1][0]).toMatch('select `b1`.*, `b0`.`a_id` as `fk__a_id`, `b0`.`b_id` as `fk__b_id` from `b_a_collection` as `b0` inner join `b` as `b1` on `b0`.`b_id` = `b1`.`id` where `b1`.`id` in (?, ?, ?) and `b0`.`a_id` in (?, ?, ?) order by `b0`.`id` asc'); - expect(res2.map(a => a.id)).toEqual([a1.id, a2.id, a3.id]); + expect(res2.map(a => a.id)).toEqual([1, 2, 3]); orm.em.clear(); mock.mock.calls.length = 0; - const res3 = await orm.em.find(B, { aCollection: [1, 2, 3] }, { populate: ['aCollection'] }); + const res3 = await orm.em.find(B, { aCollection: [1, 2, 3] }, { populate: ['aCollection'], strategy: 'select-in' }); expect(mock.mock.calls[0][0]).toMatch('select `b0`.* from `b` as `b0` left join `b_a_collection` as `b1` on `b0`.`id` = `b1`.`b_id` where `b1`.`a_id` in (?, ?, ?)'); expect(mock.mock.calls[1][0]).toMatch('select `a1`.*, `b0`.`a_id` as `fk__a_id`, `b0`.`b_id` as `fk__b_id` from `b_a_collection` as `b0` inner join `a` as `a1` on `b0`.`a_id` = `a1`.`id` where `b0`.`b_id` in (?) order by `b0`.`id` asc'); - expect(res3.map(b => b.id)).toEqual([b.id]); + expect(res3.map(b => b.id)).toEqual([1]); orm.em.clear(); mock.mock.calls.length = 0; - const res4 = await orm.em.find(A, { bCollection: [1, 2, 3] }, { populate: ['bCollection'] }); + const res4 = await orm.em.find(A, { bCollection: [1, 2, 3] }, { populate: ['bCollection'], strategy: 'select-in' }); expect(mock.mock.calls[0][0]).toMatch('select `a0`.* from `a` as `a0` left join `b_a_collection` as `b1` on `a0`.`id` = `b1`.`a_id` where `b1`.`b_id` in (?, ?, ?)'); expect(mock.mock.calls[1][0]).toMatch('select `b1`.*, `b0`.`a_id` as `fk__a_id`, `b0`.`b_id` as `fk__b_id` from `b_a_collection` as `b0` inner join `b` as `b1` on `b0`.`b_id` = `b1`.`id` where `b0`.`a_id` in (?, ?, ?) order by `b0`.`id` asc'); - expect(res4.map(a => a.id)).toEqual([a1.id, a2.id, a3.id]); + expect(res4.map(a => a.id)).toEqual([1, 2, 3]); + orm.em.clear(); + mock.mock.calls.length = 0; + }); + + test('search by m:n (joined)', async () => { + const mock = mockLogger(orm, ['query']); + const res1 = await orm.em.find(B, { aCollection: [1, 2, 3] }, { populate: ['aCollection'], populateWhere: PopulateHint.INFER }); + expect(mock.mock.calls[0][0]).toMatch('select `b0`.*, `a1`.`id` as `a1__id`, `a1`.`name` as `a1__name` from `b` as `b0` left join `b_a_collection` as `b2` on `b0`.`id` = `b2`.`b_id` and `b2`.`a_id` in (?, ?, ?) left join `a` as `a1` on `b2`.`a_id` = `a1`.`id` where `b2`.`a_id` in (?, ?, ?) order by `b2`.`id` asc'); + expect(res1.map(b => b.id)).toEqual([1]); + + orm.em.clear(); + mock.mock.calls.length = 0; + const res2 = await orm.em.find(A, { bCollection: [1, 2, 3] }, { populate: ['bCollection'], populateWhere: PopulateHint.INFER }); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`id` as `b1__id`, `b1`.`name` as `b1__name` from `a` as `a0` left join `b_a_collection` as `b2` on `a0`.`id` = `b2`.`a_id` and `b2`.`b_id` in (?, ?, ?) left join `b` as `b1` on `b2`.`b_id` = `b1`.`id` where `b2`.`b_id` in (?, ?, ?) order by `b2`.`id` asc'); + expect(res2.map(a => a.id)).toEqual([1, 2, 3]); + orm.em.clear(); + mock.mock.calls.length = 0; + + const res3 = await orm.em.find(B, { aCollection: [1, 2, 3] }, { populate: ['aCollection'], strategy: 'select-in' }); + expect(mock.mock.calls[0][0]).toMatch('select `b0`.* from `b` as `b0` left join `b_a_collection` as `b1` on `b0`.`id` = `b1`.`b_id` where `b1`.`a_id` in (?, ?, ?)'); + expect(res3.map(b => b.id)).toEqual([1]); + + orm.em.clear(); + mock.mock.calls.length = 0; + const res4 = await orm.em.find(A, { bCollection: [1, 2, 3] }, { populate: ['bCollection'] }); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`id` as `b1__id`, `b1`.`name` as `b1__name` from `a` as `a0` left join `b_a_collection` as `b2` on `a0`.`id` = `b2`.`a_id` left join `b` as `b1` on `b2`.`b_id` = `b1`.`id` left join `b_a_collection` as `b3` on `a0`.`id` = `b3`.`a_id` where `b3`.`b_id` in (?, ?, ?) order by `b2`.`id` asc'); + expect(res4.map(a => a.id)).toEqual([1, 2, 3]); orm.em.clear(); mock.mock.calls.length = 0; }); diff --git a/tests/issues/GH2647.test.ts b/tests/issues/GH2647.test.ts index 5bbd058936e0..cf969531e72c 100644 --- a/tests/issues/GH2647.test.ts +++ b/tests/issues/GH2647.test.ts @@ -111,30 +111,68 @@ describe('GH #2647, #2742', () => { return { provider, user, member, session, participant }; } - it('should be able to populate circularity', async () => { + it('should be able to populate circularity (select-in)', async () => { const { session, member } = createEntities([1, 2, 3]); await orm.em.flush(); orm.em.clear(); - const res = await orm.em.find(Participant, { session, member }); + const res = await orm.em.find(Participant, { session, member }, { strategy: 'select-in' }); expect(res).toHaveLength(1); expect(res[0]).toBe(res[0].session.lastActionBy); - await orm.em.getUnitOfWork().computeChangeSets(); + orm.em.getUnitOfWork().computeChangeSets(); expect(orm.em.getUnitOfWork().getChangeSets()).toHaveLength(0); }); - it('should be able to find entity with nested composite key (multi insert)', async () => { + it('should be able to find entity with nested composite key (multi insert, select-in)', async () => { createEntities([11, 12, 13]); createEntities([21, 22, 23]); createEntities([31, 32, 33]); await orm.em.flush(); orm.em.clear(); - const res = await orm.em.find(Participant, {}); + const res = await orm.em.find(Participant, {}, { strategy: 'select-in' }); expect(res).toHaveLength(3); expect(res[0]).toBe(res[0].session.lastActionBy); expect(res[1]).toBe(res[1].session.lastActionBy); expect(res[2]).toBe(res[2].session.lastActionBy); }); + it('should be able to populate circularity (joined)', async () => { + const { session, member } = createEntities([1, 2, 3]); + await orm.em.flush(); + orm.em.clear(); + + const res = await orm.em.find(Participant, { session, member }, { strategy: 'joined' }); + expect(res).toHaveLength(1); + expect(res[0]).toBe(res[0].session.lastActionBy); + orm.em.getUnitOfWork().computeChangeSets(); + expect(orm.em.getUnitOfWork().getChangeSets()).toHaveLength(0); + }); + + it('should be able to find entity with nested composite key (multi insert, joined)', async () => { + createEntities([11, 12, 13]); + createEntities([21, 22, 23]); + createEntities([31, 32, 33]); + await orm.em.flush(); + orm.em.clear(); + + const res = await orm.em.find(Participant, {}, { strategy: 'joined' }); + expect(res).toHaveLength(3); + expect(res[0]).toBe(res[0].session.lastActionBy); + expect(res[1]).toBe(res[1].session.lastActionBy); + expect(res[2]).toBe(res[2].session.lastActionBy); + }); + + test('creating entity instance from POJO', async () => { + const participant = orm.em.getEntityFactory().create(Participant, { + session: { + id: 3, + owner: { provider: { id: 1 }, user: { id: 2 } }, + lastActionBy: [3, [1, 2]], + }, + member: { provider: { id: 1 }, user: { id: 2 } }, + }, { merge: true, newEntity: false }); + expect(participant).toBe(participant.session.lastActionBy); + }); + }); diff --git a/tests/issues/GH2916.test.ts b/tests/issues/GH2916.test.ts index b83d8d10044a..374648666128 100644 --- a/tests/issues/GH2916.test.ts +++ b/tests/issues/GH2916.test.ts @@ -142,7 +142,8 @@ test('1:m sub-query operators $some, $none and $every', async () => { expect(mock.mock.calls[4][0]).toBe('[query] select `a0`.* from `author` as `a0` where `a0`.`id` not in (select `a0`.`id` from `author` as `a0` inner join `book` as `b1` on `a0`.`id` = `b1`.`author_id`)'); }); -test('m:n sub-query operators $some, $none and $every', async () => { +test('m:n sub-query operators $some, $none and $every (select-in)', async () => { + orm.config.set('loadStrategy', 'select-in'); const mock = mockLogger(orm); let results = await orm.em.fork().find(Book, { @@ -207,6 +208,68 @@ test('m:n sub-query operators $some, $none and $every', async () => { 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'); }); +test('m:n sub-query operators $some, $none and $every (joined)', async () => { + orm.config.set('loadStrategy', 'joined'); + const mock = mockLogger(orm); + + let results = await orm.em.fork().find(Book, { + tags: { $some: { name: ['t1', 't2'] } }, + }, { populate: ['tags'] }); + expect(results.map(res => res.tags.getIdentifiers('name'))).toEqual([ + ['t1', 't5'], + ['t1', 't3'], + ['t1', 't2'], + ['t1', 't2', 't4'], + ['t2'], + ['t2'], + ['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'))"); + + results = await orm.em.fork().find(Book, { + tags: { $none: { name: ['t1', 't2'] } }, + }, { populate: ['tags'], orderBy: { tags: { name: 1 } } }); + 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"); + + results = await orm.em.fork().find(Book, { + tags: { $every: { name: ['t1', 't2'] } }, + }, { populate: ['tags'], orderBy: { tags: { name: 1 } } }); + expect(results.map(res => res.tags.getIdentifiers('name'))).toEqual([ + ['t1', 't2'], + ['t2'], + ['t2'], + ['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"); + + results = await orm.em.fork().find(Book, { + tags: { $some: {} }, + }, { populate: ['tags'], orderBy: { tags: { name: 1 } } }); + expect(results.map(res => res.tags.getIdentifiers('name'))).toEqual([ + ['t1', 't5'], + ['t1', 't3'], + ['t1', 't2'], + ['t1', 't2', 't4'], + ['t2'], + ['t2'], + ['t2'], + ['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'); + + 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'); +}); + test('allows only one of $some, $none and $every on the given level', async () => { const mock = mockLogger(orm); let results = await orm.em.fork().find(Author, { diff --git a/tests/issues/GH4843.test.ts b/tests/issues/GH4843.test.ts index 1fdefdc6fb0e..d4190a168e43 100644 --- a/tests/issues/GH4843.test.ts +++ b/tests/issues/GH4843.test.ts @@ -92,7 +92,23 @@ describe('populate with citext', () => { afterAll(() => orm.close()); - test('composite FK', async () => { + test('composite FK (select-in)', async () => { + const a1 = await orm.em.fork().find(A, ['test1', 'test2'], { populate: ['c'], strategy: 'select-in' }); + expect(a1[0].c.$.toArray()).toEqual([{ id: 1, a: { id1: 'test1', id2: 'test2' }, b: 'test3' }]); + + const a2 = await orm.em.fork().find(A, ['asdf', 'test'], { populate: ['c'], strategy: 'select-in' }); + expect(a2[0].c.$.toArray()).toEqual([{ id: 2, a: { id1: 'asdf', id2: 'test' }, b: 'test' }]); + }); + + test('simple FK (select-in)', async () => { + const b1 = await orm.em.fork().find(B, 'test3', { populate: ['c'], strategy: 'select-in' }); + expect(b1[0].c.$.toArray()).toEqual([{ id: 1, a: { id1: 'test1', id2: 'test2' }, b: 'test3' }]); + + const b2 = await orm.em.fork().find(B, 'test', { populate: ['c'], strategy: 'select-in' }); + expect(b2[0].c.$.toArray()).toEqual([{ id: 2, a: { id1: 'asdf', id2: 'test' }, b: 'test' }]); + }); + + test('composite FK (joined)', async () => { const a1 = await orm.em.fork().find(A, ['test1', 'test2'], { populate: ['c'] }); expect(a1[0].c.$.toArray()).toEqual([{ id: 1, a: { id1: 'test1', id2: 'test2' }, b: 'test3' }]); @@ -100,7 +116,7 @@ describe('populate with citext', () => { expect(a2[0].c.$.toArray()).toEqual([{ id: 2, a: { id1: 'asdf', id2: 'test' }, b: 'test' }]); }); - test('simple FK', async () => { + test('simple FK (joined)', async () => { const b1 = await orm.em.fork().find(B, 'test3', { populate: ['c'] }); expect(b1[0].c.$.toArray()).toEqual([{ id: 1, a: { id1: 'test1', id2: 'test2' }, b: 'test3' }]); diff --git a/tests/issues/GH572.test.ts b/tests/issues/GH572.test.ts index fa088f975828..7f0a48d56b49 100644 --- a/tests/issues/GH572.test.ts +++ b/tests/issues/GH572.test.ts @@ -35,7 +35,6 @@ describe('GH issue 572', () => { entities: [A, B], dbName: ':memory:', }); - await orm.schema.dropSchema(); await orm.schema.createSchema(); }); @@ -47,7 +46,7 @@ describe('GH issue 572', () => { orderBy: { b: { camelCaseField: QueryOrder.ASC } }, populate: ['b'], }); - expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`id` as `b_id` from `a` as `a0` left join `b` as `b1` on `a0`.`id` = `b1`.`a_id` order by `b1`.`camel_case_field` asc'); + expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`id` as `b1__id`, `b1`.`camel_case_field` as `b1__camel_case_field`, `b1`.`a_id` as `b1__a_id`, `b1`.`id` as `b_id` from `a` as `a0` left join `b` as `b1` on `a0`.`id` = `b1`.`a_id` order by `b1`.`camel_case_field` asc'); expect(res1).toHaveLength(0); const qb1 = orm.em.createQueryBuilder(A, 'a').select('a.*').orderBy({ b: { camelCaseField: QueryOrder.ASC } }); expect(qb1.getQuery()).toMatch('select `a`.* from `a` as `a` left join `b` as `b1` on `a`.`id` = `b1`.`a_id` order by `b1`.`camel_case_field` asc');