Skip to content

Commit

Permalink
feat(query-builder): add support for lateral sub-query joins
Browse files Browse the repository at this point in the history
Closes #624
  • Loading branch information
B4nan committed Nov 5, 2023
1 parent 731087d commit 99f87c4
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 32 deletions.
4 changes: 2 additions & 2 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
} from '@mikro-orm/core';
import type { AbstractSqlConnection } from './AbstractSqlConnection';
import type { AbstractSqlPlatform } from './AbstractSqlPlatform';
import { QueryBuilder, QueryType } from './query';
import { JoinType, QueryBuilder, QueryType } from './query';
import { SqlEntityManager } from './SqlEntityManager';
import type { Field } from './typings';
import { PivotCollectionPersister } from './PivotCollectionPersister';
Expand Down Expand Up @@ -979,7 +979,7 @@ export abstract class AbstractSqlDriver<Connection extends AbstractSqlConnection
const tableAlias = qb.getNextAlias(prop.name);
const field = parentTableAlias ? `${parentTableAlias}.${prop.name}` : prop.name;
const path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
qb.join(field, tableAlias, {}, 'leftJoin', path);
qb.join(field, tableAlias, {}, JoinType.leftJoin, path);
const childExplicitFields = explicitFields?.filter(f => Utils.isPlainObject(f)).map(o => (o as Dictionary)[prop.name])[0] || [];

explicitFields?.forEach(f => {
Expand Down
16 changes: 12 additions & 4 deletions packages/knex/src/query/ObjectCriteriaNode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { ReferenceKind, Utils, type Dictionary, type EntityKey, raw, ALIAS_REPLACEMENT, RawQueryFragment } from '@mikro-orm/core';
import {
ALIAS_REPLACEMENT,
type Dictionary,
type EntityKey,
raw,
RawQueryFragment,
ReferenceKind,
Utils,
} from '@mikro-orm/core';
import { CriteriaNode } from './CriteriaNode';
import type { IQueryBuilder } from '../typings';
import { QueryType } from './enums';
import { JoinType, QueryType } from './enums';

/**
* @internal
Expand Down Expand Up @@ -140,10 +148,10 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
const field = `${alias}.${this.prop!.name}`;

if (this.prop!.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator)) {
qb.join(field, nestedAlias, undefined, 'pivotJoin', this.getPath());
qb.join(field, nestedAlias, undefined, JoinType.pivotJoin, this.getPath());
} else {
const prev = qb._fields?.slice();
qb.join(field, nestedAlias, undefined, 'leftJoin', this.getPath());
qb.join(field, nestedAlias, undefined, JoinType.leftJoin, this.getPath());
qb._fields = prev;
}

Expand Down
45 changes: 33 additions & 12 deletions packages/knex/src/query/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
Utils,
ValidationError,
} from '@mikro-orm/core';
import { QueryType } from './enums';
import { QueryType, JoinType } from './enums';
import type { AbstractSqlDriver } from '../AbstractSqlDriver';
import { type Alias, QueryBuilderHelper } from './QueryBuilderHelper';
import type { SqlEntityManager } from '../SqlEntityManager';
Expand Down Expand Up @@ -213,33 +213,42 @@ export class QueryBuilder<T extends object = AnyEntity> {
return this.init(QueryType.COUNT) as CountQueryBuilder<T>;
}

join(field: string | Knex.QueryBuilder | QueryBuilder<any>, alias: string, cond: QBFilterQuery = {}, type: 'leftJoin' | 'innerJoin' | 'pivotJoin' = 'innerJoin', path?: string, schema?: string): this {
join(field: string | Knex.QueryBuilder | QueryBuilder<any>, alias: string, cond: QBFilterQuery = {}, type = JoinType.innerJoin, path?: string, schema?: string): this {
this.joinReference(field, alias, cond, type, path, schema);
return this;
}

innerJoin(field: string | Knex.QueryBuilder | QueryBuilder<any>, alias: string, cond: QBFilterQuery = {}, schema?: string): this {
this.join(field, alias, cond, 'innerJoin', undefined, schema);
this.join(field, alias, cond, JoinType.innerJoin, undefined, schema);
return this;
}

innerJoinLateral(field: string | Knex.QueryBuilder | QueryBuilder<any>, alias: string, cond: QBFilterQuery = {}, schema?: string): this {
this.join(field, alias, cond, JoinType.innerJoinLateral, undefined, schema);
return this;
}

leftJoin(field: string | Knex.QueryBuilder | QueryBuilder<any>, alias: string, cond: QBFilterQuery = {}, schema?: string): this {
return this.join(field, alias, cond, 'leftJoin', undefined, schema);
return this.join(field, alias, cond, JoinType.leftJoin, undefined, schema);
}

joinAndSelect(field: string | [field: string, qb: Knex.QueryBuilder | QueryBuilder<any>], alias: string, cond: QBFilterQuery = {}, type: 'leftJoin' | 'innerJoin' | 'pivotJoin' = 'innerJoin', path?: string, fields?: string[], schema?: string): SelectQueryBuilder<T> {
leftJoinLateral(field: string | Knex.QueryBuilder | QueryBuilder<any>, alias: string, cond: QBFilterQuery = {}, schema?: string): this {
return this.join(field, alias, cond, JoinType.leftJoinLateral, undefined, schema);
}

joinAndSelect(field: string | [field: string, qb: Knex.QueryBuilder | QueryBuilder<any>], alias: string, cond: QBFilterQuery = {}, type = JoinType.innerJoin, path?: string, fields?: string[], schema?: string): SelectQueryBuilder<T> {
if (!this.type) {
this.select('*');
}

let subquery!: string;
let subquery: string | undefined;

if (Array.isArray(field)) {
subquery = field[1] instanceof QueryBuilder ? field[1].getFormattedQuery() : field[1].toString();
field = field[0];
}

const prop = this.joinReference(field, alias, cond, type, path, schema);
const prop = this.joinReference(field, alias, cond, type, path, schema, subquery);
const [fromAlias] = this.helper.splitField(field as EntityKey<T>);

if (subquery) {
Expand All @@ -262,11 +271,19 @@ export class QueryBuilder<T extends object = AnyEntity> {
}

leftJoinAndSelect(field: string | [field: string, qb: Knex.QueryBuilder | QueryBuilder<any>], alias: string, cond: QBFilterQuery = {}, fields?: string[], schema?: string): SelectQueryBuilder<T> {
return this.joinAndSelect(field, alias, cond, 'leftJoin', undefined, fields, schema);
return this.joinAndSelect(field, alias, cond, JoinType.leftJoin, undefined, fields, schema);
}

leftJoinLateralAndSelect(field: string | [field: string, qb: Knex.QueryBuilder | QueryBuilder<any>], alias: string, cond: QBFilterQuery = {}, fields?: string[], schema?: string): SelectQueryBuilder<T> {
return this.joinAndSelect(field, alias, cond, JoinType.leftJoinLateral, undefined, fields, schema);
}

innerJoinAndSelect(field: string | [field: string, qb: Knex.QueryBuilder | QueryBuilder<any>], alias: string, cond: QBFilterQuery = {}, fields?: string[], schema?: string): SelectQueryBuilder<T> {
return this.joinAndSelect(field, alias, cond, 'innerJoin', undefined, fields, schema);
return this.joinAndSelect(field, alias, cond, JoinType.innerJoin, undefined, fields, schema);
}

innerJoinLateralAndSelect(field: string | [field: string, qb: Knex.QueryBuilder | QueryBuilder<any>], alias: string, cond: QBFilterQuery = {}, fields?: string[], schema?: string): SelectQueryBuilder<T> {
return this.joinAndSelect(field, alias, cond, JoinType.innerJoinLateral, undefined, fields, schema);
}

protected getFieldsForJoinedLoad(prop: EntityProperty<T>, alias: string, explicitFields?: string[]): Field<T>[] {
Expand Down Expand Up @@ -874,7 +891,7 @@ export class QueryBuilder<T extends object = AnyEntity> {
return qb;
}

private joinReference(field: string | Knex.QueryBuilder | QueryBuilder, alias: string, cond: Dictionary, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', path?: string, schema?: string): EntityProperty<T> {
private joinReference(field: string | Knex.QueryBuilder | QueryBuilder, alias: string, cond: Dictionary, type: JoinType, path?: string, schema?: string, subquery?: string): EntityProperty<T> {
this.ensureNotFinalized();

if (typeof field === 'object') {
Expand Down Expand Up @@ -902,6 +919,10 @@ export class QueryBuilder<T extends object = AnyEntity> {
return prop;
}

if (!subquery && type.includes('lateral')) {
throw new Error(`Lateral join can be used only with a sub-query.`);
}

const [fromAlias, fromField] = this.helper.splitField(field as EntityKey<T>);
const q = (str: string) => `'${str}'`;

Expand Down Expand Up @@ -934,7 +955,7 @@ export class QueryBuilder<T extends object = AnyEntity> {
} else if (prop.kind === ReferenceKind.MANY_TO_MANY) {
let pivotAlias = alias;

if (type !== 'pivotJoin') {
if (type !== JoinType.pivotJoin) {
const oldPivotAlias = this.getAliasForJoinPath(path + '[pivot]');
pivotAlias = oldPivotAlias ?? this.getNextAlias(prop.pivotEntity);
aliasedName = `${fromAlias}.${prop.name}#${pivotAlias}`;
Expand Down Expand Up @@ -1189,7 +1210,7 @@ export class QueryBuilder<T extends object = AnyEntity> {
const prop = meta.properties[fromField as EntityKey<T>];
const alias = this.getNextAlias(prop.pivotEntity ?? prop.type);
const aliasedName = `${fromAlias}.${prop.name}#${alias}`;
this._joins[aliasedName] = this.helper.joinOneToReference(prop, this.mainAlias.aliasName, alias, 'leftJoin');
this._joins[aliasedName] = this.helper.joinOneToReference(prop, this.mainAlias.aliasName, alias, JoinType.leftJoin);
this._populateMap[aliasedName] = this._joins[aliasedName].alias;
}
});
Expand Down
12 changes: 6 additions & 6 deletions packages/knex/src/query/QueryBuilderHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
ReferenceKind,
Utils,
} from '@mikro-orm/core';
import { QueryType } from './enums';
import { JoinType, QueryType } from './enums';
import type { Field, JoinOptions } from '../typings';
import type { AbstractSqlDriver } from '../AbstractSqlDriver';
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform';
Expand Down Expand Up @@ -173,7 +173,7 @@ export class QueryBuilderHelper {
return data;
}

joinOneToReference(prop: EntityProperty, ownerAlias: string, alias: string, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', cond: Dictionary = {}, schema?: string): JoinOptions {
joinOneToReference(prop: EntityProperty, ownerAlias: string, alias: string, type: JoinType, cond: Dictionary = {}, schema?: string): JoinOptions {
const prop2 = prop.targetMeta!.properties[prop.mappedBy || prop.inversedBy];
const table = this.getTableName(prop.type);
const joinColumns = prop.owner ? prop.referencedColumnNames : prop2.joinColumns;
Expand All @@ -187,7 +187,7 @@ export class QueryBuilderHelper {
};
}

joinManyToOneReference(prop: EntityProperty, ownerAlias: string, alias: string, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', cond: Dictionary = {}, schema?: string): JoinOptions {
joinManyToOneReference(prop: EntityProperty, ownerAlias: string, alias: string, type: JoinType, cond: Dictionary = {}, schema?: string): JoinOptions {
return {
prop, type, cond, ownerAlias, alias,
table: this.getTableName(prop.type),
Expand All @@ -197,7 +197,7 @@ export class QueryBuilderHelper {
};
}

joinManyToManyReference(prop: EntityProperty, ownerAlias: string, alias: string, pivotAlias: string, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', cond: Dictionary, path: string, schema?: string): Dictionary<JoinOptions> {
joinManyToManyReference(prop: EntityProperty, ownerAlias: string, alias: string, pivotAlias: string, type: JoinType, cond: Dictionary, path: string, schema?: string): Dictionary<JoinOptions> {
const pivotMeta = this.metadata.find(prop.pivotEntity)!;
const ret = {
[`${ownerAlias}.${prop.name}#${pivotAlias}`]: {
Expand All @@ -214,7 +214,7 @@ export class QueryBuilderHelper {
} as JoinOptions,
};

if (type === 'pivotJoin') {
if (type === JoinType.pivotJoin) {
return ret;
}

Expand All @@ -230,7 +230,7 @@ export class QueryBuilderHelper {
processJoins(qb: Knex.QueryBuilder, joins: Dictionary<JoinOptions>, schema?: string): void {
Object.values(joins).forEach(join => {
let table = join.table;
const method = join.type === 'innerJoin' ? 'inner join' : 'left join';
const method = join.type === JoinType.pivotJoin ? 'left join' : join.type;
const conditions: string[] = [];
const params: Knex.Value[] = [];
schema = join.schema && join.schema !== '*' ? join.schema : schema;
Expand Down
3 changes: 2 additions & 1 deletion packages/knex/src/query/ScalarCriteriaNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ReferenceKind } from '@mikro-orm/core';
import { CriteriaNode } from './CriteriaNode';
import type { IQueryBuilder } from '../typings';
import { JoinType } from './enums';

/**
* @internal
Expand All @@ -13,7 +14,7 @@ export class ScalarCriteriaNode<T extends object> extends CriteriaNode<T> {
const parentPath = this.parent!.getPath(); // the parent is always there, otherwise `shouldJoin` would return `false`
const nestedAlias = qb.getAliasForJoinPath(path) || qb.getNextAlias(this.prop?.pivotTable ?? this.entityName);
const field = `${alias}.${this.prop!.name}`;
const type = this.prop!.kind === ReferenceKind.MANY_TO_MANY ? 'pivotJoin' : 'leftJoin';
const type = this.prop!.kind === ReferenceKind.MANY_TO_MANY ? JoinType.pivotJoin : JoinType.leftJoin;
qb.join(field, nestedAlias, undefined, type, path);

// select the owner as virtual property when joining from 1:1 inverse side, but only if the parent is root entity
Expand Down
8 changes: 8 additions & 0 deletions packages/knex/src/query/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ export enum QueryType {
UPDATE = 'UPDATE',
DELETE = 'DELETE',
}

export enum JoinType {
leftJoin = 'left join',
innerJoin = 'inner join',
pivotJoin = 'pivot join',
innerJoinLateral = 'inner join lateral',
leftJoinLateral = 'left join lateral',
}
6 changes: 3 additions & 3 deletions packages/knex/src/typings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Knex } from 'knex';
import type { CheckCallback, Dictionary, EntityProperty, GroupOperator, RawQueryFragment, QBFilterQuery, QueryOrderMap, Type } from '@mikro-orm/core';
import type { QueryType } from './query/enums';
import type { JoinType, QueryType } from './query/enums';
import type { DatabaseSchema, DatabaseTable } from './schema';

export interface Table {
Expand All @@ -20,7 +20,7 @@ export type Field<T> = AnyString | keyof T | RawQueryFragment | KnexStringRef |
export interface JoinOptions {
table: string;
schema?: string;
type: 'leftJoin' | 'innerJoin' | 'pivotJoin';
type: JoinType;
alias: string;
ownerAlias: string;
inverseAlias?: string;
Expand Down Expand Up @@ -136,7 +136,7 @@ export interface IQueryBuilder<T> {
delete(cond?: QBFilterQuery): this;
truncate(): this;
count(field?: string | string[], distinct?: boolean): this;
join(field: string, alias: string, cond?: QBFilterQuery, type?: 'leftJoin' | 'innerJoin' | 'pivotJoin', path?: string): this;
join(field: string, alias: string, cond?: QBFilterQuery, type?: JoinType, path?: string): this;
leftJoin(field: string, alias: string, cond?: QBFilterQuery): this;
joinAndSelect(field: string, alias: string, cond?: QBFilterQuery): this;
leftJoinAndSelect(field: string, alias: string, cond?: QBFilterQuery, fields?: string[]): this;
Expand Down
Loading

0 comments on commit 99f87c4

Please sign in to comment.