diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index d7722eca..af9ad090 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -141,7 +141,7 @@ com.h2database h2 ${h2.version} - test + true @@ -190,7 +190,7 @@ com.microsoft.sqlserver mssql-jdbc ${mssql.version} - test + true diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java index 4ab9deaf..bb7a0ef3 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core.convert; import java.sql.Timestamp; +import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.time.temporal.Temporal; import java.util.Date; @@ -52,6 +53,7 @@ public enum JdbcColumnTypes { javaToDbType.put(Enum.class, String.class); javaToDbType.put(ZonedDateTime.class, String.class); + javaToDbType.put(OffsetDateTime.class, OffsetDateTime.class); javaToDbType.put(Temporal.class, Timestamp.class); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java index 45ce0c6b..efad38c1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java @@ -18,6 +18,7 @@ package org.springframework.data.jdbc.core.convert; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; import org.springframework.data.convert.CustomConversions; @@ -35,7 +36,7 @@ import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; */ public class JdbcCustomConversions extends CustomConversions { - private static final List STORE_CONVERTERS = Arrays + public static final List STORE_CONVERTERS = Arrays .asList(Jsr310TimestampBasedConverters.getConvertersToRegister().toArray()); private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS); @@ -48,7 +49,7 @@ public class JdbcCustomConversions extends CustomConversions { } /** - * Create a new {@link JdbcCustomConversions} instance registering the given converters. + * Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store converters. * * @param converters must not be {@literal null}. */ @@ -56,6 +57,15 @@ public class JdbcCustomConversions extends CustomConversions { super(new ConverterConfiguration(STORE_CONVERSIONS, converters, JdbcCustomConversions::isDateTimeApiConversion)); } + /** + * Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store converters. + * + * @since 2.3 + */ + public JdbcCustomConversions(StoreConversions storeConversions, List userConverters) { + super(new ConverterConfiguration(storeConversions, userConverters, JdbcCustomConversions::isDateTimeApiConversion)); + } + /** * Create a new {@link JdbcCustomConversions} instance given * {@link org.springframework.data.convert.CustomConversions.ConverterConfiguration}. diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java new file mode 100644 index 00000000..2824496a --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.dialect; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.data.relational.core.dialect.Db2Dialect; +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +/** + * {@link Db2Dialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @since 2.3 + */ +public class JdbcDb2Dialect extends Db2Dialect { + + public static JdbcDb2Dialect INSTANCE = new JdbcDb2Dialect(); + + @Override + public Collection getConverters() { + + ArrayList converters = new ArrayList<>(super.getConverters()); + converters.add(OffsetDateTime2TimestampConverter.INSTANCE); + + return converters; + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcH2Dialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcH2Dialect.java new file mode 100644 index 00000000..b7fdd1b9 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcH2Dialect.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.dialect; + +import org.h2.api.TimestampWithTimeZone; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.relational.core.dialect.Db2Dialect; +import org.springframework.data.relational.core.dialect.H2Dialect; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Collection; +import java.util.Collections; + +/** + * {@link Db2Dialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @since 2.3 + */ +public class JdbcH2Dialect extends H2Dialect { + + public static JdbcH2Dialect INSTANCE = new JdbcH2Dialect(); + + @Override + public Collection getConverters() { + return Collections.singletonList(TimestampWithTimeZone2OffsetDateTimeConverter.INSTANCE); + } + + @ReadingConverter + enum TimestampWithTimeZone2OffsetDateTimeConverter implements Converter { + INSTANCE; + + + @Override + public OffsetDateTime convert(TimestampWithTimeZone source) { + + long nanosInSecond = 1_000_000_000; + long nanosInMinute = nanosInSecond * 60; + long nanosInHour = nanosInMinute * 60; + + long hours = (source.getNanosSinceMidnight() / nanosInHour); + + long nanosInHours = hours * nanosInHour; + long nanosLeft = source.getNanosSinceMidnight() - nanosInHours; + long minutes = nanosLeft / nanosInMinute; + + long nanosInMinutes = minutes * nanosInMinute; + nanosLeft -= nanosInMinutes; + long seconds = nanosLeft / nanosInSecond; + + long nanosInSeconds = seconds * nanosInSecond; + nanosLeft -= nanosInSeconds; + ZoneOffset offset = ZoneOffset.ofTotalSeconds(source.getTimeZoneOffsetSeconds()); + + return OffsetDateTime.of(source.getYear(), source.getMonth(), source.getDay(), (int)hours, (int)minutes, (int)seconds, (int)nanosLeft, offset ); + + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcMySqlDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcMySqlDialect.java new file mode 100644 index 00000000..26ce00d2 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcMySqlDialect.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.dialect; + +import java.sql.JDBCType; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.jdbc.core.convert.JdbcValue; +import org.springframework.data.relational.core.dialect.Db2Dialect; +import org.springframework.data.relational.core.dialect.MySqlDialect; +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +/** + * {@link Db2Dialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @since 2.3 + */ +public class JdbcMySqlDialect extends MySqlDialect { + + public JdbcMySqlDialect(IdentifierProcessing identifierProcessing) { + super(identifierProcessing); + } + + @Override + public Collection getConverters() { + + ArrayList converters = new ArrayList<>(super.getConverters()); + converters.add(OffsetDateTime2TimestampJdbcValueConverter.INSTANCE); + + return converters; + } + + @WritingConverter + enum OffsetDateTime2TimestampJdbcValueConverter implements Converter { + INSTANCE; + + @Override + public JdbcValue convert(OffsetDateTime source) { + return JdbcValue.of(source, JDBCType.TIMESTAMP); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java new file mode 100644 index 00000000..1592814e --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.dialect; + +import microsoft.sql.DateTimeOffset; + +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.relational.core.dialect.Db2Dialect; +import org.springframework.data.relational.core.dialect.SqlServerDialect; + +/** + * {@link Db2Dialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @since 2.3 + */ +public class JdbcSqlServerDialect extends SqlServerDialect { + + public static JdbcSqlServerDialect INSTANCE = new JdbcSqlServerDialect(); + + @Override + public Collection getConverters() { + return Collections.singletonList(DateTimeOffset2OffsetDateTimeConverter.INSTANCE); + } + + @ReadingConverter + enum DateTimeOffset2OffsetDateTimeConverter implements Converter { + INSTANCE; + + @Override + public OffsetDateTime convert(DateTimeOffset source) { + return source.getOffsetDateTime(); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/OffsetDateTime2TimestampConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/OffsetDateTime2TimestampConverter.java new file mode 100644 index 00000000..185a0c26 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/OffsetDateTime2TimestampConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.dialect; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.relational.core.dialect.Db2Dialect; + +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/** + * {@link WritingConverter} from {@link OffsetDateTime} to {@link Timestamp}. + * The conversion preserves the {@link java.time.Instant} represented by {@link OffsetDateTime} + * + * @author Jens Schauder + * @since 2.3 + */ +@WritingConverter +enum OffsetDateTime2TimestampConverter implements Converter { + + INSTANCE; + @Override + public Timestamp convert(OffsetDateTime source) { + return Timestamp.from(source.toInstant()); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index fa144841..46fbe3bc 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -15,6 +15,9 @@ */ package org.springframework.data.jdbc.repository.config; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; import org.springframework.context.ApplicationContext; @@ -22,6 +25,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; @@ -33,6 +37,7 @@ import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.NamingStrategy; @@ -56,7 +61,7 @@ public class AbstractJdbcConfiguration { * Register a {@link JdbcMappingContext} and apply an optional {@link NamingStrategy}. * * @param namingStrategy optional {@link NamingStrategy}. Use {@link NamingStrategy#INSTANCE} as fallback. - * @param customConversions see {@link #jdbcCustomConversions()}. + * @param customConversions see {@link #jdbcCustomConversions(Dialect)}. * @return must not be {@literal null}. */ @Bean @@ -71,10 +76,10 @@ public class AbstractJdbcConfiguration { /** * Creates a {@link RelationalConverter} using the configured - * {@link #jdbcMappingContext(Optional, JdbcCustomConversions)}. Will get {@link #jdbcCustomConversions()} applied. + * {@link #jdbcMappingContext(Optional, JdbcCustomConversions)}. Will get {@link #jdbcCustomConversions(Dialect)} ()} applied. * * @see #jdbcMappingContext(Optional, JdbcCustomConversions) - * @see #jdbcCustomConversions() + * @see #jdbcCustomConversions(Dialect) () * @return must not be {@literal null}. */ @Bean @@ -96,8 +101,22 @@ public class AbstractJdbcConfiguration { * @return will never be {@literal null}. */ @Bean - public JdbcCustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(); + public JdbcCustomConversions jdbcCustomConversions(Dialect dialect) { + + return new JdbcCustomConversions(CustomConversions.StoreConversions.of(JdbcSimpleTypes.HOLDER, + storeConverters(dialect)), userConverters()); + } + + private List userConverters() { + return Collections.emptyList(); + } + + private List storeConverters(Dialect dialect) { + + List converters = new ArrayList<>(); + converters.addAll(dialect.getConverters()); + converters.addAll(JdbcCustomConversions.STORE_CONVERTERS); + return converters; } /** @@ -134,7 +153,7 @@ public class AbstractJdbcConfiguration { * Resolves a {@link Dialect JDBC dialect} by inspecting {@link NamedParameterJdbcOperations}. * * @param operations the {@link NamedParameterJdbcOperations} allowing access to a {@link java.sql.Connection}. - * @return + * @return the {@link Dialect} to be used. * @since 2.0 * @throws org.springframework.data.jdbc.repository.config.DialectResolver.NoDialectException if the {@link Dialect} * cannot be determined. diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java index 36cb6b71..d7f49fd6 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java @@ -28,10 +28,15 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.dao.NonTransientDataAccessException; +import org.springframework.data.jdbc.core.dialect.JdbcDb2Dialect; +import org.springframework.data.jdbc.core.dialect.JdbcH2Dialect; +import org.springframework.data.jdbc.core.dialect.JdbcMySqlDialect; +import org.springframework.data.jdbc.core.dialect.JdbcSqlServerDialect; import org.springframework.data.relational.core.dialect.Db2Dialect; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.H2Dialect; import org.springframework.data.relational.core.dialect.HsqlDbDialect; +import org.springframework.data.relational.core.dialect.MariaDbDialect; import org.springframework.data.relational.core.dialect.MySqlDialect; import org.springframework.data.relational.core.dialect.OracleDialect; import org.springframework.data.relational.core.dialect.PostgresDialect; @@ -118,19 +123,22 @@ public class DialectResolver { return HsqlDbDialect.INSTANCE; } if (name.contains("h2")) { - return H2Dialect.INSTANCE; + return JdbcH2Dialect.INSTANCE; } - if (name.contains("mysql") || name.contains("mariadb")) { - return new MySqlDialect(getIdentifierProcessing(metaData)); + if (name.contains("mysql")) { + return new JdbcMySqlDialect(getIdentifierProcessing(metaData)); + } + if (name.contains("mariadb")) { + return new MariaDbDialect(getIdentifierProcessing(metaData)); } if (name.contains("postgresql")) { return PostgresDialect.INSTANCE; } if (name.contains("microsoft")) { - return SqlServerDialect.INSTANCE; + return JdbcSqlServerDialect.INSTANCE; } if (name.contains("db2")) { - return Db2Dialect.INSTANCE; + return JdbcDb2Dialect.INSTANCE; } if (name.contains("oracle")) { return OracleDialect.INSTANCE; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java index ae85b337..e5d0c291 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java @@ -22,6 +22,7 @@ import java.sql.JDBCType; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Map; @@ -62,6 +63,7 @@ public final class JdbcUtil { sqlTypeMappings.put(Date.class, Types.DATE); sqlTypeMappings.put(Time.class, Types.TIME); sqlTypeMappings.put(Timestamp.class, Types.TIMESTAMP); + sqlTypeMappings.put(OffsetDateTime.class, Types.TIMESTAMP_WITH_TIMEZONE); } private JdbcUtil() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/JdbcH2DialectTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/JdbcH2DialectTests.java new file mode 100644 index 00000000..f3eb1761 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/JdbcH2DialectTests.java @@ -0,0 +1,51 @@ +package org.springframework.data.jdbc.core.dialect; + +import static org.assertj.core.api.Assertions.*; + +import java.time.OffsetDateTime; + +import org.h2.api.TimestampWithTimeZone; +import org.h2.util.DateTimeUtils; +import org.junit.jupiter.api.Test; + +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests for {@link JdbcH2Dialect}. + * + * @author Jens Schauder + */ +class JdbcH2DialectTests { + + @Test + void TimestampWithTimeZone2OffsetDateTimeConverterConvertsProperly() { + + JdbcH2Dialect.TimestampWithTimeZone2OffsetDateTimeConverter converter = JdbcH2Dialect.TimestampWithTimeZone2OffsetDateTimeConverter.INSTANCE; + long dateValue = 123456789; + long timeNanos = 987654321; + int timeZoneOffsetSeconds = 4 * 60 * 60; + TimestampWithTimeZone timestampWithTimeZone = new TimestampWithTimeZone(dateValue, timeNanos, + timeZoneOffsetSeconds); + + OffsetDateTime offsetDateTime = converter.convert(timestampWithTimeZone); + + assertThat(offsetDateTime.getOffset().getTotalSeconds()).isEqualTo(timeZoneOffsetSeconds); + assertThat(offsetDateTime.getNano()).isEqualTo(timeNanos); + assertThat(offsetDateTime.toEpochSecond()) + .isEqualTo(DateTimeUtils.getEpochSeconds(dateValue, timeNanos, timeZoneOffsetSeconds)); + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/OffsetDateTime2TimestampConverterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/OffsetDateTime2TimestampConverterUnitTests.java new file mode 100644 index 00000000..0f7a1822 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/OffsetDateTime2TimestampConverterUnitTests.java @@ -0,0 +1,44 @@ +package org.springframework.data.jdbc.core.dialect; + +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests for {@link OffsetDateTime2TimestampConverter}. + * + * @author Jens Schauder + */ +class OffsetDateTime2TimestampConverterUnitTests { + + @Test + void conversionPreservesInstant() { + + OffsetDateTime offsetDateTime = OffsetDateTime.of(5, 5, 5, 5,5,5,123456789, ZoneOffset.ofHours(3)); + + Timestamp timestamp = OffsetDateTime2TimestampConverter.INSTANCE.convert(offsetDateTime); + + assertThat(timestamp.toInstant()).isEqualTo(offsetDateTime.toInstant()); + } +} \ No newline at end of file diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index d5d73f76..54497a5d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -27,14 +27,16 @@ import lombok.Value; import java.io.IOException; import java.sql.ResultSet; import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationListener; @@ -92,6 +94,9 @@ public class JdbcRepositoryIntegrationTests { @BeforeEach public void before() { + + repository.deleteAll(); + eventListener.events.clear(); } @@ -282,17 +287,7 @@ public class JdbcRepositoryIntegrationTests { @Test // DATAJDBC-464, DATAJDBC-318 public void executeQueryWithParameterRequiringConversion() { - Instant now = Instant.now(); - - DummyEntity first = repository.save(createDummyEntity()); - first.setPointInTime(now.minusSeconds(1000L)); - first.setName("first"); - - DummyEntity second = repository.save(createDummyEntity()); - second.setPointInTime(now.plusSeconds(1000L)); - second.setName("second"); - - repository.saveAll(asList(first, second)); + Instant now = createDummyBeforeAndAfterNow(); assertThat(repository.after(now)) // .extracting(DummyEntity::getName) // @@ -408,7 +403,7 @@ public class JdbcRepositoryIntegrationTests { @Test // #945 @EnabledOnFeature(TestDatabaseFeatures.Feature.IS_POSTGRES) public void usePrimitiveArrayAsArgument() { - assertThat(repository.unnestPrimitive(new int[]{1, 2, 3})).containsExactly(1,2,3); + assertThat(repository.unnestPrimitive(new int[] { 1, 2, 3 })).containsExactly(1, 2, 3); } @Test // GH-774 @@ -442,6 +437,17 @@ public class JdbcRepositoryIntegrationTests { assertThat(slice.hasNext()).isTrue(); } + @Test // #935 + public void queryByOffsetDateTime() { + + Instant now = createDummyBeforeAndAfterNow(); + OffsetDateTime timeArgument = OffsetDateTime.ofInstant(now, ZoneOffset.ofHours(2)); + + List entities = repository.findByOffsetDateTime(timeArgument); + + assertThat(entities).extracting(DummyEntity::getName).containsExactly("second"); + } + @Test // #971 public void stringQueryProjectionShouldReturnProjectedEntities() { @@ -486,6 +492,29 @@ public class JdbcRepositoryIntegrationTests { assertThat(result.getContent().get(0).getName()).isEqualTo("Entity Name"); } + private Instant createDummyBeforeAndAfterNow() { + + Instant now = Instant.now(); + + DummyEntity first = createDummyEntity(); + Instant earlier = now.minusSeconds(1000L); + OffsetDateTime earlierPlus3 = earlier.atOffset(ZoneOffset.ofHours(3)); + first.setPointInTime(earlier); + first.offsetDateTime = earlierPlus3; + + first.setName("first"); + + DummyEntity second = createDummyEntity(); + Instant later = now.plusSeconds(1000L); + OffsetDateTime laterPlus3 = later.atOffset(ZoneOffset.ofHours(3)); + second.setPointInTime(later); + second.offsetDateTime = laterPlus3; + second.setName("second"); + + repository.saveAll(asList(first, second)); + return now; + } + interface DummyEntityRepository extends CrudRepository { List findAllByNamedQuery(); @@ -525,6 +554,9 @@ public class JdbcRepositoryIntegrationTests { Page findPageProjectionByName(String name, Pageable pageable); Slice findSliceByNameContains(String name, Pageable pageable); + + @Query("SELECT * FROM DUMMY_ENTITY WHERE OFFSET_DATE_TIME > :threshhold") + List findByOffsetDateTime(@Param("threshhold") OffsetDateTime threshhold); } @Configuration @@ -573,6 +605,7 @@ public class JdbcRepositoryIntegrationTests { static class DummyEntity { String name; Instant pointInTime; + OffsetDateTime offsetDateTime; @Id private Long idProp; public DummyEntity(String name) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java index 1ec72619..797ad126 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java @@ -110,7 +110,7 @@ public class AbstractJdbcConfigurationIntegrationTests { } @Override - public JdbcCustomConversions jdbcCustomConversions() { + public JdbcCustomConversions jdbcCustomConversions(Dialect dialect) { return new JdbcCustomConversions(Collections.singletonList(Blah2BlubbConverter.INSTANCE)); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/support/JdbcUtilTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/support/JdbcUtilTests.java new file mode 100644 index 00000000..30f532fe --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/support/JdbcUtilTests.java @@ -0,0 +1,37 @@ +package org.springframework.data.jdbc.support; + +import static org.assertj.core.api.Assertions.*; + +import java.sql.Types; +import java.time.OffsetDateTime; + +import org.junit.jupiter.api.Test; + +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests for {@link JdbcUtil}. + * + * @author Jens Schauder + */ +class JdbcUtilTests { + + @Test + void test() { + assertThat(JdbcUtil.sqlTypeFor(OffsetDateTime.class)).isEqualTo(Types.TIMESTAMP_WITH_TIMEZONE); + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java index 8da00a77..5e2cb7b5 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java @@ -77,15 +77,4 @@ class MySqlDataSourceConfiguration extends DataSourceConfiguration implements In new ByteArrayResource("DROP DATABASE test;CREATE DATABASE test;".getBytes())); } } - - private DataSource createRootDataSource() { - - MysqlDataSource dataSource = new MysqlDataSource(); - dataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl()); - dataSource.setUser("root"); - dataSource.setPassword(MYSQL_CONTAINER.getPassword()); - dataSource.setDatabaseName(MYSQL_CONTAINER.getDatabaseName()); - - return dataSource; - } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 20c73b85..2e4207e1 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -15,6 +15,10 @@ */ package org.springframework.data.jdbc.testing; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Optional; import javax.sql.DataSource; @@ -38,6 +42,7 @@ import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; import org.springframework.data.jdbc.repository.config.DialectResolver; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.relational.core.dialect.Dialect; @@ -108,8 +113,17 @@ public class TestConfiguration { } @Bean - CustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(); + CustomConversions jdbcCustomConversions(Dialect dialect) { + return new JdbcCustomConversions(CustomConversions.StoreConversions.of(JdbcSimpleTypes.HOLDER, + storeConverters(dialect)), Collections.emptyList()); + } + + private List storeConverters(Dialect dialect) { + + List converters = new ArrayList<>(); + converters.addAll(dialect.getConverters()); + converters.addAll(JdbcCustomConversions.STORE_CONVERTERS); + return converters; } @Bean diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 358bcbbb..daa41534 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -4,5 +4,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP -- with time zone is only supported with z/OS ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index 6649c143..b9b31016 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -2,5 +2,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index 6649c143..b9b31016 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -2,5 +2,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 83aea089..f9b08644 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -2,5 +2,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP(3) + POINT_IN_TIME TIMESTAMP(3), + OFFSET_DATE_TIME TIMESTAMP(3) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index e632e642..c71942b4 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -3,5 +3,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME DATETIME + POINT_IN_TIME DATETIME, + OFFSET_DATE_TIME DATETIMEOFFSET ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 83aea089..9c4085b2 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -1,6 +1,9 @@ +SET SQL_MODE='ALLOW_INVALID_DATES'; + CREATE TABLE dummy_entity ( id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP(3) + POINT_IN_TIME TIMESTAMP(3) default null, + OFFSET_DATE_TIME TIMESTAMP(3) default null ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 28b2d80e..a3d83134 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -4,5 +4,6 @@ CREATE TABLE DUMMY_ENTITY ( ID_PROP NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, NAME VARCHAR2(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 803ef247..5e670bfe 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -3,5 +3,6 @@ CREATE TABLE dummy_entity ( id_Prop SERIAL PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql index 8e100e80..c14f1200 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql @@ -1,2 +1,2 @@ -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp DATETIME PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp DATETIME NOT NULL PRIMARY KEY, data VARCHAR(100)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp TIMESTAMP PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp TIMESTAMP NOT NULL PRIMARY KEY, data VARCHAR(100)); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql index 8e100e80..c14f1200 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql @@ -1,2 +1,2 @@ -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp DATETIME PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp DATETIME NOT NULL PRIMARY KEY, data VARCHAR(100)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp TIMESTAMP PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp TIMESTAMP NOT NULL PRIMARY KEY, data VARCHAR(100)); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java index 8f4b57a9..ae88a762 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java @@ -15,6 +15,9 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; + import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.LockOptions; @@ -110,4 +113,9 @@ public class Db2Dialect extends AbstractDialect { public IdentifierProcessing getIdentifierProcessing() { return IdentifierProcessing.ANSI; } + + @Override + public Collection getConverters() { + return Collections.singletonList(Timestamp2OffsetDateTimeConverter.INSTANCE); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java index 35097866..864a21ba 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java @@ -15,6 +15,9 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; + import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.render.SelectRenderContext; @@ -82,7 +85,16 @@ public interface Dialect { return Escaper.DEFAULT; } - default IdGeneration getIdGeneration(){ + default IdGeneration getIdGeneration() { return IdGeneration.DEFAULT; - }; + } + + /** + * Return a collection of converters for this dialect. + * + * @return a collection of converters for this dialect. + */ + default Collection getConverters() { + return Collections.emptySet(); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java new file mode 100644 index 00000000..9ed37348 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.dialect; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +/** + * A SQL dialect for MariaDb. + * + * @author Jens Schauder + * @since 2.3 + */ +public class MariaDbDialect extends MySqlDialect { + + public MariaDbDialect(IdentifierProcessing identifierProcessing) { + super(identifierProcessing); + } + + @Override + public Collection getConverters() { + return Collections.singletonList(Timestamp2OffsetDateTimeConverter.INSTANCE); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java index 51b65d00..6dc8e8c8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java @@ -15,11 +15,14 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; + import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.data.relational.core.sql.LockOptions; import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.util.Assert; -import org.springframework.data.relational.core.sql.LockOptions; /** * A SQL dialect for MySQL. @@ -161,4 +164,9 @@ public class MySqlDialect extends AbstractDialect { public IdentifierProcessing getIdentifierProcessing() { return identifierProcessing; } + + @Override + public Collection getConverters() { + return Collections.singletonList(Timestamp2OffsetDateTimeConverter.INSTANCE); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java index ad9889f2..f19ae642 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java @@ -15,15 +15,8 @@ */ package org.springframework.data.relational.core.dialect; -import java.util.List; - -import org.springframework.data.relational.core.sql.IdentifierProcessing; -import org.springframework.data.relational.core.sql.LockOptions; -import org.springframework.data.relational.core.sql.Table; -import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; -import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; +import java.util.Collection; +import java.util.Collections; /** * An SQL dialect for Oracle. @@ -51,4 +44,10 @@ public class OracleDialect extends AnsiDialect { public IdGeneration getIdGeneration() { return ID_GENERATION; } + + @Override + public Collection getConverters() { + return Collections.singletonList(Timestamp2OffsetDateTimeConverter.INSTANCE); + } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java index 08beb8b9..e21618d6 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java @@ -15,14 +15,16 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; import java.util.List; import org.springframework.data.relational.core.sql.IdentifierProcessing; -import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; -import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.data.relational.core.sql.LockOptions; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; +import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -113,6 +115,11 @@ public class PostgresDialect extends AbstractDialect { return ARRAY_COLUMNS; } + @Override + public Collection getConverters() { + return Collections.singletonList(Timestamp2OffsetDateTimeConverter.INSTANCE); + } + static class PostgresLockClause implements LockClause { private final IdentifierProcessing identifierProcessing; @@ -165,7 +172,7 @@ public class PostgresDialect extends AbstractDialect { public Position getClausePosition() { return Position.AFTER_ORDER_BY; } - }; + } static class PostgresArrayColumns implements ArrayColumns { @@ -195,4 +202,5 @@ public class PostgresDialect extends AbstractDialect { public IdentifierProcessing getIdentifierProcessing() { return IdentifierProcessing.create(Quoting.ANSI, LetterCasing.LOWER_CASE); } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Timestamp2OffsetDateTimeConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Timestamp2OffsetDateTimeConverter.java new file mode 100644 index 00000000..230f234d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Timestamp2OffsetDateTimeConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.dialect; + +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneId; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +/** + * A reading convert to convert {@link Timestamp} to {@link OffsetDateTime}. For the conversion the {@link Timestamp} + * gets considered to be at UTC and the result of the conversion will have an offset of 0 and represent the same + * instant. + * + * @author Jens Schauder + * @since 2.3 + */ +@ReadingConverter +enum Timestamp2OffsetDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public OffsetDateTime convert(Timestamp timestamp) { + return OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/Timestamp2OffsetDateTimeConverterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/Timestamp2OffsetDateTimeConverterUnitTests.java new file mode 100644 index 00000000..a4c127ab --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/Timestamp2OffsetDateTimeConverterUnitTests.java @@ -0,0 +1,43 @@ +package org.springframework.data.relational.core.dialect; + +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests {@link Timestamp2OffsetDateTimeConverter}. + * + * @author Jens Schauder + */ +class Timestamp2OffsetDateTimeConverterUnitTests { + + @Test + void conversionMaintainsInstant() { + + Timestamp timestamp = Timestamp.from(Instant.now()); + OffsetDateTime converted = Timestamp2OffsetDateTimeConverter.INSTANCE.convert(timestamp); + + assertThat(converted.toInstant()).isEqualTo(timestamp.toInstant()); + } +} \ No newline at end of file