Skip to content

Commit

Permalink
Add outer and cross apply (mssql) (#1074)
Browse files Browse the repository at this point in the history
Co-authored-by: Drew Marshall <[email protected]>
Co-authored-by: Drew Marshall <[email protected]>
Co-authored-by: igalklebanov <[email protected]>
  • Loading branch information
3 people authored Jan 12, 2025
1 parent f75bc1e commit 6ccc00a
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/operation-node/join-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type JoinType =
| 'LateralInnerJoin'
| 'LateralLeftJoin'
| 'Using'
| 'OuterApply'
| 'CrossApply'

export interface JoinNode extends OperationNode {
readonly kind: 'JoinNode'
Expand Down
19 changes: 14 additions & 5 deletions src/parser/join-parser.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { JoinNode, JoinType } from '../operation-node/join-node.js'
import { JoinBuilder } from '../query-builder/join-builder.js'
import {
AnyColumn,
AnyColumnWithTable,
DrainOuterGeneric,
} from '../util/type-utils.js'
import { parseReferentialBinaryOperation } from './binary-operation-parser.js'
import { createJoinBuilder } from './parse-utils.js'
import {
TableExpression,
parseTableExpression,
From,
FromTables,
TableExpression,
parseTableExpression,
} from './table-parser.js'
import { parseReferentialBinaryOperation } from './binary-operation-parser.js'
import { JoinBuilder } from '../query-builder/join-builder.js'
import { createJoinBuilder } from './parse-utils.js'

export type JoinReferenceExpression<
DB,
Expand Down Expand Up @@ -41,6 +41,8 @@ export function parseJoin(joinType: JoinType, args: any[]): JoinNode {
return parseSingleOnJoin(joinType, args[0], args[1], args[2])
} else if (args.length === 2) {
return parseCallbackJoin(joinType, args[0], args[1])
} else if (args.length === 1) {
return parseOnlessJoin(joinType, args[0])
} else {
throw new Error('not implemented')
}
Expand All @@ -66,3 +68,10 @@ function parseSingleOnJoin(
parseReferentialBinaryOperation(lhsColumn, '=', rhsColumn),
)
}

function parseOnlessJoin(
joinType: JoinType,
from: TableExpression<any, any>,
): JoinNode {
return JoinNode.create(joinType, parseTableExpression(from))
}
86 changes: 80 additions & 6 deletions src/query-builder/select-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,13 +579,13 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O>
selectAll(): SelectQueryBuilder<DB, TB, O & AllSelection<DB, TB>>

/**
* Joins another table to the query using an inner join.
* Joins another table to the query using an `inner join`.
*
* ### Examples
*
* <!-- siteExample("join", "Simple inner join", 10) -->
*
* Simple inner joins can be done by providing a table name and two columns to join:
* Simple `inner join`s can be done by providing a table name and two columns to join:
*
* ```ts
* const result = await db
Expand Down Expand Up @@ -721,7 +721,7 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O>
): SelectQueryBuilderWithInnerJoin<DB, TB, O, TE>

/**
* Just like {@link innerJoin} but adds a left join instead of an inner join.
* Just like {@link innerJoin} but adds a `left join` instead of an `inner join`.
*/
leftJoin<
TE extends TableExpression<DB, TB>,
Expand All @@ -742,7 +742,7 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O>
): SelectQueryBuilderWithLeftJoin<DB, TB, O, TE>

/**
* Just like {@link innerJoin} but adds a right join instead of an inner join.
* Just like {@link innerJoin} but adds a `right join` instead of an `inner join`.
*/
rightJoin<
TE extends TableExpression<DB, TB>,
Expand All @@ -763,7 +763,9 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O>
): SelectQueryBuilderWithRightJoin<DB, TB, O, TE>

/**
* Just like {@link innerJoin} but adds a full join instead of an inner join.
* Just like {@link innerJoin} but adds a `full join` instead of an `inner join`.
*
* This is only supported by some dialects like PostgreSQL, MS SQL Server and SQLite.
*/
fullJoin<
TE extends TableExpression<DB, TB>,
Expand All @@ -786,6 +788,8 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O>
/**
* Just like {@link innerJoin} but adds a lateral join instead of an inner join.
*
* This is only supported by some dialects like PostgreSQL and MySQL.
*
* ### Examples
*
* ```ts
Expand Down Expand Up @@ -835,7 +839,10 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O>
): SelectQueryBuilderWithInnerJoin<DB, TB, O, TE>

/**
* Just like {@link innerJoin} but adds a lateral left join instead of an inner join.
* Just like {@link innerJoin} but adds a `left join lateral` instead of an `inner join`.
*
* This is only supported by some dialects like PostgreSQL and MySQL.
*
* ### Examples
*
* ```ts
Expand Down Expand Up @@ -884,6 +891,53 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O>
callback: FN,
): SelectQueryBuilderWithLeftJoin<DB, TB, O, TE>

/**
* Joins another table to the query using a `cross apply`.
*
* This is only supported by some dialects like MS SQL Server.
*
* ### Examples
*
* ```ts
* await db.selectFrom('person')
* .crossApply(
* (eb) =>
* eb.selectFrom('pet')
* .select('name')
* .whereRef('pet.owner_id', '=', 'person.id')
* .as('p')
* )
* .select(['first_name', 'p.name'])
* .orderBy('first_name')
* .execute()
* ```
*
* The generated SQL (MS SQL Server):
*
* ```sql
* select "person"."first_name", "p"."name"
* from "person"
* cross apply (
* select "name"
* from "pet"
* where "pet"."owner_id" = "person"."id"
* ) as "p"
* order by "first_name"
* ```
*/
crossApply<TE extends TableExpression<DB, TB>>(
table: TE,
): SelectQueryBuilderWithInnerJoin<DB, TB, O, TE>

/**
* Just like {@link crossApply} but adds an `outer apply` instead of a `cross apply`.
*
* This is only supported by some dialects like MS SQL Server.
*/
outerApply<TE extends TableExpression<DB, TB>>(
table: TE,
): SelectQueryBuilderWithLeftJoin<DB, TB, O, TE>

/**
* Adds an `order by` clause to the query.
*
Expand Down Expand Up @@ -2395,6 +2449,26 @@ class SelectQueryBuilderImpl<DB, TB extends keyof DB, O>
})
}

crossApply(table: any): any {
return new SelectQueryBuilderImpl({
...this.#props,
queryNode: QueryNode.cloneWithJoin(
this.#props.queryNode,
parseJoin('CrossApply', [table]),
),
})
}

outerApply(table: any): any {
return new SelectQueryBuilderImpl({
...this.#props,
queryNode: QueryNode.cloneWithJoin(
this.#props.queryNode,
parseJoin('OuterApply', [table]),
),
})
}

orderBy(...args: any[]): SelectQueryBuilder<DB, TB, O> {
return new SelectQueryBuilderImpl({
...this.#props,
Expand Down
2 changes: 2 additions & 0 deletions src/query-compiler/default-query-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1815,5 +1815,7 @@ const JOIN_TYPE_SQL: Readonly<Record<JoinType, string>> = freeze({
FullJoin: 'full join',
LateralInnerJoin: 'inner join lateral',
LateralLeftJoin: 'left join lateral',
OuterApply: 'outer apply',
CrossApply: 'cross apply',
Using: 'using',
})
68 changes: 68 additions & 0 deletions test/node/src/join.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,5 +780,73 @@ for (const dialect of DIALECTS) {
})
})
}

if (dialect === 'mssql') {
describe('apply', () => {
it('should cross apply an expression', async () => {
const q = ctx.db
.selectFrom('person')
.crossApply((eb) =>
eb
.selectFrom('pet')
.whereRef('pet.owner_id', '=', 'person.id')
.select('pet.name')
.as('pets'),
)
.select(['person.first_name', 'pets.name'])
.orderBy('pets.name')

testSql(q, dialect, {
postgres: NOT_SUPPORTED,
mysql: NOT_SUPPORTED,
sqlite: NOT_SUPPORTED,
mssql: {
sql: `select "person"."first_name", "pets"."name" from "person" cross apply (select "pet"."name" from "pet" where "pet"."owner_id" = "person"."id") as "pets" order by "pets"."name"`,
parameters: [],
},
})

const result = await q.execute()

expect(result).to.deep.equal([
{ first_name: 'Jennifer', name: 'Catto' },
{ first_name: 'Arnold', name: 'Doggo' },
{ first_name: 'Sylvester', name: 'Hammo' },
])
})

it('should outer apply an expression', async () => {
const q = ctx.db
.selectFrom('person')
.outerApply((eb) =>
eb
.selectFrom('pet')
.whereRef('pet.owner_id', '=', 'person.id')
.select('pet.name')
.as('pets'),
)
.select(['person.first_name', 'pets.name'])
.orderBy('pets.name')

testSql(q, dialect, {
postgres: NOT_SUPPORTED,
mysql: NOT_SUPPORTED,
sqlite: NOT_SUPPORTED,
mssql: {
sql: `select "person"."first_name", "pets"."name" from "person" outer apply (select "pet"."name" from "pet" where "pet"."owner_id" = "person"."id") as "pets" order by "pets"."name"`,
parameters: [],
},
})

const result = await q.execute()

expect(result).to.deep.equal([
{ first_name: 'Jennifer', name: 'Catto' },
{ first_name: 'Arnold', name: 'Doggo' },
{ first_name: 'Sylvester', name: 'Hammo' },
])
})
})
}
})
}
81 changes: 81 additions & 0 deletions test/typings/test-d/join.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,87 @@ async function testJoin(db: Kysely<Database>) {
.innerJoin('pet', 'pet.owner_id', 'person.id')
.set({ last_name: 'Jennifer' })
.where('pet.id', '=', '1')
.execute()

const r9 = await db
.selectFrom('person')
.innerJoinLateral(
(eb) =>
eb
.selectFrom('pet')
.whereRef('pet.owner_id', '=', 'person.id')
.select('pet.name')
.as('pets'),
(jb) => jb.onTrue(),
)
.select(['person.first_name', 'pets.name'])
.execute()

expectType<
{
first_name: string
name: string
}[]
>(r9)

const r10 = await db
.selectFrom('person')
.leftJoinLateral(
(eb) =>
eb
.selectFrom('pet')
.whereRef('pet.owner_id', '=', 'person.id')
.select('pet.name')
.as('pets'),
(jb) => jb.onTrue(),
)
.select(['person.first_name', 'pets.name'])
.execute()

expectType<
{
first_name: string
name: string | null
}[]
>(r10)

const r11 = await db
.selectFrom('person')
.crossApply((eb) =>
eb
.selectFrom('pet')
.whereRef('pet.owner_id', '=', 'person.id')
.select('pet.name')
.as('pets'),
)
.select(['person.first_name', 'pets.name'])
.execute()

expectType<
{
first_name: string
name: string
}[]
>(r11)

const r12 = await db
.selectFrom('person')
.outerApply((eb) =>
eb
.selectFrom('pet')
.whereRef('pet.owner_id', '=', 'person.id')
.select('pet.name')
.as('pets'),
)
.select(['person.first_name', 'pets.name'])
.execute()

expectType<
{
first_name: string
name: string | null
}[]
>(r12)

// Refer to table that's not joined
expectError(
Expand Down

0 comments on commit 6ccc00a

Please sign in to comment.