Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add MERGE query support. #700

Merged
merged 66 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
42f457e
add merge query node.
igalklebanov Sep 16, 2023
1c60272
add merge query to visitor.
igalklebanov Sep 16, 2023
6a7f54a
add merge query to transformer.
igalklebanov Sep 16, 2023
0dca364
Merge branch 'master' into merge
igalklebanov Sep 22, 2023
4f3c604
add using as join type.
igalklebanov Sep 22, 2023
fbddc99
tighten using to JoinNode.
igalklebanov Sep 22, 2023
0fcb705
make table optional in UpdateQueryNode.
igalklebanov Sep 22, 2023
2dd3b25
add MergeQueryNode to RootOperationNode union.
igalklebanov Sep 22, 2023
07c1112
add SimpleTableReference type.
igalklebanov Sep 22, 2023
b232b73
add mergeInto to query creator.
igalklebanov Sep 22, 2023
0ee03a7
MergeQueryBuilder initial commit.
igalklebanov Sep 22, 2023
00bdcdc
handle when condition parsing.
igalklebanov Sep 22, 2023
3a3eb3f
handle matched then parsing.
igalklebanov Sep 22, 2023
ab72360
extract merge parser to own file.
igalklebanov Sep 22, 2023
152a19d
make into optional in insert query node.
igalklebanov Sep 22, 2023
238413f
add UT generic at insert query builder.
igalklebanov Sep 22, 2023
1a27106
implement not matched merge query builder methods.
igalklebanov Sep 22, 2023
106a588
export simple table reference.
igalklebanov Sep 22, 2023
e5ce1a4
handle merge query at with schema transformer.
igalklebanov Sep 22, 2023
c171dc2
add missing UT generic in remaining insert query builder references.
igalklebanov Sep 22, 2023
0296a57
small fixes.
igalklebanov Sep 22, 2023
4ef41ba
add MergeResult.
igalklebanov Sep 22, 2023
0dd9cb0
merge unit test suite initial commit.
igalklebanov Sep 22, 2023
8a2789c
add merge query node at query node.
igalklebanov Sep 23, 2023
8b5578b
add missing utility methods at merge query builder.
igalklebanov Sep 23, 2023
36fddd8
some js docs, generic rename, etc.
igalklebanov Sep 23, 2023
cb606b4
fix import.
igalklebanov Sep 23, 2023
2d53010
remove UT generic from InsertQueryBuilder.
igalklebanov Sep 23, 2023
98033e7
forgot to remove UT from query creator.
igalklebanov Sep 23, 2023
996bd1d
jsdocs at query creator.
igalklebanov Sep 24, 2023
b0935d4
minor API changes, js docs.
igalklebanov Sep 25, 2023
c936e45
prevent awaits, renames.
igalklebanov Sep 25, 2023
4ad72d1
export merge result.
igalklebanov Sep 25, 2023
4e6d072
excess generic.
igalklebanov Sep 25, 2023
d9f3e6f
some test cases.
igalklebanov Sep 25, 2023
2d46c08
Merge branch 'master' into merge
igalklebanov Sep 28, 2023
c861b5c
no parens around merge query update subquery.
igalklebanov Sep 29, 2023
d2b6db6
merge update test cases.
igalklebanov Sep 29, 2023
7495c48
not matched do nothing test case.
igalklebanov Sep 29, 2023
961d2d3
not matched insert values test cases.
igalklebanov Oct 4, 2023
3f93a37
matched and.
igalklebanov Oct 4, 2023
c414bf8
not matched and.
igalklebanov Oct 4, 2023
61a6889
remove only.
igalklebanov Oct 4, 2023
3451321
fix typings tests.
igalklebanov Oct 4, 2023
cb7c4e8
whenNotMatchedBySource*
igalklebanov Oct 4, 2023
52c29a0
whenNotMatchedBySource update fix.
igalklebanov Oct 4, 2023
79b23f8
test-d initial commit.
igalklebanov Nov 1, 2023
7e7deb7
test-d when not matched by source.
igalklebanov Nov 2, 2023
6553216
test-d then delete.
igalklebanov Nov 2, 2023
a7c66cd
test-d then update.
igalklebanov Nov 3, 2023
37076a2
test-d then insert.
igalklebanov Nov 3, 2023
3984e55
fix typings tests.
igalklebanov Nov 3, 2023
d7800c7
Merge branch 'master' into merge
igalklebanov Nov 5, 2023
2381102
Merge branch 'master' into merge
igalklebanov Nov 21, 2023
2c5da90
Merge branch 'master' into merge
igalklebanov Nov 21, 2023
9a66e5f
Merge branch 'master' into merge
igalklebanov Dec 29, 2023
b374fc1
Merge branch 'master' into merge
igalklebanov Dec 29, 2023
c3b52f8
Merge branch 'master' into merge
igalklebanov Dec 29, 2023
8fdb084
Merge branch 'master' into merge
igalklebanov Dec 29, 2023
bb7896c
Merge branch 'master' into merge
igalklebanov Dec 29, 2023
4b11475
Merge branch 'master' into merge
igalklebanov Dec 30, 2023
d1db041
Merge branch 'master' into merge
igalklebanov Dec 31, 2023
7ea1f1d
Merge branch 'master' into merge
igalklebanov Jan 6, 2024
e5228dc
Merge branch 'master' into merge
igalklebanov Jan 8, 2024
ba9708e
type perf.
igalklebanov Jan 8, 2024
3f34b81
type perf?
igalklebanov Jan 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/dialect/mssql/mssql-query-compiler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AddColumnNode } from '../../operation-node/add-column-node.js'
import { AlterTableColumnAlterationNode } from '../../operation-node/alter-table-node.js'
import { DropColumnNode } from '../../operation-node/drop-column-node.js'
import { MergeQueryNode } from '../../operation-node/merge-query-node.js'
import { DefaultQueryCompiler } from '../../query-compiler/default-query-compiler.js'

export class MssqlQueryCompiler extends DefaultQueryCompiler {
Expand Down Expand Up @@ -73,6 +74,11 @@ export class MssqlQueryCompiler extends DefaultQueryCompiler {
this.visitNode(node.column)
}

protected override visitMergeQuery(node: MergeQueryNode): void {
super.visitMergeQuery(node)
this.append(';')
koskimas marked this conversation as resolved.
Show resolved Hide resolved
}

protected override announcesNewColumnDataType(): boolean {
return false
}
Expand Down
2 changes: 1 addition & 1 deletion src/dialect/postgres/postgres-dialect-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export type PostgresCursorConstructor = new <T>(
) => PostgresCursor<T>

export interface PostgresQueryResult<R> {
command: 'UPDATE' | 'DELETE' | 'INSERT' | 'SELECT'
command: 'UPDATE' | 'DELETE' | 'INSERT' | 'SELECT' | 'MERGE'
rowCount: number
rows: R[]
}
Expand Down
3 changes: 2 additions & 1 deletion src/dialect/postgres/postgres-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ class PostgresConnection implements DatabaseConnection {
if (
result.command === 'INSERT' ||
result.command === 'UPDATE' ||
result.command === 'DELETE'
result.command === 'DELETE' ||
result.command === 'MERGE'
) {
const numAffectedRows = BigInt(result.rowCount)

Expand Down
2 changes: 1 addition & 1 deletion src/driver/database-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface QueryResult<O> {
readonly numUpdatedOrDeletedRows?: bigint

/**
* This is defined for insert, update and delete queries and contains
* This is defined for insert, update, delete and merge queries and contains
* the number of rows the query inserted/updated/deleted.
*/
readonly numAffectedRows?: bigint
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export * from './query-builder/on-conflict-builder.js'
export * from './query-builder/aggregate-function-builder.js'
export * from './query-builder/case-builder.js'
export * from './query-builder/json-path-builder.js'
export * from './query-builder/merge-query-builder.js'
export * from './query-builder/merge-result.js'

export * from './raw-builder/raw-builder.js'
export * from './raw-builder/sql.js'
Expand Down Expand Up @@ -197,6 +199,8 @@ 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/merge-query-node.js'
export * from './operation-node/matched-node.js'

export * from './util/column-type.js'
export * from './util/compilable.js'
Expand Down Expand Up @@ -238,6 +242,7 @@ export {
ValueExpressionOrList,
} from './parser/value-parser.js'
export {
SimpleTableReference,
TableExpression,
TableExpressionOrList,
} from './parser/table-parser.js'
Expand Down
8 changes: 7 additions & 1 deletion src/operation-node/insert-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type InsertQueryNodeProps = Omit<InsertQueryNode, 'kind' | 'into'>

export interface InsertQueryNode extends OperationNode {
readonly kind: 'InsertQueryNode'
readonly into: TableNode
readonly into?: TableNode
readonly columns?: ReadonlyArray<ColumnNode>
readonly values?: OperationNode
readonly returning?: ReturningNode
Expand Down Expand Up @@ -46,6 +46,12 @@ export const InsertQueryNode = freeze({
})
},

createWithoutInto(): InsertQueryNode {
return freeze({
kind: 'InsertQueryNode',
})
},

cloneWith(
insertQuery: InsertQueryNode,
props: InsertQueryNodeProps
Expand Down
3 changes: 1 addition & 2 deletions src/operation-node/join-node.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { freeze } from '../util/object-utils.js'
import { AliasNode } from './alias-node.js'
import { OnNode } from './on-node.js'
import { OperationNode } from './operation-node.js'
import { TableNode } from './table-node.js'

export type JoinType =
| 'InnerJoin'
Expand All @@ -11,6 +9,7 @@ export type JoinType =
| 'FullJoin'
| 'LateralInnerJoin'
| 'LateralLeftJoin'
| 'Using'

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

export interface MatchedNode extends OperationNode {
readonly kind: 'MatchedNode'
readonly not: boolean
readonly bySource: boolean
}

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

create(not: boolean, bySource: boolean = false): MatchedNode {
return freeze({
kind: 'MatchedNode',
not,
bySource,
})
},
})
66 changes: 66 additions & 0 deletions src/operation-node/merge-query-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 { TableNode } from './table-node.js'
import { WhenNode } from './when-node.js'
import { WithNode } from './with-node.js'

export interface MergeQueryNode extends OperationNode {
readonly kind: 'MergeQueryNode'
readonly into: TableNode | AliasNode
readonly using?: JoinNode
readonly whens?: ReadonlyArray<WhenNode>
readonly with?: WithNode
}

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

create(into: TableNode | AliasNode, withNode?: WithNode): MergeQueryNode {
return freeze({
kind: 'MergeQueryNode',
into,
...(withNode && { with: withNode }),
})
},

cloneWithUsing(mergeNode: MergeQueryNode, using: JoinNode): MergeQueryNode {
return freeze({
...mergeNode,
using,
})
},

cloneWithWhen(mergeNode: MergeQueryNode, when: WhenNode): MergeQueryNode {
return freeze({
...mergeNode,
whens: mergeNode.whens
? freeze([...mergeNode.whens, when])
: freeze([when]),
})
},

cloneWithThen(
mergeNode: MergeQueryNode,
then: OperationNode
): MergeQueryNode {
return freeze({
...mergeNode,
whens: mergeNode.whens
? freeze([
...mergeNode.whens.slice(0, -1),
WhenNode.cloneWithResult(
mergeNode.whens[mergeNode.whens.length - 1],
then
),
])
: undefined,
})
},
})
22 changes: 22 additions & 0 deletions src/operation-node/operation-node-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ import { JSONPathNode } from './json-path-node.js'
import { JSONPathLegNode } from './json-path-leg-node.js'
import { JSONOperatorChainNode } from './json-operator-chain-node.js'
import { TupleNode } from './tuple-node.js'
import { MergeQueryNode } from './merge-query-node.js'
import { MatchedNode } from './matched-node.js'
import { AddIndexNode } from './add-index-node.js'

/**
Expand Down Expand Up @@ -209,6 +211,8 @@ export class OperationNodeTransformer {
JSONPathLegNode: this.transformJSONPathLeg.bind(this),
JSONOperatorChainNode: this.transformJSONOperatorChain.bind(this),
TupleNode: this.transformTuple.bind(this),
MergeQueryNode: this.transformMergeQuery.bind(this),
MatchedNode: this.transformMatched.bind(this),
AddIndexNode: this.transformAddIndex.bind(this),
})

Expand Down Expand Up @@ -982,6 +986,24 @@ export class OperationNodeTransformer {
})
}

protected transformMergeQuery(node: MergeQueryNode): MergeQueryNode {
return requireAllProps<MergeQueryNode>({
kind: 'MergeQueryNode',
into: this.transformNode(node.into),
using: this.transformNode(node.using),
whens: this.transformNodeList(node.whens),
with: this.transformNode(node.with),
})
}

protected transformMatched(node: MatchedNode): MatchedNode {
return requireAllProps<MatchedNode>({
kind: 'MatchedNode',
not: node.not,
bySource: node.bySource,
})
}

protected transformAddIndex(node: AddIndexNode): AddIndexNode {
return requireAllProps<AddIndexNode>({
kind: 'AddIndexNode',
Expand Down
6 changes: 6 additions & 0 deletions src/operation-node/operation-node-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ import { JSONPathNode } from './json-path-node.js'
import { JSONPathLegNode } from './json-path-leg-node.js'
import { JSONOperatorChainNode } from './json-operator-chain-node.js'
import { TupleNode } from './tuple-node.js'
import { MergeQueryNode } from './merge-query-node.js'
import { MatchedNode } from './matched-node.js'
import { AddIndexNode } from './add-index-node.js'

export abstract class OperationNodeVisitor {
Expand Down Expand Up @@ -186,6 +188,8 @@ export abstract class OperationNodeVisitor {
JSONPathLegNode: this.visitJSONPathLeg.bind(this),
JSONOperatorChainNode: this.visitJSONOperatorChain.bind(this),
TupleNode: this.visitTuple.bind(this),
MergeQueryNode: this.visitMergeQuery.bind(this),
MatchedNode: this.visitMatched.bind(this),
AddIndexNode: this.visitAddIndex.bind(this),
})

Expand Down Expand Up @@ -291,5 +295,7 @@ export abstract class OperationNodeVisitor {
protected abstract visitJSONPathLeg(node: JSONPathLegNode): void
protected abstract visitJSONOperatorChain(node: JSONOperatorChainNode): void
protected abstract visitTuple(node: TupleNode): void
protected abstract visitMergeQuery(node: MergeQueryNode): void
protected abstract visitMatched(node: MatchedNode): void
protected abstract visitAddIndex(node: AddIndexNode): void
}
2 changes: 2 additions & 0 deletions src/operation-node/operation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export type OperationNodeKind =
| 'JSONPathLegNode'
| 'JSONOperatorChainNode'
| 'TupleNode'
| 'MergeQueryNode'
| 'MatchedNode'
| 'AddIndexNode'

export interface OperationNode {
Expand Down
5 changes: 4 additions & 1 deletion src/operation-node/query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ 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 { MergeQueryNode } from './merge-query-node.js'

export type QueryNode =
| SelectQueryNode
| InsertQueryNode
| UpdateQueryNode
| DeleteQueryNode
| MergeQueryNode

type HasJoins = { joins?: ReadonlyArray<JoinNode> }
type HasWhere = { where?: WhereNode }
Expand All @@ -32,7 +34,8 @@ export const QueryNode = freeze({
SelectQueryNode.is(node) ||
InsertQueryNode.is(node) ||
UpdateQueryNode.is(node) ||
DeleteQueryNode.is(node)
DeleteQueryNode.is(node) ||
MergeQueryNode.is(node)
)
},

Expand Down
2 changes: 1 addition & 1 deletion src/operation-node/schemable-identifier-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const SchemableIdentifierNode = freeze({

createWithSchema(
schema: string,
identifier: string
identifier: string,
): SchemableIdentifierNode {
return freeze({
kind: 'SchemableIdentifierNode',
Expand Down
8 changes: 7 additions & 1 deletion src/operation-node/update-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode

export interface UpdateQueryNode extends OperationNode {
readonly kind: 'UpdateQueryNode'
readonly table: OperationNode
readonly table?: OperationNode
readonly from?: FromNode
readonly joins?: ReadonlyArray<JoinNode>
readonly where?: WhereNode
Expand All @@ -40,6 +40,12 @@ export const UpdateQueryNode = freeze({
})
},

createWithoutTable(): UpdateQueryNode {
return freeze({
kind: 'UpdateQueryNode',
})
},

cloneWithFromItems(
updateQuery: UpdateQueryNode,
fromItems: ReadonlyArray<OperationNode>
Expand Down
7 changes: 5 additions & 2 deletions src/parser/binary-operation-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { SelectType } from '../util/column-type.js'
import { AndNode } from '../operation-node/and-node.js'
import { ParensNode } from '../operation-node/parens-node.js'
import { OrNode } from '../operation-node/or-node.js'
import { WhenNode } from '../operation-node/when-node.js'
import { RawNode } from '../operation-node/raw-node.js'

export type OperandValueExpression<
DB,
Expand Down Expand Up @@ -128,7 +130,8 @@ export function parseFilterObject(

export function parseFilterList(
list: ReadonlyArray<OperationNodeSource | OperationNode>,
combinator: 'and' | 'or'
combinator: 'and' | 'or',
withParens = true
): OperationNode {
const combine = combinator === 'and' ? AndNode.create : OrNode.create

Expand All @@ -146,7 +149,7 @@ export function parseFilterList(
node = combine(node, toOperationNode(list[i]))
}

if (list.length > 1) {
if (list.length > 1 && withParens) {
return ParensNode.create(node)
}

Expand Down
20 changes: 12 additions & 8 deletions src/parser/insert-values-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,20 @@ export type InsertObjectOrList<DB, TB extends keyof DB> =
| InsertObject<DB, TB>
| ReadonlyArray<InsertObject<DB, TB>>

export type InsertObjectOrListFactory<DB, TB extends keyof DB> = (
eb: ExpressionBuilder<DB, TB>
) => InsertObjectOrList<DB, TB>

export type InsertExpression<DB, TB extends keyof DB> =
| InsertObjectOrList<DB, TB>
| InsertObjectOrListFactory<DB, TB>
export type InsertObjectOrListFactory<
DB,
TB extends keyof DB,
UT extends keyof DB = never
> = (eb: ExpressionBuilder<DB, TB | UT>) => InsertObjectOrList<DB, TB>

export type InsertExpression<
DB,
TB extends keyof DB,
UT extends keyof DB = never
> = InsertObjectOrList<DB, TB> | InsertObjectOrListFactory<DB, TB, UT>

export function parseInsertExpression(
arg: InsertExpression<any, any>
arg: InsertExpression<any, any, any>
): [ReadonlyArray<ColumnNode>, ValuesNode] {
const objectOrList = isFunction(arg) ? arg(expressionBuilder()) : arg
const list = isReadonlyArray(objectOrList)
Expand Down
Loading
Loading