Skip to content

Commit

Permalink
Fix handling of TINYINT(1) UNSIGNED when tinyInt1isBit is set
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jchrys committed Feb 19, 2025
1 parent e211cd8 commit 167617d
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
* <p>
* Note: Only singed 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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
* <p>
* Note: Only singed TINYINT(1) columns can be treated as BIT or Boolean.
* Ref: https://bugs.mysql.com/bug.php?id=100309
*
* @since 1.4.0
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
*/
final class BooleanCodec extends AbstractPrimitiveCodec<Boolean> {

private static final Integer INTEGER_ONE = Integer.valueOf(1);

static final BooleanCodec INSTANCE = new BooleanCodec();

private BooleanCodec() {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@
*/
final class DefaultCodecs implements Codecs {

private static final Integer INTEGER_ONE = Integer.valueOf(1);

private static final List<Codec<?>> DEFAULT_CODECS = InternalArrays.asImmutableList(
ByteCodec.INSTANCE,
ShortCodec.INSTANCE,
Expand Down Expand Up @@ -369,18 +367,26 @@ 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) {

// ref: https://bugs.mysql.com/bug.php?id=100722
// Only SIGNED TINYINT(1) can be treated as Boolean or 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,21 @@ public void tinyInt1isBitTrueTestValue1() {
);
}

// ONLY `SIGNED TINYINT(1)` can be treated as Boolean or Bit.
// ref: https://bugs.mysql.com/bug.php?id=100722
@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())
Expand Down

0 comments on commit 167617d

Please sign in to comment.