From 24ec2d610a945f4fa9fc13f9c2d31118f96b0d41 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 20 Feb 2025 03:45:51 +0900 Subject: [PATCH] Fix handling of `TINYINT(1) UNSIGNED` when `tinyInt1isBit` is set Motivation: When the `tinyInt1isBit` flag is set, attempting to convert `TINYINT(1) UNSIGNED` to a boolean results in immediate rejection. Modifications: Prevent conversion of `TINYINT(1) UNSIGNED` to boolean when `tinyInt1isBit` is enabled. Result: `TINYINT(1) UNSIGNED` is handled correctly without unnecessary conversion attempts. --- .../mysql/MySqlConnectionConfiguration.java | 6 +++-- .../mysql/MySqlConnectionFactoryProvider.java | 6 +++-- .../r2dbc/mysql/codec/BooleanCodec.java | 4 +++- .../r2dbc/mysql/codec/DefaultCodecs.java | 22 ++++++++++--------- .../mysql/ConnectionIntegrationTest.java | 13 +++++++++++ 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 93ba9ce78..3857451a1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -1220,8 +1220,10 @@ public Builder metrics(boolean enabled) { /** * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type. - * When enabled, TINYINT(1) columns (both SIGNED and UNSIGNED) will be treated as - * BIT. default to {@code true}. + * When enabled, TINYINT(1) columns will be treated as BIT. Defaults to {@code true}. + *

+ * Note: Only signed TINYINT(1) columns can be treated as BIT or Boolean. + * Ref: https://bugs.mysql.com/bug.php?id=100309 * * @param tinyInt1isBit {@code true} to treat TINYINT(1) as BIT * @return this {@link Builder} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index bff335123..5905c56ca 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -332,8 +332,10 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr /** * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type. - * When enabled, TINYINT(1) columns (both SIGNED and UNSIGNED) will be treated as - * BIT. default to {@code true}. + * When enabled, TINYINT(1) columns will be treated as BIT. Defaults to {@code true}. + *

+ * Note: Only signed TINYINT(1) columns can be treated as BIT or Boolean. + * Ref: https://bugs.mysql.com/bug.php?id=100309 * * @since 1.4.0 */ diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index 0a59265c3..8fb98c273 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java @@ -32,6 +32,8 @@ */ final class BooleanCodec extends AbstractPrimitiveCodec { + private static final Integer INTEGER_ONE = Integer.valueOf(1); + static final BooleanCodec INSTANCE = new BooleanCodec(); private BooleanCodec() { @@ -86,7 +88,7 @@ public MySqlParameter encode(Object value, CodecContext context) { public boolean doCanDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); return ((type == MySqlType.BIT || type == MySqlType.TINYINT) && - Integer.valueOf(1).equals(metadata.getPrecision())) || type == MySqlType.VARCHAR; + INTEGER_ONE.equals(metadata.getPrecision())) || type == MySqlType.VARCHAR; } public Boolean createFromLong(long l) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index ba022875a..34f2c67c1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -45,8 +45,6 @@ */ final class DefaultCodecs implements Codecs { - private static final Integer INTEGER_ONE = Integer.valueOf(1); - private static final List> DEFAULT_CODECS = InternalArrays.asImmutableList( ByteCodec.INSTANCE, ShortCodec.INSTANCE, @@ -369,18 +367,22 @@ private static Class chooseClass(final MySqlReadableMetadata metadata, Class< return type.isAssignableFrom(javaType) ? javaType : type; } - private static Class getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) { - final MySqlType type = metadata.getType(); - final Integer precision = metadata.getPrecision(); - if (INTEGER_ONE.equals(precision) && (type == MySqlType.TINYINT || type == MySqlType.TINYINT_UNSIGNED) - && codecContext.isTinyInt1isBit()) { - return Boolean.class; + private static boolean shouldBeTreatedAsBoolean(final @Nullable Integer precision, final MySqlType type, + final CodecContext context) { + if (precision == null || precision != 1) { + return false; } - // ref: https://github.com/asyncer-io/r2dbc-mysql/issues/277 // BIT(1) should be treated as Boolean by default. - if (INTEGER_ONE.equals(precision) && type == MySqlType.BIT) { + return type == MySqlType.BIT || type == MySqlType.TINYINT && context.isTinyInt1isBit(); + } + + private static Class getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) { + final MySqlType type = metadata.getType(); + final Integer precision = metadata.getPrecision(); + + if (shouldBeTreatedAsBoolean(precision, type, codecContext)) { return Boolean.class; } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index fccd38530..4e4fa34ae 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -592,6 +592,19 @@ public void tinyInt1isBitTrueTestValue1() { ); } + @Test + public void tinyInt1isBitTrueTestUnsignedTinyInt1isNotBoolean() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1) UNSIGNED)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Short.class)) + .doOnNext(value -> assertThat(value).isEqualTo(Short.valueOf((short)1))) + ); + } + @Test public void tinyInt1isBitTrueTestValue0() { complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute())