Skip to content

Commit

Permalink
Implemented relaxed conversion logic for BooleanCodec (#290)
Browse files Browse the repository at this point in the history
@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.
  • Loading branch information
svats0001 authored Nov 29, 2024
1 parent acff429 commit 673aac8
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*/
Expand Down Expand Up @@ -55,4 +65,109 @@ public ByteBuf[] binaryParameters(Charset charset) {
public ByteBuf sized(ByteBuf value) {
return value;
}

@Test
void decodeString() {
Codec<Boolean> 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);
}
}

0 comments on commit 673aac8

Please sign in to comment.