From 673aac858c864b043d0da3a5a840887c32f7b4f6 Mon Sep 17 00:00:00 2001 From: svats0001 <104880778+svats0001@users.noreply.github.com> Date: Sat, 30 Nov 2024 03:27:54 +1100 Subject: [PATCH] Implemented relaxed conversion logic for BooleanCodec (#290) @jchrys #285 Motivation: To allow boolean values stored as strings in MySQL database such as "true", "false", "1" and "0" to be converted to their corresponding boolean values. Modification: BooleanCodec: Changed decode method to check if VARCHAR value is a boolean value. Changed doCanDecode to add VARCHAR. BooleanCodecTest: Added decodeString test to ensure boolean values stored as strings are converted into the correct corresponding boolean values. Result: Boolean values stored as strings can now be converted to their corresponding boolean values. Drawbacks are that there could be a string column containing numeric data with values other than 0 or 1 and the column isn't used for storing boolean values at the same time the codec interprets the 0's and 1's as boolean. Only boolean values "true", "false", "1" and "0" are decoded, other possible types of boolean value strings haven't been included. Also, doCanDecode states that the VARCHAR data type can be decoded but only a small subset of this data type can be decoded and it's not possible to highlight the conditions in the doCanDecode method. --- .../r2dbc/mysql/codec/BooleanCodec.java | 49 +++++++- .../r2dbc/mysql/codec/BooleanCodecTest.java | 115 ++++++++++++++++++ 2 files changed, 161 insertions(+), 3 deletions(-) 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 f546ba751..0a59265c3 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 @@ -16,12 +16,15 @@ package io.asyncer.r2dbc.mysql.codec; +import java.math.BigInteger; + import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.r2dbc.spi.R2dbcNonTransientResourceException; import reactor.core.publisher.Mono; /** @@ -38,7 +41,35 @@ private BooleanCodec() { @Override public Boolean decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { - return binary || metadata.getType() == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0'; + MySqlType dataType = metadata.getType(); + + if (dataType == MySqlType.VARCHAR) { + if (!value.isReadable()) { + return createFromLong(0); + } + + String s = value.toString(metadata.getCharCollation(context).getCharset()); + + if (s.equalsIgnoreCase("Y") || s.equalsIgnoreCase("yes") || + s.equalsIgnoreCase("T") || s.equalsIgnoreCase("true")) { + return createFromLong(1); + } else if (s.equalsIgnoreCase("N") || s.equalsIgnoreCase("no") || + s.equalsIgnoreCase("F") || s.equalsIgnoreCase("false")) { + return createFromLong(0); + } else if (s.matches("-?\\d*\\.\\d*") || s.matches("-?\\d*\\.\\d+[eE]-?\\d+") + || s.matches("-?\\d*[eE]-?\\d+")) { + return createFromDouble(Double.parseDouble(s)); + } else if (s.matches("-?\\d+")) { + if (!CodecUtils.isGreaterThanLongMax(s)) { + return createFromLong(CodecUtils.parseLong(value)); + } + return createFromBigInteger(new BigInteger(s)); + } + throw new R2dbcNonTransientResourceException("The value '" + s + "' of type '" + dataType + + "' cannot be encoded into a Boolean.", "22018"); + } + + return binary || dataType == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0'; } @Override @@ -54,8 +85,20 @@ public MySqlParameter encode(Object value, CodecContext context) { @Override public boolean doCanDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); - return (type == MySqlType.BIT || type == MySqlType.TINYINT) && - Integer.valueOf(1).equals(metadata.getPrecision()); + return ((type == MySqlType.BIT || type == MySqlType.TINYINT) && + Integer.valueOf(1).equals(metadata.getPrecision())) || type == MySqlType.VARCHAR; + } + + public Boolean createFromLong(long l) { + return (l == -1 || l > 0); + } + + public Boolean createFromDouble(double d) { + return (d == -1.0d || d > 0); + } + + public Boolean createFromBigInteger(BigInteger b) { + return b.compareTo(BigInteger.valueOf(0)) > 0 || b.compareTo(BigInteger.valueOf(-1)) == 0; } private static final class BooleanMySqlParameter extends AbstractMySqlParameter { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java index 999111de5..dbfd5c104 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java @@ -16,12 +16,22 @@ package io.asyncer.r2dbc.mysql.codec; +import io.asyncer.r2dbc.mysql.ConnectionContextTest; +import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.r2dbc.spi.R2dbcNonTransientException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.nio.charset.Charset; import java.util.Arrays; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + /** * Unit tests for {@link BooleanCodec}. */ @@ -55,4 +65,109 @@ public ByteBuf[] binaryParameters(Charset charset) { public ByteBuf sized(ByteBuf value) { return value; } + + @Test + void decodeString() { + Codec codec = getCodec(); + Charset c = ConnectionContextTest.mock().getClientCollation().getCharset(); + byte[] bOne = new byte[]{(byte)1}; + byte[] bZero = new byte[]{(byte)0}; + ByteBuffer bitValOne = ByteBuffer.wrap(bOne); + ByteBuffer bitValZero = ByteBuffer.wrap(bZero); + Decoding d1 = new Decoding(Unpooled.copiedBuffer("true", c), "true", MySqlType.VARCHAR); + Decoding d2 = new Decoding(Unpooled.copiedBuffer("false", c), "false", MySqlType.VARCHAR); + Decoding d3 = new Decoding(Unpooled.copiedBuffer("1", c), "1", MySqlType.VARCHAR); + Decoding d4 = new Decoding(Unpooled.copiedBuffer("0", c), "0", MySqlType.VARCHAR); + Decoding d5 = new Decoding(Unpooled.copiedBuffer("Y", c), "Y", MySqlType.VARCHAR); + Decoding d6 = new Decoding(Unpooled.copiedBuffer("no", c), "no", MySqlType.VARCHAR); + Decoding d7 = new Decoding(Unpooled.copiedBuffer("26.57", c), "26.57", MySqlType.VARCHAR); + Decoding d8 = new Decoding(Unpooled.copiedBuffer("-57", c), "=57", MySqlType.VARCHAR); + Decoding d9 = new Decoding(Unpooled.copiedBuffer("100000", c), "100000", MySqlType.VARCHAR); + Decoding d10 = new Decoding(Unpooled.copiedBuffer("-12345678901234567890", c), + "-12345678901234567890", MySqlType.VARCHAR); + Decoding d11 = new Decoding(Unpooled.copiedBuffer("Banana", c), "Banana", MySqlType.VARCHAR); + Decoding d12 = new Decoding(Unpooled.copiedBuffer(bitValOne), bitValOne, MySqlType.BIT); + Decoding d13 = new Decoding(Unpooled.copiedBuffer(bitValZero), bitValZero, MySqlType.BIT); + Decoding d14 = new Decoding(Unpooled.copyDouble(26.57d), 26.57d, MySqlType.DOUBLE); + Decoding d15 = new Decoding(Unpooled.copiedBuffer(bOne), bOne, MySqlType.TINYINT); + Decoding d16 = new Decoding(Unpooled.copiedBuffer(bZero), bZero, MySqlType.TINYINT); + Decoding d17 = new Decoding(Unpooled.copiedBuffer("1e4", c), "1e4", MySqlType.VARCHAR); + Decoding d18 = new Decoding(Unpooled.copiedBuffer("-1.34e10", c), "-1.34e10", MySqlType.VARCHAR); + Decoding d19 = new Decoding(Unpooled.copiedBuffer("-0", c), "-0", MySqlType.VARCHAR); + + assertThat(codec.decode(d1.content(), d1.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d1) + .isEqualTo(true); + + assertThat(codec.decode(d2.content(), d2.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d2) + .isEqualTo(false); + + assertThat(codec.decode(d3.content(), d3.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d3) + .isEqualTo(true); + + assertThat(codec.decode(d4.content(), d4.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d4) + .isEqualTo(false); + + assertThat(codec.decode(d5.content(), d5.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d5) + .isEqualTo(true); + + assertThat(codec.decode(d6.content(), d6.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d6) + .isEqualTo(false); + + assertThat(codec.decode(d7.content(), d7.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d7) + .isEqualTo(true); + + assertThat(codec.decode(d8.content(), d8.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d8) + .isEqualTo(false); + + assertThat(codec.decode(d9.content(), d9.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d9) + .isEqualTo(true); + + assertThat(codec.decode(d10.content(), d10.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d10) + .isEqualTo(false); + + assertThatThrownBy(() -> {codec.decode(d11.content(), d11.metadata(), Boolean.class, false, ConnectionContextTest.mock());}) + .isInstanceOf(R2dbcNonTransientException.class); + + assertThat(codec.decode(d12.content(), d12.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d12) + .isEqualTo(true); + + assertThat(codec.decode(d13.content(), d13.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d13) + .isEqualTo(false); + + assertThat(codec.decode(d14.content(), d14.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d14) + .isEqualTo(true); + + assertThat(codec.decode(d15.content(), d15.metadata(), Boolean.class, true, ConnectionContextTest.mock())) + .as("Decode failed, %s", d15) + .isEqualTo(true); + + assertThat(codec.decode(d16.content(), d16.metadata(), Boolean.class, true, ConnectionContextTest.mock())) + .as("Decode failed, %s", d16) + .isEqualTo(false); + + assertThat(codec.decode(d17.content(), d17.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d17) + .isEqualTo(true); + + assertThat(codec.decode(d18.content(), d18.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d18) + .isEqualTo(false); + + assertThat(codec.decode(d19.content(), d19.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d19) + .isEqualTo(false); + } }