From 29203d660b449f4a5d01f2df8f45014be15ae62d Mon Sep 17 00:00:00 2001 From: bog-walk <82039410+bog-walk@users.noreply.github.com> Date: Fri, 12 Jan 2024 08:50:30 -0500 Subject: [PATCH] fix: EXPOSED-257 Upsert incorrectly parameterizes non-literal WHERE arguments (#1965) * fix: EXPOSED-257 Upsert incorectly parameterizes non-literal WHERE arguments Currently when using upsert() with a where argument, only literal values generate valid SQL. Attempting to use a value with a param() wrapper or the plain native type results in an exception that the parameter count is incorrect. This occurs because the where arguments were not correctly added to the list of statement arguments in the super class, so an override has now been included in UpsertStatement directly. --- exposed-core/api/exposed-core.api | 2 ++ .../exposed/sql/statements/InsertStatement.kt | 4 +-- .../exposed/sql/statements/UpsertStatement.kt | 13 +++++++ .../sql/tests/shared/dml/UpsertTests.kt | 34 +++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index f5fa8cf28d..818b458dd3 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -2963,6 +2963,8 @@ 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 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 getWhere ()Lorg/jetbrains/exposed/sql/Op; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt index 4c5cf05157..6f82f51250 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt @@ -209,7 +209,7 @@ open class InsertStatement( } override fun arguments(): List>> { - return arguments!!.map { args -> + return arguments?.map { args -> val builder = QueryBuilder(true) args.filter { (_, value) -> value != DefaultValueMarker @@ -217,6 +217,6 @@ open class InsertStatement( builder.registerArgument(column, value) } builder.args - } + } ?: emptyList() } } 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 de2e74da10..20cf632830 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 @@ -30,4 +30,17 @@ open class UpsertStatement( } return functionProvider.upsert(table, arguments!!.first(), onUpdate, where, transaction, keys = keys) } + + override fun arguments(): List>> { + return arguments?.map { args -> + val builder = QueryBuilder(true) + args.filter { (_, value) -> + value != DefaultValueMarker + }.forEach { (column, value) -> + builder.registerArgument(column, value) + } + where?.toQueryBuilder(builder) + builder.args + } ?: emptyList() + } } 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 8a3ac4fba1..fe1047f24e 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 @@ -6,7 +6,9 @@ import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.concat import org.jetbrains.exposed.sql.SqlExpressionBuilder.less +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.statements.BatchUpsertStatement import org.jetbrains.exposed.sql.tests.* @@ -383,6 +385,38 @@ class UpsertTests : DatabaseTestsBase() { } } + @Test + fun testUpsertWithWhereParameterized() { + val tester = object : IntIdTable("tester") { + val name = varchar("name", 64).uniqueIndex() + val age = integer("age") + } + + withTables(excludeSettings = TestDB.mySqlRelatedDB + upsertViaMergeDB, tester) { + val id1 = tester.upsert { + it[name] = "Anya" + it[age] = 10 + } get tester.id + tester.upsert { + it[name] = "Anna" + it[age] = 50 + } + + val nameStartsWithA = tester.name like "A%" + val nameEndsWithA = tester.name like stringLiteral("%a") + val nameIsNotAnna = tester.name neq stringParam("Anna") + val updatedAge = 20 + tester.upsert(tester.name, where = { nameStartsWithA and nameEndsWithA and nameIsNotAnna }) { + it[name] = "Anya" + it[age] = updatedAge + } + + assertEquals(2, tester.selectAll().count()) + val updatedResult = tester.selectAll().where { tester.age eq updatedAge }.single() + assertEquals(id1, updatedResult[tester.id]) + } + } + @Test fun testUpsertWithSubQuery() { val tester1 = object : IntIdTable("tester_1") {