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 aef812111e [flink] Support compact_chain_table procedure (#8508)
aef812111e is described below

commit aef812111e4e04079be624c52d895fcc27431e01
Author: Juntao Zhang <[email protected]>
AuthorDate: Fri Jul 10 08:37:23 2026 +0800

    [flink] Support compact_chain_table procedure (#8508)
---
 docs/docs/flink/procedures.md                      |  23 ++
 .../procedure/CompactChainTableProcedure.java      |  71 ++++
 .../flink/action/CompactChainTableAction.java      | 159 +++++++++
 .../action/CompactChainTableActionFactory.java     |  61 ++++
 .../procedure/CompactChainTableProcedure.java      |  72 ++++
 .../paimon/flink/source/FlinkSourceBuilder.java    |  10 +-
 .../paimon/flink/source/StaticFileStoreSource.java |  20 +-
 .../paimon/flink/source/SystemTableSource.java     |   4 +-
 .../services/org.apache.paimon.factories.Factory   |   2 +
 .../action/CompactChainTableActionITCase.java      | 181 ++++++++++
 .../CompactChainTableProcedureITCase.java          | 395 +++++++++++++++++++++
 11 files changed, 993 insertions(+), 5 deletions(-)

diff --git a/docs/docs/flink/procedures.md b/docs/docs/flink/procedures.md
index d264a58ca2..a8ab2abb65 100644
--- a/docs/docs/flink/procedures.md
+++ b/docs/docs/flink/procedures.md
@@ -139,6 +139,29 @@ All available procedures are listed below.
             compat_strategy => 'full')
       </td>
    </tr>
+   <tr>
+      <td>compact_chain_table</td>
+      <td>
+         -- Use named argument<br/>
+         CALL [catalog.]sys.compact_chain_table(
+            `table` => 'table',
+            partition => 'partition',
+            overwrite => overwrite) <br/><br/>
+         -- Use indexed argument<br/>
+         CALL [catalog.]sys.compact_chain_table('table', 'partition') <br/>
+         CALL [catalog.]sys.compact_chain_table('table', 'partition', 
overwrite) <br/><br/>
+      </td>
+      <td>
+         To compact chain table by merging snapshot and delta branches into 
the snapshot branch. Arguments:
+            <li>table: the target chain table identifier. Cannot be empty.</li>
+            <li>partition: partition specification format (e.g., 
'dt=20250810,hour=22'). Cannot be empty.</li>
+            <li>overwrite: whether to overwrite if the partition already 
exists in the snapshot branch. Default is false. Optional.</li>
+      </td>
+      <td>
+         CALL sys.compact_chain_table(`table` => 'default.T', partition => 
'dt=20250810,hour=22')<br/><br/>
+         CALL sys.compact_chain_table('default.T', 'dt=20250810,hour=22', true)
+      </td>
+   </tr>
    <tr>
       <td>create_tag</td>
       <td>
diff --git 
a/paimon-flink/paimon-flink-1.18/src/main/java/org/apache/paimon/flink/procedure/CompactChainTableProcedure.java
 
b/paimon-flink/paimon-flink-1.18/src/main/java/org/apache/paimon/flink/procedure/CompactChainTableProcedure.java
new file mode 100644
index 0000000000..57e4d4cb26
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-1.18/src/main/java/org/apache/paimon/flink/procedure/CompactChainTableProcedure.java
@@ -0,0 +1,71 @@
+/*
+ * 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.catalog.Identifier;
+import org.apache.paimon.flink.action.CompactChainTableAction;
+
+import org.apache.flink.table.procedure.ProcedureContext;
+
+import java.util.Map;
+
+/**
+ * Compact database procedure. Usage:
+ *
+ * <pre><code>
+ *  -- NOTE: use '' as placeholder for optional arguments
+ *
+ *  -- compact chain table (tableId should be 'database_name.table_name')
+ *  CALL sys.compact_chain_table('tableId', 'partition')
+ *
+ *  -- compact chain table with overwrite
+ *  CALL sys.compact_chain_table('tableId', 'partition', true)
+ *
+ * </code></pre>
+ */
+public class CompactChainTableProcedure extends ProcedureBase {
+
+    public static final String IDENTIFIER = "compact_chain_table";
+
+    public String[] call(ProcedureContext procedureContext, String tableId, 
String partition)
+            throws Exception {
+        return call(procedureContext, tableId, partition, null);
+    }
+
+    public String[] call(
+            ProcedureContext procedureContext, String tableId, String 
partition, Boolean overwrite)
+            throws Exception {
+        Map<String, String> catalogOptions = catalog.options();
+        Identifier identifier = Identifier.fromString(tableId);
+        CompactChainTableAction action =
+                new CompactChainTableAction(
+                        identifier.getDatabaseName(),
+                        identifier.getObjectName(),
+                        catalogOptions,
+                        partition,
+                        overwrite);
+        return execute(
+                procedureContext, action, "Compact chain table Job : " + 
identifier.getFullName());
+    }
+
+    @Override
+    public String identifier() {
+        return IDENTIFIER;
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactChainTableAction.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactChainTableAction.java
new file mode 100644
index 0000000000..5dd78e9dab
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactChainTableAction.java
@@ -0,0 +1,159 @@
+/*
+ * 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.action;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.flink.sink.FlinkSinkBuilder;
+import org.apache.paimon.flink.source.FlinkSourceBuilder;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.partition.PartitionPredicate;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.predicate.PredicateBuilder;
+import org.apache.paimon.table.ChainGroupReadTable;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.ParameterUtils;
+import org.apache.paimon.utils.StringUtils;
+
+import org.apache.flink.api.common.RuntimeExecutionMode;
+import org.apache.flink.configuration.ExecutionOptions;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.functions.sink.v2.DiscardingSink;
+import org.apache.flink.table.data.RowData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static 
org.apache.paimon.flink.FlinkConnectorOptions.SCAN_DEDICATED_SPLIT_GENERATION;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/**
+ * Action to compact chain table by merging snapshot and delta branches into 
the snapshot branch.
+ */
+public class CompactChainTableAction extends TableActionBase {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(CompactChainTableAction.class);
+
+    public static final String IDENTIFIER = "compact_chain_table";
+
+    protected String partition;
+
+    protected boolean overwrite;
+
+    public CompactChainTableAction(
+            String databaseName,
+            String tableName,
+            Map<String, String> catalogConfig,
+            String partition,
+            Boolean overwrite) {
+        super(databaseName, tableName, catalogConfig);
+        this.partition = partition;
+        this.overwrite = overwrite != null && overwrite;
+    }
+
+    public CompactChainTableAction withOverwrite(boolean overwrite) {
+        this.overwrite = overwrite;
+        return this;
+    }
+
+    public CompactChainTableAction withPartition(String partition) {
+        this.partition = partition;
+        return this;
+    }
+
+    @Override
+    public void build() throws Exception {
+        checkArgument(
+                StringUtils.isNotEmpty(partition),
+                "Partition string cannot be empty for compact_chain_table");
+        checkArgument(
+                !partition.contains(";"),
+                "compact_chain_table only supports a single partition, but 
multiple partitions were provided: %s",
+                partition);
+        checkArgument(
+                new CoreOptions(table.options()).isChainTable(),
+                "compact_chain_table only supports chain table");
+        checkArgument(
+                table instanceof FallbackReadFileStoreTable,
+                "Table %s is not a chain table",
+                identifier.getFullName());
+        checkArgument(
+                env.getConfiguration().get(ExecutionOptions.RUNTIME_MODE)
+                        == RuntimeExecutionMode.BATCH,
+                "compact_chain_table only supports batch execution mode");
+        checkArgument(
+                
!Options.fromMap(table.options()).get(SCAN_DEDICATED_SPLIT_GENERATION),
+                "compact_chain_table does not support %s.",
+                SCAN_DEDICATED_SPLIT_GENERATION.key());
+
+        ChainGroupReadTable chainTable =
+                (ChainGroupReadTable) ((FallbackReadFileStoreTable) 
table).other();
+        FileStoreTable snapshotTable = chainTable.wrapped();
+
+        RowType partitionType = snapshotTable.schema().logicalPartitionType();
+        String partitionDefaultName = 
snapshotTable.coreOptions().partitionDefaultName();
+        Map<String, String> partitionSpec = 
ParameterUtils.parseCommaSeparatedKeyValues(partition);
+        Predicate partitionPredicate =
+                PredicateBuilder.partition(partitionSpec, partitionType, 
partitionDefaultName);
+        checkArgument(
+                partitionPredicate != null,
+                "Failed to build partition predicate for partition: %s",
+                partition);
+        PartitionPredicate predicate =
+                PartitionPredicate.fromPredicate(partitionType, 
partitionPredicate);
+
+        boolean partitionExists =
+                
!snapshotTable.newScan().withPartitionFilter(predicate).listPartitions().isEmpty();
+
+        if (partitionExists && !overwrite) {
+            LOG.info(
+                    "Partition {} already exists in snapshot branch, skipping 
compaction.",
+                    partition);
+            buildEmptyPipeline();
+            return;
+        }
+        DataStream<RowData> source =
+                new FlinkSourceBuilder(chainTable)
+                        .env(env)
+                        .sourceBounded(true)
+                        .partitionPredicate(predicate)
+                        .withSkipPreloadTargetSnapshot(partitionExists && 
overwrite)
+                        .sourceName(identifier.getFullName() + 
"-chain-compact-source")
+                        .build();
+
+        FlinkSinkBuilder sinkBuilder =
+                new FlinkSinkBuilder(
+                                snapshotTable.copy(
+                                        Collections.singletonMap(
+                                                
CoreOptions.DYNAMIC_PARTITION_OVERWRITE.key(),
+                                                "true")))
+                        .forRowData(source);
+        if (partitionExists) {
+            sinkBuilder.overwrite(partitionSpec);
+        }
+        sinkBuilder.build();
+    }
+
+    private void buildEmptyPipeline() {
+        env.fromSequence(0, 0).sinkTo(new DiscardingSink<>());
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactChainTableActionFactory.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactChainTableActionFactory.java
new file mode 100644
index 0000000000..f777f9276d
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactChainTableActionFactory.java
@@ -0,0 +1,61 @@
+/*
+ * 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.action;
+
+import java.util.Map;
+import java.util.Optional;
+
+/** Factory to create {@link CompactChainTableAction}. */
+public class CompactChainTableActionFactory implements ActionFactory {
+
+    public static final String IDENTIFIER = "compact_chain_table";
+
+    private static final String PARTITION = "partition";
+    private static final String OVERWRITE = "overwrite";
+
+    @Override
+    public String identifier() {
+        return IDENTIFIER;
+    }
+
+    @Override
+    public Optional<Action> create(MultipleParameterToolAdapter params) {
+        String database = params.getRequired(DATABASE);
+        String table = params.getRequired(TABLE);
+        Map<String, String> catalogConfig = catalogConfigMap(params);
+        String partition = params.getRequired(PARTITION);
+        boolean overwrite = params.getBoolean(OVERWRITE, false);
+        return Optional.of(
+                new CompactChainTableAction(database, table, catalogConfig, 
partition, overwrite));
+    }
+
+    @Override
+    public void printHelp() {
+        System.out.println("Action \"compact_chain_table\" compacts a chain 
table partition.");
+        System.out.println();
+        System.out.println("Syntax:");
+        System.out.println(
+                "  compact_chain_table --warehouse <warehouse_path> --database 
<db> --table <table> --partition <partition> [--overwrite true]");
+        System.out.println("Examples:");
+        System.out.println(
+                "  compact_chain_table --warehouse hdfs:///path/to/warehouse 
--database test_db --table test_table --partition dt=20250810");
+        System.out.println(
+                "  compact_chain_table --warehouse hdfs:///path/to/warehouse 
--database test_db --table test_table --partition dt=20250810,hour=22 
--overwrite true");
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactChainTableProcedure.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactChainTableProcedure.java
new file mode 100644
index 0000000000..0e23d3322d
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactChainTableProcedure.java
@@ -0,0 +1,72 @@
+/*
+ * 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.catalog.Identifier;
+import org.apache.paimon.flink.action.CompactChainTableAction;
+
+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.Map;
+
+/**
+ * Procedure to compact chain table. Usage:
+ *
+ * <pre><code>
+ *  -- Compact chain table, overwrite default is false
+ *  CALL sys.compact_chain_table('db.table', 'dt=20250810,hour=22', [true])
+ * </code></pre>
+ */
+public class CompactChainTableProcedure extends ProcedureBase {
+
+    public static final String IDENTIFIER = "compact_chain_table";
+
+    @ProcedureHint(
+            argument = {
+                @ArgumentHint(name = "table", type = @DataTypeHint("STRING")),
+                @ArgumentHint(name = "partition", type = 
@DataTypeHint("STRING")),
+                @ArgumentHint(
+                        name = "overwrite",
+                        type = @DataTypeHint("BOOLEAN"),
+                        isOptional = true)
+            })
+    public String[] call(
+            ProcedureContext procedureContext, String tableId, String 
partition, Boolean overwrite)
+            throws Exception {
+        Map<String, String> catalogOptions = catalog.options();
+        Identifier identifier = Identifier.fromString(tableId);
+        CompactChainTableAction action =
+                new CompactChainTableAction(
+                        identifier.getDatabaseName(),
+                        identifier.getObjectName(),
+                        catalogOptions,
+                        partition,
+                        overwrite);
+        return execute(
+                procedureContext, action, "Compact chain table Job : " + 
identifier.getFullName());
+    }
+
+    @Override
+    public String identifier() {
+        return IDENTIFIER;
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
index d00d2843ff..b80b52be77 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
@@ -90,12 +90,14 @@ public class FlinkSourceBuilder {
     @Nullable private Long limit;
     @Nullable private WatermarkStrategy<RowData> watermarkStrategy;
     @Nullable private DynamicPartitionFilteringInfo 
dynamicPartitionFilteringInfo;
+    private boolean skipPreloadTargetSnapshot;
 
     public FlinkSourceBuilder(Table table) {
         this.table = table;
         this.sourceName = table.name();
         this.conf = Options.fromMap(table.options());
         this.unordered = unordered(table);
+        this.skipPreloadTargetSnapshot = false;
     }
 
     private static boolean unordered(Table table) {
@@ -186,6 +188,11 @@ public class FlinkSourceBuilder {
         return this;
     }
 
+    public FlinkSourceBuilder withSkipPreloadTargetSnapshot(boolean skip) {
+        this.skipPreloadTargetSnapshot = skip;
+        return this;
+    }
+
     private ReadBuilder createReadBuilder(@Nullable 
org.apache.paimon.types.RowType readType) {
         ReadBuilder readBuilder = table.newReadBuilder();
         if (readType != null) {
@@ -213,7 +220,8 @@ public class FlinkSourceBuilder {
                         
options.get(FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_ASSIGN_MODE),
                         dynamicPartitionFilteringInfo,
                         outerProject(),
-                        options.get(CoreOptions.BLOB_AS_DESCRIPTOR)));
+                        options.get(CoreOptions.BLOB_AS_DESCRIPTOR),
+                        skipPreloadTargetSnapshot));
     }
 
     private DataStream<RowData> buildContinuousFileSource() {
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSource.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSource.java
index 775100aa81..940feb9d31 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSource.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSource.java
@@ -23,6 +23,7 @@ import org.apache.paimon.flink.metrics.FlinkMetricRegistry;
 import org.apache.paimon.flink.source.assigners.FIFOSplitAssigner;
 import org.apache.paimon.flink.source.assigners.PreAssignSplitAssigner;
 import org.apache.paimon.flink.source.assigners.SplitAssigner;
+import org.apache.paimon.table.ChainGroupReadTable;
 import org.apache.paimon.table.source.InnerTableScan;
 import org.apache.paimon.table.source.ReadBuilder;
 import org.apache.paimon.table.source.TableScan;
@@ -49,13 +50,23 @@ public class StaticFileStoreSource extends FlinkSource {
 
     @Nullable private final DynamicPartitionFilteringInfo 
dynamicPartitionFilteringInfo;
 
+    private final boolean skipPreloadTargetSnapshot;
+
     public StaticFileStoreSource(
             ReadBuilder readBuilder,
             @Nullable Long limit,
             int splitBatchSize,
             SplitAssignMode splitAssignMode,
             boolean blobAsDescriptor) {
-        this(readBuilder, limit, splitBatchSize, splitAssignMode, null, null, 
blobAsDescriptor);
+        this(
+                readBuilder,
+                limit,
+                splitBatchSize,
+                splitAssignMode,
+                null,
+                null,
+                blobAsDescriptor,
+                false);
     }
 
     public StaticFileStoreSource(
@@ -65,11 +76,13 @@ public class StaticFileStoreSource extends FlinkSource {
             SplitAssignMode splitAssignMode,
             @Nullable DynamicPartitionFilteringInfo 
dynamicPartitionFilteringInfo,
             @Nullable NestedProjectedRowData rowData,
-            boolean blobAsDescriptor) {
+            boolean blobAsDescriptor,
+            boolean skipPreloadTargetSnapshot) {
         super(readBuilder, limit, rowData, blobAsDescriptor);
         this.splitBatchSize = splitBatchSize;
         this.splitAssignMode = splitAssignMode;
         this.dynamicPartitionFilteringInfo = dynamicPartitionFilteringInfo;
+        this.skipPreloadTargetSnapshot = skipPreloadTargetSnapshot;
     }
 
     @Override
@@ -92,6 +105,9 @@ public class StaticFileStoreSource extends FlinkSource {
     private List<FileStoreSourceSplit> getSplits(SplitEnumeratorContext 
context) {
         FileStoreSourceSplitGenerator splitGenerator = new 
FileStoreSourceSplitGenerator();
         TableScan scan = readBuilder.newScan();
+        if (skipPreloadTargetSnapshot && scan instanceof 
ChainGroupReadTable.ChainTableBatchScan) {
+            ((ChainGroupReadTable.ChainTableBatchScan) 
scan).skipPreloadTargetSnapshot();
+        }
         // register scan metrics
         if (context.metricGroup() != null) {
             ((InnerTableScan) scan)
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/SystemTableSource.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/SystemTableSource.java
index d9ede90a14..a78f44bc6f 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/SystemTableSource.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/SystemTableSource.java
@@ -114,8 +114,8 @@ public class SystemTableSource extends FlinkTableSource {
                             Boolean.parseBoolean(
                                     table.options()
                                             .getOrDefault(
-                                                    
CoreOptions.BLOB_AS_DESCRIPTOR.key(),
-                                                    "false")));
+                                                    
CoreOptions.BLOB_AS_DESCRIPTOR.key(), "false")),
+                            false);
         }
         return new PaimonDataStreamScanProvider(
                 source.getBoundedness() == Boundedness.BOUNDED,
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 26a1a2b9cb..b9696067ae 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
@@ -51,10 +51,12 @@ org.apache.paimon.flink.action.RescaleActionFactory
 org.apache.paimon.flink.action.CloneActionFactory
 org.apache.paimon.flink.action.DataEvolutionMergeIntoActionFactory
 org.apache.paimon.flink.action.ReassignRowIdActionFactory
+org.apache.paimon.flink.action.CompactChainTableActionFactory
 
 ### procedure factories
 org.apache.paimon.flink.procedure.CompactDatabaseProcedure
 org.apache.paimon.flink.procedure.CompactProcedure
+org.apache.paimon.flink.procedure.CompactChainTableProcedure
 org.apache.paimon.flink.procedure.RewriteFileIndexProcedure
 org.apache.paimon.flink.procedure.CreateTagProcedure
 org.apache.paimon.flink.procedure.CreateTagFromTimestampProcedure
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/CompactChainTableActionITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/CompactChainTableActionITCase.java
new file mode 100644
index 0000000000..c9bca5b6b7
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/CompactChainTableActionITCase.java
@@ -0,0 +1,181 @@
+/*
+ * 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.action;
+
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.source.ReadBuilder;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableRead;
+
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap;
+
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.apache.paimon.flink.util.ReadWriteTableTestUtil.bEnv;
+import static org.apache.paimon.flink.util.ReadWriteTableTestUtil.buildDdl;
+import static org.apache.paimon.flink.util.ReadWriteTableTestUtil.init;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** IT cases for {@link CompactChainTableAction}. */
+public class CompactChainTableActionITCase extends CompactActionITCaseBase {
+
+    private static final List<String> FIELDS_SPEC =
+            Arrays.asList("t1 INT", "t2 INT", "t3 INT", "dt STRING");
+    private static final List<String> PRIMARY_KEYS = Arrays.asList("dt", "t1");
+    private static final List<String> PARTITION_KEYS = 
Collections.singletonList("dt");
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        init(warehouse);
+        prepareChainTable(Collections.emptyMap());
+    }
+
+    private void prepareChainTable(Map<String, String> extraOptions) throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put("bucket", "1");
+        options.put("bucket-key", "t1");
+        options.put("sequence.field", "t2");
+        options.put("merge-engine", "deduplicate");
+        options.put("chain-table.enabled", "true");
+        options.put("partition.timestamp-pattern", "$dt");
+        options.put("partition.timestamp-formatter", "yyyyMMdd");
+        options.putAll(extraOptions);
+
+        String ddl = buildDdl(tableName, FIELDS_SPEC, PRIMARY_KEYS, 
PARTITION_KEYS, options);
+        bEnv.executeSql(ddl).await();
+
+        // Create snapshot / delta branches and configure fallback branches.
+        bEnv.executeSql(
+                        String.format(
+                                "CALL sys.create_branch('%s.%s', 'snapshot')", 
database, tableName))
+                .await();
+        bEnv.executeSql(
+                        String.format(
+                                "CALL sys.create_branch('%s.%s', 'delta')", 
database, tableName))
+                .await();
+        bEnv.executeSql(
+                        String.format(
+                                "ALTER TABLE `%s`.`%s` SET 
('scan.fallback-snapshot-branch' = 'snapshot', 'scan.fallback-delta-branch' = 
'delta')",
+                                database, tableName))
+                .await();
+        bEnv.executeSql(
+                        String.format(
+                                "ALTER TABLE `%s`.`%s$branch_snapshot` SET 
('scan.fallback-snapshot-branch' = 'snapshot', 'scan.fallback-delta-branch' = 
'delta')",
+                                database, tableName))
+                .await();
+        bEnv.executeSql(
+                        String.format(
+                                "ALTER TABLE `%s`.`%s$branch_delta` SET 
('scan.fallback-snapshot-branch' = 'snapshot', 'scan.fallback-delta-branch' = 
'delta')",
+                                database, tableName))
+                .await();
+    }
+
+    private void writeBranch(String branch, GenericRow... rows) throws 
Exception {
+        FileStoreTable branchTable = 
getFileStoreTable(tableName).switchToBranch(branch);
+        String commitUser = UUID.randomUUID().toString();
+        try (org.apache.paimon.table.sink.StreamTableWrite write =
+                        
branchTable.newStreamWriteBuilder().withCommitUser(commitUser).newWrite();
+                org.apache.paimon.table.sink.StreamTableCommit commit =
+                        branchTable
+                                .newStreamWriteBuilder()
+                                .withCommitUser(commitUser)
+                                .newCommit()) {
+            for (GenericRow row : rows) {
+                write.write(row);
+            }
+            commit.commit(0L, write.prepareCommit(true, 0L));
+        }
+    }
+
+    private List<String> readSnapshot(Map<String, String> partitionFilter) 
throws Exception {
+        FileStoreTable snapshotTable = 
getFileStoreTable(tableName).switchToBranch("snapshot");
+        ReadBuilder readBuilder =
+                
snapshotTable.newReadBuilder().withPartitionFilter(partitionFilter);
+        TableRead read = readBuilder.newRead();
+        List<Split> splits = readBuilder.newScan().plan().splits();
+        return getResult(read, splits, ROW_TYPE);
+    }
+
+    private void runAction(String partition, boolean overwrite) throws 
Exception {
+        CompactChainTableAction action =
+                createAction(
+                        CompactChainTableAction.class,
+                        "compact_chain_table",
+                        "--warehouse",
+                        warehouse,
+                        "--database",
+                        database,
+                        "--table",
+                        tableName,
+                        "--partition",
+                        partition,
+                        "--overwrite",
+                        String.valueOf(overwrite));
+        StreamExecutionEnvironment env = 
streamExecutionEnvironmentBuilder().batchMode().build();
+        action.withStreamExecutionEnvironment(env).run();
+    }
+
+    @Test
+    public void testCompactChainTableBasic() throws Exception {
+        // Compact empty partition
+        runAction("dt=20260708", false);
+
+        writeBranch(
+                "snapshot",
+                rowData(1, 1, 1, BinaryString.fromString("20260708")),
+                rowData(2, 1, 1, BinaryString.fromString("20260708")));
+        writeBranch("delta", rowData(3, 1, 1, 
BinaryString.fromString("20260709")));
+        // Compact a partition that only exists in the delta branch.
+        runAction("dt=20260709", false);
+        // Compact empty partition
+        runAction("dt=20260710", false);
+
+        assertThat(readSnapshot(ImmutableMap.of("dt", "20260709")))
+                .containsExactlyInAnyOrder(
+                        "+I[1, 1, 1, 20260709]", "+I[2, 1, 1, 20260709]", 
"+I[3, 1, 1, 20260709]");
+    }
+
+    @Test
+    public void testCompactChainTableOverwrite() throws Exception {
+        writeBranch("snapshot", rowData(1, 1, 1, 
BinaryString.fromString("20260708")));
+        writeBranch("snapshot", rowData(2, 1, 1, 
BinaryString.fromString("20260709")));
+        writeBranch("delta", rowData(3, 1, 1, 
BinaryString.fromString("20260709")));
+
+        // Without overwrite, the existing snapshot partition is skipped.
+        runAction("dt=20260709", false);
+        assertThat(readSnapshot(ImmutableMap.of("dt", "20260709")))
+                .containsExactlyInAnyOrder("+I[2, 1, 1, 20260709]");
+
+        // With overwrite, merge the snapshot anchor and the delta data.
+        runAction("dt=20260709", true);
+        assertThat(readSnapshot(ImmutableMap.of("dt", "20260709")))
+                .containsExactlyInAnyOrder("+I[1, 1, 1, 20260709]", "+I[3, 1, 
1, 20260709]");
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactChainTableProcedureITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactChainTableProcedureITCase.java
new file mode 100644
index 0000000000..aaf2963e7f
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactChainTableProcedureITCase.java
@@ -0,0 +1,395 @@
+/*
+ * 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.apache.flink.types.Row;
+import org.apache.flink.util.CloseableIterator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** IT cases for {@link CompactChainTableProcedure}. */
+public class CompactChainTableProcedureITCase extends CatalogITCaseBase {
+
+    private List<String> collectResult(String query) throws Exception {
+        List<String> result = new ArrayList<>();
+        try (CloseableIterator<Row> it = tEnv.executeSql(query).collect()) {
+            while (it.hasNext()) {
+                result.add(it.next().toString());
+            }
+        }
+        return result;
+    }
+
+    private void createChainTableWithDate(String tableName) {
+        sql(
+                "CREATE TABLE %s ("
+                        + "  t1 BIGINT,"
+                        + "  t2 BIGINT,"
+                        + "  t3 STRING,"
+                        + "  dt STRING"
+                        + ") PARTITIONED BY (dt) WITH ("
+                        + "  'primary-key' = 'dt,t1',"
+                        + "  'bucket-key' = 't1',"
+                        + "  'bucket' = '1',"
+                        + "  'sequence.field' = 't2',"
+                        + "  'merge-engine' = 'deduplicate',"
+                        + "  'chain-table.enabled' = 'true',"
+                        + "  'partition.timestamp-pattern' = '$dt',"
+                        + "  'partition.timestamp-formatter' = 'yyyyMMdd'"
+                        + ")",
+                tableName);
+    }
+
+    private void setupChainTableBranches(String tableName) {
+        String db = tEnv.getCurrentDatabase();
+        sql("CALL sys.create_branch('%s.%s', 'snapshot')", db, tableName);
+        sql("CALL sys.create_branch('%s.%s', 'delta')", db, tableName);
+
+        sql(
+                "ALTER TABLE %s SET ("
+                        + "  'scan.fallback-snapshot-branch' = 'snapshot',"
+                        + "  'scan.fallback-delta-branch' = 'delta'"
+                        + ")",
+                tableName);
+        sql(
+                "ALTER TABLE `%s$branch_snapshot` SET ("
+                        + "  'scan.fallback-snapshot-branch' = 'snapshot',"
+                        + "  'scan.fallback-delta-branch' = 'delta'"
+                        + ")",
+                tableName);
+        sql(
+                "ALTER TABLE `%s$branch_delta` SET ("
+                        + "  'scan.fallback-snapshot-branch' = 'snapshot',"
+                        + "  'scan.fallback-delta-branch' = 'delta'"
+                        + ")",
+                tableName);
+    }
+
+    @Nullable
+    @Override
+    protected Boolean sqlSyncMode() {
+        return true;
+    }
+
+    @Test
+    public void testCompactChainTableBasic() throws Exception {
+        // Create chain table
+        createChainTableWithDate("chain_compact_t1");
+        setupChainTableBranches("chain_compact_t1");
+
+        // Compact empty partition
+        sql("CALL sys.compact_chain_table('default.chain_compact_t1', 
'dt=20260224')");
+
+        // Insert snapshot data
+        sql(
+                "INSERT INTO `chain_compact_t1$branch_snapshot` PARTITION (dt 
= '20260222') VALUES (0, 1, '0')");
+        sql(
+                "INSERT INTO `chain_compact_t1$branch_snapshot` PARTITION (dt 
= '20260223') VALUES (1, 1, '1')");
+
+        // Insert delta data
+        sql(
+                "INSERT INTO `chain_compact_t1$branch_delta` PARTITION (dt = 
'20260224') VALUES (2, 2, '2')");
+
+        // Before compaction: verify chain read shows snapshot + delta
+        assertThat(collectResult("SELECT * FROM chain_compact_t1 WHERE dt = 
'20260224'"))
+                .containsExactlyInAnyOrder("+I[1, 1, 1, 20260224]", "+I[2, 2, 
2, 20260224]");
+
+        sql("CALL sys.compact_chain_table('default.chain_compact_t1', 
'dt=20260224')");
+
+        // After compaction: verify snapshot branch has merged data
+        assertThat(collectResult("SELECT * FROM chain_compact_t1 WHERE dt = 
'20260224'"))
+                .containsExactlyInAnyOrder("+I[1, 1, 1, 20260224]", "+I[2, 2, 
2, 20260224]");
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
chain_compact_t1$branch_snapshot WHERE dt = '20260223'"))
+                .containsExactlyInAnyOrder("+I[1, 1, 1, 20260223]");
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t1$branch_snapshot` WHERE dt = '20260224'"))
+                .containsExactlyInAnyOrder("+I[1, 1, 1, 20260224]", "+I[2, 2, 
2, 20260224]");
+    }
+
+    @Test
+    public void testCompactChainTableOverwrite() throws Exception {
+        // Create chain table
+        createChainTableWithDate("chain_compact_t2");
+        setupChainTableBranches("chain_compact_t2");
+
+        sql("INSERT INTO `chain_compact_t2` PARTITION (dt = '20260222') VALUES 
(1, 1, '1')");
+
+        // Insert snapshot data
+        sql(
+                "INSERT INTO `chain_compact_t2$branch_snapshot` PARTITION (dt 
= '20260223') VALUES (2, 1, '2')");
+        sql(
+                "INSERT INTO `chain_compact_t2$branch_snapshot` PARTITION (dt 
= '20260224') VALUES (3, 1, '3')");
+        sql(
+                "INSERT INTO `chain_compact_t2$branch_snapshot` PARTITION (dt 
= '20260225') VALUES (3, 2, '3-1')");
+
+        // Insert delta data
+        sql(
+                "INSERT INTO `chain_compact_t2$branch_delta` PARTITION (dt = 
'20260225') VALUES (4, 2, '4')");
+
+        // First call should skip because partition exists
+        sql("CALL sys.compact_chain_table('default.chain_compact_t2', 
'dt=20260225')");
+
+        assertThat(collectResult("SELECT * FROM chain_compact_t2 WHERE dt = 
'20260225'"))
+                .containsExactlyInAnyOrder("+I[3, 2, 3-1, 20260225]");
+
+        sql("CALL sys.compact_chain_table('default.chain_compact_t2', 
'dt=20260225', true)");
+
+        // Check snapshots commit_kind
+        assertThat(
+                        collectResult(
+                                "SELECT snapshot_id, commit_kind FROM 
`chain_compact_t2$branch_snapshot$snapshots`"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, APPEND]", "+I[2, APPEND]", "+I[3, APPEND]", 
"+I[4, OVERWRITE]");
+
+        // Verify snapshot branch now has the data
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t2$branch_snapshot` WHERE dt = '20260225'"))
+                .containsExactlyInAnyOrder("+I[3, 1, 3, 20260225]", "+I[4, 2, 
4, 20260225]");
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t2$branch_snapshot` WHERE dt <> '20260225'"))
+                .containsExactlyInAnyOrder("+I[2, 1, 2, 20260223]", "+I[3, 1, 
3, 20260224]");
+    }
+
+    @Test
+    public void testCompactChainTableMultiplePartitions() throws Exception {
+        sql(
+                "CREATE TABLE chain_compact_t3 ("
+                        + "  t1 BIGINT,"
+                        + "  t2 BIGINT,"
+                        + "  t3 STRING,"
+                        + "  dt STRING,"
+                        + "  hr STRING"
+                        + ") PARTITIONED BY (dt, hr) WITH ("
+                        + "  'primary-key' = 'dt,hr,t1',"
+                        + "  'bucket-key' = 't1',"
+                        + "  'bucket' = '2',"
+                        + "  'sequence.field' = 't2',"
+                        + "  'merge-engine' = 'deduplicate',"
+                        + "  'chain-table.enabled' = 'true',"
+                        + "  'partition.timestamp-pattern' = '$dt $hr:00:00',"
+                        + "  'partition.timestamp-formatter' = 'yyyyMMdd 
HH:mm:ss'"
+                        + ")");
+        setupChainTableBranches("chain_compact_t3");
+
+        // Write snapshot branch data
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_snapshot` PARTITION (dt 
= '20250810', hr = '20') VALUES (0, 1, '0')");
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_snapshot` PARTITION (dt 
= '20250810', hr = '22') VALUES (1, 1, '1'),(2, 1, '1')");
+
+        // Write delta branch data
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_delta` PARTITION (dt = 
'20250810', hr = '21') VALUES (1, 1, '1'),(2, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_delta` PARTITION (dt = 
'20250810', hr = '22') VALUES (1, 2, '1-1'),(3, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_delta` PARTITION (dt = 
'20250810', hr = '23') VALUES (2, 2, '1-1'),(4, 1, '1')");
+
+        // Compact partition 20250810/hr=23
+        sql("CALL sys.compact_chain_table('default.chain_compact_t3', 'dt = 
20250810, hr = 23')");
+
+        // Verify chain read still works correctly
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM chain_compact_t3 WHERE dt = 
'20250810' AND hr = '23'"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, 1, 1, 20250810, 23]",
+                        "+I[2, 2, 1-1, 20250810, 23]",
+                        "+I[4, 1, 1, 20250810, 23]");
+
+        // Write more snapshot and delta data
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_snapshot` PARTITION (dt 
= '20250811', hr = '00') VALUES (1, 2, '1-1'),(2, 2, '1-1'),(3, 2, '1-1'),(4, 
2, '1-1')");
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_snapshot` PARTITION (dt 
= '20250811', hr = '02') VALUES (1, 2, '1-1'),(2, 2, '1-1'),(3, 2, '1-1'),(4, 
2, '1-1'),(5, 1, '1'),(6, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_delta` PARTITION (dt = 
'20250811', hr = '00') VALUES (3, 2, '1-1'),(4, 2, '1-1')");
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_delta` PARTITION (dt = 
'20250811', hr = '01') VALUES (5, 1, '1'),(6, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t3$branch_delta` PARTITION (dt = 
'20250811', hr = '02') VALUES (5, 2, '1-1'),(6, 2, '1-1')");
+
+        // Compact partition 20250811/hr=02 without overwrite
+        sql("CALL sys.compact_chain_table('default.chain_compact_t3', 
'dt=20250811,hr=02')");
+
+        // Verify data is unchanged after skip
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM chain_compact_t3 WHERE dt = 
'20250811' AND hr = '02'"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, 2, 1-1, 20250811, 02]",
+                        "+I[2, 2, 1-1, 20250811, 02]",
+                        "+I[3, 2, 1-1, 20250811, 02]",
+                        "+I[4, 2, 1-1, 20250811, 02]",
+                        "+I[5, 1, 1, 20250811, 02]",
+                        "+I[6, 1, 1, 20250811, 02]");
+
+        // Compact with overwrite to test overwrite path
+        sql("CALL sys.compact_chain_table('default.chain_compact_t3', 
'dt=20250811,hr=02', true)");
+
+        // Check snapshots commit_kind
+        assertThat(
+                        collectResult(
+                                "SELECT snapshot_id, commit_kind FROM 
`chain_compact_t3$branch_snapshot$snapshots`"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, APPEND]",
+                        "+I[2, APPEND]",
+                        "+I[3, APPEND]",
+                        "+I[4, APPEND]",
+                        "+I[5, APPEND]",
+                        "+I[6, OVERWRITE]");
+
+        // Check all snapshot partition data
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM chain_compact_t3 WHERE dt = 
'20250811' AND hr = '02'"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, 2, 1-1, 20250811, 02]",
+                        "+I[2, 2, 1-1, 20250811, 02]",
+                        "+I[3, 2, 1-1, 20250811, 02]",
+                        "+I[4, 2, 1-1, 20250811, 02]",
+                        "+I[5, 2, 1-1, 20250811, 02]",
+                        "+I[6, 2, 1-1, 20250811, 02]");
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM chain_compact_t3 WHERE dt = 
'20250811' AND hr = '00'"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, 2, 1-1, 20250811, 00]",
+                        "+I[2, 2, 1-1, 20250811, 00]",
+                        "+I[3, 2, 1-1, 20250811, 00]",
+                        "+I[4, 2, 1-1, 20250811, 00]");
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t3$branch_snapshot` WHERE dt = '20250810' AND hr = '23'"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, 1, 1, 20250810, 23]",
+                        "+I[2, 2, 1-1, 20250810, 23]",
+                        "+I[4, 1, 1, 20250810, 23]");
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t3$branch_snapshot` WHERE dt = '20250810' AND hr = '22'"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, 1, 1, 20250810, 22]", "+I[2, 1, 1, 20250810, 
22]");
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testCompactChainTableWithGroupPartition(boolean overwrite) 
throws Exception {
+        sql(
+                String.format(
+                        "CREATE TABLE chain_compact_t4 ("
+                                + "  t1 BIGINT,"
+                                + "  t2 BIGINT,"
+                                + "  t3 STRING,"
+                                + "  region STRING,"
+                                + "  dt STRING,"
+                                + "  hr STRING"
+                                + ") PARTITIONED BY (region, dt, hr) "
+                                + " WITH ("
+                                + "  'dynamic-partition-overwrite' = '%s',"
+                                + "  'primary-key' = 'region,dt,hr,t1',"
+                                + "  'bucket-key' = 't1',"
+                                + "  'bucket' = '1',"
+                                + "  'sequence.field' = 't2',"
+                                + "  'merge-engine' = 'deduplicate',"
+                                + "  'chain-table.enabled' = 'true',"
+                                + "  'partition.timestamp-pattern' = '$dt 
$hr:00:00',"
+                                + "  'partition.timestamp-formatter' = 
'yyyyMMdd HH:mm:ss',"
+                                + "  'chain-table.chain-partition-keys' = 
'dt,hr'"
+                                + ")",
+                        overwrite));
+        setupChainTableBranches("chain_compact_t4");
+
+        // Write snapshot branch data
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_snapshot` PARTITION 
(region='CN', dt = '20250810', hr = '20') VALUES (1, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_snapshot` PARTITION 
(region='CN', dt = '20250810', hr = '21') VALUES (2, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_snapshot` PARTITION 
(region='CN', dt = '20250810', hr = '22') VALUES (3, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_snapshot` PARTITION 
(region='UK', dt = '20250810', hr = '21') VALUES (21, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_snapshot` PARTITION 
(region='FR', dt = '20250810', hr = '22') VALUES (31, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_snapshot` PARTITION 
(region='CA', dt = '20250810', hr = '21') VALUES (41, 1, '1')");
+
+        // Write delta branch data
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_delta` PARTITION 
(region='CN', dt = '20250810', hr = '22') VALUES (4, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_delta` PARTITION 
(region='US', dt = '20250810', hr = '22') VALUES (11, 1, '1')");
+        sql(
+                "INSERT INTO `chain_compact_t4$branch_delta` PARTITION 
(region='UK', dt = '20250810', hr = '22') VALUES (22, 1, '1')");
+
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t4$branch_snapshot` WHERE dt = '20250810' AND hr = '22'"))
+                .containsExactlyInAnyOrder(
+                        "+I[3, 1, 1, CN, 20250810, 22]", "+I[31, 1, 1, FR, 
20250810, 22]");
+
+        sql("CALL sys.compact_chain_table('default.chain_compact_t4', 
'dt=20250810,hr=22', true)");
+
+        assertThat(
+                        collectResult(
+                                "select snapshot_id,commit_kind from 
`chain_compact_t4$branch_snapshot$snapshots`"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, APPEND]",
+                        "+I[2, APPEND]",
+                        "+I[3, APPEND]",
+                        "+I[4, APPEND]",
+                        "+I[5, APPEND]",
+                        "+I[6, APPEND]",
+                        "+I[7, OVERWRITE]");
+
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t4$branch_snapshot` WHERE dt = '20250810' AND hr = '21'"))
+                .containsExactlyInAnyOrder(
+                        "+I[2, 1, 1, CN, 20250810, 21]",
+                        "+I[21, 1, 1, UK, 20250810, 21]",
+                        "+I[41, 1, 1, CA, 20250810, 21]");
+
+        assertThat(
+                        collectResult(
+                                "SELECT * FROM 
`chain_compact_t4$branch_snapshot` WHERE dt = '20250810' AND hr = '22'"))
+                .containsExactlyInAnyOrder(
+                        "+I[2, 1, 1, CN, 20250810, 22]",
+                        "+I[4, 1, 1, CN, 20250810, 22]",
+                        "+I[11, 1, 1, US, 20250810, 22]",
+                        "+I[21, 1, 1, UK, 20250810, 22]",
+                        "+I[22, 1, 1, UK, 20250810, 22]",
+                        "+I[31, 1, 1, FR, 20250810, 22]");
+    }
+}

Reply via email to