Skip to content

Commit

Permalink
add OUTPUT clause support. (#828)
Browse files Browse the repository at this point in the history
* OutputNode.

* add to insert query compilation.

* QueryNode.cloneWithOutput.

* OutputInterface with output method only.

* enable returning @ mssql adapter.

* add output to insert query builder.

* add outputAll.

* add to delete query compilation.

* add to delete query builder.

* add to update query compilation.

* add to update query builder.

* jsdocs.

* supportsOutput.

* supportsOutput.

* use supportsOutput @ insert query builder.

* use supportsOutput @ delete query builder.

* use supportsOutput @ update query builder.

* compilation fixes.

* export output interface.

* insert test cases.

* delete test cases.

* update test cases.

* typings tests.

* re-add test cases.

* make supportsOutput optional.

* remove unused imports.

* add merge example to output jsdocs.

* handle merge output compilation.

* add output @ merge query builder.

* transformer.

* leftovers merge query builder.

* some merge tests.

* merge typings tests.

* fix returning types.
  • Loading branch information
igalklebanov authored Mar 24, 2024
1 parent 64a56f9 commit ecbcaf8
Show file tree
Hide file tree
Showing 29 changed files with 1,104 additions and 102 deletions.
4 changes: 4 additions & 0 deletions src/dialect/dialect-adapter-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export abstract class DialectAdapterBase implements DialectAdapter {
return false
}

get supportsOutput(): boolean {
return false
}

abstract acquireMigrationLock(
db: Kysely<any>,
options: MigrationLockOptions,
Expand Down
6 changes: 6 additions & 0 deletions src/dialect/dialect-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 2 additions & 4 deletions src/dialect/mssql/mssql-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ export class MssqlAdapter extends DialectAdapterBase {
return true
}

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
get supportsOutput(): boolean {
return true
}

async acquireMigrationLock(db: Kysely<any>): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -203,6 +204,7 @@ export * from './operation-node/merge-query-node.js'
export * from './operation-node/matched-node.js'
export * from './operation-node/fetch-node.js'
export * from './operation-node/top-node.js'
export * from './operation-node/output-node.js'

export * from './util/column-type.js'
export * from './util/compilable.js'
Expand Down
2 changes: 2 additions & 0 deletions src/operation-node/delete-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { OrderByItemNode } from './order-by-item-node.js'
import { ExplainNode } from './explain-node.js'
import { UsingNode } from './using-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'

export interface DeleteQueryNode extends OperationNode {
readonly kind: 'DeleteQueryNode'
Expand All @@ -24,6 +25,7 @@ export interface DeleteQueryNode extends OperationNode {
readonly limit?: LimitNode
readonly explain?: ExplainNode
readonly top?: TopNode
readonly output?: OutputNode
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/operation-node/insert-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { TopNode } from './top-node.js'
Expand All @@ -25,6 +26,7 @@ export interface InsertQueryNode extends OperationNode {
readonly explain?: ExplainNode
readonly defaultValues?: boolean
readonly top?: TopNode
readonly output?: OutputNode
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/operation-node/merge-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -14,6 +15,7 @@ export interface MergeQueryNode extends OperationNode {
readonly whens?: ReadonlyArray<WhenNode>
readonly with?: WithNode
readonly top?: TopNode
readonly output?: OutputNode
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/operation-node/operation-node-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import { AddIndexNode } from './add-index-node.js'
import { CastNode } from './cast-node.js'
import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'

/**
* Transforms an operation node tree into another one.
Expand Down Expand Up @@ -220,6 +221,7 @@ export class OperationNodeTransformer {
CastNode: this.transformCast.bind(this),
FetchNode: this.transformFetch.bind(this),
TopNode: this.transformTop.bind(this),
OutputNode: this.transformOutput.bind(this),
})

transformNode<T extends OperationNode | undefined>(node: T): T {
Expand Down Expand Up @@ -384,6 +386,7 @@ export class OperationNodeTransformer {
explain: this.transformNode(node.explain),
defaultValues: node.defaultValues,
top: this.transformNode(node.top),
output: this.transformNode(node.output),
})
}

Expand All @@ -407,6 +410,7 @@ export class OperationNodeTransformer {
limit: this.transformNode(node.limit),
explain: this.transformNode(node.explain),
top: this.transformNode(node.top),
output: this.transformNode(node.output),
})
}

Expand Down Expand Up @@ -514,6 +518,7 @@ export class OperationNodeTransformer {
explain: this.transformNode(node.explain),
limit: this.transformNode(node.limit),
top: this.transformNode(node.top),
output: this.transformNode(node.output),
})
}

Expand Down Expand Up @@ -1008,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),
})
}

Expand Down Expand Up @@ -1054,6 +1060,13 @@ export class OperationNodeTransformer {
})
}

protected transformOutput(node: OutputNode): OutputNode {
return requireAllProps<OutputNode>({
kind: 'OutputNode',
selections: this.transformNodeList(node.selections),
})
}

protected transformDataType(node: DataTypeNode): DataTypeNode {
// An Object.freezed leaf node. No need to clone.
return node
Expand Down
3 changes: 3 additions & 0 deletions src/operation-node/operation-node-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { AddIndexNode } from './add-index-node.js'
import { CastNode } from './cast-node.js'
import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'

export abstract class OperationNodeVisitor {
protected readonly nodeStack: OperationNode[] = []
Expand Down Expand Up @@ -197,6 +198,7 @@ export abstract class OperationNodeVisitor {
CastNode: this.visitCast.bind(this),
FetchNode: this.visitFetch.bind(this),
TopNode: this.visitTop.bind(this),
OutputNode: this.visitOutput.bind(this),
})

protected readonly visitNode = (node: OperationNode): void => {
Expand Down Expand Up @@ -307,4 +309,5 @@ export abstract class OperationNodeVisitor {
protected abstract visitCast(node: CastNode): void
protected abstract visitFetch(node: FetchNode): void
protected abstract visitTop(node: TopNode): void
protected abstract visitOutput(node: OutputNode): void
}
1 change: 1 addition & 0 deletions src/operation-node/operation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export type OperationNodeKind =
| 'CastNode'
| 'FetchNode'
| 'TopNode'
| 'OutputNode'

export interface OperationNode {
readonly kind: OperationNodeKind
Expand Down
35 changes: 35 additions & 0 deletions src/operation-node/output-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { freeze } from '../util/object-utils.js'
import { OperationNode } from './operation-node.js'

export interface OutputNode extends OperationNode {
readonly kind: 'OutputNode'
readonly selections: ReadonlyArray<OperationNode>
}

/**
* @internal
*/
export const OutputNode = freeze({
is(node: OperationNode): node is OutputNode {
return node.kind === 'OutputNode'
},

create(selections: ReadonlyArray<OperationNode>): OutputNode {
return freeze({
kind: 'OutputNode',
selections: freeze(selections),
})
},

cloneWithSelections(
output: OutputNode,
selections: ReadonlyArray<OperationNode>
): OutputNode {
return freeze({
...output,
selections: output.selections
? freeze([...output.selections, ...selections])
: freeze(selections),
})
},
})
14 changes: 14 additions & 0 deletions src/operation-node/query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ExplainFormat } from '../util/explainable.js'
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'

export type QueryNode =
| SelectQueryNode
Expand All @@ -26,6 +27,7 @@ type HasWhere = { where?: WhereNode }
type HasReturning = { returning?: ReturningNode }
type HasExplain = { explain?: ExplainNode }
type HasTop = { top?: TopNode }
type HasOutput = { output?: OutputNode }

/**
* @internal
Expand Down Expand Up @@ -100,4 +102,16 @@ export const QueryNode = freeze({
top,
})
},

cloneWithOutput<T extends HasOutput>(
node: T,
selections: ReadonlyArray<SelectionNode>,
): T {
return freeze({
...node,
output: node.output
? OutputNode.cloneWithSelections(node.output, selections)
: OutputNode.create(selections),
})
},
})
2 changes: 2 additions & 0 deletions src/operation-node/update-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FromNode } from './from-node.js'
import { ExplainNode } from './explain-node.js'
import { LimitNode } from './limit-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'

export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode

Expand All @@ -26,6 +27,7 @@ export interface UpdateQueryNode extends OperationNode {
readonly explain?: ExplainNode
readonly limit?: LimitNode
readonly top?: TopNode
readonly output?: OutputNode
}

/**
Expand Down
47 changes: 19 additions & 28 deletions src/parser/returning-parser.ts
Original file line number Diff line number Diff line change
@@ -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<DB, TB extends keyof DB, O, SE> = O extends
| InsertResult
| DeleteResult
| UpdateResult
| MergeResult
? Selection<DB, TB, SE>
: O extends DeleteResult
? Selection<DB, TB, SE>
: O extends UpdateResult
? Selection<DB, TB, SE>
: O & Selection<DB, TB, SE>
: O & Selection<DB, TB, SE>

export type ReturningCallbackRow<
DB,
TB extends keyof DB,
O,
CB,
> = O extends InsertResult
export type ReturningCallbackRow<DB, TB extends keyof DB, O, CB> = O extends
| InsertResult
| DeleteResult
| UpdateResult
| MergeResult
? CallbackSelection<DB, TB, CB>
: O extends DeleteResult
? CallbackSelection<DB, TB, CB>
: O extends UpdateResult
? CallbackSelection<DB, TB, CB>
: O & CallbackSelection<DB, TB, CB>
: O & CallbackSelection<DB, TB, CB>

export type ReturningAllRow<DB, TB extends keyof DB, O> = O extends InsertResult
export type ReturningAllRow<DB, TB extends keyof DB, O> = O extends
| InsertResult
| DeleteResult
| UpdateResult
| MergeResult
? AllSelection<DB, TB>
: O extends DeleteResult
? AllSelection<DB, TB>
: O extends UpdateResult
? AllSelection<DB, TB>
: O & AllSelection<DB, TB>
: O & AllSelection<DB, TB>
Loading

0 comments on commit ecbcaf8

Please sign in to comment.