Skip to content

Commit c269d98

Browse files
committed
Add support for Postgres enumerated types
We now provide a EnumCodec to map Java types to Postgres enumerated types. [closes #244]
1 parent c91fb4c commit c269d98

8 files changed

+594
-4
lines changed

README.md

+35-4
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ CREATE TABLE my_table (my_json JSON);
201201
```java
202202
connection.createStatement("INSERT INTO my_table (my_json) VALUES($1)")
203203
.bind("$1", Json.of("{\"hello\": \"world\"}")).execute();
204-
```
204+
```
205+
205206
**Consume JSON**
206207

207208
```java
@@ -215,7 +216,7 @@ connection.createStatement("SELECT my_json FROM my_table")
215216
```java
216217
connection.createStatement("INSERT INTO my_table (my_json) VALUES($1::JSON)")
217218
.bind("$1", "{\"hello\": \"world\"}").execute();
218-
```
219+
```
219220

220221
**Consume JSON as scalar type**
221222

@@ -251,7 +252,6 @@ On application shutdown, `close()` the `ReplicationStream`.
251252
Note that a connection is busy once the replication is active and a connection can have at most one active replication stream.
252253

253254
```java
254-
255255
Mono<PostgresqlReplicationConnection> replicationMono = connectionFactory.replication();
256256

257257
// later:
@@ -275,6 +275,35 @@ Flux<T> replicationStream = replicationConnection.startReplication(replicationRe
275275
});
276276
```
277277

278+
## Postgres Enum Types
279+
280+
Applications may make use of Postgres enumerated types by using `EnumCodec` to map custom types to Java `enum` types.
281+
`EnumCodec` requires the Postgres OID and the Java to map enum values to the Postgres protocol and to materialize Enum instances from Postgres results.
282+
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.
283+
284+
Example:
285+
286+
**SQL:**
287+
288+
```sql
289+
CREATE TYPE my_enum AS ENUM ('FIRST', 'SECOND');
290+
```
291+
292+
**Java Model:**
293+
294+
```java
295+
enum MyEnumType {
296+
FIRST, SECOND;
297+
}
298+
```
299+
300+
**Codec Registration:**
301+
302+
```java
303+
PostgresqlConnectionConfiguration.builder()
304+
.codecRegistrar(EnumCodec.builder().withEnum("my_enum", MyEnumType.class).build());
305+
```
306+
278307
## Data Type Mapping
279308

280309
This reference table shows the type mapping between [PostgreSQL][p] and Java data types:
@@ -293,6 +322,7 @@ This reference table shows the type mapping between [PostgreSQL][p] and Java dat
293322
| [`circle`][psql-circle-ref] | Not yet supported.|
294323
| [`date`][psql-date-ref] | [`LocalDate`][java-ld-ref]|
295324
| [`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]|
325+
| [enumerated types][psql-enum-ref] | Client code `Enum` types through `EnumCodec`|
296326
| [`hstore`][psql-hstore-ref] | [**`Map`**][java-map-ref]|
297327
| [`inet`][psql-inet-ref] | [**`InetAddress`**][java-inet-ref]|
298328
| [`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]|
@@ -345,6 +375,7 @@ Support for the following single-dimensional arrays (read and write):
345375
[psql-circle-ref]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-CIRCLE
346376
[psql-date-ref]: https://www.postgresql.org/docs/current/datatype-datetime.html
347377
[psql-floating-point-ref]: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-FLOAT
378+
[psql-enum-ref]: https://www.postgresql.org/docs/current/datatype-enum.html
348379
[psql-hstore-ref]: https://www.postgresql.org/docs/current/hstore.html
349380
[psql-inet-ref]: https://www.postgresql.org/docs/current/datatype-net-types.html#DATATYPE-INET
350381
[psql-integer-ref]: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-INT
@@ -408,7 +439,7 @@ This driver accepts the following extensions:
408439

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

411-
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.
442+
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.
412443

413444
## Logging
414445
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
@@ -370,6 +370,7 @@ public Builder connectTimeout(@Nullable Duration connectTimeout) {
370370

371371
/**
372372
* Registers a {@link CodecRegistrar} that can contribute extension {@link Codec}s.
373+
* Calling this method adds a {@link CodecRegistrar} and does not replace existing {@link Extension}s.
373374
*
374375
* @param codecRegistrar registrar to contribute codecs
375376
* @return this {@link Builder}
@@ -400,6 +401,7 @@ public Builder enableSsl() {
400401

401402
/**
402403
* Registers a {@link Extension} to extend driver functionality.
404+
* Calling this method adds a {@link Extension} and does not replace existing {@link Extension}s.
403405
*
404406
* @param extension extension to extend driver functionality
405407
* @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)