IGNITE-21963: fix incorrect serialization for some BigDecimal values (#3599)
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
index 85b15d0..f100e97 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
@@ -129,7 +129,7 @@
* Calculates byte size for BigDecimal value.
*/
public static int sizeInBytes(BigDecimal val) {
- return sizeInBytes(val.unscaledValue());
+ return sizeInBytes(val.unscaledValue()) + Short.BYTES /* Size of scale */;
}
/**
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
index ed12a78..1e9ba96 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
@@ -55,6 +55,8 @@
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -97,9 +99,12 @@
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DynamicNode;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* KvMarshaller test.
@@ -108,6 +113,7 @@
private static final Column[] SINGLE_INT64_ID_COLUMNS = {
new Column("id", INT64, false)
};
+ private static final Logger log = LoggerFactory.getLogger(KvMarshallerTest.class);
/**
* Return list of marshaller factories for test.
@@ -706,6 +712,46 @@
}
}
+ @Test
+ public void testVariableLengthBigDecimalAndBytes() throws MarshallerException {
+ List<Map.Entry<Integer, Integer>> args = new ArrayList<>();
+ // Breaks marshalling if big decimal size does not include 2 additional bytes
+ // used by length
+ args.add(Map.entry(6, 251));
+
+ for (int i = 0; i < 100; i++) {
+ args.add(Map.entry(rnd.nextInt(8) + 1, rnd.nextInt(512) + 1));
+ }
+
+ for (Map.Entry<Integer, Integer> arg : args) {
+ int keyLength = arg.getKey();
+ int valLength = arg.getValue();
+
+ StringBuilder sb = new StringBuilder();
+ for (int j = 0; j < keyLength; j++) {
+ sb.append(rnd.nextInt(10));
+ }
+
+ BigDecimal key = new BigDecimal(sb.toString());
+ byte[] val = new byte[valLength];
+
+ log.info("key: {}, val: {} (length)", key, valLength);
+
+ Column[] keyCols = {new Column("KEY", NativeTypes.decimalOf(12, 3), false)};
+ Column[] valCols = {new Column("VAL", BYTES, false)};
+
+ SchemaDescriptor schema = new SchemaDescriptor(1, keyCols, valCols);
+
+ ReflectionMarshallerFactory factory = new ReflectionMarshallerFactory();
+ KvMarshaller<BigDecimal, byte[]> marshaller = factory.create(schema,
+ Mapper.of(BigDecimal.class, "KEY"), Mapper.of(byte[].class, "VAL"));
+
+ Row row = marshaller.marshal(key, val);
+ assertEquals(key, row.decimalValue(0).setScale(0, RoundingMode.UNNECESSARY));
+ assertArrayEquals(row.bytesValue(1), val);
+ }
+ }
+
/**
* Generate random key-value pair of given types and check serialization and deserialization works fine.
*
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtilTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtilTest.java
new file mode 100644
index 0000000..9f6f9ee
--- /dev/null
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtilTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.schema.marshaller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.binarytuple.BinaryTupleCommon;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.type.NativeType;
+import org.apache.ignite.internal.type.NativeTypes;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@link MarshallerUtil}.
+ */
+public class MarshallerUtilTest extends BaseIgniteAbstractTest {
+
+ @ParameterizedTest
+ @MethodSource("getValueSizes")
+ public void testGetValueSize(Object val, NativeType nativeType, int expectedSize) {
+ int valueSize = MarshallerUtil.getValueSize(val, nativeType);
+ assertEquals(expectedSize, valueSize);
+ }
+
+ private static Stream<Arguments> getValueSizes() {
+ return Stream.of(
+ // bytes
+ Arguments.of(new byte[0], NativeTypes.BYTES, 1),
+ Arguments.of(new byte[1], NativeTypes.BYTES, 1),
+ Arguments.of(new byte[]{BinaryTupleCommon.VARLEN_EMPTY_BYTE}, NativeTypes.BYTES, 2),
+ Arguments.of(new byte[]{BinaryTupleCommon.VARLEN_EMPTY_BYTE, 1}, NativeTypes.BYTES, 3),
+ // pojo
+ Arguments.of(new Object(), NativeTypes.BYTES, 0),
+ // string
+ Arguments.of("", NativeTypes.STRING, 1),
+ Arguments.of("1", NativeTypes.STRING, 1),
+ Arguments.of("abc", NativeTypes.STRING, 3),
+ Arguments.of(new String(new byte[]{-36, -128}, StandardCharsets.UTF_8), NativeTypes.STRING, 2),
+ Arguments.of(new String(new byte[]{-30, -104, -128}, StandardCharsets.UTF_8), NativeTypes.STRING, 3),
+ Arguments.of(new String(new byte[]{97, -36, -128, -30, -104, -128}, StandardCharsets.UTF_8), NativeTypes.STRING, 6),
+ // number
+ Arguments.of(BigInteger.ONE, NativeTypes.numberOf(12), 1),
+ Arguments.of(BigInteger.valueOf(123456789), NativeTypes.numberOf(12), 4),
+ // decimal
+ Arguments.of(BigDecimal.ONE, NativeTypes.decimalOf(12, 1), 3),
+ Arguments.of(BigDecimal.valueOf(123456789), NativeTypes.decimalOf(12, 3), 6)
+ );
+ }
+}