diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index cd18ebd003776..85d5496bd6054 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -97,6 +97,7 @@ 1. Proxy: Fix column length for PostgreSQL string binary protocol value - [35840](https://github.com/apache/shardingsphere/pull/35840) 1. Proxy: Fix the connection leak caused by rollback failure in Proxy - [35867](https://github.com/apache/shardingsphere/pull/35867) 1. Proxy: Fix the behavior difference of select built-in function names with spaces -[#36537](https://github.com/apache/shardingsphere/pull/36537) +1. Proxy: Fix MySQL text protocol datetime fractional seconds output - [#37410](https://github.com/apache/shardingsphere/pull/37410) 1. Proxy: Fix IndexOutOfBoundsException for MySQL no-FROM multi-projection SELECT routed to admin path - [#37391](https://github.com/apache/shardingsphere/pull/37391) 1. Proxy: Fix MySQL binary protocol datetime/time fractional seconds precision - [#37294](https://github.com/apache/shardingsphere/pull/37294) 1. Proxy: Fix PostgreSQL boolean text output to return `t`/`f` as per protocol - [#37184](https://github.com/apache/shardingsphere/pull/37184) diff --git a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacket.java b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacket.java index 11fd151632a63..2b73862d1eee1 100644 --- a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacket.java +++ b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacket.java @@ -70,9 +70,27 @@ private void writeDataIntoPayload(final MySQLPacketPayload payload, final Object } else if (data instanceof Boolean) { payload.writeBytesLenenc((boolean) data ? new byte[]{1} : new byte[]{0}); } else if (data instanceof LocalDateTime) { - payload.writeStringLenenc(DateTimeFormatterFactory.getDatetimeFormatter().format((LocalDateTime) data)); + payload.writeStringLenenc(formatLocalDateTime((LocalDateTime) data)); } else { payload.writeStringLenenc(data.toString()); } } + + private String formatLocalDateTime(final LocalDateTime value) { + int nanos = value.getNano(); + if (0 == nanos) { + return DateTimeFormatterFactory.getDatetimeFormatter().format(value); + } + StringBuilder result = new StringBuilder(DateTimeFormatterFactory.getDatetimeFormatter().format(value)).append('.'); + String microsecondsText = String.format("%06d", nanos / 1000); + int endIndex = microsecondsText.length(); + while (endIndex > 0 && '0' == microsecondsText.charAt(endIndex - 1)) { + endIndex--; + } + if (0 == endIndex) { + return result.substring(0, result.length() - 1); + } + result.append(microsecondsText, 0, endIndex); + return result.toString(); + } } diff --git a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacketTest.java b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacketTest.java index f36c6e2635d57..16ad403cbfcc8 100644 --- a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacketTest.java +++ b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/command/query/text/MySQLTextResultSetRowPacketTest.java @@ -84,4 +84,12 @@ void assertLocalDateTime() { actual.write(payload); verify(payload).writeStringLenenc(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.parse(localDateTimeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))); } + + @Test + void assertLocalDateTimeWithMicros() { + LocalDateTime dateTime = LocalDateTime.of(2022, 2, 18, 17, 32, 38, 123456000); + MySQLTextResultSetRowPacket actual = new MySQLTextResultSetRowPacket(Collections.singletonList(dateTime)); + actual.write(payload); + verify(payload).writeStringLenenc("2022-02-18 17:32:38.123456"); + } } diff --git a/kernel/data-pipeline/scenario/cdc/core/src/test/java/org/apache/shardingsphere/data/pipeline/cdc/util/ColumnValueConvertUtilsTest.java b/kernel/data-pipeline/scenario/cdc/core/src/test/java/org/apache/shardingsphere/data/pipeline/cdc/util/ColumnValueConvertUtilsTest.java index 0fef98ee44826..74b058801df6d 100644 --- a/kernel/data-pipeline/scenario/cdc/core/src/test/java/org/apache/shardingsphere/data/pipeline/cdc/util/ColumnValueConvertUtilsTest.java +++ b/kernel/data-pipeline/scenario/cdc/core/src/test/java/org/apache/shardingsphere/data/pipeline/cdc/util/ColumnValueConvertUtilsTest.java @@ -28,13 +28,23 @@ import com.google.protobuf.StringValue; import org.junit.jupiter.api.Test; +import javax.sql.rowset.serial.SerialBlob; +import javax.sql.rowset.serial.SerialClob; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.OffsetTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Date; import static org.hamcrest.MatcherAssert.assertThat; @@ -42,79 +52,188 @@ import static org.hamcrest.Matchers.isA; import static org.junit.jupiter.api.Assertions.assertTrue; +@SuppressWarnings("UseOfObsoleteDateTimeApi") class ColumnValueConvertUtilsTest { - @SuppressWarnings("UseOfObsoleteDateTimeApi") - @Test - void assertConvertToProtobufMessage() { - Message actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(null); - assertThat(actualMessage, isA(Empty.class)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(1); - assertThat(actualMessage, isA(Int32Value.class)); - assertThat(((Int32Value) actualMessage).getValue(), is(1)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage((byte) 1); - assertThat(actualMessage, isA(Int32Value.class)); - assertThat(((Int32Value) actualMessage).getValue(), is(1)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage((short) 1); - assertThat(actualMessage, isA(Int32Value.class)); - assertThat(((Int32Value) actualMessage).getValue(), is(1)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(1L); - assertThat(actualMessage, isA(Int64Value.class)); - assertThat(((Int64Value) actualMessage).getValue(), is(1L)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(new BigInteger("1234")); - assertThat(actualMessage, isA(StringValue.class)); - assertThat(new BigInteger(((StringValue) actualMessage).getValue()), is(new BigInteger("1234"))); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(1.0F); - assertThat(actualMessage, isA(FloatValue.class)); - assertThat(((FloatValue) actualMessage).getValue(), is(1.0F)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(1.23); - assertThat(actualMessage, isA(DoubleValue.class)); - assertThat(((DoubleValue) actualMessage).getValue(), is(1.23)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(new BigDecimal("100")); - assertThat(actualMessage, isA(StringValue.class)); - assertThat(((StringValue) actualMessage).getValue(), is("100")); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage("abcd"); - assertThat(actualMessage, isA(StringValue.class)); - assertThat(((StringValue) actualMessage).getValue(), is("abcd")); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(true); - assertThat(actualMessage, isA(BoolValue.class)); - assertTrue(((BoolValue) actualMessage).getValue()); - Timestamp now = new Timestamp(System.currentTimeMillis()); - long epochSecond = now.toInstant().getEpochSecond(); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(now.toLocalDateTime()); - assertThat(actualMessage, isA(com.google.protobuf.Timestamp.class)); - assertThat(((com.google.protobuf.Timestamp) actualMessage).getSeconds(), is(epochSecond)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(now); - assertThat(actualMessage, isA(com.google.protobuf.Timestamp.class)); - assertThat(((com.google.protobuf.Timestamp) actualMessage).getSeconds(), is(epochSecond)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(new Date(now.getTime())); - assertThat(actualMessage, isA(com.google.protobuf.Timestamp.class)); - assertThat(((com.google.protobuf.Timestamp) actualMessage).getSeconds(), is(epochSecond)); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(now.toInstant()); - assertThat(actualMessage, isA(com.google.protobuf.Timestamp.class)); - assertThat(((com.google.protobuf.Timestamp) actualMessage).getNanos(), is(now.toInstant().getNano())); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(now.toLocalDateTime().toLocalTime()); - assertThat(actualMessage, isA(Int64Value.class)); - assertThat(((Int64Value) actualMessage).getValue(), is(now.toLocalDateTime().toLocalTime().toNanoOfDay())); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage("123456".getBytes()); - assertThat(actualMessage, isA(BytesValue.class)); - assertThat(((BytesValue) actualMessage).getValue().toByteArray(), is("123456".getBytes())); - OffsetTime offsetTime = OffsetTime.now(); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(offsetTime); - assertThat(actualMessage, isA(Int64Value.class)); - assertThat(((Int64Value) actualMessage).getValue(), is(offsetTime.toLocalTime().toNanoOfDay())); - OffsetDateTime offsetDateTime = OffsetDateTime.now(); - actualMessage = ColumnValueConvertUtils.convertToProtobufMessage(offsetDateTime); - assertThat(actualMessage, isA(com.google.protobuf.Timestamp.class)); - assertThat(((com.google.protobuf.Timestamp) actualMessage).getSeconds(), is(offsetDateTime.toEpochSecond())); - assertThat(((com.google.protobuf.Timestamp) actualMessage).getNanos(), is(offsetDateTime.getNano())); - } - - @Test - void assertTimeConvert() { - Time time = new Time(-3600L * 1000L - 1234L); + @Test + void assertConvertNullToEmpty() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(null); + assertThat(actual, isA(Empty.class)); + } + + @Test + void assertConvertIntegerToInt32Value() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(1); + assertThat(((Int32Value) actual).getValue(), is(1)); + } + + @Test + void assertConvertShortToInt32Value() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage((short) 2); + assertThat(((Int32Value) actual).getValue(), is(2)); + } + + @Test + void assertConvertByteToInt32Value() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage((byte) 3); + assertThat(((Int32Value) actual).getValue(), is(3)); + } + + @Test + void assertConvertLongToInt64Value() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(4L); + assertThat(((Int64Value) actual).getValue(), is(4L)); + } + + @Test + void assertConvertBigIntegerToStringValue() { + BigInteger expectedBigInteger = new BigInteger("1234"); + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(expectedBigInteger); + assertThat(new BigInteger(((StringValue) actual).getValue()), is(expectedBigInteger)); + } + + @Test + void assertConvertFloatToFloatValue() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(1.5F); + assertThat(((FloatValue) actual).getValue(), is(1.5F)); + } + + @Test + void assertConvertDoubleToDoubleValue() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(2.5D); + assertThat(((DoubleValue) actual).getValue(), is(2.5D)); + } + + @Test + void assertConvertBigDecimalToStringValue() { + BigDecimal expectedBigDecimal = new BigDecimal("1000.01"); + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(expectedBigDecimal); + assertThat(((StringValue) actual).getValue(), is(expectedBigDecimal.toString())); + } + + @Test + void assertConvertStringToStringValue() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage("abcd"); + assertThat(((StringValue) actual).getValue(), is("abcd")); + } + + @Test + void assertConvertBooleanToBoolValue() { + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(true); + assertTrue(((BoolValue) actual).getValue()); + } + + @Test + void assertConvertBytesToBytesValue() { + byte[] expectedBytes = "123456".getBytes(StandardCharsets.UTF_8); + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(expectedBytes); + assertThat(((BytesValue) actual).getValue().toByteArray(), is(expectedBytes)); + } + + @Test + void assertConvertTimeToInt64Value() { + Time time = Time.valueOf(LocalTime.of(1, 2, 3)); int nanos = new Timestamp(time.getTime()).getNanos(); - Int64Value actualMessage = (Int64Value) ColumnValueConvertUtils.convertToProtobufMessage(time); - assertThat(LocalTime.ofNanoOfDay(actualMessage.getValue()), is(time.toLocalTime().withNano(nanos))); + Int64Value actual = (Int64Value) ColumnValueConvertUtils.convertToProtobufMessage(time); + assertThat(LocalTime.ofNanoOfDay(actual.getValue()), is(LocalTime.of(1, 2, 3, nanos))); + } + + @Test + void assertConvertSqlDateToInt64Value() { + java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.of(2020, 1, 2)); + Int64Value actual = (Int64Value) ColumnValueConvertUtils.convertToProtobufMessage(sqlDate); + assertThat(actual.getValue(), is(sqlDate.toLocalDate().toEpochDay())); + } + + @Test + void assertConvertUtilDateToTimestampMessage() { + Date utilDate = new Date(1_600_000_000_123L); + com.google.protobuf.Timestamp actual = (com.google.protobuf.Timestamp) ColumnValueConvertUtils.convertToProtobufMessage(utilDate); + assertThat(actual.getSeconds(), is(utilDate.getTime() / 1000L)); + assertThat(actual.getNanos(), is((int) ((utilDate.getTime() % 1000L) * 1_000_000L))); + } + + @Test + void assertConvertLocalDateTimeToTimestampMessage() { + LocalDateTime localDateTime = LocalDateTime.of(2021, 5, 6, 7, 8, 9, 123000000); + com.google.protobuf.Timestamp actual = (com.google.protobuf.Timestamp) ColumnValueConvertUtils.convertToProtobufMessage(localDateTime); + Timestamp expectedTimestamp = Timestamp.valueOf(localDateTime); + assertThat(actual.getSeconds(), is(expectedTimestamp.getTime() / 1000L)); + assertThat(actual.getNanos(), is(expectedTimestamp.getNanos())); + } + + @Test + void assertConvertLocalDateToInt64Value() { + LocalDate localDate = LocalDate.of(2022, 3, 4); + Int64Value actual = (Int64Value) ColumnValueConvertUtils.convertToProtobufMessage(localDate); + assertThat(actual.getValue(), is(localDate.toEpochDay())); + } + + @Test + void assertConvertLocalTimeToInt64Value() { + LocalTime localTime = LocalTime.of(5, 6, 7, 8); + Int64Value actual = (Int64Value) ColumnValueConvertUtils.convertToProtobufMessage(localTime); + assertThat(actual.getValue(), is(localTime.toNanoOfDay())); + } + + @Test + void assertConvertOffsetDateTimeToTimestampMessage() { + OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.of(2020, 1, 2, 3, 4, 5, 600000000), ZoneOffset.ofHours(1)); + com.google.protobuf.Timestamp actual = (com.google.protobuf.Timestamp) ColumnValueConvertUtils.convertToProtobufMessage(offsetDateTime); + Timestamp expectedTimestamp = Timestamp.valueOf(offsetDateTime.toLocalDateTime()); + assertThat(actual.getSeconds(), is(expectedTimestamp.getTime() / 1000L)); + assertThat(actual.getNanos(), is(expectedTimestamp.getNanos())); + } + + @Test + void assertConvertOffsetTimeToInt64Value() { + OffsetTime offsetTime = OffsetTime.of(1, 2, 3, 4, ZoneOffset.ofHours(-2)); + Int64Value actual = (Int64Value) ColumnValueConvertUtils.convertToProtobufMessage(offsetTime); + assertThat(actual.getValue(), is(offsetTime.toLocalTime().toNanoOfDay())); + } + + @Test + void assertConvertZonedDateTimeToTimestampMessage() { + ZonedDateTime zonedDateTime = ZonedDateTime.of(LocalDateTime.of(2022, 7, 8, 9, 10, 11, 120000000), ZoneId.of("UTC")); + com.google.protobuf.Timestamp actual = (com.google.protobuf.Timestamp) ColumnValueConvertUtils.convertToProtobufMessage(zonedDateTime); + Timestamp expectedTimestamp = Timestamp.valueOf(zonedDateTime.toLocalDateTime()); + assertThat(actual.getSeconds(), is(expectedTimestamp.getTime() / 1000L)); + assertThat(actual.getNanos(), is(expectedTimestamp.getNanos())); + } + + @Test + void assertConvertInstantToTimestampMessage() { + Instant instant = Instant.ofEpochSecond(1_700_000_000L, 123_000_000L); + com.google.protobuf.Timestamp actual = (com.google.protobuf.Timestamp) ColumnValueConvertUtils.convertToProtobufMessage(instant); + assertThat(actual.getSeconds(), is(instant.getEpochSecond())); + assertThat(actual.getNanos(), is(instant.getNano())); + } + + @Test + void assertConvertClobToStringValue() throws SQLException { + SerialClob clob = new SerialClob("clob_value".toCharArray()); + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(clob); + assertThat(((StringValue) actual).getValue(), is("clob_value")); + } + + @Test + void assertConvertBlobToBytesValue() throws SQLException { + byte[] expectedBlobBytes = "blob_value".getBytes(StandardCharsets.UTF_8); + SerialBlob blob = new SerialBlob(expectedBlobBytes); + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(blob); + assertThat(((BytesValue) actual).getValue().toByteArray(), is(expectedBlobBytes)); + } + + @Test + void assertConvertCustomObjectToStringValue() { + Object customObject = new Object() { + + @Override + public String toString() { + return "custom_object"; + } + }; + Message actual = ColumnValueConvertUtils.convertToProtobufMessage(customObject); + assertThat(((StringValue) actual).getValue(), is("custom_object")); } }