Skip to content

Commit a5ccc04

Browse files
mp911deschauder
authored andcommitted
Add support for Postgres UUID arrays using JDBC.
We now use Postgres' JDBC drivers TypeInfoCache to register and determine array types including support for UUID. Closes #1567
1 parent 74ca3d7 commit a5ccc04

File tree

6 files changed

+217
-3
lines changed

6 files changed

+217
-3
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/aot/JdbcRuntimeHints.java

+11
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
package org.springframework.data.jdbc.aot;
1717

1818
import java.util.Arrays;
19+
import java.util.UUID;
1920

2021
import org.springframework.aot.hint.MemberCategory;
2122
import org.springframework.aot.hint.RuntimeHints;
2223
import org.springframework.aot.hint.RuntimeHintsRegistrar;
2324
import org.springframework.aot.hint.TypeReference;
25+
import org.springframework.data.jdbc.core.dialect.JdbcPostgresDialect;
2426
import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository;
2527
import org.springframework.data.relational.auditing.RelationalAuditingCallback;
2628
import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
@@ -54,5 +56,14 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader)
5456
TypeReference.of("org.springframework.aop.SpringProxy"),
5557
TypeReference.of("org.springframework.aop.framework.Advised"),
5658
TypeReference.of("org.springframework.core.DecoratingProxy"));
59+
60+
hints.reflection().registerType(TypeReference.of("org.postgresql.jdbc.TypeInfoCache"),
61+
MemberCategory.PUBLIC_CLASSES);
62+
63+
for (Class<?> simpleType : JdbcPostgresDialect.INSTANCE.simpleTypes()) {
64+
hints.reflection().registerType(TypeReference.of(simpleType), MemberCategory.PUBLIC_CLASSES);
65+
}
66+
67+
hints.reflection().registerType(TypeReference.of(UUID.class.getName()), MemberCategory.PUBLIC_CLASSES);
5768
}
5869
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.sql.Array;
1919
import java.sql.SQLType;
2020

21-
import org.springframework.data.jdbc.support.JdbcUtil;
2221
import org.springframework.jdbc.core.ConnectionCallback;
2322
import org.springframework.jdbc.core.JdbcOperations;
2423
import org.springframework.util.Assert;
@@ -66,9 +65,9 @@ public Array createArray(Object[] value) {
6665
Assert.notNull(value, "Value must not be null");
6766

6867
Class<?> componentType = arrayColumns.getArrayType(value.getClass());
68+
SQLType jdbcType = arrayColumns.getSqlType(componentType);
6969

70-
SQLType jdbcType = JdbcUtil.targetSqlTypeFor(componentType);
71-
Assert.notNull(jdbcType, () -> String.format("Couldn't determine JDBCType for %s", componentType));
70+
Assert.notNull(jdbcType, () -> String.format("Couldn't determine SQLType for %s", componentType));
7271
String typeName = arrayColumns.getArrayTypeName(jdbcType);
7372

7473
return operations.execute((ConnectionCallback<Array>) c -> c.createArrayOf(typeName, value));

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcArrayColumns.java

+12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.sql.SQLType;
1919

20+
import org.springframework.data.jdbc.support.JdbcUtil;
2021
import org.springframework.data.relational.core.dialect.ArrayColumns;
2122

2223
/**
@@ -33,6 +34,17 @@ default Class<?> getArrayType(Class<?> userType) {
3334
return ArrayColumns.unwrapComponentType(userType);
3435
}
3536

37+
/**
38+
* Determine the {@link SQLType} for a given {@link Class array component type}.
39+
*
40+
* @param componentType component type of the array.
41+
* @return the dialect-supported array type.
42+
* @since 3.1.3
43+
*/
44+
default SQLType getSqlType(Class<?> componentType) {
45+
return JdbcUtil.targetSqlTypeFor(getArrayType(componentType));
46+
}
47+
3648
/**
3749
* The appropriate SQL type as a String which should be used to represent the given {@link SQLType} in an
3850
* {@link java.sql.Array}. Defaults to the name of the argument.

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java

+121
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,29 @@
1515
*/
1616
package org.springframework.data.jdbc.core.dialect;
1717

18+
import java.sql.Array;
1819
import java.sql.JDBCType;
20+
import java.sql.SQLException;
1921
import java.sql.SQLType;
22+
import java.sql.Types;
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import java.util.HashMap;
26+
import java.util.Iterator;
27+
import java.util.Map;
28+
import java.util.UUID;
2029

30+
import org.postgresql.core.Oid;
31+
import org.postgresql.jdbc.TypeInfoCache;
2132
import org.springframework.data.jdbc.core.convert.JdbcArrayColumns;
2233
import org.springframework.data.relational.core.dialect.PostgresDialect;
34+
import org.springframework.util.ClassUtils;
2335

2436
/**
2537
* JDBC specific Postgres Dialect.
2638
*
2739
* @author Jens Schauder
40+
* @author Mark Paluch
2841
* @since 2.3
2942
*/
3043
public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect {
@@ -40,11 +53,31 @@ public JdbcArrayColumns getArraySupport() {
4053

4154
static class JdbcPostgresArrayColumns implements JdbcArrayColumns {
4255

56+
private static final boolean TYPE_INFO_PRESENT = ClassUtils.isPresent("org.postgresql.jdbc.TypeInfoCache",
57+
JdbcPostgresDialect.class.getClassLoader());
58+
59+
private static final TypeInfoWrapper TYPE_INFO_WRAPPER;
60+
61+
static {
62+
TYPE_INFO_WRAPPER = TYPE_INFO_PRESENT ? new TypeInfoCacheWrapper() : new TypeInfoWrapper();
63+
}
64+
4365
@Override
4466
public boolean isSupported() {
4567
return true;
4668
}
4769

70+
@Override
71+
public SQLType getSqlType(Class<?> componentType) {
72+
73+
SQLType sqlType = TYPE_INFO_WRAPPER.getArrayTypeMap().get(componentType);
74+
if (sqlType != null) {
75+
return sqlType;
76+
}
77+
78+
return JdbcArrayColumns.super.getSqlType(componentType);
79+
}
80+
4881
@Override
4982
public String getArrayTypeName(SQLType jdbcType) {
5083

@@ -58,4 +91,92 @@ public String getArrayTypeName(SQLType jdbcType) {
5891
return jdbcType.getName();
5992
}
6093
}
94+
95+
/**
96+
* Wrapper for Postgres types. Defaults to no-op to guard runtimes against absent TypeInfoCache.
97+
*
98+
* @since 3.1.3
99+
*/
100+
static class TypeInfoWrapper {
101+
102+
/**
103+
* @return a type map between a Java array component type and its Postgres type.
104+
*/
105+
Map<Class<?>, SQLType> getArrayTypeMap() {
106+
return Collections.emptyMap();
107+
}
108+
}
109+
110+
/**
111+
* {@link TypeInfoWrapper} backed by {@link TypeInfoCache}.
112+
*
113+
* @since 3.1.3
114+
*/
115+
static class TypeInfoCacheWrapper extends TypeInfoWrapper {
116+
117+
private final Map<Class<?>, SQLType> arrayTypes = new HashMap<>();
118+
119+
public TypeInfoCacheWrapper() {
120+
121+
TypeInfoCache cache = new TypeInfoCache(null, 0);
122+
addWellKnownTypes(cache);
123+
124+
Iterator<String> it = cache.getPGTypeNamesWithSQLTypes();
125+
126+
try {
127+
128+
while (it.hasNext()) {
129+
130+
String pgTypeName = it.next();
131+
int oid = cache.getPGType(pgTypeName);
132+
String javaClassName = cache.getJavaClass(oid);
133+
int arrayOid = cache.getJavaArrayType(pgTypeName);
134+
135+
if (!ClassUtils.isPresent(javaClassName, getClass().getClassLoader())) {
136+
continue;
137+
}
138+
139+
Class<?> javaClass = ClassUtils.forName(javaClassName, getClass().getClassLoader());
140+
141+
// avoid accidental usage of smaller database types that map to the same Java type or generic-typed SQL
142+
// arrays.
143+
if (javaClass == Array.class || javaClass == String.class || javaClass == Integer.class || oid == Oid.OID
144+
|| oid == Oid.MONEY) {
145+
continue;
146+
}
147+
148+
arrayTypes.put(javaClass, new PGSQLType(pgTypeName, arrayOid));
149+
}
150+
} catch (SQLException | ClassNotFoundException e) {
151+
throw new IllegalStateException("Cannot create type info mapping", e);
152+
}
153+
}
154+
155+
private static void addWellKnownTypes(TypeInfoCache cache) {
156+
cache.addCoreType("uuid", Oid.UUID, Types.OTHER, UUID.class.getName(), Oid.UUID_ARRAY);
157+
}
158+
159+
@Override
160+
Map<Class<?>, SQLType> getArrayTypeMap() {
161+
return arrayTypes;
162+
}
163+
164+
record PGSQLType(String name, int oid) implements SQLType {
165+
166+
@Override
167+
public String getName() {
168+
return name;
169+
}
170+
171+
@Override
172+
public String getVendor() {
173+
return "Postgres";
174+
}
175+
176+
@Override
177+
public Integer getVendorTypeNumber() {
178+
return oid;
179+
}
180+
}
181+
}
61182
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* JDBC-specific Dialect implementations.
3+
*/
4+
@NonNullApi
5+
package org.springframework.data.jdbc.core.dialect;
6+
7+
import org.springframework.lang.NonNullApi;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.convert;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.ArgumentMatchers.*;
20+
import static org.mockito.Mockito.*;
21+
22+
import java.sql.Array;
23+
import java.sql.SQLException;
24+
import java.util.UUID;
25+
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.ExtendWith;
28+
import org.mockito.Mock;
29+
import org.mockito.junit.jupiter.MockitoExtension;
30+
import org.postgresql.core.BaseConnection;
31+
import org.springframework.data.jdbc.core.dialect.JdbcPostgresDialect;
32+
import org.springframework.jdbc.core.ConnectionCallback;
33+
import org.springframework.jdbc.core.JdbcOperations;
34+
35+
/**
36+
* Unit tests for {@link DefaultJdbcTypeFactory}.
37+
*
38+
* @author Mark Paluch
39+
*/
40+
@ExtendWith(MockitoExtension.class)
41+
class DefaultJdbcTypeFactoryTest {
42+
43+
@Mock JdbcOperations operations;
44+
@Mock BaseConnection connection;
45+
46+
@Test // GH-1567
47+
void shouldProvidePostgresArrayType() throws SQLException {
48+
49+
DefaultJdbcTypeFactory sut = new DefaultJdbcTypeFactory(operations, JdbcPostgresDialect.INSTANCE.getArraySupport());
50+
51+
when(operations.execute(any(ConnectionCallback.class))).thenAnswer(invocation -> {
52+
53+
ConnectionCallback callback = invocation.getArgument(0, ConnectionCallback.class);
54+
return callback.doInConnection(connection);
55+
});
56+
57+
UUID uuids[] = new UUID[] { UUID.randomUUID(), UUID.randomUUID() };
58+
when(connection.createArrayOf("uuid", uuids)).thenReturn(mock(Array.class));
59+
Array array = sut.createArray(uuids);
60+
61+
assertThat(array).isNotNull();
62+
}
63+
64+
}

0 commit comments

Comments
 (0)