This is an automated email from the ASF dual-hosted git repository.
sandynz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new da38be837eb mysql: escape JSON binlog control characters (#38800)
da38be837eb is described below
commit da38be837eb600ffafe75f755821604c45d3362b
Author: Hongsheng Zhong <[email protected]>
AuthorDate: Thu Jun 4 20:17:35 2026 +0800
mysql: escape JSON binlog control characters (#38800)
* mysql: escape JSON binlog control characters
* Update RELEASE-NOTES.md
---
RELEASE-NOTES.md | 3 +-
.../column/value/string/MySQLJsonValueDecoder.java | 36 +++++++++++++++++++---
.../value/string/MySQLJsonValueDecoderTest.java | 13 ++++++++
.../general/MySQLMigrationGeneralE2EIT.java | 5 ++-
4 files changed, 51 insertions(+), 6 deletions(-)
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 72134e2ff89..99cbdb0275b 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -22,7 +22,8 @@
1. Proxy: Force PostgreSQL backend `client_encoding` to UTF8 and reject
non-UTF8 values to keep pgjdbc parameter encoding consistent -
[#38645](https://github.com/apache/shardingsphere/pull/38645)
1. Mode: Fix rule metadata not removed from memory after dropping rules in
Etcd cluster mode -
[#38561](https://github.com/apache/shardingsphere/pull/38561)
1. Pipeline: Fix MySQL JSON literal decoding in migration -
[#38622](https://github.com/apache/shardingsphere/pull/38622)
-1. Pipeline: Fix MySQL zero-value temporal binlog decoding with fractional
precision in migration -
[#35531](https://github.com/apache/shardingsphere/issues/35531)
+1. Pipeline: Fix MySQL zero-value temporal binlog decoding with fractional
precision in migration -
[#38629](https://github.com/apache/shardingsphere/pull/38629)
+1. Pipeline: Fix escape MySQL JSON binlog control characters -
[#38800](https://github.com/apache/shardingsphere/pull/38800)
1. Sharding: Support ORDER BY MySQL VARBINARY column by wrapping byte[] values
in a Comparable adapter -
[#38699](https://github.com/apache/shardingsphere/pull/38699)
### Enhancements
diff --git
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
index 4e8201e16df..d1056f03274 100644
---
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
+++
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
@@ -117,7 +117,8 @@ public final class MySQLJsonValueDecoder {
if (0 < i) {
stringBuilder.append(',');
}
- stringBuilder.append('"').append(keys[i]).append("\":");
+ outputString(keys[i], stringBuilder);
+ stringBuilder.append(':');
decodeValueEntry(isSmall, byteBuf, stringBuilder);
}
stringBuilder.append('}');
@@ -233,10 +234,37 @@ public final class MySQLJsonValueDecoder {
out.append('"');
for (int i = 0; i < str.length(); ++i) {
char c = str.charAt(i);
- if (c == '"' || c == '\\') {
- out.append('\\');
+ switch (c) {
+ case '"':
+ case '\\':
+ out.append('\\').append(c);
+ break;
+ case '\b':
+ out.append("\\b");
+ break;
+ case '\f':
+ out.append("\\f");
+ break;
+ case '\n':
+ out.append("\\n");
+ break;
+ case '\r':
+ out.append("\\r");
+ break;
+ case '\t':
+ out.append("\\t");
+ break;
+ default:
+ if (c < ' ') {
+ out.append("\\u00");
+ if (c < 0x10) {
+ out.append('0');
+ }
+ out.append(Integer.toHexString(c));
+ } else {
+ out.append(c);
+ }
}
- out.append(c);
}
out.append('"');
}
diff --git
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
index c4d48d1b02a..d89b845027c 100644
---
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
+++
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
@@ -280,6 +280,13 @@ class MySQLJsonValueDecoderTest {
Arguments.of("decode top-level literal false",
mockTopLevelLiteralByteBuf(JsonValueTypes.LITERAL_FALSE), "false"),
Arguments.of("decode top-level string",
mockTopLevelStringByteBuf("hello"), "\"hello\""),
Arguments.of("decode top-level string with escaped chars",
mockTopLevelStringByteBuf("a\"\\b"), "\"a\\\"\\\\b\""),
+ Arguments.of("decode top-level string with backspace",
mockTopLevelStringByteBuf("a\bb"), "\"a\\bb\""),
+ Arguments.of("decode top-level string with form feed",
mockTopLevelStringByteBuf("a\fb"), "\"a\\fb\""),
+ Arguments.of("decode top-level string with newline",
mockTopLevelStringByteBuf("a\nb"), "\"a\\nb\""),
+ Arguments.of("decode top-level string with carriage return",
mockTopLevelStringByteBuf("a\rb"), "\"a\\rb\""),
+ Arguments.of("decode top-level string with tab",
mockTopLevelStringByteBuf("a\tb"), "\"a\\tb\""),
+ Arguments.of("decode top-level string with unicode control
char 0x01", mockTopLevelStringByteBuf("a" + (char) 0x01 + "b"),
"\"a\\u0001b\""),
+ Arguments.of("decode top-level string with unicode control
char 0x1f", mockTopLevelStringByteBuf("a" + (char) 0x1f + "b"),
"\"a\\u001fb\""),
Arguments.of("decode top-level int32",
mockTopLevelInt32ByteBuf(123), "123"),
Arguments.of("decode top-level double",
mockTopLevelDoubleByteBuf(45.6D), "45.6"),
Arguments.of("decode int16 top-level",
mockTopLevelInt16ByteBuf(), "-32768"),
@@ -320,6 +327,12 @@ class MySQLJsonValueDecoderTest {
"{\"key1\":9223372036854775807,\"key2\":9223372036854775808}"),
Arguments.of("decode small object with double",
Arrays.asList(new Object[][]{new Object[]{JsonValueTypes.DOUBLE, "key1",
Double.MAX_VALUE}}),
"{\"key1\":1.7976931348623157E308}"),
+ Arguments.of("decode small object with tab string",
Collections.singletonList(new Object[]{JsonValueTypes.STRING, "key1", "a\tb"}),
+ "{\"key1\":\"a\\tb\"}"),
+ Arguments.of("decode small object with unicode control
string", Collections.singletonList(new Object[]{JsonValueTypes.STRING, "key1",
"a" + (char) 0x01 + "b"}),
+ "{\"key1\":\"a\\u0001b\"}"),
+ Arguments.of("decode small object with tab key",
Collections.singletonList(new Object[]{JsonValueTypes.STRING, "key\t1",
"value1"}),
+ "{\"key\\t1\":\"value1\"}"),
Arguments.of("decode small object with string",
Arrays.asList(new Object[]{JsonValueTypes.STRING,
"key1", value1}, new Object[]{JsonValueTypes.STRING, "key2", value2}, new
Object[]{JsonValueTypes.STRING, "key3", value3},
new Object[]{JsonValueTypes.STRING, "key4",
value4}),
diff --git
a/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
b/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
index 30572e86e71..c5910a6b094 100644
---
a/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
+++
b/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
@@ -84,7 +84,10 @@ class MySQLMigrationGeneralE2EIT extends
AbstractMigrationE2EIT {
containerComposer.startIncrementTask(
new
E2EIncrementalTask(containerComposer.getSourceDataSource(), SOURCE_TABLE_NAME,
new SnowflakeKeyGenerateAlgorithm(), containerComposer.getDatabaseType(), 30));
TimeUnit.SECONDS.timedJoin(containerComposer.getIncreaseTaskThread(), 30L);
- orderDAO.insert(10000L, 1, "OK");
+ containerComposer.sourceExecuteWithLog(String.format("""
+ INSERT INTO %s (order_id, user_id, status, t_json) VALUES
+ (10000, 1, 'OK',
'{"backspace":"a\\\\bb","form_feed":"a\\\\fb","newline":"a\\\\nb","carriage_return":"a\\\\rb","tab":"a\\\\tb","unicode":"a\\\\u0001b","tab\\\\tkey":"value"}')
+ """, SOURCE_TABLE_NAME));
orderItemDAO.insert(10000L, 10000L, 1, "OK");
distSQLFacade.pauseJob(orderJobId);
distSQLFacade.resumeJob(orderJobId);