Skip to content

Commit

Permalink
test: Fix failing datetime tests in MariaDB (#1805)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
bog-walk authored Jul 28, 2023
1 parent 9de706c commit 74fba00
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}

Expand Down Expand Up @@ -442,25 +451,29 @@ fun <T : Temporal> 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 MysqlDialect ->
if ((currentDialectTest 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 MariaDBDialect ->
assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db")
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 ${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")
}
}

Expand All @@ -472,6 +485,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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}

Expand Down Expand Up @@ -437,25 +446,29 @@ fun <T> 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 MysqlDialect ->
if ((currentDialectTest 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 MariaDBDialect ->
assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db")
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 ${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")
}
}

Expand All @@ -467,6 +480,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()
}
Expand Down

0 comments on commit 74fba00

Please sign in to comment.