Skip to content

Commit 6e642f5

Browse files
committed
Add support for Postgres enumerated types
Add support for Postgres enumerated types [#244][#284]
1 parent 3c7ca5c commit 6e642f5

8 files changed

+591
-2
lines changed

README.md

+32-2
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,6 @@ On application shutdown, `close()` the `ReplicationStream`.
274274
Note that a connection is busy once the replication is active and a connection can have at most one active replication stream.
275275

276276
```java
277-
278277
Mono<PostgresqlReplicationConnection> replicationMono = connectionFactory.replication();
279278

280279
// later:
@@ -298,6 +297,35 @@ Flux<T> replicationStream = replicationConnection.startReplication(replicationRe
298297
});
299298
```
300299

300+
## Postgres Enum Types
301+
302+
Applications may make use of Postgres enumerated types by using `EnumCodec` to map custom types to Java `enum` types.
303+
`EnumCodec` requires the Postgres OID and the Java to map enum values to the Postgres protocol and to materialize Enum instances from Postgres results.
304+
You can configure a `CodecRegistrar` through `EnumCodec.builder()` for one or more enumeration type mappings. Make sure to use different Java enum types otherwise the driver is not able to distinguish between Postgres OIDs.
305+
306+
Example:
307+
308+
**SQL:**
309+
310+
```sql
311+
CREATE TYPE my_enum AS ENUM ('FIRST', 'SECOND');
312+
```
313+
314+
**Java Model:**
315+
316+
```java
317+
enum MyEnumType {
318+
FIRST, SECOND;
319+
}
320+
```
321+
322+
**Codec Registration:**
323+
324+
```java
325+
PostgresqlConnectionConfiguration.builder()
326+
.codecRegistrar(EnumCodec.builder().withEnum("my_enum", MyEnumType.class).build());
327+
```
328+
301329
## Data Type Mapping
302330

303331
This reference table shows the type mapping between [PostgreSQL][p] and Java data types:
@@ -316,6 +344,7 @@ This reference table shows the type mapping between [PostgreSQL][p] and Java dat
316344
| [`circle`][psql-circle-ref] | Not yet supported.|
317345
| [`date`][psql-date-ref] | [`LocalDate`][java-ld-ref]|
318346
| [`double precision`][psql-floating-point-ref] | [**`Double`**][java-double-ref], [`Float`][java-float-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]|
347+
| [enumerated types][psql-enum-ref] | Client code `Enum` types through `EnumCodec`|
319348
| [`hstore`][psql-hstore-ref] | [**`Map`**][java-map-ref]|
320349
| [`inet`][psql-inet-ref] | [**`InetAddress`**][java-inet-ref]|
321350
| [`integer`][psql-integer-ref] | [**`Integer`**][java-integer-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]|
@@ -368,6 +397,7 @@ Support for the following single-dimensional arrays (read and write):
368397
[psql-circle-ref]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-CIRCLE
369398
[psql-date-ref]: https://www.postgresql.org/docs/current/datatype-datetime.html
370399
[psql-floating-point-ref]: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-FLOAT
400+
[psql-enum-ref]: https://www.postgresql.org/docs/current/datatype-enum.html
371401
[psql-hstore-ref]: https://www.postgresql.org/docs/current/hstore.html
372402
[psql-inet-ref]: https://www.postgresql.org/docs/current/datatype-net-types.html#DATATYPE-INET
373403
[psql-integer-ref]: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-INT
@@ -431,7 +461,7 @@ This driver accepts the following extensions:
431461

432462
Extensions can be registered programmatically using `PostgresConnectionConfiguration` or discovered using Java's `ServiceLoader` mechanism (from `META-INF/services/io.r2dbc.postgresql.extension.Extension`).
433463

434-
The driver ships with built-in dynamic codecs (e.g. `hstore`) that are registered during the connection handshake depending on their availability while connecting. Note that Postgres extensions registered after a connection was established require a reconnect to initialize the codec.
464+
The driver ships with built-in dynamic codecs (e.g. `hstore`) that are registered during the connection handshake depending on their availability while connecting. Note that Postgres extensions registered after a connection was established require a reconnect to initialize the codec.
435465

436466
## Logging
437467
If SL4J is on the classpath, it will be used. Otherwise, there are two possible fallbacks: Console or `java.util.logging.Logger`). By default, the Console fallback is used. To use the JDK loggers, set the `reactor.logging.fallback` System property to `JDK`.

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java

+2
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ public Builder connectTimeout(@Nullable Duration connectTimeout) {
396396

397397
/**
398398
* Register a {@link CodecRegistrar} that can contribute extension {@link Codec}s.
399+
* Calling this method adds a {@link CodecRegistrar} and does not replace existing {@link Extension}s.
399400
*
400401
* @param codecRegistrar registrar to contribute codecs
401402
* @return this {@link Builder}
@@ -426,6 +427,7 @@ public Builder enableSsl() {
426427

427428
/**
428429
* Registers a {@link Extension} to extend driver functionality.
430+
* Calling this method adds a {@link Extension} and does not replace existing {@link Extension}s.
429431
*
430432
* @param extension extension to extend driver functionality
431433
* @return this {@link Builder}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright 2020 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+
17+
package io.r2dbc.postgresql.codec;
18+
19+
import io.netty.buffer.ByteBuf;
20+
import io.netty.buffer.ByteBufAllocator;
21+
import io.r2dbc.postgresql.client.Parameter;
22+
import io.r2dbc.postgresql.extension.CodecRegistrar;
23+
import io.r2dbc.postgresql.message.Format;
24+
import io.r2dbc.postgresql.util.Assert;
25+
import io.r2dbc.postgresql.util.ByteBufUtils;
26+
import reactor.core.publisher.Mono;
27+
import reactor.util.Logger;
28+
import reactor.util.Loggers;
29+
import reactor.util.annotation.Nullable;
30+
31+
import java.util.ArrayList;
32+
import java.util.LinkedHashMap;
33+
import java.util.List;
34+
import java.util.Map;
35+
36+
import static io.r2dbc.postgresql.client.Parameter.NULL_VALUE;
37+
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
38+
39+
/**
40+
* Codec to map Postgres {@code enumerated} types to Java {@link Enum} values.
41+
* This codec uses {@link Enum#name()} to map Postgres enum values as these are represented as string values.
42+
* <p>Note that enum values are case-sensitive.
43+
*
44+
* @param <T> enum type
45+
* @since 0.8.4
46+
*/
47+
public final class EnumCodec<T extends Enum<T>> implements Codec<T> {
48+
49+
private static final Logger logger = Loggers.getLogger(EnumCodec.class);
50+
51+
private final ByteBufAllocator byteBufAllocator;
52+
53+
private final Class<T> type;
54+
55+
private final int oid;
56+
57+
public EnumCodec(ByteBufAllocator byteBufAllocator, Class<T> type, int oid) {
58+
this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
59+
this.type = Assert.requireNonNull(type, "type must not be null");
60+
this.oid = oid;
61+
}
62+
63+
@Override
64+
public boolean canDecode(int dataType, Format format, Class<?> type) {
65+
Assert.requireNonNull(type, "type must not be null");
66+
return this.type.equals(type) && dataType == this.oid;
67+
}
68+
69+
@Override
70+
public boolean canEncode(Object value) {
71+
Assert.requireNonNull(value, "value must not be null");
72+
return this.type.isInstance(value);
73+
}
74+
75+
@Override
76+
public boolean canEncodeNull(Class<?> type) {
77+
Assert.requireNonNull(type, "type must not be null");
78+
return this.type.equals(type);
79+
}
80+
81+
@Override
82+
public T decode(@Nullable ByteBuf buffer, int dataType, Format format, Class<? extends T> type) {
83+
if (buffer == null) {
84+
return null;
85+
}
86+
87+
return Enum.valueOf(this.type, ByteBufUtils.decode(buffer));
88+
}
89+
90+
@Override
91+
public Parameter encode(Object value) {
92+
93+
Assert.requireNonNull(value, "value must not be null");
94+
95+
return new Parameter(FORMAT_TEXT, this.oid, Mono.fromSupplier(() -> ByteBufUtils.encode(this.byteBufAllocator, this.type.cast(value).name())));
96+
}
97+
98+
@Override
99+
public Parameter encodeNull() {
100+
return new Parameter(Format.FORMAT_BINARY, this.oid, NULL_VALUE);
101+
}
102+
103+
@Override
104+
public Class<?> type() {
105+
return this.type;
106+
}
107+
108+
/**
109+
* Create a new {@link Builder} to build a {@link CodecRegistrar} to dynamically register Postgres {@code enum} types to {@link Enum} values.
110+
*
111+
* @return a new builder.
112+
*/
113+
public static EnumCodec.Builder builder() {
114+
return new Builder();
115+
}
116+
117+
/**
118+
* Builder for {@link CodecRegistrar} to register {@link EnumCodec} for one or more enum type mappings.
119+
*/
120+
public static final class Builder {
121+
122+
private final Map<String, Class<? extends Enum<?>>> mapping = new LinkedHashMap<>();
123+
124+
/**
125+
* Add a Postgres enum type to {@link Enum} mapping.
126+
*
127+
* @param name name of the Postgres enum type
128+
* @param enumClass the corresponding Java type
129+
* @return this {@link Builder}
130+
*/
131+
public Builder withEnum(String name, Class<? extends Enum<?>> enumClass) {
132+
Assert.requireNonNull(enumClass, "Enum class must not be null");
133+
Assert.isTrue(enumClass.isEnum(), String.format("Enum class %s must be an enum type", enumClass.getName()));
134+
135+
if (this.mapping.containsKey(name)) {
136+
throw new IllegalArgumentException(String.format("Builder contains already a mapping for Postgres type %s", name));
137+
}
138+
139+
if (this.mapping.containsValue(enumClass)) {
140+
throw new IllegalArgumentException(String.format("Builder contains already a mapping for Java type %s", enumClass.getName()));
141+
}
142+
143+
this.mapping.put(Assert.requireNotEmpty(name, "Postgres type name must not be null"), enumClass);
144+
return this;
145+
}
146+
147+
/**
148+
* Build a {@link CodecRegistrar} to be used with {@code PostgresqlConnectionConfiguration.Builder#codecRegistrar(CodecRegistrar)}.
149+
* The codec registrar registers the codes to be used as part of the connection setup.
150+
*
151+
* @return a new {@link CodecRegistrar}.
152+
*/
153+
@SuppressWarnings({"unchecked", "rawtypes"})
154+
public CodecRegistrar build() {
155+
156+
Map<String, Class<? extends Enum<?>>> mapping = new LinkedHashMap<>(this.mapping);
157+
158+
return (connection, allocator, registry) -> {
159+
160+
List<String> missing = new ArrayList<>(mapping.keySet());
161+
return PostgresTypes.from(connection).lookupTypes(mapping.keySet())
162+
.filter(PostgresTypes.PostgresType::isEnum)
163+
.doOnNext(it -> {
164+
165+
Class<? extends Enum<?>> enumClass = mapping.get(it.getName());
166+
if (enumClass == null) {
167+
logger.warn(String.format("Cannot find Java type for enum type '%s' with oid %d. Known types are: %s", it.getName(), it.getOid(), mapping));
168+
return;
169+
}
170+
171+
missing.remove(it.getName());
172+
logger.debug(String.format("Registering codec for type '%s' with oid %d using Java enum type '%s'", it.getName(), it.getOid(), enumClass.getName()));
173+
registry.addLast(new EnumCodec(allocator, enumClass, it.getOid()));
174+
}).doOnComplete(() -> {
175+
176+
if (!missing.isEmpty()) {
177+
logger.warn(String.format("Could not lookup enum types for: %s", missing));
178+
}
179+
180+
}).then();
181+
};
182+
}
183+
}
184+
}

0 commit comments

Comments
 (0)