Skip to content

Commit

Permalink
fix: EXPOSED 718 Fix timestamp column not storing values in UTC time …
Browse files Browse the repository at this point in the history
…zone
  • Loading branch information
joc-a committed Jan 31, 2025
1 parent a76c7b8 commit 80edc93
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,57 @@ import java.util.*
private val DEFAULT_DATE_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_DATE.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
}

private val DEFAULT_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
}

private val DEFAULT_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ROOT).withZone(UTC)
}

private val SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSS",
Locale.ROOT
).withZone(ZoneId.systemDefault())
}

private val SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSS",
Locale.ROOT
).withZone(UTC)
}

private val MYSQL_FRACTION_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSSSSS",
Locale.ROOT
).withZone(ZoneId.systemDefault())
}

private val MYSQL_FRACTION_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSSSSS",
Locale.ROOT
).withZone(UTC)
}

private val MYSQL_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss",
Locale.ROOT
).withZone(ZoneId.systemDefault())
}

private val MYSQL_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss",
Locale.ROOT
).withZone(UTC)
}

private val ORACLE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"1970-01-01 HH:mm:ss",
Expand Down Expand Up @@ -99,6 +128,7 @@ private val POSTGRESQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy {
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.toFormatter(Locale.ROOT)
}

private val MYSQL_FRACTION_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSSSSS",
Expand Down Expand Up @@ -316,20 +346,21 @@ class JavaLocalTimeColumnType : ColumnType<LocalTime>(), IDateColumnType {
*/
class JavaInstantColumnType : ColumnType<Instant>(), IDateColumnType {
override val hasTimePart: Boolean = true

override fun sqlType(): String = currentDialect.dataTypeProvider.dateTimeType()

override fun nonNullValueToString(value: Instant): String {
return when (val dialect = currentDialect) {
is OracleDialect -> oracleDateTimeLiteral(value)

is SQLiteDialect ->
"'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value)}'"
"'${SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(value)}'"

is MysqlDialect -> {
val formatter = if (dialect.isFractionDateTimeSupported()) MYSQL_FRACTION_DATE_TIME_STRING_FORMATTER else MYSQL_DATE_TIME_STRING_FORMATTER
val formatter = if (dialect.isFractionDateTimeSupported()) MYSQL_FRACTION_TIMESTAMP_STRING_FORMATTER else MYSQL_TIMESTAMP_STRING_FORMATTER
"'${formatter.format(value)}'"
}
else -> "'${DEFAULT_DATE_TIME_STRING_FORMATTER.format(value)}'"
else -> "'${DEFAULT_TIMESTAMP_STRING_FORMATTER.format(value)}'"
}
}

Expand All @@ -343,19 +374,26 @@ class JavaInstantColumnType : ColumnType<Instant>(), IDateColumnType {
return rs.getTimestamp(index)
}

override fun notNullValueToDB(value: Instant): Any = when (currentDialect) {
is SQLiteDialect ->
SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value)
else -> java.sql.Timestamp.from(value)
override fun notNullValueToDB(value: Instant): Any {
val dialect = currentDialect
return when {
dialect is SQLiteDialect ->
SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(value)
dialect is MysqlDialect && dialect !is MariaDBDialect -> {
val formatter = if (dialect.isFractionDateTimeSupported()) MYSQL_FRACTION_TIMESTAMP_STRING_FORMATTER else MYSQL_TIMESTAMP_STRING_FORMATTER
formatter.format(value)
}
else -> java.sql.Timestamp.from(value)
}
}

override fun nonNullValueAsDefaultString(value: Instant): String {
val dialect = currentDialect
return when {
dialect is PostgreSQLDialect ->
"'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value).trimEnd('0').trimEnd('.')}'::timestamp without time zone"
"'${SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(value).trimEnd('0').trimEnd('.')}'::timestamp without time zone"
dialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle ->
"'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value).trimEnd('0').trimEnd('.')}'"
"'${SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(value).trimEnd('0').trimEnd('.')}'"
else -> super.nonNullValueAsDefaultString(value)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,30 @@ class JavaTimeTests : DatabaseTestsBase() {
assertTrue(statements.isEmpty())
}
}

@Test
fun testTimestampAlwaysSavedInUTC() {
val tester = object : Table("tester") {
val timestamp_col = timestamp("timestamp_col")
}

withTables(tester) {
// Cairo time zone
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("Africa/Cairo"))
assertEquals("Africa/Cairo", ZoneId.systemDefault().id)

val instant = Instant.now()

tester.insert {
it[timestamp_col] = instant
}

assertEquals(
instant,
tester.selectAll().single()[tester.timestamp_col]
)
}
}
}

fun <T : Temporal> assertEqualDateTime(d1: T?, d2: T?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,60 @@ private val DEFAULT_DATE_STRING_FORMATTER by lazy {
private val DEFAULT_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
}

private val DEFAULT_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ROOT).withZone(UTC)
}

private val SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSS",
Locale.ROOT
).withZone(ZoneId.systemDefault())
}

private val SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSS",
Locale.ROOT
).withZone(UTC)
}

private val MYSQL_FRACTION_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSSSSS",
Locale.ROOT
).withZone(ZoneId.systemDefault())
}

private val MYSQL_FRACTION_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSSSSS",
Locale.ROOT
).withZone(UTC)
}

private val MYSQL_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss",
Locale.ROOT
).withZone(ZoneId.systemDefault())
}

private val MYSQL_TIMESTAMP_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss",
Locale.ROOT
).withZone(UTC)
}

private val ORACLE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"1970-01-01 HH:mm:ss",
Locale.ROOT
).withZone(ZoneId.of("UTC"))
}

private val DEFAULT_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_TIME.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
}
Expand Down Expand Up @@ -111,6 +140,7 @@ private val POSTGRESQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy {
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.toFormatter(Locale.ROOT)
}

private val MYSQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSSSSS",
Expand Down Expand Up @@ -337,16 +367,16 @@ class KotlinInstantColumnType : ColumnType<Instant>(), IDateColumnType {

return when (val dialect = currentDialect) {
is MysqlDialect -> {
val formatter = if (dialect.isFractionDateTimeSupported()) MYSQL_FRACTION_DATE_TIME_STRING_FORMATTER else MYSQL_DATE_TIME_STRING_FORMATTER
val formatter = if (dialect.isFractionDateTimeSupported()) MYSQL_FRACTION_TIMESTAMP_STRING_FORMATTER else MYSQL_TIMESTAMP_STRING_FORMATTER
"'${formatter.format(instant)}'"
}

is SQLiteDialect ->
"'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(instant)}'"
"'${SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(instant)}'"

is OracleDialect -> oracleDateTimeLiteral(value)

else -> "'${DEFAULT_DATE_TIME_STRING_FORMATTER.format(instant)}'"
else -> "'${DEFAULT_TIMESTAMP_STRING_FORMATTER.format(instant)}'"
}
}

Expand All @@ -360,21 +390,27 @@ class KotlinInstantColumnType : ColumnType<Instant>(), IDateColumnType {
return rs.getTimestamp(index)
}

override fun notNullValueToDB(value: Instant): Any = when {
currentDialect is SQLiteDialect ->
SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value.toJavaInstant())

else -> java.sql.Timestamp.from(value.toJavaInstant())
override fun notNullValueToDB(value: Instant): Any {
val dialect = currentDialect
return when {
dialect is SQLiteDialect ->
SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(value.toJavaInstant())
dialect is MysqlDialect && dialect !is MariaDBDialect -> {
val formatter = if (dialect.isFractionDateTimeSupported()) MYSQL_FRACTION_TIMESTAMP_STRING_FORMATTER else MYSQL_TIMESTAMP_STRING_FORMATTER
formatter.format(value.toJavaInstant())
}
else -> java.sql.Timestamp.from(value.toJavaInstant())
}
}

override fun nonNullValueAsDefaultString(value: Instant): String {
val dialect = currentDialect
return when {
dialect is PostgreSQLDialect ->
"'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value.toJavaInstant()).trimEnd('0').trimEnd('.')}'::timestamp without time zone"
"'${SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(value.toJavaInstant()).trimEnd('0').trimEnd('.')}'::timestamp without time zone"

dialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle ->
"'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value.toJavaInstant()).trimEnd('0').trimEnd('.')}'"
"'${SQLITE_AND_ORACLE_TIMESTAMP_STRING_FORMATTER.format(value.toJavaInstant()).trimEnd('0').trimEnd('.')}'"

else -> super.nonNullValueAsDefaultString(value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,30 @@ class KotlinTimeTests : DatabaseTestsBase() {
assertTrue(statements.isEmpty())
}
}

@Test
fun testTimestampAlwaysSavedInUTC() {
val tester = object : Table("tester") {
val timestamp_col = timestamp("timestamp_col")
}

withTables(tester) {
// Cairo time zone
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("Africa/Cairo"))
assertEquals("Africa/Cairo", ZoneId.systemDefault().id)

val instant = Clock.System.now()

tester.insert {
it[timestamp_col] = instant
}

assertEquals(
instant,
tester.selectAll().single()[tester.timestamp_col]
)
}
}
}

fun <T> assertEqualDateTime(d1: T?, d2: T?) {
Expand Down

0 comments on commit 80edc93

Please sign in to comment.