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 2c60458e88 [core] Rebuild file index over the correct column after 
schema evolution (#10122)
2c60458e88 is described below

commit 2c60458e882772efe1ae76200793edd4a76c22ac
Author: jackylee <[email protected]>
AuthorDate: Thu Sep 24 13:08:38 2026 +0800

    [core] Rebuild file index over the correct column after schema evolution 
(#10122)
---
 .../apache/paimon/index/FileIndexProcessor.java    |  7 +-
 .../paimon/index/FileIndexProcessorTest.java       | 76 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 1 deletion(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/FileIndexProcessor.java 
b/paimon-core/src/main/java/org/apache/paimon/index/FileIndexProcessor.java
index de65608816..3030ac3bb2 100644
--- a/paimon-core/src/main/java/org/apache/paimon/index/FileIndexProcessor.java
+++ b/paimon-core/src/main/java/org/apache/paimon/index/FileIndexProcessor.java
@@ -136,9 +136,14 @@ public class FileIndexProcessor {
                         fileIndexOptions,
                         schemaInfo.colNameMapping);
         if (dataFileIndexWriter != null) {
+            // projectedIndexCols index into the file schema. withProjection 
would re-interpret
+            // them against the current table schema, so a schema change that 
shifts columns (drop
+            // a middle column, add another) would read the wrong column and 
rebuild the index over
+            // it. Read with the same file-schema projection the writer above 
uses.
+            RowType indexReadType = 
schemaInfo.fileSchema.project(schemaInfo.projectedIndexCols);
             try (RecordReader<InternalRow> reader =
                     table.newReadBuilder()
-                            .withProjection(schemaInfo.projectedIndexCols)
+                            .withReadType(indexReadType)
                             .newRead()
                             .createReader(
                                     DataSplit.builder()
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/index/FileIndexProcessorTest.java 
b/paimon-core/src/test/java/org/apache/paimon/index/FileIndexProcessorTest.java
index fa6f51e34d..19a5c899e5 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/index/FileIndexProcessorTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/index/FileIndexProcessorTest.java
@@ -25,11 +25,14 @@ import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericMap;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.fileindex.FileIndexFormat;
+import org.apache.paimon.fileindex.FileIndexReader;
+import org.apache.paimon.fs.ByteArraySeekableStream;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.io.DataFilePathFactory;
 import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.predicate.FieldRef;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaChange;
 import org.apache.paimon.table.FileStoreTable;
@@ -46,6 +49,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -174,4 +178,76 @@ public class FileIndexProcessorTest {
         DataFileMeta processed = processor.process(entry.partition(), 
entry.bucket(), entry);
         assertThat(processed.extraFiles()).isNotEmpty();
     }
+
+    @Test
+    public void testRebuildsIndexOnCorrectColumnAfterColumnDropAndAdd() throws 
Exception {
+        LocalFileIO fileIO = LocalFileIO.create();
+        Path warehouse = new Path(tempDir.toString());
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "1");
+        options.put(CoreOptions.FILE_FORMAT.key(), "parquet");
+        // A bitmap index is exact: querying an absent value returns an empty 
result,
+        // so the regression assertion below is deterministic (a bloom filter's
+        // probabilistic false positives could let the wrong-column behavior 
pass).
+        options.put(CoreOptions.FILE_INDEX + ".bitmap.columns", "v");
+        // v is the third field, so it sits at index 2 in the file schema.
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {DataTypes.INT(), DataTypes.INT(), 
DataTypes.INT()},
+                        new String[] {"k", "a", "v"});
+
+        Identifier identifier = Identifier.create("mydb", "t");
+        try (FileSystemCatalog catalog = new FileSystemCatalog(fileIO, 
warehouse)) {
+            catalog.createDatabase("mydb", false);
+            catalog.createTable(
+                    identifier,
+                    new Schema(
+                            rowType.getFields(),
+                            Collections.emptyList(),
+                            Collections.singletonList("k"),
+                            options,
+                            ""),
+                    false);
+            FileStoreTable table = (FileStoreTable) 
catalog.getTable(identifier);
+
+            String commitUser = UUID.randomUUID().toString();
+            try (TableWriteImpl<?> write = table.newWrite(commitUser);
+                    TableCommitImpl commit = table.newCommit(commitUser)) {
+                write.write(GenericRow.of(1, 10, 100));
+                commit.commit(1, write.prepareCommit(false, 1));
+            }
+
+            // Drop the middle column and add another: v keeps file-schema 
index 2, but the
+            // current table schema now has a different column (w) at index 2.
+            table.schemaManager().commitChanges(SchemaChange.dropColumn("a"));
+            table.schemaManager().commitChanges(SchemaChange.addColumn("w", 
DataTypes.INT()));
+            FileStoreTable evolved = (FileStoreTable) 
catalog.getTable(identifier);
+
+            List<ManifestEntry> entries = 
evolved.store().newScan().plan().files();
+            assertThat(entries).isNotEmpty();
+            ManifestEntry entry = entries.get(0);
+            assertThat(entry.file().schemaId()).isEqualTo(0L);
+
+            FileIndexProcessor processor = new FileIndexProcessor(evolved);
+            DataFileMeta processed = processor.process(entry.partition(), 
entry.bucket(), entry);
+            // The small single-row bitmap index is embedded in the manifest, 
not a side file.
+            byte[] embedded = processed.embeddedIndex();
+            assertThat(embedded).isNotEmpty();
+
+            // The bitmap index for v must still contain the written value 
100. Before the fix
+            // the reader projected the current-schema column at index 2 (w, 
absent from this
+            // file), so the index was rebuilt over nulls and 100 would be 
absent.
+            FieldRef vRef = new FieldRef(0, "v", DataTypes.INT());
+            try (FileIndexFormat.Reader reader =
+                    FileIndexFormat.createReader(new 
ByteArraySeekableStream(embedded), rowType)) {
+                Set<FileIndexReader> vReaders = reader.readColumnIndex("v");
+                assertThat(vReaders).isNotEmpty();
+                for (FileIndexReader vReader : vReaders) {
+                    // Exact: 100 was indexed from v, 999 never was.
+                    assertThat(vReader.visitEqual(vRef, 
100).remain()).isTrue();
+                    assertThat(vReader.visitEqual(vRef, 
999).remain()).isFalse();
+                }
+            }
+        }
+    }
 }

Reply via email to