Skip to content

[Pg] Add support for left and inner lateral join in postgres #1079

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

50 changes: 41 additions & 9 deletions drizzle-orm/src/pg-core/query-builders/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,15 @@ export abstract class PgSelectQueryBuilderBase<
this.joinsNotNullableMap = typeof this.tableName === 'string' ? { [this.tableName]: true } : {};
}

private createJoin<TJoinType extends JoinType>(
private createJoin<
TJoinType extends JoinType,
TIsLateral extends (TJoinType extends 'full' | 'right' ? false : boolean),
>(
joinType: TJoinType,
): PgSelectJoinFn<this, TDynamic, TJoinType> {
lateral: TIsLateral,
): PgSelectJoinFn<this, TDynamic, TJoinType, TIsLateral> {
return ((
table: PgTable | Subquery | PgViewBase | SQL,
table: TIsLateral extends true ? Subquery | SQL : PgTable | Subquery | PgViewBase | SQL,
on?: ((aliases: TSelection) => SQL | undefined) | SQL | undefined,
) => {
const baseTableName = this.tableName;
Expand Down Expand Up @@ -256,7 +260,7 @@ export abstract class PgSelectQueryBuilderBase<
this.config.joins = [];
}

this.config.joins.push({ on, table, joinType, alias: tableName });
this.config.joins.push({ on, table, joinType, alias: tableName, lateral });

if (typeof tableName === 'string') {
switch (joinType) {
Expand Down Expand Up @@ -317,7 +321,21 @@ export abstract class PgSelectQueryBuilderBase<
* .leftJoin(pets, eq(users.id, pets.ownerId))
* ```
*/
leftJoin = this.createJoin('left');
leftJoin = this.createJoin('left', false);

/**
* Executes a `left join lateral` operation by adding subquery to the current query.
*
* A `lateral` join allows the right-hand expression to refer to columns from the left-hand side.
*
* Calling this method associates each row of the table with the corresponding row from the joined table, if a match is found. If no matching row exists, it sets all columns of the joined table to null.
*
* See docs: {@link https://orm.drizzle.team/docs/joins#left-join-lateral}
*
* @param table the subquery to join.
* @param on the `on` clause.
*/
leftJoinLateral = this.createJoin('left', true);

/**
* Executes a `right join` operation by adding another table to the current query.
Expand Down Expand Up @@ -346,7 +364,7 @@ export abstract class PgSelectQueryBuilderBase<
* .rightJoin(pets, eq(users.id, pets.ownerId))
* ```
*/
rightJoin = this.createJoin('right');
rightJoin = this.createJoin('right', false);

/**
* Executes an `inner join` operation, creating a new table by combining rows from two tables that have matching values.
Expand Down Expand Up @@ -375,7 +393,21 @@ export abstract class PgSelectQueryBuilderBase<
* .innerJoin(pets, eq(users.id, pets.ownerId))
* ```
*/
innerJoin = this.createJoin('inner');
innerJoin = this.createJoin('inner', false);

/**
* Executes an `inner join lateral` operation, creating a new table by combining rows from two queries that have matching values.
*
* A `lateral` join allows the right-hand expression to refer to columns from the left-hand side.
*
* Calling this method retrieves rows that have corresponding entries in both joined tables. Rows without matching entries in either table are excluded, resulting in a table that includes only matching pairs.
*
* See docs: {@link https://orm.drizzle.team/docs/joins#inner-join-lateral}
*
* @param table the subquery to join.
* @param on the `on` clause.
*/
innerJoinLateral = this.createJoin('inner', true);

/**
* Executes a `full join` operation by combining rows from two tables into a new table.
Expand Down Expand Up @@ -404,7 +436,7 @@ export abstract class PgSelectQueryBuilderBase<
* .fullJoin(pets, eq(users.id, pets.ownerId))
* ```
*/
fullJoin = this.createJoin('full');
fullJoin = this.createJoin('full', false);

/**
* Executes a `cross join` operation by combining rows from two tables into a new table.
Expand Down Expand Up @@ -432,7 +464,7 @@ export abstract class PgSelectQueryBuilderBase<
* .crossJoin(pets)
* ```
*/
crossJoin = this.createJoin('cross');
crossJoin = this.createJoin('cross', false);

private createSetOperator(
type: SetOperator,
Expand Down
5 changes: 3 additions & 2 deletions drizzle-orm/src/pg-core/query-builders/select.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ export type PgSelectJoinFn<
T extends AnyPgSelectQueryBuilder,
TDynamic extends boolean,
TJoinType extends JoinType,
TIsLateral extends boolean,
> = 'cross' extends TJoinType ? <
TJoinedTable extends PgTable | Subquery | PgViewBase | SQL,
TJoinedTable extends (TIsLateral extends true ? Subquery | SQL : PgTable | Subquery | PgViewBase | SQL),
TJoinedName extends GetSelectTableName<TJoinedTable> = GetSelectTableName<TJoinedTable>,
>(
table: TableLikeHasEmptySelection<TJoinedTable> extends true ? DrizzleTypeError<
Expand All @@ -126,7 +127,7 @@ export type PgSelectJoinFn<
: TJoinedTable,
) => PgSelectJoin<T, TDynamic, TJoinType, TJoinedTable, TJoinedName>
: <
TJoinedTable extends PgTable | Subquery | PgViewBase | SQL,
TJoinedTable extends (TIsLateral extends true ? Subquery | SQL : PgTable | Subquery | PgViewBase | SQL),
TJoinedName extends GetSelectTableName<TJoinedTable> = GetSelectTableName<TJoinedTable>,
>(
table: TableLikeHasEmptySelection<TJoinedTable> extends true ? DrizzleTypeError<
Expand Down
73 changes: 73 additions & 0 deletions integration-tests/tests/pg/pg-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5749,6 +5749,79 @@ export function tests() {
]);
});

test('left join (lateral)', async (ctx) => {
const { db } = ctx.pg;

const { id: cityId } = await db
.insert(citiesTable)
.values([{ name: 'Paris' }, { name: 'London' }])
.returning({ id: citiesTable.id })
.then((rows) => rows[0]!);

await db.insert(users2Table).values([{ name: 'John', cityId }, { name: 'Jane' }]);

const sq = db
.select({
userId: users2Table.id,
userName: users2Table.name,
cityId: users2Table.cityId,
})
.from(users2Table)
.where(eq(users2Table.cityId, citiesTable.id))
.as('sq');

const res = await db
.select({
cityId: citiesTable.id,
cityName: citiesTable.name,
userId: sq.userId,
userName: sq.userName,
})
.from(citiesTable)
.leftJoinLateral(sq, sql`true`);

expect(res).toStrictEqual([
{ cityId: 1, cityName: 'Paris', userId: 1, userName: 'John' },
{ cityId: 2, cityName: 'London', userId: null, userName: null },
]);
});

test('inner join (lateral)', async (ctx) => {
const { db } = ctx.pg;

const { id: cityId } = await db
.insert(citiesTable)
.values([{ name: 'Paris' }, { name: 'London' }])
.returning({ id: citiesTable.id })
.then((rows) => rows[0]!);

await db.insert(users2Table).values([{ name: 'John', cityId }, { name: 'Jane' }]);

const sq = db
.select({
userId: users2Table.id,
userName: users2Table.name,
cityId: users2Table.cityId,
})
.from(users2Table)
.where(eq(users2Table.cityId, citiesTable.id))
.as('sq');

const res = await db
.select({
cityId: citiesTable.id,
cityName: citiesTable.name,
userId: sq.userId,
userName: sq.userName,
})
.from(citiesTable)
.innerJoinLateral(sq, sql`true`);

expect(res).toStrictEqual([
{ cityId: 1, cityName: 'Paris', userId: 1, userName: 'John' },
]);
});

test('all types', async (ctx) => {
const { db } = ctx.pg;

Expand Down