Skip to content

Commit

Permalink
feat: Add time extension function for temporal expressions in Kotlin …
Browse files Browse the repository at this point in the history
…and Java (#2121)

- Added `time` extension functions for temporal expressions in Kotlin and Java.
- Modified `Time` function to make it work for all database dialects.
- Modified `JodaTimeMiscTableTest` and `JodaTimeDefaultsTest` to extend `DatabaseTestsBase` instead of `JodaTimeBaseTest` to match the way it is in the Kotlin and Java tests.
- H2 V1 is excluded from the tests because of a bug in the driver that messes up the fractional seconds.
  • Loading branch information
joc-a authored Jun 27, 2024
1 parent e53ea5d commit 3fafec1
Show file tree
Hide file tree
Showing 20 changed files with 136 additions and 23 deletions.
2 changes: 1 addition & 1 deletion detekt/detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ complexity:
TooManyFunctions:
thresholdInClasses: 40
thresholdInFiles: 100
thresholdInObjects: 27
thresholdInObjects: 28
thresholdInInterfaces: 12
CyclomaticComplexMethod:
threshold: 26
Expand Down
1 change: 1 addition & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -3786,6 +3786,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider {
public fun stdDevSamp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V
public fun substring (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/String;)V
public static synthetic fun substring$default (Lorg/jetbrains/exposed/sql/vendors/FunctionProvider;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/String;ILjava/lang/Object;)V
public fun time (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V
public fun update (Lorg/jetbrains/exposed/sql/Join;Ljava/util/List;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String;
public fun update (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String;
public fun upsert (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;[Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ abstract class FunctionProvider {
append(")")
}

/**
* SQL function that extracts the time field from a given temporal expression.
*
* @param expr Expression to extract the year from.
* @param queryBuilder Query builder to append the SQL function to.
*/
open fun <T> time(expr: Expression<T>, queryBuilder: QueryBuilder) {
throw UnsupportedByDialectException(
"There's no generic SQL for TIME. There must be a vendor-specific implementation.", currentDialect
)
}

/**
* SQL function that extracts the year field from a given date.
*
Expand Down Expand Up @@ -829,7 +841,9 @@ abstract class FunctionProvider {
}

/** Appends optional parameters to an EXPLAIN query. */
protected open fun StringBuilder.appendOptionsToExplain(options: String) { append("$options ") }
protected open fun StringBuilder.appendOptionsToExplain(options: String) {
append("$options ")
}

/**
* Returns the SQL command that performs an insert, update, or delete, and also returns data from any modified rows.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ internal object H2FunctionProvider : FunctionProvider() {
override fun <T> date(expr: Expression<T>, queryBuilder: QueryBuilder) = queryBuilder {
append("CAST(", expr, " AS DATE)")
}

override fun <T> time(expr: Expression<T>, queryBuilder: QueryBuilder) = queryBuilder {
append("FORMATDATETIME(", expr, ", 'HH:mm:ss.SSSSSSSSS')")
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ internal open class MysqlFunctionProvider : FunctionProvider() {
toString()
}
}

override fun <T> time(expr: Expression<T>, queryBuilder: QueryBuilder) = queryBuilder {
append("SUBSTRING_INDEX(", expr, ", ' ', -1)")
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ internal object OracleFunctionProvider : FunctionProvider() {
append("CAST(", expr, " AS DATE)")
}

override fun <T> time(expr: Expression<T>, queryBuilder: QueryBuilder) = queryBuilder {
append("('1970-01-01 ' || TO_CHAR(", expr, ", 'HH24:MI:SS.FF6'))")
}

override fun <T> year(expr: Expression<T>, queryBuilder: QueryBuilder): Unit = queryBuilder {
append("Extract(YEAR FROM ")
append(expr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() {
append("CAST(", expr, " AS DATE)")
}

override fun <T> time(expr: Expression<T>, queryBuilder: QueryBuilder) = queryBuilder {
append("TO_CHAR(", expr, ", 'HH24:MI:SS.US')")
}

override fun <T> year(expr: Expression<T>, queryBuilder: QueryBuilder): Unit = queryBuilder {
append("Extract(YEAR FROM ")
append(expr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ internal object SQLServerFunctionProvider : FunctionProvider() {
append("CAST(", expr, " AS DATE)")
}

override fun <T> time(expr: Expression<T>, queryBuilder: QueryBuilder) = queryBuilder {
append("SUBSTRING(CONVERT(NVARCHAR, ", expr, ", 121), 12, 15)")
}

override fun <T> year(expr: Expression<T>, queryBuilder: QueryBuilder): Unit = queryBuilder {
append("DATEPART(YEAR, ", expr, ")")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ internal object SQLiteFunctionProvider : FunctionProvider() {
queryBuilder: QueryBuilder
): Unit = TransactionManager.current().throwUnsupportedException("SQLite doesn't provide built in REGEXP expression, use LIKE instead.")

override fun <T> time(expr: Expression<T>, queryBuilder: QueryBuilder) = queryBuilder {
append("SUBSTR(", expr, ", INSTR(", expr, ", ' ') + 1, LENGTH(", expr, ") - INSTR(", expr, ", ' ') - 1)")
}

override fun <T> year(expr: Expression<T>, queryBuilder: QueryBuilder): Unit = queryBuilder {
append("STRFTIME('%Y',")
append(expr)
Expand Down
1 change: 1 addition & 0 deletions exposed-java-time/api/exposed-java-time.api
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public final class org/jetbrains/exposed/sql/javatime/JavaDateFunctionsKt {
public static final fun minute (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Minute;
public static final fun month (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Month;
public static final fun second (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Second;
public static final fun time (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Time;
public static final fun timeLiteral (Ljava/time/LocalTime;)Lorg/jetbrains/exposed/sql/LiteralOp;
public static final fun timeParam (Ljava/time/LocalTime;)Lorg/jetbrains/exposed/sql/Expression;
public static final fun timestampLiteral (Ljava/time/Instant;)Lorg/jetbrains/exposed/sql/LiteralOp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,14 @@ private val MYSQL_FRACTION_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy {
).withZone(ZoneId.of("UTC"))
}

private fun formatterForDateString(date: String) = dateTimeWithFractionFormat(date.substringAfterLast('.', "").length)
private fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter {
val baseFormat = "yyyy-MM-d HH:mm:ss"
private fun formatterForDateString(date: String) = dateTimeWithFractionFormat(
date,
date.substringAfterLast('.', "").length
)

private fun dateTimeWithFractionFormat(date: String, fraction: Int): DateTimeFormatter {
val containsDatePart = date.contains("T") || date.contains(" ")
val baseFormat = if (containsDatePart) "yyyy-MM-dd HH:mm:ss" else "HH:mm:ss"
val newFormat = if (fraction in 1..9) {
(1..fraction).joinToString(prefix = "$baseFormat.", separator = "") { "S" }
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ class Date<T : Temporal?>(val expr: Expression<T>) : Function<LocalDate>(JavaLoc

/** Represents an SQL function that extracts the time part from a given temporal [expr]. */
class Time<T : Temporal?>(val expr: Expression<T>) : Function<LocalTime>(JavaLocalTimeColumnType.INSTANCE) {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("Time(", expr, ")") }
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
val dialect = currentDialect
val functionProvider = when (dialect.h2Mode) {
H2Dialect.H2CompatibilityMode.SQLServer, H2Dialect.H2CompatibilityMode.PostgreSQL ->
(dialect as H2Dialect).originalFunctionProvider
else -> dialect.functionProvider
}
functionProvider.time(expr, queryBuilder)
}
}

/**
Expand Down Expand Up @@ -150,6 +158,9 @@ class Second<T : Temporal?>(val expr: Expression<T>) : Function<Int>(IntegerColu
/** Returns the date from this temporal expression. */
fun <T : Temporal?> Expression<T>.date(): Date<T> = Date(this)

/** Returns the time from this temporal expression. */
fun <T : Temporal?> Expression<T>.time(): Time<T> = Time(this)

/** Returns the year from this temporal expression, as an integer. */
fun <T : Temporal?> Expression<T>.year(): Year<T> = Year(this)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import java.time.*
import java.time.temporal.Temporal
import kotlin.test.assertEquals

open class JavaTimeBaseTest : DatabaseTestsBase() {
class JavaTimeTests : DatabaseTestsBase() {

private val timestampWithTimeZoneUnsupportedDB = TestDB.ALL_MARIADB + TestDB.MYSQL_V5

Expand Down Expand Up @@ -453,15 +453,15 @@ open class JavaTimeBaseTest : DatabaseTestsBase() {
val timestampWithTimeZone = timestampWithTimeZone("timestamptz-column")
}

withDb(excludeSettings = timestampWithTimeZoneUnsupportedDB) {
withDb(excludeSettings = timestampWithTimeZoneUnsupportedDB + TestDB.ALL_H2_V1) { testDb ->
try {
// UTC time zone
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(ZoneOffset.UTC))
assertEquals("UTC", ZoneId.systemDefault().id)

SchemaUtils.create(testTable)

val now = OffsetDateTime.parse("2023-05-04T05:04:01.700+00:00")
val now = OffsetDateTime.parse("2023-05-04T05:04:01.123123123+00:00")
val nowId = testTable.insertAndGetId {
it[timestampWithTimeZone] = now
}
Expand All @@ -472,6 +472,20 @@ open class JavaTimeBaseTest : DatabaseTestsBase() {
.single()[testTable.timestampWithTimeZone.date()]
)

val expectedTime =
when (testDb) {
TestDB.SQLITE -> OffsetDateTime.parse("2023-05-04T05:04:01.123+00:00")
TestDB.MYSQL_V8, TestDB.SQLSERVER,
in TestDB.ALL_ORACLE_LIKE,
in TestDB.ALL_POSTGRES_LIKE -> OffsetDateTime.parse("2023-05-04T05:04:01.123123+00:00")
else -> now
}.toLocalTime()
assertEquals(
expectedTime,
testTable.select(testTable.timestampWithTimeZone.time()).where { testTable.id eq nowId }
.single()[testTable.timestampWithTimeZone.time()]
)

assertEquals(
now.month.value,
testTable.select(testTable.timestampWithTimeZone.month()).where { testTable.id eq nowId }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.jodatime.*
import org.jetbrains.exposed.sql.statements.BatchDataInconsistentException
import org.jetbrains.exposed.sql.statements.BatchInsertStatement
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.constraintNamePart
import org.jetbrains.exposed.sql.tests.currentDialectTest
Expand All @@ -32,7 +33,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class JodaTimeDefaultsTest : JodaTimeBaseTest() {
class JodaTimeDefaultsTest : DatabaseTestsBase() {
object TableWithDBDefault : IntIdTable() {
var cIndex = 0
val field = varchar("field", 100)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package org.jetbrains.exposed
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.jodatime.date
import org.jetbrains.exposed.sql.jodatime.datetime
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.shared.MiscTable
import org.jetbrains.exposed.sql.tests.shared.checkInsert
Expand All @@ -23,7 +24,7 @@ object Misc : MiscTable() {
val tn = datetime("tn").nullable()
}

class JodaTimeMiscTableTest : JodaTimeBaseTest() {
class JodaTimeMiscTableTest : DatabaseTestsBase() {
@Test
fun testInsert01() {
val tbl = Misc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import org.joda.time.DateTimeZone
import org.junit.Test
import kotlin.test.assertEquals

open class JodaTimeBaseTest : DatabaseTestsBase() {
class JodaTimeTests : DatabaseTestsBase() {
init {
DateTimeZone.setDefault(DateTimeZone.UTC)
}
Expand Down
4 changes: 4 additions & 0 deletions exposed-kotlin-datetime/api/exposed-kotlin-datetime.api
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions
public static final fun InstantMonthFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun InstantSecondExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun InstantSecondFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun InstantTimeExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun InstantTimeFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun InstantYearExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun InstantYearFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
Expand All @@ -67,6 +68,7 @@ public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions
public static final fun LocalDateTimeDateFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeDayExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeDayFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeHourExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeHourFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
Expand All @@ -76,6 +78,7 @@ public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions
public static final fun LocalDateTimeMonthFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeSecondExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeSecondFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeTimeExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeTimeFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeYearExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun LocalDateTimeYearFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
Expand All @@ -93,6 +96,7 @@ public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions
public static final fun OffsetDateTimeMonthFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun OffsetDateTimeSecondExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun OffsetDateTimeSecondFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun OffsetDateTimeTimeExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun OffsetDateTimeTimeFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun OffsetDateTimeYearExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
public static final fun OffsetDateTimeYearFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,14 @@ private val MYSQL_OFFSET_DATE_TIME_AS_DEFAULT_FORMATTER by lazy {
).withZone(ZoneId.of("UTC"))
}

private fun formatterForDateString(date: String) = dateTimeWithFractionFormat(date.substringAfterLast('.', "").length)
private fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter {
val baseFormat = "yyyy-MM-d HH:mm:ss"
private fun formatterForDateString(date: String) = dateTimeWithFractionFormat(
date,
date.substringAfterLast('.', "").length
)

private fun dateTimeWithFractionFormat(date: String, fraction: Int): DateTimeFormatter {
val containsDatePart = date.contains("T") || date.contains(" ")
val baseFormat = if (containsDatePart) "yyyy-MM-dd HH:mm:ss" else "HH:mm:ss"
val newFormat = if (fraction in 1..9) {
(1..fraction).joinToString(prefix = "$baseFormat.", separator = "") { "S" }
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.Function
import org.jetbrains.exposed.sql.vendors.H2Dialect
import org.jetbrains.exposed.sql.vendors.MysqlDialect
import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect
import org.jetbrains.exposed.sql.vendors.SQLServerDialect
import org.jetbrains.exposed.sql.vendors.currentDialect
import org.jetbrains.exposed.sql.vendors.h2Mode
Expand Down Expand Up @@ -41,10 +40,13 @@ fun <T : OffsetDateTime?> Date(expr: Expression<T>): Function<LocalDate> = DateI

internal class TimeInternal(val expr: Expression<*>) : Function<LocalTime>(KotlinLocalTimeColumnType.INSTANCE) {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
when (currentDialect) {
is PostgreSQLDialect -> append(expr, "::time")
else -> append("Time(", expr, ")")
val dialect = currentDialect
val functionProvider = when (dialect.h2Mode) {
H2Dialect.H2CompatibilityMode.SQLServer, H2Dialect.H2CompatibilityMode.PostgreSQL ->
(dialect as H2Dialect).originalFunctionProvider
else -> dialect.functionProvider
}
functionProvider.time(expr, queryBuilder)
}
}

Expand Down Expand Up @@ -292,6 +294,22 @@ fun <T : Instant?> Expression<T>.date() = Date(this)
@JvmName("OffsetDateTimeDateExt")
fun <T : OffsetDateTime?> Expression<T>.date() = Date(this)

/** Returns the time from this date expression. */
@JvmName("LocalDateTimeExt")
fun <T : LocalDate?> Expression<T>.time() = Time(this)

/** Returns the time from this datetime expression. */
@JvmName("LocalDateTimeTimeExt")
fun <T : LocalDateTime?> Expression<T>.time() = Time(this)

/** Returns the time from this timestamp expression. */
@JvmName("InstantTimeExt")
fun <T : Instant?> Expression<T>.time() = Time(this)

/** Returns the time from this timestampWithTimeZone expression. */
@JvmName("OffsetDateTimeTimeExt")
fun <T : OffsetDateTime?> Expression<T>.time() = Time(this)

/** Returns the year from this date expression, as an integer. */
@JvmName("LocalDateYearExt")
fun <T : LocalDate?> Expression<T>.year() = Year(this)
Expand Down
Loading

0 comments on commit 3fafec1

Please sign in to comment.