From 3fafec1a31f88b1ea4fda13fdf17c6dc182e5693 Mon Sep 17 00:00:00 2001 From: Jocelyne <38375996+joc-a@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:53:53 +0300 Subject: [PATCH] feat: Add time extension function for temporal expressions in Kotlin 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. --- detekt/detekt-config.yml | 2 +- exposed-core/api/exposed-core.api | 1 + .../exposed/sql/vendors/FunctionProvider.kt | 16 +++++++++++- .../org/jetbrains/exposed/sql/vendors/H2.kt | 4 +++ .../exposed/sql/vendors/MysqlDialect.kt | 4 +++ .../exposed/sql/vendors/OracleDialect.kt | 4 +++ .../exposed/sql/vendors/PostgreSQL.kt | 4 +++ .../exposed/sql/vendors/SQLServerDialect.kt | 4 +++ .../exposed/sql/vendors/SQLiteDialect.kt | 4 +++ exposed-java-time/api/exposed-java-time.api | 1 + .../sql/javatime/JavaDateColumnType.kt | 11 +++++--- .../exposed/sql/javatime/JavaDateFunctions.kt | 13 +++++++++- .../org/jetbrains/exposed/JavaTimeTests.kt | 20 +++++++++++--- .../jetbrains/exposed/JodaTimeDefaultsTest.kt | 3 ++- .../exposed/JodaTimeMiscTableTest.kt | 3 ++- .../org/jetbrains/exposed/JodaTimeTests.kt | 2 +- .../api/exposed-kotlin-datetime.api | 4 +++ .../kotlin/datetime/KotlinDateColumnType.kt | 11 +++++--- .../kotlin/datetime/KotlinDateFunctions.kt | 26 ++++++++++++++++--- .../sql/kotlin/datetime/KotlinTimeTests.kt | 22 +++++++++++++--- 20 files changed, 136 insertions(+), 23 deletions(-) diff --git a/detekt/detekt-config.yml b/detekt/detekt-config.yml index 134b846fe9..7cd99f1938 100644 --- a/detekt/detekt-config.yml +++ b/detekt/detekt-config.yml @@ -58,7 +58,7 @@ complexity: TooManyFunctions: thresholdInClasses: 40 thresholdInFiles: 100 - thresholdInObjects: 27 + thresholdInObjects: 28 thresholdInInterfaces: 12 CyclomaticComplexMethod: threshold: 26 diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index e96fa28049..5371ee0415 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -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; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt index aa618715ef..9269980c5b 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt @@ -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 time(expr: Expression, 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. * @@ -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. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt index 605faabb20..1d38802732 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt @@ -136,6 +136,10 @@ internal object H2FunctionProvider : FunctionProvider() { override fun date(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { append("CAST(", expr, " AS DATE)") } + + override fun time(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { + append("FORMATDATETIME(", expr, ", 'HH:mm:ss.SSSSSSSSS')") + } } /** diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt index 77509f8b30..cbd7109ed6 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt @@ -271,6 +271,10 @@ internal open class MysqlFunctionProvider : FunctionProvider() { toString() } } + + override fun time(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { + append("SUBSTRING_INDEX(", expr, ", ' ', -1)") + } } /** diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt index d74bdbf762..a1566eb1b4 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt @@ -135,6 +135,10 @@ internal object OracleFunctionProvider : FunctionProvider() { append("CAST(", expr, " AS DATE)") } + override fun time(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { + append("('1970-01-01 ' || TO_CHAR(", expr, ", 'HH24:MI:SS.FF6'))") + } + override fun year(expr: Expression, queryBuilder: QueryBuilder): Unit = queryBuilder { append("Extract(YEAR FROM ") append(expr) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index 0a8a25a6c4..874cab924e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -101,6 +101,10 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { append("CAST(", expr, " AS DATE)") } + override fun time(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { + append("TO_CHAR(", expr, ", 'HH24:MI:SS.US')") + } + override fun year(expr: Expression, queryBuilder: QueryBuilder): Unit = queryBuilder { append("Extract(YEAR FROM ") append(expr) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index 3b86bbb5c7..c5a3c175d3 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -119,6 +119,10 @@ internal object SQLServerFunctionProvider : FunctionProvider() { append("CAST(", expr, " AS DATE)") } + override fun time(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { + append("SUBSTRING(CONVERT(NVARCHAR, ", expr, ", 121), 12, 15)") + } + override fun year(expr: Expression, queryBuilder: QueryBuilder): Unit = queryBuilder { append("DATEPART(YEAR, ", expr, ")") } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt index 9c5a9927fa..268980d64f 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt @@ -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 time(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { + append("SUBSTR(", expr, ", INSTR(", expr, ", ' ') + 1, LENGTH(", expr, ") - INSTR(", expr, ", ' ') - 1)") + } + override fun year(expr: Expression, queryBuilder: QueryBuilder): Unit = queryBuilder { append("STRFTIME('%Y',") append(expr) diff --git a/exposed-java-time/api/exposed-java-time.api b/exposed-java-time/api/exposed-java-time.api index e796eda710..c3d05a5ef3 100644 --- a/exposed-java-time/api/exposed-java-time.api +++ b/exposed-java-time/api/exposed-java-time.api @@ -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; diff --git a/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt index ad78a59d19..d5779d695c 100644 --- a/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt +++ b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt @@ -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 { diff --git a/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt index 369e588758..bed3ef38d0 100644 --- a/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt +++ b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt @@ -24,7 +24,15 @@ class Date(val expr: Expression) : Function(JavaLoc /** Represents an SQL function that extracts the time part from a given temporal [expr]. */ class Time(val expr: Expression) : Function(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) + } } /** @@ -150,6 +158,9 @@ class Second(val expr: Expression) : Function(IntegerColu /** Returns the date from this temporal expression. */ fun Expression.date(): Date = Date(this) +/** Returns the time from this temporal expression. */ +fun Expression.time(): Time = Time(this) + /** Returns the year from this temporal expression, as an integer. */ fun Expression.year(): Year = Year(this) diff --git a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt index 0877a88601..1eba4258ce 100644 --- a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt +++ b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt @@ -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 @@ -453,7 +453,7 @@ 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)) @@ -461,7 +461,7 @@ open class JavaTimeBaseTest : DatabaseTestsBase() { 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 } @@ -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 } diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt index 26c5db6001..52c08c41da 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt @@ -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 @@ -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) diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt index cef53f0586..a094eb1489 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt @@ -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 @@ -23,7 +24,7 @@ object Misc : MiscTable() { val tn = datetime("tn").nullable() } -class JodaTimeMiscTableTest : JodaTimeBaseTest() { +class JodaTimeMiscTableTest : DatabaseTestsBase() { @Test fun testInsert01() { val tbl = Misc diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt index 80e2857957..f82ecfe1ef 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt @@ -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) } diff --git a/exposed-kotlin-datetime/api/exposed-kotlin-datetime.api b/exposed-kotlin-datetime/api/exposed-kotlin-datetime.api index d264b64a8e..e11cc7a83f 100644 --- a/exposed-kotlin-datetime/api/exposed-kotlin-datetime.api +++ b/exposed-kotlin-datetime/api/exposed-kotlin-datetime.api @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt index d5ac61f4f1..69bfb90c91 100644 --- a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt +++ b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt @@ -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 { diff --git a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt index 6c226754e2..35629fdcaa 100644 --- a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt +++ b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt @@ -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 @@ -41,10 +40,13 @@ fun Date(expr: Expression): Function = DateI internal class TimeInternal(val expr: Expression<*>) : Function(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) } } @@ -292,6 +294,22 @@ fun Expression.date() = Date(this) @JvmName("OffsetDateTimeDateExt") fun Expression.date() = Date(this) +/** Returns the time from this date expression. */ +@JvmName("LocalDateTimeExt") +fun Expression.time() = Time(this) + +/** Returns the time from this datetime expression. */ +@JvmName("LocalDateTimeTimeExt") +fun Expression.time() = Time(this) + +/** Returns the time from this timestamp expression. */ +@JvmName("InstantTimeExt") +fun Expression.time() = Time(this) + +/** Returns the time from this timestampWithTimeZone expression. */ +@JvmName("OffsetDateTimeTimeExt") +fun Expression.time() = Time(this) + /** Returns the year from this date expression, as an integer. */ @JvmName("LocalDateYearExt") fun Expression.year() = Year(this) diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt index 991c512566..c91e827fdf 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt @@ -29,7 +29,7 @@ import java.time.ZoneOffset import kotlin.test.assertEquals import kotlin.time.Duration -open class KotlinTimeBaseTest : DatabaseTestsBase() { +class KotlinTimeTests : DatabaseTestsBase() { private val timestampWithTimeZoneUnsupportedDB = TestDB.ALL_MARIADB + TestDB.MYSQL_V5 @@ -450,7 +450,7 @@ open class KotlinTimeBaseTest : 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)) @@ -458,9 +458,9 @@ open class KotlinTimeBaseTest : DatabaseTestsBase() { SchemaUtils.create(testTable) - val now = OffsetDateTime.now(ZoneId.systemDefault()) + val now = OffsetDateTime.parse("2023-05-04T05:04:01.123123123+00:00") val nowId = testTable.insertAndGetId { - it[timestampWithTimeZone] = now + it[testTable.timestampWithTimeZone] = now } assertEquals( @@ -469,6 +469,20 @@ open class KotlinTimeBaseTest : 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().toKotlinLocalTime() + assertEquals( + expectedTime, + testTable.select(testTable.timestampWithTimeZone.time()).where { testTable.id eq nowId } + .single()[testTable.timestampWithTimeZone.time()] + ) + assertEquals( now.year, testTable.select(testTable.timestampWithTimeZone.year()).where { testTable.id eq nowId }