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

raminqaf pushed a commit to branch release-2.4
in repository https://gitbox.apache.org/repos/asf/flink.git

commit 16e07dcf5778228244cb2af6ef657e5ef667bdb6
Author: Ramin Gharib <[email protected]>
AuthorDate: Tue Oct 27 13:20:43 2026 +0100

    [FLINK-41241][core] Add VariantFieldGetter to read an object field by a 
fixed key
    
    A VariantFieldGetter reads one field of object variants by a key known up 
front, such as 'user' in v['user']. It encodes the key once and remembers the 
index where it last found it. Objects of one stream mostly share their keys, so 
that index is checked first, and a wrong guess costs one key comparison before 
the regular search. The index is only a hint that is always verified, so a 
getter can be shared between threads.
    
    BinaryVariant#getField(String) and the getter share one lookup, 
BinaryVariant#getFieldByKey(byte[], VariantFieldGetter). 
BinaryVariant#getFieldByUtf8Key exposes it for a key that is already UTF-8 
encoded. It decodes the object header through new BinaryVariantUtil helpers 
instead of handleObject, because the JIT does not reliably remove 
handleObject's capturing lambda on this path. handleObject is built on the same 
helpers.
    
    For an object written before FLINK-41180 that holds a key twice, the getter 
can return a different one of the two fields than getField.
    
    JMH, JDK 17, 4 lookups per row, master getField vs getter: 13 keys 214 ns 
to 44 ns, 40 keys (binary search) 158 ns to 44 ns, field index moving every row 
296 ns to 242 ns. The getter allocates only the 4 returned views, 96 B per row, 
where master allocates 192 to 288 B.
    
    Generated-by: Claude Code 2.1.311 (Claude Opus 5.5)
    (cherry picked from commit e36950aff2f61bafdb046eb3293d55fc725b7953)
---
 .../apache/flink/types/variant/BinaryVariant.java  | 104 +++++++++++++++------
 .../flink/types/variant/BinaryVariantUtil.java     |  48 +++++++---
 .../flink/types/variant/VariantFieldGetter.java    |  63 +++++++++++++
 .../flink/types/variant/BinaryVariantTest.java     |  25 +++++
 4 files changed, 199 insertions(+), 41 deletions(-)

diff --git 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariant.java 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariant.java
index 25427486205..b9de7c07a53 100644
--- a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariant.java
+++ b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariant.java
@@ -20,6 +20,8 @@ package org.apache.flink.types.variant;
 
 import org.apache.flink.annotation.Internal;
 
+import javax.annotation.Nullable;
+
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -47,6 +49,10 @@ import static 
org.apache.flink.types.variant.BinaryVariantUtil.malformedVariant;
 import static 
org.apache.flink.types.variant.BinaryVariantUtil.metadataKeyEquals;
 import static org.apache.flink.types.variant.BinaryVariantUtil.microsToInstant;
 import static org.apache.flink.types.variant.BinaryVariantUtil.nanosToInstant;
+import static org.apache.flink.types.variant.BinaryVariantUtil.objectIdSize;
+import static 
org.apache.flink.types.variant.BinaryVariantUtil.objectOffsetSize;
+import static org.apache.flink.types.variant.BinaryVariantUtil.objectSizeBytes;
+import static org.apache.flink.types.variant.BinaryVariantUtil.objectTypeInfo;
 import static org.apache.flink.types.variant.BinaryVariantUtil.readUnsigned;
 import static org.apache.flink.types.variant.BinaryVariantUtil.valueSize;
 import static 
org.apache.flink.types.variant.BinaryVariantUtil.variantConstructorSizeLimit;
@@ -288,6 +294,16 @@ public final class BinaryVariant implements Variant {
         return getFieldByKey(fieldName);
     }
 
+    /**
+     * Returns the field whose key has the UTF-8 bytes {@code key}, like 
{@link #getField(String)}
+     * without decoding the key, or null if the object has no such field.
+     *
+     * @throws VariantTypeException if the variant is not an object
+     */
+    public @Nullable Variant getFieldByUtf8Key(byte[] key) {
+        return getFieldByKey(key, null);
+    }
+
     @Override
     public List<String> getFieldNames() throws VariantTypeException {
         return handleObject(
@@ -341,32 +357,68 @@ public final class BinaryVariant implements Variant {
     // Find the field value whose key is equal to `key`. Return null if the 
key is not found.
     // It is only legal to call it when `getType()` is `Type.OBJECT`.
     private BinaryVariant getFieldByKey(String key) {
-        byte[] keyBytes = encodeKey(key);
-        return handleObject(
-                value,
-                pos,
-                (size, idSize, offsetSize, idStart, offsetStart, dataStart) -> 
{
-                    int index;
-                    // Use linear search for a short list. Switch to binary 
search when the length
-                    // reaches `BINARY_SEARCH_THRESHOLD`.
-                    if (size < BINARY_SEARCH_THRESHOLD) {
-                        index = findFieldLinear(keyBytes, size, idSize, 
idStart);
-                    } else {
-                        index = findFieldBinary(keyBytes, size, idSize, 
idStart);
-                        // Objects that earlier Flink versions wrote are 
sorted in UTF-16 order
-                        // instead. Only a miss for a key with a code point 
from U+E000 up can be
-                        // wrong there, see `hasCodePointFromE000`. Such a 
miss scans all fields,
-                        // because an object does not record which order it 
uses.
-                        if (index < 0 && hasCodePointFromE000(keyBytes)) {
-                            index = findFieldLinear(keyBytes, size, idSize, 
idStart);
-                        }
-                    }
-                    if (index < 0) {
-                        return null;
-                    }
-                    int offset = readUnsigned(value, offsetStart + offsetSize 
* index, offsetSize);
-                    return new BinaryVariant(value, metadata, dataStart + 
offset);
-                });
+        return getFieldByKey(encodeKey(key), null);
+    }
+
+    /**
+     * Returns the field whose key has the UTF-8 bytes {@code key}, or null if 
there is none.
+     *
+     * <p>A {@code getter} remembers the index at which it last found its key. 
That field is checked
+     * first, so a correct guess skips the search and a wrong one costs one 
key comparison.
+     *
+     * @throws VariantTypeException if the variant is not an object
+     */
+    @Nullable
+    BinaryVariant getFieldByKey(byte[] key, @Nullable VariantFieldGetter 
getter) {
+        // Decode the header here instead of in `handleObject` and its 
capturing lambda, so that a
+        // lookup allocates nothing but the returned view.
+        int typeInfo = objectTypeInfo(value, pos);
+        int sizeBytes = objectSizeBytes(typeInfo);
+        int size = readUnsigned(value, pos + 1, sizeBytes);
+        int idSize = objectIdSize(typeInfo);
+        int idStart = pos + 1 + sizeBytes;
+        // Read the remembered index once, so that a getter shared between 
threads only ever
+        // returns a field whose key it checked.
+        int index = getter == null ? -1 : getter.lastIndex;
+        if (index < 0 || !hasKeyAt(key, index, size, idSize, idStart)) {
+            index = findField(key, size, idSize, idStart);
+            if (index < 0) {
+                return null;
+            }
+            if (getter != null) {
+                getter.lastIndex = index;
+            }
+        }
+        int offsetSize = objectOffsetSize(typeInfo);
+        int offsetStart = idStart + size * idSize;
+        int dataStart = offsetStart + (size + 1) * offsetSize;
+        int offset = readUnsigned(value, offsetStart + offsetSize * index, 
offsetSize);
+        return new BinaryVariant(value, metadata, dataStart + offset);
+    }
+
+    private boolean hasKeyAt(byte[] key, int index, int size, int idSize, int 
idStart) {
+        return index < size
+                && metadataKeyEquals(
+                        metadata, readUnsigned(value, idStart + idSize * 
index, idSize), key);
+    }
+
+    /**
+     * Like {@code findFieldLinear}, but uses binary search from {@code 
BINARY_SEARCH_THRESHOLD}
+     * fields on.
+     */
+    private int findField(byte[] key, int size, int idSize, int idStart) {
+        if (size < BINARY_SEARCH_THRESHOLD) {
+            return findFieldLinear(key, size, idSize, idStart);
+        }
+        int index = findFieldBinary(key, size, idSize, idStart);
+        // Objects that earlier Flink versions wrote are sorted in UTF-16 
order instead. Only a miss
+        // for a key with a code point from U+E000 up can be wrong there, see
+        // `hasCodePointFromE000`. Such a miss scans all fields, because an 
object does not record
+        // which order it uses.
+        if (index < 0 && hasCodePointFromE000(key)) {
+            index = findFieldLinear(key, size, idSize, idStart);
+        }
+        return index;
     }
 
     // Return the index of the field whose key has the UTF-8 bytes `key`, or 
-1 if there is none.
diff --git 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
index 23cdf8c6779..85d13a9f501 100644
--- 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
+++ 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
@@ -788,28 +788,46 @@ public class BinaryVariantUtil {
      * parameters and returns what it returns.
      */
     public static <T> T handleObject(byte[] value, int pos, ObjectHandler<T> 
handler) {
-        checkIndex(pos, value.length);
-        int basicType = value[pos] & BASIC_TYPE_MASK;
-        int typeInfo = (value[pos] >> BASIC_TYPE_BITS) & TYPE_INFO_MASK;
-        if (basicType != OBJECT) {
-            throw unexpectedType(Type.OBJECT);
-        }
-        // Refer to the comment of the `OBJECT` constant for the details of 
the object header
-        // encoding. Suppose `typeInfo` has a bit representation of 
0_b4_b3b2_b1b0, the following
-        // line extracts b4 to determine whether the object uses a 1/4-byte 
size.
-        boolean largeSize = ((typeInfo >> 4) & 0x1) != 0;
-        int sizeBytes = (largeSize ? U32_SIZE : 1);
+        int typeInfo = objectTypeInfo(value, pos);
+        int sizeBytes = objectSizeBytes(typeInfo);
         int size = readUnsigned(value, pos + 1, sizeBytes);
-        // Extracts b3b2 to determine the integer size of the field id list.
-        int idSize = ((typeInfo >> 2) & 0x3) + 1;
-        // Extracts b1b0 to determine the integer size of the offset list.
-        int offsetSize = (typeInfo & 0x3) + 1;
+        int idSize = objectIdSize(typeInfo);
+        int offsetSize = objectOffsetSize(typeInfo);
         int idStart = pos + 1 + sizeBytes;
         int offsetStart = idStart + size * idSize;
         int dataStart = offsetStart + (size + 1) * offsetSize;
         return handler.apply(size, idSize, offsetSize, idStart, offsetStart, 
dataStart);
     }
 
+    /**
+     * Returns the type info of the object at {@code pos}. Refer to the 
comment of the {@code
+     * OBJECT} constant for its encoding, a bit representation of 
0_b4_b3b2_b1b0.
+     *
+     * @throws VariantTypeException if the value at {@code pos} is not an 
object
+     */
+    static int objectTypeInfo(byte[] value, int pos) {
+        checkIndex(pos, value.length);
+        if ((value[pos] & BASIC_TYPE_MASK) != OBJECT) {
+            throw unexpectedType(Type.OBJECT);
+        }
+        return (value[pos] >> BASIC_TYPE_BITS) & TYPE_INFO_MASK;
+    }
+
+    /** Extracts b4 of an object type info to determine whether the size takes 
1 or 4 bytes. */
+    static int objectSizeBytes(int typeInfo) {
+        return ((typeInfo >> 4) & 0x1) != 0 ? U32_SIZE : 1;
+    }
+
+    /** Extracts b3b2 of an object type info to determine the integer size of 
the field ids. */
+    static int objectIdSize(int typeInfo) {
+        return ((typeInfo >> 2) & 0x3) + 1;
+    }
+
+    /** Extracts b1b0 of an object type info to determine the integer size of 
the offsets. */
+    static int objectOffsetSize(int typeInfo) {
+        return (typeInfo & 0x3) + 1;
+    }
+
     /** A handler that receives the decoded header fields of a variant array. 
*/
     public interface ArrayHandler<T> {
         /**
diff --git 
a/flink-core/src/main/java/org/apache/flink/types/variant/VariantFieldGetter.java
 
b/flink-core/src/main/java/org/apache/flink/types/variant/VariantFieldGetter.java
new file mode 100644
index 00000000000..e499f827d0a
--- /dev/null
+++ 
b/flink-core/src/main/java/org/apache/flink/types/variant/VariantFieldGetter.java
@@ -0,0 +1,63 @@
+/*
+ * 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.types.variant;
+
+import org.apache.flink.annotation.Internal;
+
+import javax.annotation.Nullable;
+
+import java.io.Serializable;
+
+import static org.apache.flink.types.variant.BinaryVariantUtil.encodeKey;
+
+/**
+ * Reads one field of object variants by a key that is known up front, such as 
{@code 'user'} in
+ * {@code v['user']}.
+ *
+ * <p>The getter remembers the index at which it last found the key and checks 
it first. Objects of
+ * one stream mostly share their keys, so the key is usually at the same 
index. A wrong guess costs
+ * one key comparison before the regular search.
+ *
+ * <p>The remembered index is only a hint that is always verified, so a getter 
is safe to share
+ * between threads.
+ */
+@Internal
+public final class VariantFieldGetter implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final byte[] key;
+    // Read and written by BinaryVariant#getFieldByKey. -1 until the getter 
has found its key.
+    int lastIndex = -1;
+
+    public VariantFieldGetter(final String fieldName) {
+        this.key = encodeKey(fieldName);
+    }
+
+    /**
+     * Returns the field of an object variant like {@link Variant#getField}, 
or null if the object
+     * has no such field. For an object that holds the key twice, the field it 
returns may differ
+     * between calls.
+     *
+     * @throws VariantTypeException if the variant is not an object
+     */
+    public @Nullable Variant getField(final Variant variant) {
+        return ((BinaryVariant) variant).getFieldByKey(key, this);
+    }
+}
diff --git 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
index 8729209bbf4..961c2ff4a4d 100644
--- 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
+++ 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
@@ -36,6 +36,7 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.ZoneOffset;
 import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
@@ -586,6 +587,30 @@ class BinaryVariantTest {
         assertThat(malformed.getField("�")).isNull();
     }
 
+    @Test
+    void testGetFieldWithGetterWhenIndexChanges() {
+        final List<Variant> objects =
+                List.of(
+                        builder.object().add("a", builder.of(1)).add("user", 
builder.of(2)).build(),
+                        builder.object().add("a", builder.of(3)).add("user", 
builder.of(4)).build(),
+                        builder.object().add("user", builder.of(5)).build(),
+                        builder.object().add("a", builder.of(6)).add("b", 
builder.of(7)).build(),
+                        builder.object().build(),
+                        builder.object()
+                                .add("a", builder.of(8))
+                                .add("user", builder.of(9))
+                                .build());
+
+        final VariantFieldGetter userFieldGetter = new 
VariantFieldGetter("user");
+        final List<Integer> actualValues = new ArrayList<>();
+        for (final Variant object : objects) {
+            final Variant field = userFieldGetter.getField(object);
+            actualValues.add(field == null ? null : field.getInt());
+        }
+
+        assertThat(actualValues).containsExactly(2, 4, 5, null, null, 9);
+    }
+
     @Test
     void testVariantException() {
         assertThatThrownBy(() -> new BinaryVariant(new byte[0], new byte[0]))

Reply via email to