Skip to content

Commit e0dd066

Browse files
committed
feat: add ZonedDateTimeColumnType for enhanced date-time handling; increment version to 1.21.7-2.2.0
1 parent a9384f3 commit e0dd066

File tree

3 files changed

+161
-2
lines changed

3 files changed

+161
-2
lines changed

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
kotlin.code.style=official
22
kotlin.stdlib.default.dependency=false
33
org.gradle.parallel=true
4-
version=1.21.7-2.1.0
4+
version=1.21.7-2.2.0
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package dev.slne.surf.database.database.columns
2+
3+
4+
import org.jetbrains.exposed.sql.*
5+
import org.jetbrains.exposed.sql.Function
6+
import org.jetbrains.exposed.sql.vendors.*
7+
import java.sql.ResultSet
8+
import java.sql.Timestamp
9+
import java.time.*
10+
import java.time.format.DateTimeFormatter
11+
import java.util.*
12+
13+
private val SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER by lazy {
14+
DateTimeFormatter.ofPattern(
15+
"yyyy-MM-dd HH:mm:ss.SSS",
16+
Locale.ROOT
17+
).withZone(ZoneOffset.UTC)
18+
}
19+
20+
private val MYSQL_FRACTION_DATE_TIME_STRING_FORMATTER by lazy {
21+
DateTimeFormatter.ofPattern(
22+
"yyyy-MM-dd HH:mm:ss.SSSSSS",
23+
Locale.ROOT
24+
).withZone(ZoneOffset.UTC)
25+
}
26+
27+
private val MYSQL_DATE_TIME_STRING_FORMATTER by lazy {
28+
DateTimeFormatter.ofPattern(
29+
"yyyy-MM-dd HH:mm:ss",
30+
Locale.ROOT
31+
).withZone(ZoneOffset.UTC)
32+
}
33+
34+
private val DEFAULT_DATE_TIME_STRING_FORMATTER by lazy {
35+
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ROOT).withZone(ZoneOffset.UTC)
36+
}
37+
38+
private fun oracleDateTimeLiteral(instant: Instant) =
39+
"TO_TIMESTAMP('${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(instant)}', 'YYYY-MM-DD HH24:MI:SS.FF3')"
40+
41+
private fun formatterForDateString(date: String) = dateTimeWithFractionFormat(
42+
date.substringAfterLast('.', "").length
43+
)
44+
45+
private fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter {
46+
val baseFormat = "yyyy-MM-dd HH:mm:ss"
47+
val newFormat = if (fraction in 1..9) {
48+
(1..fraction).joinToString(prefix = "$baseFormat.", separator = "") { "S" }
49+
} else {
50+
baseFormat
51+
}
52+
return DateTimeFormatter.ofPattern(newFormat).withLocale(Locale.ROOT).withZone(ZoneOffset.UTC)
53+
}
54+
55+
class ZonedDateTimeColumnType : ColumnType<ZonedDateTime>(), IDateColumnType {
56+
override val hasTimePart: Boolean = true
57+
override fun sqlType(): String = currentDialect.dataTypeProvider.timestampType()
58+
59+
override fun nonNullValueToString(value: ZonedDateTime): String {
60+
val instant = value.withZoneSameInstant(ZoneOffset.UTC).toInstant()
61+
62+
return when (val dialect = currentDialect) {
63+
is SQLiteDialect -> "'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(instant)}'"
64+
is OracleDialect -> oracleDateTimeLiteral(instant)
65+
is MysqlDialect -> {
66+
val formatter =
67+
if (dialect.isFractionDateTimeSupported()) MYSQL_FRACTION_DATE_TIME_STRING_FORMATTER else MYSQL_DATE_TIME_STRING_FORMATTER
68+
"'${formatter.format(instant)}'"
69+
}
70+
71+
else -> "'${DEFAULT_DATE_TIME_STRING_FORMATTER.format(instant)}'"
72+
}
73+
}
74+
75+
override fun valueFromDB(value: Any): ZonedDateTime = when (value) {
76+
is ZonedDateTime -> value
77+
is OffsetDateTime -> value.toLocalDateTime().atZone(ZoneId.systemDefault())
78+
is LocalDateTime -> value.atZone(ZoneId.systemDefault())
79+
is Timestamp -> value.toLocalDateTime().atZone(ZoneId.systemDefault())
80+
is Date -> value.toInstant().atZone(ZoneId.systemDefault())
81+
is Instant -> value.atZone(ZoneId.systemDefault())
82+
is Int -> longToZonedDateTime(value.toLong())
83+
is Long -> longToZonedDateTime(value)
84+
is String -> runCatching { ZonedDateTime.parse(value, formatterForDateString(value)) }
85+
.getOrElse {
86+
runCatching { Instant.parse(value).atZone(ZoneOffset.UTC) }
87+
.getOrElse { error("Cannot parse ZonedDateTime from String: $value") }
88+
}
89+
90+
else -> error("Unexpected value of type ZonedDateTime: $value of ${value::class.qualifiedName}")
91+
}
92+
93+
94+
override fun notNullValueToDB(value: ZonedDateTime): Any = when {
95+
currentDialect is SQLiteDialect -> SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(
96+
LocalDateTime.ofInstant(value.toInstant(), ZoneOffset.UTC)
97+
)
98+
99+
else -> {
100+
val datetime = LocalDateTime.ofInstant(value.toInstant(), ZoneOffset.UTC)
101+
Timestamp.valueOf(datetime)
102+
}
103+
}
104+
105+
override fun readObject(rs: ResultSet, index: Int): Any? {
106+
return if (currentDialect is OracleDialect) {
107+
rs.getObject(index, Timestamp::class.java)
108+
} else {
109+
super.readObject(rs, index)
110+
}
111+
}
112+
113+
override fun nonNullValueAsDefaultString(value: ZonedDateTime): String {
114+
val dialect = currentDialect
115+
return when {
116+
dialect is PostgreSQLDialect ->
117+
"'${
118+
SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(
119+
value.withZoneSameInstant(
120+
ZoneOffset.UTC
121+
)
122+
).trimEnd('0').trimEnd('.')
123+
}'::timestamp without time zone"
124+
125+
(dialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.Oracle ->
126+
"'${
127+
SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(
128+
value.withZoneSameInstant(
129+
ZoneOffset.UTC
130+
)
131+
).trimEnd('0').trimEnd('.')
132+
}'"
133+
134+
else -> super.nonNullValueAsDefaultString(value)
135+
}
136+
}
137+
138+
private fun longToZonedDateTime(value: Long) =
139+
ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneId.systemDefault())
140+
141+
companion object {
142+
internal val INSTANCE = ZonedDateTimeColumnType()
143+
}
144+
}
145+
146+
fun Table.zonedDateTime(name: String): Column<ZonedDateTime> =
147+
registerColumn(name, ZonedDateTimeColumnType())
148+
149+
150+
open class CurrentTimestampBase<T>(columnType: IColumnType<T & Any>) : Function<T>(columnType) {
151+
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
152+
+when {
153+
(currentDialect as? MysqlDialect)?.isFractionDateTimeSupported() == true -> "CURRENT_TIMESTAMP(6)"
154+
else -> "CURRENT_TIMESTAMP"
155+
}
156+
}
157+
}
158+
159+
object CurrentZonedDateTime : CurrentTimestampBase<ZonedDateTime>(ZonedDateTimeColumnType.INSTANCE)

0 commit comments

Comments
 (0)