Skip to content

Commit

Permalink
fix(core): ignore collection operators in populateWhere conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Mar 2, 2024
1 parent b6efd44 commit 7b6b363
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 11 deletions.
4 changes: 2 additions & 2 deletions packages/knex/src/query/ArrayCriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export class ArrayCriteriaNode<T extends object> extends CriteriaNode<T> {
});
}

override willAutoJoin(qb: IQueryBuilder<T>, alias?: string) {
override willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions) {
return this.payload.some((node: CriteriaNode<T>) => {
return node.willAutoJoin(qb, alias);
return node.willAutoJoin(qb, alias, options);
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/query/CriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class CriteriaNode<T extends object> implements ICriteriaNode<T> {
return false;
}

willAutoJoin(qb: IQueryBuilder<T>, alias?: string) {
willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions) {
return false;
}

Expand Down
19 changes: 15 additions & 4 deletions packages/knex/src/query/ObjectCriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {

if (this.shouldAutoJoin(qb, nestedAlias)) {
if (keys.some(k => ['$some', '$none', '$every'].includes(k))) {
// ignore collection operators when used on a non-relational property - this can happen when they get into
// populateWhere via `infer` on m:n properties with select-in strategy
if (!this.prop?.targetMeta) {
return {};
}

const $and: Dictionary[] = [];
const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop!.kind) || (this.prop!.kind === ReferenceKind.ONE_TO_ONE && this.prop!.owner);
const primaryKeys = this.metadata.find(this.entityName)!.primaryKeys.map(pk => {
return [QueryType.SELECT, QueryType.COUNT].includes(qb.type!) ? `${alias}.${pk}` : pk;
return [QueryType.SELECT, QueryType.COUNT].includes(qb.type!) ? `${knownKey ? alias : ownerAlias}.${pk}` : pk;
});

for (const key of keys) {
Expand Down Expand Up @@ -100,8 +107,8 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
}, {} as Dictionary);
}

override willAutoJoin(qb: IQueryBuilder<T>, alias?: string) {
const nestedAlias = qb.getAliasForJoinPath(this.getPath());
override willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions) {
const nestedAlias = qb.getAliasForJoinPath(this.getPath(), options);
const ownerAlias = alias || qb.alias;
const keys = Object.keys(this.payload);

Expand All @@ -115,7 +122,7 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {

return keys.some(field => {
const childNode = this.payload[field] as CriteriaNode<T>;
return childNode.willAutoJoin(qb, this.prop ? alias : ownerAlias);
return childNode.willAutoJoin(qb, this.prop ? alias : ownerAlias, options);
});
}

Expand Down Expand Up @@ -173,6 +180,10 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
return false;
}

if (keys.some(k => ['$some', '$none', '$every'].includes(k))) {
return true;
}

const meta = this.metadata.find(this.entityName)!;
const embeddable = this.prop.kind === ReferenceKind.EMBEDDED;
const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) || (this.prop.kind === ReferenceKind.ONE_TO_ONE && this.prop.owner);
Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/query/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export class QueryBuilder<T extends object = AnyEntity> {
const criteriaNode = CriteriaNodeFactory.createNode<T>(this.metadata, this.mainAlias.entityName, cond);
const ignoreBranching = this.__populateWhere === 'infer';

if ([QueryType.UPDATE, QueryType.DELETE].includes(this.type!) && criteriaNode.willAutoJoin(this)) {
if ([QueryType.UPDATE, QueryType.DELETE].includes(this.type!) && criteriaNode.willAutoJoin(this, undefined, { ignoreBranching })) {
// use sub-query to support joining
this.setFlag(this.type === QueryType.UPDATE ? QueryFlag.UPDATE_SUB_QUERY : QueryFlag.DELETE_SUB_QUERY);
this.select(this.mainAlias.metadata!.primaryKeys, true);
Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/query/QueryBuilderHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ export class QueryBuilderHelper {

/* istanbul ignore next */
if (!op) {
throw new Error(`Invalid query condition: ${inspect(cond)}`);
throw new Error(`Invalid query condition: ${inspect(cond, { depth: 5 })}`);
}

const replacement = this.getOperatorReplacement(op, value);
Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/query/ScalarCriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class ScalarCriteriaNode<T extends object> extends CriteriaNode<T> {
return this.payload;
}

override willAutoJoin<T>(qb: IQueryBuilder<T>, alias?: string) {
override willAutoJoin<T>(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions) {
return this.shouldJoin();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export interface ICriteriaNode<T extends object> {
index?: number;
process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
shouldInline(payload: any): boolean;
willAutoJoin(qb: IQueryBuilder<T>, alias?: string): boolean;
willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
shouldRename(payload: any): boolean;
renameFieldToPK<T>(qb: IQueryBuilder<T>): string;
getPath(addIndex?: boolean): string;
Expand Down
1 change: 1 addition & 0 deletions tests/features/collection/collection-operators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ beforeAll(async () => {
entities: [Author],
dbName: ':memory:',
loggerFactory: options => new SimpleLogger(options),
populateWhere: 'infer',
});

await orm.schema.createSchema();
Expand Down
124 changes: 124 additions & 0 deletions tests/features/collection/populate-where-infer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Collection, Entity, ManyToOne, MikroORM, OneToMany, PrimaryKey, Property, Ref } from '@mikro-orm/sqlite';
import { mockLogger } from '../../helpers';

@Entity()
class User {

@PrimaryKey()
id!: number;

@ManyToOne(() => Location, { ref: true })
location!: Ref<Location>;

@OneToMany(() => Server, x => x.user)
servers = new Collection<Server>(this);

}

@Entity()
class Server {

@PrimaryKey()
id!: number;

@Property()
name!: string;

@ManyToOne(() => User, { ref: true })
user!: Ref<User>;

}

@Entity()
class Location {

@PrimaryKey()
id!: number;

@Property()
location!: string;

@OneToMany(() => User, x => x.location)
users = new Collection<User>(this);

}

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
dbName: ':memory:',
entities: [User],
});
await orm.schema.createSchema();

orm.em.create(User, {
id: 1,
location: {
location: 'loc name',
},
servers: [
{ name: 's1' },
{ name: 's2' },
{ name: 'test' },
],
});

await orm.em.flush();
orm.em.clear();
});

afterAll(async () => {
await orm.close(true);
});

test('$every without populateWhere', async () => {
const mock = mockLogger(orm);

const res = await orm.em.fork().find(
User,
{
servers: {
$every: {
name: {
$ne: 'test',
},
},
},
},
{
populate: ['servers'],
},
);
expect(res).toHaveLength(0);
expect(mock.mock.calls[0][0]).toMatch('select `u0`.*, `s1`.`id` as `s1__id`, `s1`.`name` as `s1__name`, `s1`.`user_id` as `s1__user_id` ' +
'from `user` as `u0` ' +
'left join `server` as `s1` on `u0`.`id` = `s1`.`user_id` ' +
'where `u0`.`id` not in (select `u0`.`id` from `user` as `u0` inner join `server` as `s1` on `u0`.`id` = `s1`.`user_id` where not (`s1`.`name` != \'test\'))');
});

test('$every with populateWhere: infer', async () => {
const mock = mockLogger(orm);

const res = await orm.em.fork().find(
User,
{
servers: {
$every: {
name: {
$ne: 'test',
},
},
},
},
{
populate: ['servers'],
populateWhere: 'infer',
},
);
expect(res).toHaveLength(0);
expect(mock.mock.calls[0][0]).toMatch('select `u0`.*, `s1`.`id` as `s1__id`, `s1`.`name` as `s1__name`, `s1`.`user_id` as `s1__user_id` ' +
'from `user` as `u0` ' +
'left join `server` as `s1` on `u0`.`id` = `s1`.`user_id` ' +
'where `u0`.`id` not in (select `u0`.`id` from `user` as `u0` inner join `server` as `s1` on `u0`.`id` = `s1`.`user_id` where not (`s1`.`name` != \'test\'))');
});

0 comments on commit 7b6b363

Please sign in to comment.