Skip to content

Commit 72c24ed

Browse files
merge #226
2 parents d48fce7 + f1154c7 commit 72c24ed

File tree

5 files changed

+369
-22
lines changed

5 files changed

+369
-22
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ subprojects { project ->
157157
name = "Simon Schoof"
158158
email = "simon.schoof@hey.com"
159159
}
160+
developer {
161+
id = "pedrod"
162+
name = "Pedro Domingues"
163+
email = "pedro.domingues.pt@gmail.com"
164+
}
160165
}
161166
scm {
162167
url = "https://github.com/kotlin-orm/ktorm.git"
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2018-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.ktorm.support.postgresql
18+
19+
import org.ktorm.database.Database
20+
import org.ktorm.dsl.AssignmentsBuilder
21+
import org.ktorm.dsl.KtormDsl
22+
import org.ktorm.dsl.batchInsert
23+
import org.ktorm.expression.ColumnAssignmentExpression
24+
import org.ktorm.expression.ColumnExpression
25+
import org.ktorm.expression.SqlExpression
26+
import org.ktorm.expression.TableExpression
27+
import org.ktorm.schema.BaseTable
28+
import org.ktorm.schema.Column
29+
30+
/**
31+
* Bulk insert expression, represents a bulk insert statement in PostgreSQL.
32+
*
33+
* For example: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... ON
34+
* CONFLICT (...) DO NOTHING/UPDATE SET ...`.
35+
*
36+
* @property table the table to be inserted.
37+
* @property assignments column assignments of the bulk insert statement.
38+
* @property conflictTarget the index columns on which the conflict may happens.
39+
* @property updateAssignments the updated column assignments while key conflict exists.
40+
*/
41+
public data class BulkInsertExpression(
42+
val table: TableExpression,
43+
val assignments: List<List<ColumnAssignmentExpression<*>>>,
44+
val conflictTarget: List<ColumnExpression<*>>,
45+
val updateAssignments: List<ColumnAssignmentExpression<*>> = emptyList(),
46+
override val isLeafNode: Boolean = false,
47+
override val extraProperties: Map<String, Any> = emptyMap()
48+
) : SqlExpression()
49+
50+
/**
51+
* Construct a bulk insert expression in the given closure, then execute it and return the
52+
* effected row count.
53+
*
54+
* The usage is almost the same as [batchInsert], but this function is implemented by generating a
55+
* special SQL using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations.
56+
* For this reason, its performance is much better than [batchInsert].
57+
*
58+
* The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... ON
59+
* CONFLICT (...) DO NOTHING/UPDATE SET ...`.
60+
*
61+
* Usage:
62+
*
63+
* ```kotlin
64+
* database.bulkInsert(Employees) {
65+
* item {
66+
* set(it.id, 1)
67+
* set(it.name, "vince")
68+
* set(it.job, "engineer")
69+
* set(it.salary, 1000)
70+
* set(it.hireDate, LocalDate.now())
71+
* set(it.departmentId, 1)
72+
* }
73+
* item {
74+
* set(it.id, 5)
75+
* set(it.name, "vince")
76+
* set(it.job, "engineer")
77+
* set(it.salary, 1000)
78+
* set(it.hireDate, LocalDate.now())
79+
* set(it.departmentId, 1)
80+
* }
81+
*
82+
* onDuplicateKey(Employees.id) {
83+
* // Or leave this empty to simply ignore without updating (do nothing)
84+
* set(it.salary, it.salary + 900)
85+
* }
86+
* }
87+
* ```
88+
*
89+
* @since 3.3.0
90+
* @param table the table to be inserted.
91+
* @param block the DSL block, extension function of [BulkInsertStatementBuilder],
92+
* used to construct the expression.
93+
* @return the effected row count.
94+
* @see batchInsert
95+
*/
96+
public fun <T : BaseTable<*>> Database.bulkInsert(
97+
table: T,
98+
block: BulkInsertStatementBuilder<T>.() -> Unit
99+
): Int {
100+
val builder = BulkInsertStatementBuilder(table).apply(block)
101+
102+
val expression = BulkInsertExpression(
103+
table = table.asExpression(),
104+
assignments = builder.assignments,
105+
conflictTarget = builder.conflictColumns.map { it.asExpression() },
106+
updateAssignments = builder.updateAssignments
107+
)
108+
109+
return executeUpdate(expression)
110+
}
111+
112+
/**
113+
* DSL builder for bulk insert statements.
114+
*/
115+
@KtormDsl
116+
public class BulkInsertStatementBuilder<T : BaseTable<*>>(internal val table: T) {
117+
internal val assignments = ArrayList<List<ColumnAssignmentExpression<*>>>()
118+
internal val conflictColumns = ArrayList<Column<*>>()
119+
internal val updateAssignments = ArrayList<ColumnAssignmentExpression<*>>()
120+
121+
/**
122+
* Add the assignments of a new row to the bulk insert.
123+
*/
124+
public fun item(block: AssignmentsBuilder.(T) -> Unit) {
125+
val builder = PostgreSqlAssignmentsBuilder()
126+
builder.block(table)
127+
128+
if (assignments.isEmpty()
129+
|| assignments[0].map { it.column.name } == builder.assignments.map { it.column.name }
130+
) {
131+
assignments += builder.assignments
132+
} else {
133+
throw IllegalArgumentException("Every item in a batch operation must be the same.")
134+
}
135+
}
136+
137+
/**
138+
* Specify the update assignments while any key conflict exists.
139+
*/
140+
public fun onDuplicateKey(vararg columns: Column<*>, block: AssignmentsBuilder.(T) -> Unit) {
141+
val builder = PostgreSqlAssignmentsBuilder()
142+
builder.block(table)
143+
144+
updateAssignments += builder.assignments
145+
conflictColumns += columns
146+
}
147+
}

ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.ktorm.support.postgresql
1818

1919
import org.ktorm.database.Database
20+
import org.ktorm.dsl.batchInsert
2021
import org.ktorm.schema.BaseTable
2122
import java.lang.reflect.InvocationTargetException
2223

@@ -70,3 +71,51 @@ internal val Database.Companion.global: Database get() {
7071
public fun <T : BaseTable<*>> T.insertOrUpdate(block: InsertOrUpdateStatementBuilder.(T) -> Unit): Int {
7172
return Database.global.insertOrUpdate(this, block)
7273
}
74+
75+
/**
76+
* Construct a bulk insert-or-update expression in the given closure, then execute it and return the effected
77+
* row count.
78+
*
79+
* The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL
80+
* using PostgreSQL's bulk insert (with on conflict) syntax, instead of based on JDBC batch operations.
81+
* For this reason, its performance is much better than [batchInsert].
82+
*
83+
* The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... ON
84+
* CONFLICT (...) DO NOTHING/UPDATE SET ...`.
85+
*
86+
* Usage:
87+
*
88+
* ```kotlin
89+
* database.bulkInsert(Employees) {
90+
* item {
91+
* set(it.id, 1)
92+
* set(it.name, "vince")
93+
* set(it.job, "engineer")
94+
* set(it.salary, 1000)
95+
* set(it.hireDate, LocalDate.now())
96+
* set(it.departmentId, 1)
97+
* }
98+
* item {
99+
* set(it.id, 5)
100+
* set(it.name, "vince")
101+
* set(it.job, "engineer")
102+
* set(it.salary, 1000)
103+
* set(it.hireDate, LocalDate.now())
104+
* set(it.departmentId, 1)
105+
* }
106+
*
107+
* onDuplicateKey(Employees.id) {
108+
* // Or leave this empty to simply ignore without updating (do nothing)
109+
* set(it.salary, it.salary + 900)
110+
* }
111+
* }
112+
* ```
113+
*
114+
* @param block the DSL block, extension function of [BulkInsertStatementBuilder],
115+
* used to construct the expression.
116+
* @return the effected row count.
117+
* @see batchInsert
118+
*/
119+
public fun <T : BaseTable<*>> T.bulkInsert(block: BulkInsertStatementBuilder<T>.() -> Unit): Int {
120+
return Database.global.bulkInsert(this, block)
121+
}

ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public open class PostgreSqlFormatter(
5050
override fun visit(expr: SqlExpression): SqlExpression {
5151
val result = when (expr) {
5252
is InsertOrUpdateExpression -> visitInsertOrUpdate(expr)
53+
is BulkInsertExpression -> visitBulkInsert(expr)
5354
else -> super.visit(expr)
5455
}
5556

@@ -129,42 +130,85 @@ public open class PostgreSqlFormatter(
129130
return expr
130131
}
131132

133+
protected open fun visitBulkInsert(expr: BulkInsertExpression): BulkInsertExpression {
134+
generateMultipleInsertSQL(expr.table.name.quoted, expr.assignments)
135+
136+
generateOnConflictSQL(expr.conflictTarget, expr.updateAssignments)
137+
138+
return expr
139+
}
140+
132141
protected open fun visitInsertOrUpdate(expr: InsertOrUpdateExpression): InsertOrUpdateExpression {
142+
generateMultipleInsertSQL(expr.table.name.quoted, listOf(expr.assignments))
143+
144+
generateOnConflictSQL(expr.conflictTarget, expr.updateAssignments)
145+
146+
return expr
147+
}
148+
149+
private fun generateMultipleInsertSQL(
150+
quotedTableName: String,
151+
assignmentsList: List<List<ColumnAssignmentExpression<*>>>
152+
) {
153+
if (assignmentsList.isEmpty()) {
154+
throw IllegalStateException("The insert expression has no values to insert")
155+
}
156+
133157
writeKeyword("insert into ")
134-
visitTable(expr.table.copy(tableAlias = null))
135-
write("(")
136158

137-
for ((i, assignment) in expr.assignments.withIndex()) {
159+
write("$quotedTableName (")
160+
assignmentsList.first().forEachIndexed { i, assignment ->
138161
if (i > 0) write(", ")
139162
checkColumnName(assignment.column.name)
140163
write(assignment.column.name.quoted)
141164
}
142165

143-
writeKeyword(") values (")
144-
visitExpressionList(expr.assignments.map { it.expression as ArgumentExpression })
166+
writeKeyword(")")
167+
writeKeyword(" values ")
168+
169+
assignmentsList.forEachIndexed { i, assignments ->
170+
if (i > 0) write(", ")
171+
writeKeyword("( ")
172+
visitExpressionList(assignments.map { it.expression as ArgumentExpression })
173+
writeKeyword(")")
174+
}
175+
145176
removeLastBlank()
146-
writeKeyword(") on conflict (")
177+
}
178+
179+
private fun generateOnConflictSQL(
180+
conflictTarget: List<ColumnExpression<*>>,
181+
updateAssignments: List<ColumnAssignmentExpression<*>>
182+
) {
183+
if (conflictTarget.isEmpty()) {
184+
// We are just performing an Insert operation, so any conflict will interrupt the query with an error
185+
return
186+
}
147187

148-
for ((i, column) in expr.conflictTarget.withIndex()) {
188+
writeKeyword(" on conflict (")
189+
conflictTarget.forEachIndexed { i, column ->
149190
if (i > 0) write(", ")
150191
checkColumnName(column.name)
151192
write(column.name.quoted)
152193
}
153194

154-
writeKeyword(") do update set ")
155-
156-
for ((i, assignment) in expr.updateAssignments.withIndex()) {
157-
if (i > 0) {
158-
removeLastBlank()
159-
write(", ")
195+
writeKeyword(") do ")
196+
197+
if (updateAssignments.isNotEmpty()) {
198+
writeKeyword("update set ")
199+
updateAssignments.forEachIndexed { i, assignment ->
200+
if (i > 0) {
201+
removeLastBlank()
202+
write(", ")
203+
}
204+
checkColumnName(assignment.column.name)
205+
write("${assignment.column.name.quoted} ")
206+
write("= ")
207+
visit(assignment.expression)
160208
}
161-
checkColumnName(assignment.column.name)
162-
write("${assignment.column.name.quoted} ")
163-
write("= ")
164-
visit(assignment.expression)
209+
} else {
210+
writeKeyword("nothing")
165211
}
166-
167-
return expr
168212
}
169213
}
170214

@@ -225,7 +269,8 @@ public open class PostgreSqlExpressionVisitor : SqlExpressionVisitor() {
225269
if (table === expr.table
226270
&& assignments === expr.assignments
227271
&& conflictTarget === expr.conflictTarget
228-
&& updateAssignments === expr.updateAssignments) {
272+
&& updateAssignments === expr.updateAssignments
273+
) {
229274
return expr
230275
} else {
231276
return expr.copy(

0 commit comments

Comments
 (0)