diff --git a/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md b/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md index ef4460fc47..b85accd6e9 100644 --- a/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md +++ b/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md @@ -663,6 +663,26 @@ StarWarsFilms.upsert( it[director] = "JJ Abrams" } ``` +If the update operation should be identical to the insert operation except for a few columns, +then `onUpdateExclude` should be provided an argument with the specific columns to exclude. +This parameter could also be used for the reverse case when only a small subset of columns should be updated but duplicating the insert values is tedious: +```kotlin +// on conflict, all columns EXCEPT [director] are updated with values from the lambda block +StarWarsFilms.upsert(onUpdateExclude = listOf(StarWarsFilms.director)) { + it[sequelId] = 9 + it[name] = "The Rise of Skywalker" + it[director] = "JJ Abrams" +} + +// on conflict, ONLY column [director] is updated with value from the lambda block +StarWarsFilms.upsert( + onUpdateExclude = StarWarsFilms.columns - setOf(StarWarsFilms.director) +) { + it[sequelId] = 9 + it[name] = "The Rise of Skywalker" + it[director] = "JJ Abrams" +} +``` If a specific database supports user-defined key columns and none are provided, the table's primary key is used. If there is no defined primary key, the first unique index is used. If there are no unique indices, each database handles this case differently, so it is strongly advised that keys are defined to avoid unexpected results. diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index fde30674ca..eae323069d 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -1615,10 +1615,10 @@ public final class org/jetbrains/exposed/sql/QueriesKt { public static final fun batchReplace (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; public static synthetic fun batchReplace$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; public static synthetic fun batchReplace$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; - public static final fun batchUpsert (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; - public static final fun batchUpsert (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; - public static synthetic fun batchUpsert$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; - public static synthetic fun batchUpsert$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static final fun batchUpsert (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static final fun batchUpsert (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static synthetic fun batchUpsert$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static synthetic fun batchUpsert$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; public static final fun deleteAll (Lorg/jetbrains/exposed/sql/Table;)I public static final fun deleteIgnoreWhere (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Integer;Ljava/lang/Long;Lkotlin/jvm/functions/Function2;)I public static synthetic fun deleteIgnoreWhere$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Integer;Ljava/lang/Long;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)I @@ -1651,8 +1651,8 @@ public final class org/jetbrains/exposed/sql/QueriesKt { public static final fun update (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;)I public static synthetic fun update$default (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I public static synthetic fun update$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)I - public static final fun upsert (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; - public static synthetic fun upsert$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; + public static final fun upsert (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; + public static synthetic fun upsert$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; } public class org/jetbrains/exposed/sql/Query : org/jetbrains/exposed/sql/AbstractQuery { @@ -2807,10 +2807,11 @@ public class org/jetbrains/exposed/sql/statements/BatchUpdateStatement : org/jet } public class org/jetbrains/exposed/sql/statements/BatchUpsertStatement : org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement { - public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Z)V - public synthetic fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Z)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getKeys ()[Lorg/jetbrains/exposed/sql/Column; public final fun getOnUpdate ()Ljava/util/List; + public final fun getOnUpdateExclude ()Ljava/util/List; public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; } @@ -3033,11 +3034,12 @@ public class org/jetbrains/exposed/sql/statements/UpdateStatement : org/jetbrain } public class org/jetbrains/exposed/sql/statements/UpsertStatement : org/jetbrains/exposed/sql/statements/InsertStatement { - public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;)V + public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;)V public synthetic fun arguments ()Ljava/lang/Iterable; public fun arguments ()Ljava/util/List; public final fun getKeys ()[Lorg/jetbrains/exposed/sql/Column; public final fun getOnUpdate ()Ljava/util/List; + public final fun getOnUpdateExclude ()Ljava/util/List; public final fun getWhere ()Lorg/jetbrains/exposed/sql/Op; public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; } @@ -3523,6 +3525,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider { public fun delete (ZLorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; public fun getDEFAULT_VALUE_EXPRESSION ()Ljava/lang/String; protected final fun getKeyColumnsForUpsert (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;)Ljava/util/List; + protected final fun getUpdateColumnsForUpsert (Ljava/util/List;Ljava/util/List;Ljava/util/List;)Ljava/util/List; public fun groupConcat (Lorg/jetbrains/exposed/sql/GroupConcat;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun hour (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun insert (ZLorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; @@ -3547,7 +3550,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider { public static synthetic fun substring$default (Lorg/jetbrains/exposed/sql/vendors/FunctionProvider;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/String;ILjava/lang/Object;)V public fun update (Lorg/jetbrains/exposed/sql/Join;Ljava/util/List;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; public fun update (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; - public fun upsert (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;[Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String; + public fun upsert (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;[Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String; public fun varPop (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun varSamp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun year (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt index cf37cc4c98..8b4a277d87 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt @@ -418,15 +418,18 @@ fun Join.update(where: (SqlExpressionBuilder.() -> Op)? = null, limit: * If no columns are provided, primary keys will be used. If the table does not have any primary keys, the first unique index will be attempted. * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. * If left null, all columns will be updated with the values provided for the insert. + * @param onUpdateExclude List of specific columns to exclude from updating. + * If left null, all columns will be updated with the values provided for the insert. * @param where Condition that determines which rows to update, if a unique violation is found. * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpsertTests.testUpsertWithUniqueIndexConflict */ fun T.upsert( vararg keys: Column<*>, onUpdate: List, Expression<*>>>? = null, + onUpdateExclude: List>? = null, where: (SqlExpressionBuilder.() -> Op)? = null, body: T.(UpsertStatement) -> Unit -) = UpsertStatement(this, *keys, onUpdate = onUpdate, where = where?.let { SqlExpressionBuilder.it() }).apply { +) = UpsertStatement(this, *keys, onUpdate = onUpdate, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }).apply { body(this) execute(TransactionManager.current()) } @@ -442,6 +445,8 @@ fun T.upsert( * primary keys will be used. If the table does not have any primary keys, the first unique index will be attempted. * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. * If left null, all columns will be updated with the values provided for the insert. + * @param onUpdateExclude List of specific columns to exclude from updating. + * If left null, all columns will be updated with the values provided for the insert. * @param shouldReturnGeneratedValues Specifies whether newly generated values (for example, auto-incremented IDs) should be returned. * See [Batch Insert](https://github.com/JetBrains/Exposed/wiki/DSL#batch-insert) for more details. * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpsertTests.testBatchUpsertWithNoConflict @@ -450,10 +455,11 @@ fun T.batchUpsert( data: Iterable, vararg keys: Column<*>, onUpdate: List, Expression<*>>>? = null, + onUpdateExclude: List>? = null, shouldReturnGeneratedValues: Boolean = true, body: BatchUpsertStatement.(E) -> Unit ): List { - return batchUpsert(data.iterator(), *keys, onUpdate = onUpdate, shouldReturnGeneratedValues = shouldReturnGeneratedValues, body = body) + return batchUpsert(data.iterator(), onUpdate, onUpdateExclude, shouldReturnGeneratedValues, keys = keys, body = body) } /** @@ -467,6 +473,8 @@ fun T.batchUpsert( * primary keys will be used. If the table does not have any primary keys, the first unique index will be attempted. * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. * If left null, all columns will be updated with the values provided for the insert. + * @param onUpdateExclude List of specific columns to exclude from updating. + * If left null, all columns will be updated with the values provided for the insert. * @param shouldReturnGeneratedValues Specifies whether newly generated values (for example, auto-incremented IDs) should be returned. * See [Batch Insert](https://github.com/JetBrains/Exposed/wiki/DSL#batch-insert) for more details. * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpsertTests.testBatchUpsertWithSequence @@ -475,10 +483,11 @@ fun T.batchUpsert( data: Sequence, vararg keys: Column<*>, onUpdate: List, Expression<*>>>? = null, + onUpdateExclude: List>? = null, shouldReturnGeneratedValues: Boolean = true, body: BatchUpsertStatement.(E) -> Unit ): List { - return batchUpsert(data.iterator(), *keys, onUpdate = onUpdate, shouldReturnGeneratedValues = shouldReturnGeneratedValues, body = body) + return batchUpsert(data.iterator(), onUpdate, onUpdateExclude, shouldReturnGeneratedValues, keys = keys, body = body) } /** @@ -492,18 +501,21 @@ fun T.batchUpsert( * primary keys will be used. If the table does not have any primary keys, the first unique index will be attempted. * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. * If left null, all columns will be updated with the values provided for the insert. + * @param onUpdateExclude List of specific columns to exclude from updating. + * If left null, all columns will be updated with the values provided for the insert. * @param shouldReturnGeneratedValues Specifies whether newly generated values (for example, auto-incremented IDs) should be returned. * See [Batch Insert](https://github.com/JetBrains/Exposed/wiki/DSL#batch-insert) for more details. * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpsertTests.testBatchUpsertWithNoConflict */ private fun T.batchUpsert( data: Iterator, - vararg keys: Column<*>, onUpdate: List, Expression<*>>>? = null, + onUpdateExclude: List>? = null, shouldReturnGeneratedValues: Boolean = true, + vararg keys: Column<*>, body: BatchUpsertStatement.(E) -> Unit ): List = executeBatch(data, body) { - BatchUpsertStatement(this, *keys, onUpdate = onUpdate, shouldReturnGeneratedValues = shouldReturnGeneratedValues) + BatchUpsertStatement(this, *keys, onUpdate = onUpdate, onUpdateExclude = onUpdateExclude, shouldReturnGeneratedValues = shouldReturnGeneratedValues) } /** diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt index 85c97ee11f..4d82570dab 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt @@ -19,6 +19,8 @@ import org.jetbrains.exposed.sql.vendors.MysqlFunctionProvider * primary keys will be used. If the table does not have any primary keys, the first unique index will be attempted. * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. * If left null, all columns will be updated with the values provided for the insert. + * @param onUpdateExclude List of specific columns to exclude from updating. + * If left null, all columns will be updated with the values provided for the insert. * @param shouldReturnGeneratedValues Specifies whether newly generated values (for example, auto-incremented IDs) should be returned. * See [Batch Insert](https://github.com/JetBrains/Exposed/wiki/DSL#batch-insert) for more details. */ @@ -26,6 +28,7 @@ open class BatchUpsertStatement( table: Table, vararg val keys: Column<*>, val onUpdate: List, Expression<*>>>?, + val onUpdateExclude: List>?, shouldReturnGeneratedValues: Boolean = true ) : BaseBatchInsertStatement(table, ignore = false, shouldReturnGeneratedValues) { @@ -37,6 +40,6 @@ open class BatchUpsertStatement( } else -> dialect.functionProvider } - return functionProvider.upsert(table, arguments!!.first(), onUpdate, null, transaction, keys = keys) + return functionProvider.upsert(table, arguments!!.first(), onUpdate, onUpdateExclude, null, transaction, keys = keys) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt index 20cf632830..92a28a068e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt @@ -11,12 +11,15 @@ import org.jetbrains.exposed.sql.vendors.* * primary keys will be used. If the table does not have any primary keys, the first unique index will be attempted. * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. * If left null, all columns will be updated with the values provided for the insert. + * @param onUpdateExclude List of specific columns to exclude from updating. + * If left null, all columns will be updated with the values provided for the insert. * @param where Condition that determines which rows to update, if a unique violation is found. This clause may not be supported by all vendors. */ open class UpsertStatement( table: Table, vararg val keys: Column<*>, val onUpdate: List, Expression<*>>>?, + val onUpdateExclude: List>?, val where: Op? ) : InsertStatement(table) { @@ -28,7 +31,7 @@ open class UpsertStatement( } else -> dialect.functionProvider } - return functionProvider.upsert(table, arguments!!.first(), onUpdate, where, transaction, keys = keys) + return functionProvider.upsert(table, arguments!!.first(), onUpdate, onUpdateExclude, where, transaction, keys = keys) } override fun arguments(): List>> { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt index 20c0853d99..c300394f9a 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt @@ -534,6 +534,7 @@ abstract class FunctionProvider { * @param table Table to either insert values into or update values from. * @param data Pairs of columns to use for insert or update and values to insert or update. * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. + * @param onUpdateExclude List of specific columns to exclude from updating. * @param where Condition that determines which rows to update, if a unique violation is found. * @param transaction Transaction where the operation is executed. */ @@ -541,6 +542,7 @@ abstract class FunctionProvider { table: Table, data: List, Any?>>, onUpdate: List, Expression<*>>>?, + onUpdateExclude: List>?, where: Op?, transaction: Transaction, vararg keys: Column<*> @@ -557,7 +559,7 @@ abstract class FunctionProvider { val autoIncColumn = table.autoIncColumn val nextValExpression = autoIncColumn?.autoIncColumnType?.nextValExpression val dataColumnsWithoutAutoInc = autoIncColumn?.let { dataColumns - autoIncColumn } ?: dataColumns - val updateColumns = dataColumns.filter { it !in keyColumns }.ifEmpty { dataColumns } + val updateColumns = getUpdateColumnsForUpsert(dataColumns, onUpdateExclude, keyColumns) return with(QueryBuilder(true)) { +"MERGE INTO " @@ -606,6 +608,18 @@ abstract class FunctionProvider { } } + /** Returns the columns to be used in the update clause of an upsert statement. */ + protected fun getUpdateColumnsForUpsert( + dataColumns: List>, + toExclude: List>?, + keyColumns: List>? + ): List> { + val updateColumns = toExclude?.let { dataColumns - it.toSet() } ?: dataColumns + return keyColumns?.let { keys -> + updateColumns.filter { it !in keys }.ifEmpty { updateColumns } + } ?: updateColumns + } + /** * Appends the complete default SQL insert (no ignore) command to [this] QueryBuilder. */ diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt index e9cd377c46..b018811f26 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt @@ -230,6 +230,7 @@ internal open class MysqlFunctionProvider : FunctionProvider() { table: Table, data: List, Any?>>, onUpdate: List, Expression<*>>>?, + onUpdateExclude: List>?, where: Op?, transaction: Transaction, vararg keys: Column<*> @@ -256,7 +257,8 @@ internal open class MysqlFunctionProvider : FunctionProvider() { onUpdate?.appendTo { (columnToUpdate, updateExpression) -> append("${transaction.identity(columnToUpdate)}=$updateExpression") } ?: run { - data.unzip().first.appendTo { column -> + val updateColumns = getUpdateColumnsForUpsert(data.unzip().first, onUpdateExclude, null) + updateColumns.appendTo { column -> val columnName = transaction.identity(column) if (isAliasSupported) { append("$columnName=NEW.$columnName") diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt index cf2c3f313f..5f56a4e33e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt @@ -266,11 +266,12 @@ internal object OracleFunctionProvider : FunctionProvider() { table: Table, data: List, Any?>>, onUpdate: List, Expression<*>>>?, + onUpdateExclude: List>?, where: Op?, transaction: Transaction, vararg keys: Column<*> ): String { - val statement = super.upsert(table, data, onUpdate, where, transaction, *keys) + val statement = super.upsert(table, data, onUpdate, onUpdateExclude, where, transaction, *keys) val dualTable = data.appendTo(QueryBuilder(true), prefix = "(SELECT ", postfix = " FROM DUAL) S") { (column, value) -> registerArgument(column, value) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index 0352e8c8e4..bee5325120 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -262,6 +262,7 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { table: Table, data: List, Any?>>, onUpdate: List, Expression<*>>>?, + onUpdateExclude: List>?, where: Op?, transaction: Transaction, vararg keys: Column<*> @@ -271,8 +272,7 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { transaction.throwUnsupportedException("UPSERT requires a unique key or constraint as a conflict target") } - val dataColumns = data.unzip().first - val updateColumns = dataColumns.filter { it !in keyColumns }.ifEmpty { dataColumns } + val updateColumns = getUpdateColumnsForUpsert(data.unzip().first, onUpdateExclude, keyColumns) return with(QueryBuilder(true)) { appendInsertToUpsertClause(table, data, transaction) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index fb6e4b5d37..6cc09d52bc 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -216,12 +216,13 @@ internal object SQLServerFunctionProvider : FunctionProvider() { table: Table, data: List, Any?>>, onUpdate: List, Expression<*>>>?, + onUpdateExclude: List>?, where: Op?, transaction: Transaction, vararg keys: Column<*> ): String { // SQLSERVER MERGE statement must be terminated by a semi-colon (;) - return super.upsert(table, data, onUpdate, where, transaction, *keys) + ";" + return super.upsert(table, data, onUpdate, onUpdateExclude, where, transaction, *keys) + ";" } override fun delete(ignore: Boolean, table: Table, where: String?, limit: Int?, transaction: Transaction): String { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt index 90d16627cb..e244b7d883 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt @@ -201,6 +201,7 @@ internal object SQLiteFunctionProvider : FunctionProvider() { table: Table, data: List, Any?>>, onUpdate: List, Expression<*>>>?, + onUpdateExclude: List>?, where: Op?, transaction: Transaction, vararg keys: Column<*> @@ -216,8 +217,7 @@ internal object SQLiteFunctionProvider : FunctionProvider() { } +" DO" - val dataColumns = data.unzip().first - val updateColumns = dataColumns.filter { it !in keyColumns }.ifEmpty { dataColumns } + val updateColumns = getUpdateColumnsForUpsert(data.unzip().first, onUpdateExclude, keyColumns) appendUpdateToUpsertClause(table, updateColumns, onUpdate, transaction, isAliasNeeded = false) where?.let { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt index fe1047f24e..abd30c14c5 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt @@ -14,9 +14,11 @@ import org.jetbrains.exposed.sql.statements.BatchUpsertStatement import org.jetbrains.exposed.sql.tests.* import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.expectException +import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Test import java.util.* import kotlin.properties.Delegates +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull // Upsert implementation does not support H2 version 1 @@ -344,6 +346,66 @@ class UpsertTests : DatabaseTestsBase() { } } + @Test + fun testUpsertWithUpdateExcludingColumns() { + val tester = object : Table("tester") { + val item = varchar("item", 64).uniqueIndex() + val code = uuid("code").clientDefault { UUID.randomUUID() } + val gains = integer("gains") + val losses = integer("losses") + } + + withTables(tester) { testDb -> + excludingH2Version1(testDb) { + db.useNestedTransactions = true + + val itemA = "Item A" + tester.upsert { + it[item] = itemA + it[gains] = 50 + it[losses] = 50 + } + + val (insertCode, insertGains, insertLosses) = tester.selectAll().single().let { + Triple(it[tester.code], it[tester.gains], it[tester.losses]) + } + + transaction { + // all fields get updated by default, including columns with default values + tester.upsert { + it[item] = itemA + it[gains] = 200 + it[losses] = 0 + } + + val (updateCode, updateGains, updateLosses) = tester.selectAll().single().let { + Triple(it[tester.code], it[tester.gains], it[tester.losses]) + } + assertNotEquals(insertCode, updateCode) + assertNotEquals(insertGains, updateGains) + assertNotEquals(insertLosses, updateLosses) + + rollback() + } + + tester.upsert(onUpdateExclude = listOf(tester.code, tester.gains)) { + it[item] = itemA + it[gains] = 200 + it[losses] = 0 + } + + val (updateCode, updateGains, updateLosses) = tester.selectAll().single().let { + Triple(it[tester.code], it[tester.gains], it[tester.losses]) + } + assertEquals(insertCode, updateCode) + assertEquals(insertGains, updateGains) + assertNotEquals(insertLosses, updateLosses) + + db.useNestedTransactions = false + } + } + } + @Test fun testUpsertWithWhere() { val tester = object : IntIdTable("tester") {