Skip to content

Commit

Permalink
feat: EXPOSED-258 Enhance upsert to allow exclusion of columns set on…
Browse files Browse the repository at this point in the history
… conflict (#2006)

* feat: EXPOSED-258 Enhance upsert to allow exclusion of columns set on conflict

Currently, if the onUpdate parameter of upsert() is provided an argument, these
column-value pairs will be used in the update clause of the upsert statement.
If the argument is left null, a duplicate of the column-value assignments from
the insert block will be used instead.
If a user wants to only update a subset of columns, or not update columns with
default values, the only option is to provide an argument to onUpdate that
duplicates most of the insert block.

This can become verbose, so an additional parameter, onUpdateExclude, has been
introduced, which duplicates all insert column-value pairs except those for the
specifically excluded columns.
This parameter could also be used in the case when the number of columns to update
is small but the user wants to avoid duplicating the insert values.

* feat: EXPOSED-258 Enhance upsert to allow exclusion of columns set on conflict

Edit documentation section to include new sample

* feat: EXPOSED-258 Enhance upsert to allow exclusion of columns set on conflict

- Change order of private batchInsert() vararg parameter
- Change onExcludeUpdate parameter to be a List instead of Set
  • Loading branch information
bog-walk committed Feb 26, 2024
1 parent 77f68b9 commit b22a3bc
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 25 deletions.
20 changes: 20 additions & 0 deletions documentation-website/Writerside/topics/Deep-Dive-into-DSL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 13 additions & 10 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 <init> (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Z)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Z)V
public synthetic fun <init> (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;
}

Expand Down Expand Up @@ -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 <init> (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;)V
public fun <init> (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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
22 changes: 17 additions & 5 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,18 @@ fun Join.update(where: (SqlExpressionBuilder.() -> Op<Boolean>)? = 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 : Table> T.upsert(
vararg keys: Column<*>,
onUpdate: List<Pair<Column<*>, Expression<*>>>? = null,
onUpdateExclude: List<Column<*>>? = null,
where: (SqlExpressionBuilder.() -> Op<Boolean>)? = null,
body: T.(UpsertStatement<Long>) -> Unit
) = UpsertStatement<Long>(this, *keys, onUpdate = onUpdate, where = where?.let { SqlExpressionBuilder.it() }).apply {
) = UpsertStatement<Long>(this, *keys, onUpdate = onUpdate, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }).apply {
body(this)
execute(TransactionManager.current())
}
Expand All @@ -442,6 +445,8 @@ fun <T : Table> 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
Expand All @@ -450,10 +455,11 @@ fun <T : Table, E : Any> T.batchUpsert(
data: Iterable<E>,
vararg keys: Column<*>,
onUpdate: List<Pair<Column<*>, Expression<*>>>? = null,
onUpdateExclude: List<Column<*>>? = null,
shouldReturnGeneratedValues: Boolean = true,
body: BatchUpsertStatement.(E) -> Unit
): List<ResultRow> {
return batchUpsert(data.iterator(), *keys, onUpdate = onUpdate, shouldReturnGeneratedValues = shouldReturnGeneratedValues, body = body)
return batchUpsert(data.iterator(), onUpdate, onUpdateExclude, shouldReturnGeneratedValues, keys = keys, body = body)
}

/**
Expand All @@ -467,6 +473,8 @@ fun <T : Table, E : Any> 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
Expand All @@ -475,10 +483,11 @@ fun <T : Table, E : Any> T.batchUpsert(
data: Sequence<E>,
vararg keys: Column<*>,
onUpdate: List<Pair<Column<*>, Expression<*>>>? = null,
onUpdateExclude: List<Column<*>>? = null,
shouldReturnGeneratedValues: Boolean = true,
body: BatchUpsertStatement.(E) -> Unit
): List<ResultRow> {
return batchUpsert(data.iterator(), *keys, onUpdate = onUpdate, shouldReturnGeneratedValues = shouldReturnGeneratedValues, body = body)
return batchUpsert(data.iterator(), onUpdate, onUpdateExclude, shouldReturnGeneratedValues, keys = keys, body = body)
}

/**
Expand All @@ -492,18 +501,21 @@ fun <T : Table, E : Any> 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 : Table, E> T.batchUpsert(
data: Iterator<E>,
vararg keys: Column<*>,
onUpdate: List<Pair<Column<*>, Expression<*>>>? = null,
onUpdateExclude: List<Column<*>>? = null,
shouldReturnGeneratedValues: Boolean = true,
vararg keys: Column<*>,
body: BatchUpsertStatement.(E) -> Unit
): List<ResultRow> = executeBatch(data, body) {
BatchUpsertStatement(this, *keys, onUpdate = onUpdate, shouldReturnGeneratedValues = shouldReturnGeneratedValues)
BatchUpsertStatement(this, *keys, onUpdate = onUpdate, onUpdateExclude = onUpdateExclude, shouldReturnGeneratedValues = shouldReturnGeneratedValues)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ 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.
*/
open class BatchUpsertStatement(
table: Table,
vararg val keys: Column<*>,
val onUpdate: List<Pair<Column<*>, Expression<*>>>?,
val onUpdateExclude: List<Column<*>>?,
shouldReturnGeneratedValues: Boolean = true
) : BaseBatchInsertStatement(table, ignore = false, shouldReturnGeneratedValues) {

Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key : Any>(
table: Table,
vararg val keys: Column<*>,
val onUpdate: List<Pair<Column<*>, Expression<*>>>?,
val onUpdateExclude: List<Column<*>>?,
val where: Op<Boolean>?
) : InsertStatement<Key>(table) {

Expand All @@ -28,7 +31,7 @@ open class UpsertStatement<Key : Any>(
}
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<Iterable<Pair<IColumnType, Any?>>> {
Expand Down
Loading

0 comments on commit b22a3bc

Please sign in to comment.