Skip to content

Commit

Permalink
Support tinyInt1isBit
Browse files Browse the repository at this point in the history
Motivation:
Aligning with MySQL connector.

Modifications:
Implemented `tinyInt1isBit` flag.

Result:
Improved compatibility with MySQL connectors.
  • Loading branch information
jchrys committed Jan 31, 2025
1 parent 820bdf4 commit ea5a23d
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public final class ConnectionContext implements CodecContext {

private final int localInfileBufferSize;

private final boolean tinyInt1isBit;

private final boolean preserveInstants;

private int connectionId = -1;
Expand Down Expand Up @@ -107,12 +109,14 @@ public final class ConnectionContext implements CodecContext {
ZeroDateOption zeroDateOption,
@Nullable Path localInfilePath,
int localInfileBufferSize,
boolean tinyInt1isBit,
boolean preserveInstants,
@Nullable ZoneId timeZone
) {
this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null");
this.localInfilePath = localInfilePath;
this.localInfileBufferSize = localInfileBufferSize;
this.tinyInt1isBit = tinyInt1isBit;
this.preserveInstants = preserveInstants;
this.timeZone = timeZone;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,24 +134,26 @@ public final class MySqlConnectionConfiguration {

private final boolean metrics;

private final boolean tinyInt1isBit;

private MySqlConnectionConfiguration(
boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
ZeroDateOption zeroDateOption,
boolean preserveInstants,
String connectionTimeZone,
boolean forceConnectionTimeZoneToSession,
String user, @Nullable CharSequence password, @Nullable String database,
boolean createDatabaseIfNotExist, @Nullable Predicate<String> preferPrepareStatement,
List<String> sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout,
@Nullable Path loadLocalInfilePath, int localInfileBufferSize,
int queryCacheSize, int prepareCacheSize,
Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel,
@Nullable LoopResources loopResources,
Extensions extensions, @Nullable Publisher<String> passwordPublisher,
@Nullable AddressResolverGroup<?> resolver,
boolean metrics
) {
boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
ZeroDateOption zeroDateOption,
boolean preserveInstants,
String connectionTimeZone,
boolean forceConnectionTimeZoneToSession,
String user, @Nullable CharSequence password, @Nullable String database,
boolean createDatabaseIfNotExist, @Nullable Predicate<String> preferPrepareStatement,
List<String> sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout,
@Nullable Path loadLocalInfilePath, int localInfileBufferSize,
int queryCacheSize, int prepareCacheSize,
Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel,
@Nullable LoopResources loopResources,
Extensions extensions, @Nullable Publisher<String> passwordPublisher,
@Nullable AddressResolverGroup<?> resolver,
boolean metrics,
boolean tinyInt1isBit) {
this.isHost = isHost;
this.domain = domain;
this.port = port;
Expand Down Expand Up @@ -182,6 +184,7 @@ private MySqlConnectionConfiguration(
this.passwordPublisher = passwordPublisher;
this.resolver = resolver;
this.metrics = metrics;
this.tinyInt1isBit = tinyInt1isBit;
}

/**
Expand Down Expand Up @@ -321,6 +324,10 @@ boolean isMetrics() {
return metrics;
}

boolean isTinyInt1isBit() {
return tinyInt1isBit;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down Expand Up @@ -359,7 +366,8 @@ public boolean equals(Object o) {
extensions.equals(that.extensions) &&
Objects.equals(passwordPublisher, that.passwordPublisher) &&
Objects.equals(resolver, that.resolver) &&
metrics == that.metrics;
metrics == that.metrics &&
tinyInt1isBit == that.tinyInt1isBit;
}

@Override
Expand All @@ -374,7 +382,7 @@ public int hashCode() {
loadLocalInfilePath, localInfileBufferSize,
queryCacheSize, prepareCacheSize,
compressionAlgorithms, zstdCompressionLevel,
loopResources, extensions, passwordPublisher, resolver, metrics);
loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit);
}

@Override
Expand Down Expand Up @@ -409,7 +417,8 @@ private String buildCommonToStringPart() {
", extensions=" + extensions +
", passwordPublisher=" + passwordPublisher +
", resolver=" + resolver +
", metrics=" + metrics;
", metrics=" + metrics +
", tinyint1isBit=" + tinyInt1isBit;
}

/**
Expand Down Expand Up @@ -511,6 +520,8 @@ public static final class Builder {

private boolean metrics;

private boolean tinyInt1isBit = true;

/**
* Builds an immutable {@link MySqlConnectionConfiguration} with current options.
*
Expand Down Expand Up @@ -545,11 +556,11 @@ public MySqlConnectionConfiguration build() {
loadLocalInfilePath,
localInfileBufferSize, queryCacheSize, prepareCacheSize,
compressionAlgorithms, zstdCompressionLevel, loopResources,
Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics);
Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit);
}

/**
* Configures the database. Default no database.
* Configures the database. Default no database.
*
* @param database the database, or {@code null} if no database want to be login.
* @return this {@link Builder}.
Expand Down Expand Up @@ -1207,6 +1218,20 @@ public Builder metrics(boolean enabled) {
return this;
}

/**
* 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
* {@link Boolean} by default.
*
* @param tinyInt1isBit {@code true} to treat TINYINT(1) as BIT
* @return this {@link Builder}
* @since 1.4.0
*/
public Builder tinyInt1isBit(boolean tinyInt1isBit) {
this.tinyInt1isBit = tinyInt1isBit;
return this;
}

private SslMode requireSslMode() {
SslMode sslMode = this.sslMode;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ private static Mono<MySqlConnection> getMySqlConnection(
configuration.getZeroDateOption(),
configuration.getLoadLocalInfilePath(),
configuration.getLocalInfileBufferSize(),
configuration.isTinyInt1isBit(),
configuration.isPreserveInstants(),
connectionTimeZone
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,17 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr
*/
public static final Option<Boolean> METRICS = Option.valueOf("metrics");

/**
* Since the MySQL server silently converts BIT to TINYINT(1) when creating tables,
* should the driver treat the datatype TINYINT(1) as the BIT type?
* </p>
* Note: If {@code tinyInt1isBit=true}, TINYINT(1) columns, whether SIGNED or UNSIGNED,
* will be represented as {@link Boolean} by default.
*
* @since 1.4.0
*/
public static final Option<Boolean> TINY_INT_1_IS_BIT = Option.valueOf("tinyInt1isBit");

@Override
public ConnectionFactory create(ConnectionFactoryOptions options) {
requireNonNull(options, "connectionFactoryOptions must not be null");
Expand Down Expand Up @@ -424,7 +435,9 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) {
mapper.optional(STATEMENT_TIMEOUT).as(Duration.class, Duration::parse)
.to(builder::statementTimeout);
mapper.optional(METRICS).asBoolean()
.to(builder::metrics);
.to(builder::metrics);
mapper.optional(TINY_INT_1_IS_BIT).asBoolean()
.to(builder::tinyInt1isBit);

return builder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,11 @@ public interface CodecContext {
* @return if is MariaDB.
*/
boolean isMariaDb();


/**
*
* @return true if tinyInt(1) is treated as bit.
*/
boolean isTinyInt1isBit();
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
*/
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 @@ -137,6 +139,7 @@ private DefaultCodecs(List<Codec<?>> codecs) {
* Note: this method should NEVER release {@code buf} because of it come from {@code MySqlRow} which will release
* this buffer.
*/
@Nullable
@Override
public <T> T decode(FieldValue value, MySqlReadableMetadata metadata, Class<?> type, boolean binary,
CodecContext context) {
Expand All @@ -151,7 +154,7 @@ public <T> T decode(FieldValue value, MySqlReadableMetadata metadata, Class<?> t
return null;
}

Class<?> target = chooseClass(metadata, type);
Class<?> target = chooseClass(metadata, type, context);

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

@Nullable
@Override
public <T> T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type,
boolean binary, CodecContext context) {
Expand Down Expand Up @@ -359,18 +363,27 @@ private <T> T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat
* @param type the {@link Class} specified by the user.
* @return the {@link Class} to use for decoding.
*/
private static Class<?> chooseClass(final MySqlReadableMetadata metadata, Class<?> type) {
final Class<?> javaType = getDefaultJavaType(metadata);
private static Class<?> chooseClass(final MySqlReadableMetadata metadata, Class<?> type,
final CodecContext codecContext) {
final Class<?> javaType = getDefaultJavaType(metadata, codecContext);
return type.isAssignableFrom(javaType) ? javaType : type;
}

private static Class<?> getDefaultJavaType(final MySqlReadableMetadata metadata) {
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;
}

// ref: https://github.com/asyncer-io/r2dbc-mysql/issues/277
// BIT(1) should be treated as Boolean by default.
if (type == MySqlType.BIT && Integer.valueOf(1).equals(metadata.getPrecision())) {
if (INTEGER_ONE.equals(metadata.getPrecision()) && type == MySqlType.BIT) {
return Boolean.class;
}

return type.getJavaType();
}

Expand Down

0 comments on commit ea5a23d

Please sign in to comment.