Skip to content

Commit 1e97cb9

Browse files
committed
Add support for OffsetDateTime and ZonedDateTime column types in database schema
1 parent 9d55157 commit 1e97cb9

7 files changed

Lines changed: 364 additions & 2 deletions

File tree

api/surf-database-r2dbc.api

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
public final class dev/slne/surf/database/DatabaseApi {
2+
public static final field Companion Ldev/slne/surf/database/DatabaseApi$Companion;
3+
public final fun getDatabase ()Lorg/jetbrains/exposed/v1/r2dbc/R2dbcDatabase;
4+
public final fun shutdown ()V
5+
}
6+
7+
public final class dev/slne/surf/database/DatabaseApi$Companion {
8+
public final fun create (Lio/r2dbc/spi/ConnectionFactory;Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Lorg/slf4j/event/Level;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/database/DatabaseApi;
9+
public final fun create (Ljava/nio/file/Path;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/database/DatabaseApi;
10+
public static synthetic fun create$default (Ldev/slne/surf/database/DatabaseApi$Companion;Lio/r2dbc/spi/ConnectionFactory;Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Lorg/slf4j/event/Level;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/database/DatabaseApi;
11+
public static synthetic fun create$default (Ldev/slne/surf/database/DatabaseApi$Companion;Ljava/nio/file/Path;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/database/DatabaseApi;
12+
}
13+
14+
public abstract interface annotation class dev/slne/surf/database/TestOnlyDatabaseApi : java/lang/annotation/Annotation {
15+
}
16+
17+
public final class dev/slne/surf/database/columns/CharUuidColumnType : org/jetbrains/exposed/v1/core/ColumnType {
18+
public static final field Companion Ldev/slne/surf/database/columns/CharUuidColumnType$Companion;
19+
public fun <init> ()V
20+
public synthetic fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String;
21+
public fun nonNullValueToString (Ljava/util/UUID;)Ljava/lang/String;
22+
public synthetic fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object;
23+
public fun notNullValueToDB (Ljava/util/UUID;)Ljava/lang/Object;
24+
public fun sqlType ()Ljava/lang/String;
25+
public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
26+
public fun valueFromDB (Ljava/lang/Object;)Ljava/util/UUID;
27+
}
28+
29+
public final class dev/slne/surf/database/columns/CharUuidColumnType$Companion {
30+
}
31+
32+
public final class dev/slne/surf/database/columns/CharUuidColumnTypeKt {
33+
public static final fun charUuid (Lorg/jetbrains/exposed/v1/core/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/v1/core/Column;
34+
}
35+
36+
public final class dev/slne/surf/database/columns/ComponentColumnTypeKt {
37+
public static final fun component (Lorg/jetbrains/exposed/v1/core/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/v1/core/Column;
38+
}
39+
40+
public final class dev/slne/surf/database/columns/InetAddressColumnType : org/jetbrains/exposed/v1/core/ColumnType {
41+
public fun <init> ()V
42+
public synthetic fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object;
43+
public fun notNullValueToDB (Ljava/net/InetAddress;)Ljava/lang/Object;
44+
public fun setParameter (Lorg/jetbrains/exposed/v1/core/statements/api/PreparedStatementApi;ILjava/lang/Object;)V
45+
public fun sqlType ()Ljava/lang/String;
46+
public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
47+
public fun valueFromDB (Ljava/lang/Object;)Ljava/net/InetAddress;
48+
}
49+
50+
public final class dev/slne/surf/database/columns/InetAddressColumnTypeKt {
51+
public static final fun inet (Lorg/jetbrains/exposed/v1/core/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/v1/core/Column;
52+
}
53+
54+
public final class dev/slne/surf/database/columns/NativeUuidColumnType : org/jetbrains/exposed/v1/core/ColumnType {
55+
public fun <init> ()V
56+
public fun sqlType ()Ljava/lang/String;
57+
public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
58+
public fun valueFromDB (Ljava/lang/Object;)Ljava/util/UUID;
59+
}
60+
61+
public final class dev/slne/surf/database/columns/NativeUuidColumnTypeKt {
62+
public static final fun nativeUuid (Lorg/jetbrains/exposed/v1/core/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/v1/core/Column;
63+
}
64+
65+
public final class dev/slne/surf/database/columns/time/CurrentOffsetDateTime : dev/slne/surf/database/columns/time/CurrentTimestampBase {
66+
public static final field INSTANCE Ldev/slne/surf/database/columns/time/CurrentOffsetDateTime;
67+
}
68+
69+
public class dev/slne/surf/database/columns/time/CurrentTimestampBase : org/jetbrains/exposed/v1/core/Function {
70+
public fun <init> (Lorg/jetbrains/exposed/v1/core/IColumnType;)V
71+
public fun toQueryBuilder (Lorg/jetbrains/exposed/v1/core/QueryBuilder;)V
72+
}
73+
74+
public final class dev/slne/surf/database/columns/time/CurrentZonedDateTime : dev/slne/surf/database/columns/time/CurrentTimestampBase {
75+
public static final field INSTANCE Ldev/slne/surf/database/columns/time/CurrentZonedDateTime;
76+
}
77+
78+
public final class dev/slne/surf/database/columns/time/OffsetDateTimeColumnType : dev/slne/surf/database/columns/time/UtcInstantDateTimeColumnType {
79+
public static final field Companion Ldev/slne/surf/database/columns/time/OffsetDateTimeColumnType$Companion;
80+
public fun <init> ()V
81+
public synthetic fun fromInstant (Ljava/time/Instant;)Ljava/lang/Object;
82+
public synthetic fun toInstant (Ljava/lang/Object;)Ljava/time/Instant;
83+
}
84+
85+
public final class dev/slne/surf/database/columns/time/OffsetDateTimeColumnType$Companion {
86+
}
87+
88+
public final class dev/slne/surf/database/columns/time/OffsetDateTimeColumnTypeKt {
89+
public static final fun offsetDateTime (Lorg/jetbrains/exposed/v1/core/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/v1/core/Column;
90+
}
91+
92+
public abstract class dev/slne/surf/database/columns/time/UtcInstantDateTimeColumnType : org/jetbrains/exposed/v1/core/ColumnType, org/jetbrains/exposed/v1/core/IDateColumnType {
93+
public fun <init> ()V
94+
protected abstract fun fromInstant (Ljava/time/Instant;)Ljava/lang/Object;
95+
public fun getHasTimePart ()Z
96+
public fun nonNullValueAsDefaultString (Ljava/lang/Object;)Ljava/lang/String;
97+
public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String;
98+
public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object;
99+
public fun readObject (Lorg/jetbrains/exposed/v1/core/statements/api/RowApi;I)Ljava/lang/Object;
100+
public fun sqlType ()Ljava/lang/String;
101+
protected abstract fun toInstant (Ljava/lang/Object;)Ljava/time/Instant;
102+
public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
103+
}
104+
105+
public final class dev/slne/surf/database/columns/time/ZonedDateTimeColumnType : dev/slne/surf/database/columns/time/UtcInstantDateTimeColumnType {
106+
public static final field Companion Ldev/slne/surf/database/columns/time/ZonedDateTimeColumnType$Companion;
107+
public fun <init> ()V
108+
public synthetic fun fromInstant (Ljava/time/Instant;)Ljava/lang/Object;
109+
public synthetic fun toInstant (Ljava/lang/Object;)Ljava/time/Instant;
110+
}
111+
112+
public final class dev/slne/surf/database/columns/time/ZonedDateTimeColumnType$Companion {
113+
}
114+
115+
public final class dev/slne/surf/database/columns/time/ZonedDateTimeColumnTypeKt {
116+
public static final fun zonedDateTime (Lorg/jetbrains/exposed/v1/core/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/v1/core/Column;
117+
}
118+
119+
public final class dev/slne/surf/database/logger/ComponentSqlLogger : org/jetbrains/exposed/v1/core/SqlLogger {
120+
public fun <init> (Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Lorg/slf4j/event/Level;)V
121+
public synthetic fun <init> (Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Lorg/slf4j/event/Level;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
122+
public fun log (Lorg/jetbrains/exposed/v1/core/statements/StatementContext;Lorg/jetbrains/exposed/v1/core/Transaction;)V
123+
}
124+
125+
public class dev/slne/surf/database/table/AuditableLongIdTable : org/jetbrains/exposed/v1/core/dao/id/ULongIdTable {
126+
public fun <init> ()V
127+
public fun <init> (Ljava/lang/String;)V
128+
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
129+
public final fun getCreatedAt ()Lorg/jetbrains/exposed/v1/core/Column;
130+
public final fun getUpdatedAt ()Lorg/jetbrains/exposed/v1/core/Column;
131+
}
132+

build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ plugins {
55
// id("dev.slne.surf.surfapi.gradle.standalone") version "1.21.11+" /* Uncomment to use tests */
66
}
77

8+
surfCoreApi {
9+
withApiValidation()
10+
}
11+
812
group = "dev.slne.surf"
913
version = findProperty("version") as String
1014

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ org.gradle.parallel=true
44
#org.gradle.caching=true
55
#org.gradle.configureondemand=true
66

7-
version=1.0.0-SNAPSHOT
7+
version=1.0.1-SNAPSHOT

gradle/wrapper/gradle-wrapper.properties

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

0 commit comments

Comments
 (0)