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

lzljs3620320 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 8489a2d46 [flink] Add CompactManifestProcedure to enable compact 
manifest manually (#4327)
8489a2d46 is described below

commit 8489a2d4668bb506f019db5e1ed86354f00de10f
Author: YeJunHao <[email protected]>
AuthorDate: Sun Oct 20 15:40:13 2024 +0800

    [flink] Add CompactManifestProcedure to enable compact manifest manually 
(#4327)
---
 .../apache/paimon/operation/FileStoreCommit.java   |   3 +
 .../paimon/operation/FileStoreCommitImpl.java      | 234 +++++++++++++++++----
 .../paimon/operation/FileStoreCommitTest.java      |  88 +++++++-
 .../flink/procedure/CompactManifestProcedure.java  |  60 ++++++
 .../services/org.apache.paimon.factories.Factory   |   1 +
 .../procedure/CompactManifestProcedureITCase.java  | 133 ++++++++++++
 .../org/apache/paimon/spark/SparkProcedures.java   |   2 +
 .../spark/procedure/CompactManifestProcedure.java  |  95 +++++++++
 .../procedure/CompactManifestProcedureTest.scala   |  50 +++++
 9 files changed, 623 insertions(+), 43 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
index e15225793..43456cbe7 100644
--- a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
+++ b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
@@ -74,6 +74,9 @@ public interface FileStoreCommit extends AutoCloseable {
 
     void truncateTable(long commitIdentifier);
 
+    /** Compact the manifest entries only. */
+    void compactManifest();
+
     /** Abort an unsuccessful commit. The data files will be deleted. */
     void abort(List<CommitMessage> commitMessages);
 
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
index 34f928cc2..9ce089992 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
@@ -1025,47 +1025,7 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
                     e);
         }
 
-        boolean success;
-        try {
-            Callable<Boolean> callable =
-                    () -> {
-                        boolean committed =
-                                fileIO.tryToWriteAtomic(newSnapshotPath, 
newSnapshot.toJson());
-                        if (committed) {
-                            snapshotManager.commitLatestHint(newSnapshotId);
-                        }
-                        return committed;
-                    };
-            if (lock != null) {
-                success =
-                        lock.runWithLock(
-                                () ->
-                                        // fs.rename may not returns false if 
target file
-                                        // already exists, or even not atomic
-                                        // as we're relying on external 
locking, we can first
-                                        // check if file exist then rename to 
work around this
-                                        // case
-                                        !fileIO.exists(newSnapshotPath) && 
callable.call());
-            } else {
-                success = callable.call();
-            }
-        } catch (Throwable e) {
-            // exception when performing the atomic rename,
-            // we cannot clean up because we can't determine the success
-            throw new RuntimeException(
-                    String.format(
-                            "Exception occurs when committing snapshot #%d 
(path %s) by user %s "
-                                    + "with identifier %s and kind %s. "
-                                    + "Cannot clean up because we can't 
determine the success.",
-                            newSnapshotId,
-                            newSnapshotPath,
-                            commitUser,
-                            identifier,
-                            commitKind.name()),
-                    e);
-        }
-
-        if (success) {
+        if (commitSnapshotImpl(newSnapshot, newSnapshotPath)) {
             if (LOG.isDebugEnabled()) {
                 LOG.debug(
                         String.format(
@@ -1104,6 +1064,154 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
                 baseDataFiles);
     }
 
+    public void compactManifest() {
+        int cnt = 0;
+        ManifestCompactResult retryResult = null;
+        while (true) {
+            cnt++;
+            retryResult = compactManifest(retryResult);
+            if (retryResult.isSuccess()) {
+                break;
+            }
+
+            if (cnt >= commitMaxRetries) {
+                retryResult.cleanAll();
+                throw new RuntimeException(
+                        String.format(
+                                "Commit compact manifest failed after %s 
attempts, there maybe exist commit conflicts between multiple jobs.",
+                                commitMaxRetries));
+            }
+        }
+    }
+
+    private ManifestCompactResult compactManifest(@Nullable 
ManifestCompactResult lastResult) {
+        Snapshot latestSnapshot = snapshotManager.latestSnapshot();
+
+        if (latestSnapshot == null) {
+            return new SuccessManifestCompactResult();
+        }
+
+        List<ManifestFileMeta> mergeBeforeManifests =
+                manifestList.readDataManifests(latestSnapshot);
+        List<ManifestFileMeta> mergeAfterManifests;
+
+        if (lastResult != null) {
+            List<ManifestFileMeta> oldMergeBeforeManifests = 
lastResult.mergeBeforeManifests;
+            List<ManifestFileMeta> oldMergeAfterManifests = 
lastResult.mergeAfterManifests;
+
+            Set<String> retryMergeBefore =
+                    oldMergeBeforeManifests.stream()
+                            .map(ManifestFileMeta::fileName)
+                            .collect(Collectors.toSet());
+
+            List<ManifestFileMeta> manifestsFromOther =
+                    mergeBeforeManifests.stream()
+                            .filter(m -> 
!retryMergeBefore.remove(m.fileName()))
+                            .collect(Collectors.toList());
+
+            if (retryMergeBefore.isEmpty()) {
+                // no manifest compact from latest failed commit to latest 
commit
+                mergeAfterManifests = new ArrayList<>(oldMergeAfterManifests);
+                mergeAfterManifests.addAll(manifestsFromOther);
+            } else {
+                // manifest compact happens, quit
+                lastResult.cleanAll();
+                return new SuccessManifestCompactResult();
+            }
+        } else {
+            // the fist trial
+            mergeAfterManifests =
+                    ManifestFileMerger.merge(
+                            mergeBeforeManifests,
+                            manifestFile,
+                            manifestTargetSize.getBytes(),
+                            1,
+                            1,
+                            partitionType,
+                            manifestReadParallelism);
+
+            if (new HashSet<>(mergeBeforeManifests).equals(new 
HashSet<>(mergeAfterManifests))) {
+                // no need to commit this snapshot, because no compact were 
happened
+                return new SuccessManifestCompactResult();
+            }
+        }
+
+        String baseManifestList = manifestList.write(mergeAfterManifests);
+        String deltaManifestList = manifestList.write(Collections.emptyList());
+
+        // prepare snapshot file
+        Snapshot newSnapshot =
+                new Snapshot(
+                        latestSnapshot.id() + 1,
+                        latestSnapshot.schemaId(),
+                        baseManifestList,
+                        deltaManifestList,
+                        null,
+                        latestSnapshot.indexManifest(),
+                        commitUser,
+                        Long.MAX_VALUE,
+                        Snapshot.CommitKind.COMPACT,
+                        System.currentTimeMillis(),
+                        latestSnapshot.logOffsets(),
+                        latestSnapshot.totalRecordCount(),
+                        0L,
+                        0L,
+                        latestSnapshot.watermark(),
+                        latestSnapshot.statistics());
+
+        Path newSnapshotPath =
+                branchName.equals(DEFAULT_MAIN_BRANCH)
+                        ? snapshotManager.snapshotPath(newSnapshot.id())
+                        : 
snapshotManager.copyWithBranch(branchName).snapshotPath(newSnapshot.id());
+
+        if (!commitSnapshotImpl(newSnapshot, newSnapshotPath)) {
+            return new ManifestCompactResult(
+                    baseManifestList, deltaManifestList, mergeBeforeManifests, 
mergeAfterManifests);
+        } else {
+            return new SuccessManifestCompactResult();
+        }
+    }
+
+    private boolean commitSnapshotImpl(Snapshot newSnapshot, Path 
newSnapshotPath) {
+        try {
+            Callable<Boolean> callable =
+                    () -> {
+                        boolean committed =
+                                fileIO.tryToWriteAtomic(newSnapshotPath, 
newSnapshot.toJson());
+                        if (committed) {
+                            snapshotManager.commitLatestHint(newSnapshot.id());
+                        }
+                        return committed;
+                    };
+            if (lock != null) {
+                return lock.runWithLock(
+                        () ->
+                                // fs.rename may not returns false if target 
file
+                                // already exists, or even not atomic
+                                // as we're relying on external locking, we 
can first
+                                // check if file exist then rename to work 
around this
+                                // case
+                                !fileIO.exists(newSnapshotPath) && 
callable.call());
+            } else {
+                return callable.call();
+            }
+        } catch (Throwable e) {
+            // exception when performing the atomic rename,
+            // we cannot clean up because we can't determine the success
+            throw new RuntimeException(
+                    String.format(
+                            "Exception occurs when committing snapshot #%d 
(path %s) by user %s "
+                                    + "with identifier %s and kind %s. "
+                                    + "Cannot clean up because we can't 
determine the success.",
+                            newSnapshot.id(),
+                            newSnapshotPath,
+                            commitUser,
+                            newSnapshot.commitIdentifier(),
+                            newSnapshot.commitKind().name()),
+                    e);
+        }
+    }
+
     private List<SimpleFileEntry> readIncrementalChanges(
             Snapshot from, Snapshot to, List<BinaryRow> changedPartitions) {
         List<SimpleFileEntry> entries = new ArrayList<>();
@@ -1489,4 +1597,48 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
             return false;
         }
     }
+
+    private class ManifestCompactResult implements CommitResult {
+
+        private final String baseManifestList;
+        private final String deltaManifestList;
+        private final List<ManifestFileMeta> mergeBeforeManifests;
+        private final List<ManifestFileMeta> mergeAfterManifests;
+
+        public ManifestCompactResult(
+                String baseManifestList,
+                String deltaManifestList,
+                List<ManifestFileMeta> mergeBeforeManifests,
+                List<ManifestFileMeta> mergeAfterManifests) {
+            this.baseManifestList = baseManifestList;
+            this.deltaManifestList = deltaManifestList;
+            this.mergeBeforeManifests = mergeBeforeManifests;
+            this.mergeAfterManifests = mergeAfterManifests;
+        }
+
+        public void cleanAll() {
+            manifestList.delete(deltaManifestList);
+            cleanUpNoReuseTmpManifests(baseManifestList, mergeBeforeManifests, 
mergeAfterManifests);
+        }
+
+        @Override
+        public boolean isSuccess() {
+            return false;
+        }
+    }
+
+    private class SuccessManifestCompactResult extends ManifestCompactResult {
+
+        public SuccessManifestCompactResult() {
+            super(null, null, null, null);
+        }
+
+        @Override
+        public void cleanAll() {}
+
+        @Override
+        public boolean isSuccess() {
+            return true;
+        }
+    }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
index 77ee6b8e7..67945df60 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
@@ -33,6 +33,7 @@ import org.apache.paimon.index.IndexFileHandler;
 import org.apache.paimon.index.IndexFileMeta;
 import org.apache.paimon.manifest.IndexManifestEntry;
 import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.manifest.ManifestFileMeta;
 import org.apache.paimon.mergetree.compact.DeduplicateMergeFunction;
 import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.schema.Schema;
@@ -910,17 +911,100 @@ public class FileStoreCommitTest {
         assertThat(dvs.get("f2").isDeleted(3)).isTrue();
     }
 
+    @Test
+    public void testManifestCompact() throws Exception {
+        TestFileStore store = createStore(false);
+
+        List<KeyValue> keyValues = generateDataList(1);
+        BinaryRow partition = gen.getPartition(keyValues.get(0));
+        // commit 1
+        Snapshot snapshot1 =
+                store.commitData(keyValues, s -> partition, kv -> 0, 
Collections.emptyMap()).get(0);
+        // commit 2
+        Snapshot snapshot2 =
+                store.overwriteData(keyValues, s -> partition, kv -> 0, 
Collections.emptyMap())
+                        .get(0);
+        // commit 3
+        Snapshot snapshot3 =
+                store.overwriteData(keyValues, s -> partition, kv -> 0, 
Collections.emptyMap())
+                        .get(0);
+
+        long deleteNum =
+                
store.manifestListFactory().create().readDataManifests(snapshot3).stream()
+                        .mapToLong(ManifestFileMeta::numDeletedFiles)
+                        .sum();
+        assertThat(deleteNum).isGreaterThan(0);
+        store.newCommit().compactManifest();
+        Snapshot latest = store.snapshotManager().latestSnapshot();
+        assertThat(
+                        
store.manifestListFactory().create().readDataManifests(latest).stream()
+                                .mapToLong(ManifestFileMeta::numDeletedFiles)
+                                .sum())
+                .isEqualTo(0);
+    }
+
+    @Test
+    public void testManifestCompactFull() throws Exception {
+        // Disable full compaction by options.
+        TestFileStore store =
+                createStore(
+                        false,
+                        Collections.singletonMap(
+                                
CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(),
+                                String.valueOf(Long.MAX_VALUE)));
+
+        List<KeyValue> keyValues = generateDataList(1);
+        BinaryRow partition = gen.getPartition(keyValues.get(0));
+        // commit 1
+        Snapshot snapshot =
+                store.commitData(keyValues, s -> partition, kv -> 0, 
Collections.emptyMap()).get(0);
+
+        for (int i = 0; i < 100; i++) {
+            snapshot =
+                    store.overwriteData(keyValues, s -> partition, kv -> 0, 
Collections.emptyMap())
+                            .get(0);
+        }
+
+        long deleteNum =
+                
store.manifestListFactory().create().readDataManifests(snapshot).stream()
+                        .mapToLong(ManifestFileMeta::numDeletedFiles)
+                        .sum();
+        assertThat(deleteNum).isGreaterThan(0);
+        store.newCommit().compactManifest();
+        Snapshot latest = store.snapshotManager().latestSnapshot();
+        assertThat(
+                        
store.manifestListFactory().create().readDataManifests(latest).stream()
+                                .mapToLong(ManifestFileMeta::numDeletedFiles)
+                                .sum())
+                .isEqualTo(0);
+    }
+
+    private TestFileStore createStore(boolean failing, Map<String, String> 
options)
+            throws Exception {
+        return createStore(failing, 1, CoreOptions.ChangelogProducer.NONE, 
options);
+    }
+
     private TestFileStore createStore(boolean failing) throws Exception {
         return createStore(failing, 1);
     }
 
     private TestFileStore createStore(boolean failing, int numBucket) throws 
Exception {
-        return createStore(failing, numBucket, 
CoreOptions.ChangelogProducer.NONE);
+        return createStore(
+                failing, numBucket, CoreOptions.ChangelogProducer.NONE, 
Collections.emptyMap());
     }
 
     private TestFileStore createStore(
             boolean failing, int numBucket, CoreOptions.ChangelogProducer 
changelogProducer)
             throws Exception {
+        return createStore(failing, numBucket, changelogProducer, 
Collections.emptyMap());
+    }
+
+    private TestFileStore createStore(
+            boolean failing,
+            int numBucket,
+            CoreOptions.ChangelogProducer changelogProducer,
+            Map<String, String> options)
+            throws Exception {
         String root =
                 failing
                         ? FailingFileIO.getFailingPath(failingName, 
tempDir.toString())
@@ -934,7 +1018,7 @@ public class FileStoreCommitTest {
                                 
TestKeyValueGenerator.DEFAULT_PART_TYPE.getFieldNames(),
                                 TestKeyValueGenerator.getPrimaryKeys(
                                         
TestKeyValueGenerator.GeneratorMode.MULTI_PARTITIONED),
-                                Collections.emptyMap(),
+                                options,
                                 null));
         return new TestFileStore.Builder(
                         "avro",
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
new file mode 100644
index 000000000..3e52322a6
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
@@ -0,0 +1,60 @@
+/*
+ * 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.flink.procedure;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.operation.FileStoreCommit;
+import org.apache.paimon.table.FileStoreTable;
+
+import org.apache.flink.table.annotation.ArgumentHint;
+import org.apache.flink.table.annotation.DataTypeHint;
+import org.apache.flink.table.annotation.ProcedureHint;
+import org.apache.flink.table.procedure.ProcedureContext;
+
+import java.util.Collections;
+
+/** Compact manifest file to reduce deleted manifest entries. */
+public class CompactManifestProcedure extends ProcedureBase {
+
+    private static final String COMMIT_USER = 
"Compact-Manifest-Procedure-Committer";
+
+    @Override
+    public String identifier() {
+        return "compact_manifest";
+    }
+
+    @ProcedureHint(argument = {@ArgumentHint(name = "table", type = 
@DataTypeHint("STRING"))})
+    public String[] call(ProcedureContext procedureContext, String tableId) 
throws Exception {
+
+        FileStoreTable table =
+                (FileStoreTable)
+                        table(tableId)
+                                .copy(
+                                        Collections.singletonMap(
+                                                
CoreOptions.COMMIT_USER_PREFIX.key(), COMMIT_USER));
+
+        try (FileStoreCommit commit =
+                table.store()
+                        .newCommit(table.coreOptions().createCommitUser())
+                        .ignoreEmptyCommit(false)) {
+            commit.compactManifest();
+        }
+        return new String[] {"success"};
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
 
b/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
index 5014b9c3c..2cf57201d 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
+++ 
b/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
@@ -74,3 +74,4 @@ org.apache.paimon.flink.procedure.RenameTagProcedure
 org.apache.paimon.flink.procedure.FastForwardProcedure
 org.apache.paimon.flink.procedure.MarkPartitionDoneProcedure
 org.apache.paimon.flink.procedure.CloneProcedure
+org.apache.paimon.flink.procedure.CompactManifestProcedure
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
new file mode 100644
index 000000000..89cdc48d8
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
@@ -0,0 +1,133 @@
+/*
+ * 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.flink.procedure;
+
+import org.apache.paimon.flink.CatalogITCaseBase;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Objects;
+
+/** IT Case for {@link CompactManifestProcedure}. */
+public class CompactManifestProcedureITCase extends CatalogITCaseBase {
+
+    @Test
+    public void testManifestCompactProcedure() {
+        sql(
+                "CREATE TABLE T ("
+                        + " k INT,"
+                        + " v STRING,"
+                        + " hh INT,"
+                        + " dt STRING"
+                        + ") PARTITIONED BY (dt, hh) WITH ("
+                        + " 'write-only' = 'true',"
+                        + " 'file.format' = 'parquet',"
+                        + " 'manifest.full-compaction-threshold-size' = '10000 
T',"
+                        + " 'bucket' = '-1'"
+                        + ")");
+
+        sql(
+                "INSERT INTO T VALUES (1, '10', 15, '20221208'), (4, '100', 
16, '20221208'), (5, '1000', 15, '20221209')");
+
+        sql(
+                "INSERT OVERWRITE T VALUES (1, '10', 15, '20221208'), (4, 
'100', 16, '20221208'), (5, '1000', 15, '20221209')");
+
+        sql(
+                "INSERT OVERWRITE T VALUES (1, '10', 15, '20221208'), (4, 
'100', 16, '20221208'), (5, '1000', 15, '20221209')");
+
+        sql(
+                "INSERT OVERWRITE T VALUES (1, '101', 15, '20221208'), (4, 
'1001', 16, '20221208'), (5, '10001', 15, '20221209')");
+
+        Assertions.assertThat(
+                        sql("SELECT sum(num_deleted_files) FROM 
T$manifests").get(0).getField(0))
+                .isEqualTo(9L);
+
+        Assertions.assertThat(
+                        Objects.requireNonNull(
+                                        sql("CALL sys.compact_manifest(`table` 
=> 'default.T')")
+                                                .get(0)
+                                                .getField(0))
+                                .toString())
+                .isEqualTo("success");
+
+        Assertions.assertThat(
+                        sql("SELECT sum(num_deleted_files) FROM 
T$manifests").get(0).getField(0))
+                .isEqualTo(0L);
+
+        Assertions.assertThat(sql("SELECT * FROM T ORDER BY k").toString())
+                .isEqualTo(
+                        "[+I[1, 101, 15, 20221208], +I[4, 1001, 16, 20221208], 
+I[5, 10001, 15, 20221209]]");
+    }
+
+    @Test
+    public void testManifestCompactProcedureWithBranch() {
+        sql(
+                "CREATE TABLE T ("
+                        + " k INT,"
+                        + " v STRING,"
+                        + " hh INT,"
+                        + " dt STRING"
+                        + ") PARTITIONED BY (dt, hh) WITH ("
+                        + " 'write-only' = 'true',"
+                        + " 'manifest.full-compaction-threshold-size' = '10000 
T',"
+                        + " 'bucket' = '-1'"
+                        + ")");
+
+        sql(
+                "INSERT INTO `T` VALUES (1, '10', 15, '20221208'), (4, '100', 
16, '20221208'), (5, '1000', 15, '20221209')");
+
+        sql("CALL sys.create_tag('default.T', 'tag1', 1)");
+
+        sql("call sys.create_branch('default.T', 'branch1', 'tag1')");
+
+        sql(
+                "INSERT OVERWRITE T$branch_branch1 VALUES (1, '10', 15, 
'20221208'), (4, '100', 16, '20221208'), (5, '1000', 15, '20221209')");
+
+        sql(
+                "INSERT OVERWRITE T$branch_branch1 VALUES (1, '10', 15, 
'20221208'), (4, '100', 16, '20221208'), (5, '1000', 15, '20221209')");
+
+        sql(
+                "INSERT OVERWRITE T$branch_branch1 VALUES (1, '101', 15, 
'20221208'), (4, '1001', 16, '20221208'), (5, '10001', 15, '20221209')");
+
+        Assertions.assertThat(
+                        sql("SELECT sum(num_deleted_files) FROM 
T$branch_branch1$manifests")
+                                .get(0)
+                                .getField(0))
+                .isEqualTo(9L);
+
+        Assertions.assertThat(
+                        Objects.requireNonNull(
+                                        sql("CALL sys.compact_manifest(`table` 
=> 'default.T$branch_branch1')")
+                                                .get(0)
+                                                .getField(0))
+                                .toString())
+                .isEqualTo("success");
+
+        Assertions.assertThat(
+                        sql("SELECT sum(num_deleted_files) FROM 
T$branch_branch1$manifests")
+                                .get(0)
+                                .getField(0))
+                .isEqualTo(0L);
+
+        Assertions.assertThat(sql("SELECT * FROM T$branch_branch1 ORDER BY 
k").toString())
+                .isEqualTo(
+                        "[+I[1, 101, 15, 20221208], +I[4, 1001, 16, 20221208], 
+I[5, 10001, 15, 20221209]]");
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
index d2a718041..dee0c38d4 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.spark;
 
+import org.apache.paimon.spark.procedure.CompactManifestProcedure;
 import org.apache.paimon.spark.procedure.CompactProcedure;
 import org.apache.paimon.spark.procedure.CreateBranchProcedure;
 import org.apache.paimon.spark.procedure.CreateTagFromTimestampProcedure;
@@ -81,6 +82,7 @@ public class SparkProcedures {
         procedureBuilders.put("fast_forward", FastForwardProcedure::builder);
         procedureBuilders.put("reset_consumer", 
ResetConsumerProcedure::builder);
         procedureBuilders.put("mark_partition_done", 
MarkPartitionDoneProcedure::builder);
+        procedureBuilders.put("compact_manifest", 
CompactManifestProcedure::builder);
         return procedureBuilders.build();
     }
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
new file mode 100644
index 000000000..dd064d892
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
@@ -0,0 +1,95 @@
+/*
+ * 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.spark.procedure;
+
+import org.apache.paimon.operation.FileStoreCommit;
+import org.apache.paimon.table.FileStoreTable;
+
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.types.DataTypes;
+import org.apache.spark.sql.types.Metadata;
+import org.apache.spark.sql.types.StructField;
+import org.apache.spark.sql.types.StructType;
+
+import static org.apache.spark.sql.types.DataTypes.StringType;
+
+/**
+ * Compact manifest procedure. Usage:
+ *
+ * <pre><code>
+ *  CALL sys.compact_manifest(table => 'tableId')
+ * </code></pre>
+ */
+public class CompactManifestProcedure extends BaseProcedure {
+
+    private static final ProcedureParameter[] PARAMETERS =
+            new ProcedureParameter[] {ProcedureParameter.required("table", 
StringType)};
+
+    private static final StructType OUTPUT_TYPE =
+            new StructType(
+                    new StructField[] {
+                        new StructField("result", DataTypes.BooleanType, true, 
Metadata.empty())
+                    });
+
+    protected CompactManifestProcedure(TableCatalog tableCatalog) {
+        super(tableCatalog);
+    }
+
+    @Override
+    public ProcedureParameter[] parameters() {
+        return PARAMETERS;
+    }
+
+    @Override
+    public StructType outputType() {
+        return OUTPUT_TYPE;
+    }
+
+    @Override
+    public InternalRow[] call(InternalRow args) {
+
+        Identifier tableIdent = toIdentifier(args.getString(0), 
PARAMETERS[0].name());
+        FileStoreTable table = (FileStoreTable) 
loadSparkTable(tableIdent).getTable();
+
+        try (FileStoreCommit commit =
+                table.store()
+                        .newCommit(table.coreOptions().createCommitUser())
+                        .ignoreEmptyCommit(false)) {
+            commit.compactManifest();
+        }
+
+        return new InternalRow[] {newInternalRow(true)};
+    }
+
+    @Override
+    public String description() {
+        return "This procedure execute compact action on paimon table.";
+    }
+
+    public static ProcedureBuilder builder() {
+        return new Builder<CompactManifestProcedure>() {
+            @Override
+            public CompactManifestProcedure doBuild() {
+                return new CompactManifestProcedure(tableCatalog());
+            }
+        };
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
new file mode 100644
index 000000000..c1c902513
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
@@ -0,0 +1,50 @@
+/*
+ * 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.spark.procedure
+
+import org.apache.paimon.spark.PaimonSparkTestBase
+
+import org.apache.spark.sql.streaming.StreamTest
+import org.assertj.core.api.Assertions
+
+/** Test compact manifest procedure. See [[CompactManifestProcedure]]. */
+class CompactManifestProcedureTest extends PaimonSparkTestBase with StreamTest 
{
+
+  test("Paimon Procedure: compact manifest") {
+    spark.sql(
+      s"""
+         |CREATE TABLE T (id INT, value STRING, dt STRING, hh INT)
+         |TBLPROPERTIES ('bucket'='-1', 'write-only'='true', 
'compaction.min.file-num'='2', 'compaction.max.file-num'='2')
+         |PARTITIONED BY (dt, hh)
+         |""".stripMargin)
+
+    spark.sql(s"INSERT INTO T VALUES (5, '5', '2024-01-02', 0), (6, '6', 
'2024-01-02', 1)")
+    spark.sql(s"INSERT OVERWRITE T VALUES (5, '5', '2024-01-02', 0), (6, '6', 
'2024-01-02', 1)")
+    spark.sql(s"INSERT OVERWRITE T VALUES (5, '5', '2024-01-02', 0), (6, '6', 
'2024-01-02', 1)")
+    spark.sql(s"INSERT OVERWRITE T VALUES (5, '5', '2024-01-02', 0), (6, '6', 
'2024-01-02', 1)")
+
+    Thread.sleep(10000);
+
+    var rows = spark.sql("SELECT sum(num_deleted_files) FROM 
`T$manifests`").collectAsList()
+    Assertions.assertThat(rows.get(0).getLong(0)).isEqualTo(6L)
+    spark.sql("CALL sys.compact_manifest(table => 'T')")
+    rows = spark.sql("SELECT sum(num_deleted_files) FROM 
`T$manifests`").collectAsList()
+    Assertions.assertThat(rows.get(0).getLong(0)).isEqualTo(0L)
+  }
+}


Reply via email to