diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24d9d052..7553edc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,19 +14,19 @@ jobs: strategy: fail-fast: true matrix: - java: [8, 11, 17, 20] + java: [8, 11, 17, 21] steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: ${{ matrix.java }} - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 - name: Assemble the Project run: ./gradlew assemble @@ -45,6 +45,8 @@ jobs: ktorm-core/build/reports/jacoco/test/jacocoTestReport.csv ktorm-global/build/reports/jacoco/test/jacocoTestReport.csv ktorm-jackson/build/reports/jacoco/test/jacocoTestReport.csv + ktorm-ksp-annotations/build/reports/jacoco/test/jacocoTestReport.csv + ktorm-ksp-compiler/build/reports/jacoco/test/jacocoTestReport.csv ktorm-support-mysql/build/reports/jacoco/test/jacocoTestReport.csv ktorm-support-oracle/build/reports/jacoco/test/jacocoTestReport.csv ktorm-support-postgresql/build/reports/jacoco/test/jacocoTestReport.csv @@ -78,16 +80,16 @@ jobs: needs: build steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 8 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 - name: Assemble the Project run: ./gradlew assemble diff --git a/README.md b/README.md index c510fd87..d7e6a078 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ object Employees : Table("t_employee") { } ``` -> Naming Strategy: It's highly recommended to name your entity classes by singular nouns, name table objects by plurals (eg. Employee/Employees, Department/Departments). +> 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). 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: diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 9c56d76a..449f27dd 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,7 +9,7 @@ repositories { } dependencies { - api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") - api("org.moditect:moditect-gradle-plugin:1.0.0-rc3") - api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1") + api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") + api("org.moditect:moditect:1.0.0.RC1") + api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.6") } diff --git a/buildSrc/src/main/kotlin/ktorm.modularity.gradle.kts b/buildSrc/src/main/kotlin/ktorm.modularity.gradle.kts index 1d6fd479..baf94b13 100644 --- a/buildSrc/src/main/kotlin/ktorm.modularity.gradle.kts +++ b/buildSrc/src/main/kotlin/ktorm.modularity.gradle.kts @@ -1,30 +1,37 @@ plugins { id("kotlin") - id("org.moditect.gradleplugin") } -moditect { - // Generate a multi-release jar, the module descriptor will be located at META-INF/versions/9/module-info.class - addMainModuleInfo { - jvmVersion.set("9") - overwriteExistingFiles.set(true) - module { - moduleInfoFile = file("src/main/moditect/module-info.java") +val moditect by tasks.registering { + doLast { + // Generate a multi-release modulized jar, module descriptor position: META-INF/versions/9/module-info.class + val inputJar = tasks.jar.flatMap { it.archiveFile }.map { it.asFile.toPath() }.get() + val outputDir = file("build/moditect").apply { mkdirs() }.toPath() + val moduleInfo = file("src/main/moditect/module-info.java").readText() + val version = project.version.toString() + org.moditect.commands.AddModuleInfo(moduleInfo, null, version, inputJar, outputDir, "9", true).run() + + // Replace the original jar with the modulized jar. + copy { + from(outputDir.resolve(inputJar.fileName)) + into(inputJar.parent) } } +} - // Let kotlin compiler know the module descriptor. - if (JavaVersion.current() >= JavaVersion.VERSION_1_9) { - sourceSets.main { - kotlin.srcDir("src/main/moditect") - } +tasks { + moditect { + dependsOn(jar) } + jar { + finalizedBy(moditect) + } +} - // Workaround to avoid circular task dependencies, see https://github.com/moditect/moditect-gradle-plugin/issues/14 - afterEvaluate { - val compileJava = tasks.compileJava.get() - val addDependenciesModuleInfo = tasks.addDependenciesModuleInfo.get() - compileJava.setDependsOn(compileJava.dependsOn - addDependenciesModuleInfo) +if (JavaVersion.current() >= JavaVersion.VERSION_1_9) { + // Let kotlin compiler know the module descriptor. + sourceSets.main { + kotlin.srcDir("src/main/moditect") } } diff --git a/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts b/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts index bb46335d..1f40f5e2 100644 --- a/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts @@ -155,6 +155,11 @@ publishing { id.set("brohacz") name.set("Michal Brosig") } + developer { + id.set("hc224") + name.set("hc224") + email.set("hc224@pm.me") + } } } } diff --git a/detekt.yml b/detekt.yml index 6e1c8b3b..8c08a502 100644 --- a/detekt.yml +++ b/detekt.yml @@ -49,7 +49,7 @@ complexity: active: true threshold: 4 ComplexInterface: - active: true + active: false threshold: 12 includeStaticDeclarations: false CyclomaticComplexMethod: @@ -75,7 +75,7 @@ complexity: active: false threshold: 7 NestedBlockDepth: - active: true + active: false threshold: 5 StringLiteralDuplication: active: false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 27313fbc..2fa91c5f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ktorm-core/ktorm-core.gradle.kts b/ktorm-core/ktorm-core.gradle.kts index f405badd..6ae9cc1c 100644 --- a/ktorm-core/ktorm-core.gradle.kts +++ b/ktorm-core/ktorm-core.gradle.kts @@ -19,7 +19,7 @@ val testOutput by configurations.creating { } val testJar by tasks.registering(Jar::class) { - dependsOn("testClasses") + dependsOn(tasks.testClasses) from(sourceSets.test.map { it.output }) archiveClassifier.set("test") } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt index 257813fc..781c8748 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt @@ -609,6 +609,7 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { return index } } + throw SQLException("Invalid column name: $columnLabel") } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt index b903d0d0..d97fcc55 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt @@ -147,7 +147,7 @@ public class Database( public val name: String /** - * The name of the connected database product, eg. MySQL, H2. + * The name of the connected database product, e.g. MySQL, H2. */ public val productName: String diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt index 2630ad24..83fc2997 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt @@ -23,6 +23,7 @@ import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable import java.lang.reflect.Proxy +import java.sql.SQLException import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf import kotlin.reflect.jvm.jvmErasure @@ -69,12 +70,12 @@ import kotlin.reflect.jvm.jvmErasure * * - For [Boolean] type, the default value is `false`. * - For [Char] type, the default value is `\u0000`. - * - For number types (such as [Int], [Long], [Double], etc), the default value is zero. + * - For number types (such as [Int], [Long], [Double], etc.), the default value is zero. * - For [String] type, the default value is an empty string. * - For entity types, the default value is a new-created entity object which is empty. * - For enum types, the default value is the first value of the enum, whose ordinal is 0. * - For array types, the default value is a new-created empty array. - * - For collection types (such as [Set], [List], [Map], etc), the default value is a new created mutable collection + * - For collection types (such as [Set], [List], [Map], etc.), the default value is a new created mutable collection * of the concrete type. * - For any other types, the default value is an instance created by its no-args constructor. If the constructor * doesn't exist, an exception is thrown. @@ -128,7 +129,7 @@ import kotlin.reflect.jvm.jvmErasure * refer to their documentation for more details. * * Besides of JDK serialization, the ktorm-jackson module also supports serializing entities in JSON format. This - * module provides an extension for Jackson, the famous JSON framework in Java word. It supports serializing entity + * module provides an extension for Jackson, the famous JSON framework in Java world. It supports serializing entity * objects into JSON format and parsing JSONs as entity objects. More details can be found in its documentation. */ public interface Entity> : Serializable { @@ -143,6 +144,13 @@ public interface Entity> : Serializable { */ public val properties: Map + /** + * Return the immutable view of this entity's changed properties and their original values. + * + * @since 4.1.0 + */ + public val changedProperties: Map + /** * Update the property changes of this entity into the database and return the affected record number. * @@ -156,18 +164,18 @@ public interface Entity> : Serializable { * `fromDatabase` references point to the database they are obtained from. For entity objects created by * [Entity.create] or [Entity.Factory], their `fromDatabase` references are `null` initially, so we can not call * [flushChanges] on them. But once we use them with [add] or [update] function, `fromDatabase` will be modified - * to the current database, so we will be able to call [flushChanges] on them afterwards. + * to the current database, so we will be able to call [flushChanges] on them afterward. * * @see add * @see update */ + @Throws(SQLException::class) public fun flushChanges(): Int /** * Clear the tracked property changes of this entity. * - * After calling this function, the [flushChanges] doesn't do anything anymore because the property changes - * are discarded. + * After calling this function, [flushChanges] will do nothing because property changes are discarded. */ public fun discardChanges() @@ -185,13 +193,14 @@ public interface Entity> : Serializable { * @see update * @see flushChanges */ + @Throws(SQLException::class) public fun delete(): Int /** * Obtain a property's value by its name. * * Note that this function doesn't follow the rules of default values discussed in the class level documentation. - * If the value doesn't exist, we will return `null` simply. + * If the value doesn't exist, it will simply return `null`. */ public operator fun get(name: String): Any? @@ -221,8 +230,8 @@ public interface Entity> : Serializable { public override fun hashCode(): Int /** - * Return a string representation of this table. - * The format is like `Employee{id=1, name=Eric, job=contributor, hireDate=2021-05-05, salary=50}`. + * Return a string representation of this entity. + * The format is like `Employee(id=1, name=Eric, job=contributor, hireDate=2021-05-05, salary=50)`. */ public override fun toString(): String diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt index bfed33b7..3e90194e 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt @@ -17,12 +17,11 @@ package org.ktorm.entity import org.ktorm.dsl.* -import org.ktorm.dsl.AliasRemover import org.ktorm.expression.* import org.ktorm.schema.* /** - * Insert the given entity into this sequence and return the affected record number. + * Insert the given entity into the table and return the affected record number. * * If we use an auto-increment key in our table, we need to tell Ktorm which is the primary key by calling * [Table.primaryKey] while registering columns, then this function will obtain the generated key from the @@ -233,7 +232,6 @@ private fun EntitySequence<*, *>.checkForDml() { */ private fun Entity<*>.findInsertColumns(table: Table<*>): Map, Any?> { val assignments = LinkedHashMap, Any?>() - for (column in table.columns) { if (column.binding != null && implementation.hasColumnValue(column.binding)) { assignments[column] = implementation.getColumnValue(column.binding) @@ -246,10 +244,9 @@ private fun Entity<*>.findInsertColumns(table: Table<*>): Map, Any?> { /** * Return columns associated with their values for update. */ +@Suppress("ConvertArgumentToSet") private fun Entity<*>.findUpdateColumns(table: Table<*>): Map, Any?> { val assignments = LinkedHashMap, Any?>() - - @Suppress("ConvertArgumentToSet") for (column in table.columns - table.primaryKeys) { if (column.binding != null && implementation.hasColumnValue(column.binding)) { assignments[column] = implementation.getColumnValue(column.binding) @@ -264,7 +261,6 @@ private fun Entity<*>.findUpdateColumns(table: Table<*>): Map, Any?> { */ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map, Any?> { val assignments = LinkedHashMap, Any?>() - for (column in fromTable.columns) { val binding = column.binding ?: continue @@ -286,11 +282,13 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map): Map { + check(parent == null) { "The entity is not attached to any database yet." } + val fromTable = fromTable ?: error("The entity is not attached to any database yet.") + + // Create an empty entity object to collect changed properties. + val result = Entity.create(entityClass, parent, fromDatabase, fromTable) + for (column in fromTable.columns) { + val binding = column.binding ?: continue + + when (binding) { + is ReferenceBinding -> { + if (binding.onProperty.name in changedProperties) { + val origin = changedProperties[binding.onProperty.name] as Entity<*>? + val originId = origin?.implementation?.getPrimaryKeyValue(binding.referenceTable as Table<*>) + result.implementation.setColumnValue(binding, originId) + } + } + is NestedBinding -> { + var anyChanged = false + var curr: Any? = this + + for (prop in binding.properties) { + if (curr is Entity<*>) { + curr = curr.implementation + } + + check(curr is EntityImplementation?) + + if (curr != null) { + if (prop.name in curr.changedProperties) { + curr = curr.changedProperties[prop.name] + anyChanged = true + } else { + curr = curr.getProperty(prop) + } + } + } + + if (anyChanged) { + result.implementation.setColumnValue(binding, curr) + } + } + } + } + + return result.properties +} + /** * Clear the tracked property changes of this entity. * diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt index caf34f73..d49957f6 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt @@ -92,6 +92,7 @@ internal fun EntityImplementation.getColumnValue(binding: ColumnBinding): Any? { curr = child?.implementation } } + return curr?.getProperty(binding.properties.last()) } } @@ -127,6 +128,7 @@ internal fun EntityImplementation.setColumnValue(binding: ColumnBinding, value: fromDatabase = this.fromDatabase, fromTable = binding.referenceTable as Table<*> ) + this.setProperty(binding.onProperty, child, forceSet) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt index b0134fc3..2bf5bd93 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt @@ -50,4 +50,14 @@ public class EntityExtensionsApi { public fun Entity<*>.setColumnValue(binding: ColumnBinding, value: Any?) { implementation.setColumnValue(binding, value) } + + /** + * Check if this entity is attached to the database. + * + * @since 4.1.0 + */ + public fun Entity<*>.isAttached(): Boolean { + val impl = this.implementation + return impl.fromDatabase != null && impl.fromTable != null && impl.parent == null + } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index a1b1f769..3a223d2a 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -38,7 +38,7 @@ internal class EntityImplementation( @Transient var fromDatabase: Database? = fromDatabase @Transient var fromTable: Table<*>? = fromTable @Transient var parent: EntityImplementation? = parent - @Transient var changedProperties = LinkedHashSet() + @Transient var changedProperties = LinkedHashMap() override fun invoke(proxy: Any, method: Method, args: Array?): Any? { return when (method.declaringClass.kotlin) { @@ -54,6 +54,7 @@ internal class EntityImplementation( when (method.name) { "getEntityClass" -> this.entityClass "getProperties" -> Collections.unmodifiableMap(this.values) + "getChangedProperties" -> this.findChangedProperties() "flushChanges" -> this.doFlushChanges() "discardChanges" -> this.doDiscardChanges() "delete" -> this.doDelete() @@ -150,13 +151,17 @@ internal class EntityImplementation( throw UnsupportedOperationException(msg) } + // Save property changes and original values. + if (name !in changedProperties) { + changedProperties[name] = values[name] + } + values[name] = value - changedProperties.add(name) } private fun copy(): Entity<*> { val entity = Entity.create(entityClass, parent, fromDatabase, fromTable) - entity.implementation.changedProperties.addAll(changedProperties) + entity.implementation.changedProperties.putAll(changedProperties) for ((name, value) in values) { if (value is Entity<*>) { @@ -204,7 +209,7 @@ internal class EntityImplementation( val javaClass = Class.forName(input.readUTF(), true, Thread.currentThread().contextClassLoader) entityClass = javaClass.kotlin values = input.readObject() as LinkedHashMap - changedProperties = LinkedHashSet() + changedProperties = LinkedHashMap() } override fun equals(other: Any?): Boolean { diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt index e0e8f1a4..3caf8f24 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt @@ -44,6 +44,7 @@ internal val Method.kotlinProperty: Pair, Boolean>? get() { return Pair(prop, false) } } + return null } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index 8e9701ed..ae623e56 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -52,7 +52,7 @@ public abstract class SqlExpression { } /** - * Base class of scalar expressions. An expression is "scalar" if it has a return value (eg. `a + 1`). + * Base class of scalar expressions. An expression is "scalar" if it has a return value (e.g. `a + 1`). * * @param T the return value's type of this scalar expression. */ @@ -85,7 +85,7 @@ public abstract class QuerySourceExpression : SqlExpression() * @property orderBy a list of order-by expressions, used in the `order by` clause of a query. * @property offset the offset of the first returned record. * @property limit max record numbers returned by the query. - * @property tableAlias the alias when this query is nested in another query's source, eg. `select * from (...) alias`. + * @property tableAlias the alias when this query is nested in another query's source, e.g. `select * from (...) alias`. */ public sealed class QueryExpression : QuerySourceExpression() { public abstract val orderBy: List @@ -151,7 +151,7 @@ public data class InsertExpression( ) : SqlExpression() /** - * Insert-from-query expression, eg. `insert into tmp(num) select 1 from dual`. + * Insert-from-query expression, e.g. `insert into tmp(num) select 1 from dual`. * * @property table the table to be inserted. * @property columns the columns to be inserted. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt index b1f425b9..fbe62d3e 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt @@ -372,7 +372,7 @@ public abstract class BaseTable( /** * Convert this table to a [TableExpression]. */ - public fun asExpression(): TableExpression { + public open fun asExpression(): TableExpression { return TableExpression(tableName, alias, catalog, schema) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt index 6d4f8095..5bfb1b3c 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt @@ -29,7 +29,7 @@ import kotlin.reflect.KProperty1 public sealed class ColumnBinding /** - * Bind the column to nested properties, eg. `employee.manager.department.id`. + * Bind the column to nested properties, e.g. `employee.manager.department.id`. * * @property properties the nested properties, cannot be empty. */ @@ -118,19 +118,19 @@ public data class Column( * * @see ColumnDeclaringExpression */ - val label: String get() = toString(separator = "_") + val label: String = toString(separator = "_") /** * Return all the bindings of this column, including the primary [binding] and [extraBindings]. */ - val allBindings: List get() = binding?.let { listOf(it) + extraBindings } ?: emptyList() + val allBindings: List = binding?.let { listOf(it) + extraBindings } ?: emptyList() /** * If the column is bound to a reference table, return the table, otherwise return null. * * Shortcut for `(binding as? ReferenceBinding)?.referenceTable`. */ - val referenceTable: BaseTable<*>? get() = (binding as? ReferenceBinding)?.referenceTable + val referenceTable: BaseTable<*>? = (binding as? ReferenceBinding)?.referenceTable /** * Convert this column to a [ColumnExpression]. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt index 7ad6581c..90903862 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt @@ -56,7 +56,7 @@ public open class Table>( ) : BaseTable(tableName, alias, catalog, schema, entityClass) { /** - * Bind the column to nested properties, eg. `employee.manager.department.id`. + * Bind the column to nested properties, e.g. `employee.manager.department.id`. * * Note: Since [Column] is immutable, this function will create a new [Column] instance and replace the origin * registered one. diff --git a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt index 54044f18..98b1ad04 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt @@ -1,5 +1,6 @@ package org.ktorm.entity +import org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException import org.junit.Test import org.ktorm.BaseTest import org.ktorm.database.Database @@ -271,10 +272,99 @@ class EntityTest : BaseTest() { companion object : Entity.Factory() var id: Int? var name: String? + var job: String? } object Parents : Table("t_employee") { val id = int("id").primaryKey().bindTo { it.child?.grandChild?.id } + val name = varchar("name").bindTo { it.child?.grandChild?.name } + val job = varchar("job").bindTo { it.child?.grandChild?.job } + } + + @Test + fun testInternalChangedPropertiesForNestedBinding1() { + val p1 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError() + p1.child?.grandChild?.job = "Senior Engineer" + p1.child?.grandChild?.job = "Expert Engineer" + + assert(p1.implementation.changedProperties.size == 0) + assert(p1.child?.implementation?.changedProperties?.size == 0) + assert(p1.child?.grandChild?.implementation?.changedProperties?.size == 1) + assert(p1.child?.grandChild?.implementation?.changedProperties?.get("job") == "engineer") + assert(p1.flushChanges() == 1) + } + + @Test + fun testInternalChangedPropertiesForNestedBinding2() { + val p2 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError() + p2.child?.grandChild?.name = "Vincent" + p2.child?.grandChild?.job = "Senior Engineer" + p2.child?.grandChild?.job = "Expert Engineer" + + assert(p2.implementation.changedProperties.size == 0) + assert(p2.child?.implementation?.changedProperties?.size == 0) + assert(p2.child?.grandChild?.implementation?.changedProperties?.size == 2) + assert(p2.child?.grandChild?.implementation?.changedProperties?.get("name") == "vince") + assert(p2.child?.grandChild?.implementation?.changedProperties?.get("job") == "engineer") + assert(p2.flushChanges() == 1) + } + + @Test + fun testChangedPropertiesForNestedBinding1() { + val p1 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError() + p1.child?.grandChild?.job = "Senior Engineer" + p1.child?.grandChild?.job = "Expert Engineer" + + assert(p1.changedProperties.size == 1) + assert(p1.changedProperties["child"].toString() == "Child(grandChild=GrandChild(job=engineer))") + assert(p1.flushChanges() == 1) + } + + @Test + fun testChangedPropertiesForNestedBinding2() { + val p2 = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError() + p2.child?.grandChild?.name = "Vincent" + p2.child?.grandChild?.job = "Senior Engineer" + p2.child?.grandChild?.job = "Expert Engineer" + + assert(p2.changedProperties.size == 1) + assert(p2.changedProperties["child"].toString() == "Child(grandChild=GrandChild(name=vince, job=engineer))") + assert(p2.flushChanges() == 1) + } + + @Test + fun testChangedPropertiesForReferenceBinding() { + val e = database.employees.find { it.id eq 1 } ?: throw AssertionError() + e.name = "Vincent" + e.job = "Senior Engineer" + e.job = "Expert Engineer" + e.manager = database.employees.find { it.id eq 2 } + e.manager = database.employees.find { it.id eq 2 } + e.salary = 999999 + e.department = database.departments.find { it.id eq 2 } ?: throw AssertionError() + e.department = database.departments.find { it.id eq 2 } ?: throw AssertionError() + + val changed = e.changedProperties + assert(changed.size == 5) + assert(changed["name"] == "vince") + assert(changed["job"] == "engineer") + assert(changed["manager"].toString() == "Employee(id=null)") + assert(changed["salary"] == 100L) + assert(changed["department"].toString() == "Department(id=1)") + assert(e.flushChanges() == 1) + } + + @Test + fun testExceptionThrowsByProxy() { + try { + val e = database.employees.find { it.id eq 1 } ?: throw AssertionError() + e.department = Department() + e.flushChanges() + + throw AssertionError("failed") + } catch (e: JdbcSQLIntegrityConstraintViolationException) { + assert(e.message!!.contains("NULL not allowed for column \"department_id\"")) + } } @Test @@ -783,4 +873,4 @@ class EntityTest : BaseTest() { assert(departmentTransient !== departmentAttached) assert(departmentTransient.hashCode() == departmentAttached.hashCode()) } -} \ No newline at end of file +} diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt index 6776f776..00474bad 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt @@ -63,10 +63,11 @@ internal class EntityDeserializers : SimpleDeserializers() { parser: JsonParser, ctx: DeserializationContext ): Map> { + val skipNames = Entity::class.memberProperties.map { it.name }.toSet() return entityClass.memberProperties .asSequence() .filter { it.isAbstract } - .filter { it.name != "entityClass" && it.name != "properties" } + .filter { it.name !in skipNames } .filter { it.findAnnotationForDeserialization() == null } .filter { prop -> val jsonProperty = prop.findAnnotationForDeserialization() diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt index aeab57ec..59fce3cc 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt @@ -62,10 +62,11 @@ internal class EntitySerializers : SimpleSerializers() { } private fun findReadableProperties(entity: Entity<*>): Map> { + val skipNames = Entity::class.memberProperties.map { it.name }.toSet() return entity.entityClass.memberProperties .asSequence() .filter { it.isAbstract } - .filter { it.name != "entityClass" && it.name != "properties" } + .filter { it.name !in skipNames } .filter { it.findAnnotationForSerialization() == null } .filter { prop -> val jsonProperty = prop.findAnnotationForSerialization() diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt index 2006cdd5..0ec47aa1 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt @@ -19,7 +19,7 @@ package org.ktorm.jackson import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.kotlinModule import org.ktorm.schema.* import java.lang.reflect.InvocationTargetException import java.sql.PreparedStatement @@ -31,7 +31,7 @@ import java.sql.Types */ public val sharedObjectMapper: ObjectMapper = ObjectMapper() .registerModule(KtormModule()) - .registerModule(KotlinModule()) + .registerModule(kotlinModule()) .registerModule(JavaTimeModule()) /** diff --git a/ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts b/ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts index 3c2766db..b5354918 100644 --- a/ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts +++ b/ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts @@ -9,7 +9,7 @@ dependencies { compileOnly(kotlin("maven-plugin")) compileOnly(kotlin("compiler")) compileOnly("org.apache.maven:maven-core:3.9.3") - implementation("com.google.devtools.ksp:symbol-processing-cmdline:1.9.0-1.0.13") + implementation("com.google.devtools.ksp:symbol-processing-cmdline:1.9.23-1.0.20") implementation(project(":ktorm-ksp-compiler")) { exclude(group = "com.pinterest.ktlint", module = "ktlint-rule-engine") exclude(group = "com.pinterest.ktlint", module = "ktlint-ruleset-standard") diff --git a/ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts b/ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts index 2285feb5..7a927ca1 100644 --- a/ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts +++ b/ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts @@ -9,7 +9,7 @@ dependencies { implementation(project(":ktorm-core")) implementation(project(":ktorm-ksp-annotations")) implementation(project(":ktorm-ksp-spi")) - implementation("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.13") + implementation("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20") implementation("com.squareup:kotlinpoet-ksp:1.11.0") implementation("org.atteo:evo-inflector:1.3") implementation("com.pinterest.ktlint:ktlint-rule-engine:0.50.0") { diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt index 0846cd73..bbf4f8ab 100644 --- a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt @@ -22,16 +22,19 @@ import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName +import org.ktorm.entity.Entity import org.ktorm.ksp.compiler.util._type import org.ktorm.ksp.spi.TableMetadata +import kotlin.reflect.full.memberProperties @OptIn(KotlinPoetKspPreview::class) internal object ComponentFunctionGenerator { fun generate(table: TableMetadata): Sequence { + val skipNames = Entity::class.memberProperties.map { it.name }.toSet() return table.entityClass.getAllProperties() .filter { it.isAbstract() } - .filterNot { it.simpleName.asString() in setOf("entityClass", "properties") } + .filter { it.simpleName.asString() !in skipNames } .mapIndexed { i, prop -> FunSpec.builder("component${i + 1}") .addKdoc("Return the value of [%L.%L]. ", diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt index 046467ce..14d79898 100644 --- a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt @@ -25,6 +25,7 @@ import org.ktorm.entity.Entity import org.ktorm.ksp.annotation.Undefined import org.ktorm.ksp.compiler.util.* import org.ktorm.ksp.spi.TableMetadata +import kotlin.reflect.full.memberProperties @OptIn(KotlinPoetKspPreview::class) internal object PseudoConstructorFunctionGenerator { @@ -43,9 +44,10 @@ internal object PseudoConstructorFunctionGenerator { } internal fun buildParameters(table: TableMetadata): Sequence { + val skipNames = Entity::class.memberProperties.map { it.name }.toSet() return table.entityClass.getAllProperties() .filter { it.isAbstract() } - .filterNot { it.simpleName.asString() in setOf("entityClass", "properties") } + .filter { it.simpleName.asString() !in skipNames } .map { prop -> val propName = prop.simpleName.asString() val propType = prop._type.makeNullable().toTypeName() @@ -63,8 +65,9 @@ internal object PseudoConstructorFunctionGenerator { addStatement("val·entity·=·%T.create<%T>()", Entity::class.asClassName(), table.entityClass.toClassName()) } + val skipNames = Entity::class.memberProperties.map { it.name }.toSet() for (prop in table.entityClass.getAllProperties()) { - if (!prop.isAbstract() || prop.simpleName.asString() in setOf("entityClass", "properties")) { + if (!prop.isAbstract() || prop.simpleName.asString() in skipNames) { continue } diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt index 55b94489..872b1802 100644 --- a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt @@ -31,6 +31,7 @@ import org.ktorm.ksp.spi.TableMetadata import org.ktorm.schema.TypeReference import java.lang.reflect.InvocationTargetException import java.util.* +import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.jvmName @OptIn(KspExperimental::class) @@ -119,6 +120,8 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn } private fun KSClassDeclaration.getProperties(ignoreProperties: Set): Sequence { + val skipNames = Entity::class.memberProperties.map { it.name }.toSet() + val constructorParams = HashSet() if (classKind == CLASS) { primaryConstructor?.parameters?.mapTo(constructorParams) { it.name!!.asString() } @@ -129,7 +132,7 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn .filterNot { it.isAnnotationPresent(Ignore::class) } .filterNot { classKind == CLASS && !it.hasBackingField } .filterNot { classKind == INTERFACE && !it.isAbstract() } - .filterNot { classKind == INTERFACE && it.simpleName.asString() in setOf("entityClass", "properties") } + .filterNot { classKind == INTERFACE && it.simpleName.asString() in skipNames } .sortedByDescending { it.simpleName.asString() in constructorParams } } diff --git a/ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts b/ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts index 86f5b1ce..6e869a9f 100644 --- a/ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts +++ b/ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts @@ -6,6 +6,6 @@ plugins { } dependencies { - api("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.13") + api("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20") api("com.squareup:kotlinpoet-ksp:1.11.0") } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 927b7147..9eb4a84f 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -309,6 +309,42 @@ private fun > buildBulkInsertExpression( * on conflict (id) do update set salary = t_employee.salary + ? * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdate(Employees) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * ``` + * * @since 3.3.0 * @param table the table to be inserted. * @param block the DSL block used to construct the expression. @@ -360,6 +396,43 @@ public fun > Database.bulkInsertOrUpdate( * returning id * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Employees.id) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id + * ``` + * * @since 3.4.0 * @param table the table to be inserted. * @param returning the column to return @@ -413,6 +486,43 @@ public fun , C : Any> Database.bulkInsertOrUpdateReturning( * returning id, job * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job + * ``` + * * @since 3.4.0 * @param table the table to be inserted. * @param returning the columns to return @@ -467,6 +577,43 @@ public fun , C1 : Any, C2 : Any> Database.bulkInsertOrUpdateRet * returning id, job, salary * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job, salary + * ``` + * * @since 3.4.0 * @param table the table to be inserted. * @param returning the columns to return diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt index 9596c5fa..ec721bb9 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt @@ -68,6 +68,31 @@ internal val Database.Companion.global: Database get() { * on conflict (id) do update set salary = salary + ? * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * Employees.insertOrUpdate { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = salary + ? + * ``` + * * @param block the DSL block used to construct the expression. * @return the effected row count. */ @@ -157,6 +182,42 @@ public fun > T.bulkInsert(block: BulkInsertStatementBuilder. * on conflict (id) do update set salary = salary + ? * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * Employees.bulkInsertOrUpdate { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = salary + ? + * ``` + * * @since 3.3.0 * @param block the DSL block used to construct the expression. * @return the effected row count. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 707672dc..22419ac8 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -75,6 +75,32 @@ public data class InsertOrUpdateExpression( * on conflict (id) do update set salary = t_employee.salary + ? * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.insertOrUpdate(Employees) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * ``` + * * @since 2.7 * @param table the table to be inserted. * @param block the DSL block used to construct the expression. @@ -116,6 +142,33 @@ public fun > Database.insertOrUpdate( * returning id * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * val id = database.insertOrUpdateReturning(Employees, Employees.id) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id + * ``` + * * @since 3.4.0 * @param table the table to be inserted. * @param returning the column to return @@ -162,6 +215,33 @@ public fun , C : Any> Database.insertOrUpdateReturning( * returning id, job * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * val (id, job) = database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job + * ``` + * * @since 3.4.0 * @param table the table to be inserted. * @param returning the columns to return @@ -210,6 +290,34 @@ public fun , C1 : Any, C2 : Any> Database.insertOrUpdateReturni * returning id, job, salary * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * val (id, job, salary) = + * database.insertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job, salary + * ``` + * * @since 3.4.0 * @param table the table to be inserted. * @param returning the columns to return diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt index 1f412387..13383313 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt @@ -311,6 +311,42 @@ private fun > Database.buildBulkInsertExpression( * on conflict (id) do update set salary = t_employee.salary + ? * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdate(Employees) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * ``` + * * @param table the table to be inserted. * @param block the DSL block used to construct the expression. * @return the effected row count. @@ -361,6 +397,43 @@ public fun > Database.bulkInsertOrUpdate( * returning id * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Employees.id) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id + * ``` + * * @since 3.6.0 * @param table the table to be inserted. * @param returning the column to return @@ -414,6 +487,43 @@ public fun , C : Any> Database.bulkInsertOrUpdateReturning( * returning id, job * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job + * ``` + * * @since 3.6.0 * @param table the table to be inserted. * @param returning the columns to return @@ -468,6 +578,43 @@ public fun , C1 : Any, C2 : Any> Database.bulkInsertOrUpdateRet * returning id, job, salary * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job, salary + * ``` + * * @since 3.6.0 * @param table the table to be inserted. * @param returning the columns to return diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt index 04b2a371..d59994c8 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt @@ -62,7 +62,7 @@ public data class InsertOrUpdateExpression( * set(it.salary, 1000) * set(it.hireDate, LocalDate.now()) * set(it.departmentId, 1) - * onConflict(it.id) { + * onConflict { * set(it.salary, it.salary + 900) * } * } @@ -76,6 +76,32 @@ public data class InsertOrUpdateExpression( * on conflict (id) do update set salary = t_employee.salary + ? * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * database.insertOrUpdate(Employees) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * ``` + * * @param table the table to be inserted. * @param block the DSL block used to construct the expression. * @return the effected row count. @@ -116,6 +142,33 @@ public fun > Database.insertOrUpdate( * returning id * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * val id = database.insertOrUpdateReturning(Employees, Employees.id) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id + * ``` + * * @since 3.6.0 * @param table the table to be inserted. * @param returning the column to return @@ -162,6 +215,33 @@ public fun , C : Any> Database.insertOrUpdateReturning( * returning id, job * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * val (id, job) = database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job + * ``` + * * @since 3.6.0 * @param table the table to be inserted. * @param returning the columns to return @@ -210,6 +290,34 @@ public fun , C1 : Any, C2 : Any> Database.insertOrUpdateReturni * returning id, job, salary * ``` * + * By default, the column used in the `on conflict` statement is the primary key you already defined in + * the schema definition. If you want, you can specify one or more columns for the `on conflict` statement + * as belows: + * + * ```kotlin + * val (id, job, salary) = + * database.insertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onConflict(it.name, it.job) { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) + * on conflict (name, job) do update set salary = t_employee.salary + ? + * returning id, job, salary + * ``` + * * @since 3.6.0 * @param table the table to be inserted. * @param returning the columns to return diff --git a/ktorm.version b/ktorm.version index fcdb2e10..ee74734a 100644 --- a/ktorm.version +++ b/ktorm.version @@ -1 +1 @@ -4.0.0 +4.1.0