Skip to content

Commit

Permalink
fix: EXPOSED-116 UUID conversion error with upsert in H2 (#1823)
Browse files Browse the repository at this point in the history
Using a UUID column as a key constraint in H2's MERGE INTO -- USING statement
was failing with: org.h2.jdbc.JdbcSQLDataException: Data conversion error converting
"U&'\\fffdK\\fffdS\\001bJr\\fffdG%\\fffdE\\00100\\fffd' (TESTER: ""ID"" UUID NOT NULL)";

This error occurs because the statement uses a derived column list to merge ON
(T.ID=S.ID), so the UUID as a ByteArray in T.ID needs to be converted internally
to compare with the String in S.ID. This error goes away if a regular integer
id column is used or if the key constraint is swapped with another.

The error also goes away if the identical MERGE statement is placed in an exec()
directly, indicating that the issue lies with how Exposed sends UUID values to
the H2 database.

Switching the sent value from a ByteArray to a String allows comparison without
conversion. A String is still one of 3 acceptable ways to store a UUID in H2 and
sending a String still results in a UUID being retrieved back from the database.
  • Loading branch information
bog-walk authored Aug 4, 2023
1 parent 0daf194 commit 4b88ab7
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.intellij.lang.annotations.Language
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.util.*

internal object H2DataTypeProvider : DataTypeProvider() {
override fun binaryType(): String {
Expand All @@ -12,6 +13,7 @@ internal object H2DataTypeProvider : DataTypeProvider() {
}

override fun uuidType(): String = "UUID"
override fun uuidToDB(value: UUID): Any = value.toString()
override fun dateTimeType(): String = "DATETIME(9)"

override fun timestampWithTimeZoneType(): String = "TIMESTAMP(9) WITH TIME ZONE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.jetbrains.exposed.sql.vendors
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.util.*

internal object OracleDataTypeProvider : DataTypeProvider() {
override fun byteType(): String = "SMALLINT"
Expand All @@ -26,11 +27,25 @@ internal object OracleDataTypeProvider : DataTypeProvider() {

override fun binaryType(length: Int): String {
@Suppress("MagicNumber")
return if (length < 2000) "RAW ($length)"
else binaryType()
return if (length < 2000) "RAW ($length)" else binaryType()
}

override fun uuidType(): String {
return if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) {
"UUID"
} else {
return "RAW(16)"
}
}

override fun uuidToDB(value: UUID): Any {
return if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) {
H2DataTypeProvider.uuidToDB(value)
} else {
super.uuidToDB(value)
}
}

override fun uuidType(): String = "RAW(16)"
override fun dateTimeType(): String = "TIMESTAMP"
override fun booleanType(): String = "CHAR(1)"
override fun booleanToStatementString(bool: Boolean) = if (bool) "1" else "0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,17 @@ class UpsertTests : DatabaseTestsBase() {
it[name] = "A"
}

tester.upsert { // insert because only 1 constraint is equal
tester.upsert { // insert because only 1 constraint is equal
it[idA] = 7
it[idB] = insertStmt get tester.idB
it[name] = "B"
}
tester.upsert { // insert because both constraints differ
tester.upsert { // insert because both constraints differ
it[idA] = 99
it[idB] = 99
it[name] = "C"
}
tester.upsert { // update because both constraints match
tester.upsert { // update because both constraints match
it[idA] = insertStmt get tester.idA
it[idB] = insertStmt get tester.idB
it[name] = "D"
Expand Down Expand Up @@ -157,6 +157,32 @@ class UpsertTests : DatabaseTestsBase() {
}
}

@Test
fun testUpsertWithUUIDKeyConflict() {
val tester = object : Table("tester") {
val id = uuid("id").autoGenerate()
val title = text("title")

override val primaryKey = PrimaryKey(id)
}

withTables(tester) { testDb ->
excludingH2Version1(testDb) {
val uuid1 = tester.upsert {
it[title] = "A"
} get tester.id
tester.upsert {
it[id] = uuid1
it[title] = "B"
}

val result = tester.selectAll().single()
assertEquals(uuid1, result[tester.id])
assertEquals("B", result[tester.title])
}
}
}

@Test
fun testUpsertWithNoUniqueConstraints() {
val tester = object : Table("tester") {
Expand Down Expand Up @@ -254,18 +280,18 @@ class UpsertTests : DatabaseTestsBase() {
withTables(tester) { testDb ->
excludingH2Version1(testDb) {
val testWord = "Test"
tester.upsert { // default expression in insert
tester.upsert { // default expression in insert
it[word] = testWord
}
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(onUpdate = listOf(tester.phrase to phraseConcat)) { // expression in update
it[word] = testWord
}
assertEquals("$testWord - $defaultPhrase", tester.selectAll().single()[tester.phrase])

tester.upsert { // provided expression in insert
tester.upsert { // provided expression in insert
it[word] = "$testWord 2"
it[phrase] = concat(stringLiteral("foo"), stringLiteral("bar"))
}
Expand Down

0 comments on commit 4b88ab7

Please sign in to comment.