This is an automated email from the ASF dual-hosted git repository. amanin pushed a commit to branch fix/sql-temporal in repository https://gitbox.apache.org/repos/asf/sis.git
commit 850986b7d679c462d914396493d0694bf73756fc Author: Alexis Manin <[email protected]> AuthorDate: Tue May 24 10:46:20 2022 +0200 chore(Storage): Add tests for sql Time and Time with timezone conversion to java.time objects. --- .../sql/feature/TemporalValueGetterTest.java | 102 +++++++++++++++++++-- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/TemporalValueGetterTest.java b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/TemporalValueGetterTest.java index a3a9805b1b..ff6f65eff2 100644 --- a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/TemporalValueGetterTest.java +++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/TemporalValueGetterTest.java @@ -7,19 +7,25 @@ import java.sql.Statement; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiPredicate; import java.util.stream.Collectors; import org.apache.sis.test.TestCase; import org.apache.sis.test.sql.TestDatabase; import org.junit.AfterClass; +import org.junit.Assert; import org.junit.Assume; import org.junit.AssumptionViolatedException; import org.junit.BeforeClass; import org.junit.Test; +import static java.time.ZoneOffset.UTC; import static java.time.ZoneOffset.ofHours; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -131,18 +137,98 @@ public class TemporalValueGetterTest extends TestCase { ValueGetter<Instant> getter = ValueGetter.AsInstant.INSTANCE; execute(driver, db, "SELECT \"value\" FROM TEST_TIMESTAMP_WITH_TIMEZONE", r -> { - r.next(); - assertEquals(LocalDateTime.of(2022, 5, 1, 1, 1, 1).atOffset(ofHours(1)).toInstant(), getter.getValue(null, r, 1)); - r.next(); - assertEquals(LocalDateTime.of(2022, 5, 31, 23, 59, 59, 999_000_000).atOffset(ofHours(2)).toInstant(), getter.getValue(null, r, 1)); - r.next(); - // Ensure no NPE occurs, i.e the converter is resilient to null inputs - assertNull(getter.getValue(null, r, 1)); - assertFalse("Defense against test change: only 3 values should be present in the test table", r.next()); + Instant[] expectedValues = { + LocalDateTime.of(2022, 5, 1, 1, 1, 1).atOffset(ofHours(1)).toInstant(), + LocalDateTime.of(2022, 5, 31, 23, 59, 59, 999_000_000).atOffset(ofHours(2)).toInstant(), + null + }; + assertContentEquals(expectedValues, r, 1, getter, "Timestamp with time zone conversion"); }); }); } + /** + * Note: It does not test sub-second precision, although PostGreSQL, HSQLDB and H2 support it. + * The conversion from {@link java.sql.Timestamp} makes it difficult to get back the information. + * See {@link ValueGetter.AsLocalTime} todo section for details. + */ + @Test + public void sqlTimeToLocalTime() { + testOnEachDatabase(((driver, db) -> { + execute(driver, db, + "CREATE TABLE TEST_TIME (\"value\" TIME)", + "INSERT INTO TEST_TIME (\"value\") VALUES ('08:01:01'), ('09:30:30'), (null)"); + ValueGetter<LocalTime> getter = ValueGetter.AsLocalTime.INSTANCE; + execute(driver, db, "SELECT \"value\" FROM TEST_TIME", r -> { + LocalTime[] expectedValues = { + LocalTime.of(8, 1, 1), + LocalTime.of(9, 30, 30), + null + }; + assertContentEquals(expectedValues, r, 1, getter, "Time conversion"); + }); + })); + } + + @Test + public void sqlTimeWithTimeZoneToOffsetTime() { + testOnEachDatabase(((driver, db) -> { + Assume.assumeFalse("Derby does not support TIMESTAMP WITH TIME ZONE data type", DEFAULT_DRIVER.equals(driver)); + execute(driver, db, + "CREATE TABLE TEST_TIME_WITH_TIMEZONE (\"value\" TIME WITH TIME ZONE)", + "INSERT INTO TEST_TIME_WITH_TIMEZONE (\"value\") VALUES ('08:01:01+00:00'), ('09:30:30-01:00'), (null)"); + ValueGetter<OffsetTime> getter = ValueGetter.AsOffsetTime.INSTANCE; + execute(driver, db, "SELECT \"value\" FROM TEST_TIME_WITH_TIMEZONE", r -> { + OffsetTime[] expectedValues = { + LocalTime.of(8, 1, 1).atOffset(UTC), + LocalTime.of(10, 30, 30).atOffset(UTC), + null + }; + + // Databases don't properly preserve insert time zone, so we can only check if values represent the same + // instant from the start of day. + assertContentEquals(expectedValues, r, 1, getter, OffsetTime::isEqual, "Time conversion"); + }); + })); + } + + private <V> void assertContentEquals(V[] expected, ResultSet actual, int columnIndex, ValueGetter<V> valueConverter, String title) throws Exception { + assertContentEquals(expected, actual, columnIndex, valueConverter, Objects::equals, title); + } + + /** + * Equivalent of {@link Assert#assertArrayEquals(String, Object[], Object[])}, except "actual values" are retrieved + * by parsing an {@link ResultSet SQL query result}. + * + * @param expected Values that should be returned by the query+converter combo. + * @param actual Result of an SQL query to validate. + * @param columnIndex Column to read values from in given result set. + * @param valueConverter How to map each SQL result value to JAVA API. Must not be null. + * @param comparisonFunction How to compare an expected element with. Must not be null. + * @param title An error message title for assertion failures. + * @param <V> Type of values to compare. + */ + private <V> void assertContentEquals(V[] expected, ResultSet actual, int columnIndex, ValueGetter<V> valueConverter, BiPredicate<V, V> comparisonFunction, String title) throws Exception { + final List<V> values = new ArrayList<>(expected.length); + while (values.size() < expected.length && actual.next()) { + values.add(valueConverter.getValue(null, actual, columnIndex)); + } + + assertEquals(title + ": Not enough values in query result", expected.length, values.size()); + assertFalse(title + ": Too many values in query result", actual.next()); + + for (int i = 0 ; i < expected.length ; i++) { + final V expectedValue = expected[i]; + final V actualValue = values.get(i); + + if (expectedValue == actualValue) continue; + + if (expectedValue == null || actualValue == null || !comparisonFunction.test(expectedValue, actualValue)) { + throw new AssertionError(String.format("%s: error at index %d -> Expected: %s, but was: %s", title, i, expectedValue, actualValue)); + } + } + } + private void testOnEachDatabase(DatabaseTester tester) { DATABASES.forEach((driver, db) -> { try {
