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 f75c52d2b7 [core] Support projected binary index manifest scans (#9095)
f75c52d2b7 is described below

commit f75c52d2b74c479c3f7ba0f24a1233832ca4da4d
Author: YeJunHao <[email protected]>
AuthorDate: Fri Aug 7 19:21:26 2026 +0800

    [core] Support projected binary index manifest scans (#9095)
---
 .../org/apache/paimon/index/GlobalIndexMeta.java   |  20 +-
 .../org/apache/paimon/index/IndexFileHandler.java  |  15 ++
 .../paimon/manifest/BinaryIndexManifestEntry.java  | 282 +++++++++++++++++++++
 .../apache/paimon/manifest/IndexManifestEntry.java |  34 ++-
 .../manifest/IndexManifestEntrySerializer.java     |   2 +-
 .../apache/paimon/manifest/IndexManifestFile.java  |  65 ++++-
 .../apache/paimon/index/IndexFileHandlerTest.java  |  14 +
 .../manifest/BinaryIndexManifestEntryTest.java     | 152 +++++++++++
 8 files changed, 565 insertions(+), 19 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/GlobalIndexMeta.java 
b/paimon-core/src/main/java/org/apache/paimon/index/GlobalIndexMeta.java
index 354c2261cf..026db07867 100644
--- a/paimon-core/src/main/java/org/apache/paimon/index/GlobalIndexMeta.java
+++ b/paimon-core/src/main/java/org/apache/paimon/index/GlobalIndexMeta.java
@@ -35,17 +35,23 @@ import java.util.Objects;
 /** Schema for global index. */
 public class GlobalIndexMeta {
 
+    public static final String ROW_RANGE_START = "_ROW_RANGE_START";
+    public static final String ROW_RANGE_END = "_ROW_RANGE_END";
+    public static final String INDEX_FIELD_ID = "_INDEX_FIELD_ID";
+    public static final String EXTRA_FIELD_IDS = "_EXTRA_FIELD_IDS";
+    public static final String INDEX_META = "_INDEX_META";
+    public static final String SOURCE_META = "_SOURCE_META";
+
     public static final RowType SCHEMA =
             new RowType(
                     true,
                     Arrays.asList(
-                            new DataField(0, "_ROW_RANGE_START", new 
BigIntType(false)),
-                            new DataField(1, "_ROW_RANGE_END", new 
BigIntType(false)),
-                            new DataField(2, "_INDEX_FIELD_ID", new 
IntType(false)),
-                            new DataField(
-                                    3, "_EXTRA_FIELD_IDS", DataTypes.ARRAY(new 
IntType(false))),
-                            new DataField(4, "_INDEX_META", DataTypes.BYTES()),
-                            new DataField(5, "_SOURCE_META", 
DataTypes.BYTES())));
+                            new DataField(0, ROW_RANGE_START, new 
BigIntType(false)),
+                            new DataField(1, ROW_RANGE_END, new 
BigIntType(false)),
+                            new DataField(2, INDEX_FIELD_ID, new 
IntType(false)),
+                            new DataField(3, EXTRA_FIELD_IDS, 
DataTypes.ARRAY(new IntType(false))),
+                            new DataField(4, INDEX_META, DataTypes.BYTES()),
+                            new DataField(5, SOURCE_META, DataTypes.BYTES())));
 
     private final long rowRangeStart;
     private final long rowRangeEnd;
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/IndexFileHandler.java 
b/paimon-core/src/main/java/org/apache/paimon/index/IndexFileHandler.java
index 8fa6554886..6621cb56cd 100644
--- a/paimon-core/src/main/java/org/apache/paimon/index/IndexFileHandler.java
+++ b/paimon-core/src/main/java/org/apache/paimon/index/IndexFileHandler.java
@@ -28,10 +28,12 @@ import org.apache.paimon.fs.Path;
 import org.apache.paimon.index.pkfulltext.PkFullTextIndexFile;
 import org.apache.paimon.index.pksorted.PkSortedIndexFile;
 import org.apache.paimon.index.pkvector.PkVectorAnnSegmentFile;
+import org.apache.paimon.manifest.BinaryIndexManifestEntry;
 import org.apache.paimon.manifest.IndexManifestEntry;
 import org.apache.paimon.manifest.IndexManifestEntrySerializer;
 import org.apache.paimon.manifest.IndexManifestFile;
 import org.apache.paimon.options.MemorySize;
+import org.apache.paimon.utils.CloseableIterator;
 import org.apache.paimon.utils.Filter;
 import org.apache.paimon.utils.IndexFilePathFactories;
 import org.apache.paimon.utils.Pair;
@@ -113,6 +115,19 @@ public class IndexFileHandler {
         return scan(snapshotManager.latestSnapshot(), indexType);
     }
 
+    public CloseableIterator<BinaryIndexManifestEntry> scan(
+            BinaryIndexManifestEntry.Projection projection) {
+        return scan(snapshotManager.latestSnapshot(), projection);
+    }
+
+    public CloseableIterator<BinaryIndexManifestEntry> scan(
+            @Nullable Snapshot snapshot, BinaryIndexManifestEntry.Projection 
projection) {
+        if (snapshot == null || snapshot.indexManifest() == null) {
+            return CloseableIterator.empty();
+        }
+        return indexManifestFile.scan(snapshot.indexManifest(), projection);
+    }
+
     public List<IndexManifestEntry> scan(@Nullable Snapshot snapshot, String 
indexType) {
         if (snapshot == null) {
             return Collections.emptyList();
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/BinaryIndexManifestEntry.java
 
b/paimon-core/src/main/java/org/apache/paimon/manifest/BinaryIndexManifestEntry.java
new file mode 100644
index 0000000000..e951316f09
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/manifest/BinaryIndexManifestEntry.java
@@ -0,0 +1,282 @@
+/*
+ * 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.paimon.manifest;
+
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.index.GlobalIndexMeta;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.RowType;
+
+import javax.annotation.Nullable;
+
+import java.util.Arrays;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+import static org.apache.paimon.utils.Preconditions.checkState;
+
+/** Reusable binary view of a projected index manifest entry. */
+public final class BinaryIndexManifestEntry {
+
+    public static final Projection GLOBAL_INDEX_PROJECTION = 
createGlobalIndexProjection();
+
+    private final Projection projection;
+    private @Nullable InternalRow row;
+
+    private BinaryIndexManifestEntry(Projection projection) {
+        this.projection = projection;
+    }
+
+    private static Projection createGlobalIndexProjection() {
+        RowType manifestType = IndexManifestEntry.MANIFEST_ROW_TYPE;
+        return Projection.create(
+                new RowType(
+                        false,
+                        Arrays.asList(
+                                manifestType.getField(IndexManifestEntry.KIND),
+                                
manifestType.getField(IndexManifestEntry.PARTITION),
+                                
manifestType.getField(IndexManifestEntry.BUCKET),
+                                
manifestType.getField(IndexManifestEntry.INDEX_TYPE),
+                                manifestType
+                                        
.getField(IndexManifestEntry.GLOBAL_INDEX)
+                                        .newType(
+                                                GlobalIndexMeta.SCHEMA.project(
+                                                        
GlobalIndexMeta.ROW_RANGE_START,
+                                                        
GlobalIndexMeta.ROW_RANGE_END,
+                                                        
GlobalIndexMeta.INDEX_FIELD_ID,
+                                                        
GlobalIndexMeta.EXTRA_FIELD_IDS)))));
+    }
+
+    BinaryIndexManifestEntry replace(InternalRow row) {
+        checkArgument(row != null, "Index manifest row cannot be null.");
+        checkArgument(
+                row.getFieldCount() == 
projection.projectedType.getFieldCount(),
+                "Index manifest row field count %s does not match projected 
field count %s.",
+                row.getFieldCount(),
+                projection.projectedType.getFieldCount());
+        this.row = row;
+        return this;
+    }
+
+    void clear() {
+        row = null;
+    }
+
+    public boolean isAdd() {
+        return current().getByte(requiredPosition(projection.kindPosition, 
IndexManifestEntry.KIND))
+                == FileKind.ADD.toByteValue();
+    }
+
+    public boolean isDelete() {
+        return current().getByte(requiredPosition(projection.kindPosition, 
IndexManifestEntry.KIND))
+                == FileKind.DELETE.toByteValue();
+    }
+
+    public byte[] partitionBytes() {
+        byte[] partition =
+                current()
+                        .getBinary(
+                                requiredPosition(
+                                        projection.partitionPosition,
+                                        IndexManifestEntry.PARTITION));
+        checkState(partition != null, "Serialized index manifest partition 
cannot be null.");
+        return partition;
+    }
+
+    public int bucket() {
+        return current()
+                .getInt(requiredPosition(projection.bucketPosition, 
IndexManifestEntry.BUCKET));
+    }
+
+    public BinaryString indexType() {
+        BinaryString indexType =
+                current()
+                        .getString(
+                                requiredPosition(
+                                        projection.indexTypePosition,
+                                        IndexManifestEntry.INDEX_TYPE));
+        checkState(indexType != null, "Index type cannot be null.");
+        return indexType;
+    }
+
+    public boolean hasGlobalIndexMeta() {
+        return !current()
+                .isNullAt(
+                        requiredPosition(
+                                projection.globalIndexPosition, 
IndexManifestEntry.GLOBAL_INDEX));
+    }
+
+    public long rowRangeStart() {
+        return globalIndex()
+                .getLong(
+                        requiredPosition(
+                                projection.rowRangeStartPosition, 
GlobalIndexMeta.ROW_RANGE_START));
+    }
+
+    public long rowRangeEnd() {
+        return globalIndex()
+                .getLong(
+                        requiredPosition(
+                                projection.rowRangeEndPosition, 
GlobalIndexMeta.ROW_RANGE_END));
+    }
+
+    public int indexFieldId() {
+        return globalIndex()
+                .getInt(
+                        requiredPosition(
+                                projection.indexFieldIdPosition, 
GlobalIndexMeta.INDEX_FIELD_ID));
+    }
+
+    public boolean hasExtraFields() {
+        int position =
+                requiredPosition(projection.extraFieldIdsPosition, 
GlobalIndexMeta.EXTRA_FIELD_IDS);
+        InternalRow global = globalIndex();
+        return !global.isNullAt(position) && global.getArray(position).size() 
> 0;
+    }
+
+    private InternalRow globalIndex() {
+        InternalRow global =
+                current()
+                        .getRow(
+                                requiredPosition(
+                                        projection.globalIndexPosition,
+                                        IndexManifestEntry.GLOBAL_INDEX),
+                                projection.projectedGlobalIndexFieldCount);
+        checkState(global != null, "Global index metadata is not present.");
+        return global;
+    }
+
+    private InternalRow current() {
+        checkState(row != null, "Binary index manifest entry is not backed by 
a row.");
+        return row;
+    }
+
+    private static int requiredPosition(int position, String fieldName) {
+        if (position < 0) {
+            throw new UnsupportedOperationException(
+                    String.format(
+                            "The selected binary index manifest projection 
does not contain %s.",
+                            fieldName));
+        }
+        return position;
+    }
+
+    /** Projected index manifest schema together with its bound binary field 
layout. */
+    public static final class Projection {
+
+        private final RowType projectedType;
+        private final int kindPosition;
+        private final int partitionPosition;
+        private final int bucketPosition;
+        private final int indexTypePosition;
+        private final int globalIndexPosition;
+        private final int projectedGlobalIndexFieldCount;
+        private final int rowRangeStartPosition;
+        private final int rowRangeEndPosition;
+        private final int indexFieldIdPosition;
+        private final int extraFieldIdsPosition;
+
+        private Projection(
+                RowType projectedType,
+                int kindPosition,
+                int partitionPosition,
+                int bucketPosition,
+                int indexTypePosition,
+                int globalIndexPosition,
+                int projectedGlobalIndexFieldCount,
+                int rowRangeStartPosition,
+                int rowRangeEndPosition,
+                int indexFieldIdPosition,
+                int extraFieldIdsPosition) {
+            this.projectedType = projectedType;
+            this.kindPosition = kindPosition;
+            this.partitionPosition = partitionPosition;
+            this.bucketPosition = bucketPosition;
+            this.indexTypePosition = indexTypePosition;
+            this.globalIndexPosition = globalIndexPosition;
+            this.projectedGlobalIndexFieldCount = 
projectedGlobalIndexFieldCount;
+            this.rowRangeStartPosition = rowRangeStartPosition;
+            this.rowRangeEndPosition = rowRangeEndPosition;
+            this.indexFieldIdPosition = indexFieldIdPosition;
+            this.extraFieldIdsPosition = extraFieldIdsPosition;
+        }
+
+        public static Projection create(RowType projectedType) {
+            checkArgument(projectedType != null, "Projected index manifest 
type cannot be null.");
+            validateProjection(projectedType);
+
+            int globalIndexPosition = 
projectedType.getFieldIndex(IndexManifestEntry.GLOBAL_INDEX);
+            int projectedGlobalIndexFieldCount = 0;
+            int rowRangeStartPosition = -1;
+            int rowRangeEndPosition = -1;
+            int indexFieldIdPosition = -1;
+            int extraFieldIdsPosition = -1;
+            if (globalIndexPosition >= 0) {
+                RowType globalIndexType =
+                        (RowType) 
projectedType.getFields().get(globalIndexPosition).type();
+                projectedGlobalIndexFieldCount = 
globalIndexType.getFieldCount();
+                rowRangeStartPosition =
+                        
globalIndexType.getFieldIndex(GlobalIndexMeta.ROW_RANGE_START);
+                rowRangeEndPosition = 
globalIndexType.getFieldIndex(GlobalIndexMeta.ROW_RANGE_END);
+                indexFieldIdPosition =
+                        
globalIndexType.getFieldIndex(GlobalIndexMeta.INDEX_FIELD_ID);
+                extraFieldIdsPosition =
+                        
globalIndexType.getFieldIndex(GlobalIndexMeta.EXTRA_FIELD_IDS);
+            }
+
+            return new Projection(
+                    projectedType,
+                    projectedType.getFieldIndex(IndexManifestEntry.KIND),
+                    projectedType.getFieldIndex(IndexManifestEntry.PARTITION),
+                    projectedType.getFieldIndex(IndexManifestEntry.BUCKET),
+                    projectedType.getFieldIndex(IndexManifestEntry.INDEX_TYPE),
+                    globalIndexPosition,
+                    projectedGlobalIndexFieldCount,
+                    rowRangeStartPosition,
+                    rowRangeEndPosition,
+                    indexFieldIdPosition,
+                    extraFieldIdsPosition);
+        }
+
+        private static void validateProjection(RowType projectedType) {
+            for (DataField projectedField : projectedType.getFields()) {
+                checkArgument(
+                        
IndexManifestEntry.MANIFEST_ROW_TYPE.containsField(projectedField.id()),
+                        "Unknown projected index manifest field '%s' (id %s).",
+                        projectedField.name(),
+                        projectedField.id());
+                DataField manifestField =
+                        
IndexManifestEntry.MANIFEST_ROW_TYPE.getField(projectedField.id());
+                checkArgument(
+                        projectedField.isPrunedFrom(manifestField),
+                        "Projected index manifest field '%s' does not match 
%s.",
+                        projectedField.name(),
+                        manifestField);
+            }
+        }
+
+        RowType projectedType() {
+            return projectedType;
+        }
+
+        public BinaryIndexManifestEntry createEntry() {
+            return new BinaryIndexManifestEntry(this);
+        }
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntry.java 
b/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntry.java
index 10ac3a86a2..f69716d455 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntry.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntry.java
@@ -45,23 +45,37 @@ import static 
org.apache.paimon.utils.SerializationUtils.newStringType;
 @Public
 public class IndexManifestEntry {
 
+    public static final String KIND = "_KIND";
+    public static final String PARTITION = "_PARTITION";
+    public static final String BUCKET = "_BUCKET";
+    public static final String INDEX_TYPE = "_INDEX_TYPE";
+    public static final String FILE_NAME = "_FILE_NAME";
+    public static final String FILE_SIZE = "_FILE_SIZE";
+    public static final String ROW_COUNT = "_ROW_COUNT";
+    public static final String DELETION_VECTORS_RANGES = 
"_DELETIONS_VECTORS_RANGES";
+    public static final String EXTERNAL_PATH = "_EXTERNAL_PATH";
+    public static final String GLOBAL_INDEX = "_GLOBAL_INDEX";
+
     public static final RowType SCHEMA =
             new RowType(
                     false,
                     Arrays.asList(
-                            new DataField(0, "_KIND", new TinyIntType(false)),
-                            new DataField(1, "_PARTITION", 
newBytesType(false)),
-                            new DataField(2, "_BUCKET", new IntType(false)),
-                            new DataField(3, "_INDEX_TYPE", 
newStringType(false)),
-                            new DataField(4, "_FILE_NAME", 
newStringType(false)),
-                            new DataField(5, "_FILE_SIZE", new 
BigIntType(false)),
-                            new DataField(6, "_ROW_COUNT", new 
BigIntType(false)),
+                            new DataField(0, KIND, new TinyIntType(false)),
+                            new DataField(1, PARTITION, newBytesType(false)),
+                            new DataField(2, BUCKET, new IntType(false)),
+                            new DataField(3, INDEX_TYPE, newStringType(false)),
+                            new DataField(4, FILE_NAME, newStringType(false)),
+                            new DataField(5, FILE_SIZE, new BigIntType(false)),
+                            new DataField(6, ROW_COUNT, new BigIntType(false)),
                             new DataField(
                                     7,
-                                    "_DELETIONS_VECTORS_RANGES",
+                                    DELETION_VECTORS_RANGES,
                                     new ArrayType(true, 
DeletionVectorMeta.SCHEMA)),
-                            new DataField(8, "_EXTERNAL_PATH", 
newStringType(true)),
-                            new DataField(9, "_GLOBAL_INDEX", 
GlobalIndexMeta.SCHEMA)));
+                            new DataField(8, EXTERNAL_PATH, 
newStringType(true)),
+                            new DataField(9, GLOBAL_INDEX, 
GlobalIndexMeta.SCHEMA)));
+
+    public static final RowType MANIFEST_ROW_TYPE =
+            ManifestSchemaUtils.withFormatIdentifier(SCHEMA);
 
     private final FileKind kind;
     private final BinaryRow partition;
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntrySerializer.java
 
b/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntrySerializer.java
index 496ee70835..c37bb77a00 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntrySerializer.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntrySerializer.java
@@ -46,7 +46,7 @@ public class IndexManifestEntrySerializer extends 
ObjectSerializer<IndexManifest
     private static final int FORMAT_IDENTIFIER = 1;
 
     public IndexManifestEntrySerializer() {
-        
super(ManifestSchemaUtils.withFormatIdentifier(IndexManifestEntry.SCHEMA));
+        super(IndexManifestEntry.MANIFEST_ROW_TYPE);
     }
 
     @Override
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestFile.java 
b/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestFile.java
index b4e7240aea..8a312f59cf 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestFile.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestFile.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.manifest;
 
+import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.format.FileFormat;
 import org.apache.paimon.format.FormatReaderFactory;
 import org.apache.paimon.format.FormatWriterFactory;
@@ -25,21 +26,30 @@ import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.table.BucketMode;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.CloseableIterator;
 import org.apache.paimon.utils.FileStorePathFactory;
+import org.apache.paimon.utils.FileUtils;
 import org.apache.paimon.utils.ObjectsFile;
 import org.apache.paimon.utils.PathFactory;
 import org.apache.paimon.utils.SegmentsCache;
 
 import javax.annotation.Nullable;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /** Index manifest file. */
 public class IndexManifestFile extends ObjectsFile<IndexManifestEntry> {
 
+    private final FileFormat fileFormat;
+    private final RowType manifestType;
+
     private IndexManifestFile(
             FileIO fileIO,
+            FileFormat fileFormat,
             RowType schema,
             FormatReaderFactory readerFactory,
             FormatWriterFactory writerFactory,
@@ -55,12 +65,64 @@ public class IndexManifestFile extends 
ObjectsFile<IndexManifestEntry> {
                 compression,
                 pathFactory,
                 cache);
+        this.fileFormat = fileFormat;
+        this.manifestType = schema;
     }
 
     public Path indexManifestFilePath(String fileName) {
         return pathFactory.toPath(fileName);
     }
 
+    /**
+     * Scans projected index manifest entries without materializing {@link 
IndexManifestEntry}s.
+     *
+     * <p>The returned iterator reuses the same mutable {@link 
BinaryIndexManifestEntry} for all
+     * records. An entry is only valid until the next call to {@link 
CloseableIterator#hasNext()},
+     * {@link CloseableIterator#next()}, or {@link CloseableIterator#close()}, 
and must not be
+     * retained. The caller must close the iterator.
+     *
+     * <p>This method intentionally bypasses the manifest cache because cached 
entries are
+     * materialized with the complete index manifest schema.
+     */
+    public CloseableIterator<BinaryIndexManifestEntry> scan(
+            String fileName, BinaryIndexManifestEntry.Projection projection) {
+        BinaryIndexManifestEntry entry = projection.createEntry();
+        try {
+            CloseableIterator<InternalRow> rows =
+                    FileUtils.createFormatReader(
+                                    fileIO,
+                                    fileFormat.createReaderFactory(
+                                            manifestType,
+                                            projection.projectedType(),
+                                            Collections.emptyList()),
+                                    pathFactory.toPath(fileName),
+                                    null)
+                            .toCloseableIterator();
+            return new CloseableIterator<BinaryIndexManifestEntry>() {
+                @Override
+                public boolean hasNext() {
+                    entry.clear();
+                    return rows.hasNext();
+                }
+
+                @Override
+                public BinaryIndexManifestEntry next() {
+                    entry.clear();
+                    InternalRow row = rows.next();
+                    return row == null ? null : entry.replace(row);
+                }
+
+                @Override
+                public void close() throws Exception {
+                    entry.clear();
+                    rows.close();
+                }
+            };
+        } catch (IOException e) {
+            throw new UncheckedIOException("Failed to read index manifest " + 
fileName, e);
+        }
+    }
+
     /** Write new index files to index manifest. */
     @Nullable
     public String writeIndexFiles(
@@ -97,9 +159,10 @@ public class IndexManifestFile extends 
ObjectsFile<IndexManifestEntry> {
         }
 
         public IndexManifestFile create() {
-            RowType schema = 
ManifestSchemaUtils.withFormatIdentifier(IndexManifestEntry.SCHEMA);
+            RowType schema = IndexManifestEntry.MANIFEST_ROW_TYPE;
             return new IndexManifestFile(
                     fileIO,
+                    fileFormat,
                     schema,
                     fileFormat.createReaderFactory(schema, schema, new 
ArrayList<>()),
                     fileFormat.createWriterFactory(schema),
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/index/IndexFileHandlerTest.java 
b/paimon-core/src/test/java/org/apache/paimon/index/IndexFileHandlerTest.java
index 70d5881e34..dbb759d794 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/index/IndexFileHandlerTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/index/IndexFileHandlerTest.java
@@ -27,11 +27,13 @@ import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.io.CompactIncrement;
 import org.apache.paimon.io.DataIncrement;
+import org.apache.paimon.manifest.BinaryIndexManifestEntry;
 import org.apache.paimon.manifest.FileKind;
 import org.apache.paimon.manifest.IndexManifestEntry;
 import org.apache.paimon.options.MemorySize;
 import org.apache.paimon.table.sink.CommitMessageImpl;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.CloseableIterator;
 import org.apache.paimon.utils.FileStorePathFactory;
 import org.apache.paimon.utils.IndexFilePathFactories;
 import org.apache.paimon.utils.Pair;
@@ -149,6 +151,18 @@ public class IndexFileHandlerTest {
 
         Snapshot snapshot = store.snapshotManager().latestSnapshot();
         IndexFileHandler indexFileHandler = store.newIndexFileHandler();
+        int binaryEntryCount = 0;
+        try (CloseableIterator<BinaryIndexManifestEntry> entries =
+                indexFileHandler.scan(snapshot, 
BinaryIndexManifestEntry.GLOBAL_INDEX_PROJECTION)) {
+            while (entries.hasNext()) {
+                BinaryIndexManifestEntry entry = entries.next();
+                assertThat(entry.isAdd()).isTrue();
+                assertThat(entry.indexType()).isNotNull();
+                binaryEntryCount++;
+            }
+        }
+        assertThat(binaryEntryCount).isEqualTo(3);
+
         assertThat(
                         indexFileHandler.scanBuckets(
                                 snapshot, DELETION_VECTORS_INDEX, 
Collections.emptySet()))
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/manifest/BinaryIndexManifestEntryTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/manifest/BinaryIndexManifestEntryTest.java
new file mode 100644
index 0000000000..02d952ed5e
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/manifest/BinaryIndexManifestEntryTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.paimon.manifest;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.index.GlobalIndexMeta;
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.TableTestBase;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.CloseableIterator;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.apache.paimon.utils.SerializationUtils.deserializeBinaryRow;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link BinaryIndexManifestEntry}. */
+class BinaryIndexManifestEntryTest extends TableTestBase {
+
+    @Test
+    void testProjectedScanAndReusableEntry() throws Exception {
+        createTableDefault();
+        FileStoreTable table = getTableDefault();
+        IndexManifestFile indexManifestFile = 
table.store().indexManifestFileFactory().create();
+
+        BinaryRow firstPartition = partition(1);
+        BinaryRow secondPartition = partition(2);
+        IndexManifestEntry add =
+                entry(
+                        FileKind.ADD,
+                        firstPartition,
+                        3,
+                        "btree",
+                        new GlobalIndexMeta(10, 19, 1, new int[] {2}, null));
+        IndexManifestEntry delete =
+                entry(FileKind.DELETE, secondPartition, 4, "deletion-vector", 
null);
+        String fileName = 
indexManifestFile.writeWithoutRolling(Arrays.asList(add, delete));
+
+        try (CloseableIterator<BinaryIndexManifestEntry> entries =
+                indexManifestFile.scan(
+                        fileName, 
BinaryIndexManifestEntry.GLOBAL_INDEX_PROJECTION)) {
+            assertThat(entries.hasNext()).isTrue();
+            BinaryIndexManifestEntry first = entries.next();
+            assertThat(first.isAdd()).isTrue();
+            assertThat(first.isDelete()).isFalse();
+            
assertThat(deserializeBinaryRow(first.partitionBytes())).isEqualTo(firstPartition);
+            assertThat(first.bucket()).isEqualTo(3);
+            assertThat(first.indexType().toString()).isEqualTo("btree");
+            assertThat(first.hasGlobalIndexMeta()).isTrue();
+            assertThat(first.rowRangeStart()).isEqualTo(10);
+            assertThat(first.rowRangeEnd()).isEqualTo(19);
+            assertThat(first.indexFieldId()).isEqualTo(1);
+            assertThat(first.hasExtraFields()).isTrue();
+
+            assertThat(entries.hasNext()).isTrue();
+            assertThatThrownBy(first::bucket)
+                    .isInstanceOf(IllegalStateException.class)
+                    .hasMessageContaining("not backed by a row");
+
+            BinaryIndexManifestEntry second = entries.next();
+            assertThat(second).isSameAs(first);
+            assertThat(second.isAdd()).isFalse();
+            assertThat(second.isDelete()).isTrue();
+            
assertThat(deserializeBinaryRow(second.partitionBytes())).isEqualTo(secondPartition);
+            assertThat(second.bucket()).isEqualTo(4);
+            
assertThat(second.indexType().toString()).isEqualTo("deletion-vector");
+            assertThat(second.hasGlobalIndexMeta()).isFalse();
+            assertThatThrownBy(second::rowRangeStart)
+                    .isInstanceOf(IllegalStateException.class)
+                    .hasMessageContaining("not present");
+
+            assertThat(entries.hasNext()).isFalse();
+            assertThatThrownBy(second::bucket)
+                    .isInstanceOf(IllegalStateException.class)
+                    .hasMessageContaining("not backed by a row");
+        }
+    }
+
+    @Test
+    void testCustomProjectionAndOrdering() {
+        RowType manifestType = IndexManifestEntry.MANIFEST_ROW_TYPE;
+        BinaryIndexManifestEntry entry =
+                BinaryIndexManifestEntry.Projection.create(
+                                new RowType(
+                                        false,
+                                        Arrays.asList(
+                                                manifestType.getField(
+                                                        
IndexManifestEntry.INDEX_TYPE),
+                                                
manifestType.getField(IndexManifestEntry.BUCKET),
+                                                
manifestType.getField(IndexManifestEntry.KIND))))
+                        .createEntry()
+                        .replace(
+                                GenericRow.of(
+                                        BinaryString.fromString("btree"),
+                                        3,
+                                        FileKind.ADD.toByteValue()));
+
+        assertThat(entry.indexType().toString()).isEqualTo("btree");
+        assertThat(entry.bucket()).isEqualTo(3);
+        assertThat(entry.isAdd()).isTrue();
+        assertThatThrownBy(entry::partitionBytes)
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining(IndexManifestEntry.PARTITION);
+        assertThatThrownBy(entry::hasGlobalIndexMeta)
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining(IndexManifestEntry.GLOBAL_INDEX);
+    }
+
+    private static IndexManifestEntry entry(
+            FileKind kind,
+            BinaryRow partition,
+            int bucket,
+            String indexType,
+            GlobalIndexMeta globalIndexMeta) {
+        return new IndexManifestEntry(
+                kind,
+                partition,
+                bucket,
+                new IndexFileMeta(indexType, "index-file", 100, 10, 
globalIndexMeta, null));
+    }
+
+    private static BinaryRow partition(int value) {
+        BinaryRow row = new BinaryRow(1);
+        BinaryRowWriter writer = new BinaryRowWriter(row);
+        writer.writeInt(0, value);
+        writer.complete();
+        return row;
+    }
+}

Reply via email to