laskoviymishka commented on code in PR #17726:
URL: https://github.com/apache/iceberg/pull/17726#discussion_r3977586419


##########
api/src/main/java/org/apache/iceberg/variants/VariantUtil.java:
##########
@@ -99,13 +99,13 @@ static String readString(ByteBuffer buffer, int offset, int 
length) {
     }
   }
 
-  static <T extends Comparable<T>> int find(int size, T key, Function<Integer, 
T> resolve) {
+  static int find(int size, String key, Function<Integer, String> resolve) {
     int low = 0;
     int high = size - 1;
     while (low <= high) {
       int mid = (low + high) >>> 1;
-      T value = resolve.apply(mid);
-      int cmp = key.compareTo(value);
+      String value = resolve.apply(mid);
+      int cmp = VariantMetadata.FIELD_NAME_ORDER.compare(key, value);

Review Comment:
   This is the backward-compat read I raised last round, and it's still the one 
thing I'd want a deliberate call on before we merge.
   
   New files are fine now that both the write side and this search share 
`FIELD_NAME_ORDER`. The gap is old files that set `sorted_strings=1` but 
physically ordered the dictionary in UTF-16 — only reachable with 
supplementary-plane field names, so a narrow blast radius, but a real one. This 
search now runs UTF-8 order over that UTF-16 layout, the binary search 
diverges, and `SerializedMetadata.id()` returns -1 for a name that's actually 
present. Silent wrong-answer-on-read, no exception.
   
   A one-line linear-scan fallback in `SerializedMetadata.id()` on a 
binary-search miss would cover it cheaply (only on the miss path), or we 
document the break in a release note — either's fine, I just don't want it 
landing silently. wdyt?



##########
api/src/main/java/org/apache/iceberg/variants/VariantUtil.java:
##########
@@ -99,13 +99,13 @@ static String readString(ByteBuffer buffer, int offset, int 
length) {
     }
   }
 
-  static <T extends Comparable<T>> int find(int size, T key, Function<Integer, 
T> resolve) {
+  static int find(int size, String key, Function<Integer, String> resolve) {

Review Comment:
   Now that `find` is concrete on `String`, `Function<Integer, String>` boxes 
an `Integer` on every `resolve.apply(mid)` — and this is the binary-search hot 
path (once per step in `id()`, twice per `SerializedObject.get()`). All three 
call sites already fit `IntFunction<String>`, so since we're touching the 
signature anyway I'd switch it while we're here.



##########
api/src/test/java/org/apache/iceberg/variants/VariantTestUtil.java:
##########
@@ -163,7 +163,10 @@ public static ByteBuffer createMetadata(Collection<String> 
fieldNames, boolean s
     }
 
     int numElements = fieldNames.size();
-    Stream<String> names = sortNames ? fieldNames.stream().sorted() : 
fieldNames.stream();
+    Stream<String> names =
+        sortNames
+            ? fieldNames.stream().sorted(VariantMetadata.FIELD_NAME_ORDER)

Review Comment:
   `createMetadata` sorts by `FIELD_NAME_ORDER` now, but `createObject` down at 
line 246 still uses plain `.sorted()` (UTF-16). With 
`NAME_3_BYTE`/`NAME_4_BYTE` now first-class test data, a test that builds an 
object with supplementary names and then calls `SerializedObject.get()` would 
binary-search over mis-ordered fields and fail. I'd switch 246 to 
`.sorted(VariantMetadata.FIELD_NAME_ORDER)` for consistency while we're in here.



##########
core/src/test/java/org/apache/iceberg/variants/TestVariantMetadataFieldOrdering.java:
##########
@@ -0,0 +1,60 @@
+/*
+ * 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.iceberg.variants;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Test;
+
+public class TestVariantMetadataFieldOrdering {
+
+  // U+FFFF encodes to 3 UTF-8 bytes (EF BF BF), U+10000 to 4 (F0 90 80 80)
+  private static final String NAME_3_BYTE = new 
String(Character.toChars(0xFFFF));
+  private static final String NAME_4_BYTE = new 
String(Character.toChars(0x10000));
+
+  @Test
+  public void utf8OrderedDictionaryIsSortedAndOrdered() {
+    SerializedMetadata metadata =
+        (SerializedMetadata) Variants.metadata(ImmutableList.of(NAME_3_BYTE, 
NAME_4_BYTE));
+
+    assertThat(metadata.isSorted()).isTrue();
+    assertThat(metadata.get(0)).isEqualTo(NAME_3_BYTE);
+    assertThat(metadata.get(1)).isEqualTo(NAME_4_BYTE);
+    assertThat(metadata.id(NAME_3_BYTE)).isEqualTo(0);
+    assertThat(metadata.id(NAME_4_BYTE)).isEqualTo(1);
+  }
+
+  @Test
+  public void utf16OrderedDictionaryIsNotFlaggedSorted() {
+    // UTF-16 order differs from UTF-8: high surrogate D800 sorts before FFFF
+    List<String> utf16Ordered =
+        ImmutableList.of(NAME_3_BYTE, 
NAME_4_BYTE).stream().sorted().collect(Collectors.toList());
+    assertThat(utf16Ordered).containsExactly(NAME_4_BYTE, NAME_3_BYTE);
+
+    SerializedMetadata metadata = (SerializedMetadata) 
Variants.metadata(utf16Ordered);
+
+    assertThat(metadata.isSorted()).isFalse();

Review Comment:
   This nicely documents the unsorted/linear path. The case it doesn't cover is 
the hazard from the `find` comment: a dict flagged `sorted_strings=1` but laid 
out in UTF-16 order. I'd add a test that hand-builds exactly that buffer and 
asserts today's behavior (`id()` returns -1), so whichever way we go on the 
fallback it's pinned by a test rather than invisible in the suite.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to