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 4dddb86aca [core] Wire sorted primary-key indexes into writes (#8602)
4dddb86aca is described below

commit 4dddb86acaf46afa9f98acc74e6cd5e57fcb1684
Author: Jingsong Lee <[email protected]>
AuthorDate: Mon Jul 13 22:30:53 2026 +0800

    [core] Wire sorted primary-key indexes into writes (#8602)
    
    Wire source-backed BTree and Bitmap indexes into the primary-key table
    write lifecycle.
---
 .../java/org/apache/paimon/KeyValueFileStore.java  |  19 +-
 .../paimon/globalindex/GlobalIndexCoverage.java    |   3 +
 .../paimon/globalindex/GlobalIndexScanner.java     |  18 +-
 .../org/apache/paimon/index/IndexFileHandler.java  |   5 +
 .../pk/BucketedPrimaryKeyIndexMaintainer.java      | 327 +++++++++++++++--
 .../pksorted/BucketedSortedIndexMaintainer.java    |  28 +-
 .../pkvector/BucketedVectorIndexMaintainer.java    |  35 +-
 .../pk/BucketedPrimaryKeyIndexMaintainerTest.java  | 351 ++++++++++++++++++
 .../PrimaryKeySortedIndexMaintenanceTest.java      | 396 +++++++++++++++++++++
 .../BucketedVectorIndexMaintainerTest.java         |  86 +++++
 .../paimon/operation/PrimaryKeyIndexWriteTest.java | 197 ++++++++++
 .../paimon/table/BtreeGlobalIndexTableTest.java    |  59 +++
 12 files changed, 1474 insertions(+), 50 deletions(-)

diff --git a/paimon-core/src/main/java/org/apache/paimon/KeyValueFileStore.java 
b/paimon-core/src/main/java/org/apache/paimon/KeyValueFileStore.java
index 8e0d4da7e2..7531f91a77 100644
--- a/paimon-core/src/main/java/org/apache/paimon/KeyValueFileStore.java
+++ b/paimon-core/src/main/java/org/apache/paimon/KeyValueFileStore.java
@@ -25,7 +25,6 @@ import org.apache.paimon.format.FileFormatDiscover;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.index.DynamicBucketIndexMaintainer;
 import org.apache.paimon.index.pk.BucketedPrimaryKeyIndexMaintainer;
-import org.apache.paimon.index.pkvector.BucketedVectorIndexMaintainer;
 import org.apache.paimon.io.KeyValueFileReaderFactory;
 import org.apache.paimon.mergetree.compact.MergeFunctionFactory;
 import org.apache.paimon.operation.AbstractFileStoreWrite;
@@ -40,7 +39,6 @@ import org.apache.paimon.schema.SchemaManager;
 import org.apache.paimon.schema.TableSchema;
 import org.apache.paimon.table.BucketMode;
 import org.apache.paimon.table.CatalogEnvironment;
-import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.KeyComparatorSupplier;
 import org.apache.paimon.utils.UserDefinedSeqComparator;
@@ -173,19 +171,12 @@ public class KeyValueFileStore extends 
AbstractFileStore<KeyValue> {
             dvMaintainerFactory = 
BucketedDvMaintainer.factory(newIndexFileHandler());
         }
         BucketedPrimaryKeyIndexMaintainer.Factory 
primaryKeyIndexMaintainerFactory = null;
-        if (options.primaryKeyVectorIndexEnabled()) {
-            String vectorColumn = options.primaryKeyVectorIndexColumn();
-            DataField vectorField = schema.nameToFieldMap().get(vectorColumn);
+        if (options.primaryKeyVectorIndexEnabled()
+                || !options.primaryKeyBTreeIndexColumns().isEmpty()
+                || !options.primaryKeyBitmapIndexColumns().isEmpty()) {
             primaryKeyIndexMaintainerFactory =
-                    BucketedPrimaryKeyIndexMaintainer.Factory.ofVector(
-                            new BucketedVectorIndexMaintainer.Factory(
-                                            newIndexFileHandler(),
-                                            newReaderFactoryBuilder(),
-                                            vectorField,
-                                            
options.primaryKeyVectorDistanceMetric(vectorColumn),
-                                            
options.primaryKeyVectorIndexType(vectorColumn))
-                                    .withIndexOptions(
-                                            
options.primaryKeyVectorIndexOptions(vectorColumn)));
+                    BucketedPrimaryKeyIndexMaintainer.Factory.create(
+                            newIndexFileHandler(), newReaderFactoryBuilder(), 
schema);
         }
         return new KeyValueFileStoreWrite(
                 fileIO,
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexCoverage.java
 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexCoverage.java
index 5191d0f942..6353b6616e 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexCoverage.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexCoverage.java
@@ -64,6 +64,9 @@ public class GlobalIndexCoverage {
         this.coverageByField = new HashMap<>();
         for (IndexFileMeta indexFile : indexFiles) {
             GlobalIndexMeta meta = checkNotNull(indexFile.globalIndexMeta());
+            if (meta.sourceMeta() != null) {
+                continue;
+            }
             Range range = new Range(meta.rowRangeStart(), meta.rowRangeEnd());
             addCoverage(meta.indexFieldId(), range);
             if (meta.extraFieldIds() != null) {
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
index 7edb68365b..1385970b24 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
@@ -187,7 +187,8 @@ public class GlobalIndexScanner implements Closeable {
             FileStoreTable table,
             @Nullable PartitionPredicate partitionFilter,
             Collection<IndexFileMeta> indexFiles) {
-        if (indexFiles.isEmpty()) {
+        List<IndexFileMeta> ordinaryIndexFiles = 
ordinaryGlobalIndexFiles(indexFiles);
+        if (ordinaryIndexFiles.isEmpty()) {
             return Optional.empty();
         }
         return Optional.of(
@@ -199,7 +200,7 @@ public class GlobalIndexScanner implements Closeable {
                         table.rowType(),
                         table.fileIO(),
                         table.store().pathFactory().globalIndexFileFactory(),
-                        indexFiles));
+                        ordinaryIndexFiles));
     }
 
     public static Optional<GlobalIndexScanner> create(
@@ -241,7 +242,7 @@ public class GlobalIndexScanner implements Closeable {
                         return false;
                     }
                     GlobalIndexMeta globalIndex = 
entry.indexFile().globalIndexMeta();
-                    if (globalIndex == null) {
+                    if (globalIndex == null || globalIndex.sourceMeta() != 
null) {
                         return false;
                     }
                     // Collect indexes whose primary column is filtered, and 
also multi-column
@@ -261,6 +262,17 @@ public class GlobalIndexScanner implements Closeable {
         return indexFileFilter;
     }
 
+    private static List<IndexFileMeta> ordinaryGlobalIndexFiles(
+            Collection<IndexFileMeta> indexFiles) {
+        return indexFiles.stream()
+                .filter(
+                        indexFile -> {
+                            GlobalIndexMeta meta = indexFile.globalIndexMeta();
+                            return meta != null && meta.sourceMeta() == null;
+                        })
+                .collect(Collectors.toList());
+    }
+
     public Optional<GlobalIndexResult> scan(Predicate predicate) {
         return globalIndexEvaluator.evaluate(predicate);
     }
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 799aa3ef66..9fafedb7aa 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
@@ -25,6 +25,7 @@ import org.apache.paimon.deletionvectors.DeletionVector;
 import org.apache.paimon.deletionvectors.DeletionVectorsIndexFile;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.Path;
+import org.apache.paimon.index.pksorted.PkSortedIndexFile;
 import org.apache.paimon.index.pkvector.PkVectorAnnSegmentFile;
 import org.apache.paimon.manifest.IndexManifestEntry;
 import org.apache.paimon.manifest.IndexManifestEntrySerializer;
@@ -89,6 +90,10 @@ public class IndexFileHandler {
         return new PkVectorAnnSegmentFile(fileIO, pathFactories.get(partition, 
bucket));
     }
 
+    public PkSortedIndexFile pkSortedIndex(BinaryRow partition, int bucket) {
+        return new PkSortedIndexFile(fileIO, pathFactories.get(partition, 
bucket));
+    }
+
     public Optional<IndexFileMeta> scanHashIndex(
             Snapshot snapshot, BinaryRow partition, int bucket) {
         List<IndexFileMeta> result = scan(snapshot, HASH_INDEX, partition, 
bucket);
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
 
b/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
index 702a1dc9cf..5a8403c410 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
@@ -18,44 +18,169 @@
 
 package org.apache.paimon.index.pk;
 
+import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.index.IndexFileHandler;
 import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.index.pksorted.BucketedSortedIndexMaintainer;
+import org.apache.paimon.index.pksorted.PkSortedDataFileReader;
+import org.apache.paimon.index.pksorted.PkSortedIndexBuilder;
+import org.apache.paimon.index.pksorted.PkSortedIndexFile;
 import org.apache.paimon.index.pkvector.BucketedVectorIndexMaintainer;
 import org.apache.paimon.io.CompactIncrement;
 import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.io.DataIncrement;
+import org.apache.paimon.io.KeyValueFileReaderFactory;
+import org.apache.paimon.schema.TableSchema;
+import org.apache.paimon.types.DataField;
 
 import javax.annotation.Nullable;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutorService;
 
-/** Coordinates source-backed primary-key indexes for one bucket. */
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** Coordinates all source-backed primary-key indexes for one bucket. */
 public final class BucketedPrimaryKeyIndexMaintainer {
 
-    private final BucketedVectorIndexMaintainer vectorMaintainer;
+    @Nullable private final BucketedVectorIndexMaintainer vectorMaintainer;
+    private final List<BucketedSortedIndexMaintainer> sortedMaintainers;
 
-    private BucketedPrimaryKeyIndexMaintainer(BucketedVectorIndexMaintainer 
vectorMaintainer) {
+    private BucketedPrimaryKeyIndexMaintainer(
+            @Nullable BucketedVectorIndexMaintainer vectorMaintainer,
+            List<BucketedSortedIndexMaintainer> sortedMaintainers) {
         this.vectorMaintainer = vectorMaintainer;
+        List<BucketedSortedIndexMaintainer> sorted = new 
ArrayList<>(sortedMaintainers);
+        
sorted.sort(Comparator.comparingInt(BucketedSortedIndexMaintainer::fieldId));
+        this.sortedMaintainers = Collections.unmodifiableList(sorted);
     }
 
     public static BucketedPrimaryKeyIndexMaintainer ofVector(
             BucketedVectorIndexMaintainer vectorMaintainer) {
-        return new BucketedPrimaryKeyIndexMaintainer(vectorMaintainer);
+        return new BucketedPrimaryKeyIndexMaintainer(vectorMaintainer, 
Collections.emptyList());
+    }
+
+    public static BucketedPrimaryKeyIndexMaintainer of(
+            BucketedVectorIndexMaintainer vectorMaintainer,
+            List<BucketedSortedIndexMaintainer> sortedMaintainers) {
+        return new BucketedPrimaryKeyIndexMaintainer(vectorMaintainer, 
sortedMaintainers);
+    }
+
+    public static BucketedPrimaryKeyIndexMaintainer ofSorted(
+            List<BucketedSortedIndexMaintainer> sortedMaintainers) {
+        return new BucketedPrimaryKeyIndexMaintainer(null, sortedMaintainers);
     }
 
-    public void prepareCommit(
+    public synchronized void prepareCommit(
             DataIncrement appendIncrement,
             CompactIncrement compactIncrement,
             boolean waitCompaction)
             throws Exception {
-        BucketedVectorIndexMaintainer.VectorIndexCommit vectorCommit =
-                vectorMaintainer.prepareCommit(appendIncrement, 
compactIncrement, waitCompaction);
-        vectorCommit
-                .appendIncrement()
+        BucketedVectorIndexMaintainer.VectorIndexCommit vectorCommit = null;
+        List<BucketedSortedIndexMaintainer.SortedIndexCommit> sortedCommits = 
new ArrayList<>();
+        try {
+            if (vectorMaintainer != null) {
+                vectorCommit =
+                        vectorMaintainer.prepareCommit(
+                                appendIncrement, compactIncrement, 
waitCompaction);
+            }
+
+            if (waitCompaction) {
+                prepareSortedBlocking(appendIncrement, compactIncrement, 
sortedCommits);
+            } else {
+                prepareSortedNonBlocking(appendIncrement, compactIncrement, 
sortedCommits);
+            }
+
+            if (vectorCommit != null) {
+                mergeVectorCommit(appendIncrement, compactIncrement, 
vectorCommit);
+            }
+            for (BucketedSortedIndexMaintainer.SortedIndexCommit sortedCommit 
: sortedCommits) {
+                mergeSortedCommit(appendIncrement, compactIncrement, 
sortedCommit);
+            }
+        } catch (Throwable failure) {
+            for (int i = sortedCommits.size() - 1; i >= 0; i--) {
+                sortedCommits.get(i).abort(failure);
+            }
+            if (vectorCommit != null) {
+                vectorCommit.abort(failure);
+            }
+            if (failure instanceof Exception) {
+                throw (Exception) failure;
+            }
+            if (failure instanceof Error) {
+                throw (Error) failure;
+            }
+            throw new RuntimeException(failure);
+        }
+    }
+
+    private void prepareSortedBlocking(
+            DataIncrement appendIncrement,
+            CompactIncrement compactIncrement,
+            List<BucketedSortedIndexMaintainer.SortedIndexCommit> commits)
+            throws Exception {
+        BucketedSortedIndexMaintainer active = activeSortedMaintainer();
+        for (BucketedSortedIndexMaintainer maintainer : sortedMaintainers) {
+            commits.add(
+                    maintainer.prepareCommit(
+                            appendIncrement, compactIncrement, true, 
maintainer == active));
+        }
+        for (BucketedSortedIndexMaintainer maintainer : sortedMaintainers) {
+            if (maintainer != active) {
+                commits.add(
+                        maintainer.prepareCommit(appendIncrement, 
compactIncrement, true, true));
+            }
+        }
+    }
+
+    private void prepareSortedNonBlocking(
+            DataIncrement appendIncrement,
+            CompactIncrement compactIncrement,
+            List<BucketedSortedIndexMaintainer.SortedIndexCommit> commits)
+            throws Exception {
+        BucketedSortedIndexMaintainer active = activeSortedMaintainer();
+        for (BucketedSortedIndexMaintainer maintainer : sortedMaintainers) {
+            commits.add(
+                    maintainer.prepareCommit(
+                            appendIncrement, compactIncrement, false, 
maintainer == active));
+        }
+        if (activeSortedMaintainer() != null) {
+            return;
+        }
+
+        for (BucketedSortedIndexMaintainer maintainer : sortedMaintainers) {
+            if (maintainer == active && 
!maintainer.state().uncoveredSourceFiles().isEmpty()) {
+                continue;
+            }
+            if (!maintainer.state().uncoveredSourceFiles().isEmpty()) {
+                commits.add(
+                        maintainer.prepareCommit(appendIncrement, 
compactIncrement, false, true));
+                break;
+            }
+        }
+    }
+
+    @Nullable
+    private BucketedSortedIndexMaintainer activeSortedMaintainer() {
+        for (BucketedSortedIndexMaintainer maintainer : sortedMaintainers) {
+            if (maintainer.buildNotCompleted()) {
+                return maintainer;
+            }
+        }
+        return null;
+    }
+
+    private static void mergeSortedCommit(
+            DataIncrement appendIncrement,
+            CompactIncrement compactIncrement,
+            BucketedSortedIndexMaintainer.SortedIndexCommit commit) {
+        commit.appendIncrement()
                 .ifPresent(
                         increment ->
                                 applyIndexIncrement(
@@ -63,8 +188,29 @@ public final class BucketedPrimaryKeyIndexMaintainer {
                                         appendIncrement.deletedIndexFiles(),
                                         increment.newIndexFiles(),
                                         increment.deletedIndexFiles()));
-        vectorCommit
-                .compactIncrement()
+        commit.compactIncrement()
+                .ifPresent(
+                        increment ->
+                                applyIndexIncrement(
+                                        compactIncrement.newIndexFiles(),
+                                        compactIncrement.deletedIndexFiles(),
+                                        increment.newIndexFiles(),
+                                        increment.deletedIndexFiles()));
+    }
+
+    private static void mergeVectorCommit(
+            DataIncrement appendIncrement,
+            CompactIncrement compactIncrement,
+            BucketedVectorIndexMaintainer.VectorIndexCommit commit) {
+        commit.appendIncrement()
+                .ifPresent(
+                        increment ->
+                                applyIndexIncrement(
+                                        appendIncrement.newIndexFiles(),
+                                        appendIncrement.deletedIndexFiles(),
+                                        increment.newIndexFiles(),
+                                        increment.deletedIndexFiles()));
+        commit.compactIncrement()
                 .ifPresent(
                         increment ->
                                 applyIndexIncrement(
@@ -84,32 +230,103 @@ public final class BucketedPrimaryKeyIndexMaintainer {
     }
 
     public boolean buildNotCompleted() {
-        return vectorMaintainer.buildNotCompleted();
+        if (vectorMaintainer != null && vectorMaintainer.buildNotCompleted()) {
+            return true;
+        }
+        return activeSortedMaintainer() != null;
     }
 
     public void withExecutor(ExecutorService executor) {
-        vectorMaintainer.withExecutor(executor);
+        if (vectorMaintainer != null) {
+            vectorMaintainer.withExecutor(executor);
+        }
+        for (BucketedSortedIndexMaintainer maintainer : sortedMaintainers) {
+            maintainer.withExecutor(executor);
+        }
     }
 
     public void close() {
-        vectorMaintainer.close();
+        if (vectorMaintainer != null) {
+            vectorMaintainer.close();
+        }
+        for (BucketedSortedIndexMaintainer maintainer : sortedMaintainers) {
+            maintainer.close();
+        }
     }
 
-    /** Factory to restore configured source-backed primary-key indexes for a 
bucket. */
+    /** Factory to restore all configured source-backed indexes for a bucket. 
*/
     public static final class Factory {
 
-        private final BucketedVectorIndexMaintainer.Factory vectorFactory;
+        private final IndexFileHandler handler;
+        @Nullable private final BucketedVectorIndexMaintainer.Factory 
vectorFactory;
+        private final List<SortedDefinitionFactory> sortedFactories;
 
-        private Factory(BucketedVectorIndexMaintainer.Factory vectorFactory) {
+        private Factory(
+                IndexFileHandler handler,
+                @Nullable BucketedVectorIndexMaintainer.Factory vectorFactory,
+                List<SortedDefinitionFactory> sortedFactories) {
+            this.handler = handler;
             this.vectorFactory = vectorFactory;
+            this.sortedFactories = Collections.unmodifiableList(new 
ArrayList<>(sortedFactories));
         }
 
         public static Factory ofVector(BucketedVectorIndexMaintainer.Factory 
vectorFactory) {
-            return new Factory(vectorFactory);
+            return new Factory(
+                    vectorFactory.indexFileHandler(), vectorFactory, 
Collections.emptyList());
+        }
+
+        public static Factory create(
+                IndexFileHandler handler,
+                KeyValueFileReaderFactory.Builder readerFactoryBuilder,
+                TableSchema schema) {
+            CoreOptions coreOptions = new CoreOptions(schema.options());
+            Map<String, DataField> fields = schema.nameToFieldMap();
+            BucketedVectorIndexMaintainer.Factory vectorFactory = null;
+            List<SortedDefinitionFactory> sortedFactories = new ArrayList<>();
+            for (PrimaryKeyIndexDefinition definition :
+                    PrimaryKeyIndexDefinitions.create(schema).definitions()) {
+                DataField field = fields.get(definition.column());
+                checkArgument(
+                        field != null,
+                        "Primary-key index column '%s' does not exist.",
+                        definition.column());
+                switch (definition.family()) {
+                    case VECTOR:
+                        checkArgument(
+                                vectorFactory == null,
+                                "Only one primary-key vector index is 
supported.");
+                        vectorFactory =
+                                new BucketedVectorIndexMaintainer.Factory(
+                                                handler,
+                                                readerFactoryBuilder,
+                                                field,
+                                                
coreOptions.primaryKeyVectorDistanceMetric(
+                                                        definition.column()),
+                                                definition.indexType())
+                                        
.withIndexOptions(definition.options());
+                        break;
+                    case BTREE:
+                    case BITMAP:
+                        sortedFactories.add(
+                                new SortedDefinitionFactory(
+                                        readerFactoryBuilder,
+                                        field,
+                                        definition.indexType(),
+                                        definition.options()));
+                        break;
+                    default:
+                        throw new IllegalArgumentException(
+                                "Unsupported primary-key index family " + 
definition.family());
+                }
+            }
+            checkArgument(
+                    vectorFactory != null || !sortedFactories.isEmpty(),
+                    "No primary-key index definition is configured.");
+            return new Factory(handler, vectorFactory, sortedFactories);
         }
 
         public IndexFileHandler indexFileHandler() {
-            return vectorFactory.indexFileHandler();
+            return handler;
         }
 
         public BucketedPrimaryKeyIndexMaintainer create(
@@ -118,12 +335,7 @@ public final class BucketedPrimaryKeyIndexMaintainer {
                 @Nullable List<DataFileMeta> restoredDataFiles,
                 @Nullable List<IndexFileMeta> restoredPayloads,
                 ExecutorService executor) {
-            List<DataFileMeta> dataFiles =
-                    restoredDataFiles == null ? Collections.emptyList() : 
restoredDataFiles;
-            List<IndexFileMeta> payloads =
-                    restoredPayloads == null ? Collections.emptyList() : 
restoredPayloads;
-            return BucketedPrimaryKeyIndexMaintainer.ofVector(
-                    vectorFactory.create(partition, bucket, dataFiles, 
payloads, executor));
+            return create(partition, bucket, restoredDataFiles, 
restoredPayloads, executor, null);
         }
 
         public BucketedPrimaryKeyIndexMaintainer create(
@@ -133,7 +345,70 @@ public final class BucketedPrimaryKeyIndexMaintainer {
                 @Nullable List<IndexFileMeta> restoredPayloads,
                 ExecutorService executor,
                 @Nullable IOManager ioManager) {
-            return create(partition, bucket, restoredDataFiles, 
restoredPayloads, executor);
+            List<DataFileMeta> dataFiles =
+                    restoredDataFiles == null ? Collections.emptyList() : 
restoredDataFiles;
+            List<IndexFileMeta> payloads =
+                    restoredPayloads == null ? Collections.emptyList() : 
restoredPayloads;
+            BucketedVectorIndexMaintainer vector =
+                    vectorFactory == null
+                            ? null
+                            : vectorFactory.create(
+                                    partition, bucket, dataFiles, payloads, 
executor);
+            List<BucketedSortedIndexMaintainer> sorted = new ArrayList<>();
+            for (SortedDefinitionFactory factory : sortedFactories) {
+                sorted.add(
+                        factory.create(
+                                handler, partition, bucket, dataFiles, 
payloads, executor,
+                                ioManager));
+            }
+            return new BucketedPrimaryKeyIndexMaintainer(vector, sorted);
+        }
+
+        private static final class SortedDefinitionFactory {
+
+            private final KeyValueFileReaderFactory.Builder 
readerFactoryBuilder;
+            private final DataField field;
+            private final String indexType;
+            private final org.apache.paimon.options.Options options;
+
+            private SortedDefinitionFactory(
+                    KeyValueFileReaderFactory.Builder readerFactoryBuilder,
+                    DataField field,
+                    String indexType,
+                    org.apache.paimon.options.Options options) {
+                this.readerFactoryBuilder = readerFactoryBuilder;
+                this.field = field;
+                this.indexType = indexType;
+                this.options = options;
+            }
+
+            private BucketedSortedIndexMaintainer create(
+                    IndexFileHandler handler,
+                    BinaryRow partition,
+                    int bucket,
+                    List<DataFileMeta> restoredDataFiles,
+                    List<IndexFileMeta> restoredPayloads,
+                    ExecutorService executor,
+                    @Nullable IOManager ioManager) {
+                PkSortedIndexFile indexFile = handler.pkSortedIndex(partition, 
bucket);
+                PkSortedIndexBuilder builder =
+                        new PkSortedIndexBuilder(
+                                new PkSortedDataFileReader.Factory(
+                                        readerFactoryBuilder, partition, 
bucket, field),
+                                indexFile,
+                                field,
+                                indexType,
+                                options,
+                                ioManager);
+                return new BucketedSortedIndexMaintainer(
+                        field.id(),
+                        indexType,
+                        indexFile,
+                        builder::build,
+                        restoredDataFiles,
+                        restoredPayloads,
+                        executor);
+            }
         }
     }
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/pksorted/BucketedSortedIndexMaintainer.java
 
b/paimon-core/src/main/java/org/apache/paimon/index/pksorted/BucketedSortedIndexMaintainer.java
index 6a887f470d..44695b5f54 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/index/pksorted/BucketedSortedIndexMaintainer.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/index/pksorted/BucketedSortedIndexMaintainer.java
@@ -169,7 +169,16 @@ public class BucketedSortedIndexMaintainer {
                     changed && hasCompactDataTransition
                             ? Optional.of(new SortedIndexIncrement(created, 
removed))
                             : Optional.empty();
-            return new SortedIndexCommit(appendChange, compactChange);
+            return new SortedIndexCommit(
+                    appendChange,
+                    compactChange,
+                    failure ->
+                            rollbackPrepareCommit(
+                                    originalSourceFiles,
+                                    originalGroups,
+                                    originalRestoredDeletions,
+                                    created,
+                                    failure));
         } catch (Throwable failure) {
             rollbackPrepareCommit(
                     originalSourceFiles,
@@ -187,7 +196,7 @@ public class BucketedSortedIndexMaintainer {
         }
     }
 
-    private void rollbackPrepareCommit(
+    private synchronized void rollbackPrepareCommit(
             Map<String, DataFileMeta> originalSourceFiles,
             List<PkSortedIndexGroup> originalGroups,
             List<IndexFileMeta> originalRestoredDeletions,
@@ -498,12 +507,15 @@ public class BucketedSortedIndexMaintainer {
 
         private final Optional<SortedIndexIncrement> appendIncrement;
         private final Optional<SortedIndexIncrement> compactIncrement;
+        private final AbortAction abortAction;
 
         private SortedIndexCommit(
                 Optional<SortedIndexIncrement> appendIncrement,
-                Optional<SortedIndexIncrement> compactIncrement) {
+                Optional<SortedIndexIncrement> compactIncrement,
+                AbortAction abortAction) {
             this.appendIncrement = appendIncrement;
             this.compactIncrement = compactIncrement;
+            this.abortAction = abortAction;
         }
 
         public Optional<SortedIndexIncrement> appendIncrement() {
@@ -513,6 +525,16 @@ public class BucketedSortedIndexMaintainer {
         public Optional<SortedIndexIncrement> compactIncrement() {
             return compactIncrement;
         }
+
+        public void abort(Throwable failure) {
+            abortAction.abort(failure);
+        }
+    }
+
+    @FunctionalInterface
+    private interface AbortAction {
+
+        void abort(Throwable failure);
     }
 
     /** Index-file additions and deletions emitted by one sorted definition. */
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
 
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
index 3b106f79c8..1e002aa787 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
@@ -93,8 +93,17 @@ public class BucketedVectorIndexMaintainer {
                         
coreOptions.primaryKeyVectorIndexCompactionLevelFanout(),
                         
coreOptions.primaryKeyVectorIndexCompactionStaleRatioThreshold());
         this.executor = executor;
+
+        List<IndexFileMeta> definitionPayloads = new ArrayList<>();
+        for (IndexFileMeta payload : restoredPayloads) {
+            if (algorithm.equals(payload.indexType())
+                    && payload.globalIndexMeta() != null
+                    && payload.globalIndexMeta().indexFieldId() == 
vectorFieldId) {
+                definitionPayloads.add(payload);
+            }
+        }
         PkVectorBucketIndexState restoredState =
-                new PkVectorBucketIndexState(vectorFieldId, algorithm, 
restoredPayloads);
+                new PkVectorBucketIndexState(vectorFieldId, algorithm, 
definitionPayloads);
         this.annSegments = new ArrayList<>(restoredState.annSegments());
         this.activeSourceFiles = new LinkedHashMap<>();
         for (DataFileMeta file : restoredDataFiles) {
@@ -183,7 +192,12 @@ public class BucketedVectorIndexMaintainer {
                     !indexChanged || !hasCompactDataTransition
                             ? Optional.empty()
                             : Optional.of(new VectorIndexIncrement(created, 
removed));
-            return new VectorIndexCommit(appendChange, compactChange);
+            return new VectorIndexCommit(
+                    appendChange,
+                    compactChange,
+                    failure ->
+                            rollbackPrepareCommit(
+                                    originalSegments, originalSourceFiles, 
generated, failure));
         } catch (Throwable failure) {
             rollbackPrepareCommit(originalSegments, originalSourceFiles, 
generated, failure);
             if (failure instanceof Exception) {
@@ -203,7 +217,7 @@ public class BucketedVectorIndexMaintainer {
         pendingBuild = build;
     }
 
-    private void rollbackPrepareCommit(
+    private synchronized void rollbackPrepareCommit(
             List<IndexFileMeta> originalSegments,
             Map<String, DataFileMeta> originalSourceFiles,
             List<IndexFileMeta> generated,
@@ -474,12 +488,15 @@ public class BucketedVectorIndexMaintainer {
 
         private final Optional<VectorIndexIncrement> appendIncrement;
         private final Optional<VectorIndexIncrement> compactIncrement;
+        private final AbortAction abortAction;
 
         private VectorIndexCommit(
                 Optional<VectorIndexIncrement> appendIncrement,
-                Optional<VectorIndexIncrement> compactIncrement) {
+                Optional<VectorIndexIncrement> compactIncrement,
+                AbortAction abortAction) {
             this.appendIncrement = appendIncrement;
             this.compactIncrement = compactIncrement;
+            this.abortAction = abortAction;
         }
 
         public Optional<VectorIndexIncrement> appendIncrement() {
@@ -489,6 +506,16 @@ public class BucketedVectorIndexMaintainer {
         public Optional<VectorIndexIncrement> compactIncrement() {
             return compactIncrement;
         }
+
+        public void abort(Throwable failure) {
+            abortAction.abort(failure);
+        }
+    }
+
+    @FunctionalInterface
+    private interface AbortAction {
+
+        void abort(Throwable failure);
     }
 
     /** Index-file additions and deletions emitted by one bucket state update. 
*/
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainerTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainerTest.java
new file mode 100644
index 0000000000..c2acfda21b
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainerTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.index.pk;
+
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.index.GlobalIndexMeta;
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.index.IndexPathFactory;
+import org.apache.paimon.index.pksorted.BucketedSortedIndexMaintainer;
+import org.apache.paimon.index.pksorted.PkSortedIndexFile;
+import org.apache.paimon.index.pkvector.BucketedVectorIndexMaintainer;
+import org.apache.paimon.io.CompactIncrement;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.io.DataIncrement;
+import org.apache.paimon.manifest.FileSource;
+import org.apache.paimon.stats.SimpleStats;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link BucketedPrimaryKeyIndexMaintainer}. */
+class BucketedPrimaryKeyIndexMaintainerTest {
+
+    @TempDir java.nio.file.Path tempPath;
+    private final ExecutorService buildExecutor = 
Executors.newFixedThreadPool(2);
+
+    @AfterEach
+    void shutdownExecutor() {
+        buildExecutor.shutdownNow();
+    }
+
+    @Test
+    void testDelegatesVectorLifecycle() {
+        BucketedVectorIndexMaintainer vector = 
mock(BucketedVectorIndexMaintainer.class);
+        ExecutorService executor = mock(ExecutorService.class);
+        when(vector.buildNotCompleted()).thenReturn(true);
+        BucketedPrimaryKeyIndexMaintainer maintainer =
+                BucketedPrimaryKeyIndexMaintainer.ofVector(vector);
+
+        assertThat(maintainer.buildNotCompleted()).isTrue();
+        maintainer.withExecutor(executor);
+        maintainer.close();
+
+        verify(vector).withExecutor(executor);
+        verify(vector).close();
+    }
+
+    @Test
+    void testScalarFailureDoesNotSuppressOtherDefinitionsOrVector() throws 
Exception {
+        DataFileMeta source = dataFile("data-1", 3);
+        IndexFileMeta vectorPayload =
+                new IndexFileMeta("vector", "vector", 1, 3, (GlobalIndexMeta) 
null, null);
+        BucketedVectorIndexMaintainer vector = 
mock(BucketedVectorIndexMaintainer.class);
+        BucketedVectorIndexMaintainer.VectorIndexCommit vectorCommit =
+                mock(BucketedVectorIndexMaintainer.VectorIndexCommit.class);
+        BucketedVectorIndexMaintainer.VectorIndexIncrement vectorIncrement =
+                mock(BucketedVectorIndexMaintainer.VectorIndexIncrement.class);
+        when(vector.prepareCommit(any(), any(), 
eq(true))).thenReturn(vectorCommit);
+        when(vectorCommit.appendIncrement()).thenReturn(Optional.empty());
+        
when(vectorCommit.compactIncrement()).thenReturn(Optional.of(vectorIncrement));
+        
when(vectorIncrement.newIndexFiles()).thenReturn(Collections.singletonList(vectorPayload));
+        
when(vectorIncrement.deletedIndexFiles()).thenReturn(Collections.emptyList());
+
+        List<String> buildOrder = Collections.synchronizedList(new 
ArrayList<>());
+        BucketedSortedIndexMaintainer btree =
+                sortedMaintainer(
+                        7,
+                        "btree",
+                        source,
+                        dataFile -> {
+                            buildOrder.add("btree");
+                            throw new IllegalStateException("expected BTree 
failure");
+                        });
+        IndexFileMeta bitmapPayload = payload("bitmap", source, 3, 8, 
"bitmap");
+        BucketedSortedIndexMaintainer bitmap =
+                sortedMaintainer(
+                        8,
+                        "bitmap",
+                        source,
+                        dataFile -> {
+                            buildOrder.add("bitmap");
+                            return Collections.singletonList(bitmapPayload);
+                        });
+        BucketedPrimaryKeyIndexMaintainer maintainer =
+                BucketedPrimaryKeyIndexMaintainer.of(vector, 
Arrays.asList(bitmap, btree));
+        CompactIncrement compactIncrement = compactAfter(source);
+
+        maintainer.prepareCommit(DataIncrement.emptyIncrement(), 
compactIncrement, true);
+
+        assertThat(buildOrder).containsExactly("btree", "btree", "btree", 
"bitmap");
+        
assertThat(compactIncrement.newIndexFiles()).containsExactly(vectorPayload, 
bitmapPayload);
+        assertThat(compactIncrement.deletedIndexFiles()).isEmpty();
+        assertThat(maintainer.buildNotCompleted()).isFalse();
+    }
+
+    @Test
+    void testScalarDefinitionsNeverBuildConcurrently() throws Exception {
+        DataFileMeta source = dataFile("data-1", 3);
+        AtomicInteger activeBuilds = new AtomicInteger();
+        AtomicInteger peakBuilds = new AtomicInteger();
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        BucketedSortedIndexMaintainer first =
+                sortedMaintainer(
+                        7,
+                        "btree",
+                        source,
+                        dataFile -> {
+                            enterBuild(activeBuilds, peakBuilds);
+                            firstStarted.countDown();
+                            releaseFirst.await();
+                            activeBuilds.decrementAndGet();
+                            return Collections.singletonList(
+                                    payload("btree", source, 3, 7, "btree"));
+                        });
+        BucketedSortedIndexMaintainer second =
+                sortedMaintainer(
+                        8,
+                        "bitmap",
+                        source,
+                        dataFile -> {
+                            enterBuild(activeBuilds, peakBuilds);
+                            secondStarted.countDown();
+                            activeBuilds.decrementAndGet();
+                            return Collections.singletonList(
+                                    payload("bitmap", source, 3, 8, "bitmap"));
+                        });
+        BucketedPrimaryKeyIndexMaintainer maintainer =
+                
BucketedPrimaryKeyIndexMaintainer.ofSorted(Arrays.asList(second, first));
+
+        maintainer.prepareCommit(DataIncrement.emptyIncrement(), 
compactAfter(source), false);
+        assertThat(firstStarted.await(5, TimeUnit.SECONDS)).isTrue();
+        assertThat(secondStarted.getCount()).isEqualTo(1);
+        assertThat(maintainer.buildNotCompleted()).isTrue();
+
+        releaseFirst.countDown();
+        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+        while (secondStarted.getCount() > 0 && System.nanoTime() < deadline) {
+            maintainer.prepareCommit(
+                    DataIncrement.emptyIncrement(), 
CompactIncrement.emptyIncrement(), false);
+            secondStarted.await(10, TimeUnit.MILLISECONDS);
+        }
+        assertThat(secondStarted.getCount()).isZero();
+        maintainer.prepareCommit(
+                DataIncrement.emptyIncrement(), 
CompactIncrement.emptyIncrement(), true);
+
+        assertThat(peakBuilds).hasValue(1);
+        assertThat(maintainer.buildNotCompleted()).isFalse();
+    }
+
+    @Test
+    void testBlockingPrepareRollsBackAllDefinitionsAfterInterruption() throws 
Exception {
+        DataFileMeta source = dataFile("data-1", 3);
+        AtomicInteger firstBuilds = new AtomicInteger();
+        AtomicInteger secondBuilds = new AtomicInteger();
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        CountDownLatch blockSecond = new CountDownLatch(1);
+        BucketedSortedIndexMaintainer first =
+                sortedMaintainer(
+                        7,
+                        "btree",
+                        source,
+                        dataFile -> {
+                            int attempt = firstBuilds.incrementAndGet();
+                            return Collections.singletonList(
+                                    payload("btree-" + attempt, source, 3, 7, 
"btree"));
+                        });
+        BucketedSortedIndexMaintainer second =
+                sortedMaintainer(
+                        8,
+                        "bitmap",
+                        source,
+                        dataFile -> {
+                            int attempt = secondBuilds.incrementAndGet();
+                            if (attempt == 1) {
+                                secondStarted.countDown();
+                                blockSecond.await();
+                            }
+                            return Collections.singletonList(
+                                    payload("bitmap-" + attempt, source, 3, 8, 
"bitmap"));
+                        });
+        BucketedPrimaryKeyIndexMaintainer maintainer =
+                
BucketedPrimaryKeyIndexMaintainer.ofSorted(Arrays.asList(second, first));
+        CompactIncrement failedIncrement = compactAfter(source);
+        AtomicReference<Throwable> failure = new AtomicReference<>();
+        Thread commitThread =
+                new Thread(
+                        () -> {
+                            try {
+                                maintainer.prepareCommit(
+                                        DataIncrement.emptyIncrement(), 
failedIncrement, true);
+                            } catch (Throwable t) {
+                                failure.set(t);
+                            }
+                        });
+
+        commitThread.start();
+        assertThat(secondStarted.await(5, TimeUnit.SECONDS)).isTrue();
+        commitThread.interrupt();
+        commitThread.join(TimeUnit.SECONDS.toMillis(5));
+
+        assertThat(commitThread.isAlive()).isFalse();
+        assertThat(failure.get()).isInstanceOf(InterruptedException.class);
+        assertThat(failedIncrement.newIndexFiles()).isEmpty();
+        assertThat(failedIncrement.deletedIndexFiles()).isEmpty();
+        assertThat(first.state().groups()).isEmpty();
+        assertThat(second.state().groups()).isEmpty();
+
+        CompactIncrement retryIncrement = compactAfter(source);
+        maintainer.prepareCommit(DataIncrement.emptyIncrement(), 
retryIncrement, true);
+
+        assertThat(firstBuilds).hasValue(2);
+        assertThat(secondBuilds).hasValue(2);
+        assertThat(retryIncrement.newIndexFiles())
+                .extracting(IndexFileMeta::fileName)
+                .containsExactly("btree-2", "bitmap-2");
+    }
+
+    private BucketedSortedIndexMaintainer sortedMaintainer(
+            int fieldId,
+            String indexType,
+            DataFileMeta source,
+            BucketedSortedIndexMaintainer.BuildFunction buildFunction) {
+        return new BucketedSortedIndexMaintainer(
+                fieldId,
+                indexType,
+                new PkSortedIndexFile(LocalFileIO.create(), pathFactory()),
+                buildFunction,
+                Collections.emptyList(),
+                Collections.emptyList(),
+                buildExecutor);
+    }
+
+    private static void enterBuild(AtomicInteger activeBuilds, AtomicInteger 
peakBuilds) {
+        int active = activeBuilds.incrementAndGet();
+        while (true) {
+            int peak = peakBuilds.get();
+            if (active <= peak || peakBuilds.compareAndSet(peak, active)) {
+                return;
+            }
+        }
+    }
+
+    private static CompactIncrement compactAfter(DataFileMeta source) {
+        return new CompactIncrement(
+                Collections.emptyList(),
+                Collections.singletonList(source),
+                Collections.emptyList());
+    }
+
+    private static IndexFileMeta payload(
+            String fileName,
+            DataFileMeta sourceFile,
+            long payloadRowCount,
+            int fieldId,
+            String indexType) {
+        PrimaryKeyIndexSourceFile source =
+                new PrimaryKeyIndexSourceFile(sourceFile.fileName(), 
sourceFile.rowCount());
+        return new IndexFileMeta(
+                indexType,
+                fileName,
+                1,
+                payloadRowCount,
+                new GlobalIndexMeta(
+                        0,
+                        source.rowCount() - 1,
+                        fieldId,
+                        null,
+                        new byte[] {1},
+                        new PrimaryKeyIndexSourceMeta(source).serialize()),
+                null);
+    }
+
+    private static DataFileMeta dataFile(String fileName, long rowCount) {
+        return DataFileMeta.forAppend(
+                        fileName,
+                        100,
+                        rowCount,
+                        SimpleStats.EMPTY_STATS,
+                        0,
+                        1,
+                        1,
+                        Collections.emptyList(),
+                        null,
+                        FileSource.COMPACT,
+                        null,
+                        null,
+                        null,
+                        null)
+                .upgrade(1);
+    }
+
+    private IndexPathFactory pathFactory() {
+        Path directory = new Path(tempPath.toUri());
+        return new IndexPathFactory() {
+            @Override
+            public Path toPath(String fileName) {
+                return new Path(directory, fileName);
+            }
+
+            @Override
+            public Path newPath() {
+                return new Path(directory, UUID.randomUUID().toString());
+            }
+
+            @Override
+            public boolean isExternalPath() {
+                return false;
+            }
+        };
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/index/pksorted/PrimaryKeySortedIndexMaintenanceTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/index/pksorted/PrimaryKeySortedIndexMaintenanceTest.java
new file mode 100644
index 0000000000..8ed3b4234f
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/index/pksorted/PrimaryKeySortedIndexMaintenanceTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.index.pksorted;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.KeyValue;
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.TestFileStore;
+import org.apache.paimon.TestKeyValueGenerator;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryVector;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.index.pk.PrimaryKeyIndexSourceFile;
+import org.apache.paimon.index.pk.PrimaryKeyIndexSourceMeta;
+import org.apache.paimon.index.pk.PrimaryKeyIndexSourcePolicy;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.memory.HeapMemorySegmentPool;
+import org.apache.paimon.memory.MemoryOwner;
+import org.apache.paimon.mergetree.compact.DeduplicateMergeFunction;
+import org.apache.paimon.mergetree.compact.LookupMergeFunction;
+import org.apache.paimon.operation.AbstractFileStoreWrite;
+import org.apache.paimon.operation.FileStoreCommit;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.schema.TableSchema;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.CommitMessageImpl;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.CommitIncrement;
+import org.apache.paimon.utils.InternalRowUtils;
+import org.apache.paimon.utils.RecordWriter;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** End-to-end maintenance tests for source-backed BTree and Bitmap indexes. */
+class PrimaryKeySortedIndexMaintenanceTest {
+
+    @TempDir java.nio.file.Path tempDir;
+    private IOManager ioManager;
+
+    @BeforeEach
+    void before() throws IOException {
+        ioManager = new IOManagerImpl(tempDir.toString());
+    }
+
+    @AfterEach
+    void after() throws Exception {
+        ioManager.close();
+    }
+
+    @Test
+    void testBuildRestoreAndReplaceSourceGroups() throws Exception {
+        TestFileStore store = createStore();
+        TestKeyValueGenerator generator = new TestKeyValueGenerator();
+        List<KeyValue> firstBatch =
+                Arrays.asList(
+                        withKey(generator.nextInsert("20201110", 10, 30L, new 
int[] {3}, "c"), 0),
+                        withKey(generator.nextInsert("20201110", 10, 10L, new 
int[] {1}, "a"), 4),
+                        withKey(generator.nextInsert("20201110", 10, null, new 
int[] {0}, null), 8),
+                        withKey(generator.nextInsert("20201110", 10, 20L, new 
int[] {2}, "b"), 2),
+                        withKey(generator.nextInsert("20201110", 10, 40L, new 
int[] {4}, "d"), 6));
+        BinaryRow partition = generator.getPartition(firstBatch.get(0));
+
+        writeAndCommit(store, partition, 0, firstBatch.subList(0, 3), 0);
+        writeAndCommit(store, partition, 0, firstBatch.subList(3, 5), 1);
+        assertThat(store.newScan().plan().files()).hasSize(2);
+        compactAndCommit(store, partition, 0, 2);
+
+        List<DataFileMeta> compactedFiles =
+                store.newScan().plan().files().stream()
+                        .map(entry -> entry.file())
+                        .collect(Collectors.toList());
+        
assertThat(compactedFiles).anyMatch(PrimaryKeyIndexSourcePolicy::shouldRead);
+        List<IndexFileMeta> firstPayloads = sourcePayloads(store, partition, 
0);
+        assertCompleteGroups(firstPayloads, 5);
+        Set<String> firstPayloadNames =
+                
firstPayloads.stream().map(IndexFileMeta::fileName).collect(Collectors.toSet());
+        Set<String> firstSources = sourceNames(firstPayloads);
+        assertThat(firstSources).isNotEmpty();
+
+        KeyValue additional =
+                withKey(generator.nextInsert("20201110", 10, 50L, new int[] 
{5}, "e"), 5);
+        writeAndCommit(store, partition, 0, Arrays.asList(additional), 3);
+        compactAndCommit(store, partition, 0, 4);
+
+        List<IndexFileMeta> replacementPayloads = sourcePayloads(store, 
partition, 0);
+        assertCompleteGroups(replacementPayloads, 6);
+        assertThat(replacementPayloads)
+                .extracting(IndexFileMeta::fileName)
+                .doesNotContainAnyElementsOf(firstPayloadNames);
+        
assertThat(sourceNames(replacementPayloads)).doesNotContainAnyElementsOf(firstSources);
+
+        AbstractFileStoreWrite<KeyValue> restored = store.newWrite();
+        restored.withIOManager(ioManager);
+        AbstractFileStoreWrite.WriterContainer<KeyValue> container =
+                restored.createWriterContainer(partition, 0);
+        restored.prepareCommit(false, 5);
+        assertThat(container.primaryKeyIndexMaintainer).isNotNull();
+        
assertThat(container.primaryKeyIndexMaintainer.buildNotCompleted()).isFalse();
+        restored.close();
+    }
+
+    @Test
+    void testRestoreMixedVectorAndScalarPayloads() throws Exception {
+        TestFileStore store = createMixedStore();
+        TestKeyValueGenerator generator = new TestKeyValueGenerator();
+        KeyValue first =
+                withVector(
+                        withKey(generator.nextInsert("20201110", 10, 30L, new 
int[] {3}, "c"), 0),
+                        new float[] {1, 0});
+        KeyValue second =
+                withVector(
+                        withKey(generator.nextInsert("20201110", 10, 10L, new 
int[] {1}, "a"), 1),
+                        new float[] {0, 1});
+        BinaryRow partition = generator.getPartition(first);
+
+        writeAndCommit(store, partition, 0, Collections.singletonList(first), 
0);
+        writeAndCommit(store, partition, 0, Collections.singletonList(second), 
1);
+        compactAndCommit(store, partition, 0, 2);
+
+        assertThat(sourcePayloads(store, partition, 0))
+                .extracting(IndexFileMeta::indexType)
+                .contains("test-vector-ann", "btree", "bitmap");
+
+        AbstractFileStoreWrite<KeyValue> restored = store.newWrite();
+        restored.withIOManager(ioManager);
+        AbstractFileStoreWrite.WriterContainer<KeyValue> container =
+                restored.createWriterContainer(partition, 0);
+        restored.prepareCommit(false, 3);
+
+        assertThat(container.primaryKeyIndexMaintainer).isNotNull();
+        
assertThat(container.primaryKeyIndexMaintainer.buildNotCompleted()).isFalse();
+        restored.close();
+    }
+
+    private void writeAndCommit(
+            TestFileStore store,
+            BinaryRow partition,
+            int bucket,
+            List<KeyValue> records,
+            long identifier)
+            throws Exception {
+        AbstractFileStoreWrite<KeyValue> write = store.newWrite();
+        write.withIOManager(ioManager);
+        AbstractFileStoreWrite.WriterContainer<KeyValue> container =
+                write.createWriterContainer(partition, bucket);
+        RecordWriter<KeyValue> writer = container.writer;
+        ((MemoryOwner) writer)
+                .setMemoryPool(
+                        new HeapMemorySegmentPool(
+                                TestFileStore.WRITE_BUFFER_SIZE.getBytes(),
+                                (int) TestFileStore.PAGE_SIZE.getBytes()));
+        for (KeyValue record : records) {
+            writer.write(record);
+        }
+        commit(
+                store,
+                Collections.singletonList(prepareCommit(container, partition, 
bucket)),
+                identifier);
+        container.writer.close();
+        container.primaryKeyIndexMaintainer.close();
+        write.close();
+    }
+
+    private void compactAndCommit(
+            TestFileStore store, BinaryRow partition, int bucket, long 
identifier)
+            throws Exception {
+        AbstractFileStoreWrite<KeyValue> write = store.newWrite();
+        write.withIOManager(ioManager);
+        AbstractFileStoreWrite.WriterContainer<KeyValue> container =
+                write.createWriterContainer(partition, bucket);
+        ((MemoryOwner) container.writer)
+                .setMemoryPool(
+                        new HeapMemorySegmentPool(
+                                TestFileStore.WRITE_BUFFER_SIZE.getBytes(),
+                                (int) TestFileStore.PAGE_SIZE.getBytes()));
+        container.writer.compact(true);
+        commit(
+                store,
+                Collections.singletonList(prepareCommit(container, partition, 
bucket)),
+                identifier);
+        container.writer.close();
+        container.primaryKeyIndexMaintainer.close();
+        write.close();
+    }
+
+    private CommitMessage prepareCommit(
+            AbstractFileStoreWrite.WriterContainer<KeyValue> container,
+            BinaryRow partition,
+            int bucket)
+            throws Exception {
+        CommitIncrement increment = container.writer.prepareCommit(true);
+        container.primaryKeyIndexMaintainer.prepareCommit(
+                increment.newFilesIncrement(), increment.compactIncrement(), 
true);
+        return new CommitMessageImpl(
+                partition, bucket, 1, increment.newFilesIncrement(), 
increment.compactIncrement());
+    }
+
+    private void commit(TestFileStore store, List<CommitMessage> messages, 
long identifier) {
+        ManifestCommittable committable = new ManifestCommittable(identifier, 
null, messages);
+        try (FileStoreCommit commit = store.newCommit()) {
+            commit.withIOManager(ioManager).commit(committable, false);
+        }
+    }
+
+    private static List<IndexFileMeta> sourcePayloads(
+            TestFileStore store, BinaryRow partition, int bucket) {
+        Snapshot snapshot = store.snapshotManager().latestSnapshot();
+        return store.newIndexFileHandler().scanSourceIndexes(snapshot, 
partition, bucket);
+    }
+
+    private static void assertCompleteGroups(List<IndexFileMeta> payloads, 
long rowCount) {
+        Map<String, List<IndexFileMeta>> byType =
+                
payloads.stream().collect(Collectors.groupingBy(IndexFileMeta::indexType));
+        assertThat(byType.keySet()).containsExactlyInAnyOrder("btree", 
"bitmap");
+        for (List<IndexFileMeta> typePayloads : byType.values()) {
+            assertThat(typePayloads)
+                    .allMatch(payload -> 
payload.globalIndexMeta().sourceMeta() != null);
+            
assertThat(typePayloads.stream().mapToLong(IndexFileMeta::rowCount).sum())
+                    .as("total row count of %s source groups", 
typePayloads.get(0).indexType())
+                    .isEqualTo(rowCount);
+            Map<PrimaryKeyIndexSourceFile, List<IndexFileMeta>> bySource =
+                    typePayloads.stream()
+                            .collect(
+                                    Collectors.groupingBy(
+                                            payload ->
+                                                    
PrimaryKeyIndexSourceMeta.fromIndexFile(payload)
+                                                            .sourceFile()));
+            for (Map.Entry<PrimaryKeyIndexSourceFile, List<IndexFileMeta>> 
sourceGroup :
+                    bySource.entrySet()) {
+                
assertThat(sourceGroup.getValue().stream().mapToLong(IndexFileMeta::rowCount).sum())
+                        .isEqualTo(sourceGroup.getKey().rowCount());
+            }
+        }
+    }
+
+    private static Set<String> sourceNames(List<IndexFileMeta> payloads) {
+        Set<String> result = new HashSet<>();
+        for (IndexFileMeta payload : payloads) {
+            
result.add(PrimaryKeyIndexSourceMeta.fromIndexFile(payload).sourceFile().fileName());
+        }
+        return result;
+    }
+
+    private static KeyValue withKey(KeyValue record, int key) {
+        return new KeyValue()
+                .replace(
+                        TestKeyValueGenerator.KEY_SERIALIZER
+                                .toBinaryRow(GenericRow.of(key, (long) key))
+                                .copy(),
+                        record.sequenceNumber(),
+                        record.valueKind(),
+                        record.value());
+    }
+
+    private static KeyValue withVector(KeyValue record, float[] vector) {
+        InternalRow original = record.value();
+        GenericRow extended =
+                new 
GenericRow(TestKeyValueGenerator.DEFAULT_ROW_TYPE.getFieldCount() + 1);
+        for (int i = 0; i < 
TestKeyValueGenerator.DEFAULT_ROW_TYPE.getFieldCount(); i++) {
+            extended.setField(
+                    i,
+                    InternalRowUtils.get(
+                            original, i, 
TestKeyValueGenerator.DEFAULT_ROW_TYPE.getTypeAt(i)));
+        }
+        extended.setField(
+                TestKeyValueGenerator.DEFAULT_ROW_TYPE.getFieldCount(),
+                BinaryVector.fromPrimitiveArray(vector));
+        return record.replaceValue(extended);
+    }
+
+    private TestFileStore createStore() throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "1");
+        options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+        options.put(CoreOptions.COMPACTION_FORCE_REWRITE_ALL_FILES.key(), 
"true");
+        options.put(CoreOptions.PK_BTREE_INDEX_COLUMNS.key(), "itemId");
+        options.put(CoreOptions.PK_BITMAP_INDEX_COLUMNS.key(), "comment");
+        options.put(
+                "fields.itemId.pk-btree.index.options",
+                "{\"sorted-index.records-per-range\":\"2\"}");
+        options.put(
+                "fields.comment.pk-bitmap.index.options",
+                "{\"sorted-index.records-per-range\":\"2\"}");
+        SchemaManager schemaManager =
+                new SchemaManager(LocalFileIO.create(), new 
Path(tempDir.toUri()));
+        TableSchema schema =
+                schemaManager.createTable(
+                        new Schema(
+                                
TestKeyValueGenerator.DEFAULT_ROW_TYPE.getFields(),
+                                
TestKeyValueGenerator.DEFAULT_PART_TYPE.getFieldNames(),
+                                TestKeyValueGenerator.getPrimaryKeys(
+                                        
TestKeyValueGenerator.GeneratorMode.MULTI_PARTITIONED),
+                                options,
+                                null));
+        return new TestFileStore.Builder(
+                        "avro",
+                        tempDir.toString(),
+                        1,
+                        TestKeyValueGenerator.DEFAULT_PART_TYPE,
+                        TestKeyValueGenerator.KEY_TYPE,
+                        TestKeyValueGenerator.DEFAULT_ROW_TYPE,
+                        
TestKeyValueGenerator.TestKeyValueFieldsExtractor.EXTRACTOR,
+                        LookupMergeFunction.wrap(
+                                DeduplicateMergeFunction.factory(),
+                                new CoreOptions(options),
+                                TestKeyValueGenerator.KEY_TYPE,
+                                TestKeyValueGenerator.DEFAULT_ROW_TYPE),
+                        schema)
+                .build();
+    }
+
+    private TestFileStore createMixedStore() throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "1");
+        options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+        options.put(CoreOptions.COMPACTION_FORCE_REWRITE_ALL_FILES.key(), 
"true");
+        options.put(CoreOptions.PK_BTREE_INDEX_COLUMNS.key(), "itemId");
+        options.put(CoreOptions.PK_BITMAP_INDEX_COLUMNS.key(), "comment");
+        options.put(CoreOptions.PK_VECTOR_INDEX_COLUMNS.key(), "embedding");
+        options.put("fields.embedding.pk-vector.index.type", 
"test-vector-ann");
+        options.put("fields.embedding.pk-vector.distance.metric", "l2");
+
+        List<DataField> fields =
+                new 
ArrayList<>(TestKeyValueGenerator.DEFAULT_ROW_TYPE.getFields());
+        fields.add(new DataField(7, "embedding", DataTypes.VECTOR(2, 
DataTypes.FLOAT())));
+        RowType rowType = new RowType(fields);
+        SchemaManager schemaManager =
+                new SchemaManager(LocalFileIO.create(), new 
Path(tempDir.toUri()));
+        TableSchema schema =
+                schemaManager.createTable(
+                        new Schema(
+                                rowType.getFields(),
+                                
TestKeyValueGenerator.DEFAULT_PART_TYPE.getFieldNames(),
+                                TestKeyValueGenerator.getPrimaryKeys(
+                                        
TestKeyValueGenerator.GeneratorMode.MULTI_PARTITIONED),
+                                options,
+                                null));
+        return new TestFileStore.Builder(
+                        "avro",
+                        tempDir.toString(),
+                        1,
+                        TestKeyValueGenerator.DEFAULT_PART_TYPE,
+                        TestKeyValueGenerator.KEY_TYPE,
+                        rowType,
+                        
TestKeyValueGenerator.TestKeyValueFieldsExtractor.EXTRACTOR,
+                        LookupMergeFunction.wrap(
+                                DeduplicateMergeFunction.factory(),
+                                new CoreOptions(options),
+                                TestKeyValueGenerator.KEY_TYPE,
+                                rowType),
+                        schema)
+                .build();
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
index 2a6b0dc65e..8cd89bb59c 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
@@ -20,6 +20,7 @@ package org.apache.paimon.index.pkvector;
 
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.index.GlobalIndexMeta;
 import org.apache.paimon.index.IndexFileMeta;
 import org.apache.paimon.index.IndexPathFactory;
 import org.apache.paimon.index.pk.PrimaryKeyIndexSourceFile;
@@ -69,6 +70,72 @@ class BucketedVectorIndexMaintainerTest {
         executor.shutdownNow();
     }
 
+    @Test
+    void testRestoreFiltersPayloadsByVectorDefinition() {
+        DataField vectorField =
+                new DataField(7, "embedding", DataTypes.VECTOR(2, 
DataTypes.FLOAT()));
+        DataFileMeta data = dataFile("data");
+        IndexFileMeta vectorPayload = payload("ann", data, 7, 
"test-vector-ann");
+        List<IndexFileMeta> restoredPayloads =
+                Arrays.asList(
+                        payload("btree", data, 3, "btree"),
+                        payload("bitmap", data, 5, "bitmap"),
+                        payload("other-ann", data, 8, "test-vector-ann"),
+                        vectorPayload);
+
+        BucketedVectorIndexMaintainer maintainer =
+                new BucketedVectorIndexMaintainer(
+                        7,
+                        new PkVectorAnnSegmentFile(LocalFileIO.create(), 
pathFactory()),
+                        vectorField,
+                        indexOptions(),
+                        "l2",
+                        "test-vector-ann",
+                        mock(PkVectorDataFileReader.Factory.class),
+                        Collections.singletonList(data),
+                        restoredPayloads,
+                        executor);
+
+        assertThat(maintainer.segments()).containsExactly(vectorPayload);
+    }
+
+    @Test
+    void testPreparedCommitCanBeAbortedByCoordinator() throws Exception {
+        DataField vectorField =
+                new DataField(7, "embedding", DataTypes.VECTOR(2, 
DataTypes.FLOAT()));
+        DataFileMeta data = dataFile("data");
+        PkVectorDataFileReader.Factory readerFactory = 
mock(PkVectorDataFileReader.Factory.class);
+        PkVectorDataFileReader dataReader = reader(new float[][] {{1, 0}});
+        when(readerFactory.create(data)).thenReturn(dataReader);
+        BucketedVectorIndexMaintainer maintainer =
+                new BucketedVectorIndexMaintainer(
+                        7,
+                        new PkVectorAnnSegmentFile(LocalFileIO.create(), 
pathFactory()),
+                        vectorField,
+                        indexOptions(),
+                        "l2",
+                        "test-vector-ann",
+                        readerFactory,
+                        Collections.emptyList(),
+                        Collections.emptyList(),
+                        executor);
+
+        BucketedVectorIndexMaintainer.VectorIndexCommit commit =
+                maintainer.prepareCommit(
+                        DataIncrement.emptyIncrement(),
+                        new CompactIncrement(
+                                Collections.emptyList(),
+                                Collections.singletonList(data),
+                                Collections.emptyList()),
+                        true);
+        assertThat(maintainer.segments()).hasSize(1);
+
+        commit.abort(new IllegalStateException("later definition failed"));
+
+        assertThat(maintainer.segments()).isEmpty();
+        assertThat(fileCount()).isZero();
+    }
+
     @Test
     void testNonBlockingPrepareCommitPublishesCompletedAnnLater() throws 
Exception {
         LocalFileIO fileIO = LocalFileIO.create();
@@ -659,6 +726,25 @@ class BucketedVectorIndexMaintainerTest {
                 .upgrade(1);
     }
 
+    private static IndexFileMeta payload(
+            String fileName, DataFileMeta source, int fieldId, String 
indexType) {
+        PrimaryKeyIndexSourceFile sourceFile =
+                new PrimaryKeyIndexSourceFile(source.fileName(), 
source.rowCount());
+        return new IndexFileMeta(
+                indexType,
+                fileName,
+                1,
+                source.rowCount(),
+                new GlobalIndexMeta(
+                        0,
+                        source.rowCount() - 1,
+                        fieldId,
+                        null,
+                        new byte[] {1},
+                        new PrimaryKeyIndexSourceMeta(sourceFile).serialize()),
+                null);
+    }
+
     private IndexPathFactory pathFactory() {
         Path directory = new Path(tempPath.toUri());
         return new IndexPathFactory() {
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/PrimaryKeyIndexWriteTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/PrimaryKeyIndexWriteTest.java
new file mode 100644
index 0000000000..f864f61526
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/PrimaryKeyIndexWriteTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.operation;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.KeyValue;
+import org.apache.paimon.TestFileStore;
+import org.apache.paimon.TestKeyValueGenerator;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.mergetree.compact.DeduplicateMergeFunction;
+import org.apache.paimon.postpone.PostponeBucketFileStoreWrite;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.schema.TableSchema;
+import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests BTree and Bitmap primary-key index wiring through the file-store 
writer. */
+class PrimaryKeyIndexWriteTest {
+
+    @TempDir java.nio.file.Path tempDir;
+    private IOManager ioManager;
+
+    @BeforeEach
+    void before() throws IOException {
+        ioManager = new IOManagerImpl(tempDir.toString());
+    }
+
+    @AfterEach
+    void after() throws Exception {
+        ioManager.close();
+    }
+
+    @Test
+    void testCreatesCoordinatorForBTreeAndBitmapDefinitions() throws Exception 
{
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "10");
+        options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+        options.put(CoreOptions.PK_BTREE_INDEX_COLUMNS.key(), "itemId");
+        options.put(CoreOptions.PK_BITMAP_INDEX_COLUMNS.key(), "comment");
+        TestFileStore store = createStore(options);
+        KeyValueFileStoreWrite write = (KeyValueFileStoreWrite) 
store.newWrite();
+        write.withIOManager(ioManager);
+        TestKeyValueGenerator generator = new TestKeyValueGenerator();
+        KeyValue record = generator.next();
+
+        AbstractFileStoreWrite.WriterContainer<KeyValue> container =
+                write.createWriterContainer(generator.getPartition(record), 1);
+
+        assertThat(container.primaryKeyIndexMaintainer).isNotNull();
+        
assertThat(container.primaryKeyIndexMaintainer.buildNotCompleted()).isFalse();
+        write.close();
+    }
+
+    @Test
+    void testCreatesCoordinatorForMultipleScalarDefinitionsAndVector() throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "10");
+        options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+        options.put(CoreOptions.PK_BTREE_INDEX_COLUMNS.key(), "itemId,score");
+        options.put(CoreOptions.PK_BITMAP_INDEX_COLUMNS.key(), 
"comment,state");
+        options.put(CoreOptions.PK_VECTOR_INDEX_COLUMNS.key(), "embedding");
+        options.put("fields.embedding.pk-vector.index.type", "hnsw");
+
+        List<DataField> fields =
+                new 
ArrayList<>(TestKeyValueGenerator.DEFAULT_ROW_TYPE.getFields());
+        fields.add(new DataField(7, "score", DataTypes.BIGINT()));
+        fields.add(new DataField(8, "state", DataTypes.INT()));
+        fields.add(new DataField(9, "embedding", DataTypes.VECTOR(3, 
DataTypes.FLOAT())));
+        TestFileStore store = createStore(options, new RowType(fields));
+        KeyValueFileStoreWrite write = (KeyValueFileStoreWrite) 
store.newWrite();
+        write.withIOManager(ioManager);
+        TestKeyValueGenerator generator = new TestKeyValueGenerator();
+        KeyValue record = generator.next();
+
+        AbstractFileStoreWrite.WriterContainer<KeyValue> container =
+                write.createWriterContainer(generator.getPartition(record), 1);
+
+        assertThat(readField(container.primaryKeyIndexMaintainer, 
"vectorMaintainer")).isNotNull();
+        assertThat((List<?>) readField(container.primaryKeyIndexMaintainer, 
"sortedMaintainers"))
+                .hasSize(4);
+        write.close();
+    }
+
+    @Test
+    void testPostponeBucketDefersCoordinatorUntilFixedBucketCompaction() 
throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), 
String.valueOf(BucketMode.POSTPONE_BUCKET));
+        options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+        options.put(CoreOptions.PK_BTREE_INDEX_COLUMNS.key(), "itemId");
+        options.put(CoreOptions.PK_BITMAP_INDEX_COLUMNS.key(), "comment");
+        TableSchema schema = createSchema(options, 
TestKeyValueGenerator.DEFAULT_ROW_TYPE);
+        TestKeyValueGenerator generator = new TestKeyValueGenerator();
+        KeyValue record = generator.next();
+
+        TestFileStore postponeStore =
+                createStore(
+                        schema, TestKeyValueGenerator.DEFAULT_ROW_TYPE, 
BucketMode.POSTPONE_BUCKET);
+        AbstractFileStoreWrite<KeyValue> postponeWrite = 
postponeStore.newWrite();
+        postponeWrite.withIOManager(ioManager);
+        AbstractFileStoreWrite.WriterContainer<KeyValue> postponeContainer =
+                postponeWrite.createWriterContainer(
+                        generator.getPartition(record), 
BucketMode.POSTPONE_BUCKET);
+
+        
assertThat(postponeWrite).isInstanceOf(PostponeBucketFileStoreWrite.class);
+        assertThat(postponeContainer.primaryKeyIndexMaintainer).isNull();
+        postponeWrite.close();
+
+        // Postpone compaction keeps the table schema but uses a fixed-bucket 
runtime view.
+        TestFileStore compactStore = createStore(schema, 
TestKeyValueGenerator.DEFAULT_ROW_TYPE, 1);
+        KeyValueFileStoreWrite compactWrite = (KeyValueFileStoreWrite) 
compactStore.newWrite();
+        compactWrite.withIOManager(ioManager);
+        AbstractFileStoreWrite.WriterContainer<KeyValue> compactContainer =
+                
compactWrite.createWriterContainer(generator.getPartition(record), 0);
+
+        assertThat(compactContainer.primaryKeyIndexMaintainer).isNotNull();
+        compactWrite.close();
+    }
+
+    private TestFileStore createStore(Map<String, String> options) throws 
Exception {
+        return createStore(options, TestKeyValueGenerator.DEFAULT_ROW_TYPE);
+    }
+
+    private TestFileStore createStore(Map<String, String> options, RowType 
rowType)
+            throws Exception {
+        return createStore(createSchema(options, rowType), rowType, 10);
+    }
+
+    private TableSchema createSchema(Map<String, String> options, RowType 
rowType)
+            throws Exception {
+        SchemaManager schemaManager =
+                new SchemaManager(LocalFileIO.create(), new 
Path(tempDir.toUri()));
+        return schemaManager.createTable(
+                new Schema(
+                        rowType.getFields(),
+                        
TestKeyValueGenerator.DEFAULT_PART_TYPE.getFieldNames(),
+                        TestKeyValueGenerator.getPrimaryKeys(
+                                
TestKeyValueGenerator.GeneratorMode.MULTI_PARTITIONED),
+                        options,
+                        null));
+    }
+
+    private TestFileStore createStore(TableSchema schema, RowType rowType, int 
numBuckets) {
+        return new TestFileStore.Builder(
+                        "avro",
+                        tempDir.toString(),
+                        numBuckets,
+                        TestKeyValueGenerator.DEFAULT_PART_TYPE,
+                        TestKeyValueGenerator.KEY_TYPE,
+                        rowType,
+                        
TestKeyValueGenerator.TestKeyValueFieldsExtractor.EXTRACTOR,
+                        DeduplicateMergeFunction.factory(),
+                        schema)
+                .build();
+    }
+
+    private static Object readField(Object target, String name) throws 
Exception {
+        Field field = target.getClass().getDeclaredField(name);
+        field.setAccessible(true);
+        return field.get(target);
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
index 9ae9666200..5f6a9152fd 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
@@ -19,13 +19,18 @@
 package org.apache.paimon.table;
 
 import org.apache.paimon.CoreOptions;
+import org.apache.paimon.Snapshot;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.globalindex.DataEvolutionBatchScan;
+import org.apache.paimon.globalindex.GlobalIndexCoverage;
 import org.apache.paimon.globalindex.GlobalIndexResult;
 import org.apache.paimon.globalindex.GlobalIndexScanner;
 import org.apache.paimon.globalindex.IndexedSplit;
 import org.apache.paimon.globalindex.sorted.SortedGlobalIndexBuilder;
+import org.apache.paimon.index.GlobalIndexMeta;
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.manifest.IndexManifestEntry;
 import org.apache.paimon.partition.PartitionPredicate;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.predicate.PredicateBuilder;
@@ -214,6 +219,60 @@ public class BtreeGlobalIndexTableTest extends 
DataEvolutionTestBase {
         }
     }
 
+    @Test
+    public void testSourceBackedIndexIsExcludedFromGlobalRowIdScan() throws 
Exception {
+        write(10L);
+        FileStoreTable table =
+                tableWithSearchMode((FileStoreTable) 
catalog.getTable(identifier()), "full");
+        IndexFileMeta sourceBacked =
+                new IndexFileMeta(
+                        "btree",
+                        "source-backed-index",
+                        0,
+                        10,
+                        new GlobalIndexMeta(0, 9, 1, null, null, new byte[] 
{1}),
+                        null);
+
+        assertThat(GlobalIndexScanner.create(table, 
Collections.singletonList(sourceBacked)))
+                .isEmpty();
+
+        GlobalIndexCoverage coverage =
+                new GlobalIndexCoverage(
+                        table,
+                        table.snapshotManager().latestSnapshot(),
+                        PartitionPredicate.ALWAYS_TRUE,
+                        Collections.singletonList(sourceBacked));
+        assertThat(coverage.unindexedRanges(1)).containsExactly(new Range(0, 
9));
+    }
+
+    @Test
+    public void testOrdinaryAndSourceBackedBTreeIndexesCanCoexist() throws 
Exception {
+        write(10L);
+        createIndex("f1");
+        FileStoreTable table = (FileStoreTable) catalog.getTable(identifier());
+        Snapshot snapshot = table.snapshotManager().latestSnapshot();
+        List<IndexFileMeta> mixedIndexes =
+                table.store().newIndexFileHandler().scan(snapshot, 
"btree").stream()
+                        .map(IndexManifestEntry::indexFile)
+                        .collect(Collectors.toCollection(ArrayList::new));
+        mixedIndexes.add(
+                new IndexFileMeta(
+                        "btree",
+                        "source-backed-index",
+                        0,
+                        10,
+                        new GlobalIndexMeta(0, 9, 1, null, null, new byte[] 
{1}),
+                        null));
+        Predicate predicate =
+                new PredicateBuilder(table.rowType()).equal(1, 
BinaryString.fromString("a7"));
+
+        try (GlobalIndexScanner scanner =
+                GlobalIndexScanner.create(table, 
mixedIndexes).orElseThrow(AssertionError::new)) {
+            assertThat(scanner.scan(predicate).get().results().toRangeList())
+                    .containsExactly(new Range(7, 7));
+        }
+    }
+
     @Test
     public void testBTreeGlobalIndexSearchModeUsesAllPredicateFieldCoverage() 
throws Exception {
         write(500L);


Reply via email to