From 1a4ceeb93796ff625ae194aa491a8b59ea3e5067 Mon Sep 17 00:00:00 2001 From: Jocelyne Date: Tue, 6 Feb 2024 00:12:26 +0100 Subject: [PATCH] fix: EXPOSED-281 Timestamp with time zone column default falsely triggers ALTER statement -`KotlinOffsetDateTimeColumnType` now overrides `nonNullValueAsDefaultString` to match the default value obtained from the metadata for PostgreSQL, MySQL, and H2 Oracle. --- .../kotlin/datetime/KotlinDateColumnType.kt | 36 +++++++++++++++++++ .../sql/kotlin/datetime/DefaultsTest.kt | 25 +++++++++++++ 2 files changed, 61 insertions(+) diff --git a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt index 72cf1813f3..4c403a26cc 100644 --- a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt +++ b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt @@ -88,6 +88,29 @@ internal val ORACLE_OFFSET_DATE_TIME_FORMATTER by lazy { ) } +// Example result: 2023-07-07 14:42:29.343+00 +internal val POSTGRESQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSS[x]", + Locale.ROOT + ).withZone(ZoneId.of("UTC")) +} + +// Example result: 2023-07-07 14:42:29.343 +internal val H2_ORACLE_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSS", + Locale.ROOT + ) +} + +internal val MYSQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSSSSS", + Locale.ROOT + ).withZone(ZoneId.of("UTC")) +} + internal val DEFAULT_OFFSET_DATE_TIME_FORMATTER by lazy { DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(Locale.ROOT) } @@ -400,6 +423,19 @@ class KotlinOffsetDateTimeColumnType : ColumnType(), IDateColumnType { else -> error("Unexpected value: $value of ${value::class.qualifiedName}") } + override fun nonNullValueAsDefaultString(value: Any): String = when (value) { + is OffsetDateTime -> { + when { + currentDialect is PostgreSQLDialect -> "'${value.format(POSTGRESQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER)}'::timestamp with time zone" + currentDialect is H2Dialect && currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> + "'${value.format(H2_ORACLE_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER)}'" + currentDialect is MysqlDialect -> "'${value.format(MYSQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER)}'" + else -> super.nonNullValueAsDefaultString(value) + } + } + else -> super.nonNullValueAsDefaultString(value) + } + companion object { internal val INSTANCE = KotlinOffsetDateTimeColumnType() } diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt index 43f2ba0333..4994998a05 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt @@ -547,4 +547,29 @@ class DefaultsTest : DatabaseTestsBase() { assertEquals(0, statements.size) } } + + @Test + fun testTimestampWithTimeZoneDefaultDoesNotTriggerAlterStatement() { + val offsetDateTime = OffsetDateTime.now(ZoneId.of("Japan")) + + val tester = object : Table("tester") { + val timestampWithTimeZoneWithDefault = timestampWithTimeZone("timestampWithTimeZoneWithDefault").default(offsetDateTime) + } + + // SQLite does not support ALTER TABLE on a column that has a default value + // MariaDB does not support TIMESTAMP WITH TIME ZONE column type + val unsupportedDatabases = listOf(TestDB.SQLITE, TestDB.MARIADB) + withDb(excludeSettings = unsupportedDatabases) { + if (!isOldMySql()) { + try { + SchemaUtils.drop(tester) + SchemaUtils.create(tester) + val statements = SchemaUtils.addMissingColumnsStatements(tester) + assertEquals(0, statements.size) + } finally { + SchemaUtils.drop(tester) + } + } + } + } }