Skip to content

Add support for Postgres UUID arrays using JDBC #1570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.2.0-SNAPSHOT</version>
<version>3.2.0-GH-1567-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data Relational Parent</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-jdbc-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.2.0-SNAPSHOT</version>
<version>3.2.0-GH-1567-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
37 changes: 34 additions & 3 deletions spring-data-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-jdbc</artifactId>
<version>3.2.0-SNAPSHOT</version>
<version>3.2.0-GH-1567-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.2.0-SNAPSHOT</version>
<version>3.2.0-GH-1567-SNAPSHOT</version>
</parent>

<properties>
Expand Down Expand Up @@ -122,7 +122,7 @@
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
<scope>test</scope>
<optional>true</optional>
</dependency>

<dependency>
Expand Down Expand Up @@ -237,6 +237,37 @@


<profiles>
<profile>
<id>postgres</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>postgres-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*IntegrationTests.java</include>
</includes>
<excludes>
<exclude>**/*HsqlIntegrationTests.java</exclude>
</excludes>
<systemPropertyVariables>
<spring.profiles.active>postgres</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>all-dbs</id>
<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Array>) c -> c.createArrayOf(typeName, value));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.sql.SQLType;

import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.relational.core.dialect.ArrayColumns;

/**
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -33,18 +50,74 @@ public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect

private static final JdbcPostgresArrayColumns ARRAY_COLUMNS = new JdbcPostgresArrayColumns();

private static final Set<Class<?>> SIMPLE_TYPES;

static {

Set<Class<?>> simpleTypes = new HashSet<>(PostgresDialect.INSTANCE.simpleTypes());
List<String> 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<Class<?>> 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<Class<?>> 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) {

Expand All @@ -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<Class<?>, SQLType> getArrayTypeMap() {
return Collections.emptyMap();
}
}

/**
* {@link TypeInfoWrapper} backed by {@link TypeInfoCache}.
*
* @since 3.1.3
*/
static class TypeInfoCacheWrapper extends TypeInfoWrapper {

private final Map<Class<?>, SQLType> arrayTypes = new HashMap<>();

public TypeInfoCacheWrapper() {

TypeInfoCache cache = new TypeInfoCache(null, 0);
addWellKnownTypes(cache);

Iterator<String> 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<Class<?>, 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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* JDBC-specific Dialect implementations.
*/
@NonNullApi
package org.springframework.data.jdbc.core.dialect;

import org.springframework.lang.NonNullApi;
Loading