This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 8510ae0273 [common] Widen hilbert index bytes beyond 8 dimensions 
(#9777)
8510ae0273 is described below

commit 8510ae0273ea46adc58ea1c32f4f755d51b5fd2e
Author: YangJie <[email protected]>
AuthorDate: Sun Sep 13 22:41:48 2026 -0400

    [common] Widen hilbert index bytes beyond 8 dimensions (#9777)
---
 .../apache/paimon/sort/hilbert/HilbertIndexer.java |   9 +-
 .../paimon/sort/hilbert/HilbertIndexerTest.java    | 108 +++++++++++++++++++++
 2 files changed, 116 insertions(+), 1 deletion(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/sort/hilbert/HilbertIndexer.java
 
b/paimon-common/src/main/java/org/apache/paimon/sort/hilbert/HilbertIndexer.java
index 21f725015e..b4d67e48ca 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/sort/hilbert/HilbertIndexer.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/sort/hilbert/HilbertIndexer.java
@@ -317,7 +317,14 @@ public class HilbertIndexer implements Serializable {
         long[] data = 
Arrays.stream(points).mapToLong(Long::longValue).toArray();
         HilbertCurve hilbertCurve = 
HilbertCurve.bits(BITS_NUM).dimensions(points.length);
         BigInteger index = hilbertCurve.index(data);
-        return ConvertBinaryUtil.paddingToNByte(index.toByteArray(), BITS_NUM);
+        // an N-dimensional 63-bit index needs up to 63*N/8 + 1 bytes: the 
extra byte covers
+        // BigInteger's sign byte when the top bit is set. paddingToNByte 
drops trailing bytes
+        // when the array is longer than the width, so a narrower width 
silently discards
+        // low-order bits — at 8 dimensions that also inverts the order, 
because the sign byte
+        // makes an upper-half index sort below a smaller lower-half one. 
Hilbert keys are
+        // transient sort keys in every consumer, so widening them breaks 
nothing.
+        int paddingBytes = BITS_NUM * data.length / 8 + 1;
+        return ConvertBinaryUtil.paddingToNByte(index.toByteArray(), 
paddingBytes);
     }
 
     /** Process function interface. */
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/sort/hilbert/HilbertIndexerTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/sort/hilbert/HilbertIndexerTest.java
index 238e80cbb5..f547edb2e1 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/sort/hilbert/HilbertIndexerTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/sort/hilbert/HilbertIndexerTest.java
@@ -23,9 +23,13 @@ import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
 
+import org.davidmoten.hilbert.HilbertCurve;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigInteger;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -60,6 +64,110 @@ public class HilbertIndexerTest {
         assertThat(falseIndex).isNotEqualTo(trueIndex);
     }
 
+    @Test
+    public void testHighDimensionIndexKeepsAllBits() {
+        // 9 dimensions: the 63*9-bit index needs 71 bytes; distinct points 
that differ
+        // only in the low-order bits must stay distinct instead of being 
truncated away
+        Long[][] points = {
+            {0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L},
+            {0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 1L},
+        };
+        byte[] first = HilbertIndexer.hilbertCurvePosBytes(points[0]);
+        byte[] second = HilbertIndexer.hilbertCurvePosBytes(points[1]);
+        assertThat(first).hasSize(71);
+        assertThat(second).hasSize(71);
+        assertThat(first).isNotEqualTo(second);
+
+        // 16 dimensions: the top bit being set adds BigInteger's sign byte, 
so the width
+        // must cover it or the low byte is truncated away
+        Long[] highBits =
+                new Long[] {
+                    Long.MAX_VALUE, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 
0L, 0L, 0L, 0L, 0L
+                };
+        Long[] highBitsVariant = highBits.clone();
+        highBitsVariant[15] = 1L;
+        byte[] highFirst = HilbertIndexer.hilbertCurvePosBytes(highBits);
+        byte[] highSecond = 
HilbertIndexer.hilbertCurvePosBytes(highBitsVariant);
+        assertThat(highFirst).hasSize(127);
+        assertThat(highSecond).hasSize(127);
+        assertThat(highFirst).isNotEqualTo(highSecond);
+
+        // the width is 63*N/8 + 1 for every N, so a 2-dimension key is 16 
bytes and an
+        // 8-dimension one is 64 — the extra byte over the 63-byte magnitude 
is what makes
+        // room for BigInteger's sign byte
+        assertThat(HilbertIndexer.hilbertCurvePosBytes(new Long[] {0L, 
0L})).hasSize(16);
+        assertThat(HilbertIndexer.hilbertCurvePosBytes(new Long[] {0L, 0L, 0L, 
0L, 0L, 0L, 0L, 0L}))
+                .hasSize(64);
+    }
+
+    /**
+     * At 8 dimensions the index fills 63 bytes, so the top half of the space 
carries BigInteger's
+     * sign byte and spills to 64. Truncating that back to 63 does not merely 
lose resolution: the
+     * leading zero makes a large index sort below a smaller one.
+     */
+    @Test
+    public void testEightDimensionKeysOrderLikeTheirIndex() {
+        List<Long[]> points = new ArrayList<>();
+        for (long i = 0; i < 24; i++) {
+            // spread the points over the whole space so some land in the top 
half
+            long v = Long.MAX_VALUE / 23 * i;
+            points.add(new Long[] {v, v / 3, i, Long.MAX_VALUE - v, v / 7, i * 
31, v / 11, i});
+        }
+
+        for (Long[] left : points) {
+            for (Long[] right : points) {
+                int indexOrder = index(left).compareTo(index(right));
+                int keyOrder =
+                        compareUnsigned(
+                                HilbertIndexer.hilbertCurvePosBytes(left),
+                                HilbertIndexer.hilbertCurvePosBytes(right));
+                assertThat(Integer.signum(keyOrder))
+                        .as(
+                                "key order must follow index order for %s vs 
%s",
+                                Arrays.toString(left), Arrays.toString(right))
+                        .isEqualTo(Integer.signum(indexOrder));
+            }
+        }
+    }
+
+    /**
+     * Sizes and inequalities are proxies for the property the width exists to 
guarantee: the key
+     * carries the whole index. State it directly, at the two dimension counts 
where the index needs
+     * its last byte the most.
+     */
+    @Test
+    public void testKeyCarriesTheWholeIndex() {
+        for (int dimensions : new int[] {8, 9}) {
+            Long[] topOfSpace = new Long[dimensions];
+            Arrays.fill(topOfSpace, Long.MAX_VALUE);
+            Long[] oneLowBitOff = topOfSpace.clone();
+            oneLowBitOff[dimensions - 1] = Long.MAX_VALUE - 1;
+
+            for (Long[] point : new Long[][] {topOfSpace, oneLowBitOff}) {
+                byte[] key = HilbertIndexer.hilbertCurvePosBytes(point);
+                assertThat(new BigInteger(1, key))
+                        .as("key must round-trip the index at %s dimensions", 
dimensions)
+                        .isEqualTo(index(point));
+            }
+        }
+    }
+
+    private static BigInteger index(Long[] points) {
+        long[] data = 
Arrays.stream(points).mapToLong(Long::longValue).toArray();
+        return HilbertCurve.bits(63).dimensions(points.length).index(data);
+    }
+
+    private static int compareUnsigned(byte[] left, byte[] right) {
+        assertThat(left).hasSameSizeAs(right);
+        for (int i = 0; i < left.length; i++) {
+            int cmp = Integer.compare(left[i] & 0xFF, right[i] & 0xFF);
+            if (cmp != 0) {
+                return cmp;
+            }
+        }
+        return 0;
+    }
+
     private static GenericRow booleanRow(Boolean value) {
         GenericRow row = new GenericRow(2);
         row.setField(0, value);

Reply via email to