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

yuzelin 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 55fe85f641 [core] Fix shared decompression buffer in stable manifest 
blocks (#9430)
55fe85f641 is described below

commit 55fe85f641b82ba8cebf6d555884641eb9546078
Author: yuzelin <[email protected]>
AuthorDate: Fri Aug 28 00:21:12 2026 +0800

    [core] Fix shared decompression buffer in stable manifest blocks (#9430)
---
 .../apache/paimon/manifest/ManifestAvroReader.java |   5 +-
 .../paimon/operation/ManifestFileMergerTest.java   | 142 +++++++++++++++++++++
 .../paimon/format/avro/AvroRecordDecoder.java      |   7 +
 3 files changed, 153 insertions(+), 1 deletion(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroReader.java 
b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroReader.java
index 54d52434f8..a61a65f900 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroReader.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroReader.java
@@ -381,7 +381,10 @@ public final class ManifestAvroReader implements 
AutoCloseable {
         /** Returns an independently owned block which remains valid after 
this reader advances. */
         public RawBlock stableCopy() {
             return new RawBlock(
-                    decoderContext, rawBlockCopySupported, block.stableCopy(), 
blockOrdinal);
+                    new DecoderContext(decoderContext.decoder.copy()),
+                    rawBlockCopySupported,
+                    block.stableCopy(),
+                    blockOrdinal);
         }
     }
 
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java
index 7df1318902..ff6e738f4d 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java
@@ -19,20 +19,32 @@
 package org.apache.paimon.operation;
 
 import org.apache.paimon.CoreOptions;
+import org.apache.paimon.format.FormatWriter;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.PositionOutputStream;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.ManifestAvroReader;
 import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.manifest.ManifestEntrySerializer;
 import org.apache.paimon.manifest.ManifestFile;
 import org.apache.paimon.manifest.ManifestFileMeta;
 import org.apache.paimon.manifest.ManifestFileMetaTestBase;
 import org.apache.paimon.options.Options;
+import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.RowType;
 
 import org.junit.jupiter.api.BeforeEach;
 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.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -90,6 +102,87 @@ public class ManifestFileMergerTest extends 
ManifestFileMetaTestBase {
                 .containsExactly("replacement");
     }
 
+    @Test
+    public void testFullCompactionWithStableLegacyManifestBlocks() throws 
Exception {
+        List<ManifestEntry> baseEntries = new ArrayList<>();
+
+        // Pending blocks contain many short records and therefore have a 
larger record count.
+        for (int i = 0; i < 2_000; i++) {
+            baseEntries.add(makeEntry(true, "short-" + i, null));
+        }
+
+        // Current blocks contain fewer long records while retaining a similar 
decompressed size.
+        String longName = String.join("", Collections.nCopies(2_048, "x"));
+        for (int i = 0; i < 200; i++) {
+            baseEntries.add(makeEntry(true, "long-" + i + "-" + longName, 
null));
+        }
+
+        ManifestFileMeta base = makeLegacyManifest(baseEntries);
+
+        List<Long> blockRecordCounts = new ArrayList<>();
+        try (ManifestAvroReader reader =
+                manifestFile.scanAvroBlocks(base.fileName(), base.fileSize())) 
{
+            assertThat(reader.rawBlockCopySupported()).isFalse();
+
+            while (reader.hasNext()) {
+                blockRecordCounts.add(reader.next().recordCount());
+            }
+        }
+
+        assertThat(blockRecordCounts.size()).isGreaterThan(2);
+
+        // Find the first block whose record count decreases because it 
contains long file names.
+        int changedBlock = -1;
+        long changedBlockStart = 0;
+        long blockStart = blockRecordCounts.get(0);
+
+        for (int i = 1; i < blockRecordCounts.size() - 1; i++) {
+            if (blockRecordCounts.get(i) < blockRecordCounts.get(i - 1)) {
+                changedBlock = i;
+                changedBlockStart = blockStart;
+                break;
+            }
+
+            blockStart += blockRecordCounts.get(i);
+        }
+
+        assertThat(changedBlock).isGreaterThan(0);
+
+        // Delete an entry from the middle of the selected block so that it 
must be rewritten.
+        int deletedEntryIndex =
+                Math.toIntExact(changedBlockStart + 
blockRecordCounts.get(changedBlock) / 2);
+
+        String deletedFileName = baseEntries.get(deletedEntryIndex).fileName();
+        ManifestFileMeta delta = makeManifest(makeEntry(false, 
deletedFileName, null));
+
+        List<ManifestFileMeta> newFilesForAbort = new ArrayList<>();
+        Optional<List<ManifestFileMeta>> compacted =
+                ManifestFileBlockMerger.tryFullCompaction(
+                        Arrays.asList(base, delta),
+                        newFilesForAbort,
+                        manifestFile,
+                        1,
+                        1,
+                        NO_PARTITION_TYPE,
+                        1);
+
+        assertThat(compacted).isPresent();
+
+        List<ManifestEntry> actual =
+                compacted.get().stream()
+                        .flatMap(
+                                meta ->
+                                        manifestFile.read(meta.fileName(), 
meta.fileSize())
+                                                .stream())
+                        .collect(Collectors.toList());
+
+        assertThat(actual.size()).isEqualTo(baseEntries.size() - 1);
+        assertThat(actual).allMatch(entry -> entry.kind() == FileKind.ADD);
+        assertThat(actual)
+                .extracting(entry -> entry.file().fileName())
+                .doesNotContain(deletedFileName);
+    }
+
     @Override
     public ManifestFile getManifestFile() {
         return manifestFile;
@@ -99,4 +192,53 @@ public class ManifestFileMergerTest extends 
ManifestFileMetaTestBase {
     public RowType getPartitionType() {
         return NO_PARTITION_TYPE;
     }
+
+    private ManifestFileMeta makeLegacyManifest(List<ManifestEntry> entries) 
throws Exception {
+        ManifestFileMeta current = makeManifest(entries.toArray(new 
ManifestEntry[0]));
+
+        RowType legacyFileType =
+                DataFileMeta.SCHEMA.project(
+                        new int[] {
+                            0, 1, 2, 3, 4, 5, 6, 7, 8,
+                            9, 10, 11, 12, 13, 14, 15, 16, 17
+                        });
+
+        List<DataField> legacyManifestFields =
+                ManifestEntry.MANIFEST_ROW_TYPE.getFields().stream()
+                        .map(
+                                field ->
+                                        ManifestEntry.FILE.equals(field.name())
+                                                ? field.newType(legacyFileType)
+                                                : field)
+                        .collect(Collectors.toList());
+
+        RowType legacyManifestType = new RowType(false, legacyManifestFields);
+
+        Path path = new Path(new Path(tempDir.toUri()), "manifest/" + 
current.fileName());
+
+        FileIO fileIO = LocalFileIO.create();
+        ManifestEntrySerializer serializer = new ManifestEntrySerializer();
+
+        try (PositionOutputStream out = fileIO.newOutputStream(path, true);
+                FormatWriter writer =
+                        
avro.createWriterFactory(legacyManifestType).create(out, "zstd")) {
+            for (ManifestEntry entry : entries) {
+                writer.addElement(serializer.toRow(entry));
+            }
+        }
+
+        return new ManifestFileMeta(
+                current.fileName(),
+                fileIO.getFileStatus(path).getLen(),
+                current.numAddedFiles(),
+                current.numDeletedFiles(),
+                current.partitionStats(),
+                current.schemaId(),
+                current.minBucket(),
+                current.maxBucket(),
+                current.minLevel(),
+                current.maxLevel(),
+                current.minRowId(),
+                current.maxRowId());
+    }
 }
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroRecordDecoder.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroRecordDecoder.java
index a4dafc1296..48788ad651 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroRecordDecoder.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroRecordDecoder.java
@@ -35,6 +35,7 @@ import java.nio.ByteBuffer;
  */
 public final class AvroRecordDecoder {
 
+    private final Schema writerSchema;
     private final Schema recordSchema;
     private final int recordBranch;
 
@@ -44,6 +45,7 @@ public final class AvroRecordDecoder {
     private int blockLength;
 
     AvroRecordDecoder(Schema writerSchema) {
+        this.writerSchema = writerSchema;
         if (writerSchema.getType() == Schema.Type.UNION) {
             int branchIndex = -1;
             Schema record = null;
@@ -71,6 +73,11 @@ public final class AvroRecordDecoder {
         }
     }
 
+    /** Creates an independent decoder with the same writer schema and no 
current block. */
+    public AvroRecordDecoder copy() {
+        return new AvroRecordDecoder(writerSchema);
+    }
+
     /** Returns the number of fields in the writer record. */
     public int fieldCount() {
         return recordSchema.getFields().size();

Reply via email to