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