diff --git a/package.json b/package.json index 2ea97a5aa..5c8b985f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kysely", - "version": "0.19.4", + "version": "0.19.5", "description": "Type safe SQL query builder", "repository": { "type": "git", diff --git a/src/operation-node/join-node.ts b/src/operation-node/join-node.ts index 19109f74b..e5c944c9d 100644 --- a/src/operation-node/join-node.ts +++ b/src/operation-node/join-node.ts @@ -6,7 +6,13 @@ import { OperationNode } from './operation-node.js' import { TableNode } from './table-node.js' export type JoinTableNode = TableNode | AliasNode -export type JoinType = 'InnerJoin' | 'LeftJoin' | 'RightJoin' | 'FullJoin' +export type JoinType = + | 'InnerJoin' + | 'LeftJoin' + | 'RightJoin' + | 'FullJoin' + | 'LateralInnerJoin' + | 'LateralLeftJoin' export interface JoinNode extends OperationNode { readonly kind: 'JoinNode' diff --git a/src/parser/join-parser.ts b/src/parser/join-parser.ts index cae631f5c..f70149420 100644 --- a/src/parser/join-parser.ts +++ b/src/parser/join-parser.ts @@ -3,8 +3,8 @@ import { AnyColumn, AnyColumnWithTable } from '../util/type-utils.js' import { TableExpression, parseTableExpression, - TableExpressionDatabase, - TableExpressionTables, + From, + FromTables, } from './table-parser.js' import { parseReferenceFilter } from './filter-parser.js' import { JoinBuilder } from '../query-builder/join-builder.js' @@ -16,19 +16,19 @@ export type JoinReferenceExpression = export type JoinCallbackExpression = ( join: JoinBuilder< - TableExpressionDatabase, - TableExpressionTables + From, + FromTables > ) => JoinBuilder type AnyJoinColumn = AnyColumn< - TableExpressionDatabase, - TableExpressionTables + From, + FromTables > type AnyJoinColumnWithTable = AnyColumnWithTable< - TableExpressionDatabase, - TableExpressionTables + From, + FromTables > export function parseJoin(joinType: JoinType, args: any[]): JoinNode { diff --git a/src/parser/table-parser.ts b/src/parser/table-parser.ts index 0b911cc6e..1b2ba4a6e 100644 --- a/src/parser/table-parser.ts +++ b/src/parser/table-parser.ts @@ -24,19 +24,24 @@ export type TableReference = | AnyTable | AnyAliasedRawBuilder -export type TableExpressionDatabase< - DB, - TE, - A extends keyof any = ExtractAliasFromTableExpression -> = { - [C in keyof DB | A]: C extends A +export type From = { + [C in + | keyof DB + | ExtractAliasFromTableExpression< + DB, + TE + >]: C extends ExtractAliasFromTableExpression ? ExtractRowTypeFromTableExpression : C extends keyof DB ? DB[C] : never } -export type ExtractAliasFromTableExpression = +export type FromTables = + | TB + | ExtractAliasFromTableExpression + +type ExtractAliasFromTableExpression = TE extends `${string} as ${infer TA}` ? TA : TE extends keyof DB @@ -51,11 +56,7 @@ export type ExtractAliasFromTableExpression = ? RA : never -export type TableExpressionTables = - | TB - | ExtractAliasFromTableExpression - -export type ExtractRowTypeFromTableExpression< +type ExtractRowTypeFromTableExpression< DB, TE, A extends keyof any diff --git a/src/query-builder/expression-builder.ts b/src/query-builder/expression-builder.ts index 1c72b45d9..2121aa473 100644 --- a/src/query-builder/expression-builder.ts +++ b/src/query-builder/expression-builder.ts @@ -3,9 +3,9 @@ import { SelectQueryNode } from '../operation-node/select-query-node.js' import { parseTableExpressionOrList, TableExpression, - TableExpressionDatabase, + From, TableExpressionOrList, - TableExpressionTables, + FromTables, } from '../parser/table-parser.js' import { WithSchemaPlugin } from '../plugin/with-schema/with-schema-plugin.js' import { createQueryId } from '../util/query-id.js' @@ -108,19 +108,11 @@ export class ExpressionBuilder { */ selectFrom>( from: TE[] - ): SelectQueryBuilder< - TableExpressionDatabase, - TableExpressionTables, - {} - > + ): SelectQueryBuilder, FromTables, {}> selectFrom>( from: TE - ): SelectQueryBuilder< - TableExpressionDatabase, - TableExpressionTables, - {} - > + ): SelectQueryBuilder, FromTables, {}> selectFrom(table: TableExpressionOrList): any { return new SelectQueryBuilder({ diff --git a/src/query-builder/select-query-builder.ts b/src/query-builder/select-query-builder.ts index 178c27871..828fc1528 100644 --- a/src/query-builder/select-query-builder.ts +++ b/src/query-builder/select-query-builder.ts @@ -898,6 +898,54 @@ export class SelectQueryBuilder }) } + /** + * Just like {@link innerJoin} but adds a lateral join instead of an inner join. + */ + innerJoinLateral< + TE extends TableExpression, + K1 extends JoinReferenceExpression, + K2 extends JoinReferenceExpression + >(table: TE, k1: K1, k2: K2): SelectQueryBuilderWithInnerJoin + + innerJoinLateral< + TE extends TableExpression, + FN extends JoinCallbackExpression + >(table: TE, callback: FN): SelectQueryBuilderWithInnerJoin + + innerJoinLateral(...args: any): any { + return new SelectQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithJoin( + this.#props.queryNode, + parseJoin('LateralInnerJoin', args) + ), + }) + } + + /** + * Just like {@link innerJoin} but adds a lateral left join instead of an inner join. + */ + leftJoinLateral< + TE extends TableExpression, + K1 extends JoinReferenceExpression, + K2 extends JoinReferenceExpression + >(table: TE, k1: K1, k2: K2): SelectQueryBuilderWithLeftJoin + + leftJoinLateral< + TE extends TableExpression, + FN extends JoinCallbackExpression + >(table: TE, callback: FN): SelectQueryBuilderWithLeftJoin + + leftJoinLateral(...args: any): any { + return new SelectQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithJoin( + this.#props.queryNode, + parseJoin('LateralLeftJoin', args) + ), + }) + } + /** * Adds an `order by` clause to the query. * diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts index 2f87c15ff..959b63e34 100644 --- a/src/query-builder/update-query-builder.ts +++ b/src/query-builder/update-query-builder.ts @@ -7,8 +7,8 @@ import { } from '../parser/join-parser.js' import { TableExpression, - TableExpressionDatabase, - TableExpressionTables, + From, + FromTables, parseTableExpressionOrList, TableExpressionOrList, } from '../parser/table-parser.js' @@ -212,21 +212,11 @@ export class UpdateQueryBuilder */ from>( table: TE - ): UpdateQueryBuilder< - TableExpressionDatabase, - UT, - TableExpressionTables, - O - > + ): UpdateQueryBuilder, UT, FromTables, O> from>( table: TE[] - ): UpdateQueryBuilder< - TableExpressionDatabase, - UT, - TableExpressionTables, - O - > + ): UpdateQueryBuilder, UT, FromTables, O> from(from: TableExpressionOrList): any { return new UpdateQueryBuilder({ diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 47b419ebc..410bd49f5 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -1147,4 +1147,6 @@ const JOIN_TYPE_SQL: Readonly> = freeze({ LeftJoin: 'left join', RightJoin: 'right join', FullJoin: 'full join', + LateralInnerJoin: 'inner join lateral', + LateralLeftJoin: 'left join lateral', }) diff --git a/src/query-creator.ts b/src/query-creator.ts index 33e859020..9fcc0e12c 100644 --- a/src/query-creator.ts +++ b/src/query-creator.ts @@ -11,9 +11,9 @@ import { parseTableExpression, parseTableExpressionOrList, TableExpression, - TableExpressionDatabase, + From, TableExpressionOrList, - TableExpressionTables, + FromTables, TableReference, } from './parser/table-parser.js' import { QueryExecutor } from './query-executor/query-executor.js' @@ -148,19 +148,11 @@ export class QueryCreator { */ selectFrom>( from: TE[] - ): SelectQueryBuilder< - TableExpressionDatabase, - TableExpressionTables, - {} - > + ): SelectQueryBuilder, FromTables, {}> selectFrom>( from: TE - ): SelectQueryBuilder< - TableExpressionDatabase, - TableExpressionTables, - {} - > + ): SelectQueryBuilder, FromTables, {}> selectFrom(from: TableExpressionOrList): any { return new SelectQueryBuilder({ @@ -245,11 +237,7 @@ export class QueryCreator { */ deleteFrom>( table: TR - ): DeleteQueryBuilder< - TableExpressionDatabase, - TableExpressionTables, - DeleteResult - > { + ): DeleteQueryBuilder, FromTables, DeleteResult> { return new DeleteQueryBuilder({ queryId: createQueryId(), executor: this.#props.executor, @@ -286,9 +274,9 @@ export class QueryCreator { updateTable>( table: TR ): UpdateQueryBuilder< - TableExpressionDatabase, - TableExpressionTables, - TableExpressionTables, + From, + FromTables, + FromTables, UpdateResult > { return new UpdateQueryBuilder({ diff --git a/test/node/src/join.test.ts b/test/node/src/join.test.ts index 0c0d4a09d..76c032947 100644 --- a/test/node/src/join.test.ts +++ b/test/node/src/join.test.ts @@ -575,6 +575,72 @@ for (const dialect of BUILT_IN_DIALECTS) { await query.execute() }) }) + + describe('lateral join', () => { + it('should join an expression laterally', async () => { + const query = ctx.db + .selectFrom('person') + .innerJoinLateral( + (eb) => + eb + .selectFrom('pet') + .select('name') + .whereRef('pet.owner_id', '=', 'person.id') + .as('p'), + (join) => join.on(sql`true`) + ) + .select(['first_name', 'p.name']) + .orderBy('first_name') + + testSql(query, dialect, { + postgres: { + sql: `select "first_name", "p"."name" from "person" inner join lateral (select "name" from "pet" where "pet"."owner_id" = "person"."id") as "p" on true order by "first_name"`, + parameters: [], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + const res = await query.execute() + expect(res).to.eql([ + { first_name: 'Arnold', name: 'Doggo' }, + { first_name: 'Jennifer', name: 'Catto' }, + { first_name: 'Sylvester', name: 'Hammo' }, + ]) + }) + + it('should left join an expression laterally', async () => { + const query = ctx.db + .selectFrom('person') + .leftJoinLateral( + (eb) => + eb + .selectFrom('pet') + .select('name') + .whereRef('pet.owner_id', '=', 'person.id') + .as('p'), + (join) => join.on(sql`true`) + ) + .select(['first_name', 'p.name']) + .orderBy('first_name') + + testSql(query, dialect, { + postgres: { + sql: `select "first_name", "p"."name" from "person" left join lateral (select "name" from "pet" where "pet"."owner_id" = "person"."id") as "p" on true order by "first_name"`, + parameters: [], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + const res = await query.execute() + expect(res).to.eql([ + { first_name: 'Arnold', name: 'Doggo' }, + { first_name: 'Jennifer', name: 'Catto' }, + { first_name: 'Sylvester', name: 'Hammo' }, + ]) + }) + }) } }) }