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