This is an automated email from the ASF dual-hosted git repository.
snuyanzin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new 9b8faccb884 [FLINK-41056][table] Keep non-compact decimals with
unscaled value fitting in a long as long to avoid BigDecimal on read, add,
subtract, cast and deserialize
9b8faccb884 is described below
commit 9b8faccb8842a6be4f84e3f71da7c82b0c499e00
Author: Sergey Nuyanzin <[email protected]>
AuthorDate: Sat Oct 24 21:36:25 2026 +0200
[FLINK-41056][table] Keep non-compact decimals with unscaled value fitting
in a long as long to avoid BigDecimal on read, add, subtract, cast and
deserialize
---
.../org/apache/flink/table/data/DecimalData.java | 43 +++++--
.../flink/table/data/DecimalDataInternalUtils.java | 42 +++++++
.../table/data/binary/BinarySegmentUtils.java | 22 ++++
.../apache/flink/table/data/DecimalDataUtils.java | 80 ++++++++----
.../flink/table/data/BinaryArrayDataTest.java | 32 +++++
.../apache/flink/table/data/BinaryRowDataTest.java | 36 ++++++
.../apache/flink/table/data/DecimalDataTest.java | 138 ++++++++++++++++++++-
.../apache/flink/table/data/NestedRowDataTest.java | 36 ++++++
.../flink/table/data/util/DataFormatTestUtil.java | 36 ++++++
.../runtime/typeutils/DecimalDataSerializer.java | 10 ++
.../runtime/typeutils/DecimalSerializerTest.java | 112 ++++++++++++++---
11 files changed, 531 insertions(+), 56 deletions(-)
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/DecimalData.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/DecimalData.java
index 5f4e4941163..7ba1c1b6ab1 100644
---
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/DecimalData.java
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/DecimalData.java
@@ -61,27 +61,33 @@ public final class DecimalData implements
Comparable<DecimalData> {
// The semantics of the fields are as follows:
// - `precision` and `scale` represent the precision and scale of SQL
decimal type
- // - If `decimalVal` is set, it represents the whole decimal value
- // - Otherwise, the decimal value is longVal/(10^scale).
+ // - if `longBacked`, the decimal value is longVal/(10^scale);
+ // `decimalVal` may be set and cached
+ // - otherwise (only if precision > MAX_COMPACT_PRECISION),
+ // `decimalVal` represents the value and `longVal` is undefined
//
// Note that the (precision, scale) must be correct.
- // if precision > MAX_COMPACT_PRECISION,
- // `decimalVal` represents the value. `longVal` is undefined
- // otherwise, (longVal, scale) represents the value
- // `decimalVal` may be set and cached
final int precision;
final int scale;
final long longVal;
+ final boolean longBacked;
BigDecimal decimalVal;
// this constructor does not perform any sanity check.
+ // non-compact: long-backed iff `decimalVal` is null
DecimalData(int precision, int scale, long longVal, BigDecimal decimalVal)
{
+ this(precision, scale, longVal, decimalVal, isCompact(precision) ||
decimalVal == null);
+ }
+
+ private DecimalData(
+ int precision, int scale, long longVal, BigDecimal decimalVal,
boolean longBacked) {
this.precision = precision;
this.scale = scale;
this.longVal = longVal;
this.decimalVal = decimalVal;
+ this.longBacked = longBacked;
}
//
------------------------------------------------------------------------------------------
@@ -117,7 +123,7 @@ public final class DecimalData implements
Comparable<DecimalData> {
* @throws ArithmeticException if this {@link DecimalData} does not
exactly fit in a long.
*/
public long toUnscaledLong() {
- if (isCompact()) {
+ if (longBacked) {
return longVal;
} else {
return toBigDecimal().unscaledValue().longValueExact();
@@ -130,27 +136,42 @@ public final class DecimalData implements
Comparable<DecimalData> {
* @return the unscaled byte array of this {@link DecimalData}.
*/
public byte[] toUnscaledBytes() {
+ if (longBacked) {
+ // same bytes as BigInteger#toByteArray: minimal big-endian two's
complement
+ final int length = (64 - Long.numberOfLeadingZeros(longVal ^
(longVal >> 63))) / 8 + 1;
+ final byte[] bytes = new byte[length];
+ for (int i = 0; i < length; i++) {
+ bytes[length - 1 - i] = (byte) (longVal >> (8 * i));
+ }
+ return bytes;
+ }
return toBigDecimal().unscaledValue().toByteArray();
}
- /** Returns whether the decimal value is small enough to be stored in a
long. */
+ /** Returns whether the precision is at most 18. */
public boolean isCompact() {
return precision <= MAX_COMPACT_PRECISION;
}
/** Returns a copy of this {@link DecimalData} object. */
public DecimalData copy() {
- return new DecimalData(precision, scale, longVal, decimalVal);
+ return new DecimalData(precision, scale, longVal, decimalVal,
longBacked);
}
@Override
public int hashCode() {
+ if (longBacked && longVal != Long.MIN_VALUE) {
+ // BigDecimal#hashCode without allocating; must match, hashes may
feed partitioning
+ final long abs = Math.abs(longVal);
+ final int h = (int) ((int) (abs >>> 32) * 31 + (abs &
0xFFFFFFFFL));
+ return 31 * (longVal < 0 ? -h : h) + scale;
+ }
return toBigDecimal().hashCode();
}
@Override
public int compareTo(@Nonnull DecimalData that) {
- if (this.isCompact() && that.isCompact() && this.scale == that.scale) {
+ if (this.longBacked && that.longBacked && this.scale == that.scale) {
return Long.compare(this.longVal, that.longVal);
}
return this.toBigDecimal().compareTo(that.toBigDecimal());
@@ -197,7 +218,7 @@ public final class DecimalData implements
Comparable<DecimalData> {
/**
* Rescales a compact unscaled value to the given {@code precision}/{@code
scale} with HALF_UP
* in long arithmetic (no {@link BigDecimal}). Returns {@code null} on
precision overflow, like
- * {@link #fromBigDecimal}. Callers must ensure the source and target are
compact.
+ * {@link #fromBigDecimal}. Requires source scale and target precision at
most 18.
*/
static @Nullable DecimalData fromCompactRescale(
long unscaled, int fromScale, int precision, int scale) {
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/DecimalDataInternalUtils.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/DecimalDataInternalUtils.java
new file mode 100644
index 00000000000..67ee8ecf442
--- /dev/null
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/DecimalDataInternalUtils.java
@@ -0,0 +1,42 @@
+/*
+ * 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.flink.table.data;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.table.types.logical.DecimalType;
+
+import java.math.BigDecimal;
+
+import static org.apache.flink.util.Preconditions.checkArgument;
+
+/** Internal factories for {@link DecimalData}. */
+@Internal
+public final class DecimalDataInternalUtils {
+
+ private DecimalDataInternalUtils() {}
+
+ /**
+ * Like {@link DecimalData#fromUnscaledLong}, but for a non-compact
precision, keeping the value
+ * as a long instead of a {@link BigDecimal}. Any long fits a non-compact
precision.
+ */
+ public static DecimalData fromUnscaledLong(long unscaledLong, int
precision, int scale) {
+ checkArgument(!DecimalData.isCompact(precision) && precision <=
DecimalType.MAX_PRECISION);
+ return new DecimalData(precision, scale, unscaledLong, null);
+ }
+}
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/binary/BinarySegmentUtils.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/binary/BinarySegmentUtils.java
index 653fc559df9..819f5ceab6e 100644
---
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/binary/BinarySegmentUtils.java
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/binary/BinarySegmentUtils.java
@@ -24,6 +24,7 @@ import org.apache.flink.core.memory.DataOutputView;
import org.apache.flink.core.memory.MemorySegment;
import org.apache.flink.table.data.ArrayData;
import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.DecimalDataInternalUtils;
import org.apache.flink.table.data.MapData;
import org.apache.flink.table.data.RawValueData;
import org.apache.flink.table.data.RowData;
@@ -1005,11 +1006,32 @@ public final class BinarySegmentUtils {
int scale) {
final int size = ((int) offsetAndSize);
int subOffset = (int) (offsetAndSize >> 32);
+ // up to 8 bytes fit a long; size 0 keeps the old path
+ if (size > 0 && size <= 8) {
+ final int pos = baseOffset + subOffset;
+ final long unscaled =
+ inFirstSegment(segments, pos, 8)
+ // value is in the high bytes, the rest is shifted
out
+ ? segments[0].getLongBigEndian(pos) >> (64 - 8 *
size)
+ : readUnscaledLongMultiSegments(segments, pos,
size);
+ // at most 19 digits, so it fits any non-compact precision
+ return DecimalDataInternalUtils.fromUnscaledLong(unscaled,
precision, scale);
+ }
byte[] bytes = new byte[size];
copyToBytes(segments, baseOffset + subOffset, bytes, 0, size);
return DecimalData.fromUnscaledBytes(bytes, precision, scale);
}
+ private static long readUnscaledLongMultiSegments(
+ MemorySegment[] segments, int offset, int size) {
+ // signed first byte sign-extends
+ long unscaled = getByte(segments, offset);
+ for (int i = 1; i < size; i++) {
+ unscaled = (unscaled << 8) | (getByte(segments, offset + i) &
0xFF);
+ }
+ return unscaled;
+ }
+
/**
* Gets an instance of {@link TimestampData} from underlying {@link
MemorySegment}.
*
diff --git
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/DecimalDataUtils.java
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/DecimalDataUtils.java
index 55608af3b81..9c9d6d09dbd 100644
---
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/DecimalDataUtils.java
+++
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/DecimalDataUtils.java
@@ -20,6 +20,8 @@ package org.apache.flink.table.data;
import org.apache.flink.table.types.logical.DecimalType;
+import javax.annotation.Nullable;
+
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
@@ -46,7 +48,7 @@ public final class DecimalDataUtils {
if (decimal.isCompact()) {
return ((double) decimal.longVal) / POW10[decimal.scale];
} else {
- return decimal.decimalVal.doubleValue();
+ return decimal.toBigDecimal().doubleValue();
}
}
@@ -57,35 +59,35 @@ public final class DecimalDataUtils {
* @return the signum function of this decimal.
*/
public static int signum(DecimalData decimal) {
- if (decimal.isCompact()) {
- return Long.signum(decimal.toUnscaledLong());
+ if (decimal.longBacked) {
+ return Long.signum(decimal.longVal);
} else {
return decimal.toBigDecimal().signum();
}
}
public static DecimalData negate(DecimalData decimal) {
- if (decimal.isCompact()) {
+ if (decimal.longBacked && decimal.longVal != Long.MIN_VALUE) {
return new DecimalData(decimal.precision, decimal.scale,
-decimal.longVal, null);
} else {
return new DecimalData(
- decimal.precision, decimal.scale, -1,
decimal.decimalVal.negate());
+ decimal.precision, decimal.scale, -1,
decimal.toBigDecimal().negate());
}
}
public static DecimalData abs(DecimalData decimal) {
- if (decimal.isCompact()) {
+ if (decimal.longBacked && decimal.longVal != Long.MIN_VALUE) {
if (decimal.longVal >= 0) {
return decimal;
} else {
return new DecimalData(decimal.precision, decimal.scale,
-decimal.longVal, null);
}
} else {
- if (decimal.decimalVal.signum() >= 0) {
+ final BigDecimal bd = decimal.toBigDecimal();
+ if (bd.signum() >= 0) {
return decimal;
} else {
- return new DecimalData(
- decimal.precision, decimal.scale, -1,
decimal.decimalVal.negate());
+ return new DecimalData(decimal.precision, decimal.scale, -1,
bd.negate());
}
}
}
@@ -104,11 +106,7 @@ public final class DecimalDataUtils {
}
public static DecimalData add(DecimalData v1, DecimalData v2, int
precision, int scale) {
- if (v1.isCompact()
- && v2.isCompact()
- && v1.scale == v2.scale
- && DecimalData.isCompact(precision)) {
- assert scale == v1.scale; // no need to rescale
+ if (canAddOrSubtractAsLong(v1, v2, precision, scale)) {
try {
long ls = Math.addExact(v1.longVal, v2.longVal); // checks
overflow
return new DecimalData(precision, scale, ls, null);
@@ -121,11 +119,7 @@ public final class DecimalDataUtils {
}
public static DecimalData subtract(DecimalData v1, DecimalData v2, int
precision, int scale) {
- if (v1.isCompact()
- && v2.isCompact()
- && v1.scale == v2.scale
- && DecimalData.isCompact(precision)) {
- assert scale == v1.scale; // no need to rescale
+ if (canAddOrSubtractAsLong(v1, v2, precision, scale)) {
try {
long ls = Math.subtractExact(v1.longVal, v2.longVal); //
checks overflow
return new DecimalData(precision, scale, ls, null);
@@ -137,6 +131,15 @@ public final class DecimalDataUtils {
return fromBigDecimal(bd, precision, scale);
}
+ private static boolean canAddOrSubtractAsLong(
+ DecimalData v1, DecimalData v2, int precision, int scale) {
+ return (DecimalData.isCompact(precision)
+ ? v1.isCompact() && v2.isCompact()
+ : v1.longBacked && v2.longBacked)
+ && v1.scale == scale
+ && v2.scale == scale;
+ }
+
public static DecimalData multiply(DecimalData v1, DecimalData v2, int
precision, int scale) {
BigDecimal bd = v1.toBigDecimal().multiply(v2.toBigDecimal());
return fromBigDecimal(bd, precision, scale);
@@ -180,12 +183,37 @@ public final class DecimalDataUtils {
}
public static DecimalData castToDecimal(DecimalData dec, int precision,
int scale) {
- if (dec.isCompact() && precision <= DecimalData.MAX_COMPACT_PRECISION)
{
- return DecimalData.fromCompactRescale(dec.longVal, dec.scale,
precision, scale);
+ if (dec.longBacked) {
+ if (DecimalData.isCompact(precision)) {
+ if (dec.scale < POW10.length) {
+ return DecimalData.fromCompactRescale(dec.longVal,
dec.scale, precision, scale);
+ }
+ } else {
+ final DecimalData scaled =
+ rescaleUpAsLong(dec.longVal, dec.scale, precision,
scale);
+ if (scaled != null) {
+ return scaled;
+ }
+ }
}
return fromBigDecimal(dec.toBigDecimal(), precision, scale);
}
+ // scale must not decrease; null if the result overflows a long
+ private static @Nullable DecimalData rescaleUpAsLong(
+ long unscaled, int fromScale, int precision, int scale) {
+ final int scaleDiff = scale - fromScale;
+ if (scaleDiff < 0 || scaleDiff >= POW10.length) {
+ return null;
+ }
+ try {
+ return new DecimalData(
+ precision, scale, Math.multiplyExact(unscaled,
POW10[scaleDiff]), null);
+ } catch (ArithmeticException e) {
+ return null;
+ }
+ }
+
public static DecimalData castFrom(DecimalData dec, int precision, int
scale) {
return castToDecimal(dec, precision, scale);
}
@@ -199,9 +227,13 @@ public final class DecimalDataUtils {
}
public static DecimalData castFrom(long val, int p, int s) {
- if (p <= DecimalData.MAX_COMPACT_PRECISION) {
+ if (DecimalData.isCompact(p)) {
return DecimalData.fromCompactRescale(val, 0, p, s);
}
+ final DecimalData scaled = rescaleUpAsLong(val, 0, p, s);
+ if (scaled != null) {
+ return scaled;
+ }
return fromBigDecimal(BigDecimal.valueOf(val), p, s);
}
@@ -225,8 +257,8 @@ public final class DecimalDataUtils {
}
public static int compare(DecimalData b1, long n2) {
- if (!b1.isCompact()) {
- return b1.decimalVal.compareTo(BigDecimal.valueOf(n2));
+ if (!b1.longBacked || b1.scale >= POW10.length) {
+ return b1.toBigDecimal().compareTo(BigDecimal.valueOf(n2));
}
if (b1.scale == 0) {
return Long.compare(b1.longVal, n2);
diff --git
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryArrayDataTest.java
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryArrayDataTest.java
index cb132cea5b5..838aca87bc8 100644
---
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryArrayDataTest.java
+++
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryArrayDataTest.java
@@ -45,6 +45,8 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import static org.apache.flink.table.data.StringData.fromString;
+import static
org.apache.flink.table.data.util.DataFormatTestUtil.nonCompactBoundaryDecimals;
+import static org.apache.flink.table.data.util.DataFormatTestUtil.toSegments;
import static org.apache.flink.table.utils.RawValueDataAsserter.equivalent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.HamcrestCondition.matching;
@@ -523,6 +525,36 @@ class BinaryArrayDataTest {
array.setDecimal(0, decimal2, precision);
assertThat(array.getDecimal(0, precision,
scale).toString()).isEqualTo("6.55000");
}
+
+ // 3.not compact, values around the 8-byte boundary, non-zero offset
and across segments
+ {
+ int precision = 38;
+ int scale = 2;
+ DecimalData[] decimals = nonCompactBoundaryDecimals(precision,
scale);
+ BinaryArrayData nonCompactArray = new BinaryArrayData();
+ BinaryArrayWriter nonCompactWriter =
+ new BinaryArrayWriter(nonCompactArray, decimals.length, 8);
+ for (int i = 0; i < decimals.length; i++) {
+ nonCompactWriter.writeDecimal(i, decimals[i], precision);
+ }
+ nonCompactWriter.complete();
+
+ int size = nonCompactArray.getSizeInBytes();
+ byte[] bytes =
+ BinarySegmentUtils.copyToBytes(
+ nonCompactArray.getSegments(),
nonCompactArray.getOffset(), size);
+ int baseOffset = 3;
+ for (int segmentSize = 1; segmentSize <= baseOffset + size;
segmentSize++) {
+ BinaryArrayData splitArray = new BinaryArrayData();
+ splitArray.pointTo(toSegments(bytes, baseOffset, segmentSize),
baseOffset, size);
+ for (int i = 0; i < decimals.length; i++) {
+ DecimalData decimal = splitArray.getDecimal(i, precision,
scale);
+
assertThat(decimal.toBigDecimal()).isEqualTo(decimals[i].toBigDecimal());
+ assertThat(decimal.longBacked)
+ .isEqualTo(decimals[i].toUnscaledBytes().length <=
8);
+ }
+ }
+ }
}
@Test
diff --git
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryRowDataTest.java
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryRowDataTest.java
index 18b756f9ea9..b9b74029c7a 100644
---
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryRowDataTest.java
+++
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/BinaryRowDataTest.java
@@ -37,6 +37,7 @@ import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.data.binary.BinaryArrayData;
import org.apache.flink.table.data.binary.BinaryMapData;
import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.binary.BinarySegmentUtils;
import org.apache.flink.table.data.binary.BinaryStringData;
import org.apache.flink.table.data.util.DataFormatTestUtil;
import org.apache.flink.table.data.writer.BinaryArrayWriter;
@@ -81,6 +82,8 @@ import java.util.Set;
import static org.apache.flink.table.data.StringData.fromBytes;
import static org.apache.flink.table.data.StringData.fromString;
import static org.apache.flink.table.data.util.DataFormatTestUtil.MyObj;
+import static
org.apache.flink.table.data.util.DataFormatTestUtil.nonCompactBoundaryDecimals;
+import static org.apache.flink.table.data.util.DataFormatTestUtil.toSegments;
import static org.apache.flink.table.data.util.MapDataUtil.convertToJavaMap;
import static org.apache.flink.table.utils.RawValueDataAsserter.equivalent;
import static org.assertj.core.api.Assertions.assertThat;
@@ -453,6 +456,39 @@ class BinaryRowDataTest {
row.setDecimal(0, decimal2, precision);
assertThat(row.getDecimal(0, precision,
scale).toString()).isEqualTo("6.55000");
}
+
+ // 3.not compact, values around the 8-byte boundary
+ {
+ int precision = 38;
+ int scale = 2;
+ for (DecimalData decimal : nonCompactBoundaryDecimals(precision,
scale)) {
+ BinaryRowData row = new BinaryRowData(1);
+ BinaryRowWriter writer = new BinaryRowWriter(row);
+ writer.writeDecimal(0, decimal, precision);
+ writer.complete();
+
+ assertThat(row.getDecimal(0, precision, scale).toBigDecimal())
+ .isEqualTo(decimal.toBigDecimal());
+ assertThat(row.getDecimal(0, precision, scale).longBacked)
+ .isEqualTo(decimal.toUnscaledBytes().length <= 8);
+ row.setDecimal(0, row.getDecimal(0, precision, scale),
precision);
+ assertThat(row.getDecimal(0, precision, scale).toBigDecimal())
+ .isEqualTo(decimal.toBigDecimal());
+
+ // split the row so that the decimal bytes cross a segment
boundary
+ int fixedSize = row.getFixedLengthPartSize();
+ byte[] bytes =
+ BinarySegmentUtils.copyToBytes(row.getSegments(), 0,
row.getSizeInBytes());
+ for (int split = 1; split < 8; split++) {
+ BinaryRowData splitRow = new BinaryRowData(1);
+ splitRow.pointTo(
+ toSegments(bytes, 0, fixedSize + split), 0,
row.getSizeInBytes());
+
+ assertThat(splitRow.getDecimal(0, precision,
scale).toBigDecimal())
+ .isEqualTo(decimal.toBigDecimal());
+ }
+ }
+ }
}
@Test
diff --git
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/DecimalDataTest.java
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/DecimalDataTest.java
index f64c0e5108e..4f6e444856f 100644
---
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/DecimalDataTest.java
+++
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/DecimalDataTest.java
@@ -90,7 +90,7 @@ class DecimalDataTest {
DecimalData.fromBigDecimal(BigDecimal.valueOf(val),
targetPrecision, targetScale);
final DecimalData actual = castFrom(val, targetPrecision, targetScale);
- assertCastMatches(actual, expected);
+ assertDecimalMatches(actual, expected);
}
@ParameterizedTest(name = "unscaled={0} DECIMAL({1},{2}) ->
DECIMAL({3},{4})")
@@ -102,10 +102,142 @@ class DecimalDataTest {
DecimalData.fromBigDecimal(src.toBigDecimal(),
targetPrecision, targetScale);
final DecimalData actual = castToDecimal(src, targetPrecision,
targetScale);
- assertCastMatches(actual, expected);
+ assertDecimalMatches(actual, expected);
}
- private static void assertCastMatches(DecimalData actual, DecimalData
expected) {
+ private static Stream<Arguments> unscaledLongCases() {
+ return Stream.of(
+ arguments(0L),
+ arguments(1L),
+ arguments(-1L),
+ arguments(127L),
+ arguments(128L),
+ arguments(-128L),
+ arguments(-129L),
+ arguments(1234L),
+ arguments(32767L),
+ arguments(32768L),
+ arguments(-32768L),
+ arguments(-32769L),
+ arguments((long) Integer.MAX_VALUE),
+ arguments((long) Integer.MAX_VALUE + 1),
+ arguments((long) Integer.MIN_VALUE),
+ arguments((long) Integer.MIN_VALUE - 1),
+ arguments((1L << 55) - 1),
+ arguments(1L << 55),
+ arguments(-(1L << 55)),
+ arguments(-(1L << 55) - 1),
+ arguments(Long.MAX_VALUE),
+ arguments(Long.MIN_VALUE));
+ }
+
+ @ParameterizedTest(name = "unscaled={0}")
+ @MethodSource("unscaledLongCases")
+ void testNonCompactLongBackedMatchesBigDecimal(long unscaled) {
+ final DecimalData longBacked =
DecimalDataInternalUtils.fromUnscaledLong(unscaled, 38, 2);
+ final DecimalData bigBacked =
+ DecimalData.fromBigDecimal(BigDecimal.valueOf(unscaled, 2),
38, 2);
+
+ assertThat(longBacked.longBacked).isTrue();
+
assertThat(longBacked.toUnscaledBytes()).isEqualTo(bigBacked.toUnscaledBytes());
+ assertThat(longBacked.toUnscaledLong()).isEqualTo(unscaled);
+ assertThat(longBacked.compareTo(bigBacked)).isZero();
+
assertThat(longBacked).isEqualTo(bigBacked).hasSameHashCodeAs(bigBacked);
+
+ // caching the BigDecimal must keep the long fast paths, also for
copies
+ longBacked.toBigDecimal();
+ assertThat(longBacked.longBacked).isTrue();
+ assertThat(longBacked.copy().longBacked).isTrue();
+
+ assertThat(doubleValue(longBacked))
+ .isEqualTo(BigDecimal.valueOf(unscaled, 2).doubleValue());
+ assertThat(signum(longBacked)).isEqualTo(Long.signum(unscaled));
+ assertThat(compare(longBacked, unscaled / 100))
+
.isEqualTo(bigBacked.toBigDecimal().compareTo(BigDecimal.valueOf(unscaled /
100)));
+
+ final DecimalData otherScale =
DecimalDataInternalUtils.fromUnscaledLong(unscaled, 38, 3);
+ assertThat(Integer.signum(longBacked.compareTo(otherScale)))
+ .isEqualTo(
+ Integer.signum(
+
bigBacked.toBigDecimal().compareTo(otherScale.toBigDecimal())));
+
+ assertDecimalMatches(negate(longBacked), negate(bigBacked));
+ assertThat(negate(longBacked).longBacked).isEqualTo(unscaled !=
Long.MIN_VALUE);
+ assertDecimalMatches(abs(longBacked), abs(bigBacked));
+
+ final DecimalData one = DecimalDataInternalUtils.fromUnscaledLong(1,
38, 2);
+ assertDecimalMatches(add(longBacked, one, 38, 2), add(bigBacked, one,
38, 2));
+ assertThat(add(longBacked, one, 38, 2).longBacked).isEqualTo(unscaled
!= Long.MAX_VALUE);
+ assertDecimalMatches(subtract(longBacked, one, 38, 2),
subtract(bigBacked, one, 38, 2));
+ assertThat(subtract(longBacked, one, 38, 2).longBacked)
+ .isEqualTo(unscaled != Long.MIN_VALUE);
+ assertDecimalMatches(
+ add(longBacked, longBacked, 38, 2),
+ DecimalData.fromBigDecimal(
+
bigBacked.toBigDecimal().add(bigBacked.toBigDecimal()), 38, 2));
+ assertDecimalMatches(
+ subtract(longBacked, negate(longBacked), 38, 2),
+ DecimalData.fromBigDecimal(
+
bigBacked.toBigDecimal().subtract(bigBacked.toBigDecimal().negate()),
+ 38,
+ 2));
+ }
+
+ @ParameterizedTest(name = "unscaled={0}")
+ @MethodSource("unscaledLongCases")
+ void testCompactHashCodeMatchesBigDecimal(long unscaled) {
+ final long compact = unscaled % 1_000_000_000_000_000L;
+ final BigDecimal bd = BigDecimal.valueOf(compact, 2);
+
+ assertThat(DecimalData.fromUnscaledLong(compact, 18, 2).hashCode())
+ .isEqualTo(bd.hashCode());
+ assertThat(DecimalData.fromBigDecimal(bd, 18,
2).hashCode()).isEqualTo(bd.hashCode());
+ }
+
+ private static Stream<Arguments> unscaledLongWithTargetCases() {
+ final int[][] targets = {
+ {38, 2}, {38, 4}, {38, 20}, {38, 1}, {19, 0}, {20, 2}, {18, 2},
{18, 0}, {10, 1}
+ };
+ return unscaledLongCases()
+ .flatMap(
+ value ->
+ Stream.of(targets).map(t ->
arguments(value.get()[0], t[0], t[1])));
+ }
+
+ @ParameterizedTest(name = "unscaled={0} -> DECIMAL({1},{2})")
+ @MethodSource("unscaledLongWithTargetCases")
+ void testCastLongBackedMatchesBigDecimal(long unscaled, int precision, int
scale) {
+ final DecimalData longBacked =
DecimalDataInternalUtils.fromUnscaledLong(unscaled, 38, 2);
+ final DecimalData bigBacked =
+ DecimalData.fromBigDecimal(BigDecimal.valueOf(unscaled, 2),
38, 2);
+
+ final DecimalData actual = castToDecimal(longBacked, precision, scale);
+ assertDecimalMatches(actual, castToDecimal(bigBacked, precision,
scale));
+ if (actual != null && scale >= 2 && !DecimalData.isCompact(precision))
{
+ assertThat(actual.longBacked)
+
.isEqualTo(actual.toBigDecimal().unscaledValue().bitLength() < 64);
+ }
+ }
+
+ @ParameterizedTest(name = "unscaled={0} -> DECIMAL({1},{2})")
+ @MethodSource("unscaledLongWithTargetCases")
+ void testCastCompactMatchesBigDecimal(long unscaled, int precision, int
scale) {
+ final DecimalData compact = DecimalData.fromUnscaledLong(unscaled %
10_000_000_000L, 10, 2);
+
+ assertDecimalMatches(
+ castToDecimal(compact, precision, scale),
+ DecimalData.fromBigDecimal(compact.toBigDecimal(), precision,
scale));
+ }
+
+ @ParameterizedTest(name = "{0} -> DECIMAL({1},{2})")
+ @MethodSource("unscaledLongWithTargetCases")
+ void testCastFromLongMatchesBigDecimal(long val, int precision, int scale)
{
+ assertDecimalMatches(
+ castFrom(val, precision, scale),
+ DecimalData.fromBigDecimal(BigDecimal.valueOf(val), precision,
scale));
+ }
+
+ private static void assertDecimalMatches(DecimalData actual, DecimalData
expected) {
if (expected == null) {
assertThat(actual).isNull();
} else {
diff --git
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/NestedRowDataTest.java
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/NestedRowDataTest.java
index 34a98ccd843..1b1e7c93d86 100644
---
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/NestedRowDataTest.java
+++
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/NestedRowDataTest.java
@@ -26,17 +26,23 @@ import org.apache.flink.api.java.typeutils.GenericTypeInfo;
import org.apache.flink.core.memory.MemorySegment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.binary.BinarySegmentUtils;
import org.apache.flink.table.data.binary.NestedRowData;
import org.apache.flink.table.data.writer.BinaryRowWriter;
import org.apache.flink.table.runtime.typeutils.RawValueDataSerializer;
import org.apache.flink.table.runtime.typeutils.RowDataSerializer;
import org.apache.flink.table.runtime.typeutils.StringDataSerializer;
+import org.apache.flink.table.types.logical.DecimalType;
import org.apache.flink.table.types.logical.LogicalType;
import org.junit.jupiter.api.Test;
+import java.util.Arrays;
+
import static org.apache.flink.table.data.util.DataFormatTestUtil.MyObj;
+import static
org.apache.flink.table.data.util.DataFormatTestUtil.nonCompactBoundaryDecimals;
import static org.apache.flink.table.data.util.DataFormatTestUtil.splitBytes;
+import static org.apache.flink.table.data.util.DataFormatTestUtil.toSegments;
import static org.assertj.core.api.Assertions.assertThat;
/** Test for {@link NestedRowData}s. */
@@ -129,6 +135,36 @@ class NestedRowDataTest {
assertThat(nestedRow.isNullAt(3)).isTrue();
}
+ @Test
+ void testNonCompactDecimalsWithMultipleSegments() {
+ int precision = 38;
+ int scale = 2;
+ DecimalData[] decimals = nonCompactBoundaryDecimals(precision, scale);
+ LogicalType[] types = new LogicalType[decimals.length];
+ Arrays.fill(types, new DecimalType(precision, scale));
+
+ BinaryRowData row = new BinaryRowData(1);
+ BinaryRowWriter writer = new BinaryRowWriter(row);
+ writer.writeRow(0, GenericRowData.of((Object[]) decimals), new
RowDataSerializer(types));
+ writer.complete();
+
+ int size = row.getSizeInBytes();
+ byte[] bytes = BinarySegmentUtils.copyToBytes(row.getSegments(),
row.getOffset(), size);
+ int baseOffset = 3;
+ // BinaryRowData requires its fixed-length part in the first segment
+ for (int segmentSize = baseOffset + row.getFixedLengthPartSize();
+ segmentSize <= baseOffset + size;
+ segmentSize++) {
+ row.pointTo(toSegments(bytes, baseOffset, segmentSize),
baseOffset, size);
+ RowData nestedRow = row.getRow(0, decimals.length);
+ for (int i = 0; i < decimals.length; i++) {
+ DecimalData decimal = nestedRow.getDecimal(i, precision,
scale);
+
assertThat(decimal.toBigDecimal()).isEqualTo(decimals[i].toBigDecimal());
+
assertThat(decimal.longBacked).isEqualTo(decimals[i].toUnscaledBytes().length
<= 8);
+ }
+ }
+ }
+
private BinaryRowData getBinaryRowData() {
BinaryRowData row = new BinaryRowData(1);
BinaryRowWriter writer = new BinaryRowWriter(row);
diff --git
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/util/DataFormatTestUtil.java
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/util/DataFormatTestUtil.java
index 4402056fea6..599209dc8f2 100644
---
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/util/DataFormatTestUtil.java
+++
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/data/util/DataFormatTestUtil.java
@@ -19,6 +19,7 @@ package org.apache.flink.table.data.util;
import org.apache.flink.core.memory.MemorySegment;
import org.apache.flink.core.memory.MemorySegmentFactory;
+import org.apache.flink.table.data.DecimalData;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.StringData;
import org.apache.flink.table.data.binary.BinaryRowData;
@@ -27,6 +28,10 @@ import org.apache.flink.table.types.logical.RowType;
import org.apache.commons.lang3.RandomStringUtils;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.stream.Stream;
+
import static org.apache.flink.util.Preconditions.checkArgument;
import static org.assertj.core.api.Assertions.assertThat;
@@ -121,6 +126,37 @@ public class DataFormatTestUtil {
return ret;
}
+ /** Non-compact decimals with unscaled values around the 8-byte boundary.
*/
+ public static DecimalData[] nonCompactBoundaryDecimals(int precision, int
scale) {
+ return Stream.of(
+ "0",
+ "-1",
+ "128",
+ "-129",
+ "9223372036854775807",
+ "-9223372036854775808",
+ "9223372036854775808",
+ "-9223372036854775809",
+ "12345678901234567890123456789012")
+ .map(v -> new BigDecimal(new BigInteger(v), scale))
+ .map(v -> DecimalData.fromBigDecimal(v, precision, scale))
+ .toArray(DecimalData[]::new);
+ }
+
+ /** Copies bytes to equally sized segments, starting at baseOffset of the
first one. */
+ public static MemorySegment[] toSegments(byte[] bytes, int baseOffset, int
segmentSize) {
+ MemorySegment[] ret =
+ new MemorySegment[(baseOffset + bytes.length + segmentSize -
1) / segmentSize];
+ for (int i = 0; i < ret.length; i++) {
+ ret[i] = MemorySegmentFactory.wrap(new byte[segmentSize]);
+ }
+ for (int i = 0; i < bytes.length; i++) {
+ int pos = baseOffset + i;
+ ret[pos / segmentSize].put(pos % segmentSize, bytes[i]);
+ }
+ return ret;
+ }
+
/** A simple class for testing generic type getting / setting on data
formats. */
public static class MyObj {
public int i;
diff --git
a/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/DecimalDataSerializer.java
b/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/DecimalDataSerializer.java
index 05f2757008b..f67e331bad5 100644
---
a/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/DecimalDataSerializer.java
+++
b/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/DecimalDataSerializer.java
@@ -25,6 +25,7 @@ import
org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
import org.apache.flink.core.memory.DataInputView;
import org.apache.flink.core.memory.DataOutputView;
import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.DecimalDataInternalUtils;
import java.io.IOException;
@@ -86,6 +87,15 @@ public final class DecimalDataSerializer extends
TypeSerializer<DecimalData> {
return DecimalData.fromUnscaledLong(longVal, precision, scale);
} else {
int length = source.readInt();
+ if (length > 0 && length <= 8) {
+ // signed first byte sign-extends
+ long unscaled = source.readByte();
+ for (int i = 1; i < length; i++) {
+ unscaled = (unscaled << 8) | source.readUnsignedByte();
+ }
+ // at most 19 digits, so it fits any non-compact precision
+ return DecimalDataInternalUtils.fromUnscaledLong(unscaled,
precision, scale);
+ }
byte[] bytes = new byte[length];
source.readFully(bytes);
return DecimalData.fromUnscaledBytes(bytes, precision, scale);
diff --git
a/flink-table/flink-table-type-utils/src/test/java/org/apache/flink/table/runtime/typeutils/DecimalSerializerTest.java
b/flink-table/flink-table-type-utils/src/test/java/org/apache/flink/table/runtime/typeutils/DecimalSerializerTest.java
index 821e2bd99c7..c69085c67c0 100644
---
a/flink-table/flink-table-type-utils/src/test/java/org/apache/flink/table/runtime/typeutils/DecimalSerializerTest.java
+++
b/flink-table/flink-table-type-utils/src/test/java/org/apache/flink/table/runtime/typeutils/DecimalSerializerTest.java
@@ -19,33 +19,109 @@
package org.apache.flink.table.runtime.typeutils;
import org.apache.flink.api.common.typeutils.SerializerTestBase;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputSerializer;
import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.DecimalDataInternalUtils;
+import org.apache.flink.util.StringUtils;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
/** A test for the {@link DecimalDataSerializer}. */
-class DecimalSerializerTest extends SerializerTestBase<DecimalData> {
+class DecimalSerializerTest {
+
+ /** Bytes as written by the BigDecimal-only serializer, which must stay
readable. */
+ @ParameterizedTest(name = "{0} -> {1}")
+ @CsvSource({
+ "00, 0",
+ "7f, 127",
+ "0080, 128",
+ "80, -128",
+ "ff7f, -129",
+ "7fffffffffffffff, 9223372036854775807",
+ "8000000000000000, -9223372036854775808",
+ "008000000000000000, 9223372036854775808",
+ "ff7fffffffffffffff, -9223372036854775809"
+ })
+ void testDeserializeLegacyNonCompactBytes(String hex, String unscaled)
throws IOException {
+ DecimalDataSerializer serializer = new DecimalDataSerializer(38, 2);
+ byte[] bytes = StringUtils.hexStringToByte(hex);
+ BigDecimal expected = new BigDecimal(new BigInteger(unscaled), 2);
+
+ DataOutputSerializer out = new DataOutputSerializer(16);
+ out.writeInt(bytes.length);
+ out.write(bytes);
+ byte[] serialized = out.getCopyOfBuffer();
- @Override
- protected DecimalDataSerializer createSerializer() {
- return new DecimalDataSerializer(5, 2);
+ DecimalData decimal = serializer.deserialize(new
DataInputDeserializer(serialized));
+ assertThat(decimal.toBigDecimal()).isEqualTo(expected);
+ assertThat(decimal.hashCode()).isEqualTo(expected.hashCode());
+
+ out.clear();
+ serializer.serialize(decimal, out);
+ assertThat(out.getCopyOfBuffer()).isEqualTo(serialized);
}
- @Override
- protected int getLength() {
- return -1;
+ @Nested
+ final class CompactDecimalSerializerTest extends DecimalSerializerTestBase
{
+ @Override
+ protected DecimalDataSerializer createSerializer() {
+ return new DecimalDataSerializer(5, 2);
+ }
+
+ @Override
+ protected DecimalData[] getTestData() {
+ return new DecimalData[] {
+ DecimalData.fromUnscaledLong(1, 5, 2),
+ DecimalData.fromUnscaledLong(2, 5, 2),
+ DecimalData.fromUnscaledLong(3, 5, 2),
+ DecimalData.fromUnscaledLong(4, 5, 2)
+ };
+ }
}
- @Override
- protected Class<DecimalData> getTypeClass() {
- return DecimalData.class;
+ @Nested
+ final class NonCompactDecimalSerializerTest extends
DecimalSerializerTestBase {
+ @Override
+ protected DecimalDataSerializer createSerializer() {
+ return new DecimalDataSerializer(38, 2);
+ }
+
+ @Override
+ protected DecimalData[] getTestData() {
+ return new DecimalData[] {
+ DecimalDataInternalUtils.fromUnscaledLong(0, 38, 2),
+ DecimalDataInternalUtils.fromUnscaledLong(-1, 38, 2),
+ DecimalDataInternalUtils.fromUnscaledLong(128, 38, 2),
+ DecimalDataInternalUtils.fromUnscaledLong(-129, 38, 2),
+ DecimalDataInternalUtils.fromUnscaledLong(Long.MAX_VALUE, 38,
2),
+ DecimalDataInternalUtils.fromUnscaledLong(Long.MIN_VALUE, 38,
2),
+ DecimalData.fromBigDecimal(new BigDecimal("12.34"), 38, 2),
+ DecimalData.fromBigDecimal(new
BigDecimal("92233720368547758.08"), 38, 2),
+ DecimalData.fromBigDecimal(
+ new BigDecimal("-123456789012345678901234567890.12"),
38, 2)
+ };
+ }
}
- @Override
- protected DecimalData[] getTestData() {
- return new DecimalData[] {
- DecimalData.fromUnscaledLong(1, 5, 2),
- DecimalData.fromUnscaledLong(2, 5, 2),
- DecimalData.fromUnscaledLong(3, 5, 2),
- DecimalData.fromUnscaledLong(4, 5, 2)
- };
+ abstract static class DecimalSerializerTestBase extends
SerializerTestBase<DecimalData> {
+
+ @Override
+ protected int getLength() {
+ return -1;
+ }
+
+ @Override
+ protected Class<DecimalData> getTypeClass() {
+ return DecimalData.class;
+ }
}
}