Skip to content

Commit f258717

Browse files
authoredJul 17, 2024··
Merge pull request #567 from kotlin-orm/dev
Release 4.1.0
2 parents f59e406 + c4b9347 commit f258717

File tree

36 files changed

+843
-80
lines changed

36 files changed

+843
-80
lines changed
 

‎.github/workflows/build.yml

+9-7
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ jobs:
1414
strategy:
1515
fail-fast: true
1616
matrix:
17-
java: [8, 11, 17, 20]
17+
java: [8, 11, 17, 21]
1818
steps:
1919
- name: Checkout Code
20-
uses: actions/checkout@v3
20+
uses: actions/checkout@v4
2121

2222
- name: Setup Java
23-
uses: actions/setup-java@v3
23+
uses: actions/setup-java@v4
2424
with:
2525
distribution: zulu
2626
java-version: ${{ matrix.java }}
2727

2828
- name: Setup Gradle
29-
uses: gradle/gradle-build-action@v2
29+
uses: gradle/actions/setup-gradle@v3
3030

3131
- name: Assemble the Project
3232
run: ./gradlew assemble
@@ -45,6 +45,8 @@ jobs:
4545
ktorm-core/build/reports/jacoco/test/jacocoTestReport.csv
4646
ktorm-global/build/reports/jacoco/test/jacocoTestReport.csv
4747
ktorm-jackson/build/reports/jacoco/test/jacocoTestReport.csv
48+
ktorm-ksp-annotations/build/reports/jacoco/test/jacocoTestReport.csv
49+
ktorm-ksp-compiler/build/reports/jacoco/test/jacocoTestReport.csv
4850
ktorm-support-mysql/build/reports/jacoco/test/jacocoTestReport.csv
4951
ktorm-support-oracle/build/reports/jacoco/test/jacocoTestReport.csv
5052
ktorm-support-postgresql/build/reports/jacoco/test/jacocoTestReport.csv
@@ -78,16 +80,16 @@ jobs:
7880
needs: build
7981
steps:
8082
- name: Checkout Code
81-
uses: actions/checkout@v3
83+
uses: actions/checkout@v4
8284

8385
- name: Setup Java
84-
uses: actions/setup-java@v3
86+
uses: actions/setup-java@v4
8587
with:
8688
distribution: zulu
8789
java-version: 8
8890

8991
- name: Setup Gradle
90-
uses: gradle/gradle-build-action@v2
92+
uses: gradle/actions/setup-gradle@v3
9193

9294
- name: Assemble the Project
9395
run: ./gradlew assemble

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ object Employees : Table<Employee>("t_employee") {
260260
}
261261
```
262262

263-
> Naming Strategy: It's highly recommended to name your entity classes by singular nouns, name table objects by plurals (eg. Employee/Employees, Department/Departments).
263+
> Naming Strategy: It's highly recommended to name your entity classes by singular nouns, name table objects by plurals (e.g. Employee/Employees, Department/Departments).
264264
265265
Now that column bindings are configured, so we can use [sequence APIs](#Entity-Sequence-APIs) to perform many operations on entities. Let's add two extension properties for `Database` first. These properties return new created sequence objects via `sequenceOf` and they can help us improve the readability of the code:
266266

‎buildSrc/build.gradle.kts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repositories {
99
}
1010

1111
dependencies {
12-
api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
13-
api("org.moditect:moditect-gradle-plugin:1.0.0-rc3")
14-
api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1")
12+
api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
13+
api("org.moditect:moditect:1.0.0.RC1")
14+
api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.6")
1515
}
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,37 @@
11

22
plugins {
33
id("kotlin")
4-
id("org.moditect.gradleplugin")
54
}
65

7-
moditect {
8-
// Generate a multi-release jar, the module descriptor will be located at META-INF/versions/9/module-info.class
9-
addMainModuleInfo {
10-
jvmVersion.set("9")
11-
overwriteExistingFiles.set(true)
12-
module {
13-
moduleInfoFile = file("src/main/moditect/module-info.java")
6+
val moditect by tasks.registering {
7+
doLast {
8+
// Generate a multi-release modulized jar, module descriptor position: META-INF/versions/9/module-info.class
9+
val inputJar = tasks.jar.flatMap { it.archiveFile }.map { it.asFile.toPath() }.get()
10+
val outputDir = file("build/moditect").apply { mkdirs() }.toPath()
11+
val moduleInfo = file("src/main/moditect/module-info.java").readText()
12+
val version = project.version.toString()
13+
org.moditect.commands.AddModuleInfo(moduleInfo, null, version, inputJar, outputDir, "9", true).run()
14+
15+
// Replace the original jar with the modulized jar.
16+
copy {
17+
from(outputDir.resolve(inputJar.fileName))
18+
into(inputJar.parent)
1419
}
1520
}
21+
}
1622

17-
// Let kotlin compiler know the module descriptor.
18-
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
19-
sourceSets.main {
20-
kotlin.srcDir("src/main/moditect")
21-
}
23+
tasks {
24+
moditect {
25+
dependsOn(jar)
2226
}
27+
jar {
28+
finalizedBy(moditect)
29+
}
30+
}
2331

24-
// Workaround to avoid circular task dependencies, see https://github.com/moditect/moditect-gradle-plugin/issues/14
25-
afterEvaluate {
26-
val compileJava = tasks.compileJava.get()
27-
val addDependenciesModuleInfo = tasks.addDependenciesModuleInfo.get()
28-
compileJava.setDependsOn(compileJava.dependsOn - addDependenciesModuleInfo)
32+
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
33+
// Let kotlin compiler know the module descriptor.
34+
sourceSets.main {
35+
kotlin.srcDir("src/main/moditect")
2936
}
3037
}

‎buildSrc/src/main/kotlin/ktorm.publish.gradle.kts

+5
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ publishing {
155155
id.set("brohacz")
156156
name.set("Michal Brosig")
157157
}
158+
developer {
159+
id.set("hc224")
160+
name.set("hc224")
161+
email.set("hc224@pm.me")
162+
}
158163
}
159164
}
160165
}

‎detekt.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ complexity:
4949
active: true
5050
threshold: 4
5151
ComplexInterface:
52-
active: true
52+
active: false
5353
threshold: 12
5454
includeStaticDeclarations: false
5555
CyclomaticComplexMethod:
@@ -75,7 +75,7 @@ complexity:
7575
active: false
7676
threshold: 7
7777
NestedBlockDepth:
78-
active: true
78+
active: false
7979
threshold: 5
8080
StringLiteralDuplication:
8181
active: false
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists

‎ktorm-core/ktorm-core.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ val testOutput by configurations.creating {
1919
}
2020

2121
val testJar by tasks.registering(Jar::class) {
22-
dependsOn("testClasses")
22+
dependsOn(tasks.testClasses)
2323
from(sourceSets.test.map { it.output })
2424
archiveClassifier.set("test")
2525
}

‎ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt

+1
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet {
609609
return index
610610
}
611611
}
612+
612613
throw SQLException("Invalid column name: $columnLabel")
613614
}
614615

‎ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public class Database(
147147
public val name: String
148148

149149
/**
150-
* The name of the connected database product, eg. MySQL, H2.
150+
* The name of the connected database product, e.g. MySQL, H2.
151151
*/
152152
public val productName: String
153153

‎ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt

+18-9
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import java.io.ObjectInputStream
2323
import java.io.ObjectOutputStream
2424
import java.io.Serializable
2525
import java.lang.reflect.Proxy
26+
import java.sql.SQLException
2627
import kotlin.reflect.KClass
2728
import kotlin.reflect.full.isSubclassOf
2829
import kotlin.reflect.jvm.jvmErasure
@@ -69,12 +70,12 @@ import kotlin.reflect.jvm.jvmErasure
6970
*
7071
* - For [Boolean] type, the default value is `false`.
7172
* - For [Char] type, the default value is `\u0000`.
72-
* - For number types (such as [Int], [Long], [Double], etc), the default value is zero.
73+
* - For number types (such as [Int], [Long], [Double], etc.), the default value is zero.
7374
* - For [String] type, the default value is an empty string.
7475
* - For entity types, the default value is a new-created entity object which is empty.
7576
* - For enum types, the default value is the first value of the enum, whose ordinal is 0.
7677
* - For array types, the default value is a new-created empty array.
77-
* - For collection types (such as [Set], [List], [Map], etc), the default value is a new created mutable collection
78+
* - For collection types (such as [Set], [List], [Map], etc.), the default value is a new created mutable collection
7879
* of the concrete type.
7980
* - For any other types, the default value is an instance created by its no-args constructor. If the constructor
8081
* doesn't exist, an exception is thrown.
@@ -128,7 +129,7 @@ import kotlin.reflect.jvm.jvmErasure
128129
* refer to their documentation for more details.
129130
*
130131
* Besides of JDK serialization, the ktorm-jackson module also supports serializing entities in JSON format. This
131-
* module provides an extension for Jackson, the famous JSON framework in Java word. It supports serializing entity
132+
* module provides an extension for Jackson, the famous JSON framework in Java world. It supports serializing entity
132133
* objects into JSON format and parsing JSONs as entity objects. More details can be found in its documentation.
133134
*/
134135
public interface Entity<E : Entity<E>> : Serializable {
@@ -143,6 +144,13 @@ public interface Entity<E : Entity<E>> : Serializable {
143144
*/
144145
public val properties: Map<String, Any?>
145146

147+
/**
148+
* Return the immutable view of this entity's changed properties and their original values.
149+
*
150+
* @since 4.1.0
151+
*/
152+
public val changedProperties: Map<String, Any?>
153+
146154
/**
147155
* Update the property changes of this entity into the database and return the affected record number.
148156
*
@@ -156,18 +164,18 @@ public interface Entity<E : Entity<E>> : Serializable {
156164
* `fromDatabase` references point to the database they are obtained from. For entity objects created by
157165
* [Entity.create] or [Entity.Factory], their `fromDatabase` references are `null` initially, so we can not call
158166
* [flushChanges] on them. But once we use them with [add] or [update] function, `fromDatabase` will be modified
159-
* to the current database, so we will be able to call [flushChanges] on them afterwards.
167+
* to the current database, so we will be able to call [flushChanges] on them afterward.
160168
*
161169
* @see add
162170
* @see update
163171
*/
172+
@Throws(SQLException::class)
164173
public fun flushChanges(): Int
165174

166175
/**
167176
* Clear the tracked property changes of this entity.
168177
*
169-
* After calling this function, the [flushChanges] doesn't do anything anymore because the property changes
170-
* are discarded.
178+
* After calling this function, [flushChanges] will do nothing because property changes are discarded.
171179
*/
172180
public fun discardChanges()
173181

@@ -185,13 +193,14 @@ public interface Entity<E : Entity<E>> : Serializable {
185193
* @see update
186194
* @see flushChanges
187195
*/
196+
@Throws(SQLException::class)
188197
public fun delete(): Int
189198

190199
/**
191200
* Obtain a property's value by its name.
192201
*
193202
* Note that this function doesn't follow the rules of default values discussed in the class level documentation.
194-
* If the value doesn't exist, we will return `null` simply.
203+
* If the value doesn't exist, it will simply return `null`.
195204
*/
196205
public operator fun get(name: String): Any?
197206

@@ -221,8 +230,8 @@ public interface Entity<E : Entity<E>> : Serializable {
221230
public override fun hashCode(): Int
222231

223232
/**
224-
* Return a string representation of this table.
225-
* The format is like `Employee{id=1, name=Eric, job=contributor, hireDate=2021-05-05, salary=50}`.
233+
* Return a string representation of this entity.
234+
* The format is like `Employee(id=1, name=Eric, job=contributor, hireDate=2021-05-05, salary=50)`.
226235
*/
227236
public override fun toString(): String
228237

‎ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt

+59-10
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@
1717
package org.ktorm.entity
1818

1919
import org.ktorm.dsl.*
20-
import org.ktorm.dsl.AliasRemover
2120
import org.ktorm.expression.*
2221
import org.ktorm.schema.*
2322

2423
/**
25-
* Insert the given entity into this sequence and return the affected record number.
24+
* Insert the given entity into the table and return the affected record number.
2625
*
2726
* If we use an auto-increment key in our table, we need to tell Ktorm which is the primary key by calling
2827
* [Table.primaryKey] while registering columns, then this function will obtain the generated key from the
@@ -233,7 +232,6 @@ private fun EntitySequence<*, *>.checkForDml() {
233232
*/
234233
private fun Entity<*>.findInsertColumns(table: Table<*>): Map<Column<*>, Any?> {
235234
val assignments = LinkedHashMap<Column<*>, Any?>()
236-
237235
for (column in table.columns) {
238236
if (column.binding != null && implementation.hasColumnValue(column.binding)) {
239237
assignments[column] = implementation.getColumnValue(column.binding)
@@ -246,10 +244,9 @@ private fun Entity<*>.findInsertColumns(table: Table<*>): Map<Column<*>, Any?> {
246244
/**
247245
* Return columns associated with their values for update.
248246
*/
247+
@Suppress("ConvertArgumentToSet")
249248
private fun Entity<*>.findUpdateColumns(table: Table<*>): Map<Column<*>, Any?> {
250249
val assignments = LinkedHashMap<Column<*>, Any?>()
251-
252-
@Suppress("ConvertArgumentToSet")
253250
for (column in table.columns - table.primaryKeys) {
254251
if (column.binding != null && implementation.hasColumnValue(column.binding)) {
255252
assignments[column] = implementation.getColumnValue(column.binding)
@@ -264,7 +261,6 @@ private fun Entity<*>.findUpdateColumns(table: Table<*>): Map<Column<*>, Any?> {
264261
*/
265262
private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map<Column<*>, Any?> {
266263
val assignments = LinkedHashMap<Column<*>, Any?>()
267-
268264
for (column in fromTable.columns) {
269265
val binding = column.binding ?: continue
270266

@@ -286,11 +282,13 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map<Co
286282

287283
check(curr is EntityImplementation?)
288284

289-
if (curr != null && prop.name in curr.changedProperties) {
290-
anyChanged = true
291-
}
285+
if (curr != null) {
286+
if (prop.name in curr.changedProperties) {
287+
anyChanged = true
288+
}
292289

293-
curr = curr?.getProperty(prop)
290+
curr = curr.getProperty(prop)
291+
}
294292
}
295293

296294
if (anyChanged) {
@@ -303,6 +301,57 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map<Co
303301
return assignments
304302
}
305303

304+
/**
305+
* Return changed properties associated with their original values.
306+
*/
307+
internal fun EntityImplementation.findChangedProperties(): Map<String, Any?> {
308+
check(parent == null) { "The entity is not attached to any database yet." }
309+
val fromTable = fromTable ?: error("The entity is not attached to any database yet.")
310+
311+
// Create an empty entity object to collect changed properties.
312+
val result = Entity.create(entityClass, parent, fromDatabase, fromTable)
313+
for (column in fromTable.columns) {
314+
val binding = column.binding ?: continue
315+
316+
when (binding) {
317+
is ReferenceBinding -> {
318+
if (binding.onProperty.name in changedProperties) {
319+
val origin = changedProperties[binding.onProperty.name] as Entity<*>?
320+
val originId = origin?.implementation?.getPrimaryKeyValue(binding.referenceTable as Table<*>)
321+
result.implementation.setColumnValue(binding, originId)
322+
}
323+
}
324+
is NestedBinding -> {
325+
var anyChanged = false
326+
var curr: Any? = this
327+
328+
for (prop in binding.properties) {
329+
if (curr is Entity<*>) {
330+
curr = curr.implementation
331+
}
332+
333+
check(curr is EntityImplementation?)
334+
335+
if (curr != null) {
336+
if (prop.name in curr.changedProperties) {
337+
curr = curr.changedProperties[prop.name]
338+
anyChanged = true
339+
} else {
340+
curr = curr.getProperty(prop)
341+
}
342+
}
343+
}
344+
345+
if (anyChanged) {
346+
result.implementation.setColumnValue(binding, curr)
347+
}
348+
}
349+
}
350+
}
351+
352+
return result.properties
353+
}
354+
306355
/**
307356
* Clear the tracked property changes of this entity.
308357
*

‎ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt

+2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ internal fun EntityImplementation.getColumnValue(binding: ColumnBinding): Any? {
9292
curr = child?.implementation
9393
}
9494
}
95+
9596
return curr?.getProperty(binding.properties.last())
9697
}
9798
}
@@ -127,6 +128,7 @@ internal fun EntityImplementation.setColumnValue(binding: ColumnBinding, value:
127128
fromDatabase = this.fromDatabase,
128129
fromTable = binding.referenceTable as Table<*>
129130
)
131+
130132
this.setProperty(binding.onProperty, child, forceSet)
131133
}
132134

‎ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt

+10
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,14 @@ public class EntityExtensionsApi {
5050
public fun Entity<*>.setColumnValue(binding: ColumnBinding, value: Any?) {
5151
implementation.setColumnValue(binding, value)
5252
}
53+
54+
/**
55+
* Check if this entity is attached to the database.
56+
*
57+
* @since 4.1.0
58+
*/
59+
public fun Entity<*>.isAttached(): Boolean {
60+
val impl = this.implementation
61+
return impl.fromDatabase != null && impl.fromTable != null && impl.parent == null
62+
}
5363
}

‎ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt

+9-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ internal class EntityImplementation(
3838
@Transient var fromDatabase: Database? = fromDatabase
3939
@Transient var fromTable: Table<*>? = fromTable
4040
@Transient var parent: EntityImplementation? = parent
41-
@Transient var changedProperties = LinkedHashSet<String>()
41+
@Transient var changedProperties = LinkedHashMap<String, Any?>()
4242

4343
override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
4444
return when (method.declaringClass.kotlin) {
@@ -54,6 +54,7 @@ internal class EntityImplementation(
5454
when (method.name) {
5555
"getEntityClass" -> this.entityClass
5656
"getProperties" -> Collections.unmodifiableMap(this.values)
57+
"getChangedProperties" -> this.findChangedProperties()
5758
"flushChanges" -> this.doFlushChanges()
5859
"discardChanges" -> this.doDiscardChanges()
5960
"delete" -> this.doDelete()
@@ -150,13 +151,17 @@ internal class EntityImplementation(
150151
throw UnsupportedOperationException(msg)
151152
}
152153

154+
// Save property changes and original values.
155+
if (name !in changedProperties) {
156+
changedProperties[name] = values[name]
157+
}
158+
153159
values[name] = value
154-
changedProperties.add(name)
155160
}
156161

157162
private fun copy(): Entity<*> {
158163
val entity = Entity.create(entityClass, parent, fromDatabase, fromTable)
159-
entity.implementation.changedProperties.addAll(changedProperties)
164+
entity.implementation.changedProperties.putAll(changedProperties)
160165

161166
for ((name, value) in values) {
162167
if (value is Entity<*>) {
@@ -204,7 +209,7 @@ internal class EntityImplementation(
204209
val javaClass = Class.forName(input.readUTF(), true, Thread.currentThread().contextClassLoader)
205210
entityClass = javaClass.kotlin
206211
values = input.readObject() as LinkedHashMap<String, Any?>
207-
changedProperties = LinkedHashSet()
212+
changedProperties = LinkedHashMap()
208213
}
209214

210215
override fun equals(other: Any?): Boolean {

‎ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ internal val Method.kotlinProperty: Pair<KProperty1<*, *>, Boolean>? get() {
4444
return Pair(prop, false)
4545
}
4646
}
47+
4748
return null
4849
}
4950

‎ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public abstract class SqlExpression {
5252
}
5353

5454
/**
55-
* Base class of scalar expressions. An expression is "scalar" if it has a return value (eg. `a + 1`).
55+
* Base class of scalar expressions. An expression is "scalar" if it has a return value (e.g. `a + 1`).
5656
*
5757
* @param T the return value's type of this scalar expression.
5858
*/
@@ -85,7 +85,7 @@ public abstract class QuerySourceExpression : SqlExpression()
8585
* @property orderBy a list of order-by expressions, used in the `order by` clause of a query.
8686
* @property offset the offset of the first returned record.
8787
* @property limit max record numbers returned by the query.
88-
* @property tableAlias the alias when this query is nested in another query's source, eg. `select * from (...) alias`.
88+
* @property tableAlias the alias when this query is nested in another query's source, e.g. `select * from (...) alias`.
8989
*/
9090
public sealed class QueryExpression : QuerySourceExpression() {
9191
public abstract val orderBy: List<OrderByExpression>
@@ -151,7 +151,7 @@ public data class InsertExpression(
151151
) : SqlExpression()
152152

153153
/**
154-
* Insert-from-query expression, eg. `insert into tmp(num) select 1 from dual`.
154+
* Insert-from-query expression, e.g. `insert into tmp(num) select 1 from dual`.
155155
*
156156
* @property table the table to be inserted.
157157
* @property columns the columns to be inserted.

‎ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ public abstract class BaseTable<E : Any>(
372372
/**
373373
* Convert this table to a [TableExpression].
374374
*/
375-
public fun asExpression(): TableExpression {
375+
public open fun asExpression(): TableExpression {
376376
return TableExpression(tableName, alias, catalog, schema)
377377
}
378378

‎ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import kotlin.reflect.KProperty1
2929
public sealed class ColumnBinding
3030

3131
/**
32-
* Bind the column to nested properties, eg. `employee.manager.department.id`.
32+
* Bind the column to nested properties, e.g. `employee.manager.department.id`.
3333
*
3434
* @property properties the nested properties, cannot be empty.
3535
*/
@@ -118,19 +118,19 @@ public data class Column<T : Any>(
118118
*
119119
* @see ColumnDeclaringExpression
120120
*/
121-
val label: String get() = toString(separator = "_")
121+
val label: String = toString(separator = "_")
122122

123123
/**
124124
* Return all the bindings of this column, including the primary [binding] and [extraBindings].
125125
*/
126-
val allBindings: List<ColumnBinding> get() = binding?.let { listOf(it) + extraBindings } ?: emptyList()
126+
val allBindings: List<ColumnBinding> = binding?.let { listOf(it) + extraBindings } ?: emptyList()
127127

128128
/**
129129
* If the column is bound to a reference table, return the table, otherwise return null.
130130
*
131131
* Shortcut for `(binding as? ReferenceBinding)?.referenceTable`.
132132
*/
133-
val referenceTable: BaseTable<*>? get() = (binding as? ReferenceBinding)?.referenceTable
133+
val referenceTable: BaseTable<*>? = (binding as? ReferenceBinding)?.referenceTable
134134

135135
/**
136136
* Convert this column to a [ColumnExpression].

‎ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public open class Table<E : Entity<E>>(
5656
) : BaseTable<E>(tableName, alias, catalog, schema, entityClass) {
5757

5858
/**
59-
* Bind the column to nested properties, eg. `employee.manager.department.id`.
59+
* Bind the column to nested properties, e.g. `employee.manager.department.id`.
6060
*
6161
* Note: Since [Column] is immutable, this function will create a new [Column] instance and replace the origin
6262
* registered one.

‎ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt

+91-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.ktorm.entity
22

3+
import org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException
34
import org.junit.Test
45
import org.ktorm.BaseTest
56
import org.ktorm.database.Database
@@ -271,10 +272,99 @@ class EntityTest : BaseTest() {
271272
companion object : Entity.Factory<GrandChild>()
272273
var id: Int?
273274
var name: String?
275+
var job: String?
274276
}
275277

276278
object Parents : Table<Parent>("t_employee") {
277279
val id = int("id").primaryKey().bindTo { it.child?.grandChild?.id }
280+
val name = varchar("name").bindTo { it.child?.grandChild?.name }
281+
val job = varchar("job").bindTo { it.child?.grandChild?.job }
282+
}
283+
284+
@Test
285+
fun testInternalChangedPropertiesForNestedBinding1() {
286+
val p1 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError()
287+
p1.child?.grandChild?.job = "Senior Engineer"
288+
p1.child?.grandChild?.job = "Expert Engineer"
289+
290+
assert(p1.implementation.changedProperties.size == 0)
291+
assert(p1.child?.implementation?.changedProperties?.size == 0)
292+
assert(p1.child?.grandChild?.implementation?.changedProperties?.size == 1)
293+
assert(p1.child?.grandChild?.implementation?.changedProperties?.get("job") == "engineer")
294+
assert(p1.flushChanges() == 1)
295+
}
296+
297+
@Test
298+
fun testInternalChangedPropertiesForNestedBinding2() {
299+
val p2 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError()
300+
p2.child?.grandChild?.name = "Vincent"
301+
p2.child?.grandChild?.job = "Senior Engineer"
302+
p2.child?.grandChild?.job = "Expert Engineer"
303+
304+
assert(p2.implementation.changedProperties.size == 0)
305+
assert(p2.child?.implementation?.changedProperties?.size == 0)
306+
assert(p2.child?.grandChild?.implementation?.changedProperties?.size == 2)
307+
assert(p2.child?.grandChild?.implementation?.changedProperties?.get("name") == "vince")
308+
assert(p2.child?.grandChild?.implementation?.changedProperties?.get("job") == "engineer")
309+
assert(p2.flushChanges() == 1)
310+
}
311+
312+
@Test
313+
fun testChangedPropertiesForNestedBinding1() {
314+
val p1 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError()
315+
p1.child?.grandChild?.job = "Senior Engineer"
316+
p1.child?.grandChild?.job = "Expert Engineer"
317+
318+
assert(p1.changedProperties.size == 1)
319+
assert(p1.changedProperties["child"].toString() == "Child(grandChild=GrandChild(job=engineer))")
320+
assert(p1.flushChanges() == 1)
321+
}
322+
323+
@Test
324+
fun testChangedPropertiesForNestedBinding2() {
325+
val p2 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError()
326+
p2.child?.grandChild?.name = "Vincent"
327+
p2.child?.grandChild?.job = "Senior Engineer"
328+
p2.child?.grandChild?.job = "Expert Engineer"
329+
330+
assert(p2.changedProperties.size == 1)
331+
assert(p2.changedProperties["child"].toString() == "Child(grandChild=GrandChild(name=vince, job=engineer))")
332+
assert(p2.flushChanges() == 1)
333+
}
334+
335+
@Test
336+
fun testChangedPropertiesForReferenceBinding() {
337+
val e = database.employees.find { it.id eq 1 } ?: throw AssertionError()
338+
e.name = "Vincent"
339+
e.job = "Senior Engineer"
340+
e.job = "Expert Engineer"
341+
e.manager = database.employees.find { it.id eq 2 }
342+
e.manager = database.employees.find { it.id eq 2 }
343+
e.salary = 999999
344+
e.department = database.departments.find { it.id eq 2 } ?: throw AssertionError()
345+
e.department = database.departments.find { it.id eq 2 } ?: throw AssertionError()
346+
347+
val changed = e.changedProperties
348+
assert(changed.size == 5)
349+
assert(changed["name"] == "vince")
350+
assert(changed["job"] == "engineer")
351+
assert(changed["manager"].toString() == "Employee(id=null)")
352+
assert(changed["salary"] == 100L)
353+
assert(changed["department"].toString() == "Department(id=1)")
354+
assert(e.flushChanges() == 1)
355+
}
356+
357+
@Test
358+
fun testExceptionThrowsByProxy() {
359+
try {
360+
val e = database.employees.find { it.id eq 1 } ?: throw AssertionError()
361+
e.department = Department()
362+
e.flushChanges()
363+
364+
throw AssertionError("failed")
365+
} catch (e: JdbcSQLIntegrityConstraintViolationException) {
366+
assert(e.message!!.contains("NULL not allowed for column \"department_id\""))
367+
}
278368
}
279369

280370
@Test
@@ -783,4 +873,4 @@ class EntityTest : BaseTest() {
783873
assert(departmentTransient !== departmentAttached)
784874
assert(departmentTransient.hashCode() == departmentAttached.hashCode())
785875
}
786-
}
876+
}

‎ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ internal class EntityDeserializers : SimpleDeserializers() {
6363
parser: JsonParser,
6464
ctx: DeserializationContext
6565
): Map<String, KProperty1<*, *>> {
66+
val skipNames = Entity::class.memberProperties.map { it.name }.toSet()
6667
return entityClass.memberProperties
6768
.asSequence()
6869
.filter { it.isAbstract }
69-
.filter { it.name != "entityClass" && it.name != "properties" }
70+
.filter { it.name !in skipNames }
7071
.filter { it.findAnnotationForDeserialization<JsonIgnore>() == null }
7172
.filter { prop ->
7273
val jsonProperty = prop.findAnnotationForDeserialization<JsonProperty>()

‎ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,11 @@ internal class EntitySerializers : SimpleSerializers() {
6262
}
6363

6464
private fun findReadableProperties(entity: Entity<*>): Map<String, KProperty1<*, *>> {
65+
val skipNames = Entity::class.memberProperties.map { it.name }.toSet()
6566
return entity.entityClass.memberProperties
6667
.asSequence()
6768
.filter { it.isAbstract }
68-
.filter { it.name != "entityClass" && it.name != "properties" }
69+
.filter { it.name !in skipNames }
6970
.filter { it.findAnnotationForSerialization<JsonIgnore>() == null }
7071
.filter { prop ->
7172
val jsonProperty = prop.findAnnotationForSerialization<JsonProperty>()

‎ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package org.ktorm.jackson
1919
import com.fasterxml.jackson.databind.JavaType
2020
import com.fasterxml.jackson.databind.ObjectMapper
2121
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
22-
import com.fasterxml.jackson.module.kotlin.KotlinModule
22+
import com.fasterxml.jackson.module.kotlin.kotlinModule
2323
import org.ktorm.schema.*
2424
import java.lang.reflect.InvocationTargetException
2525
import java.sql.PreparedStatement
@@ -31,7 +31,7 @@ import java.sql.Types
3131
*/
3232
public val sharedObjectMapper: ObjectMapper = ObjectMapper()
3333
.registerModule(KtormModule())
34-
.registerModule(KotlinModule())
34+
.registerModule(kotlinModule())
3535
.registerModule(JavaTimeModule())
3636

3737
/**

‎ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ dependencies {
99
compileOnly(kotlin("maven-plugin"))
1010
compileOnly(kotlin("compiler"))
1111
compileOnly("org.apache.maven:maven-core:3.9.3")
12-
implementation("com.google.devtools.ksp:symbol-processing-cmdline:1.9.0-1.0.13")
12+
implementation("com.google.devtools.ksp:symbol-processing-cmdline:1.9.23-1.0.20")
1313
implementation(project(":ktorm-ksp-compiler")) {
1414
exclude(group = "com.pinterest.ktlint", module = "ktlint-rule-engine")
1515
exclude(group = "com.pinterest.ktlint", module = "ktlint-ruleset-standard")

‎ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ dependencies {
99
implementation(project(":ktorm-core"))
1010
implementation(project(":ktorm-ksp-annotations"))
1111
implementation(project(":ktorm-ksp-spi"))
12-
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.13")
12+
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20")
1313
implementation("com.squareup:kotlinpoet-ksp:1.11.0")
1414
implementation("org.atteo:evo-inflector:1.3")
1515
implementation("com.pinterest.ktlint:ktlint-rule-engine:0.50.0") {

‎ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@ import com.squareup.kotlinpoet.KModifier
2222
import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
2323
import com.squareup.kotlinpoet.ksp.toClassName
2424
import com.squareup.kotlinpoet.ksp.toTypeName
25+
import org.ktorm.entity.Entity
2526
import org.ktorm.ksp.compiler.util._type
2627
import org.ktorm.ksp.spi.TableMetadata
28+
import kotlin.reflect.full.memberProperties
2729

2830
@OptIn(KotlinPoetKspPreview::class)
2931
internal object ComponentFunctionGenerator {
3032

3133
fun generate(table: TableMetadata): Sequence<FunSpec> {
34+
val skipNames = Entity::class.memberProperties.map { it.name }.toSet()
3235
return table.entityClass.getAllProperties()
3336
.filter { it.isAbstract() }
34-
.filterNot { it.simpleName.asString() in setOf("entityClass", "properties") }
37+
.filter { it.simpleName.asString() !in skipNames }
3538
.mapIndexed { i, prop ->
3639
FunSpec.builder("component${i + 1}")
3740
.addKdoc("Return the value of [%L.%L]. ",

‎ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.ktorm.entity.Entity
2525
import org.ktorm.ksp.annotation.Undefined
2626
import org.ktorm.ksp.compiler.util.*
2727
import org.ktorm.ksp.spi.TableMetadata
28+
import kotlin.reflect.full.memberProperties
2829

2930
@OptIn(KotlinPoetKspPreview::class)
3031
internal object PseudoConstructorFunctionGenerator {
@@ -43,9 +44,10 @@ internal object PseudoConstructorFunctionGenerator {
4344
}
4445

4546
internal fun buildParameters(table: TableMetadata): Sequence<ParameterSpec> {
47+
val skipNames = Entity::class.memberProperties.map { it.name }.toSet()
4648
return table.entityClass.getAllProperties()
4749
.filter { it.isAbstract() }
48-
.filterNot { it.simpleName.asString() in setOf("entityClass", "properties") }
50+
.filter { it.simpleName.asString() !in skipNames }
4951
.map { prop ->
5052
val propName = prop.simpleName.asString()
5153
val propType = prop._type.makeNullable().toTypeName()
@@ -63,8 +65,9 @@ internal object PseudoConstructorFunctionGenerator {
6365
addStatement("val·entity·=·%T.create<%T>()", Entity::class.asClassName(), table.entityClass.toClassName())
6466
}
6567

68+
val skipNames = Entity::class.memberProperties.map { it.name }.toSet()
6669
for (prop in table.entityClass.getAllProperties()) {
67-
if (!prop.isAbstract() || prop.simpleName.asString() in setOf("entityClass", "properties")) {
70+
if (!prop.isAbstract() || prop.simpleName.asString() in skipNames) {
6871
continue
6972
}
7073

‎ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.ktorm.ksp.spi.TableMetadata
3131
import org.ktorm.schema.TypeReference
3232
import java.lang.reflect.InvocationTargetException
3333
import java.util.*
34+
import kotlin.reflect.full.memberProperties
3435
import kotlin.reflect.jvm.jvmName
3536

3637
@OptIn(KspExperimental::class)
@@ -119,6 +120,8 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn
119120
}
120121

121122
private fun KSClassDeclaration.getProperties(ignoreProperties: Set<String>): Sequence<KSPropertyDeclaration> {
123+
val skipNames = Entity::class.memberProperties.map { it.name }.toSet()
124+
122125
val constructorParams = HashSet<String>()
123126
if (classKind == CLASS) {
124127
primaryConstructor?.parameters?.mapTo(constructorParams) { it.name!!.asString() }
@@ -129,7 +132,7 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn
129132
.filterNot { it.isAnnotationPresent(Ignore::class) }
130133
.filterNot { classKind == CLASS && !it.hasBackingField }
131134
.filterNot { classKind == INTERFACE && !it.isAbstract() }
132-
.filterNot { classKind == INTERFACE && it.simpleName.asString() in setOf("entityClass", "properties") }
135+
.filterNot { classKind == INTERFACE && it.simpleName.asString() in skipNames }
133136
.sortedByDescending { it.simpleName.asString() in constructorParams }
134137
}
135138

‎ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ plugins {
66
}
77

88
dependencies {
9-
api("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.13")
9+
api("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20")
1010
api("com.squareup:kotlinpoet-ksp:1.11.0")
1111
}

‎ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt

+147
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,42 @@ private fun <T : BaseTable<*>> buildBulkInsertExpression(
309309
* on conflict (id) do update set salary = t_employee.salary + ?
310310
* ```
311311
*
312+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
313+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
314+
* as belows:
315+
*
316+
* ```kotlin
317+
* database.bulkInsertOrUpdate(Employees) {
318+
* item {
319+
* set(it.id, 1)
320+
* set(it.name, "vince")
321+
* set(it.job, "engineer")
322+
* set(it.salary, 1000)
323+
* set(it.hireDate, LocalDate.now())
324+
* set(it.departmentId, 1)
325+
* }
326+
* item {
327+
* set(it.id, 5)
328+
* set(it.name, "vince")
329+
* set(it.job, "engineer")
330+
* set(it.salary, 1000)
331+
* set(it.hireDate, LocalDate.now())
332+
* set(it.departmentId, 1)
333+
* }
334+
* onConflict(it.name, it.job) {
335+
* set(it.salary, it.salary + 900)
336+
* }
337+
* }
338+
* ```
339+
*
340+
* Generated SQL:
341+
*
342+
* ```sql
343+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
344+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
345+
* on conflict (name, job) do update set salary = t_employee.salary + ?
346+
* ```
347+
*
312348
* @since 3.3.0
313349
* @param table the table to be inserted.
314350
* @param block the DSL block used to construct the expression.
@@ -360,6 +396,43 @@ public fun <T : BaseTable<*>> Database.bulkInsertOrUpdate(
360396
* returning id
361397
* ```
362398
*
399+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
400+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
401+
* as belows:
402+
*
403+
* ```kotlin
404+
* database.bulkInsertOrUpdateReturning(Employees, Employees.id) {
405+
* item {
406+
* set(it.id, 1)
407+
* set(it.name, "vince")
408+
* set(it.job, "engineer")
409+
* set(it.salary, 1000)
410+
* set(it.hireDate, LocalDate.now())
411+
* set(it.departmentId, 1)
412+
* }
413+
* item {
414+
* set(it.id, 5)
415+
* set(it.name, "vince")
416+
* set(it.job, "engineer")
417+
* set(it.salary, 1000)
418+
* set(it.hireDate, LocalDate.now())
419+
* set(it.departmentId, 1)
420+
* }
421+
* onConflict(it.name, it.job) {
422+
* set(it.salary, it.salary + 900)
423+
* }
424+
* }
425+
* ```
426+
*
427+
* Generated SQL:
428+
*
429+
* ```sql
430+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
431+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
432+
* on conflict (name, job) do update set salary = t_employee.salary + ?
433+
* returning id
434+
* ```
435+
*
363436
* @since 3.4.0
364437
* @param table the table to be inserted.
365438
* @param returning the column to return
@@ -413,6 +486,43 @@ public fun <T : BaseTable<*>, C : Any> Database.bulkInsertOrUpdateReturning(
413486
* returning id, job
414487
* ```
415488
*
489+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
490+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
491+
* as belows:
492+
*
493+
* ```kotlin
494+
* database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) {
495+
* item {
496+
* set(it.id, 1)
497+
* set(it.name, "vince")
498+
* set(it.job, "engineer")
499+
* set(it.salary, 1000)
500+
* set(it.hireDate, LocalDate.now())
501+
* set(it.departmentId, 1)
502+
* }
503+
* item {
504+
* set(it.id, 5)
505+
* set(it.name, "vince")
506+
* set(it.job, "engineer")
507+
* set(it.salary, 1000)
508+
* set(it.hireDate, LocalDate.now())
509+
* set(it.departmentId, 1)
510+
* }
511+
* onConflict(it.name, it.job) {
512+
* set(it.salary, it.salary + 900)
513+
* }
514+
* }
515+
* ```
516+
*
517+
* Generated SQL:
518+
*
519+
* ```sql
520+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
521+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
522+
* on conflict (name, job) do update set salary = t_employee.salary + ?
523+
* returning id, job
524+
* ```
525+
*
416526
* @since 3.4.0
417527
* @param table the table to be inserted.
418528
* @param returning the columns to return
@@ -467,6 +577,43 @@ public fun <T : BaseTable<*>, C1 : Any, C2 : Any> Database.bulkInsertOrUpdateRet
467577
* returning id, job, salary
468578
* ```
469579
*
580+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
581+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
582+
* as belows:
583+
*
584+
* ```kotlin
585+
* database.bulkInsertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) {
586+
* item {
587+
* set(it.id, 1)
588+
* set(it.name, "vince")
589+
* set(it.job, "engineer")
590+
* set(it.salary, 1000)
591+
* set(it.hireDate, LocalDate.now())
592+
* set(it.departmentId, 1)
593+
* }
594+
* item {
595+
* set(it.id, 5)
596+
* set(it.name, "vince")
597+
* set(it.job, "engineer")
598+
* set(it.salary, 1000)
599+
* set(it.hireDate, LocalDate.now())
600+
* set(it.departmentId, 1)
601+
* }
602+
* onConflict(it.name, it.job) {
603+
* set(it.salary, it.salary + 900)
604+
* }
605+
* }
606+
* ```
607+
*
608+
* Generated SQL:
609+
*
610+
* ```sql
611+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
612+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
613+
* on conflict (name, job) do update set salary = t_employee.salary + ?
614+
* returning id, job, salary
615+
* ```
616+
*
470617
* @since 3.4.0
471618
* @param table the table to be inserted.
472619
* @param returning the columns to return

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

+61
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ internal val Database.Companion.global: Database get() {
6868
* on conflict (id) do update set salary = salary + ?
6969
* ```
7070
*
71+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
72+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
73+
* as belows:
74+
*
75+
* ```kotlin
76+
* Employees.insertOrUpdate {
77+
* set(it.id, 1)
78+
* set(it.name, "vince")
79+
* set(it.job, "engineer")
80+
* set(it.salary, 1000)
81+
* set(it.hireDate, LocalDate.now())
82+
* set(it.departmentId, 1)
83+
* onConflict(it.name, it.job) {
84+
* set(it.salary, it.salary + 900)
85+
* }
86+
* }
87+
* ```
88+
*
89+
* Generated SQL:
90+
*
91+
* ```sql
92+
* insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?)
93+
* on conflict (name, job) do update set salary = salary + ?
94+
* ```
95+
*
7196
* @param block the DSL block used to construct the expression.
7297
* @return the effected row count.
7398
*/
@@ -157,6 +182,42 @@ public fun <T : BaseTable<*>> T.bulkInsert(block: BulkInsertStatementBuilder<T>.
157182
* on conflict (id) do update set salary = salary + ?
158183
* ```
159184
*
185+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
186+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
187+
* as belows:
188+
*
189+
* ```kotlin
190+
* Employees.bulkInsertOrUpdate {
191+
* item {
192+
* set(it.id, 1)
193+
* set(it.name, "vince")
194+
* set(it.job, "engineer")
195+
* set(it.salary, 1000)
196+
* set(it.hireDate, LocalDate.now())
197+
* set(it.departmentId, 1)
198+
* }
199+
* item {
200+
* set(it.id, 5)
201+
* set(it.name, "vince")
202+
* set(it.job, "engineer")
203+
* set(it.salary, 1000)
204+
* set(it.hireDate, LocalDate.now())
205+
* set(it.departmentId, 1)
206+
* }
207+
* onConflict(it.name, it.job) {
208+
* set(it.salary, it.salary + 900)
209+
* }
210+
* }
211+
* ```
212+
*
213+
* Generated SQL:
214+
*
215+
* ```sql
216+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
217+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
218+
* on conflict (name, job) do update set salary = salary + ?
219+
* ```
220+
*
160221
* @since 3.3.0
161222
* @param block the DSL block used to construct the expression.
162223
* @return the effected row count.

‎ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt

+108
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,32 @@ public data class InsertOrUpdateExpression(
7575
* on conflict (id) do update set salary = t_employee.salary + ?
7676
* ```
7777
*
78+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
79+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
80+
* as belows:
81+
*
82+
* ```kotlin
83+
* database.insertOrUpdate(Employees) {
84+
* set(it.id, 1)
85+
* set(it.name, "vince")
86+
* set(it.job, "engineer")
87+
* set(it.salary, 1000)
88+
* set(it.hireDate, LocalDate.now())
89+
* set(it.departmentId, 1)
90+
* onConflict(it.name, it.job) {
91+
* set(it.salary, it.salary + 900)
92+
* }
93+
* }
94+
* ```
95+
*
96+
* Generated SQL:
97+
*
98+
* ```sql
99+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
100+
* values (?, ?, ?, ?, ?, ?)
101+
* on conflict (name, job) do update set salary = t_employee.salary + ?
102+
* ```
103+
*
78104
* @since 2.7
79105
* @param table the table to be inserted.
80106
* @param block the DSL block used to construct the expression.
@@ -116,6 +142,33 @@ public fun <T : BaseTable<*>> Database.insertOrUpdate(
116142
* returning id
117143
* ```
118144
*
145+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
146+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
147+
* as belows:
148+
*
149+
* ```kotlin
150+
* val id = database.insertOrUpdateReturning(Employees, Employees.id) {
151+
* set(it.id, 1)
152+
* set(it.name, "vince")
153+
* set(it.job, "engineer")
154+
* set(it.salary, 1000)
155+
* set(it.hireDate, LocalDate.now())
156+
* set(it.departmentId, 1)
157+
* onConflict(it.name, it.job) {
158+
* set(it.salary, it.salary + 900)
159+
* }
160+
* }
161+
* ```
162+
*
163+
* Generated SQL:
164+
*
165+
* ```sql
166+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
167+
* values (?, ?, ?, ?, ?, ?)
168+
* on conflict (name, job) do update set salary = t_employee.salary + ?
169+
* returning id
170+
* ```
171+
*
119172
* @since 3.4.0
120173
* @param table the table to be inserted.
121174
* @param returning the column to return
@@ -162,6 +215,33 @@ public fun <T : BaseTable<*>, C : Any> Database.insertOrUpdateReturning(
162215
* returning id, job
163216
* ```
164217
*
218+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
219+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
220+
* as belows:
221+
*
222+
* ```kotlin
223+
* val (id, job) = database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) {
224+
* set(it.id, 1)
225+
* set(it.name, "vince")
226+
* set(it.job, "engineer")
227+
* set(it.salary, 1000)
228+
* set(it.hireDate, LocalDate.now())
229+
* set(it.departmentId, 1)
230+
* onConflict(it.name, it.job) {
231+
* set(it.salary, it.salary + 900)
232+
* }
233+
* }
234+
* ```
235+
*
236+
* Generated SQL:
237+
*
238+
* ```sql
239+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
240+
* values (?, ?, ?, ?, ?, ?)
241+
* on conflict (name, job) do update set salary = t_employee.salary + ?
242+
* returning id, job
243+
* ```
244+
*
165245
* @since 3.4.0
166246
* @param table the table to be inserted.
167247
* @param returning the columns to return
@@ -210,6 +290,34 @@ public fun <T : BaseTable<*>, C1 : Any, C2 : Any> Database.insertOrUpdateReturni
210290
* returning id, job, salary
211291
* ```
212292
*
293+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
294+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
295+
* as belows:
296+
*
297+
* ```kotlin
298+
* val (id, job, salary) =
299+
* database.insertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) {
300+
* set(it.id, 1)
301+
* set(it.name, "vince")
302+
* set(it.job, "engineer")
303+
* set(it.salary, 1000)
304+
* set(it.hireDate, LocalDate.now())
305+
* set(it.departmentId, 1)
306+
* onConflict(it.name, it.job) {
307+
* set(it.salary, it.salary + 900)
308+
* }
309+
* }
310+
* ```
311+
*
312+
* Generated SQL:
313+
*
314+
* ```sql
315+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
316+
* values (?, ?, ?, ?, ?, ?)
317+
* on conflict (name, job) do update set salary = t_employee.salary + ?
318+
* returning id, job, salary
319+
* ```
320+
*
213321
* @since 3.4.0
214322
* @param table the table to be inserted.
215323
* @param returning the columns to return

‎ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt

+147
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,42 @@ private fun <T : BaseTable<*>> Database.buildBulkInsertExpression(
311311
* on conflict (id) do update set salary = t_employee.salary + ?
312312
* ```
313313
*
314+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
315+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
316+
* as belows:
317+
*
318+
* ```kotlin
319+
* database.bulkInsertOrUpdate(Employees) {
320+
* item {
321+
* set(it.id, 1)
322+
* set(it.name, "vince")
323+
* set(it.job, "engineer")
324+
* set(it.salary, 1000)
325+
* set(it.hireDate, LocalDate.now())
326+
* set(it.departmentId, 1)
327+
* }
328+
* item {
329+
* set(it.id, 5)
330+
* set(it.name, "vince")
331+
* set(it.job, "engineer")
332+
* set(it.salary, 1000)
333+
* set(it.hireDate, LocalDate.now())
334+
* set(it.departmentId, 1)
335+
* }
336+
* onConflict(it.name, it.job) {
337+
* set(it.salary, it.salary + 900)
338+
* }
339+
* }
340+
* ```
341+
*
342+
* Generated SQL:
343+
*
344+
* ```sql
345+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
346+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
347+
* on conflict (name, job) do update set salary = t_employee.salary + ?
348+
* ```
349+
*
314350
* @param table the table to be inserted.
315351
* @param block the DSL block used to construct the expression.
316352
* @return the effected row count.
@@ -361,6 +397,43 @@ public fun <T : BaseTable<*>> Database.bulkInsertOrUpdate(
361397
* returning id
362398
* ```
363399
*
400+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
401+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
402+
* as belows:
403+
*
404+
* ```kotlin
405+
* database.bulkInsertOrUpdateReturning(Employees, Employees.id) {
406+
* item {
407+
* set(it.id, 1)
408+
* set(it.name, "vince")
409+
* set(it.job, "engineer")
410+
* set(it.salary, 1000)
411+
* set(it.hireDate, LocalDate.now())
412+
* set(it.departmentId, 1)
413+
* }
414+
* item {
415+
* set(it.id, 5)
416+
* set(it.name, "vince")
417+
* set(it.job, "engineer")
418+
* set(it.salary, 1000)
419+
* set(it.hireDate, LocalDate.now())
420+
* set(it.departmentId, 1)
421+
* }
422+
* onConflict(it.name, it.job) {
423+
* set(it.salary, it.salary + 900)
424+
* }
425+
* }
426+
* ```
427+
*
428+
* Generated SQL:
429+
*
430+
* ```sql
431+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
432+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
433+
* on conflict (name, job) do update set salary = t_employee.salary + ?
434+
* returning id
435+
* ```
436+
*
364437
* @since 3.6.0
365438
* @param table the table to be inserted.
366439
* @param returning the column to return
@@ -414,6 +487,43 @@ public fun <T : BaseTable<*>, C : Any> Database.bulkInsertOrUpdateReturning(
414487
* returning id, job
415488
* ```
416489
*
490+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
491+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
492+
* as belows:
493+
*
494+
* ```kotlin
495+
* database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) {
496+
* item {
497+
* set(it.id, 1)
498+
* set(it.name, "vince")
499+
* set(it.job, "engineer")
500+
* set(it.salary, 1000)
501+
* set(it.hireDate, LocalDate.now())
502+
* set(it.departmentId, 1)
503+
* }
504+
* item {
505+
* set(it.id, 5)
506+
* set(it.name, "vince")
507+
* set(it.job, "engineer")
508+
* set(it.salary, 1000)
509+
* set(it.hireDate, LocalDate.now())
510+
* set(it.departmentId, 1)
511+
* }
512+
* onConflict(it.name, it.job) {
513+
* set(it.salary, it.salary + 900)
514+
* }
515+
* }
516+
* ```
517+
*
518+
* Generated SQL:
519+
*
520+
* ```sql
521+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
522+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
523+
* on conflict (name, job) do update set salary = t_employee.salary + ?
524+
* returning id, job
525+
* ```
526+
*
417527
* @since 3.6.0
418528
* @param table the table to be inserted.
419529
* @param returning the columns to return
@@ -468,6 +578,43 @@ public fun <T : BaseTable<*>, C1 : Any, C2 : Any> Database.bulkInsertOrUpdateRet
468578
* returning id, job, salary
469579
* ```
470580
*
581+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
582+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
583+
* as belows:
584+
*
585+
* ```kotlin
586+
* database.bulkInsertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) {
587+
* item {
588+
* set(it.id, 1)
589+
* set(it.name, "vince")
590+
* set(it.job, "engineer")
591+
* set(it.salary, 1000)
592+
* set(it.hireDate, LocalDate.now())
593+
* set(it.departmentId, 1)
594+
* }
595+
* item {
596+
* set(it.id, 5)
597+
* set(it.name, "vince")
598+
* set(it.job, "engineer")
599+
* set(it.salary, 1000)
600+
* set(it.hireDate, LocalDate.now())
601+
* set(it.departmentId, 1)
602+
* }
603+
* onConflict(it.name, it.job) {
604+
* set(it.salary, it.salary + 900)
605+
* }
606+
* }
607+
* ```
608+
*
609+
* Generated SQL:
610+
*
611+
* ```sql
612+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
613+
* values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
614+
* on conflict (name, job) do update set salary = t_employee.salary + ?
615+
* returning id, job, salary
616+
* ```
617+
*
471618
* @since 3.6.0
472619
* @param table the table to be inserted.
473620
* @param returning the columns to return

‎ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt

+109-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public data class InsertOrUpdateExpression(
6262
* set(it.salary, 1000)
6363
* set(it.hireDate, LocalDate.now())
6464
* set(it.departmentId, 1)
65-
* onConflict(it.id) {
65+
* onConflict {
6666
* set(it.salary, it.salary + 900)
6767
* }
6868
* }
@@ -76,6 +76,32 @@ public data class InsertOrUpdateExpression(
7676
* on conflict (id) do update set salary = t_employee.salary + ?
7777
* ```
7878
*
79+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
80+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
81+
* as belows:
82+
*
83+
* ```kotlin
84+
* database.insertOrUpdate(Employees) {
85+
* set(it.id, 1)
86+
* set(it.name, "vince")
87+
* set(it.job, "engineer")
88+
* set(it.salary, 1000)
89+
* set(it.hireDate, LocalDate.now())
90+
* set(it.departmentId, 1)
91+
* onConflict(it.name, it.job) {
92+
* set(it.salary, it.salary + 900)
93+
* }
94+
* }
95+
* ```
96+
*
97+
* Generated SQL:
98+
*
99+
* ```sql
100+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
101+
* values (?, ?, ?, ?, ?, ?)
102+
* on conflict (name, job) do update set salary = t_employee.salary + ?
103+
* ```
104+
*
79105
* @param table the table to be inserted.
80106
* @param block the DSL block used to construct the expression.
81107
* @return the effected row count.
@@ -116,6 +142,33 @@ public fun <T : BaseTable<*>> Database.insertOrUpdate(
116142
* returning id
117143
* ```
118144
*
145+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
146+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
147+
* as belows:
148+
*
149+
* ```kotlin
150+
* val id = database.insertOrUpdateReturning(Employees, Employees.id) {
151+
* set(it.id, 1)
152+
* set(it.name, "vince")
153+
* set(it.job, "engineer")
154+
* set(it.salary, 1000)
155+
* set(it.hireDate, LocalDate.now())
156+
* set(it.departmentId, 1)
157+
* onConflict(it.name, it.job) {
158+
* set(it.salary, it.salary + 900)
159+
* }
160+
* }
161+
* ```
162+
*
163+
* Generated SQL:
164+
*
165+
* ```sql
166+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
167+
* values (?, ?, ?, ?, ?, ?)
168+
* on conflict (name, job) do update set salary = t_employee.salary + ?
169+
* returning id
170+
* ```
171+
*
119172
* @since 3.6.0
120173
* @param table the table to be inserted.
121174
* @param returning the column to return
@@ -162,6 +215,33 @@ public fun <T : BaseTable<*>, C : Any> Database.insertOrUpdateReturning(
162215
* returning id, job
163216
* ```
164217
*
218+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
219+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
220+
* as belows:
221+
*
222+
* ```kotlin
223+
* val (id, job) = database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) {
224+
* set(it.id, 1)
225+
* set(it.name, "vince")
226+
* set(it.job, "engineer")
227+
* set(it.salary, 1000)
228+
* set(it.hireDate, LocalDate.now())
229+
* set(it.departmentId, 1)
230+
* onConflict(it.name, it.job) {
231+
* set(it.salary, it.salary + 900)
232+
* }
233+
* }
234+
* ```
235+
*
236+
* Generated SQL:
237+
*
238+
* ```sql
239+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
240+
* values (?, ?, ?, ?, ?, ?)
241+
* on conflict (name, job) do update set salary = t_employee.salary + ?
242+
* returning id, job
243+
* ```
244+
*
165245
* @since 3.6.0
166246
* @param table the table to be inserted.
167247
* @param returning the columns to return
@@ -210,6 +290,34 @@ public fun <T : BaseTable<*>, C1 : Any, C2 : Any> Database.insertOrUpdateReturni
210290
* returning id, job, salary
211291
* ```
212292
*
293+
* By default, the column used in the `on conflict` statement is the primary key you already defined in
294+
* the schema definition. If you want, you can specify one or more columns for the `on conflict` statement
295+
* as belows:
296+
*
297+
* ```kotlin
298+
* val (id, job, salary) =
299+
* database.insertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) {
300+
* set(it.id, 1)
301+
* set(it.name, "vince")
302+
* set(it.job, "engineer")
303+
* set(it.salary, 1000)
304+
* set(it.hireDate, LocalDate.now())
305+
* set(it.departmentId, 1)
306+
* onConflict(it.name, it.job) {
307+
* set(it.salary, it.salary + 900)
308+
* }
309+
* }
310+
* ```
311+
*
312+
* Generated SQL:
313+
*
314+
* ```sql
315+
* insert into t_employee (id, name, job, salary, hire_date, department_id)
316+
* values (?, ?, ?, ?, ?, ?)
317+
* on conflict (name, job) do update set salary = t_employee.salary + ?
318+
* returning id, job, salary
319+
* ```
320+
*
213321
* @since 3.6.0
214322
* @param table the table to be inserted.
215323
* @param returning the columns to return

‎ktorm.version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.0.0
1+
4.1.0

0 commit comments

Comments
 (0)
Please sign in to comment.