diff --git a/exposed-java-time/build.gradle.kts b/exposed-java-time/build.gradle.kts index 457adc6faf..10f3df5260 100644 --- a/exposed-java-time/build.gradle.kts +++ b/exposed-java-time/build.gradle.kts @@ -3,6 +3,7 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { kotlin("jvm") apply true + kotlin("plugin.serialization") apply true id("testWithDBs") } 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 3c58bb9480..0e1a849d38 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 @@ -1,5 +1,11 @@ package org.jetbrains.exposed +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.between @@ -10,6 +16,7 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.vendors.* import org.junit.Assert.fail import org.junit.Test @@ -261,6 +268,42 @@ open class JavaTimeBaseTest : DatabaseTestsBase() { assertEquals(2, createdIn2023.size) } } + + @Test + fun testDateTimeAsJsonB() { + val tester = object : Table("tester") { + val created = datetime("created") + val modified = jsonb("modified", Json.Default) + } + + withTables(excludeSettings = TestDB.allH2TestDB + TestDB.SQLITE + TestDB.SQLSERVER + TestDB.ORACLE, tester) { + val dateTimeNow = LocalDateTime.now() + tester.insert { + it[created] = dateTimeNow.minusYears(1) + it[modified] = ModifierData(1, dateTimeNow) + } + tester.insert { + it[created] = dateTimeNow.plusYears(1) + it[modified] = ModifierData(2, dateTimeNow) + } + + val prefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + + // value extracted in same manner it is stored, a json string + val modifiedAsString = tester.modified.jsonExtract("${prefix}timestamp") + val allModifiedAsString = tester.slice(modifiedAsString).selectAll() + assertTrue(allModifiedAsString.all { it[modifiedAsString] == dateTimeNow.toString() }) + + // PostgreSQL requires explicit type cast to timestamp for in-DB comparison + val dateModified = if (currentDialectTest is PostgreSQLDialect) { + tester.modified.jsonExtract("${prefix}timestamp").castTo(JavaLocalDateTimeColumnType()) + } else { + tester.modified.jsonExtract("${prefix}timestamp") + } + val modifiedBeforeCreation = tester.select { dateModified less tester.created }.single() + assertEquals(2, modifiedBeforeCreation[tester.modified].userId) + } + } } fun assertEqualDateTime(d1: T?, d2: T?) { @@ -337,3 +380,16 @@ object CitiesTime : IntIdTable("CitiesTime") { val name = varchar("name", 50) // Column val local_time = datetime("local_time").nullable() // Column } + +@Serializable +data class ModifierData( + val userId: Int, + @Serializable(with = DateTimeSerializer::class) + val timestamp: LocalDateTime +) + +object DateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: LocalDateTime) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): LocalDateTime = LocalDateTime.parse(decoder.decodeString()) +} diff --git a/exposed-jodatime/build.gradle.kts b/exposed-jodatime/build.gradle.kts index 5812fde905..5d989a64a4 100644 --- a/exposed-jodatime/build.gradle.kts +++ b/exposed-jodatime/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.exposed.gradle.Versions plugins { kotlin("jvm") apply true + kotlin("plugin.serialization") apply true id("testWithDBs") } 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 21ea0076ff..d4a140e7d6 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt @@ -1,5 +1,10 @@ package org.jetbrains.exposed +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.between @@ -10,6 +15,7 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.vendors.* import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -177,6 +183,42 @@ open class JodaTimeBaseTest : DatabaseTestsBase() { assertEquals(2, createdIn2023.size) } } + + @Test + fun testDateTimeAsJsonB() { + val tester = object : Table("tester") { + val created = datetime("created") + val modified = jsonb("modified", Json.Default) + } + + withTables(excludeSettings = TestDB.allH2TestDB + TestDB.SQLITE + TestDB.SQLSERVER + TestDB.ORACLE, tester) { + val dateTimeNow = DateTime.now() + tester.insert { + it[created] = dateTimeNow.minusYears(1) + it[modified] = ModifierData(1, dateTimeNow) + } + tester.insert { + it[created] = dateTimeNow.plusYears(1) + it[modified] = ModifierData(2, dateTimeNow) + } + + val prefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + + // value extracted in same manner it is stored, a json string + val modifiedAsString = tester.modified.jsonExtract("${prefix}timestamp") + val allModifiedAsString = tester.slice(modifiedAsString).selectAll() + assertTrue(allModifiedAsString.all { it[modifiedAsString] == dateTimeNow.toString() }) + + // PostgreSQL requires explicit type cast to timestamp for in-DB comparison + val dateModified = if (currentDialectTest is PostgreSQLDialect) { + tester.modified.jsonExtract("${prefix}timestamp").castTo(DateColumnType(true)) + } else { + tester.modified.jsonExtract("${prefix}timestamp") + } + val modifiedBeforeCreation = tester.select { dateModified less tester.created }.single() + assertEquals(2, modifiedBeforeCreation[tester.modified].userId) + } + } } fun assertEqualDateTime(d1: DateTime?, d2: DateTime?) { @@ -201,3 +243,16 @@ object CitiesTime : IntIdTable("CitiesTime") { val name = varchar("name", 50) // Column val local_time = datetime("local_time").nullable() // Column } + +@Serializable +data class ModifierData( + val userId: Int, + @Serializable(with = DateTimeSerializer::class) + val timestamp: DateTime +) + +object DateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DateTime", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: DateTime) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): DateTime = DateTime.parse(decoder.decodeString()) +} diff --git a/exposed-kotlin-datetime/build.gradle.kts b/exposed-kotlin-datetime/build.gradle.kts index b477a38d0c..bb757e39da 100644 --- a/exposed-kotlin-datetime/build.gradle.kts +++ b/exposed-kotlin-datetime/build.gradle.kts @@ -3,6 +3,7 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { kotlin("jvm") apply true + kotlin("plugin.serialization") apply true id("testWithDBs") } 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 92a779e4f8..361fa26026 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 @@ -1,6 +1,8 @@ package org.jetbrains.exposed.sql.kotlin.datetime import kotlinx.datetime.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.between @@ -10,6 +12,7 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.vendors.* import org.junit.Assert.fail import org.junit.Test @@ -256,6 +259,46 @@ open class KotlinTimeBaseTest : DatabaseTestsBase() { assertEquals(2, createdIn2023.size) } } + + @Test + fun testDateTimeAsJsonB() { + val tester = object : Table("tester") { + val created = datetime("created") + val modified = jsonb("modified", Json.Default) + } + + withTables(excludeSettings = TestDB.allH2TestDB + TestDB.SQLITE + TestDB.SQLSERVER + TestDB.ORACLE, tester) { + val dateTimeNow = now() + tester.insert { + it[created] = dateTimeNow.date.minus(1, DateTimeUnit.YEAR).atTime(0, 0, 0) + it[modified] = ModifierData(1, dateTimeNow) + } + tester.insert { + it[created] = dateTimeNow.date.plus(1, DateTimeUnit.YEAR).atTime(0, 0, 0) + it[modified] = ModifierData(2, dateTimeNow) + } + + val prefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + + // value extracted in same manner it is stored, a json string + val modifiedAsString = tester.modified.jsonExtract("${prefix}timestamp") + val allModifiedAsString = tester.slice(modifiedAsString).selectAll() + assertTrue(allModifiedAsString.all { it[modifiedAsString] == dateTimeNow.toString() }) + // value extracted as json, with implicit LocalDateTime serializer() performing conversions + val modifiedAsJson = tester.modified.jsonExtract("${prefix}timestamp", toScalar = false) + val allModifiedAsJson = tester.slice(modifiedAsJson).selectAll() + assertTrue(allModifiedAsJson.all { it[modifiedAsJson] == dateTimeNow }) + + // PostgreSQL requires explicit type cast to timestamp for in-DB comparison + val dateModified = if (currentDialectTest is PostgreSQLDialect) { + tester.modified.jsonExtract("${prefix}timestamp").castTo(KotlinLocalDateTimeColumnType()) + } else { + tester.modified.jsonExtract("${prefix}timestamp") + } + val modifiedBeforeCreation = tester.select { dateModified less tester.created }.single() + assertEquals(2, modifiedBeforeCreation[tester.modified].userId) + } + } } fun assertEqualDateTime(d1: T?, d2: T?) { @@ -332,3 +375,6 @@ object CitiesTime : IntIdTable("CitiesTime") { val name = varchar("name", 50) // Column val local_time = datetime("local_time").nullable() // Column } + +@Serializable +data class ModifierData(val userId: Int, val timestamp: LocalDateTime)