|
17 | 17 | */ |
18 | 18 | package org.apache.beam.sdk.io.iceberg; |
19 | 19 |
|
| 20 | +import static org.junit.Assert.assertArrayEquals; |
| 21 | +import static org.junit.Assert.assertEquals; |
| 22 | + |
20 | 23 | import java.lang.reflect.Method; |
| 24 | +import java.nio.ByteBuffer; |
| 25 | +import java.nio.charset.StandardCharsets; |
21 | 26 | import java.util.ArrayList; |
22 | 27 | import java.util.Arrays; |
| 28 | +import java.util.HashMap; |
23 | 29 | import java.util.List; |
| 30 | +import java.util.Map; |
24 | 31 | import java.util.Set; |
25 | 32 | import java.util.stream.Collectors; |
26 | 33 | import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; |
| 34 | +import org.apache.iceberg.DataFile; |
| 35 | +import org.apache.iceberg.DataFiles; |
| 36 | +import org.apache.iceberg.FileFormat; |
| 37 | +import org.apache.iceberg.Metrics; |
| 38 | +import org.apache.iceberg.PartitionSpec; |
| 39 | +import org.apache.iceberg.types.Conversions; |
| 40 | +import org.apache.iceberg.types.Types; |
27 | 41 | import org.junit.Test; |
28 | 42 |
|
29 | 43 | /** |
@@ -73,4 +87,55 @@ public void testFieldsInEqualsMethodInSyncWithGetterFields() { |
73 | 87 | + "to this test class's FIELDS_SET."); |
74 | 88 | } |
75 | 89 | } |
| 90 | + |
| 91 | + /** |
| 92 | + * Bounds with {@code capacity > limit} must be copied by {@code [position, limit)}, not by {@link |
| 93 | + * ByteBuffer#array()}. Otherwise trailing 0x00 bytes leak into the manifest bounds and break |
| 94 | + * equality predicate pushdown in some query engines. |
| 95 | + */ |
| 96 | + @Test |
| 97 | + public void testBoundByteBufferIsCopiedByLimitNotBackingArrayLength() { |
| 98 | + // Encode bounds the same way iceberg-parquet does in the wild — via |
| 99 | + // Conversions.toByteBuffer(STRING, value). For UTF-8 strings of 10+ |
| 100 | + // characters the underlying JDK CharsetEncoder over-allocates by ~10% |
| 101 | + // and flips, producing a ByteBuffer with capacity > limit. |
| 102 | + int columnId = 3; |
| 103 | + String lowerValue = "lower_bound_str"; |
| 104 | + String upperValue = "upper_bound_str"; |
| 105 | + byte[] expectedLower = lowerValue.getBytes(StandardCharsets.UTF_8); |
| 106 | + byte[] expectedUpper = upperValue.getBytes(StandardCharsets.UTF_8); |
| 107 | + |
| 108 | + ByteBuffer lower = Conversions.toByteBuffer(Types.StringType.get(), lowerValue); |
| 109 | + ByteBuffer upper = Conversions.toByteBuffer(Types.StringType.get(), upperValue); |
| 110 | + |
| 111 | + Map<Integer, ByteBuffer> lowerBounds = new HashMap<>(); |
| 112 | + lowerBounds.put(columnId, lower); |
| 113 | + Map<Integer, ByteBuffer> upperBounds = new HashMap<>(); |
| 114 | + upperBounds.put(columnId, upper); |
| 115 | + |
| 116 | + Metrics metrics = new Metrics(1L, null, null, null, null, lowerBounds, upperBounds); |
| 117 | + |
| 118 | + DataFile dataFile = |
| 119 | + DataFiles.builder(PartitionSpec.unpartitioned()) |
| 120 | + .withFormat(FileFormat.PARQUET) |
| 121 | + .withPath("gs://test-bucket/data/test-file.parquet") |
| 122 | + .withFileSizeInBytes(1024L) |
| 123 | + .withMetrics(metrics) |
| 124 | + .build(); |
| 125 | + |
| 126 | + SerializableDataFile serialized = SerializableDataFile.from(dataFile, ""); |
| 127 | + |
| 128 | + byte[] serializedLower = serialized.getLowerBounds().get(columnId); |
| 129 | + byte[] serializedUpper = serialized.getUpperBounds().get(columnId); |
| 130 | + assertEquals( |
| 131 | + "lower bound length must match content, not backing array", |
| 132 | + expectedLower.length, |
| 133 | + serializedLower.length); |
| 134 | + assertEquals( |
| 135 | + "upper bound length must match content, not backing array", |
| 136 | + expectedUpper.length, |
| 137 | + serializedUpper.length); |
| 138 | + assertArrayEquals(expectedLower, serializedLower); |
| 139 | + assertArrayEquals(expectedUpper, serializedUpper); |
| 140 | + } |
76 | 141 | } |
0 commit comments