This is an automated email from the ASF dual-hosted git repository.
jimin pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new 55bd893dbd optimize: add support for Jackson serialization and
deserialization of PostgreSQL array types (#7669)
55bd893dbd is described below
commit 55bd893dbdf145bc215c516c5a22bc415c7cd05f
Author: maple <[email protected]>
AuthorDate: Mon Sep 29 10:15:12 2025 +0800
optimize: add support for Jackson serialization and deserialization of
PostgreSQL array types (#7669)
---
changes/en-us/2.x.md | 1 +
changes/zh-cn/2.x.md | 1 +
.../undo/parser/JacksonUndoLogParser.java | 116 ++++++++++++-
.../undo/parser/JacksonUndoLogParserTest.java | 181 +++++++++++++++++++++
4 files changed, 298 insertions(+), 1 deletion(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index d30c2bd7d6..32658d7018 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -25,6 +25,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#7492](https://github.com/apache/incubator-seata/pull/7492)] upgrade HTTP
client in common module to support HTTP/2
- [[#7551](https://github.com/apache/incubator-seata/pull/7551)] XAUtils add
support for DM Database
- [[#7559](https://github.com/apache/incubator-seata/pull/7559)] Introduce
Cleanup API for TableMetaRefreshHolder Instance
+- [[#7669](https://github.com/apache/incubator-seata/pull/7669)] add support
for Jackson serialization and deserialization of PostgreSQL array types
### bugfix:
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 1fcb5dbb73..7bdf2afd13 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -25,6 +25,7 @@
- [[#7492](https://github.com/apache/incubator-seata/pull/7492)] 升级 common
模块中的 HTTP 客户端以支持 HTTP/2
- [[#7551](https://github.com/apache/incubator-seata/pull/7551)] XAUtils支持达梦数据库
- [[#7559](https://github.com/apache/incubator-seata/pull/7559)] 为
TableMetaRefreshHolder 实例引入清理 API
+- [[#7669](https://github.com/apache/incubator-seata/pull/7669)] 添加对 Jackson
序列化和反序列化 PostgreSQL 数组类型的支持
### bugfix:
diff --git
a/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParser.java
b/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParser.java
index ae399e884c..a9944f1d9b 100644
---
a/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParser.java
+++
b/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParser.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -40,6 +41,7 @@ import org.apache.seata.common.loader.EnhancedServiceLoader;
import org.apache.seata.common.loader.EnhancedServiceNotFoundException;
import org.apache.seata.common.loader.LoadLevel;
import org.apache.seata.common.util.CollectionUtils;
+import org.apache.seata.rm.datasource.sql.serial.SerialArray;
import org.apache.seata.rm.datasource.undo.BranchUndoLog;
import org.apache.seata.rm.datasource.undo.UndoLogParser;
import org.apache.seata.rm.datasource.undo.parser.spi.JacksonSerializer;
@@ -136,6 +138,16 @@ public class JacksonUndoLogParser implements
UndoLogParser, Initialize {
*/
private final JsonDeserializer dmdbTimestampDeserializer = new
DmdbTimestampDeserializer();
+ /**
+ * customize serializer for
org.apache.seata.rm.datasource.sql.serial.SerialArray
+ */
+ private final JsonSerializer serialArraySerializer = new
SerialArraySerializer();
+
+ /**
+ * customize deserializer for
org.apache.seata.rm.datasource.sql.serial.SerialArray
+ */
+ private final JsonDeserializer serialArrayDeserializer = new
SerialArrayDeserializer();
+
@Override
public void init() {
try {
@@ -170,6 +182,8 @@ public class JacksonUndoLogParser implements UndoLogParser,
Initialize {
module.addDeserializer(SerialClob.class, clobDeserializer);
module.addSerializer(LocalDateTime.class, localDateTimeSerializer);
module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
+ module.addSerializer(SerialArray.class, serialArraySerializer);
+ module.addDeserializer(SerialArray.class, serialArrayDeserializer);
registerDmdbTimestampModuleIfPresent();
mapper.registerModule(module);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
@@ -483,7 +497,7 @@ public class JacksonUndoLogParser implements UndoLogParser,
Initialize {
}
}
- public class DmdbTimestampDeserializer extends JsonDeserializer<Object> {
+ private class DmdbTimestampDeserializer extends JsonDeserializer<Object> {
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) {
@@ -532,4 +546,104 @@ public class JacksonUndoLogParser implements
UndoLogParser, Initialize {
Objects.requireNonNull(zoneId, "zoneId must be not null");
JacksonUndoLogParser.zoneId = zoneId;
}
+
+ /**
+ * the class of serialize SerialArray type
+ */
+ private static class SerialArraySerializer extends
JsonSerializer<SerialArray> {
+
+ @Override
+ public void serializeWithType(
+ SerialArray serialArray,
+ JsonGenerator gen,
+ SerializerProvider serializers,
+ TypeSerializer typeSerializer)
+ throws IOException {
+ WritableTypeId typeIdDef =
+ typeSerializer.writeTypePrefix(gen,
typeSerializer.typeId(serialArray, JsonToken.START_OBJECT));
+ serializeValue(serialArray, gen, serializers);
+ typeSerializer.writeTypeSuffix(gen, typeIdDef);
+ }
+
+ @Override
+ public void serialize(SerialArray serialArray, JsonGenerator gen,
SerializerProvider serializers)
+ throws IOException {
+ gen.writeStartObject();
+ serializeValue(serialArray, gen, serializers);
+ gen.writeEndObject();
+ }
+
+ private void serializeValue(SerialArray serialArray, JsonGenerator
gen, SerializerProvider serializers)
+ throws IOException {
+ gen.writeFieldName("baseType");
+ try {
+ gen.writeNumber(serialArray.getBaseType());
+ } catch (SQLException e) {
+ gen.writeNull();
+ }
+ gen.writeFieldName("baseTypeName");
+ try {
+ gen.writeString(serialArray.getBaseTypeName());
+ } catch (SQLException e) {
+ gen.writeNull();
+ }
+ gen.writeFieldName("elements");
+ try {
+ Object[] elements = serialArray.getElements();
+ gen.writeStartArray();
+ if (elements != null) {
+ for (Object element : elements) {
+ gen.writeObject(element);
+ }
+ }
+ gen.writeEndArray();
+ } catch (Exception e) {
+ gen.writeNull();
+ }
+ }
+ }
+
+ /**
+ * the class of deserialize SerialArray type
+ */
+ private static class SerialArrayDeserializer extends
JsonDeserializer<SerialArray> {
+ @Override
+ public SerialArray deserialize(JsonParser p, DeserializationContext
ctxt) throws IOException {
+ try {
+ JsonNode node = p.getCodec().readTree(p);
+ SerialArray serialArray = new SerialArray();
+
+ if (node.has("baseType") && !node.get("baseType").isNull()) {
+ serialArray.setBaseType(node.get("baseType").asInt());
+ }
+
+ if (node.has("baseTypeName") &&
!node.get("baseTypeName").isNull()) {
+
serialArray.setBaseTypeName(node.get("baseTypeName").asText());
+ }
+
+ if (node.has("elements") && node.get("elements").isArray()) {
+ JsonNode elementsNode = node.get("elements");
+ Object[] elements = new Object[elementsNode.size()];
+ for (int i = 0; i < elementsNode.size(); i++) {
+ JsonNode elementNode = elementsNode.get(i);
+ if (elementNode.isNull()) {
+ elements[i] = null;
+ } else if (elementNode.isNumber()) {
+ elements[i] = elementNode.asLong();
+ } else if (elementNode.isTextual()) {
+ elements[i] = elementNode.asText();
+ } else {
+ elements[i] = elementNode;
+ }
+ }
+ serialArray.setElements(elements);
+ }
+
+ return serialArray;
+ } catch (Exception e) {
+ LOGGER.error("deserialize SerialArray error: {}",
e.getMessage(), e);
+ return null;
+ }
+ }
+ }
}
diff --git
a/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParserTest.java
b/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParserTest.java
index 37e804fc72..921f7024b0 100644
---
a/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParserTest.java
+++
b/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParserTest.java
@@ -19,6 +19,7 @@ package org.apache.seata.rm.datasource.undo.parser;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.seata.common.loader.EnhancedServiceLoader;
import org.apache.seata.rm.datasource.DataCompareUtils;
+import org.apache.seata.rm.datasource.sql.serial.SerialArray;
import org.apache.seata.rm.datasource.sql.struct.Field;
import org.apache.seata.rm.datasource.undo.AbstractUndoLogManager;
import org.apache.seata.rm.datasource.undo.BaseUndoLogParserTest;
@@ -33,14 +34,18 @@ import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
+import java.sql.Array;
import java.sql.JDBCType;
+import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
+import java.sql.Types;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
+import java.util.Map;
import static org.mockito.Mockito.mockStatic;
@@ -181,6 +186,182 @@ public class JacksonUndoLogParserTest extends
BaseUndoLogParserTest {
}
}
+ @Test
+ public void testSerializeAndDeserializeSerialArray()
+ throws NoSuchFieldException, IllegalAccessException, IOException,
SQLException {
+ // get the jackson mapper
+ java.lang.reflect.Field reflectField =
parser.getClass().getDeclaredField("mapper");
+ reflectField.setAccessible(true);
+ ObjectMapper mapper = (ObjectMapper) reflectField.get(parser);
+
+ // create a mock Array object for testing SerialArray
+ Array mockArray = new MockArray();
+ SerialArray serialArray = new SerialArray(mockArray);
+
+ // test SerialArray with BIGINT array (PostgreSQL _int8 type)
+ Field field = new Field("dept_ids",
JDBCType.ARRAY.getVendorTypeNumber(), serialArray);
+ byte[] bytes = mapper.writeValueAsBytes(field);
+ Field sameField = mapper.readValue(bytes, Field.class);
+ Assertions.assertTrue(DataCompareUtils.isFieldEquals(field,
sameField).getResult());
+
+ // verify the SerialArray properties are correctly
serialized/deserialized
+ SerialArray deserializedArray = (SerialArray) sameField.getValue();
+ Assertions.assertEquals(serialArray.getBaseType(),
deserializedArray.getBaseType());
+ Assertions.assertEquals(serialArray.getBaseTypeName(),
deserializedArray.getBaseTypeName());
+ Assertions.assertArrayEquals(serialArray.getElements(),
deserializedArray.getElements());
+ }
+
+ @Test
+ public void testSerializeAndDeserializeSerialArrayWithNulls()
+ throws NoSuchFieldException, IllegalAccessException, IOException,
SQLException {
+ // get the jackson mapper
+ java.lang.reflect.Field reflectField =
parser.getClass().getDeclaredField("mapper");
+ reflectField.setAccessible(true);
+ ObjectMapper mapper = (ObjectMapper) reflectField.get(parser);
+
+ // create SerialArray with null elements
+ Array mockArrayWithNulls = new MockArrayWithNulls();
+ SerialArray serialArray = new SerialArray(mockArrayWithNulls);
+
+ Field field = new Field("nullable_array",
JDBCType.ARRAY.getVendorTypeNumber(), serialArray);
+ byte[] bytes = mapper.writeValueAsBytes(field);
+ Field sameField = mapper.readValue(bytes, Field.class);
+
+ Assertions.assertTrue(DataCompareUtils.isFieldEquals(field,
sameField).getResult());
+
+ // verify null elements are handled correctly
+ SerialArray deserializedArray = (SerialArray) sameField.getValue();
+ Object[] elements = deserializedArray.getElements();
+ Assertions.assertEquals(3, elements.length);
+ Assertions.assertEquals(1L, elements[0]);
+ Assertions.assertNull(elements[1]);
+ Assertions.assertEquals(3L, elements[2]);
+ }
+
+ /**
+ * Mock Array class for testing SerialArray serialization
+ */
+ private static class MockArray implements Array {
+ private final Object[] elements = {1L, 2L, 3L, 4L, 5L};
+
+ @Override
+ public String getBaseTypeName() throws SQLException {
+ return "int8";
+ }
+
+ @Override
+ public int getBaseType() throws SQLException {
+ return Types.BIGINT;
+ }
+
+ @Override
+ public Object getArray() throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(Map<String, Class<?>> map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count, Map<String, Class<?>>
map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public ResultSet getResultSet() throws SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(Map<String, Class<?>> map) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count, Map<String,
Class<?>> map) throws SQLException {
+ return null;
+ }
+
+ @Override
+ public void free() throws SQLException {
+ // do nothing
+ }
+ }
+
+ /**
+ * Mock Array class with null elements for testing edge cases
+ */
+ private static class MockArrayWithNulls implements Array {
+ private final Object[] elements = {1L, null, 3L};
+
+ @Override
+ public String getBaseTypeName() throws SQLException {
+ return "int8";
+ }
+
+ @Override
+ public int getBaseType() throws SQLException {
+ return Types.BIGINT;
+ }
+
+ @Override
+ public Object getArray() throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(Map<String, Class<?>> map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count, Map<String, Class<?>>
map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public ResultSet getResultSet() throws SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(Map<String, Class<?>> map) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count, Map<String,
Class<?>> map) throws SQLException {
+ return null;
+ }
+
+ @Override
+ public void free() throws SQLException {
+ // do nothing
+ }
+ }
+
@Override
public UndoLogParser getParser() {
return parser;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]