From 8726aa890a4ffddb0f59fde9d2eb07d0a71841b4 Mon Sep 17 00:00:00 2001 From: bog-walk <82039410+bog-walk@users.noreply.github.com> Date: Sat, 17 Aug 2024 22:02:45 -0400 Subject: [PATCH] feat!: EXPOSED-436 Allow using insert values on update with upsert() (#2172) * feat: EXPOSED-436 Allow using insert values on update with upsert() onUpdate parameter of upsert() (and batchUpsert()) currently accepts a list of columns to include in the UPDATE clause and the values/expressions to use. It is not possible to refer to the values that would have been inserted had there been no conflict. The onUpdate parameter now accepts a lambda with an UpsertStatement as its receiver, so that it has access to new expression `asForInsert()`. This makes it possible to reference these values using database-specific syntax like EXCLUDE, VALUES(), and alias identifier notation. Additional: - To limit duplication across UpsertStatement and BatchUpsertStatement, a new interface, UpsertBuilder, is included to house common logic. This removes business logic from FunctionProvider for everything up to statement preparation. * feat!: EXPOSED-436 Allow using insert values on update with upsert() Deprecate existing upsert functions and replace with variants that do not have onUpdate parameter. This functionality is now accomplished using onUpdate() in the upsert lambda directly, which utilizes UpdateStatement to set column-value assignments. Change asForInsert() to accept arguments instead of column receivers and rename to insertValue(). Change visibility modifier of some interface functions to internal. Update documentation with new upsert builder construct --- .../Writerside/topics/Deep-Dive-into-DSL.md | 29 ++++- exposed-core/api/exposed-core.api | 39 ++++-- .../org/jetbrains/exposed/sql/Queries.kt | 111 ++++++++++++++--- .../sql/statements/BatchUpsertStatement.kt | 26 ++-- .../exposed/sql/statements/UpsertStatement.kt | 103 ++++++++++++++-- .../exposed/sql/vendors/FunctionProvider.kt | 93 +++----------- .../exposed/sql/vendors/MysqlDialect.kt | 46 +++---- .../exposed/sql/vendors/OracleDialect.kt | 10 +- .../exposed/sql/vendors/PostgreSQL.kt | 23 ++-- .../exposed/sql/vendors/SQLServerDialect.kt | 10 +- .../exposed/sql/vendors/SQLiteDialect.kt | 20 ++-- .../sql/tests/shared/dml/ReturningTests.kt | 7 +- .../sql/tests/shared/dml/UpsertTests.kt | 113 +++++++++++++++--- 13 files changed, 433 insertions(+), 197 deletions(-) diff --git a/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md b/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md index cea829ecfb..6277356f3b 100644 --- a/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md +++ b/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md @@ -671,23 +671,42 @@ StarWarsFilms.upsert { } ``` -If none of the optional arguments are provided to `upsert()`, the statements in the `body` block will be used for both the insert and update parts of the operation. +If none of the optional arguments are provided to `upsert()`, and an `onUpdate()` block is omitted, the statements in the `body` block will be used for both the insert and update parts of the operation. This means that, for example, if a table mapping has columns with default values and these columns are omitted from the `body` block, the default values will be -used for insertion as well as for the update operation. If the update operation should differ from the insert operation, then `onUpdate` should be provided an -argument with the specific columns to update, as seen in the example below. +used for insertion as well as for the update operation. + + +If the update operation should differ from the insert operation, then onUpdate() should be used in the lambda block to set +the specific columns to update, as seen in the example below. + +If the update operation involves functions that should use the values that would have been inserted, then these columns +should be marked using `insertValue()`, as seen in the example below. + Using another example, PostgreSQL allows more control over which key constraint columns to check for conflict, whether different values should be used for an update, and whether the update statement should have a `WHERE` clause: ```kotlin -val incrementSequelId = listOf(StarWarsFilms.sequelId to StarWarsFilms.sequelId.plus(1)) StarWarsFilms.upsert( StarWarsFilms.sequelId, - onUpdate = incrementSequelId, where = { StarWarsFilms.director like stringLiteral("JJ%") } ) { it[sequelId] = 9 it[name] = "The Rise of Skywalker" it[director] = "JJ Abrams" + + it.onUpdate { update -> + update[sequelId] = sequelId + 1 + } +} + +StarWarsFilms.upsert { + it[sequelId] = 9 + it[name] = "The Rise of Skywalker" + it[director] = "Rian Johnson" + + it.onUpdate { update -> + update[director] = concat(insertValue(StarWarsFilms.director), stringLiteral(" || "), StarWarsFilms.director) + } } ``` If the update operation should be identical to the insert operation except for a few columns, diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index fa0938895b..e7661c15ea 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -1792,9 +1792,13 @@ public final class org/jetbrains/exposed/sql/QueriesKt { 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;Ljava/util/List;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static final fun batchUpsert (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;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;Lkotlin/jvm/functions/Function1;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;Lkotlin/jvm/functions/Function1;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;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)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;Lkotlin/jvm/functions/Function1;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;Lkotlin/jvm/functions/Function1;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;Lkotlin/jvm/functions/Function1;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 @@ -1839,9 +1843,13 @@ public final class org/jetbrains/exposed/sql/QueriesKt { public static final fun updateReturning (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; public static synthetic fun updateReturning$default (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; 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 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;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)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 upsertReturning (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; + public static final fun upsertReturning (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/ReturningStatement; public static synthetic fun upsertReturning$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; + public static synthetic fun upsertReturning$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/ReturningStatement; } public class org/jetbrains/exposed/sql/Query : org/jetbrains/exposed/sql/AbstractQuery { @@ -3054,7 +3062,7 @@ public class org/jetbrains/exposed/sql/statements/BatchUpdateStatement : org/jet public synthetic fun update (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Expression;)V } -public class org/jetbrains/exposed/sql/statements/BatchUpsertStatement : org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement { +public class org/jetbrains/exposed/sql/statements/BatchUpsertStatement : org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement, org/jetbrains/exposed/sql/statements/UpsertBuilder { public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;Z)V public synthetic fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun arguments ()Ljava/lang/Iterable; @@ -3063,7 +3071,9 @@ public class org/jetbrains/exposed/sql/statements/BatchUpsertStatement : org/jet 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 insertValue (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; protected fun isColumnValuePreferredFromResultSet (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)Z + public fun onUpdate (Lkotlin/jvm/functions/Function2;)V public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; public fun prepared (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; } @@ -3379,15 +3389,33 @@ public class org/jetbrains/exposed/sql/statements/UpdateStatement : org/jetbrain public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; } -public class org/jetbrains/exposed/sql/statements/UpsertStatement : org/jetbrains/exposed/sql/statements/InsertStatement { +public abstract interface class org/jetbrains/exposed/sql/statements/UpsertBuilder { + public static final field Companion Lorg/jetbrains/exposed/sql/statements/UpsertBuilder$Companion; + public abstract fun insertValue (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public abstract fun onUpdate (Lkotlin/jvm/functions/Function2;)V +} + +public final class org/jetbrains/exposed/sql/statements/UpsertBuilder$Companion { + public final fun getFunctionProvider (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; +} + +public final class org/jetbrains/exposed/sql/statements/UpsertBuilder$DefaultImpls { + public static fun insertValue (Lorg/jetbrains/exposed/sql/statements/UpsertBuilder;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static fun onUpdate (Lorg/jetbrains/exposed/sql/statements/UpsertBuilder;Lkotlin/jvm/functions/Function2;)V +} + +public class org/jetbrains/exposed/sql/statements/UpsertStatement : org/jetbrains/exposed/sql/statements/InsertStatement, org/jetbrains/exposed/sql/statements/UpsertBuilder { 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 (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;ILkotlin/jvm/internal/DefaultConstructorMarker;)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 insertValue (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; protected fun isColumnValuePreferredFromResultSet (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)Z + public fun onUpdate (Lkotlin/jvm/functions/Function2;)V public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; public fun prepared (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; } @@ -3894,10 +3922,8 @@ public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider { public fun ()V - protected final fun appendInsertToUpsertClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;)V protected final fun appendJoinPartForUpdateClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Join;Lorg/jetbrains/exposed/sql/Transaction;)V protected fun appendOptionsToExplain (Ljava/lang/StringBuilder;Ljava/lang/String;)V - protected final fun appendUpdateToUpsertClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;Z)V public fun arraySlice (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/Integer;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun cast (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun charLength (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V @@ -3907,11 +3933,10 @@ 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 explain (ZLjava/lang/String;Ljava/lang/String;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; + public fun insertValue (Ljava/lang/String;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun jsonContains (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun jsonExists (Lorg/jetbrains/exposed/sql/Expression;[Ljava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun jsonExtract (Lorg/jetbrains/exposed/sql/Expression;[Ljava/lang/String;ZLorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V @@ -3937,7 +3962,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider { public fun time (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)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;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/lang/String;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;)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 be5ba406b8..0bdc006c53 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 @@ -476,6 +476,11 @@ fun T.updateReturning( /** * Represents the SQL statement that either inserts a new row into a table, or updates the existing row if insertion would violate a unique constraint. * + * To set specific columns with values on update that differ from the values that would be inserted, use `UpsertStatement.onUpdate()` + * within the [body] lambda block. If `onUpdate()` is omitted, all columns will be updated with the values provided for the insert. + * To specify manually that the insert value should be used when setting an update, for example in a function or expression, + * invoke `insertValue()` with the desired column as the function argument. + * * **Note:** Vendors that do not support this operation directly implement the standard MERGE USING command. * * **Note:** Currently, the `upsert()` function might return an incorrect auto-generated ID (such as a UUID) if it performs an update. @@ -484,8 +489,6 @@ fun T.updateReturning( * * @param keys (optional) Columns to include in the condition that determines a unique constraint match. * 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. @@ -493,24 +496,45 @@ fun T.updateReturning( */ 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, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }).apply { +) = UpsertStatement(this, keys = keys, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }).apply { body(this) execute(TransactionManager.current()) } +@Deprecated( + "This `upsert()` with `onUpdate` parameter will be removed in future releases. " + + "Please use function `UpsertStatement.onUpdate()` in `body` lambda block instead.", + level = DeprecationLevel.WARNING +) +fun T.upsert( + vararg keys: Column<*>, + onUpdate: List, Expression<*>>>? = null, + onUpdateExclude: List>? = null, + where: (SqlExpressionBuilder.() -> Op)? = null, + body: T.(UpsertStatement) -> Unit +): UpsertStatement { + val upsert = UpsertStatement(this, keys = keys, null, onUpdateExclude, where?.let { SqlExpressionBuilder.it() }) + onUpdate?.let { upsert.updateValues.putAll(it) } + body(upsert) + upsert.execute(TransactionManager.current()) + return upsert +} + /** * Represents the SQL statement that either inserts a new row into a table, or updates the existing row if insertion would * violate a unique constraint, and also returns specified data from the modified rows. * + * To set specific columns with values on update that differ from the values that would be inserted, use `UpsertStatement.onUpdate()` + * within the [body] lambda block. If `onUpdate()` is omitted, all columns will be updated with the values provided for the insert. + * To specify manually that the insert value should be used when setting an update, for example in a function or expression, + * invoke `insertValue()` with the desired column as the function argument. + * * @param keys (optional) Columns to include in the condition that determines a unique constraint match. 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 returning Columns and expressions to include in the returned data. This defaults to all columns in the table. - * @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. @@ -518,6 +542,23 @@ fun T.upsert( * expressions mapped to their resulting data. * @sample org.jetbrains.exposed.sql.tests.shared.dml.ReturningTests.testUpsertReturning */ +fun T.upsertReturning( + vararg keys: Column<*>, + returning: List> = columns, + onUpdateExclude: List>? = null, + where: (SqlExpressionBuilder.() -> Op)? = null, + body: T.(UpsertStatement) -> Unit +): ReturningStatement { + val upsert = UpsertStatement(this, keys = keys, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }) + body(upsert) + return ReturningStatement(this, returning, upsert) +} + +@Deprecated( + "This `upsertReturning()` with `onUpdate` parameter will be removed in future releases. " + + "Please use function `UpsertStatement.onUpdate()` in `body` lambda block instead.", + level = DeprecationLevel.WARNING +) fun T.upsertReturning( vararg keys: Column<*>, returning: List> = columns, @@ -526,19 +567,23 @@ fun T.upsertReturning( where: (SqlExpressionBuilder.() -> Op)? = null, body: T.(UpsertStatement) -> Unit ): ReturningStatement { - val update = UpsertStatement(this, *keys, onUpdate = onUpdate, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }) - body(update) - return ReturningStatement(this, returning, update) + val upsert = UpsertStatement(this, keys = keys, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }) + onUpdate?.let { upsert.updateValues.putAll(it) } + body(upsert) + return ReturningStatement(this, returning, upsert) } /** * Represents the SQL statement that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. * + * To set specific columns with values on update that differ from the values that would be inserted, use `UpsertStatement.onUpdate()` + * within the [body] lambda block. If `onUpdate()` is omitted, all columns will be updated with the values provided for the insert. + * To specify manually that the insert value should be used when setting an update, for example in a function or expression, + * invoke `insertValue()` with the desired column as the function argument. + * * @param data Collection of values to use in batch upsert. * @param keys (optional) Columns to include in the condition that determines a unique constraint match. 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. @@ -546,6 +591,22 @@ fun T.upsertReturning( * 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 */ +fun T.batchUpsert( + data: Iterable, + vararg keys: Column<*>, + onUpdateExclude: List>? = null, + where: (SqlExpressionBuilder.() -> Op)? = null, + shouldReturnGeneratedValues: Boolean = true, + body: BatchUpsertStatement.(E) -> Unit +): List { + return batchUpsert(data.iterator(), null, onUpdateExclude, where, shouldReturnGeneratedValues, keys = keys, body = body) +} + +@Deprecated( + "This `batchUpsert()` with `onUpdate` parameter will be removed in future releases. " + + "Please use function `onUpdate()` in `body` lambda block instead.", + level = DeprecationLevel.WARNING +) fun T.batchUpsert( data: Iterable, vararg keys: Column<*>, @@ -561,11 +622,14 @@ fun T.batchUpsert( /** * Represents the SQL statement that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. * + * To set specific columns with values on update that differ from the values that would be inserted, use `UpsertStatement.onUpdate()` + * within the [body] lambda block. If `onUpdate()` is omitted, all columns will be updated with the values provided for the insert. + * To specify manually that the insert value should be used when setting an update, for example in a function or expression, + * invoke `insertValue()` with the desired column as the function argument. + * * @param data Sequence of values to use in batch upsert. * @param keys (optional) Columns to include in the condition that determines a unique constraint match. 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. @@ -573,6 +637,22 @@ fun T.batchUpsert( * 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 */ +fun T.batchUpsert( + data: Sequence, + vararg keys: Column<*>, + onUpdateExclude: List>? = null, + where: (SqlExpressionBuilder.() -> Op)? = null, + shouldReturnGeneratedValues: Boolean = true, + body: BatchUpsertStatement.(E) -> Unit +): List { + return batchUpsert(data.iterator(), null, onUpdateExclude, where, shouldReturnGeneratedValues, keys = keys, body = body) +} + +@Deprecated( + "This `batchUpsert()` with `onUpdate` parameter will be removed in future releases. " + + "Please use function `onUpdate()` in `body` lambda block instead.", + level = DeprecationLevel.WARNING +) fun T.batchUpsert( data: Sequence, vararg keys: Column<*>, @@ -597,11 +677,12 @@ private fun T.batchUpsert( BatchUpsertStatement( this, *keys, - onUpdate = onUpdate, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }, shouldReturnGeneratedValues = shouldReturnGeneratedValues - ) + ).apply { + onUpdate?.let { updateValues.putAll(it) } + } } /** 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 05f2b37106..15bb77553b 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 @@ -2,8 +2,6 @@ package org.jetbrains.exposed.sql.statements import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi -import org.jetbrains.exposed.sql.vendors.H2Dialect -import org.jetbrains.exposed.sql.vendors.H2FunctionProvider import org.jetbrains.exposed.sql.vendors.MysqlFunctionProvider import org.jetbrains.exposed.sql.vendors.currentDialect @@ -13,8 +11,6 @@ import org.jetbrains.exposed.sql.vendors.currentDialect * @param table Table to either insert values into or update values from. * @param keys (optional) Columns to include in the condition that determines a unique constraint match. 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. This clause may not be supported by all vendors. @@ -24,21 +20,23 @@ import org.jetbrains.exposed.sql.vendors.currentDialect open class BatchUpsertStatement( table: Table, vararg val keys: Column<*>, - val onUpdate: List, Expression<*>>>?, + @Deprecated("This property will be removed in future releases. Use function `onUpdate()` instead.", level = DeprecationLevel.WARNING) + val onUpdate: List, Expression<*>>>? = null, val onUpdateExclude: List>?, val where: Op?, shouldReturnGeneratedValues: Boolean = true -) : BaseBatchInsertStatement(table, ignore = false, shouldReturnGeneratedValues) { +) : BaseBatchInsertStatement(table, ignore = false, shouldReturnGeneratedValues), UpsertBuilder { + internal val updateValues: MutableMap, Any?> = LinkedHashMap() override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { - val functionProvider = when (val dialect = transaction.db.dialect) { - is H2Dialect -> when (dialect.h2Mode) { - H2Dialect.H2CompatibilityMode.MariaDB, H2Dialect.H2CompatibilityMode.MySQL -> MysqlFunctionProvider() - else -> H2FunctionProvider - } - else -> dialect.functionProvider - } - return functionProvider.upsert(table, arguments!!.first(), onUpdate, onUpdateExclude, where, transaction, keys = keys) + val dialect = transaction.db.dialect + val functionProvider = UpsertBuilder.getFunctionProvider(dialect) + val keyColumns = if (functionProvider is MysqlFunctionProvider) keys.toList() else getKeyColumns(keys = keys) + val insertValues = arguments!!.first() + val insertValuesSql = insertValues.toSqlString(prepared) + val updateExpressions = updateValues.takeIf { it.isNotEmpty() }?.toList() + ?: getUpdateExpressions(insertValues.unzip().first, onUpdateExclude, keyColumns) + return functionProvider.upsert(table, insertValues, insertValuesSql, updateExpressions, keyColumns, where, transaction) } override fun arguments(): List, Any?>>> { 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 c9c2b5505d..9bb97701e8 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 @@ -2,6 +2,9 @@ package org.jetbrains.exposed.sql.statements import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.vendors.DatabaseDialect +import org.jetbrains.exposed.sql.vendors.FunctionProvider import org.jetbrains.exposed.sql.vendors.H2Dialect import org.jetbrains.exposed.sql.vendors.H2FunctionProvider import org.jetbrains.exposed.sql.vendors.MysqlFunctionProvider @@ -13,8 +16,6 @@ import org.jetbrains.exposed.sql.vendors.currentDialect * @param table Table to either insert values into or update values from. * @param keys (optional) Columns to include in the condition that determines a unique constraint match. 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. This clause may not be supported by all vendors. @@ -22,20 +23,22 @@ import org.jetbrains.exposed.sql.vendors.currentDialect open class UpsertStatement( table: Table, vararg val keys: Column<*>, - val onUpdate: List, Expression<*>>>?, + @Deprecated("This property will be removed in future releases. Use function `onUpdate()` instead.", level = DeprecationLevel.WARNING) + val onUpdate: List, Expression<*>>>? = null, val onUpdateExclude: List>?, val where: Op? -) : InsertStatement(table) { +) : InsertStatement(table), UpsertBuilder { + internal val updateValues: MutableMap, Any?> = LinkedHashMap() override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { - val functionProvider = when (val dialect = transaction.db.dialect) { - is H2Dialect -> when (dialect.h2Mode) { - H2Dialect.H2CompatibilityMode.MariaDB, H2Dialect.H2CompatibilityMode.MySQL -> MysqlFunctionProvider() - else -> H2FunctionProvider - } - else -> dialect.functionProvider - } - return functionProvider.upsert(table, arguments!!.first(), onUpdate, onUpdateExclude, where, transaction, keys = keys) + val dialect = transaction.db.dialect + val functionProvider = UpsertBuilder.getFunctionProvider(dialect) + val keyColumns = if (functionProvider is MysqlFunctionProvider) keys.toList() else getKeyColumns(keys = keys) + val insertValues = arguments!!.first() + val insertValuesSql = insertValues.toSqlString(prepared) + val updateExpressions = updateValues.takeIf { it.isNotEmpty() }?.toList() + ?: getUpdateExpressions(insertValues.unzip().first, onUpdateExclude, keyColumns) + return functionProvider.upsert(table, insertValues, insertValuesSql, updateExpressions, keyColumns, where, transaction) } override fun arguments(): List, Any?>>> { @@ -61,3 +64,79 @@ open class UpsertStatement( return super.prepared(transaction, sql) } } + +/** + * Common interface for building SQL statements that either insert a new row into a table, + * or update the existing row if insertion would violate a unique constraint. + */ +sealed interface UpsertBuilder { + /** + * Calls the specified function [body] with an [UpsertBuilder] as its receiver and an [UpdateStatement] + * as its argument, allowing values to be assigned to the UPDATE clause of an upsert statement. + * + * To specify manually that the insert value should be used when updating a column, for example within an expression + * or function, invoke `insertValue()` with the desired column as the function argument. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpsertTests.testUpsertWithManualUpdateUsingInsertValues + */ + fun onUpdate(body: UpsertBuilder.(UpdateStatement) -> Unit) { + val arguments = UpdateStatement((this as InsertStatement<*>).table, null).apply { + body.invoke(this@UpsertBuilder, this) + }.firstDataSet + when (this) { + is UpsertStatement<*> -> updateValues.putAll(arguments) + is BatchUpsertStatement -> updateValues.putAll(arguments) + } + } + + /** + * Specifies that this column should be updated using the same values that would be inserted if there was + * no violation of a unique constraint in an upsert statement. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpsertTests.testUpsertWithManualUpdateUsingInsertValues + */ + fun insertValue(column: Column): ExpressionWithColumnType = InsertValue(column, column.columnType) + + private class InsertValue( + val column: Column, + override val columnType: IColumnType + ) : ExpressionWithColumnType() { + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + val transaction = TransactionManager.current() + val functionProvider = getFunctionProvider(transaction.db.dialect) + functionProvider.insertValue(transaction.identity(column), queryBuilder) + } + } + + companion object { + /** Returns the [FunctionProvider] for valid upsert statement syntax. */ + fun getFunctionProvider(dialect: DatabaseDialect): FunctionProvider = when (dialect) { + is H2Dialect -> when (dialect.h2Mode) { + H2Dialect.H2CompatibilityMode.MariaDB, H2Dialect.H2CompatibilityMode.MySQL -> MysqlFunctionProvider.INSTANCE + else -> H2FunctionProvider + } + else -> dialect.functionProvider + } + } +} + +/** Returns the columns to be used in the conflict condition of an upsert statement. */ +internal fun UpsertBuilder.getKeyColumns(vararg keys: Column<*>): List> { + this as InsertStatement<*> + return keys.toList().ifEmpty { + table.primaryKey?.columns?.toList() ?: table.indices.firstOrNull { it.unique }?.columns + } ?: emptyList() +} + +/** Returns the expressions to be used in the update clause of an upsert statement, along with their insert column reference. */ +internal fun UpsertBuilder.getUpdateExpressions( + dataColumns: List>, + toExclude: List>?, + keyColumns: List>? +): List, Any?>> { + val updateColumns = toExclude?.let { dataColumns - it } ?: dataColumns + val updateColumnsWithoutKeys = keyColumns?.let { keys -> + updateColumns.filter { it !in keys }.ifEmpty { updateColumns } + } ?: updateColumns + return updateColumnsWithoutKeys.zip(updateColumnsWithoutKeys.map { insertValue(it) }) +} 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 9d5dcde2dd..d1739e3520 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 @@ -654,25 +654,25 @@ 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 expression Expression with the values to use in the insert clause. * @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 keyColumns Columns to include in the condition that determines a unique constraint match. * @param where Condition that determines which rows to update, if a unique violation is found. * @param transaction Transaction where the operation is executed. */ open fun upsert( table: Table, data: List, Any?>>, - onUpdate: List, Expression<*>>>?, - onUpdateExclude: List>?, + expression: String, + onUpdate: List, Any?>>, + keyColumns: List>, where: Op?, - transaction: Transaction, - vararg keys: Column<*> + transaction: Transaction ): String { if (where != null) { transaction.throwUnsupportedException("MERGE implementation of UPSERT doesn't support single WHERE clause") } - val keyColumns = getKeyColumnsForUpsert(table, *keys) - if (keyColumns.isNullOrEmpty()) { + if (keyColumns.isEmpty()) { transaction.throwUnsupportedException("UPSERT requires a unique key or constraint as a conflict target") } @@ -680,7 +680,7 @@ abstract class FunctionProvider { val autoIncColumn = table.autoIncColumn val nextValExpression = autoIncColumn?.autoIncColumnType?.nextValExpression val dataColumnsWithoutAutoInc = autoIncColumn?.let { dataColumns - autoIncColumn } ?: dataColumns - val updateColumns = getUpdateColumnsForUpsert(dataColumns, onUpdateExclude, keyColumns) + val tableIdentifier = transaction.identity(table) return with(QueryBuilder(true)) { +"MERGE INTO " @@ -699,8 +699,11 @@ abstract class FunctionProvider { append("T.$columnName=S.$columnName") } - +" WHEN MATCHED THEN" - appendUpdateToUpsertClause(table, updateColumns, onUpdate, transaction, isAliasNeeded = true) + +" WHEN MATCHED THEN UPDATE SET " + onUpdate.appendTo { (columnToUpdate, updateExpression) -> + val aliasExpression = updateExpression.toString().replace(tableIdentifier, "T") + append("T.${transaction.identity(columnToUpdate)}=$aliasExpression") + } +" WHEN NOT MATCHED THEN INSERT " dataColumnsWithoutAutoInc.appendTo(prefix = "(") { column -> @@ -721,71 +724,13 @@ abstract class FunctionProvider { } /** - * Returns the columns to be used in the conflict condition of an upsert statement. - */ - protected fun getKeyColumnsForUpsert(table: Table, vararg keys: Column<*>): List>? { - return keys.toList().ifEmpty { - table.primaryKey?.columns?.toList() ?: table.indices.firstOrNull { it.unique }?.columns - } - } - - /** 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. - */ - protected fun QueryBuilder.appendInsertToUpsertClause(table: Table, data: List, Any?>>, transaction: Transaction) { - val valuesSql = if (data.isEmpty()) { - "" - } else { - data.appendTo(QueryBuilder(true), prefix = "VALUES (", postfix = ")") { (column, value) -> - registerArgument(column, value) - }.toString() - } - val insertStatement = insert(false, table, data.unzip().first, valuesSql, transaction) - - +insertStatement - } - - /** - * Appends an SQL update command for a derived table (with or without alias identifiers) to [this] QueryBuilder. + * Appends to a [queryBuilder] the SQL syntax for a column that represents the same values from the INSERT clause + * of an [upsert] command, which should be used in the UPDATE clause. + * + * @param columnName Name of the column for update. + * @param queryBuilder Query builder to append the SQL syntax to. */ - protected fun QueryBuilder.appendUpdateToUpsertClause( - table: Table, - updateColumns: List>, - onUpdate: List, Expression<*>>>?, - transaction: Transaction, - isAliasNeeded: Boolean - ) { - +" UPDATE SET " - onUpdate?.appendTo { (columnToUpdate, updateExpression) -> - if (isAliasNeeded) { - val aliasExpression = updateExpression.toString().replace(transaction.identity(table), "T") - append("T.${transaction.identity(columnToUpdate)}=$aliasExpression") - } else { - append("${transaction.identity(columnToUpdate)}=$updateExpression") - } - } ?: run { - updateColumns.appendTo { column -> - val columnName = transaction.identity(column) - if (isAliasNeeded) { - append("T.$columnName=S.$columnName") - } else { - append("$columnName=EXCLUDED.$columnName") - } - } - } - } + open fun insertValue(columnName: String, queryBuilder: QueryBuilder) { queryBuilder { +"S.$columnName" } } /** * Returns the SQL command that deletes one or more rows of a table. 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 e288fef697..bf7d70aa5d 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 @@ -239,48 +239,48 @@ internal open class MysqlFunctionProvider : FunctionProvider() { override fun upsert( table: Table, data: List, Any?>>, - onUpdate: List, Expression<*>>>?, - onUpdateExclude: List>?, + expression: String, + onUpdate: List, Any?>>, + keyColumns: List>, where: Op?, - transaction: Transaction, - vararg keys: Column<*> + transaction: Transaction ): String { - if (keys.isNotEmpty()) { + if (keyColumns.isNotEmpty()) { transaction.throwUnsupportedException("MySQL doesn't support specifying conflict keys in UPSERT clause") } if (where != null) { transaction.throwUnsupportedException("MySQL doesn't support WHERE in UPSERT clause") } - val isAliasSupported = when (val dialect = transaction.db.dialect) { - is MysqlDialect -> dialect !is MariaDBDialect && dialect.fullVersion >= "8.0.19" - else -> false // H2_MySQL mode also uses this function provider & requires older version - } - return with(QueryBuilder(true)) { - appendInsertToUpsertClause(table, data, transaction) - if (isAliasSupported) { + +insert(false, table, data.unzip().first, expression, transaction) + if (isUpsertAliasSupported(transaction.db.dialect)) { +" AS NEW" } +" ON DUPLICATE KEY UPDATE " - onUpdate?.appendTo { (columnToUpdate, updateExpression) -> + onUpdate.appendTo { (columnToUpdate, updateExpression) -> append("${transaction.identity(columnToUpdate)}=$updateExpression") - } ?: run { - val updateColumns = getUpdateColumnsForUpsert(data.unzip().first, onUpdateExclude, null) - updateColumns.appendTo { column -> - val columnName = transaction.identity(column) - if (isAliasSupported) { - append("$columnName=NEW.$columnName") - } else { - append("$columnName=VALUES($columnName)") - } - } } toString() } } + override fun insertValue(columnName: String, queryBuilder: QueryBuilder) { + queryBuilder { + if (isUpsertAliasSupported(currentDialect)) { + +"NEW.$columnName" + } else { + +"VALUES($columnName)" + } + } + } + + private fun isUpsertAliasSupported(dialect: DatabaseDialect): Boolean = when (dialect) { + is MysqlDialect -> dialect !is MariaDBDialect && dialect.fullVersion >= "8.0.19" + else -> false // H2_MySQL mode also uses this function provider & requires older unsupported version + } + override fun time(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { append("SUBSTRING_INDEX(", expr, ", ' ', -1)") } 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 da2fb9290a..e753254b2f 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 @@ -271,13 +271,13 @@ internal object OracleFunctionProvider : FunctionProvider() { override fun upsert( table: Table, data: List, Any?>>, - onUpdate: List, Expression<*>>>?, - onUpdateExclude: List>?, + expression: String, + onUpdate: List, Any?>>, + keyColumns: List>, where: Op?, - transaction: Transaction, - vararg keys: Column<*> + transaction: Transaction ): String { - val statement = super.upsert(table, data, onUpdate, onUpdateExclude, where, transaction, *keys) + val statement = super.upsert(table, data, expression, onUpdate, keyColumns, where, transaction) 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 77d330162e..0750affc9a 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 @@ -271,29 +271,28 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { override fun upsert( table: Table, data: List, Any?>>, - onUpdate: List, Expression<*>>>?, - onUpdateExclude: List>?, + expression: String, + onUpdate: List, Any?>>, + keyColumns: List>, where: Op?, - transaction: Transaction, - vararg keys: Column<*> + transaction: Transaction ): String { - val keyColumns = getKeyColumnsForUpsert(table, *keys) - if (keyColumns.isNullOrEmpty()) { + if (keyColumns.isEmpty()) { transaction.throwUnsupportedException("UPSERT requires a unique key or constraint as a conflict target") } - val updateColumns = getUpdateColumnsForUpsert(data.unzip().first, onUpdateExclude, keyColumns) - return with(QueryBuilder(true)) { - appendInsertToUpsertClause(table, data, transaction) + +insert(false, table, data.unzip().first, expression, transaction) +" ON CONFLICT " keyColumns.appendTo(prefix = "(", postfix = ")") { column -> append(transaction.identity(column)) } - +" DO" - appendUpdateToUpsertClause(table, updateColumns, onUpdate, transaction, isAliasNeeded = false) + +" DO UPDATE SET " + onUpdate.appendTo { (columnToUpdate, updateExpression) -> + append("${transaction.identity(columnToUpdate)}=$updateExpression") + } where?.let { +" WHERE " @@ -303,6 +302,8 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { } } + override fun insertValue(columnName: String, queryBuilder: QueryBuilder) { queryBuilder { +"EXCLUDED.$columnName" } } + override fun delete( ignore: Boolean, table: Table, 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 c5a3c175d3..88ebb97f8a 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 @@ -230,14 +230,14 @@ internal object SQLServerFunctionProvider : FunctionProvider() { override fun upsert( table: Table, data: List, Any?>>, - onUpdate: List, Expression<*>>>?, - onUpdateExclude: List>?, + expression: String, + onUpdate: List, Any?>>, + keyColumns: List>, where: Op?, - transaction: Transaction, - vararg keys: Column<*> + transaction: Transaction ): String { // SQLSERVER MERGE statement must be terminated by a semi-colon (;) - return super.upsert(table, data, onUpdate, onUpdateExclude, where, transaction, *keys) + ";" + return super.upsert(table, data, expression, onUpdate, keyColumns, where, transaction) + ";" } 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 567b9d5758..fa6007ac33 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 @@ -217,25 +217,25 @@ internal object SQLiteFunctionProvider : FunctionProvider() { override fun upsert( table: Table, data: List, Any?>>, - onUpdate: List, Expression<*>>>?, - onUpdateExclude: List>?, + expression: String, + onUpdate: List, Any?>>, + keyColumns: List>, where: Op?, - transaction: Transaction, - vararg keys: Column<*> + transaction: Transaction ): String = with(QueryBuilder(true)) { - appendInsertToUpsertClause(table, data, transaction) + +insert(false, table, data.unzip().first, expression, transaction) +" ON CONFLICT" - val keyColumns = getKeyColumnsForUpsert(table, *keys) ?: emptyList() if (keyColumns.isNotEmpty()) { keyColumns.appendTo(prefix = " (", postfix = ")") { column -> append(transaction.identity(column)) } } - +" DO" - val updateColumns = getUpdateColumnsForUpsert(data.unzip().first, onUpdateExclude, keyColumns) - appendUpdateToUpsertClause(table, updateColumns, onUpdate, transaction, isAliasNeeded = false) + +" DO UPDATE SET " + onUpdate.appendTo { (columnToUpdate, updateExpression) -> + append("${transaction.identity(columnToUpdate)}=$updateExpression") + } where?.let { +" WHERE " @@ -244,6 +244,8 @@ internal object SQLiteFunctionProvider : FunctionProvider() { toString() } + override fun insertValue(columnName: String, queryBuilder: QueryBuilder) { queryBuilder { +"EXCLUDED.$columnName" } } + override fun delete( ignore: Boolean, table: Table, diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt index 225d64e9fc..b152400383 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt @@ -90,12 +90,15 @@ class ReturningTests : DatabaseTestsBase() { assertEquals(99.0, result1[Items.price]) val result2 = Items.upsertReturning( - returning = listOf(Items.name, Items.price), - onUpdate = listOf(Items.price to Items.price.times(10.0)) + returning = listOf(Items.name, Items.price) ) { it[id] = 1 it[name] = "B" it[price] = 200.0 + + it.onUpdate { update -> + update[price] = price times 10.0 + } }.single() assertEquals("A", result2[Items.name]) assertEquals(990.0, result2[Items.price]) 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 dbb1b416f8..11f56c1a63 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 @@ -11,8 +11,11 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.like import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus +import org.jetbrains.exposed.sql.SqlExpressionBuilder.times import org.jetbrains.exposed.sql.statements.BatchUpsertStatement -import org.jetbrains.exposed.sql.tests.* +import org.jetbrains.exposed.sql.statements.UpdateStatement +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.transactions.transaction @@ -244,15 +247,28 @@ class UpsertTests : DatabaseTestsBase() { fun testUpsertWithManualUpdateAssignment() { withTables(excludeSettings = TestDB.ALL_H2_V1, Words) { val testWord = "Test" - val incrementCount = listOf(Words.count to Words.count.plus(1)) repeat(3) { - Words.upsert(onUpdate = incrementCount) { + Words.upsert { it[word] = testWord + + it.onUpdate { update -> + update[count] = count + 1 + } } } assertEquals(3, Words.selectAll().single()[Words.count]) + + val updatedCount = 1000 + Words.upsert { + it[word] = testWord + + it.onUpdate { update -> + update[count] = updatedCount + } + } + assertEquals(updatedCount, Words.selectAll().single()[Words.count]) } } @@ -265,29 +281,34 @@ class UpsertTests : DatabaseTestsBase() { val losses = integer("losses").default(100) } + fun adjustGainAndLoss(statement: UpdateStatement) { + statement[tester.gains] = tester.gains.plus(tester.amount) + statement[tester.losses] = tester.losses.minus(tester.amount) + } + withTables(excludeSettings = TestDB.ALL_H2_V1, tester) { val itemA = tester.upsert { it[item] = "Item A" } get tester.item - val adjustGainAndLoss = listOf( - tester.gains to tester.gains.plus(tester.amount), - tester.losses to tester.losses.minus(tester.amount) - ) - tester.upsert(onUpdate = adjustGainAndLoss) { + tester.upsert { it[item] = "Item B" it[gains] = 200 it[losses] = 0 + + it.onUpdate { update -> adjustGainAndLoss(update) } } val insertResult = tester.selectAll().where { tester.item neq itemA }.single() assertEquals(200, insertResult[tester.gains]) assertEquals(0, insertResult[tester.losses]) - tester.upsert(onUpdate = adjustGainAndLoss) { + tester.upsert { it[item] = itemA it[gains] = 200 it[losses] = 0 + + it.onUpdate { update -> adjustGainAndLoss(update) } } val updateResult = tester.selectAll().where { tester.item eq itemA }.single() @@ -311,9 +332,12 @@ class UpsertTests : DatabaseTestsBase() { } assertEquals("Phrase", tester.selectAll().single()[tester.phrase]) - val phraseConcat = concat(" - ", listOf(tester.word, tester.phrase)) - tester.upsert(onUpdate = listOf(tester.phrase to phraseConcat)) { // expression in update + tester.upsert { // expression in update it[word] = testWord + + it.onUpdate { update -> + update[phrase] = concat(" - ", listOf(tester.word, tester.phrase)) + } } assertEquals("$testWord - $defaultPhrase", tester.selectAll().single()[tester.phrase]) @@ -325,6 +349,60 @@ class UpsertTests : DatabaseTestsBase() { } } + @Test + fun testUpsertWithManualUpdateUsingInsertValues() { + val tester = object : Table("tester") { + val id = integer("id").uniqueIndex() + val word = varchar("name", 64) + val count = integer("count").default(1) + } + + withTables(excludeSettings = TestDB.ALL_H2_V1, tester) { + tester.insert { + it[id] = 1 + it[word] = "Word A" + } + assertEquals(1, tester.selectAll().single()[tester.count]) + + // H2_Mysql & H2_Mariadb syntax does not allow VALUES() syntax to come first in complex expression + // Syntax must be column=(1 + VALUES(column)), not column=(VALUES(column) + 1) + tester.upsert { + it[id] = 1 + it[word] = "Word B" + it[count] = 9 + + it.onUpdate { update -> + update[count] = intLiteral(100) times insertValue(count) + } + } + val result = tester.selectAll().single() + assertEquals(900, result[tester.count]) + + val newWords = listOf( + Triple(2, "Word B", 2), + Triple(1, "Word A", 3), + Triple(3, "Word C", 4) + ) + tester.batchUpsert( + newWords, + ) { (id, word, count) -> + this[tester.id] = id + this[tester.word] = word + this[tester.count] = count + + onUpdate { + it[tester.word] = concat(tester.word, stringLiteral(" || "), insertValue(tester.count)) + it[tester.count] = intLiteral(1) plus insertValue(tester.count) + } + } + + assertEquals(3, tester.selectAll().count()) + val updatedWord = tester.selectAll().where { tester.word like "% || %" }.single() + assertEquals("Word A || 3", updatedWord[tester.word]) + assertEquals(4, updatedWord[tester.count]) + } + } + @Test fun testUpsertWithUpdateExcludingColumns() { val tester = object : Table("tester") { @@ -511,10 +589,13 @@ class UpsertTests : DatabaseTestsBase() { val vowels = listOf("A", "E", "I", "O", "U") val alphabet = ('A'..'Z').map { it.toString() } val lettersWithDuplicates = alphabet + vowels - val incrementCount = listOf(Words.count to Words.count.plus(1)) - Words.batchUpsert(lettersWithDuplicates, onUpdate = incrementCount) { letter -> + Words.batchUpsert(lettersWithDuplicates) { letter -> this[Words.word] = letter + + onUpdate { + it[Words.count] = Words.count + 1 + } } assertEquals(alphabet.size.toLong(), Words.selectAll().count()) @@ -556,18 +637,20 @@ class UpsertTests : DatabaseTestsBase() { val vowels = listOf("A", "E", "I", "O", "U") val alphabet = ('A'..'Z').map { it.toString() } val lettersWithDuplicates = alphabet + vowels - val incrementCount = listOf(Words.count to Words.count.plus(1)) val firstThreeVowels = vowels.take(3) Words.batchUpsert( lettersWithDuplicates, - onUpdate = incrementCount, // PostgresNG throws IndexOutOfBound if shouldReturnGeneratedValues == true // Related issue in pgjdbc-ng repository: https://github.com/impossibl/pgjdbc-ng/issues/545 shouldReturnGeneratedValues = false, where = { Words.word inList firstThreeVowels } ) { letter -> this[Words.word] = letter + + onUpdate { + it[Words.count] = Words.count + 1 + } } assertEquals(alphabet.size.toLong(), Words.selectAll().count())