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)
+        );
+    }
+}