Skip to content

Commit 673aac8

Browse files
authored
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.
1 parent acff429 commit 673aac8

File tree

2 files changed

+161
-3
lines changed

2 files changed

+161
-3
lines changed

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616

1717
package io.asyncer.r2dbc.mysql.codec;
1818

19+
import java.math.BigInteger;
20+
1921
import io.asyncer.r2dbc.mysql.MySqlParameter;
2022
import io.asyncer.r2dbc.mysql.ParameterWriter;
2123
import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata;
2224
import io.asyncer.r2dbc.mysql.constant.MySqlType;
2325
import io.netty.buffer.ByteBuf;
2426
import io.netty.buffer.ByteBufAllocator;
27+
import io.r2dbc.spi.R2dbcNonTransientResourceException;
2528
import reactor.core.publisher.Mono;
2629

2730
/**
@@ -38,7 +41,35 @@ private BooleanCodec() {
3841
@Override
3942
public Boolean decode(ByteBuf value, MySqlReadableMetadata metadata, Class<?> target, boolean binary,
4043
CodecContext context) {
41-
return binary || metadata.getType() == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0';
44+
MySqlType dataType = metadata.getType();
45+
46+
if (dataType == MySqlType.VARCHAR) {
47+
if (!value.isReadable()) {
48+
return createFromLong(0);
49+
}
50+
51+
String s = value.toString(metadata.getCharCollation(context).getCharset());
52+
53+
if (s.equalsIgnoreCase("Y") || s.equalsIgnoreCase("yes") ||
54+
s.equalsIgnoreCase("T") || s.equalsIgnoreCase("true")) {
55+
return createFromLong(1);
56+
} else if (s.equalsIgnoreCase("N") || s.equalsIgnoreCase("no") ||
57+
s.equalsIgnoreCase("F") || s.equalsIgnoreCase("false")) {
58+
return createFromLong(0);
59+
} else if (s.matches("-?\\d*\\.\\d*") || s.matches("-?\\d*\\.\\d+[eE]-?\\d+")
60+
|| s.matches("-?\\d*[eE]-?\\d+")) {
61+
return createFromDouble(Double.parseDouble(s));
62+
} else if (s.matches("-?\\d+")) {
63+
if (!CodecUtils.isGreaterThanLongMax(s)) {
64+
return createFromLong(CodecUtils.parseLong(value));
65+
}
66+
return createFromBigInteger(new BigInteger(s));
67+
}
68+
throw new R2dbcNonTransientResourceException("The value '" + s + "' of type '" + dataType +
69+
"' cannot be encoded into a Boolean.", "22018");
70+
}
71+
72+
return binary || dataType == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0';
4273
}
4374

4475
@Override
@@ -54,8 +85,20 @@ public MySqlParameter encode(Object value, CodecContext context) {
5485
@Override
5586
public boolean doCanDecode(MySqlReadableMetadata metadata) {
5687
MySqlType type = metadata.getType();
57-
return (type == MySqlType.BIT || type == MySqlType.TINYINT) &&
58-
Integer.valueOf(1).equals(metadata.getPrecision());
88+
return ((type == MySqlType.BIT || type == MySqlType.TINYINT) &&
89+
Integer.valueOf(1).equals(metadata.getPrecision())) || type == MySqlType.VARCHAR;
90+
}
91+
92+
public Boolean createFromLong(long l) {
93+
return (l == -1 || l > 0);
94+
}
95+
96+
public Boolean createFromDouble(double d) {
97+
return (d == -1.0d || d > 0);
98+
}
99+
100+
public Boolean createFromBigInteger(BigInteger b) {
101+
return b.compareTo(BigInteger.valueOf(0)) > 0 || b.compareTo(BigInteger.valueOf(-1)) == 0;
59102
}
60103

61104
private static final class BooleanMySqlParameter extends AbstractMySqlParameter {

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,22 @@
1616

1717
package io.asyncer.r2dbc.mysql.codec;
1818

19+
import io.asyncer.r2dbc.mysql.ConnectionContextTest;
20+
import io.asyncer.r2dbc.mysql.constant.MySqlType;
1921
import io.netty.buffer.ByteBuf;
2022
import io.netty.buffer.Unpooled;
23+
import io.r2dbc.spi.R2dbcNonTransientException;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2127

2228
import java.nio.charset.Charset;
2329
import java.util.Arrays;
2430

31+
import org.junit.jupiter.api.Test;
32+
33+
import java.nio.ByteBuffer;
34+
2535
/**
2636
* Unit tests for {@link BooleanCodec}.
2737
*/
@@ -55,4 +65,109 @@ public ByteBuf[] binaryParameters(Charset charset) {
5565
public ByteBuf sized(ByteBuf value) {
5666
return value;
5767
}
68+
69+
@Test
70+
void decodeString() {
71+
Codec<Boolean> codec = getCodec();
72+
Charset c = ConnectionContextTest.mock().getClientCollation().getCharset();
73+
byte[] bOne = new byte[]{(byte)1};
74+
byte[] bZero = new byte[]{(byte)0};
75+
ByteBuffer bitValOne = ByteBuffer.wrap(bOne);
76+
ByteBuffer bitValZero = ByteBuffer.wrap(bZero);
77+
Decoding d1 = new Decoding(Unpooled.copiedBuffer("true", c), "true", MySqlType.VARCHAR);
78+
Decoding d2 = new Decoding(Unpooled.copiedBuffer("false", c), "false", MySqlType.VARCHAR);
79+
Decoding d3 = new Decoding(Unpooled.copiedBuffer("1", c), "1", MySqlType.VARCHAR);
80+
Decoding d4 = new Decoding(Unpooled.copiedBuffer("0", c), "0", MySqlType.VARCHAR);
81+
Decoding d5 = new Decoding(Unpooled.copiedBuffer("Y", c), "Y", MySqlType.VARCHAR);
82+
Decoding d6 = new Decoding(Unpooled.copiedBuffer("no", c), "no", MySqlType.VARCHAR);
83+
Decoding d7 = new Decoding(Unpooled.copiedBuffer("26.57", c), "26.57", MySqlType.VARCHAR);
84+
Decoding d8 = new Decoding(Unpooled.copiedBuffer("-57", c), "=57", MySqlType.VARCHAR);
85+
Decoding d9 = new Decoding(Unpooled.copiedBuffer("100000", c), "100000", MySqlType.VARCHAR);
86+
Decoding d10 = new Decoding(Unpooled.copiedBuffer("-12345678901234567890", c),
87+
"-12345678901234567890", MySqlType.VARCHAR);
88+
Decoding d11 = new Decoding(Unpooled.copiedBuffer("Banana", c), "Banana", MySqlType.VARCHAR);
89+
Decoding d12 = new Decoding(Unpooled.copiedBuffer(bitValOne), bitValOne, MySqlType.BIT);
90+
Decoding d13 = new Decoding(Unpooled.copiedBuffer(bitValZero), bitValZero, MySqlType.BIT);
91+
Decoding d14 = new Decoding(Unpooled.copyDouble(26.57d), 26.57d, MySqlType.DOUBLE);
92+
Decoding d15 = new Decoding(Unpooled.copiedBuffer(bOne), bOne, MySqlType.TINYINT);
93+
Decoding d16 = new Decoding(Unpooled.copiedBuffer(bZero), bZero, MySqlType.TINYINT);
94+
Decoding d17 = new Decoding(Unpooled.copiedBuffer("1e4", c), "1e4", MySqlType.VARCHAR);
95+
Decoding d18 = new Decoding(Unpooled.copiedBuffer("-1.34e10", c), "-1.34e10", MySqlType.VARCHAR);
96+
Decoding d19 = new Decoding(Unpooled.copiedBuffer("-0", c), "-0", MySqlType.VARCHAR);
97+
98+
assertThat(codec.decode(d1.content(), d1.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
99+
.as("Decode failed, %s", d1)
100+
.isEqualTo(true);
101+
102+
assertThat(codec.decode(d2.content(), d2.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
103+
.as("Decode failed, %s", d2)
104+
.isEqualTo(false);
105+
106+
assertThat(codec.decode(d3.content(), d3.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
107+
.as("Decode failed, %s", d3)
108+
.isEqualTo(true);
109+
110+
assertThat(codec.decode(d4.content(), d4.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
111+
.as("Decode failed, %s", d4)
112+
.isEqualTo(false);
113+
114+
assertThat(codec.decode(d5.content(), d5.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
115+
.as("Decode failed, %s", d5)
116+
.isEqualTo(true);
117+
118+
assertThat(codec.decode(d6.content(), d6.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
119+
.as("Decode failed, %s", d6)
120+
.isEqualTo(false);
121+
122+
assertThat(codec.decode(d7.content(), d7.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
123+
.as("Decode failed, %s", d7)
124+
.isEqualTo(true);
125+
126+
assertThat(codec.decode(d8.content(), d8.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
127+
.as("Decode failed, %s", d8)
128+
.isEqualTo(false);
129+
130+
assertThat(codec.decode(d9.content(), d9.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
131+
.as("Decode failed, %s", d9)
132+
.isEqualTo(true);
133+
134+
assertThat(codec.decode(d10.content(), d10.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
135+
.as("Decode failed, %s", d10)
136+
.isEqualTo(false);
137+
138+
assertThatThrownBy(() -> {codec.decode(d11.content(), d11.metadata(), Boolean.class, false, ConnectionContextTest.mock());})
139+
.isInstanceOf(R2dbcNonTransientException.class);
140+
141+
assertThat(codec.decode(d12.content(), d12.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
142+
.as("Decode failed, %s", d12)
143+
.isEqualTo(true);
144+
145+
assertThat(codec.decode(d13.content(), d13.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
146+
.as("Decode failed, %s", d13)
147+
.isEqualTo(false);
148+
149+
assertThat(codec.decode(d14.content(), d14.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
150+
.as("Decode failed, %s", d14)
151+
.isEqualTo(true);
152+
153+
assertThat(codec.decode(d15.content(), d15.metadata(), Boolean.class, true, ConnectionContextTest.mock()))
154+
.as("Decode failed, %s", d15)
155+
.isEqualTo(true);
156+
157+
assertThat(codec.decode(d16.content(), d16.metadata(), Boolean.class, true, ConnectionContextTest.mock()))
158+
.as("Decode failed, %s", d16)
159+
.isEqualTo(false);
160+
161+
assertThat(codec.decode(d17.content(), d17.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
162+
.as("Decode failed, %s", d17)
163+
.isEqualTo(true);
164+
165+
assertThat(codec.decode(d18.content(), d18.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
166+
.as("Decode failed, %s", d18)
167+
.isEqualTo(false);
168+
169+
assertThat(codec.decode(d19.content(), d19.metadata(), Boolean.class, false, ConnectionContextTest.mock()))
170+
.as("Decode failed, %s", d19)
171+
.isEqualTo(false);
172+
}
58173
}

0 commit comments

Comments
 (0)