Skip to content

Commit abaa2d1

Browse files
committed
Support tinyInt1isBit
Motivation: Aligning with MySQL connector. Modifications: Implemented `tinyInt1isBit` flag. Result: Improved compatibility with MySQL connectors.
1 parent 820bdf4 commit abaa2d1

11 files changed

+165
-34
lines changed

r2dbc-mysql/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
<groupId>io.asyncer</groupId>
2121
<artifactId>r2dbc-mysql</artifactId>
22-
<version>1.3.3-SNAPSHOT</version>
22+
<version>1.4.0-SNAPSHOT</version>
2323

2424
<name>Reactive Relational Database Connectivity - MySQL</name>
2525
<url>https://github.com/asyncer-io/r2dbc-mysql</url>

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java

+9
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public final class ConnectionContext implements CodecContext {
5151

5252
private final int localInfileBufferSize;
5353

54+
private final boolean tinyInt1isBit;
55+
5456
private final boolean preserveInstants;
5557

5658
private int connectionId = -1;
@@ -107,12 +109,14 @@ public final class ConnectionContext implements CodecContext {
107109
ZeroDateOption zeroDateOption,
108110
@Nullable Path localInfilePath,
109111
int localInfileBufferSize,
112+
boolean tinyInt1isBit,
110113
boolean preserveInstants,
111114
@Nullable ZoneId timeZone
112115
) {
113116
this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null");
114117
this.localInfilePath = localInfilePath;
115118
this.localInfileBufferSize = localInfileBufferSize;
119+
this.tinyInt1isBit = tinyInt1isBit;
116120
this.preserveInstants = preserveInstants;
117121
this.timeZone = timeZone;
118122
}
@@ -216,6 +220,11 @@ public boolean isMariaDb() {
216220
return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb();
217221
}
218222

223+
@Override
224+
public boolean isTinyInt1isBit() {
225+
return tinyInt1isBit;
226+
}
227+
219228
public boolean isNoBackslashEscapes() {
220229
return (serverStatuses & ServerStatuses.NO_BACKSLASH_ESCAPES) != 0;
221230
}

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java

+47-22
Original file line numberDiff line numberDiff line change
@@ -134,24 +134,26 @@ public final class MySqlConnectionConfiguration {
134134

135135
private final boolean metrics;
136136

137+
private final boolean tinyInt1isBit;
138+
137139
private MySqlConnectionConfiguration(
138-
boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
139-
boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
140-
ZeroDateOption zeroDateOption,
141-
boolean preserveInstants,
142-
String connectionTimeZone,
143-
boolean forceConnectionTimeZoneToSession,
144-
String user, @Nullable CharSequence password, @Nullable String database,
145-
boolean createDatabaseIfNotExist, @Nullable Predicate<String> preferPrepareStatement,
146-
List<String> sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout,
147-
@Nullable Path loadLocalInfilePath, int localInfileBufferSize,
148-
int queryCacheSize, int prepareCacheSize,
149-
Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel,
150-
@Nullable LoopResources loopResources,
151-
Extensions extensions, @Nullable Publisher<String> passwordPublisher,
152-
@Nullable AddressResolverGroup<?> resolver,
153-
boolean metrics
154-
) {
140+
boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
141+
boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
142+
ZeroDateOption zeroDateOption,
143+
boolean preserveInstants,
144+
String connectionTimeZone,
145+
boolean forceConnectionTimeZoneToSession,
146+
String user, @Nullable CharSequence password, @Nullable String database,
147+
boolean createDatabaseIfNotExist, @Nullable Predicate<String> preferPrepareStatement,
148+
List<String> sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout,
149+
@Nullable Path loadLocalInfilePath, int localInfileBufferSize,
150+
int queryCacheSize, int prepareCacheSize,
151+
Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel,
152+
@Nullable LoopResources loopResources,
153+
Extensions extensions, @Nullable Publisher<String> passwordPublisher,
154+
@Nullable AddressResolverGroup<?> resolver,
155+
boolean metrics,
156+
boolean tinyInt1isBit) {
155157
this.isHost = isHost;
156158
this.domain = domain;
157159
this.port = port;
@@ -182,6 +184,7 @@ private MySqlConnectionConfiguration(
182184
this.passwordPublisher = passwordPublisher;
183185
this.resolver = resolver;
184186
this.metrics = metrics;
187+
this.tinyInt1isBit = tinyInt1isBit;
185188
}
186189

187190
/**
@@ -321,6 +324,10 @@ boolean isMetrics() {
321324
return metrics;
322325
}
323326

327+
boolean isTinyInt1isBit() {
328+
return tinyInt1isBit;
329+
}
330+
324331
@Override
325332
public boolean equals(Object o) {
326333
if (this == o) {
@@ -359,7 +366,8 @@ public boolean equals(Object o) {
359366
extensions.equals(that.extensions) &&
360367
Objects.equals(passwordPublisher, that.passwordPublisher) &&
361368
Objects.equals(resolver, that.resolver) &&
362-
metrics == that.metrics;
369+
metrics == that.metrics &&
370+
tinyInt1isBit == that.tinyInt1isBit;
363371
}
364372

365373
@Override
@@ -374,7 +382,7 @@ public int hashCode() {
374382
loadLocalInfilePath, localInfileBufferSize,
375383
queryCacheSize, prepareCacheSize,
376384
compressionAlgorithms, zstdCompressionLevel,
377-
loopResources, extensions, passwordPublisher, resolver, metrics);
385+
loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit);
378386
}
379387

380388
@Override
@@ -409,7 +417,8 @@ private String buildCommonToStringPart() {
409417
", extensions=" + extensions +
410418
", passwordPublisher=" + passwordPublisher +
411419
", resolver=" + resolver +
412-
", metrics=" + metrics;
420+
", metrics=" + metrics +
421+
", tinyint1isBit=" + tinyInt1isBit;
413422
}
414423

415424
/**
@@ -511,6 +520,8 @@ public static final class Builder {
511520

512521
private boolean metrics;
513522

523+
private boolean tinyInt1isBit = true;
524+
514525
/**
515526
* Builds an immutable {@link MySqlConnectionConfiguration} with current options.
516527
*
@@ -545,11 +556,11 @@ public MySqlConnectionConfiguration build() {
545556
loadLocalInfilePath,
546557
localInfileBufferSize, queryCacheSize, prepareCacheSize,
547558
compressionAlgorithms, zstdCompressionLevel, loopResources,
548-
Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics);
559+
Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit);
549560
}
550561

551562
/**
552-
* Configures the database. Default no database.
563+
* Configures the database. Default no database.
553564
*
554565
* @param database the database, or {@code null} if no database want to be login.
555566
* @return this {@link Builder}.
@@ -1207,6 +1218,20 @@ public Builder metrics(boolean enabled) {
12071218
return this;
12081219
}
12091220

1221+
/**
1222+
* Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type.
1223+
* When enabled, TINYINT(1) columns (both SIGNED and UNSIGNED) will be treated as
1224+
* BIT. default to {@code true}.
1225+
*
1226+
* @param tinyInt1isBit {@code true} to treat TINYINT(1) as BIT
1227+
* @return this {@link Builder}
1228+
* @since 1.4.0
1229+
*/
1230+
public Builder tinyInt1isBit(boolean tinyInt1isBit) {
1231+
this.tinyInt1isBit = tinyInt1isBit;
1232+
return this;
1233+
}
1234+
12101235
private SslMode requireSslMode() {
12111236
SslMode sslMode = this.sslMode;
12121237

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ private static Mono<MySqlConnection> getMySqlConnection(
137137
configuration.getZeroDateOption(),
138138
configuration.getLoadLocalInfilePath(),
139139
configuration.getLocalInfileBufferSize(),
140+
configuration.isTinyInt1isBit(),
140141
configuration.isPreserveInstants(),
141142
connectionTimeZone
142143
);

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,15 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr
330330
*/
331331
public static final Option<Boolean> METRICS = Option.valueOf("metrics");
332332

333+
/**
334+
* Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type.
335+
* When enabled, TINYINT(1) columns (both SIGNED and UNSIGNED) will be treated as
336+
* BIT. default to {@code true}.
337+
*
338+
* @since 1.4.0
339+
*/
340+
public static final Option<Boolean> TINY_INT_1_IS_BIT = Option.valueOf("tinyInt1isBit");
341+
333342
@Override
334343
public ConnectionFactory create(ConnectionFactoryOptions options) {
335344
requireNonNull(options, "connectionFactoryOptions must not be null");
@@ -424,7 +433,9 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) {
424433
mapper.optional(STATEMENT_TIMEOUT).as(Duration.class, Duration::parse)
425434
.to(builder::statementTimeout);
426435
mapper.optional(METRICS).asBoolean()
427-
.to(builder::metrics);
436+
.to(builder::metrics);
437+
mapper.optional(TINY_INT_1_IS_BIT).asBoolean()
438+
.to(builder::tinyInt1isBit);
428439

429440
return builder.build();
430441
}

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java

+6
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,10 @@ public interface CodecContext {
6969
* @return if is MariaDB.
7070
*/
7171
boolean isMariaDb();
72+
73+
/**
74+
*
75+
* @return true if tinyInt(1) is treated as bit.
76+
*/
77+
boolean isTinyInt1isBit();
7278
}

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java

+18-5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
*/
4646
final class DefaultCodecs implements Codecs {
4747

48+
private static final Integer INTEGER_ONE = Integer.valueOf(1);
49+
4850
private static final List<Codec<?>> DEFAULT_CODECS = InternalArrays.asImmutableList(
4951
ByteCodec.INSTANCE,
5052
ShortCodec.INSTANCE,
@@ -137,6 +139,7 @@ private DefaultCodecs(List<Codec<?>> codecs) {
137139
* Note: this method should NEVER release {@code buf} because of it come from {@code MySqlRow} which will release
138140
* this buffer.
139141
*/
142+
@Nullable
140143
@Override
141144
public <T> T decode(FieldValue value, MySqlReadableMetadata metadata, Class<?> type, boolean binary,
142145
CodecContext context) {
@@ -151,7 +154,7 @@ public <T> T decode(FieldValue value, MySqlReadableMetadata metadata, Class<?> t
151154
return null;
152155
}
153156

154-
Class<?> target = chooseClass(metadata, type);
157+
Class<?> target = chooseClass(metadata, type, context);
155158

156159
if (value instanceof NormalFieldValue) {
157160
return decodeNormal((NormalFieldValue) value, metadata, target, binary, context);
@@ -162,6 +165,7 @@ public <T> T decode(FieldValue value, MySqlReadableMetadata metadata, Class<?> t
162165
throw new IllegalArgumentException("Unknown value " + value.getClass().getSimpleName());
163166
}
164167

168+
@Nullable
165169
@Override
166170
public <T> T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type,
167171
boolean binary, CodecContext context) {
@@ -359,18 +363,27 @@ private <T> T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat
359363
* @param type the {@link Class} specified by the user.
360364
* @return the {@link Class} to use for decoding.
361365
*/
362-
private static Class<?> chooseClass(final MySqlReadableMetadata metadata, Class<?> type) {
363-
final Class<?> javaType = getDefaultJavaType(metadata);
366+
private static Class<?> chooseClass(final MySqlReadableMetadata metadata, Class<?> type,
367+
final CodecContext codecContext) {
368+
final Class<?> javaType = getDefaultJavaType(metadata, codecContext);
364369
return type.isAssignableFrom(javaType) ? javaType : type;
365370
}
366371

367-
private static Class<?> getDefaultJavaType(final MySqlReadableMetadata metadata) {
372+
private static Class<?> getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) {
368373
final MySqlType type = metadata.getType();
374+
final Integer precision = metadata.getPrecision();
375+
376+
if (INTEGER_ONE.equals(precision) && (type == MySqlType.TINYINT || type == MySqlType.TINYINT_UNSIGNED)
377+
&& codecContext.isTinyInt1isBit()) {
378+
return Boolean.class;
379+
}
380+
369381
// ref: https://github.com/asyncer-io/r2dbc-mysql/issues/277
370382
// BIT(1) should be treated as Boolean by default.
371-
if (type == MySqlType.BIT && Integer.valueOf(1).equals(metadata.getPrecision())) {
383+
if (INTEGER_ONE.equals(precision) && type == MySqlType.BIT) {
372384
return Boolean.class;
373385
}
386+
374387
return type.getJavaType();
375388
}
376389

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ void getTimeZone() {
3939
String id = i < 0 ? "UTC" + i : "UTC+" + i;
4040
ConnectionContext context = new ConnectionContext(
4141
ZeroDateOption.USE_NULL, null,
42-
8192, true, ZoneId.of(id));
42+
8192, true, true, ZoneId.of(id));
4343

4444
assertThat(context.getTimeZone()).isEqualTo(ZoneId.of(id));
4545
}
@@ -48,7 +48,7 @@ void getTimeZone() {
4848
@Test
4949
void setTwiceTimeZone() {
5050
ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null,
51-
8192, true, null);
51+
8192, true, true, null);
5252

5353
context.initSession(
5454
Caches.createPrepareCache(0),
@@ -70,7 +70,7 @@ void setTwiceTimeZone() {
7070
@Test
7171
void badSetTimeZone() {
7272
ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null,
73-
8192, true, ZoneId.systemDefault());
73+
8192, true, true, ZoneId.systemDefault());
7474
assertThatIllegalStateException().isThrownBy(() -> context.initSession(
7575
Caches.createPrepareCache(0),
7676
IsolationLevel.REPEATABLE_READ,
@@ -91,7 +91,7 @@ public static ConnectionContext mock(boolean isMariaDB) {
9191

9292
public static ConnectionContext mock(boolean isMariaDB, ZoneId zoneId) {
9393
ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null,
94-
8192, true, zoneId);
94+
8192, true, true, zoneId);
9595

9696
context.initHandshake(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"),
9797
Capability.of(~(isMariaDB ? 1 : 0)));

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java

+25
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,31 @@ void loadDataLocalInfile(String name) throws URISyntaxException, IOException {
579579
.doOnNext(it -> assertThat(it).isEqualTo(json)));
580580
}
581581

582+
@Test
583+
public void tinyInt1isBitTrueTestValue1() {
584+
complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute())
585+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
586+
.thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute())
587+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
588+
.thenMany(connection.createStatement("SELECT `value` FROM `test`").execute())
589+
.flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class)))
590+
.doOnNext(value -> assertThat(value).isInstanceOf(Boolean.class))
591+
.doOnNext(value -> assertThat(value).isEqualTo(true))
592+
);
593+
}
594+
595+
@Test
596+
public void tinyInt1isBitTrueTestValue0() {
597+
complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute())
598+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
599+
.thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 0)").execute())
600+
.flatMap(IntegrationTestSupport::extractRowsUpdated)
601+
.thenMany(connection.createStatement("SELECT `value` FROM `test`").execute())
602+
.flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class)))
603+
.doOnNext(value -> assertThat(value).isInstanceOf(Boolean.class))
604+
.doOnNext(value -> assertThat(value).isEqualTo(false)));
605+
}
606+
582607
@Test
583608
void batchCrud() {
584609
// TODO: spilt it to multiple test cases and move it to BatchIntegrationTest

0 commit comments

Comments
 (0)