diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index cc5aeb2a..81dc01a4 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -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; @@ -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; } 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 8b4c789d..fbeb9506 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 @@ -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 preferPrepareStatement, - List sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout, - @Nullable Path loadLocalInfilePath, int localInfileBufferSize, - int queryCacheSize, int prepareCacheSize, - Set compressionAlgorithms, int zstdCompressionLevel, - @Nullable LoopResources loopResources, - Extensions extensions, @Nullable Publisher 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 preferPrepareStatement, + List sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout, + @Nullable Path loadLocalInfilePath, int localInfileBufferSize, + int queryCacheSize, int prepareCacheSize, + Set compressionAlgorithms, int zstdCompressionLevel, + @Nullable LoopResources loopResources, + Extensions extensions, @Nullable Publisher passwordPublisher, + @Nullable AddressResolverGroup resolver, + boolean metrics, + boolean tinyInt1isBit) { this.isHost = isHost; this.domain = domain; this.port = port; @@ -182,6 +184,7 @@ private MySqlConnectionConfiguration( this.passwordPublisher = passwordPublisher; this.resolver = resolver; this.metrics = metrics; + this.tinyInt1isBit = tinyInt1isBit; } /** @@ -321,6 +324,10 @@ boolean isMetrics() { return metrics; } + boolean isTinyInt1isBit() { + return tinyInt1isBit; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -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 @@ -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 @@ -409,7 +417,8 @@ private String buildCommonToStringPart() { ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + ", resolver=" + resolver + - ", metrics=" + metrics; + ", metrics=" + metrics + + ", tinyint1isBit=" + tinyInt1isBit; } /** @@ -511,6 +520,8 @@ public static final class Builder { private boolean metrics; + private boolean tinyInt1isBit = true; + /** * Builds an immutable {@link MySqlConnectionConfiguration} with current options. * @@ -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}. @@ -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; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index e483d2d6..a6880cc8 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -137,6 +137,7 @@ private static Mono getMySqlConnection( configuration.getZeroDateOption(), configuration.getLoadLocalInfilePath(), configuration.getLocalInfileBufferSize(), + configuration.isTinyInt1isBit(), configuration.isPreserveInstants(), connectionTimeZone ); 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 8f045fca..ef4925e2 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 @@ -330,6 +330,17 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr */ public static final Option 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? + *

+ * 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 TINY_INT_1_IS_BIT = Option.valueOf("tinyInt1isBit"); + @Override public ConnectionFactory create(ConnectionFactoryOptions options) { requireNonNull(options, "connectionFactoryOptions must not be null"); @@ -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(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java index 8eda9c98..d535a86d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java @@ -69,4 +69,11 @@ public interface CodecContext { * @return if is MariaDB. */ boolean isMariaDb(); + + + /** + * + * @return true if tinyInt(1) is treated as bit. + */ + boolean isTinyInt1isBit(); } 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 4542a7c9..0d3cbc17 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,6 +45,8 @@ */ 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, @@ -137,6 +139,7 @@ private DefaultCodecs(List> 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 decode(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { @@ -151,7 +154,7 @@ public 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); @@ -162,6 +165,7 @@ public T decode(FieldValue value, MySqlReadableMetadata metadata, Class t throw new IllegalArgumentException("Unknown value " + value.getClass().getSimpleName()); } + @Nullable @Override public T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { @@ -359,18 +363,27 @@ private 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(); }