Skip to content

Commit fe652ff

Browse files
committed
Use binary WKB representation including SRID in PostgisGeometryCodec.
We now use WKBWriter with two dimensions preserving the SRID. Add integration tests for Postgis. [resolves #542] Signed-off-by: Mark Paluch <[email protected]>
1 parent bb59714 commit fe652ff

File tree

5 files changed

+140
-58
lines changed

5 files changed

+140
-58
lines changed

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public class BuiltinDynamicCodecs implements CodecRegistrar {
3333

3434
private static final Object EMPTY = new Object();
3535

36-
3736
enum BuiltinCodec {
3837

3938
HSTORE("hstore"),
@@ -59,7 +58,7 @@ public Codec<?> createCodec(ByteBufAllocator byteBufAllocator, int oid) {
5958
case HSTORE:
6059
return new HStoreCodec(byteBufAllocator, oid);
6160
case POSTGIS_GEOMETRY:
62-
return new PostgisGeometryCodec(byteBufAllocator, oid);
61+
return new PostgisGeometryCodec(oid);
6362
default:
6463
throw new UnsupportedOperationException(String.format("Codec %s for OID %d not supported", name(), oid));
6564
}

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

+10-10
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package io.r2dbc.postgresql.codec;
1818

1919
import io.netty.buffer.ByteBuf;
20-
import io.netty.buffer.ByteBufAllocator;
20+
import io.netty.buffer.Unpooled;
2121
import io.r2dbc.postgresql.client.EncodedParameter;
2222
import io.r2dbc.postgresql.message.Format;
2323
import io.r2dbc.postgresql.util.Assert;
@@ -26,6 +26,7 @@
2626
import org.locationtech.jts.geom.GeometryFactory;
2727
import org.locationtech.jts.io.ParseException;
2828
import org.locationtech.jts.io.WKBReader;
29+
import org.locationtech.jts.io.WKBWriter;
2930
import org.locationtech.jts.io.WKTWriter;
3031
import reactor.core.publisher.Mono;
3132

@@ -43,19 +44,14 @@ final class PostgisGeometryCodec implements Codec<Geometry>, CodecMetadata {
4344

4445
private static final Class<Geometry> TYPE = Geometry.class;
4546

46-
private final ByteBufAllocator byteBufAllocator;
47-
4847
private final GeometryFactory geometryFactory = new GeometryFactory();
4948

5049
private final int oid;
5150

5251
/**
5352
* Create a new {@link PostgisGeometryCodec}.
54-
*
55-
* @param byteBufAllocator the type handled by this codec
5653
*/
57-
PostgisGeometryCodec(ByteBufAllocator byteBufAllocator, int oid) {
58-
this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
54+
PostgisGeometryCodec(int oid) {
5955
this.oid = oid;
6056
}
6157

@@ -64,7 +60,8 @@ public boolean canDecode(int dataType, Format format, Class<?> type) {
6460
Assert.requireNonNull(format, "format must not be null");
6561
Assert.requireNonNull(type, "type must not be null");
6662

67-
return dataType == this.oid && TYPE.isAssignableFrom(type);
63+
// Object = Geometry or Geometry = type (Geometry subtype)
64+
return dataType == this.oid && (type.isAssignableFrom(TYPE) || TYPE.isAssignableFrom(type));
6865
}
6966

7067
@Override
@@ -101,8 +98,10 @@ public EncodedParameter encode(Object value) {
10198
Assert.requireType(value, Geometry.class, "value must be Geometry type");
10299
Geometry geometry = (Geometry) value;
103100

104-
return new EncodedParameter(Format.FORMAT_TEXT, this.oid, Mono.fromSupplier(
105-
() -> ByteBufUtils.encode(this.byteBufAllocator, geometry.toText())
101+
WKBWriter writer = new WKBWriter(2, true);
102+
103+
return new EncodedParameter(FORMAT_BINARY, this.oid, Mono.fromSupplier(
104+
() -> Unpooled.wrappedBuffer(writer.write(geometry))
106105
));
107106
}
108107

@@ -125,4 +124,5 @@ public Class<?> type() {
125124
public Iterable<PostgresTypeIdentifier> getDataTypes() {
126125
return Collections.singleton(AbstractCodec.getDataType(this.oid));
127126
}
127+
128128
}

src/test/java/io/r2dbc/postgresql/AbstractIntegrationTests.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ void setUp() {
8181

8282
@AfterEach
8383
void tearDown() {
84-
this.connection.close().as(StepVerifier::create).verifyComplete();
84+
if (this.connection != null) {
85+
this.connection.close().as(StepVerifier::create).verifyComplete();
86+
}
8587
}
8688

8789
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2022 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;
18+
19+
import org.junit.jupiter.api.BeforeEach;
20+
import org.junit.jupiter.api.Test;
21+
import org.locationtech.jts.geom.CoordinateSequence;
22+
import org.locationtech.jts.geom.GeometryFactory;
23+
import org.locationtech.jts.geom.Point;
24+
import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory;
25+
import org.springframework.dao.DataAccessException;
26+
import org.springframework.jdbc.core.JdbcOperations;
27+
import reactor.test.StepVerifier;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.assertj.core.api.Assumptions.assumeThat;
31+
32+
/**
33+
* Integration tests for PostGIS functionality.
34+
*/
35+
final class PostgisIntegrationTests extends AbstractIntegrationTests {
36+
37+
@Override
38+
@BeforeEach
39+
void setUp() {
40+
41+
JdbcOperations jdbcOperations = SERVER.getJdbcOperations();
42+
43+
try {
44+
jdbcOperations.execute("CREATE EXTENSION postgis");
45+
} catch (DataAccessException e) {
46+
// ignore
47+
}
48+
49+
try {
50+
jdbcOperations.queryForMap("SELECT postgis_full_version()");
51+
} catch (DataAccessException e) {
52+
assumeThat(e).isNull();
53+
}
54+
55+
super.setUp();
56+
}
57+
58+
@Test
59+
void shouldWriteAndReadPointWithSRID() {
60+
61+
JdbcOperations jdbcOperations = SERVER.getJdbcOperations();
62+
63+
jdbcOperations.execute("DROP TABLE IF EXISTS geo_test");
64+
jdbcOperations.execute("CREATE TABLE geo_test (geom geometry)");
65+
66+
PackedCoordinateSequenceFactory csFactory = new PackedCoordinateSequenceFactory();
67+
68+
CoordinateSequence cs = csFactory.create(1, 2);
69+
// initialize with a data signature where coords look like [1, 10, 100, ...]
70+
for (int i = 0; i < 1; i++) {
71+
for (int d = 0; d < 2; d++) {
72+
cs.setOrdinate(i, d, i + 1 * Math.pow(10, d + 1));
73+
}
74+
}
75+
Point point = new Point(cs, new GeometryFactory());
76+
point.setSRID(4210);
77+
78+
this.connection.createStatement("INSERT INTO geo_test VALUES($1)").bind("$1", point).execute().flatMap(it -> it.getRowsUpdated()).then().as(StepVerifier::create).verifyComplete();
79+
80+
this.connection.createStatement("SELECT * FROM geo_test").execute().flatMap(it -> it.map(row -> row.get("geom"))).as(StepVerifier::create).consumeNextWith(actual -> {
81+
assertThat(actual).isEqualTo(point);
82+
assertThat(((Point) actual).getSRID()).isEqualTo(point.getSRID());
83+
}).verifyComplete();
84+
}
85+
86+
}

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

+40-45
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.r2dbc.postgresql.codec;
1818

1919
import io.netty.buffer.ByteBuf;
20+
import io.netty.buffer.Unpooled;
2021
import io.r2dbc.postgresql.client.EncodedParameter;
2122
import io.r2dbc.postgresql.client.ParameterAssert;
2223
import io.r2dbc.postgresql.util.ByteBufUtils;
@@ -54,106 +55,100 @@ final class PostgisGeometryCodecUnitTests {
5455

5556
private static final int dataType = 23456;
5657

57-
private final PostgisGeometryCodec codec = new PostgisGeometryCodec(TEST, dataType);
58+
private final PostgisGeometryCodec codec = new PostgisGeometryCodec(dataType);
5859

5960
private final WKBWriter wkbWriter = new WKBWriter();
6061

6162
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), WGS84_SRID);
6263

63-
private final Point point = geometryFactory.createPoint(new Coordinate(1.0, 1.0));
64-
65-
@Test
66-
void constructorNoByteBufAllocator() {
67-
assertThatIllegalArgumentException().isThrownBy(() -> new PostgisGeometryCodec(null, dataType))
68-
.withMessage("byteBufAllocator must not be null");
69-
}
64+
private final Point point = this.geometryFactory.createPoint(new Coordinate(1.0, 1.0));
7065

7166
@Test
7267
void canDecodeNoFormat() {
73-
assertThatIllegalArgumentException().isThrownBy(() -> codec.canDecode(dataType, null, Geometry.class))
68+
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canDecode(dataType, null, Geometry.class))
7469
.withMessage("format must not be null");
7570
}
7671

7772
@Test
7873
void canDecodeNoClass() {
79-
assertThatIllegalArgumentException().isThrownBy(() -> codec.canDecode(dataType, FORMAT_TEXT, null))
74+
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canDecode(dataType, FORMAT_TEXT, null))
8075
.withMessage("type must not be null");
8176
}
8277

8378
@Test
8479
void canDecode() {
85-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
86-
assertThat(codec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();
87-
88-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
89-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
90-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
91-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
92-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
93-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
94-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
95-
assertThat(codec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();
96-
97-
assertThat(codec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
98-
assertThat(codec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
99-
assertThat(codec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
80+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
81+
assertThat(this.codec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();
82+
83+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
84+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
85+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
86+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
87+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
88+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
89+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
90+
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();
91+
92+
assertThat(this.codec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
93+
assertThat(this.codec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
94+
assertThat(this.codec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
10095
}
10196

10297
@Test
10398
void canEncodeNoValue() {
104-
assertThatIllegalArgumentException().isThrownBy(() -> codec.canEncode(null))
99+
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canEncode(null))
105100
.withMessage("value must not be null");
106101
}
107102

108103
@Test
109104
void canEncode() {
110-
assertThat(codec.canEncode(geometryFactory.createPoint())).isTrue();
111-
assertThat(codec.canEncode(geometryFactory.createMultiPoint())).isTrue();
112-
assertThat(codec.canEncode(geometryFactory.createLineString())).isTrue();
113-
assertThat(codec.canEncode(geometryFactory.createLinearRing())).isTrue();
114-
assertThat(codec.canEncode(geometryFactory.createMultiLineString())).isTrue();
115-
assertThat(codec.canEncode(geometryFactory.createPolygon())).isTrue();
116-
assertThat(codec.canEncode(geometryFactory.createMultiPolygon())).isTrue();
117-
assertThat(codec.canEncode(geometryFactory.createGeometryCollection())).isTrue();
118-
119-
assertThat(codec.canEncode("Geometry")).isFalse();
120-
assertThat(codec.canEncode(1)).isFalse();
105+
assertThat(this.codec.canEncode(this.geometryFactory.createPoint())).isTrue();
106+
assertThat(this.codec.canEncode(this.geometryFactory.createMultiPoint())).isTrue();
107+
assertThat(this.codec.canEncode(this.geometryFactory.createLineString())).isTrue();
108+
assertThat(this.codec.canEncode(this.geometryFactory.createLinearRing())).isTrue();
109+
assertThat(this.codec.canEncode(this.geometryFactory.createMultiLineString())).isTrue();
110+
assertThat(this.codec.canEncode(this.geometryFactory.createPolygon())).isTrue();
111+
assertThat(this.codec.canEncode(this.geometryFactory.createMultiPolygon())).isTrue();
112+
assertThat(this.codec.canEncode(this.geometryFactory.createGeometryCollection())).isTrue();
113+
114+
assertThat(this.codec.canEncode("Geometry")).isFalse();
115+
assertThat(this.codec.canEncode(1)).isFalse();
121116
}
122117

123118
@Test
124119
@SuppressWarnings("unchecked")
125120
void decode() {
126-
byte[] pointBytes = wkbWriter.write(point);
121+
byte[] pointBytes = this.wkbWriter.write(this.point);
127122
ByteBuf pointByteBuf = ByteBufUtils.encode(TEST, WKBWriter.toHex(pointBytes));
128123

129-
assertThat(codec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(point);
124+
assertThat(this.codec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point);
130125
}
131126

132127
@Test
133128
@SuppressWarnings("unchecked")
134129
void decodeNoByteBuf() {
135-
assertThat(codec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
130+
assertThat(this.codec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
136131
}
137132

138133
@Test
139134
void encode() {
140-
ByteBuf encoded = ByteBufUtils.encode(TEST, point.toText());
135+
ByteBuf encoded = Unpooled.wrappedBuffer(new WKBWriter(2, true).write(this.point));
141136

142-
ParameterAssert.assertThat(codec.encode(point))
143-
.hasFormat(FORMAT_TEXT)
137+
ParameterAssert.assertThat(this.codec.encode(this.point))
138+
.hasFormat(FORMAT_BINARY)
144139
.hasType(dataType)
145140
.hasValue(encoded);
146141
}
147142

148143
@Test
149144
void encodeNoValue() {
150-
assertThatIllegalArgumentException().isThrownBy(() -> codec.encode(null))
145+
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.encode(null))
151146
.withMessage("value must not be null");
152147
}
153148

154149
@Test
155150
void encodeNull() {
156-
assertThat(new PostgisGeometryCodec(TEST, dataType).encodeNull())
151+
assertThat(new PostgisGeometryCodec(dataType).encodeNull())
157152
.isEqualTo(new EncodedParameter(FORMAT_BINARY, dataType, NULL_VALUE));
158153
}
159154

0 commit comments

Comments
 (0)