diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/datetime/InstantColumnType.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/datetime/InstantColumnType.kt index 463282bf01..26a99a34db 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/datetime/InstantColumnType.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/datetime/InstantColumnType.kt @@ -54,14 +54,14 @@ abstract class InstantColumnType : ColumnType(), IDateColumnType { } else { MYSQL_TIMESTAMP_FORMAT } - "'${formatter.format(localDateTime)}'" + "'${formatter.formatAndTrimStart(localDateTime)}'" } - is SQLiteDialect -> "'${ORACLE_SQLITE_TIMESTAMP_FORMAT.format(localDateTime)}'" + is SQLiteDialect -> "'${ORACLE_SQLITE_TIMESTAMP_FORMAT.formatAndTrimStart(localDateTime)}'" is OracleDialect -> { - val formatted = ORACLE_SQLITE_TIMESTAMP_FORMAT.format(localDateTime) + val formatted = ORACLE_SQLITE_TIMESTAMP_FORMAT.formatAndTrimStart(localDateTime) "TO_TIMESTAMP('$formatted', 'YYYY-MM-DD HH24:MI:SS.FF3')" } - else -> "'${DEFAULT_TIMESTAMP_FORMAT.format(localDateTime)}'" + else -> "'${DEFAULT_TIMESTAMP_FORMAT.formatAndTrimStart(localDateTime)}'" } } @@ -106,15 +106,15 @@ abstract class InstantColumnType : ColumnType(), IDateColumnType { @OptIn(InternalApi::class) @Suppress("MagicNumber") return when (val dialect = currentDialect) { - is SQLiteDialect -> ORACLE_SQLITE_TIMESTAMP_FORMAT.format(localDateTime) + is SQLiteDialect -> ORACLE_SQLITE_TIMESTAMP_FORMAT.formatAndTrimStart(localDateTime) is MysqlDialect if ( dialect !is MariaDBDialect && !currentTransaction().db.version.covers(8, 0) ) -> { if (dialect.isFractionDateTimeSupported()) { - MYSQL_TIMESTAMP_FRACTION_FORMAT.format(localDateTime) + MYSQL_TIMESTAMP_FRACTION_FORMAT.formatAndTrimStart(localDateTime) } else { - MYSQL_TIMESTAMP_FORMAT.format(localDateTime) + MYSQL_TIMESTAMP_FORMAT.formatAndTrimStart(localDateTime) } } else -> localDateTime.toSqlTimestamp() @@ -128,14 +128,16 @@ abstract class InstantColumnType : ColumnType(), IDateColumnType { return when { dialect is PostgreSQLDialect -> { - val formatted = ORACLE_SQLITE_TIMESTAMP_FORMAT.format(localDateTime) + val formatted = ORACLE_SQLITE_TIMESTAMP_FORMAT.formatAndTrimStart(localDateTime) "'${formatted.trimEnd('0').trimEnd('.')}'::timestamp without time zone" } dialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> { - val formatted = ORACLE_SQLITE_TIMESTAMP_FORMAT.format(localDateTime) + val formatted = ORACLE_SQLITE_TIMESTAMP_FORMAT.formatAndTrimStart(localDateTime) "'${formatted.trimEnd('0').trimEnd('.')}'" } else -> super.nonNullValueAsDefaultString(value) } } + + private fun DateTimeFormat.formatAndTrimStart(value: LocalDateTime): String = format(value).trimStart('+') } diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/v1/datetime/DefaultsTest.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/v1/datetime/DefaultsTest.kt index 03bf747c42..456d602b7e 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/v1/datetime/DefaultsTest.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/v1/datetime/DefaultsTest.kt @@ -12,6 +12,7 @@ import org.jetbrains.exposed.v1.dao.EntityClass import org.jetbrains.exposed.v1.dao.IntEntity import org.jetbrains.exposed.v1.dao.IntEntityClass import org.jetbrains.exposed.v1.dao.flushCache +import org.jetbrains.exposed.v1.exceptions.ExposedSQLException import org.jetbrains.exposed.v1.jdbc.* import org.jetbrains.exposed.v1.tests.DatabaseTestsBase import org.jetbrains.exposed.v1.tests.MISSING_R2DBC_TEST @@ -672,4 +673,37 @@ class DefaultsTest : DatabaseTestsBase() { assertEquals(timestamp, entity[DefaultTimestampTable.timestamp]) } } + + @Test + fun testTimestampWithDistantFutureDefault() { + val tester = object : Table("tester") { + // Kotlin DISTANT_FUTURE == +100000-01-01T00:00:00Z + val status_end = timestamp("status_end").default(Instant.DISTANT_FUTURE) + val activity_end = timestamp("activity_end").clientDefault { Instant.DISTANT_FUTURE } + } + + withDb { testDb -> + // Stored range for these DB (& SQL Server) has maximum year 9999 + if (testDb == TestDB.ORACLE || testDb in TestDB.ALL_MYSQL_MARIADB) { + // Out-of-range TS value not even acceptable as just default value + expectException { SchemaUtils.create(tester) } + } else { + SchemaUtils.create(tester) + + if (testDb == TestDB.SQLSERVER) { + // TS value is only checked whether out-of-range on insert + expectException { tester.insert { } } + } else { + tester.insert { } + + val results = tester.selectAll().singleOrNull() + assertNotNull(results) + assertEquals(Instant.DISTANT_FUTURE, results[tester.status_end]) + assertEquals(Instant.DISTANT_FUTURE, results[tester.activity_end]) + } + + SchemaUtils.drop(tester) + } + } + } } diff --git a/exposed-r2dbc-tests/src/test/kotlin/org/jetbrains/exposed/v1/r2dbc/sql/tests/kotlindatetime/DefaultsTest.kt b/exposed-r2dbc-tests/src/test/kotlin/org/jetbrains/exposed/v1/r2dbc/sql/tests/kotlindatetime/DefaultsTest.kt index 332ecbb736..262e7a518d 100644 --- a/exposed-r2dbc-tests/src/test/kotlin/org/jetbrains/exposed/v1/r2dbc/sql/tests/kotlindatetime/DefaultsTest.kt +++ b/exposed-r2dbc-tests/src/test/kotlin/org/jetbrains/exposed/v1/r2dbc/sql/tests/kotlindatetime/DefaultsTest.kt @@ -3,6 +3,7 @@ package org.jetbrains.exposed.v1.r2dbc.sql.tests.kotlindatetime import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.singleOrNull import kotlinx.datetime.* import org.jetbrains.exposed.v1.core.* import org.jetbrains.exposed.v1.core.dao.id.IntIdTable @@ -571,4 +572,37 @@ class DefaultsTest : R2dbcDatabaseTestsBase() { assertEquals(timestamp, entity[DefaultTimestampTable.timestamp]) } } + + @Test + fun testTimestampWithDistantFutureDefault() { + val tester = object : Table("tester") { + // Kotlin DISTANT_FUTURE == +100000-01-01T00:00:00Z + val status_end = timestamp("status_end").default(Instant.DISTANT_FUTURE) + val activity_end = timestamp("activity_end").clientDefault { Instant.DISTANT_FUTURE } + } + + withDb { testDb -> + // Stored range for these DB (& SQL Server) has maximum year 9999 + if (testDb == TestDB.ORACLE || testDb in TestDB.ALL_MYSQL_MARIADB) { + // Out-of-range TS value not even acceptable as just default value + expectException { SchemaUtils.create(tester) } + } else { + SchemaUtils.create(tester) + + if (testDb == TestDB.SQLSERVER) { + // TS value is only checked whether out-of-range on insert + expectException { tester.insert { } } + } else { + tester.insert { } + + val results = tester.selectAll().singleOrNull() + assertNotNull(results) + assertEquals(Instant.DISTANT_FUTURE, results[tester.status_end]) + assertEquals(Instant.DISTANT_FUTURE, results[tester.activity_end]) + } + + SchemaUtils.drop(tester) + } + } + } }