Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for a custom ZoneId in exposed-java-time #1473

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,47 @@ import java.time.*
import java.time.format.DateTimeFormatter
import java.util.*

internal val DEFAULT_DATE_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_DATE.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
}
internal val DEFAULT_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
}
internal val SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSS",
Locale.ROOT
).withZone(ZoneId.systemDefault())
}

internal val ORACLE_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ofPattern(
"1900-01-01 HH:mm:ss",
Locale.ROOT
).withZone(ZoneOffset.UTC)
}

internal val DEFAULT_TIME_STRING_FORMATTER by lazy {
DateTimeFormatter.ISO_LOCAL_TIME.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
}

internal fun formatterForDateString(date: String) = dateTimeWithFractionFormat(date.substringAfterLast('.', "").length)
internal fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter {
private fun defaultDateStringFormatter(zoneId: ZoneId) = DateTimeFormatter.ISO_LOCAL_DATE.withLocale(Locale.ROOT).withZone(zoneId)
private fun defaultDateTimeStringFormatter(zoneId: ZoneId) = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ROOT).withZone(zoneId)
private fun sqliteAndOracleDateTimeStringFormatter(zoneId: ZoneId) = DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss.SSS",
Locale.ROOT
).withZone(zoneId)
private fun oracleTimeStringFormatter(zoneId: ZoneId) = DateTimeFormatter.ofPattern(
"1900-01-01 HH:mm:ss",
Locale.ROOT
).withZone(zoneId)
private fun defaultTimeStringFormatter(zoneId: ZoneId) = DateTimeFormatter.ISO_LOCAL_TIME.withLocale(Locale.ROOT).withZone(zoneId)

internal fun formatterForDateString(zoneId: ZoneId, date: String) = dateTimeWithFractionFormat(zoneId, date.substringAfterLast('.', "").length)
internal fun dateTimeWithFractionFormat(zoneId: ZoneId, fraction: Int): DateTimeFormatter {
val baseFormat = "yyyy-MM-d HH:mm:ss"
val newFormat = if (fraction in 1..9) {
(1..fraction).joinToString(prefix = "$baseFormat.", separator = "") { "S" }
} else {
baseFormat
}
return DateTimeFormatter.ofPattern(newFormat).withLocale(Locale.ROOT).withZone(ZoneId.systemDefault())
return DateTimeFormatter.ofPattern(newFormat).withLocale(Locale.ROOT).withZone(zoneId)
}

internal val LocalDate.millis get() = atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000
internal fun LocalDate.millis(zoneId: ZoneId) = atStartOfDay(zoneId).toEpochSecond() * 1000

class JavaLocalDateColumnType : ColumnType(), IDateColumnType {
class JavaLocalDateColumnType(private val zoneId: ZoneId) : ColumnType(), IDateColumnType {
override val hasTimePart: Boolean = false
private val defaultDateStringFormatter = defaultDateStringFormatter(zoneId)

override fun sqlType(): String = "DATE"

override fun nonNullValueToString(value: Any): String {
val instant = when (value) {
is String -> return value
is LocalDate -> Instant.from(value.atStartOfDay(ZoneId.systemDefault()))
is LocalDate -> Instant.from(value.atStartOfDay(zoneId))
is java.sql.Date -> Instant.ofEpochMilli(value.time)
is java.sql.Timestamp -> Instant.ofEpochSecond(value.time / 1000, value.nanos.toLong())
else -> error("Unexpected value: $value of ${value::class.qualifiedName}")
}

return "'${DEFAULT_DATE_STRING_FORMATTER.format(instant)}'"
return "'${defaultDateStringFormatter.format(instant)}'"
}

override fun valueFromDB(value: Any): Any = when (value) {
Expand All @@ -80,33 +69,31 @@ class JavaLocalDateColumnType : ColumnType(), IDateColumnType {
}

override fun notNullValueToDB(value: Any) = when {
value is LocalDate -> java.sql.Date(value.millis)
value is LocalDate -> java.sql.Date(value.millis(zoneId))
else -> value
}

private fun longToLocalDate(instant: Long) = Instant.ofEpochMilli(instant).atZone(ZoneId.systemDefault()).toLocalDate()

companion object {
internal val INSTANCE = JavaLocalDateColumnType()
}
private fun longToLocalDate(instant: Long) = Instant.ofEpochMilli(instant).atZone(zoneId).toLocalDate()
}

class JavaLocalDateTimeColumnType : ColumnType(), IDateColumnType {
class JavaLocalDateTimeColumnType(private val zoneId: ZoneId) : ColumnType(), IDateColumnType {
override val hasTimePart: Boolean = true
override fun sqlType(): String = currentDialect.dataTypeProvider.dateTimeType()
private val sqliteAndOracleDateTimeStringFormatter = sqliteAndOracleDateTimeStringFormatter(zoneId)
private val defaultDateTimeStringFormatter = defaultDateTimeStringFormatter(zoneId)

override fun nonNullValueToString(value: Any): String {
val instant = when (value) {
is String -> return value
is LocalDateTime -> Instant.from(value.atZone(ZoneId.systemDefault()))
is LocalDateTime -> Instant.from(value.atZone(zoneId))
is java.sql.Date -> Instant.ofEpochMilli(value.time)
is java.sql.Timestamp -> Instant.ofEpochSecond(value.time / 1000, value.nanos.toLong())
else -> error("Unexpected value: $value of ${value::class.qualifiedName}")
}

return when (currentDialect) {
is SQLiteDialect, is OracleDialect -> "'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(instant)}'"
else -> "'${DEFAULT_DATE_TIME_STRING_FORMATTER.format(instant)}'"
is SQLiteDialect, is OracleDialect -> "'${sqliteAndOracleDateTimeStringFormatter.format(instant)}'"
else -> "'${defaultDateTimeStringFormatter.format(instant)}'"
}
}

Expand All @@ -116,47 +103,42 @@ class JavaLocalDateTimeColumnType : ColumnType(), IDateColumnType {
is java.sql.Timestamp -> longToLocalDateTime(value.time / 1000, value.nanos.toLong())
is Int -> longToLocalDateTime(value.toLong())
is Long -> longToLocalDateTime(value)
is String -> LocalDateTime.parse(value, formatterForDateString(value))
is Instant -> LocalDateTime.from(value)
is String -> formatterForDateString(zoneId, value).parse(value, LocalDateTime::from)
else -> valueFromDB(value.toString())
}

override fun notNullValueToDB(value: Any): Any = when {
value is LocalDateTime && currentDialect is SQLiteDialect ->
SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value.atZone(ZoneId.systemDefault()))
value is LocalDateTime -> {
val instant = value.atZone(ZoneId.systemDefault()).toInstant()
java.sql.Timestamp(instant.toEpochMilli()).apply { nanos = instant.nano }
}
value is LocalDateTime && currentDialect is SQLiteDialect -> sqliteAndOracleDateTimeStringFormatter.format(value.atZone(zoneId))
value is LocalDateTime -> value
else -> value
}

private fun longToLocalDateTime(millis: Long) = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault())
private fun longToLocalDateTime(millis: Long) = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), zoneId)
private fun longToLocalDateTime(seconds: Long, nanos: Long) =
LocalDateTime.ofInstant(Instant.ofEpochSecond(seconds, nanos), ZoneId.systemDefault())

companion object {
internal val INSTANCE = JavaLocalDateTimeColumnType()
}
LocalDateTime.ofInstant(Instant.ofEpochSecond(seconds, nanos), zoneId)
}

class JavaLocalTimeColumnType : ColumnType(), IDateColumnType {
class JavaLocalTimeColumnType(private val zoneId: ZoneId) : ColumnType(), IDateColumnType {
override val hasTimePart: Boolean = true
private val oracleTimeStringFormatter = oracleTimeStringFormatter(zoneId)
private val defaultTimeStringFormatter = defaultTimeStringFormatter(zoneId)

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

override fun nonNullValueToString(value: Any): String {
val instant = when (value) {
is String -> return value
is LocalTime -> value
is java.sql.Time -> Instant.ofEpochMilli(value.time).atZone(ZoneId.systemDefault())
is java.sql.Timestamp -> Instant.ofEpochMilli(value.time).atZone(ZoneId.systemDefault())
is java.sql.Time -> Instant.ofEpochMilli(value.time).atZone(zoneId)
is java.sql.Timestamp -> Instant.ofEpochMilli(value.time).atZone(zoneId)
else -> error("Unexpected value: $value of ${value::class.qualifiedName}")
}

val formatter = if (currentDialect is OracleDialect) {
ORACLE_TIME_STRING_FORMATTER
oracleTimeStringFormatter
} else {
DEFAULT_TIME_STRING_FORMATTER
defaultTimeStringFormatter
}
return "'${formatter.format(instant)}'"
}
Expand All @@ -169,9 +151,9 @@ class JavaLocalTimeColumnType : ColumnType(), IDateColumnType {
is Long -> longToLocalTime(value)
is String -> {
val formatter = if (currentDialect is OracleDialect) {
formatterForDateString(value)
formatterForDateString(zoneId, value)
} else {
DEFAULT_TIME_STRING_FORMATTER
defaultTimeStringFormatter
}
LocalTime.parse(value, formatter)
}
Expand All @@ -183,16 +165,14 @@ class JavaLocalTimeColumnType : ColumnType(), IDateColumnType {
else -> value
}

private fun longToLocalTime(millis: Long) = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalTime()

companion object {
internal val INSTANCE = JavaLocalTimeColumnType()
}
private fun longToLocalTime(millis: Long) = Instant.ofEpochMilli(millis).atZone(zoneId).toLocalTime()
}

class JavaInstantColumnType : ColumnType(), IDateColumnType {
class JavaInstantColumnType(private val zoneId: ZoneId) : ColumnType(), IDateColumnType {
override val hasTimePart: Boolean = true
override fun sqlType(): String = currentDialect.dataTypeProvider.dateTimeType()
private val sqliteAndOracleDateTimeStringFormatter = sqliteAndOracleDateTimeStringFormatter(zoneId)
private val defaultDateTimeStringFormatter = defaultDateTimeStringFormatter(zoneId)

override fun nonNullValueToString(value: Any): String {
val instant = when (value) {
Expand All @@ -203,32 +183,31 @@ class JavaInstantColumnType : ColumnType(), IDateColumnType {
}

return when (currentDialect) {
is OracleDialect -> "'${SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(instant)}'"
else -> "'${DEFAULT_DATE_TIME_STRING_FORMATTER.format(instant)}'"
is OracleDialect, is SQLiteDialect -> "'${sqliteAndOracleDateTimeStringFormatter.format(instant)}'"
else -> "'${defaultDateTimeStringFormatter.format(instant)}'"
}
}

override fun valueFromDB(value: Any): Instant = when (value) {
is java.sql.Timestamp -> value.toInstant()
is String -> Instant.parse(value)
is Instant -> value
is String -> defaultDateTimeStringFormatter.parse(value, Instant::from)
else -> valueFromDB(value.toString())
}

override fun readObject(rs: ResultSet, index: Int): Any? {
return rs.getTimestamp(index)
// https://stackoverflow.com/questions/60793737/sqlite-jdbc-3-30-1-latest-does-not-support-java-time
if (currentDialect is SQLiteDialect)
return rs.getString(index)?.let { sqliteAndOracleDateTimeStringFormatter.parse(it, LocalDateTime::from) }
return rs.getObject(index, LocalDateTime::class.java)
}

override fun notNullValueToDB(value: Any): Any = when {
value is Instant && currentDialect is SQLiteDialect ->
SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER.format(value)
value is Instant ->
java.sql.Timestamp.from(value)
sqliteAndOracleDateTimeStringFormatter.format(value)
value is Instant -> LocalDateTime.ofInstant(value, zoneId)
else -> value
}

companion object {
internal val INSTANCE = JavaInstantColumnType()
}
}

class JavaDurationColumnType : ColumnType() {
Expand Down Expand Up @@ -273,37 +252,85 @@ class JavaDurationColumnType : ColumnType() {
/**
* A date column to store a date.
*
* @param name The column name
*/
@Deprecated("This uses the ZoneId.systemDefault() as the ZoneId, which can cause inconsistencies if you change your machine is in a different timezone. Please explictly define the ZoneId!",
ReplaceWith("date(name, ZoneId.systemDefault())", "java.time.ZoneId")
)
fun Table.date(name: String): Column<LocalDate> = date(name, ZoneId.systemDefault())

/**
* A datetime column to store both a date and a time.
*
* @param name The column name
*/
@Deprecated("This uses the ZoneId.systemDefault() as the ZoneId, which can cause inconsistencies if you change your machine is in a different timezone. Please explictly define the ZoneId!",
ReplaceWith("datetime(name, ZoneId.systemDefault())", "java.time.ZoneId")
)
fun Table.datetime(name: String): Column<LocalDateTime> = datetime(name, ZoneId.systemDefault())

/**
* A time column to store a time.
*
* Doesn't return nanos from database.
*
* @param name The column name
* @author Maxim Vorotynsky
*/
@Deprecated("This uses the ZoneId.systemDefault() as the ZoneId, which can cause inconsistencies if you change your machine is in a different timezone. Please explictly define the ZoneId!",
ReplaceWith("time(name, ZoneId.systemDefault())", "java.time.ZoneId")
)
fun Table.time(name: String): Column<LocalTime> = time(name, ZoneId.systemDefault())

/**
* A timestamp column to store both a date and a time.
*
* @param name The column name
*/
@Deprecated("This uses the ZoneId.systemDefault() as the ZoneId, which can cause inconsistencies if you change your machine is in a different timezone. Please explictly define the ZoneId!",
ReplaceWith("timestamp(name, ZoneId.systemDefault())", "java.time.ZoneId")
)
fun Table.timestamp(name: String): Column<Instant> = timestamp(name, ZoneId.systemDefault())


/**
* A date column to store a date.
*
* @param name The column name
* @param zoneId The zone ID
*/
fun Table.date(name: String): Column<LocalDate> = registerColumn(name, JavaLocalDateColumnType())
fun Table.date(name: String, zoneId: ZoneId): Column<LocalDate> = registerColumn(name, JavaLocalDateColumnType(zoneId))

/**
* A datetime column to store both a date and a time.
*
* @param name The column name
* @param zoneId The zone ID
*/
fun Table.datetime(name: String): Column<LocalDateTime> = registerColumn(name, JavaLocalDateTimeColumnType())
fun Table.datetime(name: String, zoneId: ZoneId): Column<LocalDateTime> = registerColumn(name, JavaLocalDateTimeColumnType(zoneId))

/**
* A time column to store a time.
*
* Doesn't return nanos from database.
*
* @param name The column name
* @param zoneId The zone ID
* @author Maxim Vorotynsky
*/
fun Table.time(name: String): Column<LocalTime> = registerColumn(name, JavaLocalTimeColumnType())
fun Table.time(name: String, zoneId: ZoneId): Column<LocalTime> = registerColumn(name, JavaLocalTimeColumnType(zoneId))

/**
* A timestamp column to store both a date and a time.
*
* @param name The column name
* @param zoneId The zone ID
*/
fun Table.timestamp(name: String): Column<Instant> = registerColumn(name, JavaInstantColumnType())
fun Table.timestamp(name: String, zoneId: ZoneId): Column<Instant> = registerColumn(name, JavaInstantColumnType(zoneId))

/**
* A date column to store a duration.
*
* @param name The column name
*/
fun Table.duration(name: String): Column<Duration> = registerColumn(name, JavaDurationColumnType())
fun Table.duration(name: String): Column<Duration> = registerColumn(name, JavaDurationColumnType.INSTANCE)
Loading