Skip to content

Commit 4e828b3

Browse files
committed
Introduce PreferredCodec for codec preference.
We now provide a two-pass lookup to express codec preference to handle assignable types (i.e. decoding a value with a requested Object or Number type). Previously, we relied on canDecode(…) outcome but that would render certain codecs not being able to decode values when requesting an assignable type. [resolves #693]
1 parent f6d742a commit 4e828b3

File tree

11 files changed

+111
-38
lines changed

11 files changed

+111
-38
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@ This reference table shows the type mapping between [PostgreSQL][p] and Java dat
477477
| [`xml`][psql-xml-ref] | Not yet supported. |
478478
| [`vector`][psql-vector-ref] | **`Vector`**, [`float[]`][java-float-ref] |
479479

480+
**Note on `Integer` `oid` usage**: Postgres OIDs are unsigned 32 bit integers. Make sure to consider how Java represents unsigned integers when working with large OID values or convert the value to
481+
`long` through `Integer.toUnsignedLong(int)`.
480482

481483
Types in **bold** indicate the native (default) Java type.
482484

@@ -649,7 +651,7 @@ You don't need to build from source to use R2DBC PostgreSQL (binaries in Maven C
649651
$ ./mvnw clean install
650652
```
651653

652-
If you want to build with the regular `mvn` command, you will need [Maven v3.5.0 or above](https://maven.apache.org/run-maven/index.html).
654+
If you want to build with the regular `mvn` command, you will need [Maven v3.9.0 or above](https://maven.apache.org/run-maven/index.html).
653655

654656
_Also see [CONTRIBUTING.adoc](.github/CONTRIBUTING.adoc) if you wish to submit pull requests._
655657

src/main/java/io/r2dbc/postgresql/codec/AbstractNumericCodec.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
*
4444
* @param <T> the type that is handled by this {@link Codec}.
4545
*/
46-
abstract class AbstractNumericCodec<T extends Number> extends AbstractCodec<T> implements ArrayCodecDelegate<T> {
46+
abstract class AbstractNumericCodec<T extends Number> extends AbstractCodec<T> implements ArrayCodecDelegate<T>, PreferredCodec {
4747

4848
private static final Set<PostgresqlObjectId> SUPPORTED_TYPES = EnumSet.of(INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC, OID);
4949

@@ -55,16 +55,8 @@ abstract class AbstractNumericCodec<T extends Number> extends AbstractCodec<T> i
5555
}
5656

5757
@Override
58-
public boolean canDecode(int dataType, Format format, Class<?> type) {
59-
Assert.requireNonNull(format, "format must not be null");
60-
Assert.requireNonNull(type, "type must not be null");
61-
62-
if (type == Object.class) {
63-
if (PostgresqlObjectId.isValid(dataType) && PostgresqlObjectId.valueOf(dataType) != getDefaultType()) {
64-
return false;
65-
}
66-
}
67-
return super.canDecode(dataType, format, type);
58+
public boolean isPreferred(int dataType, Format format, Class<?> type) {
59+
return isPreferenceType(type) && PostgresqlObjectId.isValid(dataType) && PostgresqlObjectId.valueOf(dataType) == getDefaultType();
6860
}
6961

7062
@Override
@@ -156,6 +148,10 @@ public Iterable<? extends PostgresTypeIdentifier> getDataTypes() {
156148
*/
157149
abstract PostgresqlObjectId getDefaultType();
158150

151+
static boolean isPreferenceType(Class<?> type) {
152+
return type == Object.class || type == Number.class;
153+
}
154+
159155
private static <T> T potentiallyConvert(Number number, Class<T> expectedType, Function<Number, T> converter) {
160156
return expectedType.isInstance(number) ? expectedType.cast(number) : converter.apply(number);
161157
}

src/main/java/io/r2dbc/postgresql/codec/AbstractTemporalCodec.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
*
4646
* @param <T> the type that is handled by this {@link Codec}
4747
*/
48-
abstract class AbstractTemporalCodec<T extends Temporal> extends BuiltinCodecSupport<T> {
48+
abstract class AbstractTemporalCodec<T extends Temporal> extends BuiltinCodecSupport<T> implements PreferredCodec {
4949

5050
private static final Set<PostgresqlObjectId> SUPPORTED_TYPES = EnumSet.of(DATE, TIMESTAMP, TIMESTAMPTZ, TIME, TIMETZ);
5151

@@ -62,16 +62,8 @@ abstract class AbstractTemporalCodec<T extends Temporal> extends BuiltinCodecSup
6262
}
6363

6464
@Override
65-
public boolean canDecode(int dataType, Format format, Class<?> type) {
66-
Assert.requireNonNull(format, "format must not be null");
67-
Assert.requireNonNull(type, "type must not be null");
68-
69-
if (type == Object.class) {
70-
if (PostgresqlObjectId.isValid(dataType) && PostgresqlObjectId.valueOf(dataType) != getDefaultType()) {
71-
return false;
72-
}
73-
}
74-
return super.canDecode(dataType, format, type);
65+
public boolean isPreferred(int dataType, Format format, Class<?> type) {
66+
return isPreferenceType(type) && PostgresqlObjectId.isValid(dataType) && PostgresqlObjectId.valueOf(dataType) == getDefaultType();
7567
}
7668

7769
@Override
@@ -166,4 +158,8 @@ static <T> T potentiallyConvert(Temporal temporal, Class<T> expectedType, Functi
166158
return expectedType.isInstance(temporal) ? expectedType.cast(temporal) : converter.apply(temporal);
167159
}
168160

161+
private static boolean isPreferenceType(Class<?> type) {
162+
return type == Object.class || type == Temporal.class;
163+
}
164+
169165
}

src/main/java/io/r2dbc/postgresql/codec/ByteCodec.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ public EncodedParameter encodeNull() {
4545
}
4646

4747
@Override
48-
boolean doCanDecode(PostgresqlObjectId type, @Nullable Format format) {
48+
public boolean canDecode(int dataType, Format format, Class<?> type) {
49+
return super.canDecode(dataType, format, type) && this.delegate.canDecode(dataType, format, Short.class);
50+
}
51+
52+
@Override
53+
boolean doCanDecode(PostgresqlObjectId type, Format format) {
4954
Assert.requireNonNull(type, "type must not be null");
5055

5156
return this.delegate.doCanDecode(type, format);

src/main/java/io/r2dbc/postgresql/codec/ConvertingArrayCodec.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.r2dbc.postgresql.message.Format;
2222
import io.r2dbc.postgresql.util.Assert;
2323

24+
import java.lang.reflect.Array;
2425
import java.util.EnumSet;
2526
import java.util.Set;
2627

@@ -43,7 +44,7 @@
4344
*
4445
* @param <T>
4546
*/
46-
final class ConvertingArrayCodec<T> extends ArrayCodec<T> {
47+
final class ConvertingArrayCodec<T> extends ArrayCodec<T> implements PreferredCodec {
4748

4849
static final Set<PostgresqlObjectId> NUMERIC_ARRAY_TYPES = EnumSet.of(INT2_ARRAY, INT4_ARRAY, INT8_ARRAY, FLOAT4_ARRAY, FLOAT8_ARRAY, NUMERIC_ARRAY, OID_ARRAY);
4950

@@ -53,25 +54,27 @@ final class ConvertingArrayCodec<T> extends ArrayCodec<T> {
5354

5455
private final Class<T> componentType;
5556

57+
private final Class<?> arrayClass;
58+
5659
private final Set<PostgresqlObjectId> supportedTypes;
5760

5861
public ConvertingArrayCodec(ByteBufAllocator byteBufAllocator, ArrayCodecDelegate<T> delegate, Class<T> componentType, Set<PostgresqlObjectId> supportedTypes) {
5962
super(byteBufAllocator, delegate, componentType);
6063
this.delegate = delegate;
6164
this.componentType = componentType;
65+
this.arrayClass = Array.newInstance(this.componentType, 0).getClass();
6266
this.supportedTypes = supportedTypes;
6367
}
6468

6569
@Override
66-
public boolean canDecode(int dataType, Format format, Class<?> type) {
67-
68-
// consider delegate priority
69-
if (type == Object.class && dataType == getDelegate().getArrayDataType().getObjectId()) {
70-
return true;
71-
}
70+
public boolean isPreferred(int dataType, Format format, Class<?> type) {
71+
return type == Object.class && dataType == getDelegate().getArrayDataType().getObjectId();
72+
}
7273

74+
@Override
75+
public boolean canDecode(int dataType, Format format, Class<?> type) {
7376
return PostgresqlObjectId.isValid(dataType) && this.supportedTypes.contains(PostgresqlObjectId.valueOf(dataType)) &&
74-
type.isArray() && getActualComponentType(type).isAssignableFrom(getComponentType());
77+
(type.isAssignableFrom(this.arrayClass) || (type.isArray() && getActualComponentType(type).isAssignableFrom(getComponentType())));
7578
}
7679

7780
@Override

src/main/java/io/r2dbc/postgresql/codec/DefaultCodecLookup.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ class DefaultCodecLookup implements CodecLookup {
4949

5050
@Override
5151
public <T> Codec<T> findDecodeCodec(int dataType, Format format, Class<? extends T> type) {
52+
Codec<T> preferred = findCodec(codec -> codec instanceof PreferredCodec && ((PreferredCodec) codec).isPreferred(dataType, format, type) && codec.canDecode(dataType, format, type));
53+
if (preferred != null) {
54+
return preferred;
55+
}
5256
return findCodec(codec -> codec.canDecode(dataType, format, type));
5357
}
5458

src/main/java/io/r2dbc/postgresql/codec/IntegerCodec.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ final class IntegerCodec extends AbstractNumericCodec<Integer> implements Primit
3131
super(Integer.class, byteBufAllocator);
3232
}
3333

34+
@Override
35+
public boolean isPreferred(int dataType, Format format, Class<?> type) {
36+
return (isPreferenceType(type) && dataType == PostgresqlObjectId.OID.getObjectId()) || super.isPreferred(dataType, format, type);
37+
}
38+
3439
@Override
3540
public PrimitiveCodec<Integer> getPrimitiveCodec() {
3641
return new PrimitiveCodec<>(Integer.TYPE, Integer.class, this);

src/main/java/io/r2dbc/postgresql/codec/IntegerCodecDelegate.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class IntegerCodecDelegate<T> extends AbstractCodec<T> {
4040
this.fromIntegerConverter = fromIntegerConverter;
4141
}
4242

43+
@Override
44+
public boolean canDecode(int dataType, Format format, Class<?> type) {
45+
return super.canDecode(dataType, format, type) && this.delegate.canDecode(dataType, format, Integer.class);
46+
}
47+
4348
@Override
4449
boolean doCanDecode(PostgresqlObjectId type, Format format) {
4550
return this.delegate.doCanDecode(type, format);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2025 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.r2dbc.postgresql.message.Format;
20+
21+
/**
22+
* Marker interface to indicate a codec expresses preference for certain data types and formats.
23+
*
24+
* @since 1.1.1
25+
*/
26+
interface PreferredCodec {
27+
28+
/**
29+
* Determine whether the codec expresses preference for the given data type, format and type.
30+
* <p>Codecs that return {@code true} should also return {@code true} from {@link Codec#canDecode(int, Format, Class)} for the same arguments to honor the API contract between the two methods.
31+
*
32+
* @param dataType the Postgres OID to decode
33+
* @param format the data type {@link Format}, text or binary
34+
* @param type the desired value type
35+
* @return {@code true} if this codec wants to be preferred to decode values for tge given {@code dataType} and {@link Format}
36+
*/
37+
boolean isPreferred(int dataType, Format format, Class<?> type);
38+
39+
}

src/test/java/io/r2dbc/postgresql/codec/DefaultCodecLookupUnitTest.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,12 @@
1919
import org.junit.jupiter.api.BeforeEach;
2020
import org.junit.jupiter.api.Test;
2121
import org.junit.jupiter.api.extension.ExtendWith;
22-
import org.mockito.ArgumentCaptor;
23-
import org.mockito.Captor;
2422
import org.mockito.Mock;
2523
import org.mockito.junit.jupiter.MockitoExtension;
2624

25+
import java.time.temporal.Temporal;
2726
import java.util.Arrays;
2827
import java.util.List;
29-
import java.util.function.Predicate;
3028

3129
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.INT2;
3230
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
@@ -44,9 +42,6 @@ class DefaultCodecLookupUnitTest {
4442

4543
DefaultCodecLookup codecFinder;
4644

47-
@Captor
48-
ArgumentCaptor<Predicate<Codec<?>>> predicateArgumentCaptor;
49-
5045
@Mock
5146
Codec<String> stringCodec;
5247

@@ -112,4 +107,15 @@ void findEncodeNullCodecNotFound() {
112107
assertThat(this.codecFinder.findEncodeNullCodec(DefaultCodecsUnitTests.class)).isNull();
113108
}
114109

110+
@Test
111+
void considersPreference() {
112+
assertThat(this.codecFinder.findDecodeCodec(PostgresqlObjectId.OID.getObjectId(), FORMAT_TEXT, Object.class)).isInstanceOf(IntegerCodec.class);
113+
assertThat(this.codecFinder.findDecodeCodec(PostgresqlObjectId.OID.getObjectId(), FORMAT_TEXT, Number.class)).isInstanceOf(IntegerCodec.class);
114+
assertThat(this.codecFinder.findDecodeCodec(PostgresqlObjectId.INT4.getObjectId(), FORMAT_TEXT, Object.class)).isInstanceOf(IntegerCodec.class);
115+
116+
assertThat(this.codecFinder.findDecodeCodec(PostgresqlObjectId.INT8.getObjectId(), FORMAT_TEXT, Object.class)).isInstanceOf(LongCodec.class);
117+
assertThat(this.codecFinder.findDecodeCodec(PostgresqlObjectId.INT8.getObjectId(), FORMAT_TEXT, Number.class)).isInstanceOf(LongCodec.class);
118+
assertThat(this.codecFinder.findDecodeCodec(PostgresqlObjectId.DATE.getObjectId(), FORMAT_TEXT, Temporal.class)).isInstanceOf(LocalDateCodec.class);
119+
}
120+
115121
}

0 commit comments

Comments
 (0)