Skip to content

Commit 7a6e194

Browse files
committed
Allow StringCodec usage for CITEXT.
We now allow registration of the StringCodec for CITEXT usage through the UNSPECIFIED OID. [resolves #551] Signed-off-by: Mark Paluch <[email protected]>
1 parent e4bb361 commit 7a6e194

File tree

3 files changed

+141
-3
lines changed

3 files changed

+141
-3
lines changed

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,38 @@ The following types are supported for JSON exchange:
277277
* `String`
278278
* `InputStream` (must be closed after usage to avoid memory leaks)
279279

280+
## CITEXT support
281+
282+
[CITEXT](https://www.postgresql.org/docs/current/citext.html) is a built-in extension to support case-insensitive `text` columns. By default, the driver sends all string values as `VARCHAR` that cannot be used directly with `CITEXT` (without casting or converting values in your SQL).
283+
284+
If you cast input, then you can send parameters to the server without further customization of the driver:
285+
286+
```sql
287+
CREATE TABLE test (ci CITEXT);
288+
SELECT ci FROM test WHERE ci = $1::citext;
289+
```
290+
291+
If you want to send individual `String`-values in a CITEXT-compatible way, then use `Parameters.in(…)`:
292+
293+
```java
294+
connection.createStatement("SELECT ci FROM test WHERE ci = $1")
295+
.bind("$1", Parameters.in(PostgresqlObjectId.UNSPECIFIED, "Hello"))
296+
.execute();
297+
```
298+
299+
If you do not have control over the created SQL or you want to send all `String` values in a CITEXT-compatible way, then you can customize the driver configuration by registering a `StringCodec` to send `String` values with the `UNSPECIFIED` OID to let Postgres infer the value type from the provided values:
300+
301+
```java
302+
Builder builder = PostgresqlConnectionConfiguration.builder();
303+
304+
builder.codecRegistrar((connection, allocator, registry) -> {
305+
registry.addFirst(new StringCodec(allocator, PostgresqlObjectId.UNSPECIFIED, PostgresqlObjectId.VARCHAR_ARRAY));
306+
return Mono.empty();
307+
});
308+
```
309+
310+
You can register also the `CodecRegistrar` as [`Extension`](#extension-mechanism) so that it gets auto-detected during `ConnectionFactory` creation.
311+
280312
## Cursors
281313

282314
The driver can consume cursors that were created by PL/pgSQL as `refcursor`.

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
4040
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
4141

42-
final class StringCodec extends AbstractCodec<String> implements ArrayCodecDelegate<String> {
42+
public final class StringCodec extends AbstractCodec<String> implements ArrayCodecDelegate<String> {
4343

4444
static final Codec<String> STRING_DECODER = StringDecoder.INSTANCE;
4545

@@ -53,11 +53,11 @@ final class StringCodec extends AbstractCodec<String> implements ArrayCodecDeleg
5353

5454
private final PostgresTypeIdentifier arrayType;
5555

56-
StringCodec(ByteBufAllocator byteBufAllocator) {
56+
public StringCodec(ByteBufAllocator byteBufAllocator) {
5757
this(byteBufAllocator, VARCHAR, VARCHAR_ARRAY);
5858
}
5959

60-
StringCodec(ByteBufAllocator byteBufAllocator, PostgresTypeIdentifier defaultType, PostgresTypeIdentifier arrayType) {
60+
public StringCodec(ByteBufAllocator byteBufAllocator, PostgresTypeIdentifier defaultType, PostgresTypeIdentifier arrayType) {
6161
super(String.class);
6262
this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
6363
this.defaultType = Assert.requireNonNull(defaultType, "defaultType must not be null");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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.codec;
18+
19+
import io.r2dbc.postgresql.AbstractIntegrationTests;
20+
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
21+
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
22+
import io.r2dbc.postgresql.api.PostgresqlConnection;
23+
import io.r2dbc.spi.Parameters;
24+
import org.junit.jupiter.api.Test;
25+
import reactor.core.publisher.Mono;
26+
import reactor.test.StepVerifier;
27+
28+
/**
29+
* Integration tests for {@link StringCodec} usage with CITEXT and customization options.
30+
*/
31+
class StringCodecIntegrationTests extends AbstractIntegrationTests {
32+
33+
@Test
34+
void stringCodecShouldConsiderCIText() {
35+
36+
SERVER.getJdbcOperations().execute("CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public");
37+
38+
SERVER.getJdbcOperations().execute("DROP TABLE IF EXISTS test");
39+
SERVER.getJdbcOperations().execute("CREATE TABLE test (ci CITEXT, cs VARCHAR)");
40+
SERVER.getJdbcOperations().execute("INSERT INTO test VALUES('HeLlO', 'HeLlO')");
41+
42+
this.connection.createStatement("SELECT ci FROM test WHERE ci = $1::citext")
43+
.bind("$1", "Hello")
44+
.execute()
45+
.flatMap(it -> it.map(r -> r.get("ci")))
46+
.as(StepVerifier::create)
47+
.expectNext("HeLlO")
48+
.verifyComplete();
49+
50+
this.connection.createStatement("SELECT ci FROM test WHERE ci = $1")
51+
.bind("$1", Parameters.in(PostgresqlObjectId.UNSPECIFIED, "Hello"))
52+
.execute()
53+
.flatMap(it -> it.map(r -> r.get("ci")))
54+
.as(StepVerifier::create)
55+
.expectNext("HeLlO")
56+
.verifyComplete();
57+
58+
this.connection.createStatement("SELECT cs::citext = $1 FROM test")
59+
.bind("$1", Parameters.in(PostgresqlObjectId.UNSPECIFIED, "Hello"))
60+
.execute()
61+
.flatMap(it -> it.map(r -> r.get(0)))
62+
.as(StepVerifier::create)
63+
.expectNext(true)
64+
.verifyComplete();
65+
66+
this.connection.createStatement("SELECT cs::citext = $1 FROM test")
67+
.bind("$1", "Hello")
68+
.execute()
69+
.flatMap(it -> it.map(r -> r.get(0)))
70+
.as(StepVerifier::create)
71+
.expectNext(false)
72+
.verifyComplete();
73+
74+
SERVER.getJdbcOperations().execute("DROP TABLE test");
75+
}
76+
77+
@Test
78+
void shouldApplyCustomizedCodec() {
79+
80+
SERVER.getJdbcOperations().execute("CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public");
81+
82+
SERVER.getJdbcOperations().execute("DROP TABLE IF EXISTS test");
83+
SERVER.getJdbcOperations().execute("CREATE TABLE test ( ci CITEXT, cs VARCHAR)");
84+
SERVER.getJdbcOperations().execute("INSERT INTO test VALUES('HELLO', 'HELLO')");
85+
86+
PostgresqlConnectionFactory custom = getConnectionFactory(builder -> builder.codecRegistrar((connection1, allocator, registry) -> {
87+
registry.addFirst(new StringCodec(allocator, PostgresqlObjectId.UNSPECIFIED, PostgresqlObjectId.VARCHAR_ARRAY));
88+
return Mono.empty();
89+
}));
90+
91+
PostgresqlConnection customizedConnection = custom.create().block();
92+
93+
customizedConnection.createStatement("SELECT cs::citext = $1 FROM test")
94+
.bind("$1", "Hello")
95+
.execute()
96+
.flatMap(it -> it.map(r -> r.get(0)))
97+
.as(StepVerifier::create)
98+
.expectNext(true)
99+
.verifyComplete();
100+
101+
customizedConnection.close().block();
102+
103+
SERVER.getJdbcOperations().execute("DROP TABLE test");
104+
}
105+
106+
}

0 commit comments

Comments
 (0)