From a5e97679df7f91962ad10de01810f532df03b789 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sun, 31 Dec 2023 15:46:02 +0200 Subject: [PATCH 01/34] OutputNode. --- src/index.ts | 1 + .../operation-node-transformer.ts | 11 +++++ src/operation-node/operation-node-visitor.ts | 3 ++ src/operation-node/operation-node.ts | 1 + src/operation-node/output-node.ts | 43 +++++++++++++++++++ src/query-compiler/default-query-compiler.ts | 12 ++++++ 6 files changed, 71 insertions(+) create mode 100644 src/operation-node/output-node.ts diff --git a/src/index.ts b/src/index.ts index 4aec54bb0..2ab5e9c86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,6 +197,7 @@ export * from './operation-node/json-path-leg-node.js' export * from './operation-node/json-path-node.js' export * from './operation-node/json-operator-chain-node.js' export * from './operation-node/tuple-node.js' +export * from './operation-node/output-node.js' export * from './util/column-type.js' export * from './util/compilable.js' diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 727ea2753..b6e3aa92e 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -88,6 +88,7 @@ import { JSONPathLegNode } from './json-path-leg-node.js' import { JSONOperatorChainNode } from './json-operator-chain-node.js' import { TupleNode } from './tuple-node.js' import { AddIndexNode } from './add-index-node.js' +import { OutputNode } from './output-node.js' /** * Transforms an operation node tree into another one. @@ -210,6 +211,7 @@ export class OperationNodeTransformer { JSONOperatorChainNode: this.transformJSONOperatorChain.bind(this), TupleNode: this.transformTuple.bind(this), AddIndexNode: this.transformAddIndex.bind(this), + OutputNode: this.transformOutput.bind(this), }) transformNode(node: T): T { @@ -371,6 +373,7 @@ export class OperationNodeTransformer { replace: node.replace, explain: this.transformNode(node.explain), defaultValues: node.defaultValues, + output: this.transformNode(node.output), }) } @@ -993,6 +996,14 @@ export class OperationNodeTransformer { }) } + protected transformOutput(node: OutputNode): OutputNode { + return requireAllProps({ + kind: 'OutputNode', + selections: this.transformNodeList(node.selections), + into: this.transformNode(node.into), + }) + } + protected transformDataType(node: DataTypeNode): DataTypeNode { // An Object.freezed leaf node. No need to clone. return node diff --git a/src/operation-node/operation-node-visitor.ts b/src/operation-node/operation-node-visitor.ts index 4e48300bc..88c4494e4 100644 --- a/src/operation-node/operation-node-visitor.ts +++ b/src/operation-node/operation-node-visitor.ts @@ -90,6 +90,7 @@ import { JSONPathLegNode } from './json-path-leg-node.js' import { JSONOperatorChainNode } from './json-operator-chain-node.js' import { TupleNode } from './tuple-node.js' import { AddIndexNode } from './add-index-node.js' +import { OutputNode } from './output-node.js' export abstract class OperationNodeVisitor { protected readonly nodeStack: OperationNode[] = [] @@ -187,6 +188,7 @@ export abstract class OperationNodeVisitor { JSONOperatorChainNode: this.visitJSONOperatorChain.bind(this), TupleNode: this.visitTuple.bind(this), AddIndexNode: this.visitAddIndex.bind(this), + OutputNode: this.visitOutput.bind(this), }) protected readonly visitNode = (node: OperationNode): void => { @@ -292,4 +294,5 @@ export abstract class OperationNodeVisitor { protected abstract visitJSONOperatorChain(node: JSONOperatorChainNode): void protected abstract visitTuple(node: TupleNode): void protected abstract visitAddIndex(node: AddIndexNode): void + protected abstract visitOutput(node: OutputNode): void } diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts index d40bb551c..e244ce539 100644 --- a/src/operation-node/operation-node.ts +++ b/src/operation-node/operation-node.ts @@ -86,6 +86,7 @@ export type OperationNodeKind = | 'JSONOperatorChainNode' | 'TupleNode' | 'AddIndexNode' + | 'OutputNode' export interface OperationNode { readonly kind: OperationNodeKind diff --git a/src/operation-node/output-node.ts b/src/operation-node/output-node.ts new file mode 100644 index 000000000..9a08bcd3d --- /dev/null +++ b/src/operation-node/output-node.ts @@ -0,0 +1,43 @@ +import { freeze } from '../util/object-utils.js' +import { OperationNode } from './operation-node.js' + +export interface OutputNode extends OperationNode { + readonly kind: 'OutputNode' + readonly selections: ReadonlyArray + readonly into?: OperationNode +} + +/** + * @internal + */ +export const OutputNode = freeze({ + is(node: OperationNode): node is OutputNode { + return node.kind === 'OutputNode' + }, + + create(selections: ReadonlyArray): OutputNode { + return freeze({ + kind: 'OutputNode', + selections: freeze(selections), + }) + }, + + cloneWithSelections( + output: OutputNode, + selections: ReadonlyArray + ): OutputNode { + return freeze({ + ...output, + selections: output.selections + ? freeze([...output.selections, ...selections]) + : freeze(selections), + }) + }, + + cloneWithInto(output: OutputNode, into: OperationNode): OutputNode { + return freeze({ + ...output, + into, + }) + }, +}) diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 64b214931..848a585af 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -105,6 +105,7 @@ import { JSONPathLegNode } from '../operation-node/json-path-leg-node.js' import { JSONOperatorChainNode } from '../operation-node/json-operator-chain-node.js' import { TupleNode } from '../operation-node/tuple-node.js' import { AddIndexNode } from '../operation-node/add-index-node.js' +import { OutputNode } from '../operation-node/output-node.js' export class DefaultQueryCompiler extends OperationNodeVisitor @@ -1466,6 +1467,17 @@ export class DefaultQueryCompiler } } + protected override visitOutput(node: OutputNode): void { + this.append('output ') + + this.compileList(node.selections) + + if (node.into) { + this.append(' into ') + this.visitNode(node.into) + } + } + protected append(str: string): void { this.#sql += str } From 33e9884e53fe323af3762373e73db36076bcec41 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sun, 31 Dec 2023 16:02:49 +0200 Subject: [PATCH 02/34] add to insert query compilation. --- src/operation-node/insert-query-node.ts | 2 ++ src/query-compiler/default-query-compiler.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/operation-node/insert-query-node.ts b/src/operation-node/insert-query-node.ts index 7d2ace57f..f30cb27c0 100644 --- a/src/operation-node/insert-query-node.ts +++ b/src/operation-node/insert-query-node.ts @@ -4,6 +4,7 @@ import { ExplainNode } from './explain-node.js' import { OnConflictNode } from './on-conflict-node.js' import { OnDuplicateKeyNode } from './on-duplicate-key-node.js' import { OperationNode } from './operation-node.js' +import { OutputNode } from './output-node.js' import { ReturningNode } from './returning-node.js' import { TableNode } from './table-node.js' import { WithNode } from './with-node.js' @@ -23,6 +24,7 @@ export interface InsertQueryNode extends OperationNode { readonly replace?: boolean readonly explain?: ExplainNode readonly defaultValues?: boolean + readonly output?: OutputNode } /** diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 848a585af..58ba47acc 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -306,6 +306,11 @@ export class DefaultQueryCompiler this.append(')') } + if (node.output) { + this.append(' ') + this.visitNode(node.output) + } + if (node.values) { this.append(' ') this.visitNode(node.values) From 87a1ad2f0c5f9dc4f7219a195e7155f6b3e20cfe Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sun, 31 Dec 2023 19:15:48 +0200 Subject: [PATCH 03/34] QueryNode.cloneWithOutput. --- src/operation-node/query-node.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/operation-node/query-node.ts b/src/operation-node/query-node.ts index 90c3a5565..e1ac519be 100644 --- a/src/operation-node/query-node.ts +++ b/src/operation-node/query-node.ts @@ -11,6 +11,12 @@ import { OperationNode } from './operation-node.js' import { ExplainNode } from './explain-node.js' import { ExplainFormat } from '../util/explainable.js' import { Expression } from '../expression/expression.js' +import { OutputNode } from './output-node.js' +import { + SelectArg, + SelectExpression, + parseSelectArg, +} from '../parser/select-parser.js' export type QueryNode = | SelectQueryNode @@ -22,6 +28,7 @@ type HasJoins = { joins?: ReadonlyArray } type HasWhere = { where?: WhereNode } type HasReturning = { returning?: ReturningNode } type HasExplain = { explain?: ExplainNode } +type HasOutput = { output?: OutputNode } /** * @internal @@ -81,4 +88,16 @@ export const QueryNode = freeze({ explain: ExplainNode.create(format, options?.toOperationNode()), }) }, + + cloneWithOutput( + node: T, + selections: ReadonlyArray + ): T { + return freeze({ + ...node, + output: node.output + ? OutputNode.cloneWithSelections(node.output, selections) + : OutputNode.create(selections), + }) + }, }) From ec31f2cf8d8bceae4f059f587f85fceb014abc8d Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sun, 31 Dec 2023 19:16:28 +0200 Subject: [PATCH 04/34] OutputInterface with output method only. --- src/query-builder/output-interface.ts | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/query-builder/output-interface.ts diff --git a/src/query-builder/output-interface.ts b/src/query-builder/output-interface.ts new file mode 100644 index 000000000..f7122ada8 --- /dev/null +++ b/src/query-builder/output-interface.ts @@ -0,0 +1,71 @@ +import { ExpressionBuilder } from '../expression/expression-builder.js' +import { AliasedExpressionOrFactory } from '../parser/expression-parser.js' +import { ReturningRow } from '../parser/returning-parser.js' +import { + AnyAliasedColumnWithTable, + AnyColumnWithTable, +} from '../util/type-utils.js' + +export interface OutputInterface< + DB, + TB extends keyof DB, + O, + OP extends OutputPrefix = OutputPrefix +> { + /** + * TODO: ... + */ + output>( + selections: ReadonlyArray + ): OutputInterface< + DB, + TB, + ReturningRow> + > + + output>( + callback: CB + ): OutputInterface< + DB, + TB, + ReturningRow> + > + + output>( + selection: OE + ): OutputInterface< + DB, + TB, + ReturningRow> + > +} + +export type OutputPrefix = 'deleted' | 'inserted' + +export type OutputDatabase = { + [K in OP]: DB[TB] +} + +export type OutputExpression< + DB, + TB extends keyof DB, + OP extends OutputPrefix, + ODB = OutputDatabase, + OTB extends keyof ODB = keyof ODB +> = + | AnyAliasedColumnWithTable + | AnyColumnWithTable + | AliasedExpressionOrFactory + +export type OutputCallback = ( + eb: ExpressionBuilder, OP> +) => ReadonlyArray> + +export type SelectExpressionFromOutputExpression = + OE extends `${OutputPrefix}.${infer C}` ? C : OE + +export type SelectExpressionFromOutputCallback = CB extends ( + eb: ExpressionBuilder +) => ReadonlyArray + ? SelectExpressionFromOutputExpression + : never From 5839b6f097e80f0517623011836d8ed34930735c Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sun, 31 Dec 2023 19:16:46 +0200 Subject: [PATCH 05/34] enable returning @ mssql adapter. --- src/dialect/mssql/mssql-adapter.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dialect/mssql/mssql-adapter.ts b/src/dialect/mssql/mssql-adapter.ts index 48b40f297..115093fbe 100644 --- a/src/dialect/mssql/mssql-adapter.ts +++ b/src/dialect/mssql/mssql-adapter.ts @@ -13,9 +13,7 @@ export class MssqlAdapter extends DialectAdapterBase { } get supportsReturning(): boolean { - // mssql should support returning with the `output` clause. - // we need to figure this out when we'll introduce support for it. - return false + return true } async acquireMigrationLock(db: Kysely): Promise { From 575e3ea594728b424ac8c8cba9683243a94b0129 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sun, 31 Dec 2023 19:22:31 +0200 Subject: [PATCH 06/34] add output to insert query builder. --- src/query-builder/insert-query-builder.ts | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/query-builder/insert-query-builder.ts b/src/query-builder/insert-query-builder.ts index fc160ed14..b378e47db 100644 --- a/src/query-builder/insert-query-builder.ts +++ b/src/query-builder/insert-query-builder.ts @@ -60,10 +60,18 @@ import { Explainable, ExplainFormat } from '../util/explainable.js' import { Expression } from '../expression/expression.js' import { KyselyTypeError } from '../util/type-error.js' import { Streamable } from '../util/streamable.js' +import { + OutputCallback, + OutputExpression, + OutputInterface, + SelectExpressionFromOutputCallback, + SelectExpressionFromOutputExpression, +} from './output-interface.js' export class InsertQueryBuilder implements ReturningInterface, + OutputInterface, OperationNodeSource, Compilable, Explainable, @@ -607,6 +615,40 @@ export class InsertQueryBuilder }) } + output>( + selections: readonly OE[] + ): InsertQueryBuilder< + DB, + TB, + ReturningRow> + > + + output>( + callback: CB + ): InsertQueryBuilder< + DB, + TB, + ReturningRow> + > + + output>( + selection: OE + ): InsertQueryBuilder< + DB, + TB, + ReturningRow> + > + + output(args: any): any { + return new InsertQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectArg(args) + ), + }) + } + /** * Simply calls the provided function passing `this` as the only argument. `$call` returns * what the provided function returns. From f7b2996a15ecbbb74b68c8e554124f954c0f23eb Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 11:30:01 +0200 Subject: [PATCH 07/34] add outputAll. --- src/query-builder/insert-query-builder.ts | 13 +++++++++++++ src/query-builder/output-interface.ts | 22 +++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/query-builder/insert-query-builder.ts b/src/query-builder/insert-query-builder.ts index b378e47db..d7c35b68b 100644 --- a/src/query-builder/insert-query-builder.ts +++ b/src/query-builder/insert-query-builder.ts @@ -33,6 +33,7 @@ import { OnDuplicateKeyNode } from '../operation-node/on-duplicate-key-node.js' import { InsertResult } from './insert-result.js' import { KyselyPlugin } from '../plugin/kysely-plugin.js' import { + ReturningAllRow, ReturningCallbackRow, ReturningRow, } from '../parser/returning-parser.js' @@ -649,6 +650,18 @@ export class InsertQueryBuilder }) } + outputAll( + table: 'inserted' + ): InsertQueryBuilder> { + return new InsertQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectAll(table) + ), + }) + } + /** * Simply calls the provided function passing `this` as the only argument. `$call` returns * what the provided function returns. diff --git a/src/query-builder/output-interface.ts b/src/query-builder/output-interface.ts index f7122ada8..c83b117bc 100644 --- a/src/query-builder/output-interface.ts +++ b/src/query-builder/output-interface.ts @@ -1,6 +1,6 @@ import { ExpressionBuilder } from '../expression/expression-builder.js' import { AliasedExpressionOrFactory } from '../parser/expression-parser.js' -import { ReturningRow } from '../parser/returning-parser.js' +import { ReturningAllRow, ReturningRow } from '../parser/returning-parser.js' import { AnyAliasedColumnWithTable, AnyColumnWithTable, @@ -20,7 +20,8 @@ export interface OutputInterface< ): OutputInterface< DB, TB, - ReturningRow> + ReturningRow>, + OP > output>( @@ -28,7 +29,8 @@ export interface OutputInterface< ): OutputInterface< DB, TB, - ReturningRow> + ReturningRow>, + OP > output>( @@ -36,13 +38,23 @@ export interface OutputInterface< ): OutputInterface< DB, TB, - ReturningRow> + ReturningRow>, + OP > + + /** + * TODO: ... + */ + outputAll(table: OP): OutputInterface, OP> } export type OutputPrefix = 'deleted' | 'inserted' -export type OutputDatabase = { +export type OutputDatabase< + DB, + TB extends keyof DB, + OP extends OutputPrefix = OutputPrefix +> = { [K in OP]: DB[TB] } From aae6e70fc52fabf325c5af1cfacc6617c1aef6aa Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 12:06:15 +0200 Subject: [PATCH 08/34] add to delete query compilation. --- src/operation-node/delete-query-node.ts | 2 ++ src/query-compiler/default-query-compiler.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/operation-node/delete-query-node.ts b/src/operation-node/delete-query-node.ts index 977dde1f4..1e99dc73a 100644 --- a/src/operation-node/delete-query-node.ts +++ b/src/operation-node/delete-query-node.ts @@ -10,6 +10,7 @@ import { OrderByNode } from './order-by-node.js' import { OrderByItemNode } from './order-by-item-node.js' import { ExplainNode } from './explain-node.js' import { UsingNode } from './using-node.js' +import { OutputNode } from './output-node.js' export interface DeleteQueryNode extends OperationNode { readonly kind: 'DeleteQueryNode' @@ -22,6 +23,7 @@ export interface DeleteQueryNode extends OperationNode { readonly orderBy?: OrderByNode readonly limit?: LimitNode readonly explain?: ExplainNode + readonly output?: OutputNode } /** diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 58ba47acc..cc04a77e5 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -366,6 +366,11 @@ export class DefaultQueryCompiler this.append('delete ') this.visitNode(node.from) + if (node.output) { + this.append(' ') + this.visitNode(node.output) + } + if (node.using) { this.append(' ') this.visitNode(node.using) From a27ee6b3b26c0971d9542f87e34eb2da9a4c8f9a Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 12:23:01 +0200 Subject: [PATCH 09/34] add to delete query builder. --- src/query-builder/delete-query-builder.ts | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/query-builder/delete-query-builder.ts b/src/query-builder/delete-query-builder.ts index 13d662441..a4cd26d96 100644 --- a/src/query-builder/delete-query-builder.ts +++ b/src/query-builder/delete-query-builder.ts @@ -71,11 +71,19 @@ import { ValueExpression, parseValueExpression, } from '../parser/value-parser.js' +import { + OutputCallback, + OutputExpression, + OutputInterface, + SelectExpressionFromOutputCallback, + SelectExpressionFromOutputExpression, +} from './output-interface.js' export class DeleteQueryBuilder implements WhereInterface, ReturningInterface, + OutputInterface, OperationNodeSource, Compilable, Explainable, @@ -564,6 +572,52 @@ export class DeleteQueryBuilder }) } + output>( + selections: readonly OE[] + ): DeleteQueryBuilder< + DB, + TB, + ReturningRow> + > + + output>( + callback: CB + ): DeleteQueryBuilder< + DB, + TB, + ReturningRow> + > + + output>( + selection: OE + ): DeleteQueryBuilder< + DB, + TB, + ReturningRow> + > + + output(args: any): any { + return new DeleteQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectArg(args) + ), + }) + } + + outputAll( + table: 'deleted' + ): DeleteQueryBuilder> { + return new DeleteQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectAll(table) + ), + }) + } + /** * Adds an `order by` clause to the query. * From c493875f15b6b3b0f5145d1eb74ba13711742991 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 12:29:15 +0200 Subject: [PATCH 10/34] add to update query compilation. --- src/operation-node/update-query-node.ts | 2 ++ src/query-compiler/default-query-compiler.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/operation-node/update-query-node.ts b/src/operation-node/update-query-node.ts index d249c8cc0..e86131085 100644 --- a/src/operation-node/update-query-node.ts +++ b/src/operation-node/update-query-node.ts @@ -9,6 +9,7 @@ import { WhereNode } from './where-node.js' import { WithNode } from './with-node.js' import { FromNode } from './from-node.js' import { ExplainNode } from './explain-node.js' +import { OutputNode } from './output-node.js' export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode @@ -22,6 +23,7 @@ export interface UpdateQueryNode extends OperationNode { readonly returning?: ReturningNode readonly with?: WithNode readonly explain?: ExplainNode + readonly output?: OutputNode } /** diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index cc04a77e5..f0631a361 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -738,6 +738,11 @@ export class DefaultQueryCompiler this.compileList(node.updates) } + if (node.output) { + this.append(' ') + this.visitNode(node.output) + } + if (node.from) { this.append(' ') this.visitNode(node.from) From 9700448b660df468fec7c76cacd5b2cf41fdae8a Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 12:54:44 +0200 Subject: [PATCH 11/34] add to update query builder. --- src/query-builder/output-interface.ts | 8 ++- src/query-builder/update-query-builder.ts | 59 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/query-builder/output-interface.ts b/src/query-builder/output-interface.ts index c83b117bc..f42980cef 100644 --- a/src/query-builder/output-interface.ts +++ b/src/query-builder/output-interface.ts @@ -61,7 +61,7 @@ export type OutputDatabase< export type OutputExpression< DB, TB extends keyof DB, - OP extends OutputPrefix, + OP extends OutputPrefix = OutputPrefix, ODB = OutputDatabase, OTB extends keyof ODB = keyof ODB > = @@ -69,7 +69,11 @@ export type OutputExpression< | AnyColumnWithTable | AliasedExpressionOrFactory -export type OutputCallback = ( +export type OutputCallback< + DB, + TB extends keyof DB, + OP extends OutputPrefix = OutputPrefix +> = ( eb: ExpressionBuilder, OP> ) => ReadonlyArray> diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts index c43fe483e..7f6d822d7 100644 --- a/src/query-builder/update-query-builder.ts +++ b/src/query-builder/update-query-builder.ts @@ -20,6 +20,7 @@ import { SelectCallback, } from '../parser/select-parser.js' import { + ReturningAllRow, ReturningCallbackRow, ReturningRow, } from '../parser/returning-parser.js' @@ -69,11 +70,20 @@ import { KyselyTypeError } from '../util/type-error.js' import { Streamable } from '../util/streamable.js' import { ExpressionOrFactory } from '../parser/expression-parser.js' import { ValueExpression } from '../parser/value-parser.js' +import { + OutputCallback, + OutputExpression, + OutputInterface, + OutputPrefix, + SelectExpressionFromOutputCallback, + SelectExpressionFromOutputExpression, +} from './output-interface.js' export class UpdateQueryBuilder implements WhereInterface, ReturningInterface, + OutputInterface, OperationNodeSource, Compilable, Explainable, @@ -598,6 +608,55 @@ export class UpdateQueryBuilder }) } + output>( + selections: readonly OE[] + ): UpdateQueryBuilder< + DB, + UT, + TB, + ReturningRow> + > + + output>( + callback: CB + ): UpdateQueryBuilder< + DB, + UT, + TB, + ReturningRow> + > + + output>( + selection: OE + ): UpdateQueryBuilder< + DB, + UT, + TB, + ReturningRow> + > + + output(args: any): any { + return new UpdateQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectArg(args) + ), + }) + } + + outputAll( + table: OutputPrefix + ): UpdateQueryBuilder> { + return new UpdateQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectAll(table) + ), + }) + } + /** * Simply calls the provided function passing `this` as the only argument. `$call` returns * what the provided function returns. From 17122ec4dcfd676268a1fe2a2e1097b2eba81df9 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 13:13:40 +0200 Subject: [PATCH 12/34] jsdocs. --- src/query-builder/output-interface.ts | 55 +++++++++++++++++++++++- src/query-builder/returning-interface.ts | 2 + 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/query-builder/output-interface.ts b/src/query-builder/output-interface.ts index f42980cef..5268ca4f7 100644 --- a/src/query-builder/output-interface.ts +++ b/src/query-builder/output-interface.ts @@ -13,7 +13,55 @@ export interface OutputInterface< OP extends OutputPrefix = OutputPrefix > { /** - * TODO: ... + * Allows you to return data from modified rows. + * + * On supported databases like MS SQL Server (MSSQL), this method can be chained + * to `insert`, `update` and `delete` queries to return data. + * + * Also see the {@link outputAll} method. + * + * ### Examples + * + * Return one column: + * + * ```ts + * const { id } = await db + * .insertInto('person') + * .output('inserted.id') + * .values({ + * first_name: 'Jennifer', + * last_name: 'Aniston' + * }) + * .executeTakeFirst() + * ``` + * + * Return multiple columns: + * + * ```ts + * const { id, first_name } = await db + * .updateTable('person') + * .set({ first_name: 'John', last_name: 'Doe' }) + * .output([ + * 'deleted.first_name as old_first_name', + * 'deleted.last_name as old_last_name', + * 'inserted.first_name as new_first_name', + * 'inserted.last_name as new_last_name', + * ]) + * .where('created_at', '<', new Date()) + * .executeTakeFirst() + * ``` + * + * Return arbitrary expressions: + * + * ```ts + * import { sql } from 'kysely' + * + * const { id, full_name } = await db + * .deleteFrom('person') + * .output((eb) => sql`concat(${eb.ref('deleted.first_name')}, ' ', ${eb.ref('deleted.last_name')})`.as('full_name') + * .where('created_at', '<', new Date()) + * .executeTakeFirst() + * ``` */ output>( selections: ReadonlyArray @@ -43,7 +91,10 @@ export interface OutputInterface< > /** - * TODO: ... + * Adds an `output {prefix}.*` to an `insert`/`update`/`delete` query on databases + * that support `output` such as MS SQL Server (MSSQL). + * + * Also see the {@link output} method. */ outputAll(table: OP): OutputInterface, OP> } diff --git a/src/query-builder/returning-interface.ts b/src/query-builder/returning-interface.ts index 2ddbfb562..b814d43be 100644 --- a/src/query-builder/returning-interface.ts +++ b/src/query-builder/returning-interface.ts @@ -80,6 +80,8 @@ export interface ReturningInterface { /** * Adds a `returning *` to an insert/update/delete query on databases * that support `returning` such as PostgreSQL. + * + * Also see the {@link returning} method. */ returningAll(): ReturningInterface> } From 29605c68823c5f553980081dfd706b543633579e Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 13:18:47 +0200 Subject: [PATCH 13/34] supportsOutput. --- src/dialect/dialect-adapter-base.ts | 4 ++++ src/dialect/mssql/mssql-adapter.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dialect/dialect-adapter-base.ts b/src/dialect/dialect-adapter-base.ts index 1bb22fb73..71801c369 100644 --- a/src/dialect/dialect-adapter-base.ts +++ b/src/dialect/dialect-adapter-base.ts @@ -20,6 +20,10 @@ export abstract class DialectAdapterBase implements DialectAdapter { return false } + get supportsOutput(): boolean { + return false + } + abstract acquireMigrationLock( db: Kysely, options: MigrationLockOptions diff --git a/src/dialect/mssql/mssql-adapter.ts b/src/dialect/mssql/mssql-adapter.ts index 115093fbe..33f944d92 100644 --- a/src/dialect/mssql/mssql-adapter.ts +++ b/src/dialect/mssql/mssql-adapter.ts @@ -12,7 +12,7 @@ export class MssqlAdapter extends DialectAdapterBase { return true } - get supportsReturning(): boolean { + get supportsOutput(): boolean { return true } From f8a2a07fdc193e37e58720901b90e3822c41c680 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 13:22:10 +0200 Subject: [PATCH 14/34] supportsOutput. --- src/dialect/dialect-adapter.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/dialect/dialect-adapter.ts b/src/dialect/dialect-adapter.ts index 0a5a7a3ee..10fe299f8 100644 --- a/src/dialect/dialect-adapter.ts +++ b/src/dialect/dialect-adapter.ts @@ -31,6 +31,12 @@ export interface DialectAdapter { */ readonly supportsReturning: boolean + /** + * Whether or not this dialect supports the `output` clause in inserts + * updates and deletes. + */ + readonly supportsOutput: boolean + /** * This method is used to acquire a lock for the migrations so that * it's not possible for two migration operations to run in parallel. From b6f158b2daaf221e687b68ea20a686cf61ca8b87 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 13:24:45 +0200 Subject: [PATCH 15/34] use supportsOutput @ insert query builder. --- src/query-builder/insert-query-builder.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/query-builder/insert-query-builder.ts b/src/query-builder/insert-query-builder.ts index d7c35b68b..d48817437 100644 --- a/src/query-builder/insert-query-builder.ts +++ b/src/query-builder/insert-query-builder.ts @@ -873,14 +873,19 @@ export class InsertQueryBuilder */ async execute(): Promise[]> { const compiledQuery = this.compile() - const query = compiledQuery.query as InsertQueryNode const result = await this.#props.executor.executeQuery( compiledQuery, this.#props.queryId ) - if (this.#props.executor.adapter.supportsReturning && query.returning) { + const { adapter } = this.#props.executor + const query = compiledQuery.query as InsertQueryNode + + if ( + (query.returning && adapter.supportsReturning) || + (query.output && adapter.supportsOutput) + ) { return result.rows as any } From 6198f1e65755879867d99f72f0dfd11df08da1ba Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 13:28:17 +0200 Subject: [PATCH 16/34] use supportsOutput @ delete query builder. --- src/query-builder/delete-query-builder.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/query-builder/delete-query-builder.ts b/src/query-builder/delete-query-builder.ts index a4cd26d96..5b5d4d1cf 100644 --- a/src/query-builder/delete-query-builder.ts +++ b/src/query-builder/delete-query-builder.ts @@ -904,14 +904,19 @@ export class DeleteQueryBuilder */ async execute(): Promise[]> { const compiledQuery = this.compile() - const query = compiledQuery.query as DeleteQueryNode const result = await this.#props.executor.executeQuery( compiledQuery, this.#props.queryId ) - if (this.#props.executor.adapter.supportsReturning && query.returning) { + const { adapter } = this.#props.executor + const query = compiledQuery.query as DeleteQueryNode + + if ( + (query.returning && adapter.supportsReturning) || + (query.output && adapter.supportsOutput) + ) { return result.rows as any } From b4e818469b000f2f92bc59f58ceadb4a661f52ad Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 13:31:23 +0200 Subject: [PATCH 17/34] use supportsOutput @ update query builder. --- src/query-builder/update-query-builder.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts index 7f6d822d7..a889e99a8 100644 --- a/src/query-builder/update-query-builder.ts +++ b/src/query-builder/update-query-builder.ts @@ -875,14 +875,19 @@ export class UpdateQueryBuilder */ async execute(): Promise[]> { const compiledQuery = this.compile() - const query = compiledQuery.query as UpdateQueryNode const result = await this.#props.executor.executeQuery( compiledQuery, this.#props.queryId ) - if (this.#props.executor.adapter.supportsReturning && query.returning) { + const { adapter } = this.#props.executor + const query = compiledQuery.query as UpdateQueryNode + + if ( + (query.returning && adapter.supportsReturning) || + (query.output && adapter.supportsOutput) + ) { return result.rows as any } From 9f1b38c8ce6b8736498285caa228e0e0a349e0dd Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 14:14:44 +0200 Subject: [PATCH 18/34] compilation fixes. --- src/operation-node/operation-node-transformer.ts | 3 ++- src/operation-node/output-node.ts | 8 -------- src/query-compiler/default-query-compiler.ts | 6 ------ 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index b6e3aa92e..27cc99c84 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -396,6 +396,7 @@ export class OperationNodeTransformer { orderBy: this.transformNode(node.orderBy), limit: this.transformNode(node.limit), explain: this.transformNode(node.explain), + output: this.transformNode(node.output), }) } @@ -499,6 +500,7 @@ export class OperationNodeTransformer { returning: this.transformNode(node.returning), with: this.transformNode(node.with), explain: this.transformNode(node.explain), + output: this.transformNode(node.output), }) } @@ -1000,7 +1002,6 @@ export class OperationNodeTransformer { return requireAllProps({ kind: 'OutputNode', selections: this.transformNodeList(node.selections), - into: this.transformNode(node.into), }) } diff --git a/src/operation-node/output-node.ts b/src/operation-node/output-node.ts index 9a08bcd3d..6f28c6b4b 100644 --- a/src/operation-node/output-node.ts +++ b/src/operation-node/output-node.ts @@ -4,7 +4,6 @@ import { OperationNode } from './operation-node.js' export interface OutputNode extends OperationNode { readonly kind: 'OutputNode' readonly selections: ReadonlyArray - readonly into?: OperationNode } /** @@ -33,11 +32,4 @@ export const OutputNode = freeze({ : freeze(selections), }) }, - - cloneWithInto(output: OutputNode, into: OperationNode): OutputNode { - return freeze({ - ...output, - into, - }) - }, }) diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index f0631a361..b3a4e5257 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -1484,13 +1484,7 @@ export class DefaultQueryCompiler protected override visitOutput(node: OutputNode): void { this.append('output ') - this.compileList(node.selections) - - if (node.into) { - this.append(' into ') - this.visitNode(node.into) - } } protected append(str: string): void { From 909450cd9a205ebd4e4ac6a89368e95fa33ef633 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 14:16:44 +0200 Subject: [PATCH 19/34] export output interface. --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 2ab5e9c86..233b84aef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export * from './expression/expression-wrapper.js' export * from './query-builder/where-interface.js' export * from './query-builder/returning-interface.js' +export * from './query-builder/output-interface.js' export * from './query-builder/having-interface.js' export * from './query-builder/select-query-builder.js' export * from './query-builder/insert-query-builder.js' From f0395daee8be97926e9ade934567ad98d93ca362 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 17:54:51 +0200 Subject: [PATCH 20/34] insert test cases. --- test/node/src/insert.test.ts | 117 +++++++++++++++++++++++++++++------ test/node/src/test-setup.ts | 26 ++------ 2 files changed, 104 insertions(+), 39 deletions(-) diff --git a/test/node/src/insert.test.ts b/test/node/src/insert.test.ts index ae1ad472b..07ed733d8 100644 --- a/test/node/src/insert.test.ts +++ b/test/node/src/insert.test.ts @@ -231,9 +231,7 @@ for (const dialect of DIALECTS) { ]) }) - // TODO: revisit this when mssql has output clause support as it also supports values expression - // https://database.guide/values-clause-in-sql-server/#:~:text=In%20SQL%20Server%2C%20VALUES%20is,statement%20or%20the%20FROM%20clause. - if (dialect === 'postgres') { + if (dialect === 'postgres' || dialect === 'mssql') { it('should insert the result of a values expression', async () => { const query = ctx.db .insertInto('person') @@ -251,7 +249,11 @@ for (const dialect of DIALECTS) { ) .select(['t.a', 't.b']) ) - .returning(['first_name', 'gender']) + .$call((qb) => + dialect === 'postgres' + ? qb.returning(['first_name', 'gender']) + : qb.output(['inserted.first_name', 'inserted.gender']) + ) testSql(query, dialect, { postgres: { @@ -259,7 +261,10 @@ for (const dialect of DIALECTS) { parameters: [1, 'foo', 2, 'bar'], }, mysql: NOT_SUPPORTED, - mssql: NOT_SUPPORTED, + mssql: { + sql: 'insert into "person" ("first_name", "gender") output "inserted"."first_name", "inserted"."gender" select "t"."a", "t"."b" from (values (@1, @2), (@3, @4)) as t(a, b)', + parameters: [1, 'foo', 2, 'bar'], + }, sqlite: NOT_SUPPORTED, }) @@ -822,24 +827,24 @@ for (const dialect of DIALECTS) { first_name: 'Sylvester', last_name: 'Barson', }) + }) - it('should insert a row, returning some fields of inserted row and conditionally returning additional fields', async () => { - const condition = true + it('should insert a row, returning some fields of inserted row and conditionally returning additional fields', async () => { + const condition = true - const query = ctx.db - .insertInto('person') - .values({ - first_name: 'Foo', - last_name: 'Barson', - gender: 'other', - }) - .returning('first_name') - .$if(condition, (qb) => qb.returning('last_name')) + const query = ctx.db + .insertInto('person') + .values({ + first_name: 'Foo', + last_name: 'Barson', + gender: 'other', + }) + .returning('first_name') + .$if(condition, (qb) => qb.returning('last_name')) - const result = await query.executeTakeFirstOrThrow() + const result = await query.executeTakeFirstOrThrow() - expect(result.last_name).to.equal('Barson') - }) + expect(result.last_name).to.equal('Barson') }) it('should insert a row and return data using `returningAll`', async () => { @@ -902,6 +907,80 @@ for (const dialect of DIALECTS) { expect(people).to.eql(values) }) } + + if (dialect === 'mssql') { + it('should insert a row and return data using `output`', async () => { + const result = await ctx.db + .insertInto('person') + .output([ + 'inserted.first_name', + 'inserted.last_name', + 'inserted.gender', + ]) + .values({ + gender: 'other', + first_name: ctx.db + .selectFrom('person') + .select(sql`max(first_name)`.as('max_first_name')), + last_name: sql`concat(cast(${'Bar'} as varchar), cast(${'son'} as varchar))`, + }) + .executeTakeFirst() + + expect(result).to.eql({ + first_name: 'Sylvester', + last_name: 'Barson', + gender: 'other', + }) + + expect(await getNewestPerson(ctx.db)).to.eql({ + first_name: 'Sylvester', + last_name: 'Barson', + }) + }) + + it('should insert a row, returning some fields of inserted row and conditionally returning additional fields', async () => { + const condition = true + + const query = ctx.db + .insertInto('person') + .output('inserted.first_name') + .$if(condition, (qb) => qb.output('inserted.last_name')) + .values({ + first_name: 'Foo', + last_name: 'Barson', + gender: 'other', + }) + + const result = await query.executeTakeFirstOrThrow() + + expect(result.last_name).to.equal('Barson') + }) + + it('should insert a row and return data using `outputAll`', async () => { + const result = await ctx.db + .insertInto('person') + .outputAll('inserted') + .values({ + gender: 'other', + first_name: ctx.db + .selectFrom('person') + .select(sql`max(first_name)`.as('max_first_name')), + last_name: sql`concat(cast(${'Bar'} as varchar), cast(${'son'} as varchar))`, + }) + .executeTakeFirst() + + expect(result).to.containSubset({ + first_name: 'Sylvester', + last_name: 'Barson', + gender: 'other', + }) + + expect(await getNewestPerson(ctx.db)).to.eql({ + first_name: 'Sylvester', + last_name: 'Barson', + }) + }) + } }) async function getNewestPerson( diff --git a/test/node/src/test-setup.ts b/test/node/src/test-setup.ts index 3a683abe9..e122a3362 100644 --- a/test/node/src/test-setup.ts +++ b/test/node/src/test-setup.ts @@ -432,26 +432,12 @@ export async function insert( } if (dialect === 'mssql') { - // TODO: use insert into "table" (...) output inserted.id values (...) when its implemented - return await ctx.db.connection().execute(async (db) => { - await qb.executeTakeFirstOrThrow() - - const { query } = qb.compile() - - const table = - query.kind === 'InsertQueryNode' && - [query.into.table.schema?.name, query.into.table.identifier.name] - .filter(Boolean) - .join('.') - - const { - rows: [{ id }], - } = await sql<{ id: number }>`select IDENT_CURRENT(${sql.lit( - table - )}) as id`.execute(db) - - return Number(id) - }) + const { id } = await qb + .output('inserted.id' as any) + .$castTo<{ id: number }>() + .executeTakeFirstOrThrow() + + return id } const { insertId } = await qb.executeTakeFirstOrThrow() From 18152ba2bbf2f12b26c4e887324323210267238e Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 18:03:07 +0200 Subject: [PATCH 21/34] delete test cases. --- test/node/src/delete.test.ts | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/node/src/delete.test.ts b/test/node/src/delete.test.ts index c9a49c409..d381360f8 100644 --- a/test/node/src/delete.test.ts +++ b/test/node/src/delete.test.ts @@ -867,5 +867,56 @@ for (const dialect of DIALECTS) { ) }) } + + if (dialect === 'mssql') { + it('should return deleted rows when `output` is used', async () => { + const query = ctx.db + .deleteFrom('person') + .output(['deleted.first_name', 'deleted.last_name as last']) + .where('gender', '=', 'male') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'delete from "person" output "deleted"."first_name", "deleted"."last_name" as "last" where "gender" = @1', + parameters: ['male'], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.execute() + + expect(result).to.have.length(2) + expect(Object.keys(result[0]).sort()).to.eql(['first_name', 'last']) + expect(result).to.containSubset([ + { first_name: 'Arnold', last: 'Schwarzenegger' }, + { first_name: 'Sylvester', last: 'Stallone' }, + ]) + }) + + it('conditional `output` statement should add optional fields', async () => { + const condition = true + + const query = ctx.db + .deleteFrom('person') + .output('deleted.first_name') + .$if(condition, (qb) => qb.output('deleted.last_name')) + .where('gender', '=', 'female') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'delete from "person" output "deleted"."first_name", "deleted"."last_name" where "gender" = @1', + parameters: ['female'], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.executeTakeFirstOrThrow() + expect(result.last_name).to.equal('Aniston') + }) + } }) } From 7d83363531c7a1afda13b7da77f90f816b36dfd3 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 18:11:23 +0200 Subject: [PATCH 22/34] update test cases. --- test/node/src/update.test.ts | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/node/src/update.test.ts b/test/node/src/update.test.ts index 952594cc0..70ffd31c2 100644 --- a/test/node/src/update.test.ts +++ b/test/node/src/update.test.ts @@ -640,5 +640,51 @@ for (const dialect of DIALECTS) { } }) } + + if (dialect === 'mssql') { + it('should update some rows and return updated rows when `output` is used', async () => { + const query = ctx.db + .updateTable('person') + .set({ last_name: 'Barson' }) + .output(['inserted.first_name', 'inserted.last_name']) + .where('gender', '=', 'male') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'update "person" set "last_name" = @1 output "inserted"."first_name", "inserted"."last_name" where "gender" = @2', + parameters: ['Barson', 'male'], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.execute() + + expect(result).to.have.length(2) + expect(Object.keys(result[0]).sort()).to.eql([ + 'first_name', + 'last_name', + ]) + expect(result).to.containSubset([ + { first_name: 'Arnold', last_name: 'Barson' }, + { first_name: 'Sylvester', last_name: 'Barson' }, + ]) + }) + + it('should update all rows, returning some fields of updated rows, and conditionally returning additional fields', async () => { + const condition = true + + const query = ctx.db + .updateTable('person') + .set({ last_name: 'Barson' }) + .output('inserted.first_name') + .$if(condition, (qb) => qb.output('inserted.last_name')) + + const result = await query.executeTakeFirstOrThrow() + + expect(result.last_name).to.equal('Barson') + }) + } }) } From ffd1634e0ea228ad5a6ca1d0d91c14a83e9bd046 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 1 Jan 2024 19:41:42 +0200 Subject: [PATCH 23/34] typings tests. --- .../test-d/delete-query-builder.test-d.ts | 94 +++++++++++++++---- test/typings/test-d/insert.test-d.ts | 66 +++++++++++++ 2 files changed, 143 insertions(+), 17 deletions(-) diff --git a/test/typings/test-d/delete-query-builder.test-d.ts b/test/typings/test-d/delete-query-builder.test-d.ts index 5eca057f3..4bf53439b 100644 --- a/test/typings/test-d/delete-query-builder.test-d.ts +++ b/test/typings/test-d/delete-query-builder.test-d.ts @@ -1,5 +1,5 @@ import { expectError, expectType } from 'tsd' -import { Kysely, DeleteResult, Selectable } from '..' +import { Kysely, DeleteResult, Selectable, sql } from '..' import { Database, Person, Pet } from '../shared' async function testDelete(db: Kysely) { @@ -76,8 +76,10 @@ async function testDelete(db: Kysely) { .using('pet') .leftJoin('person', 'NO_SUCH_COLUMN', 'pet.owner_id') ) +} - const r8 = await db +async function testReturning(db: Kysely) { + const r1 = await db .deleteFrom('person') .using(['person', 'pet']) .leftJoin('toy', 'toy.pet_id', 'pet.id') @@ -86,16 +88,16 @@ async function testDelete(db: Kysely) { ) .returningAll('person') .execute() - expectType[]>(r8) + expectType[]>(r1) - const r9 = await db + const r2 = await db .deleteFrom('pet') .where('pet.species', '=', 'cat') .returningAll('pet') .execute() - expectType[]>(r9) + expectType[]>(r2) - const r10 = await db + const r3 = await db .deleteFrom('person') .using(['person', 'pet']) .leftJoin('toy', 'toy.pet_id', 'pet.id') @@ -120,9 +122,9 @@ async function testDelete(db: Kysely) { price: number | null pet_id: string | null }[] - >(r10) + >(r3) - const r11 = await db + const r4 = await db .deleteFrom('person') .innerJoin('pet', 'pet.owner_id', 'person.id') .where('pet.species', '=', 'dog') @@ -143,23 +145,23 @@ async function testDelete(db: Kysely) { owner_id: number species: 'dog' | 'cat' }[] - >(r11) + >(r4) - const r12 = await db + const r5 = await db .deleteFrom('pet') .where('pet.species', '=', 'cat') .returningAll(['pet']) .execute() - expectType[]>(r12) + expectType[]>(r5) - const r13 = await db + const r6 = await db .deleteFrom('pet') .where('pet.species', '=', 'dog') .returningAll() .execute() - expectType[]>(r13) + expectType[]>(r6) - const r14 = await db + const r7 = await db .deleteFrom('person') .using(['person', 'pet']) .leftJoin('toy', 'toy.pet_id', 'pet.id') @@ -184,14 +186,14 @@ async function testDelete(db: Kysely) { price: number | null pet_id: string | null }[] - >(r14) + >(r7) - const r15 = await db + const r8 = await db .deleteFrom('person as p') .where('p.first_name', '=', 'Jennifer') .returning('p.id') .executeTakeFirstOrThrow() - expectType<{ id: number }>(r15) + expectType<{ id: number }>(r8) } async function testIf(db: Kysely) { @@ -244,3 +246,61 @@ async function testIf(db: Kysely) { f20?: string }>(r) } + +async function testOutput(db: Kysely) { + const r1 = await db + .deleteFrom('pet') + .outputAll('deleted') + .where('pet.species', '=', 'cat') + .execute() + expectType[]>(r1) + + const r2 = await db + .deleteFrom('person as p') + .output('deleted.id') + .where('p.first_name', '=', 'Jennifer') + .executeTakeFirstOrThrow() + expectType<{ id: number }>(r2) + + const r3 = await db + .deleteFrom('person as p') + .output(['deleted.id', 'deleted.last_name as surname']) + .where('p.first_name', '=', 'Jennifer') + .executeTakeFirstOrThrow() + expectType<{ id: number; surname: string | null }>(r3) + + const r4 = await db + .deleteFrom('person') + .output((eb) => [ + 'deleted.age', + eb + .fn('concat', [ + eb.ref('deleted.first_name'), + sql.lit(' '), + 'deleted.last_name', + ]) + .as('full_name'), + ]) + .where('deleted_at', '<', new Date()) + .executeTakeFirstOrThrow() + expectType<{ age: number; full_name: string }>(r4) + + // Non-existent column + expectError(db.deleteFrom('person').output('deleted.NO_SUCH_COLUMN')) + + // Wrong prefix + expectError(db.deleteFrom('person').output('inserted.id')) + expectError(db.deleteFrom('person').outputAll('inserted')) + + // Non-existent prefix + expectError(db.deleteFrom('person').output('NO_SUCH_PREFIX.id')) + expectError(db.deleteFrom('person').outputAll('NO_SUCH_PREFIX')) + + // table prefix + expectError(db.deleteFrom('person').output('person.id')) + expectError(db.deleteFrom('person').outputAll('person')) + + // No prefix + expectError(db.deleteFrom('person').output('id')) + expectError(db.deleteFrom('person').outputAll()) +} diff --git a/test/typings/test-d/insert.test-d.ts b/test/typings/test-d/insert.test-d.ts index d4804f477..affea4ad6 100644 --- a/test/typings/test-d/insert.test-d.ts +++ b/test/typings/test-d/insert.test-d.ts @@ -187,3 +187,69 @@ async function testReturning(db: Kysely) { // Non-existent column expectError(db.insertInto('person').values(person).returning('not_column')) } + +async function testOutput(db: Kysely) { + const person = { + first_name: 'Jennifer', + last_name: 'Aniston', + gender: 'other' as const, + age: 30, + } + + // One returning expression + const r1 = await db + .insertInto('person') + .output('inserted.id') + .values(person) + .executeTakeFirst() + + expectType<{ id: number } | undefined>(r1) + + // Multiple returning expressions + const r2 = await db + .insertInto('person') + .output(['inserted.id', 'inserted.first_name as fn']) + .values(person) + .execute() + + expectType<{ id: number; fn: string }[]>(r2) + + // Non-column reference returning expressions + const r3 = await db + .insertInto('person') + .output([ + 'inserted.id', + sql`concat(inserted.first_name, ' ', inserted.last_name)`.as( + 'full_name' + ), + ]) + .values(person) + .execute() + + expectType<{ id: number; full_name: string }[]>(r3) + + const r4 = await db + .insertInto('movie') + .outputAll('inserted') + .values({ stars: 5 }) + .executeTakeFirstOrThrow() + + expectType<{ id: string; stars: number }>(r4) + + // Non-existent column + expectError( + db.insertInto('person').output('inserted.not_column').values(person) + ) + + // Without prefix + expectError(db.insertInto('person').output('age').values(person)) + expectError(db.insertInto('person').outputAll().values(person)) + + // Non-existent prefix + expectError(db.insertInto('person').output('foo.age').values(person)) + expectError(db.insertInto('person').outputAll('foo').values(person)) + + // Wrong prefix + expectError(db.insertInto('person').output('deleted.age').values(person)) + expectError(db.insertInto('person').outputAll('deleted').values(person)) +} From fcda02439fa2ad5b2f5fedbc9b3baef8fe158364 Mon Sep 17 00:00:00 2001 From: Igal Klebanov Date: Sun, 25 Feb 2024 11:29:29 +0200 Subject: [PATCH 24/34] re-add test cases. --- test/node/src/delete.test.ts | 51 ++++++++++++++++++++++ test/node/src/insert.test.ts | 82 ++++++++++++++++++++++++++++++++++-- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/test/node/src/delete.test.ts b/test/node/src/delete.test.ts index 2ed2d3f50..efa0c61d4 100644 --- a/test/node/src/delete.test.ts +++ b/test/node/src/delete.test.ts @@ -912,5 +912,56 @@ for (const dialect of DIALECTS) { expect(result).to.be.instanceOf(DeleteResult) }) } + + if (dialect === 'mssql') { + it('should return deleted rows when `output` is used', async () => { + const query = ctx.db + .deleteFrom('person') + .output(['deleted.first_name', 'deleted.last_name as last']) + .where('gender', '=', 'male') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'delete from "person" output "deleted"."first_name", "deleted"."last_name" as "last" where "gender" = @1', + parameters: ['male'], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.execute() + + expect(result).to.have.length(2) + expect(Object.keys(result[0]).sort()).to.eql(['first_name', 'last']) + expect(result).to.containSubset([ + { first_name: 'Arnold', last: 'Schwarzenegger' }, + { first_name: 'Sylvester', last: 'Stallone' }, + ]) + }) + + it('conditional `output` statement should add optional fields', async () => { + const condition = true + + const query = ctx.db + .deleteFrom('person') + .output('deleted.first_name') + .$if(condition, (qb) => qb.output('deleted.last_name')) + .where('gender', '=', 'female') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'delete from "person" output "deleted"."first_name", "deleted"."last_name" where "gender" = @1', + parameters: ['female'], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.executeTakeFirstOrThrow() + expect(result.last_name).to.equal('Aniston') + }) + } }) } diff --git a/test/node/src/insert.test.ts b/test/node/src/insert.test.ts index 5a457f8ef..2b69a6c09 100644 --- a/test/node/src/insert.test.ts +++ b/test/node/src/insert.test.ts @@ -141,7 +141,7 @@ for (const dialect of DIALECTS) { }) }) - if (dialect !== 'mssql') { + if (dialect === 'postgres' || dialect === 'mysql' || dialect === 'sqlite') { it('should insert one row with expressions', async () => { const query = ctx.db.insertInto('person').values(({ selectFrom }) => ({ first_name: selectFrom('pet') @@ -252,7 +252,7 @@ for (const dialect of DIALECTS) { .$call((qb) => dialect === 'postgres' ? qb.returning(['first_name', 'gender']) - : qb.output(['inserted.first_name', 'inserted.gender']) + : qb.output(['inserted.first_name', 'inserted.gender']), ) testSql(query, dialect, { @@ -915,7 +915,7 @@ for (const dialect of DIALECTS) { .top(1) .columns(['first_name', 'gender']) .expression((eb) => - eb.selectFrom('pet').select(['name', eb.val('other').as('gender')]) + eb.selectFrom('pet').select(['name', eb.val('other').as('gender')]), ) testSql(query, dialect, { @@ -937,7 +937,7 @@ for (const dialect of DIALECTS) { .top(50, 'percent') .columns(['first_name', 'gender']) .expression((eb) => - eb.selectFrom('pet').select(['name', eb.val('other').as('gender')]) + eb.selectFrom('pet').select(['name', eb.val('other').as('gender')]), ) testSql(query, dialect, { @@ -953,6 +953,80 @@ for (const dialect of DIALECTS) { await query.executeTakeFirstOrThrow() }) } + + if (dialect === 'mssql') { + it('should insert a row and return data using `output`', async () => { + const result = await ctx.db + .insertInto('person') + .output([ + 'inserted.first_name', + 'inserted.last_name', + 'inserted.gender', + ]) + .values({ + gender: 'other', + first_name: ctx.db + .selectFrom('person') + .select(sql`max(first_name)`.as('max_first_name')), + last_name: sql`concat(cast(${'Bar'} as varchar), cast(${'son'} as varchar))`, + }) + .executeTakeFirst() + + expect(result).to.eql({ + first_name: 'Sylvester', + last_name: 'Barson', + gender: 'other', + }) + + expect(await getNewestPerson(ctx.db)).to.eql({ + first_name: 'Sylvester', + last_name: 'Barson', + }) + }) + + it('should insert a row, returning some fields of inserted row and conditionally returning additional fields', async () => { + const condition = true + + const query = ctx.db + .insertInto('person') + .output('inserted.first_name') + .$if(condition, (qb) => qb.output('inserted.last_name')) + .values({ + first_name: 'Foo', + last_name: 'Barson', + gender: 'other', + }) + + const result = await query.executeTakeFirstOrThrow() + + expect(result.last_name).to.equal('Barson') + }) + + it('should insert a row and return data using `outputAll`', async () => { + const result = await ctx.db + .insertInto('person') + .outputAll('inserted') + .values({ + gender: 'other', + first_name: ctx.db + .selectFrom('person') + .select(sql`max(first_name)`.as('max_first_name')), + last_name: sql`concat(cast(${'Bar'} as varchar), cast(${'son'} as varchar))`, + }) + .executeTakeFirst() + + expect(result).to.containSubset({ + first_name: 'Sylvester', + last_name: 'Barson', + gender: 'other', + }) + + expect(await getNewestPerson(ctx.db)).to.eql({ + first_name: 'Sylvester', + last_name: 'Barson', + }) + }) + } }) async function getNewestPerson( From c3c86ca43403097a3497382ab0b9f98ad30a755a Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 13:37:49 +0200 Subject: [PATCH 25/34] make supportsOutput optional. --- src/dialect/dialect-adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialect/dialect-adapter.ts b/src/dialect/dialect-adapter.ts index 7467aa78b..d4020278b 100644 --- a/src/dialect/dialect-adapter.ts +++ b/src/dialect/dialect-adapter.ts @@ -35,7 +35,7 @@ export interface DialectAdapter { * Whether or not this dialect supports the `output` clause in inserts * updates and deletes. */ - readonly supportsOutput: boolean + readonly supportsOutput?: boolean /** * This method is used to acquire a lock for the migrations so that From 83fbb3a19d0fe2dafa63c208633ccf0eb10b2dc4 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 13:38:12 +0200 Subject: [PATCH 26/34] remove unused imports. --- src/operation-node/query-node.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/operation-node/query-node.ts b/src/operation-node/query-node.ts index 00399b6dc..38cfdf995 100644 --- a/src/operation-node/query-node.ts +++ b/src/operation-node/query-node.ts @@ -14,11 +14,6 @@ import { Expression } from '../expression/expression.js' import { MergeQueryNode } from './merge-query-node.js' import { TopNode } from './top-node.js' import { OutputNode } from './output-node.js' -import { - SelectArg, - SelectExpression, - parseSelectArg, -} from '../parser/select-parser.js' export type QueryNode = | SelectQueryNode @@ -100,17 +95,17 @@ export const QueryNode = freeze({ explain: ExplainNode.create(format, options?.toOperationNode()), }) }, - + cloneWithTop(node: T, top: TopNode): T { return freeze({ ...node, top, }) }, - + cloneWithOutput( node: T, - selections: ReadonlyArray + selections: ReadonlyArray, ): T { return freeze({ ...node, From f897cdc9a9b80515452f227b29de63a43b4d2c38 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 13:40:23 +0200 Subject: [PATCH 27/34] add merge example to output jsdocs. --- src/query-builder/output-interface.ts | 43 ++++++++++++++++++++------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/query-builder/output-interface.ts b/src/query-builder/output-interface.ts index 5268ca4f7..054c5e8fe 100644 --- a/src/query-builder/output-interface.ts +++ b/src/query-builder/output-interface.ts @@ -10,13 +10,13 @@ export interface OutputInterface< DB, TB extends keyof DB, O, - OP extends OutputPrefix = OutputPrefix + OP extends OutputPrefix = OutputPrefix, > { /** * Allows you to return data from modified rows. * * On supported databases like MS SQL Server (MSSQL), this method can be chained - * to `insert`, `update` and `delete` queries to return data. + * to `insert`, `update`, `delete` and `merge` queries to return data. * * Also see the {@link outputAll} method. * @@ -62,9 +62,30 @@ export interface OutputInterface< * .where('created_at', '<', new Date()) * .executeTakeFirst() * ``` + * + * Return the action performed on the row: + * + * ```ts + * await db + * .mergeInto('person') + * .using('pet', 'pet.owner_id', 'person.id') + * .whenMatched() + * .thenDelete() + * .whenNotMatched() + * .thenInsertValues({ + * first_name: 'John', + * last_name: 'Doe', + * gender: 'male' + * }) + * .output([ + * '$action', + * 'inserted.id as inserted_id', + * 'deleted.id as deleted_id', + * ]) + * ``` */ output>( - selections: ReadonlyArray + selections: ReadonlyArray, ): OutputInterface< DB, TB, @@ -73,7 +94,7 @@ export interface OutputInterface< > output>( - callback: CB + callback: CB, ): OutputInterface< DB, TB, @@ -82,7 +103,7 @@ export interface OutputInterface< > output>( - selection: OE + selection: OE, ): OutputInterface< DB, TB, @@ -91,7 +112,7 @@ export interface OutputInterface< > /** - * Adds an `output {prefix}.*` to an `insert`/`update`/`delete` query on databases + * Adds an `output {prefix}.*` to an `insert`/`update`/`delete`/`merge` query on databases * that support `output` such as MS SQL Server (MSSQL). * * Also see the {@link output} method. @@ -104,7 +125,7 @@ export type OutputPrefix = 'deleted' | 'inserted' export type OutputDatabase< DB, TB extends keyof DB, - OP extends OutputPrefix = OutputPrefix + OP extends OutputPrefix = OutputPrefix, > = { [K in OP]: DB[TB] } @@ -114,7 +135,7 @@ export type OutputExpression< TB extends keyof DB, OP extends OutputPrefix = OutputPrefix, ODB = OutputDatabase, - OTB extends keyof ODB = keyof ODB + OTB extends keyof ODB = keyof ODB, > = | AnyAliasedColumnWithTable | AnyColumnWithTable @@ -123,16 +144,16 @@ export type OutputExpression< export type OutputCallback< DB, TB extends keyof DB, - OP extends OutputPrefix = OutputPrefix + OP extends OutputPrefix = OutputPrefix, > = ( - eb: ExpressionBuilder, OP> + eb: ExpressionBuilder, OP>, ) => ReadonlyArray> export type SelectExpressionFromOutputExpression = OE extends `${OutputPrefix}.${infer C}` ? C : OE export type SelectExpressionFromOutputCallback = CB extends ( - eb: ExpressionBuilder + eb: ExpressionBuilder, ) => ReadonlyArray ? SelectExpressionFromOutputExpression : never From 4b649d7ca84696196fb2e0165ea9c5b71dccc754 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 14:29:27 +0200 Subject: [PATCH 28/34] handle merge output compilation. --- src/operation-node/merge-query-node.ts | 2 ++ src/query-compiler/default-query-compiler.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/operation-node/merge-query-node.ts b/src/operation-node/merge-query-node.ts index fff9eaa5a..7b202d4c0 100644 --- a/src/operation-node/merge-query-node.ts +++ b/src/operation-node/merge-query-node.ts @@ -2,6 +2,7 @@ import { freeze } from '../util/object-utils.js' import { AliasNode } from './alias-node.js' import { JoinNode } from './join-node.js' import { OperationNode } from './operation-node.js' +import { OutputNode } from './output-node.js' import { TableNode } from './table-node.js' import { TopNode } from './top-node.js' import { WhenNode } from './when-node.js' @@ -14,6 +15,7 @@ export interface MergeQueryNode extends OperationNode { readonly whens?: ReadonlyArray readonly with?: WithNode readonly top?: TopNode + readonly output?: OutputNode } /** diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 2958a4908..46494d197 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -1537,6 +1537,11 @@ export class DefaultQueryCompiler this.append(' ') this.compileList(node.whens) } + + if (node.output) { + this.append(' ') + this.visitNode(node.output) + } } protected override visitMatched(node: MatchedNode): void { @@ -1581,13 +1586,13 @@ export class DefaultQueryCompiler this.visitNode(node.dataType) this.append(')') } - + protected override visitFetch(node: FetchNode): void { this.append('fetch next ') this.visitNode(node.rowCount) this.append(` rows ${node.modifier}`) } - + protected override visitOutput(node: OutputNode): void { this.append('output ') this.compileList(node.selections) From 36865035a172e37e75910aa6476a90eaefd93ab6 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 15:20:23 +0200 Subject: [PATCH 29/34] add output @ merge query builder. --- src/query-builder/merge-query-builder.ts | 105 ++++++++++++++++++++++- src/query-builder/output-interface.ts | 1 - 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/query-builder/merge-query-builder.ts b/src/query-builder/merge-query-builder.ts index fc3c7a1cb..5574393bf 100644 --- a/src/query-builder/merge-query-builder.ts +++ b/src/query-builder/merge-query-builder.ts @@ -22,6 +22,8 @@ import { } from '../parser/join-parser.js' import { parseMergeThen, parseMergeWhen } from '../parser/merge-parser.js' import { ReferenceExpression } from '../parser/reference-parser.js' +import { ReturningAllRow, ReturningRow } from '../parser/returning-parser.js' +import { parseSelectAll } from '../parser/select-parser.js' import { TableExpression } from '../parser/table-parser.js' import { parseTop } from '../parser/top-parser.js' import { @@ -50,9 +52,19 @@ import { NoResultErrorConstructor, isNoResultErrorConstructor, } from './no-result-error.js' +import { + OutputCallback, + OutputExpression, + OutputInterface, + OutputPrefix, + SelectExpressionFromOutputCallback, + SelectExpressionFromOutputExpression, +} from './output-interface.js' import { UpdateQueryBuilder } from './update-query-builder.js' -export class MergeQueryBuilder { +export class MergeQueryBuilder + implements OutputInterface +{ readonly #props: MergeQueryBuilderProps constructor(props: MergeQueryBuilderProps) { @@ -171,6 +183,49 @@ export class MergeQueryBuilder { ), }) } + + output>( + selections: readonly OE[], + ): MergeQueryBuilder< + DB, + TT, + ReturningRow> + > + + output>( + callback: CB, + ): MergeQueryBuilder< + DB, + TT, + ReturningRow> + > + + output>( + selection: OE, + ): MergeQueryBuilder< + DB, + TT, + ReturningRow> + > + + output(args: any): any { + return new MergeQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput(this.#props.queryNode, args), + }) + } + + outputAll( + table: OutputPrefix, + ): MergeQueryBuilder> { + return new MergeQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectAll(table), + ), + }) + } } preventAwait( @@ -190,7 +245,7 @@ export class WheneableMergeQueryBuilder< ST extends keyof DB, O, > - implements Compilable, OperationNodeSource + implements Compilable, OutputInterface, OperationNodeSource { readonly #props: MergeQueryBuilderProps @@ -489,6 +544,52 @@ export class WheneableMergeQueryBuilder< return this.#whenNotMatched([lhs, op, rhs], true, true) } + output>( + selections: readonly OE[], + ): WheneableMergeQueryBuilder< + DB, + TT, + ST, + ReturningRow> + > + + output>( + callback: CB, + ): WheneableMergeQueryBuilder< + DB, + TT, + ST, + ReturningRow> + > + + output>( + selection: OE, + ): WheneableMergeQueryBuilder< + DB, + TT, + ST, + ReturningRow> + > + + output(args: any): any { + return new WheneableMergeQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput(this.#props.queryNode, args), + }) + } + + outputAll( + table: OutputPrefix, + ): WheneableMergeQueryBuilder> { + return new WheneableMergeQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectAll(table), + ), + }) + } + #whenNotMatched( args: any[], refRight: boolean = false, diff --git a/src/query-builder/output-interface.ts b/src/query-builder/output-interface.ts index 054c5e8fe..8a44502fe 100644 --- a/src/query-builder/output-interface.ts +++ b/src/query-builder/output-interface.ts @@ -78,7 +78,6 @@ export interface OutputInterface< * gender: 'male' * }) * .output([ - * '$action', * 'inserted.id as inserted_id', * 'deleted.id as deleted_id', * ]) From 75599428f7a6d52034c756a2aa599059ab3f960e Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 15:32:55 +0200 Subject: [PATCH 30/34] transformer. --- src/operation-node/operation-node-transformer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 262a68f9d..bd4b74ef1 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -1013,6 +1013,7 @@ export class OperationNodeTransformer { whens: this.transformNodeList(node.whens), with: this.transformNode(node.with), top: this.transformNode(node.top), + output: this.transformNode(node.output), }) } From a72a553d6fde5020e169b410db557fbc94dee58a Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 15:48:28 +0200 Subject: [PATCH 31/34] leftovers merge query builder. --- src/query-builder/merge-query-builder.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/query-builder/merge-query-builder.ts b/src/query-builder/merge-query-builder.ts index 5574393bf..f9e7e61b7 100644 --- a/src/query-builder/merge-query-builder.ts +++ b/src/query-builder/merge-query-builder.ts @@ -23,7 +23,7 @@ import { import { parseMergeThen, parseMergeWhen } from '../parser/merge-parser.js' import { ReferenceExpression } from '../parser/reference-parser.js' import { ReturningAllRow, ReturningRow } from '../parser/returning-parser.js' -import { parseSelectAll } from '../parser/select-parser.js' +import { parseSelectAll, parseSelectArg } from '../parser/select-parser.js' import { TableExpression } from '../parser/table-parser.js' import { parseTop } from '../parser/top-parser.js' import { @@ -211,7 +211,10 @@ export class MergeQueryBuilder output(args: any): any { return new MergeQueryBuilder({ ...this.#props, - queryNode: QueryNode.cloneWithOutput(this.#props.queryNode, args), + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectArg(args), + ), }) } @@ -574,7 +577,10 @@ export class WheneableMergeQueryBuilder< output(args: any): any { return new WheneableMergeQueryBuilder({ ...this.#props, - queryNode: QueryNode.cloneWithOutput(this.#props.queryNode, args), + queryNode: QueryNode.cloneWithOutput( + this.#props.queryNode, + parseSelectArg(args), + ), }) } @@ -717,6 +723,13 @@ export class WheneableMergeQueryBuilder< this.#props.queryId, ) + if ( + (compiledQuery.query as MergeQueryNode).output && + this.#props.executor.adapter.supportsOutput + ) { + return result.rows as any + } + return [new MergeResult(result.numAffectedRows) as any] } From 037a787b7799cf5894f418ddf2ca8e6b54ee00fb Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 18:46:02 +0200 Subject: [PATCH 32/34] some merge tests. --- test/node/src/merge.test.ts | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/test/node/src/merge.test.ts b/test/node/src/merge.test.ts index 9fbe1bb22..2083b0fc5 100644 --- a/test/node/src/merge.test.ts +++ b/test/node/src/merge.test.ts @@ -1,4 +1,4 @@ -import { MergeResult } from '../../..' +import { MergeResult, sql } from '../../..' import { DIALECTS, NOT_SUPPORTED, @@ -1004,6 +1004,60 @@ for (const dialect of DIALECTS.filter( expect(result).to.be.instanceOf(MergeResult) expect(result.numChangedRows).to.equal(2n) }) + + it('should perform a merge...using table simple on...when matched then delete output id query', async () => { + const expected = await ctx.db.selectFrom('pet').select('id').execute() + + const query = ctx.db + .mergeInto('pet') + .using('person', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .output('deleted.id') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'merge into "pet" using "person" on "pet"."owner_id" = "person"."id" when matched then delete output "deleted"."id";', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.execute() + + expect(result).to.eql(expected) + }) + + it('should perform a merge...using table simple on...when matched then update set name output deleted.name, inserted.name query', async () => { + const query = ctx.db + .mergeInto('pet') + .using('person', 'pet.owner_id', 'person.id') + .whenMatched() + .thenUpdateSet((eb) => ({ + name: sql`${eb.ref('person.first_name')} + '''s pet'`, + })) + .output(['deleted.name as old_name', 'inserted.name as new_name']) + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'merge into "pet" using "person" on "pet"."owner_id" = "person"."id" when matched then update set "name" = "person"."first_name" + \'\'\'s pet\' output "deleted"."name" as "old_name", "inserted"."name" as "new_name";', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.execute() + + expect(result).to.eql([ + { old_name: 'Catto', new_name: "Jennifer's pet" }, + { old_name: 'Doggo', new_name: "Arnold's pet" }, + { old_name: 'Hammo', new_name: "Sylvester's pet" }, + ]) + }) } }) } From b8464687a4cd93de6328ab6830a99ffc5df47736 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 19:03:48 +0200 Subject: [PATCH 33/34] merge typings tests. --- test/typings/test-d/merge.test-d.ts | 104 +++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/test/typings/test-d/merge.test-d.ts b/test/typings/test-d/merge.test-d.ts index 95b811781..bff938a03 100644 --- a/test/typings/test-d/merge.test-d.ts +++ b/test/typings/test-d/merge.test-d.ts @@ -7,11 +7,12 @@ import { MergeQueryBuilder, MergeResult, NotMatchedThenableMergeQueryBuilder, + Selectable, UpdateQueryBuilder, WheneableMergeQueryBuilder, sql, } from '..' -import { Database } from '../shared' +import { Database, Person } from '../shared' async function testMergeInto(db: Kysely) { db.mergeInto('person') @@ -420,3 +421,104 @@ async function testThenInsert( }), ) } + +async function testOutput(db: Kysely) { + // One returning expression + const r1 = await db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .output('deleted.id') + .executeTakeFirst() + + expectType<{ id: number } | undefined>(r1) + + // Multiple returning expressions + const r2 = await db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .output(['deleted.id', 'deleted.first_name as fn']) + .execute() + + expectType<{ id: number; fn: string }[]>(r2) + + // Non-column reference returning expressions + const r3 = await db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenUpdateSet('age', (eb) => eb(eb.ref('age'), '+', 20)) + .output([ + 'inserted.age', + sql`concat(deleted.first_name, ' ', deleted.last_name)`.as( + 'full_name', + ), + ]) + .execute() + + expectType<{ age: number; full_name: string }[]>(r3) + + // Return all columns + const r4 = await db + .mergeInto('person') + .using('pet', 'person.id', 'pet.owner_id') + .whenNotMatched() + .thenInsertValues({ + gender: 'female', + age: 15, + first_name: 'Jane', + }) + .outputAll('inserted') + .executeTakeFirstOrThrow() + + expectType>(r4) + + // Non-existent column + expectError( + db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .output('inserted.not_column'), + ) + + // Without prefix + expectError( + db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .output('age'), + ) + expectError( + db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .outputAll(), + ) + + // Non-existent prefix + expectError( + db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .output('foo.age'), + ) + expectError( + db + .mergeInto('person') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + .outputAll('foo'), + ) +} From 8ea1e492a2d40314b8f8d2cd0f390b39b7e0b8da Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 23 Mar 2024 19:18:58 +0200 Subject: [PATCH 34/34] fix returning types. --- src/parser/returning-parser.ts | 47 ++++++++++++++-------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/parser/returning-parser.ts b/src/parser/returning-parser.ts index abc8f8cf3..dac0eee72 100644 --- a/src/parser/returning-parser.ts +++ b/src/parser/returning-parser.ts @@ -1,38 +1,29 @@ import { DeleteResult } from '../query-builder/delete-result.js' import { InsertResult } from '../query-builder/insert-result.js' +import { MergeResult } from '../query-builder/merge-result.js' import { UpdateResult } from '../query-builder/update-result.js' import { Selection, AllSelection, CallbackSelection } from './select-parser.js' -export type ReturningRow< - DB, - TB extends keyof DB, - O, - SE, -> = O extends InsertResult +export type ReturningRow = O extends + | InsertResult + | DeleteResult + | UpdateResult + | MergeResult ? Selection - : O extends DeleteResult - ? Selection - : O extends UpdateResult - ? Selection - : O & Selection + : O & Selection -export type ReturningCallbackRow< - DB, - TB extends keyof DB, - O, - CB, -> = O extends InsertResult +export type ReturningCallbackRow = O extends + | InsertResult + | DeleteResult + | UpdateResult + | MergeResult ? CallbackSelection - : O extends DeleteResult - ? CallbackSelection - : O extends UpdateResult - ? CallbackSelection - : O & CallbackSelection + : O & CallbackSelection -export type ReturningAllRow = O extends InsertResult +export type ReturningAllRow = O extends + | InsertResult + | DeleteResult + | UpdateResult + | MergeResult ? AllSelection - : O extends DeleteResult - ? AllSelection - : O extends UpdateResult - ? AllSelection - : O & AllSelection + : O & AllSelection