Skip to content

Commit 871339d

Browse files
committed
Add integration for MariaDB
- Add MariaDB workflows - Correct `BitSetCodec` in client-preparing for MariaDB, it cannot select `BIT` by HEX string - Correct JSON test cases for MariaDB, it responds `TEXT` for `JSON` type - Add `TEXT` integration test to avoid potential bugs due to differences between MySQL and MariaDB - Correct README about `BIT` and `TEXT`
1 parent 550f8c3 commit 871339d

File tree

12 files changed

+184
-64
lines changed

12 files changed

+184
-64
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Integration Tests for MariaDB
2+
3+
on:
4+
pull_request:
5+
branches: [ "trunk", "0.9.x" ]
6+
7+
jobs:
8+
mariadb-integration-tests-pr:
9+
runs-on: ubuntu-20.04
10+
strategy:
11+
matrix:
12+
mariadb-version: [ 10.6, 10.11 ]
13+
name: Integration test with MariaDB ${{ matrix.mariadb-version }}
14+
steps:
15+
- uses: actions/checkout@v3
16+
- name: Set up Temurin 8
17+
uses: actions/setup-java@v3
18+
with:
19+
distribution: temurin
20+
java-version: 8
21+
cache: maven
22+
- name: Shutdown the Default MySQL
23+
run: sudo service mysql stop
24+
- name: Set up MariaDB ${{ matrix.mariadb-version }}
25+
env:
26+
MYSQL_DATABASE: r2dbc
27+
MYSQL_ROOT_PASSWORD: r2dbc-password!@
28+
MARIADB_VERSION: ${{ matrix.mariadb-version }}
29+
run: docker-compose -f ${{ github.workspace }}/containers/mariadb-compose.yml up -d
30+
- name: Integration test with MySQL ${{ matrix.mysql-version }}
31+
run: |
32+
./mvnw -B verify -Dmaven.javadoc.skip=true \
33+
-Dmaven.surefire.skip=true \
34+
-Dtest.mysql.password=r2dbc-password!@ \
35+
-Dtest.mysql.version=${{ matrix.mariadb-version }} \
36+
-Dtest.db.type=mariadb \
37+
-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN

.github/workflows/ci-unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-20.04
1010
strategy:
1111
matrix:
12-
java-version: [ 8, 11, 17 , 21]
12+
java-version: [ 8, 11, 17, 21 ]
1313
name: linux-java-${{ matrix.java-version }}
1414
steps:
1515
- uses: actions/checkout@v3

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ This project is currently being maintained by [@jchrys](https://github.com/jchry
4343
![MySQL 8.0 status](https://img.shields.io/badge/MySQL%208.0-pass-blue)
4444
![MySQL 8.1 status](https://img.shields.io/badge/MySQL%208.1-pass-blue)
4545
![MySQL 8.2 status](https://img.shields.io/badge/MySQL%208.2-pass-blue)
46-
46+
![MariaDB 10.6 status](https://img.shields.io/badge/MariaDB%2010.6-pass-blue)
47+
![MariaDB 10.11 status](https://img.shields.io/badge/MariaDB%2010.11-pass-blue)
4748

4849

4950
In fact, it supports lower versions, in the theory, such as 4.1, 4.0, etc.
@@ -546,7 +547,9 @@ If you want to raise an issue, please follow the recommendations below:
546547
- The MySQL server does not **actively** return time zone when query `DATETIME` or `TIMESTAMP`, this driver does not attempt time zone conversion. That means should always use `LocalDateTime` for SQL type `DATETIME` or `TIMESTAMP`. Execute `SHOW VARIABLES LIKE '%time_zone%'` to get more information.
547548
- Should not turn-on the `trace` log level unless debugging. Otherwise, the security information may be exposed through `ByteBuf` dump.
548549
- If `Statement` bound `returnGeneratedValues`, the `Result` of the `Statement` can be called both: `getRowsUpdated` to get affected rows, and `map` to get last inserted ID.
549-
- The MySQL may be not support search rows by a binary field, like `BIT`, `BLOB` and `JSON`, because those data fields maybe just an address of reference in MySQL server, or maybe need strict bit-aligned. (but `VARBINARY` is OK)
550+
- The MySQL may be not support well for searching rows by a binary field, like `BIT` and `JSON`
551+
- `BIT`: cannot select 'BIT(64)' with value greater than 'Long.MAX_VALUE' (or equivalent in binary)
552+
- `JSON`: different MySQL may have different serialization formats, e.g. MariaDB and MySQL
550553

551554
## License
552555

containers/mariadb-compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: "3"
2+
3+
services:
4+
mariadb:
5+
image: mariadb:${MARIADB_VERSION}
6+
container_name: mariadb_${MARIADB_VERSION}
7+
environment:
8+
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
9+
MYSQL_DATABASE: ${MYSQL_DATABASE}
10+
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
11+
ports:
12+
- "3306:3306"

src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,24 @@ public abstract class ParameterWriter extends Writer {
4848

4949
/**
5050
* Writes a value of {@code long} to current parameter. If current mode is string mode, it will write as a
51-
* string like {@code write(String.valueOf(value))}. If it write as a numeric, nothing else can be written
52-
* before or after this.
51+
* string like {@code write(String.valueOf(value))}. If it writes as a numeric, nothing else can be
52+
* written before or after this.
5353
*
5454
* @param value the value of {@code long}.
5555
* @throws IllegalStateException if parameters filled, or something was written before that numeric.
5656
*/
5757
public abstract void writeLong(long value);
5858

59+
/**
60+
* Writes a value as an unsigned {@code long} to current parameter. If current mode is string mode, it
61+
* will write as a string like {@code write(String.valueOf(value))}. If it writes as a numeric, nothing
62+
* else can be written before or after this.
63+
*
64+
* @param value the value as an unsigned {@code long}.
65+
* @throws IllegalStateException if parameters filled, or something was written before that numeric.
66+
*/
67+
public abstract void writeUnsignedLong(long value);
68+
5969
/**
6070
* Writes a value of {@link BigInteger} to current parameter. If current mode is string mode, it will
6171
* write as a string like {@code write(value.toString())}. If it write as a numeric, nothing else can be

src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ public MySqlParameter encode(Object value, CodecContext context) {
7373

7474
MySqlType type;
7575

76-
if ((byte) bits == bits) {
76+
if (bits < 0) {
77+
type = MySqlType.BIGINT;
78+
} else if ((byte) bits == bits) {
7779
type = MySqlType.TINYINT;
7880
} else if ((short) bits == bits) {
7981
type = MySqlType.SMALLINT;
@@ -135,17 +137,7 @@ public Mono<ByteBuf> publishBinary() {
135137

136138
@Override
137139
public Mono<Void> publishText(ParameterWriter writer) {
138-
return Mono.fromRunnable(() -> {
139-
if (value == 0) {
140-
// Must filled by 0 for MySQL 5.5.x, because MySQL 5.5.x does not clear its buffer on type
141-
// BIT (i.e. unsafe allocate).
142-
// So if we do not fill the buffer, it will use last content which is an undefined
143-
// behavior. A classic bug, right?
144-
writer.writeBinary(false);
145-
} else {
146-
writer.writeHex(value);
147-
}
148-
});
140+
return Mono.fromRunnable(() -> writer.writeUnsignedLong(value));
149141
}
150142

151143
@Override

src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ public void writeLong(long value) {
7979
builder.append(value);
8080
}
8181

82+
@Override
83+
public void writeUnsignedLong(long value) {
84+
startAvailable(Mode.NUMERIC);
85+
86+
builder.append(Long.toUnsignedString(value));
87+
}
88+
8289
@Override
8390
public void writeBigInteger(BigInteger value) {
8491
requireNonNull(value, "value must not be null");

src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,38 @@ static MySqlConnectionConfiguration configuration(
102102

103103
return builder.build();
104104
}
105+
106+
boolean envIsLessThanMySql56() {
107+
String version = System.getProperty("test.mysql.version");
108+
109+
if (version == null || version.isEmpty()) {
110+
return true;
111+
}
112+
113+
ServerVersion ver = ServerVersion.parse(version);
114+
String type = System.getProperty("test.db.type");
115+
116+
if ("mariadb".equalsIgnoreCase(type)) {
117+
return false;
118+
}
119+
120+
return ver.isLessThan(ServerVersion.create(5, 6, 0));
121+
}
122+
123+
boolean envIsLessThanMySql57OrMariaDb102() {
124+
String version = System.getProperty("test.mysql.version");
125+
126+
if (version == null || version.isEmpty()) {
127+
return true;
128+
}
129+
130+
ServerVersion ver = ServerVersion.parse(version);
131+
String type = System.getProperty("test.db.type");
132+
133+
if ("mariadb".equalsIgnoreCase(type)) {
134+
return ver.isLessThan(ServerVersion.create(10, 2, 0));
135+
}
136+
137+
return ver.isLessThan(ServerVersion.create(5, 7, 0));
138+
}
105139
}

src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.junit.jupiter.api.AfterEach;
2323
import org.junit.jupiter.api.BeforeEach;
2424
import org.junit.jupiter.api.Test;
25-
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
25+
import org.junit.jupiter.api.condition.DisabledIf;
2626
import org.reactivestreams.Publisher;
2727
import reactor.core.publisher.Mono;
2828
import reactor.test.StepVerifier;
@@ -64,7 +64,7 @@ void tearDown() {
6464
JacksonCodecRegistrar.tearDown();
6565
}
6666

67-
@DisabledIfSystemProperty(named = "test.mysql.version", matches = "5\\.[56](\\.\\d+)?")
67+
@DisabledIf("envIsLessThanMySql57OrMariaDb102")
6868
@Test
6969
void json() {
7070
create().flatMap(connection -> Mono.from(connection.createStatement(TDL).execute())

src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
import io.r2dbc.spi.Result;
2323
import org.jetbrains.annotations.Nullable;
2424
import org.junit.jupiter.api.Test;
25-
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
25+
import org.junit.jupiter.api.condition.DisabledIf;
26+
import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode;
27+
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
2628
import reactor.core.publisher.Flux;
2729
import reactor.core.publisher.Mono;
2830
import reactor.util.function.Tuple2;
2931
import reactor.util.function.Tuples;
3032

33+
import java.io.IOException;
3134
import java.lang.reflect.ParameterizedType;
3235
import java.lang.reflect.Type;
3336
import java.math.BigDecimal;
@@ -206,11 +209,23 @@ void varbinary() {
206209
ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5 }));
207210
}
208211

212+
@Test
213+
void text() {
214+
testType(byte[].class, true, "TEXT", null, new byte[0], new byte[] { 1, 2, 3, 4, 5 });
215+
testType(ByteBuffer.class, true, "TEXT", null, ByteBuffer.allocate(0),
216+
ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5 }));
217+
}
218+
209219
@Test
210220
void bit() {
211221
testType(Boolean.class, true, "BIT(1)", null, false, true);
212222
testType(BitSet.class, true, "BIT(16)", null, BitSet.valueOf(new byte[] { (byte) 0xEF, (byte) 0xCD }),
213-
BitSet.valueOf(new byte[0]), BitSet.valueOf(new byte[] { 0, 0 }));
223+
BitSet.valueOf(new byte[0]), BitSet.valueOf(new byte[] { 0, 0 }), BitSet.valueOf(new byte[] { (byte) 0xCD }));
224+
testType(BitSet.class, true, "BIT(64)", null, BitSet.valueOf(new long[0]),
225+
BitSet.valueOf(new long[] { 0 }), BitSet.valueOf(new long[] { 0xEFCD }),
226+
BitSet.valueOf(new long[] { Long.MAX_VALUE }));
227+
testType(BitSet.class, false, "BIT(64)", BitSet.valueOf(new long[] { -1 }),
228+
BitSet.valueOf(new long[] { Long.MIN_VALUE }));
214229
testType(byte[].class, false, "BIT(16)", null, new byte[] { (byte) 0xCD, (byte) 0xEF },
215230
new byte[] { 0, 0 });
216231
testType(ByteBuffer.class, false, "BIT(16)", null, ByteBuffer.wrap(new byte[] { 1, 2 }),
@@ -247,11 +262,13 @@ void set() {
247262
EnumSet.of(EnumData.ONE, EnumData.THREE));
248263
}
249264

250-
@DisabledIfSystemProperty(named = "test.mysql.version", matches = "5\\.[56](\\.\\d+)?")
265+
@DisabledIf("envIsLessThanMySql57OrMariaDb102")
251266
@Test
252267
void json() {
253268
testType(String.class, false, "JSON", null, "{\"data\": 1}", "[\"data\", 1]", "1", "null",
254269
"\"R2DBC\"", "2.56");
270+
271+
255272
}
256273

257274
@Test
@@ -274,7 +291,7 @@ void time() {
274291
testType(Duration.class, true, "TIME", null, minDuration, aDuration, maxDuration);
275292
}
276293

277-
@DisabledIfSystemProperty(named = "test.mysql.version", matches = "5\\.5(\\.\\d+)?")
294+
@DisabledIf("envIsLessThanMySql56")
278295
@Test
279296
void time6() {
280297
LocalTime smallTime = LocalTime.of(0, 0, 0, 1000);
@@ -307,7 +324,7 @@ void timeDuration() {
307324
.concatMap(pair -> testTimeDuration(connection, pair.getT1(), pair.getT2()))));
308325
}
309326

310-
@DisabledIfSystemProperty(named = "test.mysql.version", matches = "5\\.5(\\.\\d+)?")
327+
@DisabledIf("envIsLessThanMySql56")
311328
@Test
312329
void timeDuration6() {
313330
long seconds = TimeUnit.HOURS.toSeconds(8) + TimeUnit.MINUTES.toSeconds(5) + 45;
@@ -337,7 +354,7 @@ void dateTime() {
337354
testType(LocalDateTime.class, true, "DATETIME", null, minDateTime, aDateTime, maxDateTime);
338355
}
339356

340-
@DisabledIfSystemProperty(named = "test.mysql.version", matches = "5\\.5(\\.\\d+)?")
357+
@DisabledIf("envIsLessThanMySql56")
341358
@Test
342359
void dateTime6() {
343360
LocalDateTime smallDateTime = LocalDateTime.of(1000, 1, 1, 0, 0, 0, 1000);
@@ -357,7 +374,7 @@ void timestamp() {
357374
testType(LocalDateTime.class, true, "TIMESTAMP", minTimestamp, aTimestamp, maxTimestamp);
358375
}
359376

360-
@DisabledIfSystemProperty(named = "test.mysql.version", matches = "5\\.5(\\.\\d+)?")
377+
@DisabledIf("envIsLessThanMySql56")
361378
@Test
362379
void timestamp6() {
363380
LocalDateTime minTimestamp = LocalDateTime.of(1970, 1, 3, 0, 0, 0, 1000);
@@ -553,41 +570,55 @@ void insertOnDuplicate() {
553570
}
554571

555572
/**
556-
* ref: https://github.com/asyncer-io/r2dbc-mysql/issues/91
573+
* ref: <a href="https://github.com/asyncer-io/r2dbc-mysql/issues/91">Issue 91</a>
557574
*/
558-
@DisabledIfSystemProperty(named = "test.mysql.version", matches = "5\\.[56](\\.\\d+)?")
575+
@DisabledIf("envIsLessThanMySql57OrMariaDb102")
559576
@Test
560577
void testUnionQueryWithJsonColumnDecodedAsString() {
561578
complete(connection ->
562-
Flux.from(connection.createStatement(
563-
"CREATE TEMPORARY TABLE test1 (id INT PRIMARY KEY AUTO_INCREMENT, value JSON)")
564-
.execute())
565-
.flatMap(IntegrationTestSupport::extractRowsUpdated)
566-
.thenMany(connection.createStatement("INSERT INTO test1 VALUES(DEFAULT, ?)")
567-
.bind(0, "{\"id\":1,\"name\":\"iron man\"}")
568-
.execute())
569-
.flatMap(IntegrationTestSupport::extractRowsUpdated)
570-
.doOnNext(it -> assertThat(it).isEqualTo(1))
571-
.thenMany(connection.createStatement(
572-
"CREATE TEMPORARY TABLE test2 (id INT PRIMARY KEY AUTO_INCREMENT, value JSON)")
573-
.execute())
574-
.flatMap(IntegrationTestSupport::extractRowsUpdated)
575-
.thenMany(connection.createStatement("INSERT INTO test2 VALUES(DEFAULT, ?)")
576-
.bind(0,
577-
"[{\"id\":2,\"name\":\"bat man\"},{\"id\":3,\"name\":\"super man\"}]")
578-
.execute())
579-
.flatMap(IntegrationTestSupport::extractRowsUpdated)
580-
.doOnNext(it -> assertThat(it).isEqualTo(1))
581-
.thenMany(
582-
connection.createStatement("SELECT value FROM test1 UNION SELECT value FROM test2")
583-
.execute())
584-
.flatMap(r -> r.map((row, metadata) -> row.get(0, String.class))
585-
.collectList()
586-
.doOnNext(it -> assertThat(it).isEqualTo(
587-
Arrays.asList("{\"id\": 1, \"name\": \"iron man\"}",
588-
"[{\"id\": 2, \"name\": \"bat man\"}, {\"id\": 3, \"name\": \"super man\"}]"))))
579+
Flux.from(connection.createStatement(
580+
"CREATE TEMPORARY TABLE test1 (id INT PRIMARY KEY AUTO_INCREMENT, value JSON)")
581+
.execute())
582+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
583+
.thenMany(connection.createStatement("INSERT INTO test1 VALUES(DEFAULT, ?)")
584+
.bind(0, "{\"id\":1,\"name\":\"iron man\"}")
585+
.execute())
586+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
587+
.doOnNext(it -> assertThat(it).isEqualTo(1))
588+
.thenMany(connection.createStatement(
589+
"CREATE TEMPORARY TABLE test2 (id INT PRIMARY KEY AUTO_INCREMENT, value JSON)")
590+
.execute())
591+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
592+
.thenMany(connection.createStatement("INSERT INTO test2 VALUES(DEFAULT, ?)")
593+
.bind(0,
594+
"[{\"id\":2,\"name\":\"bat man\"},{\"id\":3,\"name\":\"super man\"}]")
595+
.execute())
596+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
597+
.doOnNext(it -> assertThat(it).isEqualTo(1))
598+
.thenMany(
599+
connection.createStatement("SELECT value FROM test1 UNION SELECT value FROM test2")
600+
.execute())
601+
.flatMap(r -> r.map((row, metadata) -> row.get(0, String.class))
602+
.map(QueryIntegrationTestSupport::parseJson)
603+
.collectList()
604+
.doOnNext(it -> assertThat(it).isEqualTo(
605+
Arrays.asList(
606+
parseJson("{\"id\": 1, \"name\": \"iron man\"}"),
607+
parseJson(
608+
"[{\"id\": 2, \"name\": \"bat man\"}, {\"id\": 3, \"name\": \"super man\"}]"
609+
)
610+
)
611+
)))
589612
);
613+
}
590614

615+
private static JsonNode parseJson(String json) {
616+
ObjectMapper mapper = new ObjectMapper();
617+
try {
618+
return mapper.readTree(json);
619+
} catch (IOException e) {
620+
throw new RuntimeException(e);
621+
}
591622
}
592623

593624
private static Flux<Integer> extractFirstInteger(Result result) {

0 commit comments

Comments
 (0)