From 8f9372bba6ca7e1c9cebb0512204c14b6d7bb5d5 Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:30:51 -0400 Subject: [PATCH 1/2] test: Fix failing datetime tests in MariaDB The following tests in exposed-java-time and exposed-kotlin-datetime fail when run using MariaDB: KotlinTimeTests/'test string LocalDateTime with nanos'() JavaTimeTests/'test string LocalDateTime with nanos'() - Fails with the message `Failed on microseconds` because Exposed's assertion functions for the fractional part are using `roundToMicro()` to compare nanoseconds. In other databases (DB), including MySQL, the nanoseconds retrieved from the DB is rounded to the internal precision using RoundingMode.HALF_UP. MariaDB always rounds down. - These tests are flaky because they rely on Clock.System.now() (and the java equivalent) and add a set amount of nanoseconds to a dynamic datetime value. This means any resulting value on the lower half of a microsecond will be round down and pass. - The assertion has been changed to floor the nanoseconds value instead and the test now takes 2 values with constant nanoseconds to evaluate what happens when the value is low versus high. --- .../org/jetbrains/exposed/JavaTimeTests.kt | 46 +++++++++++++------ .../sql/kotlin/datetime/KotlinTimeTests.kt | 46 +++++++++++++------ 2 files changed, 62 insertions(+), 30 deletions(-) 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 fa8e527afa..8c26b88ab8 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 @@ -96,18 +96,27 @@ open class JavaTimeBaseTest : DatabaseTestsBase() { } @Test - fun `test storing LocalDateTime with nanos`() { + fun testStoringLocalDateTimeWithNanos() { val testDate = object : IntIdTable("TestLocalDateTime") { val time = datetime("time") } + withTables(testDate) { - val dateTimeWithNanos = LocalDateTime.now().withNano(123) + val dateTime = LocalDateTime.now() + val nanos = 111111 + // insert 2 separate nanosecond constants to ensure test's rounding mode matches DB precision + val dateTimeWithFewNanos = dateTime.withNano(nanos) + val dateTimeWithManyNanos = dateTime.withNano(nanos * 7) + testDate.insert { + it[time] = dateTimeWithFewNanos + } testDate.insert { - it[time] = dateTimeWithNanos + it[time] = dateTimeWithManyNanos } - val dateTimeFromDB = testDate.selectAll().single()[testDate.time] - assertEqualDateTime(dateTimeWithNanos, dateTimeFromDB) + val dateTimesFromDB = testDate.selectAll().map { it[testDate.time] } + assertEqualDateTime(dateTimeWithFewNanos, dateTimesFromDB[0]) + assertEqualDateTime(dateTimeWithManyNanos, dateTimesFromDB[1]) } } @@ -442,25 +451,30 @@ fun assertEqualDateTime(d1: T?, d2: T?) { } private fun assertEqualFractionalPart(nano1: Int, nano2: Int) { - when (currentDialectTest) { - // nanoseconds (H2, Oracle & Sqlite could be here) - // assertEquals(nano1, nano2, "Failed on nano ${currentDialectTest.name}") + val dialect = currentDialectTest + val db = dialect.name + when (dialect) { // accurate to 100 nanoseconds - is SQLServerDialect -> assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds ${currentDialectTest.name}") + is SQLServerDialect -> + assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds $db") // microseconds - is H2Dialect, is MariaDBDialect, is PostgreSQLDialect, is PostgreSQLNGDialect -> - assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") + is H2Dialect, is PostgreSQLDialect -> + assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") + is MariaDBDialect -> + assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db") is MysqlDialect -> - if ((currentDialectTest as? MysqlDialect)?.isFractionDateTimeSupported() == true) { + if ((dialect as? MysqlDialect)?.isFractionDateTimeSupported() == true) { // this should be uncommented, but mysql has different microseconds between save & read // assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") } else { // don't compare fractional part } // milliseconds - is OracleDialect -> assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - is SQLiteDialect -> assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - else -> fail("Unknown dialect ${currentDialectTest.name}") + is OracleDialect -> + assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds $db") + is SQLiteDialect -> + assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds $db") + else -> fail("Unknown dialect $db") } } @@ -472,6 +486,8 @@ private fun roundToMicro(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000), RoundingMode.HALF_UP).toInt() } +private fun floorToMicro(nanos: Int): Int = nanos / 1_000 + private fun roundToMilli(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000_000), RoundingMode.HALF_UP).toInt() } 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 3164f542ff..58120b5380 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 @@ -87,18 +87,27 @@ open class KotlinTimeBaseTest : DatabaseTestsBase() { } @Test - fun `test storing LocalDateTime with nanos`() { + fun testStoringLocalDateTimeWithNanos() { val testDate = object : IntIdTable("TestLocalDateTime") { val time = datetime("time") } + withTables(testDate) { - val dateTimeWithNanos = Clock.System.now().plus(DateTimeUnit.NANOSECOND * 123).toLocalDateTime(TimeZone.currentSystemDefault()) + val dateTime = Instant.parse("2023-05-04T05:04:00.000Z") // has 0 nanoseconds + val nanos = DateTimeUnit.NANOSECOND * 111111 + // insert 2 separate constants to ensure test's rounding mode matches DB precision + val dateTimeWithFewNanos = dateTime.plus(nanos).toLocalDateTime(TimeZone.currentSystemDefault()) + val dateTimeWithManyNanos = dateTime.plus(nanos * 7).toLocalDateTime(TimeZone.currentSystemDefault()) + testDate.insert { + it[testDate.time] = dateTimeWithFewNanos + } testDate.insert { - it[testDate.time] = dateTimeWithNanos + it[testDate.time] = dateTimeWithManyNanos } - val dateTimeFromDB = testDate.selectAll().single()[testDate.time] - assertEqualDateTime(dateTimeWithNanos, dateTimeFromDB) + val dateTimesFromDB = testDate.selectAll().map { it[testDate.time] } + assertEqualDateTime(dateTimeWithFewNanos, dateTimesFromDB[0]) + assertEqualDateTime(dateTimeWithManyNanos, dateTimesFromDB[1]) } } @@ -437,25 +446,30 @@ fun assertEqualDateTime(d1: T?, d2: T?) { } private fun assertEqualFractionalPart(nano1: Int, nano2: Int) { - when (currentDialectTest) { - // nanoseconds (H2, Oracle & Sqlite could be here) - // assertEquals(nano1, nano2, "Failed on nano ${currentDialectTest.name}") + val dialect = currentDialectTest + val db = dialect.name + when (dialect) { // accurate to 100 nanoseconds - is SQLServerDialect -> assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds ${currentDialectTest.name}") + is SQLServerDialect -> + assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds $db") // microseconds - is H2Dialect, is MariaDBDialect, is PostgreSQLDialect, is PostgreSQLNGDialect -> - assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") + is H2Dialect, is PostgreSQLDialect -> + assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") + is MariaDBDialect -> + assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db") is MysqlDialect -> - if ((currentDialectTest as? MysqlDialect)?.isFractionDateTimeSupported() == true) { + if ((dialect as? MysqlDialect)?.isFractionDateTimeSupported() == true) { // this should be uncommented, but mysql has different microseconds between save & read // assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") } else { // don't compare fractional part } // milliseconds - is OracleDialect -> assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - is SQLiteDialect -> assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - else -> fail("Unknown dialect ${currentDialectTest.name}") + is OracleDialect -> + assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds $db") + is SQLiteDialect -> + assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds $db") + else -> fail("Unknown dialect $db") } } @@ -467,6 +481,8 @@ private fun roundToMicro(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000), RoundingMode.HALF_UP).toInt() } +private fun floorToMicro(nanos: Int): Int = nanos / 1_000 + private fun roundToMilli(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000_000), RoundingMode.HALF_UP).toInt() } From 5929ae455695e257b3e48fde5104dbcc0235afc5 Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Fri, 28 Jul 2023 09:22:20 -0400 Subject: [PATCH 2/2] test: Fix failing datetime tests in MariaDB Refactor uncommented block about MySQL in assertion function. --- .../kotlin/org/jetbrains/exposed/JavaTimeTests.kt | 15 +++++++-------- .../sql/kotlin/datetime/KotlinTimeTests.kt | 15 +++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) 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 8c26b88ab8..599f8c486b 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 @@ -458,17 +458,16 @@ private fun assertEqualFractionalPart(nano1: Int, nano2: Int) { is SQLServerDialect -> assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds $db") // microseconds - is H2Dialect, is PostgreSQLDialect -> - assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") is MariaDBDialect -> assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db") - is MysqlDialect -> - if ((dialect as? MysqlDialect)?.isFractionDateTimeSupported() == true) { - // this should be uncommented, but mysql has different microseconds between save & read -// assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") - } else { - // don't compare fractional part + is H2Dialect, is PostgreSQLDialect, is MysqlDialect -> { + when ((dialect as? MysqlDialect)?.isFractionDateTimeSupported()) { + null, true -> { + assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") + } + else -> {} // don't compare fractional part } + } // milliseconds is OracleDialect -> assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds $db") 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 58120b5380..fdfba6fc6f 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 @@ -453,17 +453,16 @@ private fun assertEqualFractionalPart(nano1: Int, nano2: Int) { is SQLServerDialect -> assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds $db") // microseconds - is H2Dialect, is PostgreSQLDialect -> - assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") is MariaDBDialect -> assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db") - is MysqlDialect -> - if ((dialect as? MysqlDialect)?.isFractionDateTimeSupported() == true) { - // this should be uncommented, but mysql has different microseconds between save & read -// assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") - } else { - // don't compare fractional part + is H2Dialect, is PostgreSQLDialect, is MysqlDialect -> { + when ((dialect as? MysqlDialect)?.isFractionDateTimeSupported()) { + null, true -> { + assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") + } + else -> {} // don't compare fractional part } + } // milliseconds is OracleDialect -> assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds $db")