Skip to content

Commit 09d7ab0

Browse files
authored
add returning support in MERGE queries. (#1171)
1 parent 1c5e03a commit 09d7ab0

11 files changed

+503
-89
lines changed

deno.check.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
export interface Database {
1313
audit: AuditTable
1414
person: PersonTable
15+
person_backup: PersonTable
1516
pet: PetTable
1617
toy: ToyTable
1718
wine: WineTable

src/helpers/postgres.ts

+52
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,55 @@ export function jsonBuildObject<O extends Record<string, Expression<unknown>>>(
169169
Object.keys(obj).flatMap((k) => [sql.lit(k), obj[k]]),
170170
)})`
171171
}
172+
173+
export type MergeAction = 'INSERT' | 'UPDATE' | 'DELETE'
174+
175+
/**
176+
* The PostgreSQL `merge_action` function.
177+
*
178+
* This function can be used in a `returning` clause to get the action that was
179+
* performed in a `mergeInto` query. The function returns one of the following
180+
* strings: `'INSERT'`, `'UPDATE'`, or `'DELETE'`.
181+
*
182+
* ### Examples
183+
*
184+
* ```ts
185+
* import { mergeAction } from 'kysely/helpers/postgres'
186+
*
187+
* const result = await db
188+
* .mergeInto('person as p')
189+
* .using('person_backup as pb', 'p.id', 'pb.id')
190+
* .whenMatched()
191+
* .thenUpdateSet(({ ref }) => ({
192+
* first_name: ref('pb.first_name'),
193+
* updated_at: ref('pb.updated_at').$castTo<string | null>(),
194+
* }))
195+
* .whenNotMatched()
196+
* .thenInsertValues(({ ref}) => ({
197+
* id: ref('pb.id'),
198+
* first_name: ref('pb.first_name'),
199+
* created_at: ref('pb.updated_at'),
200+
* updated_at: ref('pb.updated_at').$castTo<string | null>(),
201+
* }))
202+
* .returning([mergeAction().as('action'), 'p.id', 'p.updated_at'])
203+
* .execute()
204+
*
205+
* result[0].action
206+
* ```
207+
*
208+
* The generated SQL (PostgreSQL):
209+
*
210+
* ```sql
211+
* merge into "person" as "p"
212+
* using "person_backup" as "pb" on "p"."id" = "pb"."id"
213+
* when matched then update set
214+
* "first_name" = "pb"."first_name",
215+
* "updated_at" = "pb"."updated_at"::text
216+
* when not matched then insert values ("id", "first_name", "created_at", "updated_at")
217+
* values ("pb"."id", "pb"."first_name", "pb"."updated_at", "pb"."updated_at")
218+
* returning merge_action() as "action", "p"."id", "p"."updated_at"
219+
* ```
220+
*/
221+
export function mergeAction(): RawBuilder<MergeAction> {
222+
return sql`merge_action()`
223+
}

src/operation-node/merge-query-node.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AliasNode } from './alias-node.js'
33
import { JoinNode } from './join-node.js'
44
import { OperationNode } from './operation-node.js'
55
import { OutputNode } from './output-node.js'
6+
import { ReturningNode } from './returning-node.js'
67
import { TableNode } from './table-node.js'
78
import { TopNode } from './top-node.js'
89
import { WhenNode } from './when-node.js'
@@ -15,6 +16,7 @@ export interface MergeQueryNode extends OperationNode {
1516
readonly whens?: ReadonlyArray<WhenNode>
1617
readonly with?: WithNode
1718
readonly top?: TopNode
19+
readonly returning?: ReturningNode
1820
readonly output?: OutputNode
1921
readonly endModifiers?: ReadonlyArray<OperationNode>
2022
}

src/operation-node/operation-node-transformer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,7 @@ export class OperationNodeTransformer {
10391039
top: this.transformNode(node.top),
10401040
endModifiers: this.transformNodeList(node.endModifiers),
10411041
output: this.transformNode(node.output),
1042+
returning: this.transformNode(node.returning),
10421043
})
10431044
}
10441045

src/query-builder/delete-query-builder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { QueryId } from '../util/query-id.js'
4141
import { freeze } from '../util/object-utils.js'
4242
import { KyselyPlugin } from '../plugin/kysely-plugin.js'
4343
import { WhereInterface } from './where-interface.js'
44-
import { ReturningInterface } from './returning-interface.js'
44+
import { MultiTableReturningInterface } from './returning-interface.js'
4545
import {
4646
isNoResultErrorConstructor,
4747
NoResultError,
@@ -82,7 +82,7 @@ import {
8282
export class DeleteQueryBuilder<DB, TB extends keyof DB, O>
8383
implements
8484
WhereInterface<DB, TB>,
85-
ReturningInterface<DB, TB, O>,
85+
MultiTableReturningInterface<DB, TB, O>,
8686
OutputInterface<DB, TB, O, 'deleted'>,
8787
OperationNodeSource,
8888
Compilable<O>,

src/query-builder/merge-query-builder.ts

+111-6
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ import {
2222
} from '../parser/join-parser.js'
2323
import { parseMergeThen, parseMergeWhen } from '../parser/merge-parser.js'
2424
import { ReferenceExpression } from '../parser/reference-parser.js'
25-
import { ReturningAllRow, ReturningRow } from '../parser/returning-parser.js'
26-
import { parseSelectAll, parseSelectArg } from '../parser/select-parser.js'
25+
import {
26+
ReturningAllRow,
27+
ReturningCallbackRow,
28+
ReturningRow,
29+
} from '../parser/returning-parser.js'
30+
import {
31+
parseSelectAll,
32+
parseSelectArg,
33+
SelectCallback,
34+
SelectExpression,
35+
} from '../parser/select-parser.js'
2736
import { TableExpression } from '../parser/table-parser.js'
2837
import { parseTop } from '../parser/top-parser.js'
2938
import {
@@ -58,10 +67,13 @@ import {
5867
SelectExpressionFromOutputCallback,
5968
SelectExpressionFromOutputExpression,
6069
} from './output-interface.js'
70+
import { MultiTableReturningInterface } from './returning-interface.js'
6171
import { UpdateQueryBuilder } from './update-query-builder.js'
6272

6373
export class MergeQueryBuilder<DB, TT extends keyof DB, O>
64-
implements OutputInterface<DB, TT, O>
74+
implements
75+
MultiTableReturningInterface<DB, TT, O>,
76+
OutputInterface<DB, TT, O>
6577
{
6678
readonly #props: MergeQueryBuilderProps
6779

@@ -215,6 +227,44 @@ export class MergeQueryBuilder<DB, TT extends keyof DB, O>
215227
})
216228
}
217229

230+
returning<SE extends SelectExpression<DB, TT>>(
231+
selections: ReadonlyArray<SE>,
232+
): MergeQueryBuilder<DB, TT, ReturningRow<DB, TT, O, SE>>
233+
234+
returning<CB extends SelectCallback<DB, TT>>(
235+
callback: CB,
236+
): MergeQueryBuilder<DB, TT, ReturningCallbackRow<DB, TT, O, CB>>
237+
238+
returning<SE extends SelectExpression<DB, TT>>(
239+
selection: SE,
240+
): MergeQueryBuilder<DB, TT, ReturningRow<DB, TT, O, SE>>
241+
242+
returning(args: any): any {
243+
return new MergeQueryBuilder({
244+
...this.#props,
245+
queryNode: QueryNode.cloneWithReturning(
246+
this.#props.queryNode,
247+
parseSelectArg(args),
248+
),
249+
})
250+
}
251+
252+
returningAll<T extends TT>(
253+
table: T,
254+
): MergeQueryBuilder<DB, TT, ReturningAllRow<DB, T, O>>
255+
256+
returningAll(): MergeQueryBuilder<DB, TT, ReturningAllRow<DB, TT, O>>
257+
258+
returningAll(table?: any): any {
259+
return new MergeQueryBuilder({
260+
...this.#props,
261+
queryNode: QueryNode.cloneWithReturning(
262+
this.#props.queryNode,
263+
parseSelectAll(table),
264+
),
265+
})
266+
}
267+
218268
output<OE extends OutputExpression<DB, TT>>(
219269
selections: readonly OE[],
220270
): MergeQueryBuilder<
@@ -274,7 +324,11 @@ export class WheneableMergeQueryBuilder<
274324
ST extends keyof DB,
275325
O,
276326
>
277-
implements Compilable<O>, OutputInterface<DB, TT, O>, OperationNodeSource
327+
implements
328+
Compilable<O>,
329+
MultiTableReturningInterface<DB, TT | ST, O>,
330+
OutputInterface<DB, TT, O>,
331+
OperationNodeSource
278332
{
279333
readonly #props: MergeQueryBuilderProps
280334

@@ -608,6 +662,54 @@ export class WheneableMergeQueryBuilder<
608662
return this.#whenNotMatched([lhs, op, rhs], true, true)
609663
}
610664

665+
returning<SE extends SelectExpression<DB, TT | ST>>(
666+
selections: ReadonlyArray<SE>,
667+
): WheneableMergeQueryBuilder<DB, TT, ST, ReturningRow<DB, TT | ST, O, SE>>
668+
669+
returning<CB extends SelectCallback<DB, TT | ST>>(
670+
callback: CB,
671+
): WheneableMergeQueryBuilder<
672+
DB,
673+
TT,
674+
ST,
675+
ReturningCallbackRow<DB, TT | ST, O, CB>
676+
>
677+
678+
returning<SE extends SelectExpression<DB, TT | ST>>(
679+
selection: SE,
680+
): WheneableMergeQueryBuilder<DB, TT, ST, ReturningRow<DB, TT | ST, O, SE>>
681+
682+
returning(args: any): any {
683+
return new WheneableMergeQueryBuilder({
684+
...this.#props,
685+
queryNode: QueryNode.cloneWithReturning(
686+
this.#props.queryNode,
687+
parseSelectArg(args),
688+
),
689+
})
690+
}
691+
692+
returningAll<T extends TT | ST>(
693+
table: T,
694+
): WheneableMergeQueryBuilder<DB, TT, ST, ReturningAllRow<DB, T, O>>
695+
696+
returningAll(): WheneableMergeQueryBuilder<
697+
DB,
698+
TT,
699+
ST,
700+
ReturningAllRow<DB, TT | ST, O>
701+
>
702+
703+
returningAll(table?: any): any {
704+
return new WheneableMergeQueryBuilder({
705+
...this.#props,
706+
queryNode: QueryNode.cloneWithReturning(
707+
this.#props.queryNode,
708+
parseSelectAll(table),
709+
),
710+
})
711+
}
712+
611713
output<OE extends OutputExpression<DB, TT>>(
612714
selections: readonly OE[],
613715
): WheneableMergeQueryBuilder<
@@ -788,9 +890,12 @@ export class WheneableMergeQueryBuilder<
788890
this.#props.queryId,
789891
)
790892

893+
const { adapter } = this.#props.executor
894+
const query = compiledQuery.query as MergeQueryNode
895+
791896
if (
792-
(compiledQuery.query as MergeQueryNode).output &&
793-
this.#props.executor.adapter.supportsOutput
897+
(query.returning && adapter.supportsReturning) ||
898+
(query.output && adapter.supportsOutput)
794899
) {
795900
return result.rows as any
796901
}

src/query-builder/returning-interface.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ReturningAllRow,
23
ReturningCallbackRow,
34
ReturningRow,
45
} from '../parser/returning-parser.js'
@@ -10,7 +11,7 @@ export interface ReturningInterface<DB, TB extends keyof DB, O> {
1011
* Allows you to return data from modified rows.
1112
*
1213
* On supported databases like PostgreSQL, this method can be chained to
13-
* `insert`, `update` and `delete` queries to return data.
14+
* `insert`, `update`, `delete` and `merge` queries to return data.
1415
*
1516
* Note that on SQLite you need to give aliases for the expressions to avoid
1617
* [this bug](https://sqlite.org/forum/forumpost/033daf0b32) in SQLite.
@@ -78,10 +79,29 @@ export interface ReturningInterface<DB, TB extends keyof DB, O> {
7879
): ReturningInterface<DB, TB, ReturningRow<DB, TB, O, SE>>
7980

8081
/**
81-
* Adds a `returning *` to an insert/update/delete query on databases
82+
* Adds a `returning *` to an insert/update/delete/merge query on databases
8283
* that support `returning` such as PostgreSQL.
8384
*
8485
* Also see the {@link returning} method.
8586
*/
8687
returningAll(): ReturningInterface<DB, TB, Selectable<DB[TB]>>
8788
}
89+
90+
export interface MultiTableReturningInterface<DB, TB extends keyof DB, O>
91+
extends ReturningInterface<DB, TB, O> {
92+
/**
93+
* Adds a `returning *` or `returning table.*` to an insert/update/delete/merge
94+
* query on databases that support `returning` such as PostgreSQL.
95+
*
96+
* Also see the {@link returning} method.
97+
*/
98+
returningAll<T extends TB>(
99+
tables: ReadonlyArray<T>,
100+
): MultiTableReturningInterface<DB, TB, ReturningAllRow<DB, T, O>>
101+
102+
returningAll<T extends TB>(
103+
table: T,
104+
): MultiTableReturningInterface<DB, TB, ReturningAllRow<DB, T, O>>
105+
106+
returningAll(): ReturningInterface<DB, TB, Selectable<DB[TB]>>
107+
}

src/query-builder/update-query-builder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { freeze } from '../util/object-utils.js'
4848
import { UpdateResult } from './update-result.js'
4949
import { KyselyPlugin } from '../plugin/kysely-plugin.js'
5050
import { WhereInterface } from './where-interface.js'
51-
import { ReturningInterface } from './returning-interface.js'
51+
import { MultiTableReturningInterface } from './returning-interface.js'
5252
import {
5353
isNoResultErrorConstructor,
5454
NoResultError,
@@ -83,7 +83,7 @@ import {
8383
export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
8484
implements
8585
WhereInterface<DB, TB>,
86-
ReturningInterface<DB, TB, O>,
86+
MultiTableReturningInterface<DB, TB, O>,
8787
OutputInterface<DB, TB, O>,
8888
OperationNodeSource,
8989
Compilable<O>,

src/query-compiler/default-query-compiler.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,11 @@ export class DefaultQueryCompiler
15851585
this.compileList(node.whens, ' ')
15861586
}
15871587

1588+
if (node.returning) {
1589+
this.append(' ')
1590+
this.visitNode(node.returning)
1591+
}
1592+
15881593
if (node.output) {
15891594
this.append(' ')
15901595
this.visitNode(node.output)

0 commit comments

Comments
 (0)