diff --git a/pom.xml b/pom.xml index 3097538048..386fbc6ea6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index d834798834..0791e42216 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 32f9269501..f869892fbe 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT @@ -122,7 +122,7 @@ org.postgresql postgresql ${postgresql.version} - test + true @@ -237,6 +237,37 @@ + + postgres + + + + org.apache.maven.plugins + maven-surefire-plugin + + + postgres-test + test + + test + + + + **/*IntegrationTests.java + + + **/*HsqlIntegrationTests.java + + + postgres + + + + + + + + all-dbs diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/aot/JdbcRuntimeHints.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/aot/JdbcRuntimeHints.java index ca9c551cb9..79abb5db2b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/aot/JdbcRuntimeHints.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/aot/JdbcRuntimeHints.java @@ -21,6 +21,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; +import org.springframework.data.jdbc.core.dialect.JdbcPostgresDialect; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; import org.springframework.data.relational.auditing.RelationalAuditingCallback; import org.springframework.data.relational.core.mapping.event.AfterConvertCallback; @@ -54,5 +55,12 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) TypeReference.of("org.springframework.aop.SpringProxy"), TypeReference.of("org.springframework.aop.framework.Advised"), TypeReference.of("org.springframework.core.DecoratingProxy")); + + hints.reflection().registerType(TypeReference.of("org.postgresql.jdbc.TypeInfoCache"), + MemberCategory.PUBLIC_CLASSES); + + for (Class simpleType : JdbcPostgresDialect.INSTANCE.simpleTypes()) { + hints.reflection().registerType(TypeReference.of(simpleType), MemberCategory.PUBLIC_CLASSES); + } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java index dd39e9568b..eff492803f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java @@ -18,7 +18,6 @@ import java.sql.Array; import java.sql.SQLType; -import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.jdbc.core.ConnectionCallback; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.util.Assert; @@ -66,9 +65,9 @@ public Array createArray(Object[] value) { Assert.notNull(value, "Value must not be null"); Class componentType = arrayColumns.getArrayType(value.getClass()); + SQLType jdbcType = arrayColumns.getSqlType(componentType); - SQLType jdbcType = JdbcUtil.targetSqlTypeFor(componentType); - Assert.notNull(jdbcType, () -> String.format("Couldn't determine JDBCType for %s", componentType)); + Assert.notNull(jdbcType, () -> String.format("Couldn't determine SQLType for %s", componentType)); String typeName = arrayColumns.getArrayTypeName(jdbcType); return operations.execute((ConnectionCallback) c -> c.createArrayOf(typeName, value)); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcArrayColumns.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcArrayColumns.java index fa12638e11..146ef51c04 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcArrayColumns.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcArrayColumns.java @@ -17,6 +17,7 @@ import java.sql.SQLType; +import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.relational.core.dialect.ArrayColumns; /** @@ -33,6 +34,17 @@ default Class getArrayType(Class userType) { return ArrayColumns.unwrapComponentType(userType); } + /** + * Determine the {@link SQLType} for a given {@link Class array component type}. + * + * @param componentType component type of the array. + * @return the dialect-supported array type. + * @since 3.1.3 + */ + default SQLType getSqlType(Class componentType) { + return JdbcUtil.targetSqlTypeFor(getArrayType(componentType)); + } + /** * The appropriate SQL type as a String which should be used to represent the given {@link SQLType} in an * {@link java.sql.Array}. Defaults to the name of the argument. diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java index 138ab5873c..ff57d1b563 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java @@ -15,16 +15,33 @@ */ package org.springframework.data.jdbc.core.dialect; +import java.sql.Array; import java.sql.JDBCType; +import java.sql.SQLException; import java.sql.SQLType; +import java.sql.Types; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import org.postgresql.core.Oid; +import org.postgresql.jdbc.TypeInfoCache; import org.springframework.data.jdbc.core.convert.JdbcArrayColumns; import org.springframework.data.relational.core.dialect.PostgresDialect; +import org.springframework.util.ClassUtils; /** * JDBC specific Postgres Dialect. * * @author Jens Schauder + * @author Mark Paluch * @since 2.3 */ public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect { @@ -33,18 +50,74 @@ public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect private static final JdbcPostgresArrayColumns ARRAY_COLUMNS = new JdbcPostgresArrayColumns(); + private static final Set> SIMPLE_TYPES; + + static { + + Set> simpleTypes = new HashSet<>(PostgresDialect.INSTANCE.simpleTypes()); + List simpleTypeNames = Arrays.asList( // + "org.postgresql.util.PGobject", // + "org.postgresql.geometric.PGpoint", // + "org.postgresql.geometric.PGbox", // + "org.postgresql.geometric.PGcircle", // + "org.postgresql.geometric.PGline", // + "org.postgresql.geometric.PGpath", // + "org.postgresql.geometric.PGpolygon", // + "org.postgresql.geometric.PGlseg" // + ); + simpleTypeNames.forEach(name -> ifClassPresent(name, simpleTypes::add)); + SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + @Override + public Set> simpleTypes() { + return SIMPLE_TYPES; + } + @Override public JdbcArrayColumns getArraySupport() { return ARRAY_COLUMNS; } + /** + * If the class is present on the class path, invoke the specified consumer {@code action} with the class object, + * otherwise do nothing. + * + * @param action block to be executed if a value is present. + */ + private static void ifClassPresent(String className, Consumer> action) { + if (ClassUtils.isPresent(className, PostgresDialect.class.getClassLoader())) { + action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader())); + } + } + static class JdbcPostgresArrayColumns implements JdbcArrayColumns { + private static final boolean TYPE_INFO_PRESENT = ClassUtils.isPresent("org.postgresql.jdbc.TypeInfoCache", + JdbcPostgresDialect.class.getClassLoader()); + + private static final TypeInfoWrapper TYPE_INFO_WRAPPER; + + static { + TYPE_INFO_WRAPPER = TYPE_INFO_PRESENT ? new TypeInfoCacheWrapper() : new TypeInfoWrapper(); + } + @Override public boolean isSupported() { return true; } + @Override + public SQLType getSqlType(Class componentType) { + + SQLType sqlType = TYPE_INFO_WRAPPER.getArrayTypeMap().get(componentType); + if (sqlType != null) { + return sqlType; + } + + return JdbcArrayColumns.super.getSqlType(componentType); + } + @Override public String getArrayTypeName(SQLType jdbcType) { @@ -58,4 +131,92 @@ public String getArrayTypeName(SQLType jdbcType) { return jdbcType.getName(); } } + + /** + * Wrapper for Postgres types. Defaults to no-op to guard runtimes against absent TypeInfoCache. + * + * @since 3.1.3 + */ + static class TypeInfoWrapper { + + /** + * @return a type map between a Java array component type and its Postgres type. + */ + Map, SQLType> getArrayTypeMap() { + return Collections.emptyMap(); + } + } + + /** + * {@link TypeInfoWrapper} backed by {@link TypeInfoCache}. + * + * @since 3.1.3 + */ + static class TypeInfoCacheWrapper extends TypeInfoWrapper { + + private final Map, SQLType> arrayTypes = new HashMap<>(); + + public TypeInfoCacheWrapper() { + + TypeInfoCache cache = new TypeInfoCache(null, 0); + addWellKnownTypes(cache); + + Iterator it = cache.getPGTypeNamesWithSQLTypes(); + + try { + + while (it.hasNext()) { + + String pgTypeName = it.next(); + int oid = cache.getPGType(pgTypeName); + String javaClassName = cache.getJavaClass(oid); + int arrayOid = cache.getJavaArrayType(pgTypeName); + + if (!ClassUtils.isPresent(javaClassName, getClass().getClassLoader())) { + continue; + } + + Class javaClass = ClassUtils.forName(javaClassName, getClass().getClassLoader()); + + // avoid accidental usage of smaller database types that map to the same Java type or generic-typed SQL + // arrays. + if (javaClass == Array.class || javaClass == String.class || javaClass == Integer.class || oid == Oid.OID + || oid == Oid.MONEY) { + continue; + } + + arrayTypes.put(javaClass, new PGSQLType(pgTypeName, arrayOid)); + } + } catch (SQLException | ClassNotFoundException e) { + throw new IllegalStateException("Cannot create type info mapping", e); + } + } + + private static void addWellKnownTypes(TypeInfoCache cache) { + cache.addCoreType("uuid", Oid.UUID, Types.OTHER, UUID.class.getName(), Oid.UUID_ARRAY); + } + + @Override + Map, SQLType> getArrayTypeMap() { + return arrayTypes; + } + + record PGSQLType(String name, int oid) implements SQLType { + + @Override + public String getName() { + return name; + } + + @Override + public String getVendor() { + return "Postgres"; + } + + @Override + public Integer getVendorTypeNumber() { + return oid; + } + } + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/package-info.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/package-info.java new file mode 100644 index 0000000000..645c30d7c6 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/package-info.java @@ -0,0 +1,7 @@ +/** + * JDBC-specific Dialect implementations. + */ +@NonNullApi +package org.springframework.data.jdbc.core.dialect; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactoryTest.java new file mode 100644 index 0000000000..f549a93ab5 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactoryTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 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.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.sql.Array; +import java.sql.SQLException; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.postgresql.core.BaseConnection; +import org.springframework.data.jdbc.core.dialect.JdbcPostgresDialect; +import org.springframework.jdbc.core.ConnectionCallback; +import org.springframework.jdbc.core.JdbcOperations; + +/** + * Unit tests for {@link DefaultJdbcTypeFactory}. + * + * @author Mark Paluch + */ +@ExtendWith(MockitoExtension.class) +class DefaultJdbcTypeFactoryTest { + + @Mock JdbcOperations operations; + @Mock BaseConnection connection; + + @Test // GH-1567 + void shouldProvidePostgresArrayType() throws SQLException { + + DefaultJdbcTypeFactory sut = new DefaultJdbcTypeFactory(operations, JdbcPostgresDialect.INSTANCE.getArraySupport()); + + when(operations.execute(any(ConnectionCallback.class))).thenAnswer(invocation -> { + + ConnectionCallback callback = invocation.getArgument(0, ConnectionCallback.class); + return callback.doInConnection(connection); + }); + + UUID uuids[] = new UUID[] { UUID.randomUUID(), UUID.randomUUID() }; + when(connection.createArrayOf("uuid", uuids)).thenReturn(mock(Array.class)); + Array array = sut.createArray(uuids); + + assertThat(array).isNotNull(); + } + +} diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index a60f8e183a..62aff792a9 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java index 2cfbfc359d..707395f14b 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java @@ -2,18 +2,13 @@ import io.r2dbc.postgresql.codec.Json; -import java.net.InetAddress; -import java.net.URI; -import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Stream; @@ -51,7 +46,7 @@ public class PostgresDialect extends org.springframework.data.relational.core.di static { Set> simpleTypes = new HashSet<>( - Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class, Map.class)); + org.springframework.data.relational.core.dialect.PostgresDialect.INSTANCE.simpleTypes()); // conditional Postgres Geo support. Stream.of("io.r2dbc.postgresql.codec.Box", // diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 57b9d707a6..e250904de2 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1567-SNAPSHOT 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 eb06c91a70..2c8cdb03fc 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,13 +15,15 @@ */ package org.springframework.data.relational.core.dialect; -import java.util.Arrays; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.rmi.server.UID; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.function.Consumer; import org.springframework.data.relational.core.sql.Functions; import org.springframework.data.relational.core.sql.IdentifierProcessing; @@ -32,7 +34,6 @@ import org.springframework.data.relational.core.sql.SimpleFunction; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.TableLike; -import org.springframework.util.ClassUtils; /** * An SQL dialect for Postgres. @@ -50,6 +51,9 @@ public class PostgresDialect extends AbstractDialect { */ public static final PostgresDialect INSTANCE = new PostgresDialect(); + private static final Set> POSTGRES_SIMPLE_TYPES = Set.of(UID.class, URL.class, URI.class, InetAddress.class, + Map.class); + protected PostgresDialect() {} private static final LimitClause LIMIT_CLAUSE = new LimitClause() { @@ -152,32 +156,7 @@ public IdentifierProcessing getIdentifierProcessing() { @Override public Set> simpleTypes() { - - Set> simpleTypes = new HashSet<>(); - List simpleTypeNames = Arrays.asList( // - "org.postgresql.util.PGobject", // - "org.postgresql.geometric.PGpoint", // - "org.postgresql.geometric.PGbox", // - "org.postgresql.geometric.PGcircle", // - "org.postgresql.geometric.PGline", // - "org.postgresql.geometric.PGpath", // - "org.postgresql.geometric.PGpolygon", // - "org.postgresql.geometric.PGlseg" // - ); - simpleTypeNames.forEach(name -> ifClassPresent(name, simpleTypes::add)); - return Collections.unmodifiableSet(simpleTypes); - } - - /** - * If the class is present on the class path, invoke the specified consumer {@code action} with the class object, - * otherwise do nothing. - * - * @param action block to be executed if a value is present. - */ - private static void ifClassPresent(String className, Consumer> action) { - if (ClassUtils.isPresent(className, PostgresDialect.class.getClassLoader())) { - action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader())); - } + return POSTGRES_SIMPLE_TYPES; } @Override