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/incubator-paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new aab45278f [flink] support one or multiple databases in a dedicated 
compaction job with one combined source and sink (#1887)
aab45278f is described below

commit aab45278f6e5ee4a016dd55a393aba2cb3df8cce
Author: LsomeYeah <[email protected]>
AuthorDate: Tue Sep 12 12:21:49 2023 +0800

    [flink] support one or multiple databases in a dedicated compaction job 
with one combined source and sink (#1887)
---
 docs/content/maintenance/multiple-writers.md       |  42 +-
 .../apache/paimon/table/system/BucketsTable.java   |  35 +-
 .../paimon/flink/action/CompactDatabaseAction.java |  94 +++-
 .../flink/action/CompactDatabaseActionFactory.java |  17 +-
 .../flink/sink/MultiTablesCompactorSink.java       | 190 +++++++
 .../sink/MultiTablesStoreCompactOperator.java      | 286 +++++++++++
 .../paimon/flink/sink/StoreMultiCommitter.java     |  23 +-
 .../source/MultiTablesCompactorSourceBuilder.java  |  98 ++++
 .../MultiTablesBatchCompactorSourceFunction.java   | 133 +++++
 .../MultiTablesCompactorSourceFunction.java        | 158 ++++++
 .../source/operator/MultiTablesReadOperator.java   | 125 +++++
 ...ultiTablesStreamingCompactorSourceFunction.java | 144 ++++++
 .../flink/utils/MultiTablesCompactorUtil.java      |  76 +++
 .../flink/action/CompactDatabaseActionITCase.java  | 563 +++++++++++++--------
 .../MultiTablesCompactorSourceBuilderITCase.java   | 553 ++++++++++++++++++++
 15 files changed, 2278 insertions(+), 259 deletions(-)

diff --git a/docs/content/maintenance/multiple-writers.md 
b/docs/content/maintenance/multiple-writers.md
index 2de3a7f97..1db009d79 100644
--- a/docs/content/maintenance/multiple-writers.md
+++ b/docs/content/maintenance/multiple-writers.md
@@ -103,16 +103,29 @@ Or run the following command to submit a compaction job 
for multiple database.
     /path/to/paimon-flink-action-{{< version >}}.jar \
     compact-database \
     --warehouse <warehouse-path> \
-    --database <database-name> \ 
+    --including-databases <database-name|name-regular-expr> \ 
     [--including-tables <paimon-table-name|name-regular-expr>] \
     [--excluding-tables <paimon-table-name|name-regular-expr>] \
-    [--catalog-conf <paimon-catalog-conf> [--catalog-conf 
<paimon-catalog-conf> ...]] 
+    [--mode <compact-mode>] \
+    [--catalog-conf <paimon-catalog-conf> [--catalog-conf 
<paimon-catalog-conf> ...]] \
+    [--compact-conf <paimon-compact-conf> [--compact-conf 
<paimon-compact-conf> ...]]
 ```
 
-* `--database` is used to specify which database is to be compacted. In 
compact mode, you need to specify a database name, in compact-database mode, 
you could specify multiple database, regular expression is supported.
+* `--including-databases` is used to specify which database is to be 
compacted. In compact mode, you need to specify a database name, in 
compact-database mode, you could specify multiple database, regular expression 
is supported.
 * `--including-tables` is used to specify which source tables are to be 
compacted, you must use '|' to separate multiple tables, the format is 
`databaseName.tableName`, regular expression is supported. For example, 
specifying "--including-tables db1.t1|db2.+" means to compact table 'db1.t1' 
and all tables in the db2 database.
 * `--excluding-tables`  is used to specify which source tables are not to be 
compacted. The usage is same as "--including-tables". "--excluding-tables" has 
higher priority than "--including-tables" if you specified both.
+* `mode` is used to specify compaction mode. Possible values:
+  * "divided" (the default mode if you haven't specified one): start a sink 
for each table, the compaction of the new table requires restarting the job.
+  * "combined": start a single combined sink for all tables, the new table 
will be automatically compacted.
 * `--catalog-conf` is the configuration for Paimon catalog. Each configuration 
should be specified in the format `key=value`. See [here]({{< ref 
"maintenance/configurations" >}}) for a complete list of catalog configurations.
+* `--compact-conf` is the configuration for compaction in combined mode. Each 
configuration should be specified in the format `key=value`. Compact 
configuration is listed below:
+
+| Key                               | Default | Type       | Description       
                                                                                
                                                                                
                          |
+|-----------------------------------|---------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| continuous.discovery-interval     | 10 s    | Duration   | The discovery 
interval of continuous reading.                                                 
                                                                                
                              |
+| sink.parallelism                  | (none)  | Integer    | Defines a custom 
parallelism for the sink. By default, if this option is not defined, the 
planner will derive the parallelism for each statement individually by also 
considering the global configuration. |
+| sink.use-managed-memory-allocator | false   | Boolean    | If true, flink 
sink will use managed memory for merge tree; otherwise, it will create an 
independent memory allocator.                                                   
                                   |
+| sink.managed.writer-buffer-memory | 256 mb  | MemorySize | Weight of writer 
buffer in managed memory, Flink will compute the memory size for writer 
according to the weight, the actual memory used depends on the running 
environment.                                |
 
 If you submit a batch job (set `execution.runtime-mode: batch` in Flink's 
configuration), all current table files will be compacted. If you submit a 
streaming job (set `execution.runtime-mode: streaming` in Flink's 
configuration), the job will continuously monitor new changes to the table and 
perform compactions as needed.
 
@@ -122,6 +135,12 @@ If you only want to submit the compaction job and don't 
want to wait until the j
 
 {{< /hint >}}
 
+{{< hint info >}}
+
+You can set `--mode combined` to enable compacting newly added tables without 
restarting job.
+
+{{< /hint >}}
+
 Example1: compact table
 
 ```bash
@@ -145,12 +164,27 @@ Example2: compact database
     /path/to/paimon-flink-action-{{< version >}}.jar \
     compact-database \
     --warehouse s3:///path/to/warehouse \
-    --database test_db \
+    --including-databases test_db \
     --catalog-conf s3.endpoint=https://****.com \
     --catalog-conf s3.access-key=***** \
     --catalog-conf s3.secret-key=*****
 ```
 
+Example3: compact database in combined mode
+
+```bash
+<FLINK_HOME>/bin/flink run \
+    /path/to/paimon-flink-action-{{< version >}}.jar \
+    compact-database \
+    --warehouse s3:///path/to/warehouse \
+    --including-databases test_db \
+    --mode combined \
+    --catalog-conf s3.endpoint=https://****.com \
+    --catalog-conf s3.access-key=***** \
+    --catalog-conf s3.secret-key=***** \
+    --compact-conf continuous.discovery-interval=*****
+```
+
 For more usage of the compact action, see
 
 ```bash
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/system/BucketsTable.java 
b/paimon-core/src/main/java/org/apache/paimon/table/system/BucketsTable.java
index 79712aa17..062a440ce 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/system/BucketsTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/system/BucketsTable.java
@@ -40,7 +40,7 @@ import org.apache.paimon.table.source.Split;
 import org.apache.paimon.table.source.TableRead;
 import org.apache.paimon.table.source.snapshot.SnapshotReader;
 import org.apache.paimon.types.BigIntType;
-import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.IntType;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.types.VarCharType;
@@ -51,7 +51,6 @@ import org.apache.paimon.utils.TagManager;
 import javax.annotation.Nullable;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -73,6 +72,25 @@ public class BucketsTable implements DataTable, 
ReadonlyTable {
 
     @Nullable private final String databaseName;
 
+    private static final RowType ROW_TYPE =
+            RowType.of(
+                    new DataType[] {
+                        new BigIntType(false),
+                        newBytesType(false),
+                        new IntType(false),
+                        newBytesType(false),
+                        new VarCharType(true, Integer.MAX_VALUE),
+                        new VarCharType(false, Integer.MAX_VALUE)
+                    },
+                    new String[] {
+                        "_SNAPSHOT_ID",
+                        "_PARTITION",
+                        "_BUCKET",
+                        "_FILES",
+                        "_DATABASE_NAME",
+                        "_TABLE_NAME"
+                    });
+
     public BucketsTable(FileStoreTable wrapped, boolean isContinuous) {
         this(wrapped, isContinuous, null);
     }
@@ -106,14 +124,7 @@ public class BucketsTable implements DataTable, 
ReadonlyTable {
 
     @Override
     public RowType rowType() {
-        List<DataField> fields = new ArrayList<>();
-        fields.add(new DataField(0, "_SNAPSHOT_ID", new BigIntType(false)));
-        fields.add(new DataField(1, "_PARTITION", newBytesType(false)));
-        fields.add(new DataField(2, "_BUCKET", new IntType(false)));
-        fields.add(new DataField(3, "_FILES", newBytesType(false)));
-        fields.add(new DataField(4, "_DATABASE_NAME", new VarCharType(true, 
Integer.MAX_VALUE)));
-        fields.add(new DataField(5, "_TABLE_NAME", new VarCharType(false, 
Integer.MAX_VALUE)));
-        return new RowType(fields);
+        return ROW_TYPE;
     }
 
     @Override
@@ -161,6 +172,10 @@ public class BucketsTable implements DataTable, 
ReadonlyTable {
         return wrapped.fileIO();
     }
 
+    public static RowType getRowType() {
+        return ROW_TYPE;
+    }
+
     private class BucketsRead implements InnerTableRead {
 
         private final DataFileMetaSerializer dataFileMetaSerializer = new 
DataFileMetaSerializer();
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseAction.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseAction.java
index abbeb5e33..e56c1f549 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseAction.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseAction.java
@@ -21,17 +21,24 @@ package org.apache.paimon.flink.action;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.FlinkConnectorOptions;
 import org.apache.paimon.flink.compact.UnawareBucketCompactionTopoBuilder;
+import org.apache.paimon.flink.sink.BucketsRowChannelComputer;
 import org.apache.paimon.flink.sink.CompactorSinkBuilder;
+import org.apache.paimon.flink.sink.MultiTablesCompactorSink;
 import org.apache.paimon.flink.source.CompactorSourceBuilder;
+import org.apache.paimon.flink.source.MultiTablesCompactorSourceBuilder;
 import org.apache.paimon.flink.utils.StreamExecutionEnvironmentUtils;
+import org.apache.paimon.options.Options;
 import org.apache.paimon.table.AppendOnlyFileStoreTable;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.Table;
+import org.apache.paimon.utils.Preconditions;
 
 import org.apache.flink.api.common.RuntimeExecutionMode;
 import org.apache.flink.configuration.ExecutionOptions;
 import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.streaming.api.datastream.DataStream;
 import org.apache.flink.streaming.api.datastream.DataStreamSource;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
 import org.apache.flink.table.data.RowData;
@@ -47,25 +54,53 @@ import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static org.apache.paimon.flink.sink.FlinkStreamPartitioner.partition;
+
 /** Database compact action for Flink. */
 public class CompactDatabaseAction extends ActionBase {
     private static final Logger LOG = 
LoggerFactory.getLogger(CompactDatabaseAction.class);
 
-    private final Pattern includingPattern;
-    private final Pattern excludingPattern;
-    private final String database;
+    private Pattern includingPattern = Pattern.compile(".*");
+    @Nullable private Pattern excludingPattern;
+    private Pattern databasePattern = Pattern.compile(".*");
+
+    private MultiTablesSinkMode databaseCompactMode = 
MultiTablesSinkMode.DIVIDED;
+
     private final Map<String, Table> tableMap = new HashMap<>();
 
-    public CompactDatabaseAction(
-            String warehouse,
-            String database,
-            @Nullable String includingTables,
-            @Nullable String excludingTables,
-            Map<String, String> catalogConfig) {
+    private Options compactOptions = new Options();
+
+    public CompactDatabaseAction(String warehouse, Map<String, String> 
catalogConfig) {
         super(warehouse, catalogConfig);
-        this.database = database;
-        this.includingPattern = Pattern.compile(includingTables == null ? ".*" 
: includingTables);
+    }
+
+    public CompactDatabaseAction includingDatabases(@Nullable String 
includingDatabases) {
+        if (includingDatabases != null) {
+            this.databasePattern = Pattern.compile(includingDatabases);
+        }
+        return this;
+    }
+
+    public CompactDatabaseAction includingTables(@Nullable String 
includingTables) {
+        if (includingTables != null) {
+            this.includingPattern = Pattern.compile(includingTables);
+        }
+        return this;
+    }
+
+    public CompactDatabaseAction excludingTables(@Nullable String 
excludingTables) {
         this.excludingPattern = excludingTables == null ? null : 
Pattern.compile(excludingTables);
+        return this;
+    }
+
+    public CompactDatabaseAction withDatabaseCompactMode(@Nullable String 
mode) {
+        this.databaseCompactMode = MultiTablesSinkMode.fromString(mode);
+        return this;
+    }
+
+    public CompactDatabaseAction withCompactOptions(Map<String, String> 
compactOptions) {
+        this.compactOptions = Options.fromMap(compactOptions);
+        return this;
     }
 
     private boolean shouldCompactionTable(String paimonFullTableName) {
@@ -82,8 +117,15 @@ public class CompactDatabaseAction extends ActionBase {
 
     @Override
     public void build(StreamExecutionEnvironment env) {
+        if (databaseCompactMode == MultiTablesSinkMode.DIVIDED) {
+            buildForDividedMode(env);
+        } else {
+            buildForCombinedMode(env);
+        }
+    }
+
+    private void buildForDividedMode(StreamExecutionEnvironment env) {
         try {
-            Pattern databasePattern = Pattern.compile(database);
             List<String> databases = catalog.listDatabases();
             for (String databaseName : databases) {
                 Matcher databaseMatcher = 
databasePattern.matcher(databaseName);
@@ -116,6 +158,10 @@ public class CompactDatabaseAction extends ActionBase {
             throw new RuntimeException(e);
         }
 
+        Preconditions.checkState(
+                !tableMap.isEmpty(),
+                "no tables to be compacted. possible cause is that there are 
no tables detected after pattern matching");
+
         ReadableConfig conf = 
StreamExecutionEnvironmentUtils.getConfiguration(env);
         boolean isStreaming =
                 conf.get(ExecutionOptions.RUNTIME_MODE) == 
RuntimeExecutionMode.STREAMING;
@@ -142,6 +188,30 @@ public class CompactDatabaseAction extends ActionBase {
         }
     }
 
+    private void buildForCombinedMode(StreamExecutionEnvironment env) {
+
+        ReadableConfig conf = 
StreamExecutionEnvironmentUtils.getConfiguration(env);
+        boolean isStreaming =
+                conf.get(ExecutionOptions.RUNTIME_MODE) == 
RuntimeExecutionMode.STREAMING;
+        // TODO: Currently, multi-tables compaction don't support tables which 
bucketmode is UNWARE.
+        MultiTablesCompactorSourceBuilder sourceBuilder =
+                new MultiTablesCompactorSourceBuilder(
+                        catalogLoader(),
+                        databasePattern,
+                        includingPattern,
+                        excludingPattern,
+                        
compactOptions.get(CoreOptions.CONTINUOUS_DISCOVERY_INTERVAL).toMillis());
+        DataStream<RowData> source =
+                
sourceBuilder.withEnv(env).withContinuousMode(isStreaming).build();
+
+        DataStream<RowData> partitioned =
+                partition(
+                        source,
+                        new BucketsRowChannelComputer(),
+                        
compactOptions.get(FlinkConnectorOptions.SINK_PARALLELISM));
+        new MultiTablesCompactorSink(catalogLoader(), 
compactOptions).sinkFrom(partitioned);
+    }
+
     private void buildForTraditionalCompaction(
             StreamExecutionEnvironment env,
             String fullName,
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseActionFactory.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseActionFactory.java
index 345733616..559c28ed4 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseActionFactory.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactDatabaseActionFactory.java
@@ -20,7 +20,6 @@ package org.apache.paimon.flink.action;
 
 import org.apache.flink.api.java.utils.MultipleParameterTool;
 
-import java.util.Map;
 import java.util.Optional;
 
 /** Factory to create {@link CompactDatabaseAction}. */
@@ -35,15 +34,17 @@ public class CompactDatabaseActionFactory implements 
ActionFactory {
 
     @Override
     public Optional<Action> create(MultipleParameterTool params) {
-        String warehouse = params.get("warehouse");
-        String database = params.get("database");
-        String includingTables = params.get("including-tables");
-        String excludingTables = params.get("excluding-tables");
-        Map<String, String> catalogConfig = optionalConfigMap(params, 
"catalog-conf");
-
         CompactDatabaseAction action =
                 new CompactDatabaseAction(
-                        warehouse, database, includingTables, excludingTables, 
catalogConfig);
+                        getRequiredValue(params, "warehouse"),
+                        optionalConfigMap(params, "catalog-conf"));
+
+        action.includingDatabases(params.get("including-databases"))
+                .includingTables(params.get("including-tables"))
+                .excludingTables(params.get("excluding-tables"))
+                .withDatabaseCompactMode(params.get("mode"))
+                .withCompactOptions(optionalConfigMap(params, "compact-conf"));
+
         return Optional.of(action);
     }
 
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesCompactorSink.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesCompactorSink.java
new file mode 100644
index 000000000..9651a86b9
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesCompactorSink.java
@@ -0,0 +1,190 @@
+/*
+ * 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.sink;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.flink.VersionedSerializerWrapper;
+import org.apache.paimon.flink.utils.StreamExecutionEnvironmentUtils;
+import org.apache.paimon.manifest.WrappedManifestCommittable;
+import org.apache.paimon.options.MemorySize;
+import org.apache.paimon.options.Options;
+
+import org.apache.flink.api.common.RuntimeExecutionMode;
+import org.apache.flink.configuration.ExecutionOptions;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.memory.ManagedMemoryUseCase;
+import org.apache.flink.streaming.api.CheckpointingMode;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.streaming.api.environment.CheckpointConfig;
+import 
org.apache.flink.streaming.api.environment.ExecutionCheckpointingOptions;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.functions.sink.DiscardingSink;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.table.data.RowData;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import static 
org.apache.paimon.flink.FlinkConnectorOptions.SINK_MANAGED_WRITER_BUFFER_MEMORY;
+import static 
org.apache.paimon.flink.FlinkConnectorOptions.SINK_USE_MANAGED_MEMORY;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** A sink for processing multi-tables in dedicated compaction job. */
+public class MultiTablesCompactorSink implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private static final String WRITER_NAME = "Writer";
+    private static final String GLOBAL_COMMITTER_NAME = "Global Committer";
+
+    private final Catalog.Loader catalogLoader;
+    private final boolean ignorePreviousFiles;
+
+    private final Options options;
+
+    public MultiTablesCompactorSink(Catalog.Loader catalogLoader, Options 
options) {
+        this.catalogLoader = catalogLoader;
+        this.ignorePreviousFiles = false;
+        this.options = options;
+    }
+
+    public DataStreamSink<?> sinkFrom(DataStream<RowData> input) {
+        // This commitUser is valid only for new jobs.
+        // After the job starts, this commitUser will be recorded into the 
states of write and
+        // commit operators.
+        // When the job restarts, commitUser will be recovered from states and 
this value is
+        // ignored.
+        String initialCommitUser = UUID.randomUUID().toString();
+        return sinkFrom(input, initialCommitUser);
+    }
+
+    public DataStreamSink<?> sinkFrom(DataStream<RowData> input, String 
initialCommitUser) {
+        // do the actually writing action, no snapshot generated in this stage
+        SingleOutputStreamOperator<MultiTableCommittable> written =
+                doWrite(input, initialCommitUser, input.getParallelism());
+
+        // commit the committable to generate a new snapshot
+        return doCommit(written, initialCommitUser);
+    }
+
+    public SingleOutputStreamOperator<MultiTableCommittable> doWrite(
+            DataStream<RowData> input, String commitUser, Integer parallelism) 
{
+        StreamExecutionEnvironment env = input.getExecutionEnvironment();
+        boolean isStreaming =
+                StreamExecutionEnvironmentUtils.getConfiguration(env)
+                                .get(ExecutionOptions.RUNTIME_MODE)
+                        == RuntimeExecutionMode.STREAMING;
+
+        SingleOutputStreamOperator<MultiTableCommittable> written =
+                input.transform(
+                                WRITER_NAME,
+                                new MultiTableCommittableTypeInfo(),
+                                createWriteOperator(
+                                        env.getCheckpointConfig(), 
isStreaming, commitUser))
+                        .setParallelism(parallelism == null ? 
input.getParallelism() : parallelism);
+
+        if (!isStreaming) {
+            assertBatchConfiguration(env, written.getParallelism());
+        }
+
+        if (options.get(SINK_USE_MANAGED_MEMORY)) {
+            MemorySize memorySize = 
options.get(SINK_MANAGED_WRITER_BUFFER_MEMORY);
+            written.getTransformation()
+                    .declareManagedMemoryUseCaseAtOperatorScope(
+                            ManagedMemoryUseCase.OPERATOR, 
memorySize.getMebiBytes());
+        }
+        return written;
+    }
+
+    protected DataStreamSink<?> doCommit(
+            DataStream<MultiTableCommittable> written, String commitUser) {
+        StreamExecutionEnvironment env = written.getExecutionEnvironment();
+        ReadableConfig conf = 
StreamExecutionEnvironmentUtils.getConfiguration(env);
+        CheckpointConfig checkpointConfig = env.getCheckpointConfig();
+        boolean isStreaming =
+                conf.get(ExecutionOptions.RUNTIME_MODE) == 
RuntimeExecutionMode.STREAMING;
+        boolean streamingCheckpointEnabled =
+                isStreaming && checkpointConfig.isCheckpointingEnabled();
+        if (streamingCheckpointEnabled) {
+            assertStreamingConfiguration(env);
+        }
+
+        SingleOutputStreamOperator<?> committed =
+                written.transform(
+                                GLOBAL_COMMITTER_NAME,
+                                new MultiTableCommittableTypeInfo(),
+                                new CommitterOperator<>(
+                                        streamingCheckpointEnabled,
+                                        commitUser,
+                                        createCommitterFactory(),
+                                        createCommittableStateManager()))
+                        .setParallelism(1)
+                        .setMaxParallelism(1);
+        return committed.addSink(new 
DiscardingSink<>()).name("end").setParallelism(1);
+    }
+
+    public static void assertStreamingConfiguration(StreamExecutionEnvironment 
env) {
+        checkArgument(
+                !env.getCheckpointConfig().isUnalignedCheckpointsEnabled(),
+                "Paimon sink currently does not support unaligned checkpoints. 
Please set "
+                        + ExecutionCheckpointingOptions.ENABLE_UNALIGNED.key()
+                        + " to false.");
+        checkArgument(
+                env.getCheckpointConfig().getCheckpointingMode() == 
CheckpointingMode.EXACTLY_ONCE,
+                "Paimon sink currently only supports EXACTLY_ONCE checkpoint 
mode. Please set "
+                        + 
ExecutionCheckpointingOptions.CHECKPOINTING_MODE.key()
+                        + " to exactly-once");
+    }
+
+    private void assertBatchConfiguration(StreamExecutionEnvironment env, int 
sinkParallelism) {
+        try {
+            checkArgument(
+                    sinkParallelism != -1 || 
!AdaptiveParallelism.isEnabled(env),
+                    "Paimon Sink does not support Flink's Adaptive Parallelism 
mode. "
+                            + "Please manually turn it off or set Paimon 
`sink.parallelism` manually.");
+        } catch (NoClassDefFoundError ignored) {
+            // before 1.17, there is no adaptive parallelism
+        }
+    }
+
+    // TODO:refactor FlinkSink to adopt this sink
+    protected OneInputStreamOperator<RowData, MultiTableCommittable> 
createWriteOperator(
+            CheckpointConfig checkpointConfig, boolean isStreaming, String 
commitUser) {
+        return new MultiTablesStoreCompactOperator(
+                catalogLoader,
+                commitUser,
+                checkpointConfig,
+                isStreaming,
+                ignorePreviousFiles,
+                options);
+    }
+
+    protected Committer.Factory<MultiTableCommittable, 
WrappedManifestCommittable>
+            createCommitterFactory() {
+        return (user, metricGroup) ->
+                new StoreMultiCommitter(
+                        catalogLoader, user, new 
CommitterMetrics(metricGroup), true);
+    }
+
+    protected CommittableStateManager<WrappedManifestCommittable> 
createCommittableStateManager() {
+        return new RestoreAndFailCommittableStateManager<>(
+                () -> new VersionedSerializerWrapper<>(new 
WrappedManifestCommittableSerializer()));
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesStoreCompactOperator.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesStoreCompactOperator.java
new file mode 100644
index 000000000..8ce822bc2
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesStoreCompactOperator.java
@@ -0,0 +1,286 @@
+/*
+ * 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.sink;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.io.DataFileMetaSerializer;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.utils.Preconditions;
+
+import org.apache.flink.runtime.state.StateInitializationContext;
+import org.apache.flink.runtime.state.StateSnapshotContext;
+import org.apache.flink.streaming.api.environment.CheckpointConfig;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.table.data.RowData;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.apache.paimon.CoreOptions.FULL_COMPACTION_DELTA_COMMITS;
+import static 
org.apache.paimon.flink.FlinkConnectorOptions.CHANGELOG_PRODUCER_FULL_COMPACTION_TRIGGER_INTERVAL;
+import static 
org.apache.paimon.flink.FlinkConnectorOptions.CHANGELOG_PRODUCER_LOOKUP_WAIT;
+import static 
org.apache.paimon.flink.sink.cdc.CdcRecordStoreWriteOperator.RETRY_SLEEP_TIME;
+import static org.apache.paimon.utils.SerializationUtils.deserializeBinaryRow;
+
+/**
+ * A dedicated operator for manual triggered compaction.
+ *
+ * <p>In-coming records are generated by sources built from {@link
+ * org.apache.paimon.flink.source.MultiTablesCompactorSourceBuilder}. The 
records will contain
+ * partition keys, bucket number, table name and database name.
+ */
+public class MultiTablesStoreCompactOperator
+        extends PrepareCommitOperator<RowData, MultiTableCommittable> {
+
+    private static final long serialVersionUID = 1L;
+
+    private StoreSinkWrite.Provider storeSinkWriteProvider;
+    private final CheckpointConfig checkpointConfig;
+    private final boolean isStreaming;
+    private final boolean ignorePreviousFiles;
+    private final String initialCommitUser;
+
+    private transient StoreSinkWriteState state;
+    private transient DataFileMetaSerializer dataFileMetaSerializer;
+
+    private final Catalog.Loader catalogLoader;
+
+    protected Catalog catalog;
+    protected Map<Identifier, FileStoreTable> tables;
+    protected Map<Identifier, StoreSinkWrite> writes;
+    protected String commitUser;
+
+    public MultiTablesStoreCompactOperator(
+            Catalog.Loader catalogLoader,
+            String initialCommitUser,
+            CheckpointConfig checkpointConfig,
+            boolean isStreaming,
+            boolean ignorePreviousFiles,
+            Options options) {
+        super(options);
+        this.catalogLoader = catalogLoader;
+        this.initialCommitUser = initialCommitUser;
+        this.checkpointConfig = checkpointConfig;
+        this.isStreaming = isStreaming;
+        this.ignorePreviousFiles = ignorePreviousFiles;
+    }
+
+    @Override
+    public void initializeState(StateInitializationContext context) throws 
Exception {
+        super.initializeState(context);
+
+        catalog = catalogLoader.load();
+
+        // Each job can only have one user name and this name must be 
consistent across restarts.
+        // We cannot use job id as commit user name here because user may 
change job id by creating
+        // a savepoint, stop the job and then resume from savepoint.
+        commitUser =
+                StateUtils.getSingleValueFromState(
+                        context, "commit_user_state", String.class, 
initialCommitUser);
+
+        state =
+                new StoreSinkWriteState(
+                        context,
+                        (tableName, partition, bucket) ->
+                                ChannelComputer.select(
+                                                partition,
+                                                bucket,
+                                                
getRuntimeContext().getNumberOfParallelSubtasks())
+                                        == 
getRuntimeContext().getIndexOfThisSubtask());
+
+        tables = new HashMap<>();
+        writes = new HashMap<>();
+    }
+
+    @Override
+    public void open() throws Exception {
+        super.open();
+        dataFileMetaSerializer = new DataFileMetaSerializer();
+    }
+
+    @Override
+    public void processElement(StreamRecord<RowData> element) throws Exception 
{
+        RowData record = element.getValue();
+
+        long snapshotId = record.getLong(0);
+        BinaryRow partition = deserializeBinaryRow(record.getBinary(1));
+        int bucket = record.getInt(2);
+        byte[] serializedFiles = record.getBinary(3);
+        List<DataFileMeta> files = 
dataFileMetaSerializer.deserializeList(serializedFiles);
+        String databaseName = record.getString(4).toString();
+        String tableName = record.getString(5).toString();
+
+        Identifier tableId = Identifier.create(databaseName, tableName);
+        FileStoreTable table = getTable(tableId);
+
+        Preconditions.checkArgument(
+                !table.coreOptions().writeOnly(),
+                CoreOptions.WRITE_ONLY.key()
+                        + " should not be true for 
MultiTablesStoreCompactOperator.");
+
+        storeSinkWriteProvider =
+                createWriteProvider(table, checkpointConfig, isStreaming, 
ignorePreviousFiles);
+
+        StoreSinkWrite write =
+                writes.computeIfAbsent(
+                        tableId,
+                        id ->
+                                storeSinkWriteProvider.provide(
+                                        table,
+                                        commitUser,
+                                        state,
+                                        
getContainingTask().getEnvironment().getIOManager(),
+                                        memoryPool));
+
+        if (write.streamingMode()) {
+            write.notifyNewFiles(snapshotId, partition, bucket, files);
+            write.compact(partition, bucket, false);
+        } else {
+            Preconditions.checkArgument(
+                    files.isEmpty(),
+                    "Batch compact job does not concern what files are 
compacted. "
+                            + "They only need to know what buckets are 
compacted.");
+            write.compact(partition, bucket, true);
+        }
+    }
+
+    @Override
+    protected List<MultiTableCommittable> prepareCommit(boolean 
waitCompaction, long checkpointId)
+            throws IOException {
+
+        List<MultiTableCommittable> committables = new LinkedList<>();
+        for (Map.Entry<Identifier, StoreSinkWrite> entry : writes.entrySet()) {
+            Identifier key = entry.getKey();
+            StoreSinkWrite write = entry.getValue();
+            committables.addAll(
+                    write.prepareCommit(waitCompaction, checkpointId).stream()
+                            .map(
+                                    committable ->
+                                            
MultiTableCommittable.fromCommittable(key, committable))
+                            .collect(Collectors.toList()));
+        }
+        return committables;
+    }
+
+    @Override
+    public void snapshotState(StateSnapshotContext context) throws Exception {
+        super.snapshotState(context);
+        for (StoreSinkWrite write : writes.values()) {
+            write.snapshotState();
+        }
+        state.snapshotState();
+    }
+
+    @Override
+    public void close() throws Exception {
+        super.close();
+        for (StoreSinkWrite write : writes.values()) {
+            write.close();
+        }
+    }
+
+    private FileStoreTable getTable(Identifier tableId) throws 
InterruptedException {
+        FileStoreTable table = tables.get(tableId);
+        if (table == null) {
+            while (true) {
+                try {
+                    table = (FileStoreTable) catalog.getTable(tableId);
+                    table =
+                            table.copy(
+                                    Collections.singletonMap(
+                                            CoreOptions.WRITE_ONLY.key(), 
"false"));
+                    tables.put(tableId, table);
+                    break;
+                } catch (Catalog.TableNotExistException e) {
+                    // table not found, waiting until table is created by
+                    //     upstream operators
+                }
+                Thread.sleep(RETRY_SLEEP_TIME.defaultValue().toMillis());
+            }
+        }
+        return table;
+    }
+
+    private StoreSinkWrite.Provider createWriteProvider(
+            FileStoreTable fileStoreTable,
+            CheckpointConfig checkpointConfig,
+            boolean isStreaming,
+            boolean ignorePreviousFiles) {
+        boolean waitCompaction;
+        if (fileStoreTable.coreOptions().writeOnly()) {
+            waitCompaction = false;
+        } else {
+            Options options = fileStoreTable.coreOptions().toConfiguration();
+            CoreOptions.ChangelogProducer changelogProducer =
+                    fileStoreTable.coreOptions().changelogProducer();
+            waitCompaction =
+                    changelogProducer == CoreOptions.ChangelogProducer.LOOKUP
+                            && options.get(CHANGELOG_PRODUCER_LOOKUP_WAIT);
+
+            int deltaCommits = -1;
+            if (options.contains(FULL_COMPACTION_DELTA_COMMITS)) {
+                deltaCommits = options.get(FULL_COMPACTION_DELTA_COMMITS);
+            } else if 
(options.contains(CHANGELOG_PRODUCER_FULL_COMPACTION_TRIGGER_INTERVAL)) {
+                long fullCompactionThresholdMs =
+                        
options.get(CHANGELOG_PRODUCER_FULL_COMPACTION_TRIGGER_INTERVAL).toMillis();
+                deltaCommits =
+                        (int)
+                                (fullCompactionThresholdMs
+                                        / 
checkpointConfig.getCheckpointInterval());
+            }
+
+            if (changelogProducer == 
CoreOptions.ChangelogProducer.FULL_COMPACTION
+                    || deltaCommits >= 0) {
+                int finalDeltaCommits = Math.max(deltaCommits, 1);
+                return (table, commitUser, state, ioManager, memoryPool) ->
+                        new GlobalFullCompactionSinkWrite(
+                                table,
+                                commitUser,
+                                state,
+                                ioManager,
+                                ignorePreviousFiles,
+                                waitCompaction,
+                                finalDeltaCommits,
+                                isStreaming,
+                                memoryPool);
+            }
+        }
+
+        return (table, commitUser, state, ioManager, memoryPool) ->
+                new StoreSinkWriteImpl(
+                        table,
+                        commitUser,
+                        state,
+                        ioManager,
+                        ignorePreviousFiles,
+                        waitCompaction,
+                        isStreaming,
+                        memoryPool);
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
index afecf7548..6559e9969 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.flink.sink;
 
+import org.apache.paimon.CoreOptions;
 import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.manifest.ManifestCommittable;
@@ -32,6 +33,7 @@ import javax.annotation.Nullable;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -54,13 +56,24 @@ public class StoreMultiCommitter
     //    referenced by table id.
     private final Map<Identifier, StoreCommitter> tableCommitters;
 
+    // compact job needs set "write-only" of a table to false
+    private final boolean isCompactJob;
+
     public StoreMultiCommitter(
             Catalog.Loader catalogLoader, String commitUser, @Nullable 
CommitterMetrics metrics) {
+        this(catalogLoader, commitUser, metrics, false);
+    }
+
+    public StoreMultiCommitter(
+            Catalog.Loader catalogLoader,
+            String commitUser,
+            @Nullable CommitterMetrics metrics,
+            boolean isCompactJob) {
         this.catalog = catalogLoader.load();
         this.commitUser = commitUser;
         this.metrics = metrics;
-
         this.tableCommitters = new HashMap<>();
+        this.isCompactJob = isCompactJob;
     }
 
     @Override
@@ -148,6 +161,12 @@ public class StoreMultiCommitter
             FileStoreTable table;
             try {
                 table = (FileStoreTable) catalog.getTable(tableId);
+                if (isCompactJob) {
+                    table =
+                            table.copy(
+                                    Collections.singletonMap(
+                                            CoreOptions.WRITE_ONLY.key(), 
"false"));
+                }
             } catch (Catalog.TableNotExistException e) {
                 throw new RuntimeException(
                         String.format(
@@ -156,7 +175,7 @@ public class StoreMultiCommitter
             }
             committer =
                     new StoreCommitter(
-                            
table.newCommit(commitUser).ignoreEmptyCommit(false), metrics);
+                            
table.newCommit(commitUser).ignoreEmptyCommit(isCompactJob), metrics);
             tableCommitters.put(tableId, committer);
         }
 
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/MultiTablesCompactorSourceBuilder.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/MultiTablesCompactorSourceBuilder.java
new file mode 100644
index 000000000..b631d3b7a
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/MultiTablesCompactorSourceBuilder.java
@@ -0,0 +1,98 @@
+/*
+ * 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.source;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.flink.LogicalTypeConversion;
+import 
org.apache.paimon.flink.source.operator.MultiTablesBatchCompactorSourceFunction;
+import 
org.apache.paimon.flink.source.operator.MultiTablesStreamingCompactorSourceFunction;
+import org.apache.paimon.table.system.BucketsTable;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.Preconditions;
+
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+
+import java.util.regex.Pattern;
+
+/**
+ * source builder to build a Flink compactor source for multi-tables. This is 
for dedicated
+ * compactor jobs.
+ */
+public class MultiTablesCompactorSourceBuilder {
+    private final Catalog.Loader catalogLoader;
+    private final Pattern includingPattern;
+    private final Pattern excludingPattern;
+    private final Pattern databasePattern;
+    private final long monitorInterval;
+
+    private boolean isContinuous = false;
+    private StreamExecutionEnvironment env;
+
+    public MultiTablesCompactorSourceBuilder(
+            Catalog.Loader catalogLoader,
+            Pattern databasePattern,
+            Pattern includingPattern,
+            Pattern excludingPattern,
+            long monitorInterval) {
+        this.catalogLoader = catalogLoader;
+        this.includingPattern = includingPattern;
+        this.excludingPattern = excludingPattern;
+        this.databasePattern = databasePattern;
+        this.monitorInterval = monitorInterval;
+    }
+
+    public MultiTablesCompactorSourceBuilder withContinuousMode(boolean 
isContinuous) {
+        this.isContinuous = isContinuous;
+        return this;
+    }
+
+    public MultiTablesCompactorSourceBuilder 
withEnv(StreamExecutionEnvironment env) {
+        this.env = env;
+        return this;
+    }
+
+    public DataStream<RowData> build() {
+        Preconditions.checkArgument(env != null, "StreamExecutionEnvironment 
should not be null.");
+        RowType produceType = BucketsTable.getRowType();
+        if (isContinuous) {
+            return MultiTablesStreamingCompactorSourceFunction.buildSource(
+                    env,
+                    "MultiTables-StreamingCompactorSource",
+                    
InternalTypeInfo.of(LogicalTypeConversion.toLogicalType(produceType)),
+                    catalogLoader,
+                    includingPattern,
+                    excludingPattern,
+                    databasePattern,
+                    monitorInterval);
+        } else {
+            return MultiTablesBatchCompactorSourceFunction.buildSource(
+                    env,
+                    "MultiTables-BatchCompactorSource",
+                    
InternalTypeInfo.of(LogicalTypeConversion.toLogicalType(produceType)),
+                    catalogLoader,
+                    includingPattern,
+                    excludingPattern,
+                    databasePattern,
+                    monitorInterval);
+        }
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesBatchCompactorSourceFunction.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesBatchCompactorSourceFunction.java
new file mode 100644
index 000000000..3da9fb2e6
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesBatchCompactorSourceFunction.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.source.operator;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.utils.JavaTypeInfo;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.EndOfScanException;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.StreamTableScan;
+
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.source.Boundedness;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.operators.StreamSource;
+import org.apache.flink.table.data.RowData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/** It is responsible for monitoring compactor source in batch mode. */
+public class MultiTablesBatchCompactorSourceFunction extends 
MultiTablesCompactorSourceFunction {
+
+    private static final Logger LOG =
+            
LoggerFactory.getLogger(MultiTablesBatchCompactorSourceFunction.class);
+
+    public MultiTablesBatchCompactorSourceFunction(
+            Catalog.Loader catalogLoader,
+            Pattern includingPattern,
+            Pattern excludingPattern,
+            Pattern databasePattern,
+            long monitorInterval) {
+        super(
+                catalogLoader,
+                includingPattern,
+                excludingPattern,
+                databasePattern,
+                false,
+                monitorInterval);
+    }
+
+    @Override
+    public void run(SourceContext<Tuple2<Split, String>> ctx) throws Exception 
{
+        this.ctx = ctx;
+        if (isRunning) {
+            boolean isEmpty;
+            synchronized (ctx.getCheckpointLock()) {
+                if (!isRunning) {
+                    return;
+                }
+                try {
+                    // batch mode do not need check for new tables
+                    List<Tuple2<Split, String>> splits = new ArrayList<>();
+                    for (Map.Entry<Identifier, StreamTableScan> entry : 
scansMap.entrySet()) {
+                        Identifier identifier = entry.getKey();
+                        StreamTableScan scan = entry.getValue();
+                        splits.addAll(
+                                scan.plan().splits().stream()
+                                        .map(split -> new Tuple2<>(split, 
identifier.getFullName()))
+                                        .collect(Collectors.toList()));
+                    }
+
+                    isEmpty = splits.isEmpty();
+                    splits.forEach(ctx::collect);
+
+                } catch (EndOfScanException esf) {
+                    LOG.info("Catching EndOfStreamException, the stream is 
finished.");
+                    return;
+                }
+            }
+            if (isEmpty) {
+                throw new Exception(
+                        "No splits were collected. Please ensure there are 
tables detected after pattern matching");
+            }
+        }
+    }
+
+    public static DataStream<RowData> buildSource(
+            StreamExecutionEnvironment env,
+            String name,
+            TypeInformation<RowData> typeInfo,
+            Catalog.Loader catalogLoader,
+            Pattern includingPattern,
+            Pattern excludingPattern,
+            Pattern databasePattern,
+            long monitorInterval) {
+        MultiTablesBatchCompactorSourceFunction function =
+                new MultiTablesBatchCompactorSourceFunction(
+                        catalogLoader,
+                        includingPattern,
+                        excludingPattern,
+                        databasePattern,
+                        monitorInterval);
+        StreamSource<Tuple2<Split, String>, ?> sourceOperator = new 
StreamSource<>(function);
+        TupleTypeInfo<Tuple2<Split, String>> tupleTypeInfo =
+                new TupleTypeInfo<>(
+                        new JavaTypeInfo<>(Split.class), 
BasicTypeInfo.STRING_TYPE_INFO);
+        return new DataStreamSource<>(
+                        env, tupleTypeInfo, sourceOperator, false, name, 
Boundedness.BOUNDED)
+                .forceNonParallel()
+                .partitionCustom(
+                        (key, numPartitions) -> key % numPartitions,
+                        split -> ((DataSplit) split.f0).bucket())
+                .transform(name, typeInfo, new 
MultiTablesReadOperator(catalogLoader, false));
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesCompactorSourceFunction.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesCompactorSourceFunction.java
new file mode 100644
index 000000000..b1f9637c6
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesCompactorSourceFunction.java
@@ -0,0 +1,158 @@
+/*
+ * 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.source.operator;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.StreamTableScan;
+import org.apache.paimon.table.system.BucketsTable;
+
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.functions.source.RichSourceFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static 
org.apache.paimon.flink.utils.MultiTablesCompactorUtil.compactOptions;
+import static 
org.apache.paimon.flink.utils.MultiTablesCompactorUtil.shouldCompactTable;
+
+/**
+ * This is the single (non-parallel) monitoring task, it is responsible for:
+ *
+ * <ol>
+ *   <li>Monitoring snapshots of the Paimon table.
+ *   <li>Creating the Tuple2<{@link Split}, String> splits corresponding to 
the incremental files
+ *   <li>Assigning them to downstream tasks for further processing.
+ * </ol>
+ *
+ * <p>The splits to be read are forwarded to the downstream {@link 
MultiTablesReadOperator} which
+ * can have parallelism greater than one.
+ *
+ * <p>Currently, only dedicated compaction job for multi-tables rely on this 
monitor.
+ */
+public abstract class MultiTablesCompactorSourceFunction
+        extends RichSourceFunction<Tuple2<Split, String>> {
+
+    private static final long serialVersionUID = 1L;
+
+    private static final Logger LOG =
+            LoggerFactory.getLogger(MultiTablesCompactorSourceFunction.class);
+
+    protected final Catalog.Loader catalogLoader;
+    protected final Pattern includingPattern;
+    protected final Pattern excludingPattern;
+    protected final Pattern databasePattern;
+    protected final boolean isStreaming;
+    protected final long monitorInterval;
+
+    protected transient Catalog catalog;
+    protected transient Map<Identifier, BucketsTable> tablesMap;
+    protected transient Map<Identifier, StreamTableScan> scansMap;
+
+    public MultiTablesCompactorSourceFunction(
+            Catalog.Loader catalogLoader,
+            Pattern includingPattern,
+            Pattern excludingPattern,
+            Pattern databasePattern,
+            boolean isStreaming,
+            long monitorInterval) {
+        this.catalogLoader = catalogLoader;
+        this.includingPattern = includingPattern;
+        this.excludingPattern = excludingPattern;
+        this.databasePattern = databasePattern;
+        this.isStreaming = isStreaming;
+        this.monitorInterval = monitorInterval;
+    }
+
+    protected volatile boolean isRunning = true;
+
+    protected transient SourceContext<Tuple2<Split, String>> ctx;
+
+    @Override
+    public void open(Configuration parameters) throws Exception {
+        tablesMap = new HashMap<>();
+        scansMap = new HashMap<>();
+        catalog = catalogLoader.load();
+
+        updateTableMap();
+    }
+
+    @Override
+    public void cancel() {
+        // this is to cover the case where cancel() is called before the run()
+        if (ctx != null) {
+            synchronized (ctx.getCheckpointLock()) {
+                isRunning = false;
+            }
+        } else {
+            isRunning = false;
+        }
+    }
+
+    protected void updateTableMap()
+            throws Catalog.DatabaseNotExistException, 
Catalog.TableNotExistException {
+        List<String> databases = catalog.listDatabases();
+
+        for (String databaseName : databases) {
+            if (databasePattern.matcher(databaseName).matches()) {
+                List<String> tables = catalog.listTables(databaseName);
+                for (String tableName : tables) {
+                    Identifier identifier = Identifier.create(databaseName, 
tableName);
+                    if (shouldCompactTable(identifier, includingPattern, 
excludingPattern)
+                            && (!tablesMap.containsKey(identifier))) {
+                        Table table = catalog.getTable(identifier);
+                        if (!(table instanceof FileStoreTable)) {
+                            LOG.error(
+                                    String.format(
+                                            "Only FileStoreTable supports 
compact action. The table type is '%s'.",
+                                            table.getClass().getName()));
+                            continue;
+                        }
+                        FileStoreTable fileStoreTable = (FileStoreTable) table;
+                        if (fileStoreTable.bucketMode() == BucketMode.UNAWARE) 
{
+                            LOG.info(
+                                    String.format(
+                                                    "the bucket mode of %s is 
unware. ",
+                                                    identifier.getFullName())
+                                            + "currently, the table with 
unware bucket mode is not support in combined mode.");
+                            continue;
+                        }
+                        BucketsTable bucketsTable =
+                                new BucketsTable(
+                                                fileStoreTable,
+                                                isStreaming,
+                                                identifier.getDatabaseName())
+                                        .copy(compactOptions(isStreaming));
+                        tablesMap.put(identifier, bucketsTable);
+                        scansMap.put(identifier, 
bucketsTable.newReadBuilder().newStreamScan());
+                    }
+                }
+            }
+        }
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesReadOperator.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesReadOperator.java
new file mode 100644
index 000000000..37366a239
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesReadOperator.java
@@ -0,0 +1,125 @@
+/*
+ * 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.source.operator;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.flink.FlinkRowData;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableRead;
+import org.apache.paimon.table.system.BucketsTable;
+import org.apache.paimon.utils.CloseableIterator;
+import org.apache.paimon.utils.Preconditions;
+
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.table.data.RowData;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static 
org.apache.paimon.flink.utils.MultiTablesCompactorUtil.compactOptions;
+
+/**
+ * The operator that reads the Tuple2<{@link Split}, String> received from the 
preceding {@link
+ * MultiTablesBatchCompactorSourceFunction} or {@link 
MultiTablesStreamingCompactorSourceFunction}.
+ * Contrary to the {@link MultiTablesCompactorSourceFunction} which has a 
parallelism of 1, this
+ * operator can have DOP > 1.
+ */
+public class MultiTablesReadOperator extends AbstractStreamOperator<RowData>
+        implements OneInputStreamOperator<Tuple2<Split, String>, RowData> {
+
+    private static final long serialVersionUID = 1L;
+
+    private final Catalog.Loader catalogLoader;
+    private final boolean isStreaming;
+
+    public MultiTablesReadOperator(Catalog.Loader catalogLoader, boolean 
isStreaming) {
+        this.catalogLoader = catalogLoader;
+        this.isStreaming = isStreaming;
+    }
+
+    private transient Catalog catalog;
+    private transient IOManagerImpl ioManager;
+    private transient Map<Identifier, BucketsTable> tablesMap;
+    private transient Map<Identifier, TableRead> readsMap;
+    private transient StreamRecord<RowData> reuseRecord;
+    private transient FlinkRowData reuseRow;
+
+    @Override
+    public void open() throws Exception {
+        super.open();
+        ioManager =
+                new IOManagerImpl(
+                        getContainingTask()
+                                .getEnvironment()
+                                .getIOManager()
+                                .getSpillingDirectoriesPaths());
+        tablesMap = new HashMap<>();
+        readsMap = new HashMap<>();
+        catalog = catalogLoader.load();
+
+        this.reuseRow = new FlinkRowData(null);
+        this.reuseRecord = new StreamRecord<>(reuseRow);
+    }
+
+    @Override
+    public void processElement(StreamRecord<Tuple2<Split, String>> record) 
throws Exception {
+        Identifier identifier = Identifier.fromString(record.getValue().f1);
+        TableRead read = getTableRead(identifier);
+        try (CloseableIterator<InternalRow> iterator =
+                read.createReader(record.getValue().f0).toCloseableIterator()) 
{
+            while (iterator.hasNext()) {
+                reuseRow.replace(iterator.next());
+                output.collect(reuseRecord);
+            }
+        }
+    }
+
+    private TableRead getTableRead(Identifier tableId) {
+        BucketsTable table = tablesMap.get(tableId);
+        if (table == null) {
+            try {
+                Table newTable = catalog.getTable(tableId);
+                Preconditions.checkArgument(
+                        newTable instanceof FileStoreTable,
+                        "Only FileStoreTable supports compact action. The 
table type is '%s'.",
+                        newTable.getClass().getName());
+                table =
+                        new BucketsTable(
+                                        (FileStoreTable) newTable,
+                                        isStreaming,
+                                        tableId.getDatabaseName())
+                                .copy(compactOptions(isStreaming));
+                tablesMap.put(tableId, table);
+                readsMap.put(tableId, 
table.newReadBuilder().newRead().withIOManager(ioManager));
+            } catch (Catalog.TableNotExistException e) {
+                LOG.error(String.format("table: %s not found.", 
tableId.getFullName()));
+            }
+        }
+
+        return readsMap.get(tableId);
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesStreamingCompactorSourceFunction.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesStreamingCompactorSourceFunction.java
new file mode 100644
index 000000000..61e085f32
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MultiTablesStreamingCompactorSourceFunction.java
@@ -0,0 +1,144 @@
+/*
+ * 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.source.operator;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.utils.JavaTypeInfo;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.EndOfScanException;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.StreamTableScan;
+
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.source.Boundedness;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.operators.StreamSource;
+import org.apache.flink.table.data.RowData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/** It is responsible for monitoring compactor source in streaming mode. */
+public class MultiTablesStreamingCompactorSourceFunction
+        extends MultiTablesCompactorSourceFunction {
+
+    private static final Logger LOG =
+            
LoggerFactory.getLogger(MultiTablesStreamingCompactorSourceFunction.class);
+
+    public MultiTablesStreamingCompactorSourceFunction(
+            Catalog.Loader catalogLoader,
+            Pattern includingPattern,
+            Pattern excludingPattern,
+            Pattern databasePattern,
+            long monitorInterval) {
+        super(
+                catalogLoader,
+                includingPattern,
+                excludingPattern,
+                databasePattern,
+                true,
+                monitorInterval);
+    }
+
+    @SuppressWarnings("BusyWait")
+    @Override
+    public void run(SourceContext<Tuple2<Split, String>> ctx) throws Exception 
{
+        this.ctx = ctx;
+        while (isRunning) {
+            boolean isEmpty;
+            synchronized (ctx.getCheckpointLock()) {
+                if (!isRunning) {
+                    return;
+                }
+                try {
+                    // check for new tables
+                    updateTableMap();
+
+                    List<Tuple2<Split, String>> splits = new ArrayList<>();
+                    for (Map.Entry<Identifier, StreamTableScan> entry : 
scansMap.entrySet()) {
+                        Identifier identifier = entry.getKey();
+                        StreamTableScan scan = entry.getValue();
+                        splits.addAll(
+                                scan.plan().splits().stream()
+                                        .map(split -> new Tuple2<>(split, 
identifier.getFullName()))
+                                        .collect(Collectors.toList()));
+                    }
+
+                    isEmpty = splits.isEmpty();
+                    splits.forEach(ctx::collect);
+
+                } catch (EndOfScanException esf) {
+                    LOG.info("Catching EndOfStreamException, the stream is 
finished.");
+                    return;
+                }
+            }
+
+            if (isEmpty) {
+                Thread.sleep(monitorInterval);
+            }
+        }
+    }
+
+    public static DataStream<RowData> buildSource(
+            StreamExecutionEnvironment env,
+            String name,
+            TypeInformation<RowData> typeInfo,
+            Catalog.Loader catalogLoader,
+            Pattern includingPattern,
+            Pattern excludingPattern,
+            Pattern databasePattern,
+            long monitorInterval) {
+
+        MultiTablesStreamingCompactorSourceFunction function =
+                new MultiTablesStreamingCompactorSourceFunction(
+                        catalogLoader,
+                        includingPattern,
+                        excludingPattern,
+                        databasePattern,
+                        monitorInterval);
+        StreamSource<Tuple2<Split, String>, ?> sourceOperator = new 
StreamSource<>(function);
+        boolean isParallel = false;
+        TupleTypeInfo<Tuple2<Split, String>> tupleTypeInfo =
+                new TupleTypeInfo<>(
+                        new JavaTypeInfo<>(Split.class), 
BasicTypeInfo.STRING_TYPE_INFO);
+        return new DataStreamSource<>(
+                        env,
+                        tupleTypeInfo,
+                        sourceOperator,
+                        isParallel,
+                        name,
+                        Boundedness.CONTINUOUS_UNBOUNDED)
+                .forceNonParallel()
+                .partitionCustom(
+                        (key, numPartitions) -> key % numPartitions,
+                        split -> ((DataSplit) split.f0).bucket())
+                .transform(name, typeInfo, new 
MultiTablesReadOperator(catalogLoader, true));
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/utils/MultiTablesCompactorUtil.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/utils/MultiTablesCompactorUtil.java
new file mode 100644
index 000000000..d26a9fe12
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/utils/MultiTablesCompactorUtil.java
@@ -0,0 +1,76 @@
+/*
+ * 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.utils;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.catalog.Identifier;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/** Utility methods for dedicated compaction job for multi-tables. */
+public class MultiTablesCompactorUtil {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(MultiTablesCompactorUtil.class);
+
+    public static Map<String, String> compactOptions(boolean isStreaming) {
+        if (isStreaming) {
+            // set 'streaming-compact' and remove 'scan.bounded.watermark'
+            return new HashMap<String, String>() {
+                {
+                    put(
+                            CoreOptions.STREAMING_COMPACT.key(),
+                            
CoreOptions.StreamingCompactionType.NORMAL.getValue());
+                    put(CoreOptions.SCAN_BOUNDED_WATERMARK.key(), null);
+                    put(CoreOptions.WRITE_ONLY.key(), "false");
+                }
+            };
+        } else {
+            // batch compactor source will compact all current files
+            return new HashMap<String, String>() {
+                {
+                    put(CoreOptions.SCAN_TIMESTAMP_MILLIS.key(), null);
+                    put(CoreOptions.SCAN_SNAPSHOT_ID.key(), null);
+                    put(
+                            CoreOptions.SCAN_MODE.key(),
+                            CoreOptions.StartupMode.LATEST_FULL.toString());
+                    put(CoreOptions.WRITE_ONLY.key(), "false");
+                }
+            };
+        }
+    }
+
+    public static boolean shouldCompactTable(
+            Identifier tableIdentifier, Pattern includingPattern, Pattern 
excludingPattern) {
+        String paimonFullTableName = tableIdentifier.getFullName();
+        boolean shouldCompaction = 
includingPattern.matcher(paimonFullTableName).matches();
+        if (excludingPattern != null) {
+            shouldCompaction =
+                    shouldCompaction && 
!excludingPattern.matcher(paimonFullTableName).matches();
+        }
+        if (!shouldCompaction) {
+            LOG.debug("Source table '{}' is excluded.", paimonFullTableName);
+        }
+        return shouldCompaction;
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/CompactDatabaseActionITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/CompactDatabaseActionITCase.java
index b78eec829..0d15e9606 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/CompactDatabaseActionITCase.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/CompactDatabaseActionITCase.java
@@ -22,10 +22,13 @@ import org.apache.paimon.CoreOptions;
 import org.apache.paimon.Snapshot;
 import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.flink.FlinkConnectorOptions;
 import org.apache.paimon.operation.FileStoreScan;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.StreamTableCommit;
+import org.apache.paimon.table.sink.StreamTableWrite;
 import org.apache.paimon.table.sink.StreamWriteBuilder;
 import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.table.source.StreamTableScan;
@@ -39,9 +42,10 @@ import org.apache.flink.api.common.RuntimeExecutionMode;
 import org.apache.flink.core.execution.JobClient;
 import org.apache.flink.streaming.api.CheckpointingMode;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
-import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import java.time.Duration;
 import java.util.ArrayList;
@@ -59,23 +63,14 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
 
     private static final String[] DATABASE_NAMES = new String[] {"db1", "db2"};
     private static final String[] TABLE_NAMES = new String[] {"t1", "t2"};
-    private static final Map<String, RowType> ROW_TYPE_MAP = new 
HashMap<>(TABLE_NAMES.length);
-
-    @BeforeAll
-    public static void beforeAll() {
-        // set different datatype and RowType
-        DataType[] dataTypes1 =
-                new DataType[] {
-                    DataTypes.INT(), DataTypes.INT(), DataTypes.INT(), 
DataTypes.STRING()
-                };
-        DataType[] dataTypes2 =
-                new DataType[] {
-                    DataTypes.INT(), DataTypes.BIGINT(), DataTypes.INT(), 
DataTypes.STRING()
-                };
-
-        ROW_TYPE_MAP.put("t1", RowType.of(dataTypes1, new String[] {"k", "v", 
"hh", "dt"}));
-        ROW_TYPE_MAP.put("t2", RowType.of(dataTypes2, new String[] {"k", "v1", 
"hh", "dt"}));
-    }
+    private static final String[] New_DATABASE_NAMES = new String[] {"db3", 
"db4"};
+    private static final String[] New_TABLE_NAMES = new String[] {"t3", "t4"};
+    private static final RowType ROW_TYPE =
+            RowType.of(
+                    new DataType[] {
+                        DataTypes.INT(), DataTypes.INT(), DataTypes.INT(), 
DataTypes.STRING()
+                    },
+                    new String[] {"k", "v", "hh", "dt"});
 
     private FileStoreTable createTable(
             String databaseName,
@@ -95,12 +90,15 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         return (FileStoreTable) catalog.getTable(identifier);
     }
 
-    @Test
+    @ParameterizedTest(name = "mode = {0}")
+    @ValueSource(strings = {"divided", "combined"})
     @Timeout(60)
-    public void testBatchCompact() throws Exception {
+    public void testBatchCompact(String mode) throws Exception {
         Map<String, String> options = new HashMap<>();
         options.put(CoreOptions.WRITE_ONLY.key(), "true");
 
+        Map<String, String> compactOptions = new HashMap<>();
+
         List<FileStoreTable> tables = new ArrayList<>();
 
         for (String dbName : DATABASE_NAMES) {
@@ -109,7 +107,7 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                         createTable(
                                 dbName,
                                 tableName,
-                                ROW_TYPE_MAP.get(tableName),
+                                ROW_TYPE,
                                 Arrays.asList("dt", "hh"),
                                 Arrays.asList("dt", "hh", "k"),
                                 options);
@@ -120,22 +118,15 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                 write = streamWriteBuilder.newWrite();
                 commit = streamWriteBuilder.newCommit();
 
-                Object value = null;
-                if (tableName.equals("t1")) {
-                    value = 100;
-                } else if (tableName.equals("t2")) {
-                    value = 100L;
-                }
-
                 writeData(
-                        rowData(1, value, 15, 
BinaryString.fromString("20221208")),
-                        rowData(1, value, 16, 
BinaryString.fromString("20221208")),
-                        rowData(1, value, 15, 
BinaryString.fromString("20221209")));
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 16, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221209")));
 
                 writeData(
-                        rowData(2, value, 15, 
BinaryString.fromString("20221208")),
-                        rowData(2, value, 16, 
BinaryString.fromString("20221208")),
-                        rowData(2, value, 15, 
BinaryString.fromString("20221209")));
+                        rowData(2, 100, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 100, 16, 
BinaryString.fromString("20221208")),
+                        rowData(2, 100, 15, 
BinaryString.fromString("20221209")));
 
                 Snapshot snapshot = 
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
                 assertThat(snapshot.id()).isEqualTo(2);
@@ -146,10 +137,26 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
         env.setRuntimeMode(RuntimeExecutionMode.BATCH);
         env.setParallelism(ThreadLocalRandom.current().nextInt(2) + 1);
-        new CompactDatabaseAction(warehouse, "db1|db2", null, null, new 
HashMap<>()).build(env);
+        if (mode.equals("divided")) {
+            new CompactDatabaseAction(warehouse, new HashMap<>())
+                    .includingDatabases(null)
+                    .includingTables(null)
+                    .excludingTables(null)
+                    .build(env);
+        } else {
+            new CompactDatabaseAction(warehouse, new HashMap<>())
+                    .includingDatabases(null)
+                    .includingTables(null)
+                    .excludingTables(null)
+                    .withDatabaseCompactMode("combined")
+                    .withCompactOptions(compactOptions)
+                    .build(env);
+        }
+
         env.execute();
 
         for (FileStoreTable table : tables) {
+            snapshotManager = table.snapshotManager();
             Snapshot snapshot =
                     
table.snapshotManager().snapshot(snapshotManager.latestSnapshotId());
             assertThat(snapshot.id()).isEqualTo(3);
@@ -163,10 +170,232 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         }
     }
 
-    @Test
+    @ParameterizedTest(name = "mode = {0}")
+    @ValueSource(strings = {"divided", "combined"})
+    public void testStreamingCompact(String mode) throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.CHANGELOG_PRODUCER.key(), "full-compaction");
+        options.put(
+                
FlinkConnectorOptions.CHANGELOG_PRODUCER_FULL_COMPACTION_TRIGGER_INTERVAL.key(),
+                "1s");
+        options.put(CoreOptions.CONTINUOUS_DISCOVERY_INTERVAL.key(), "1s");
+        options.put(CoreOptions.WRITE_ONLY.key(), "true");
+        // test that dedicated compact job will expire snapshots
+        options.put(CoreOptions.SNAPSHOT_NUM_RETAINED_MIN.key(), "3");
+        options.put(CoreOptions.SNAPSHOT_NUM_RETAINED_MAX.key(), "3");
+
+        Map<String, String> compactOptions = new HashMap<>();
+        // if CoreOptions.CONTINUOUS_DISCOVERY_INTERVAL.key() use default 
value, the cost time in
+        // combined mode will be over 1mins
+        compactOptions.put(CoreOptions.CONTINUOUS_DISCOVERY_INTERVAL.key(), 
"1s");
+
+        List<FileStoreTable> tables = new ArrayList<>();
+        for (String dbName : DATABASE_NAMES) {
+            for (String tableName : TABLE_NAMES) {
+                FileStoreTable table =
+                        createTable(
+                                dbName,
+                                tableName,
+                                ROW_TYPE,
+                                Arrays.asList("dt", "hh"),
+                                Arrays.asList("dt", "hh", "k"),
+                                options);
+                tables.add(table);
+                snapshotManager = table.snapshotManager();
+                StreamWriteBuilder streamWriteBuilder =
+                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                write = streamWriteBuilder.newWrite();
+                commit = streamWriteBuilder.newCommit();
+
+                // base records
+                writeData(
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 16, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221209")));
+
+                Snapshot snapshot = 
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
+                assertThat(snapshot.id()).isEqualTo(1);
+                
assertThat(snapshot.commitKind()).isEqualTo(Snapshot.CommitKind.APPEND);
+
+                // no full compaction has happened, so plan should be empty
+                StreamTableScan scan = table.newReadBuilder().newStreamScan();
+                TableScan.Plan plan = scan.plan();
+                assertThat(plan.splits()).isEmpty();
+            }
+        }
+
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
+        
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
+        env.getCheckpointConfig().setCheckpointInterval(500);
+        env.setParallelism(ThreadLocalRandom.current().nextInt(2) + 1);
+        if (mode.equals("divided")) {
+            new CompactDatabaseAction(warehouse, new HashMap<>())
+                    .includingDatabases(null)
+                    .includingTables(null)
+                    .excludingTables(null)
+                    .build(env);
+        } else {
+            new CompactDatabaseAction(warehouse, new HashMap<>())
+                    .includingDatabases(null)
+                    .includingTables(null)
+                    .excludingTables(null)
+                    .withDatabaseCompactMode("combined")
+                    .withCompactOptions(compactOptions)
+                    .build(env);
+        }
+        JobClient client = env.executeAsync();
+
+        for (FileStoreTable table : tables) {
+            StreamTableScan scan = table.newReadBuilder().newStreamScan();
+            // first full compaction
+            validateResult(
+                    table,
+                    ROW_TYPE,
+                    scan,
+                    Arrays.asList(
+                            "+I[1, 100, 15, 20221208]",
+                            "+I[1, 100, 15, 20221209]",
+                            "+I[1, 100, 16, 20221208]"),
+                    60_000);
+
+            snapshotManager = table.snapshotManager();
+            StreamWriteBuilder streamWriteBuilder =
+                    table.newStreamWriteBuilder().withCommitUser(commitUser);
+            write = streamWriteBuilder.newWrite();
+            commit = streamWriteBuilder.newCommit();
+
+            // incremental records
+            writeData(
+                    rowData(1, 101, 15, BinaryString.fromString("20221208")),
+                    rowData(1, 101, 16, BinaryString.fromString("20221208")),
+                    rowData(1, 101, 15, BinaryString.fromString("20221209")));
+
+            // second full compaction
+            validateResult(
+                    table,
+                    ROW_TYPE,
+                    scan,
+                    Arrays.asList(
+                            "+U[1, 101, 15, 20221208]",
+                            "+U[1, 101, 15, 20221209]",
+                            "+U[1, 101, 16, 20221208]",
+                            "-U[1, 100, 15, 20221208]",
+                            "-U[1, 100, 15, 20221209]",
+                            "-U[1, 100, 16, 20221208]"),
+                    60_000);
+
+            // assert dedicated compact job will expire snapshots
+            CommonTestUtils.waitUtil(
+                    () ->
+                            snapshotManager.latestSnapshotId() - 2
+                                    == snapshotManager.earliestSnapshotId(),
+                    Duration.ofSeconds(60_000),
+                    Duration.ofSeconds(100),
+                    String.format(
+                            "Cannot validate snapshot expiration in %s 
milliseconds.", 60_000));
+        }
+
+        // In combined mode, check whether newly created table can be detected
+        if (mode.equals("combined")) {
+            // second create tables and write data to tables
+            List<FileStoreTable> newtables = new ArrayList<>();
+            for (String dbName : New_DATABASE_NAMES) {
+                for (String tableName : New_TABLE_NAMES) {
+                    FileStoreTable table =
+                            createTable(
+                                    dbName,
+                                    tableName,
+                                    ROW_TYPE,
+                                    Arrays.asList("dt", "hh"),
+                                    Arrays.asList("dt", "hh", "k"),
+                                    options);
+                    newtables.add(table);
+                    snapshotManager = table.snapshotManager();
+                    StreamWriteBuilder streamWriteBuilder =
+                            
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                    write = streamWriteBuilder.newWrite();
+                    commit = streamWriteBuilder.newCommit();
+
+                    // base records
+                    writeData(
+                            write,
+                            commit,
+                            0,
+                            rowData(1, 100, 15, 
BinaryString.fromString("20221208")),
+                            rowData(1, 100, 16, 
BinaryString.fromString("20221208")),
+                            rowData(1, 100, 15, 
BinaryString.fromString("20221209")));
+
+                    Snapshot snapshot =
+                            
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
+                    assertThat(snapshot.id()).isEqualTo(1);
+                    
assertThat(snapshot.commitKind()).isEqualTo(Snapshot.CommitKind.APPEND);
+                }
+            }
+
+            for (FileStoreTable table : newtables) {
+                StreamTableScan scan = table.newReadBuilder().newStreamScan();
+                // first full compaction for new tables
+                validateResult(
+                        table,
+                        ROW_TYPE,
+                        scan,
+                        Arrays.asList(
+                                "+I[1, 100, 15, 20221208]",
+                                "+I[1, 100, 15, 20221209]",
+                                "+I[1, 100, 16, 20221208]"),
+                        60_000);
+
+                snapshotManager = table.snapshotManager();
+                StreamWriteBuilder streamWriteBuilder =
+                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                write = streamWriteBuilder.newWrite();
+                commit = streamWriteBuilder.newCommit();
+
+                // incremental records
+                writeData(
+                        write,
+                        commit,
+                        1,
+                        rowData(1, 101, 15, 
BinaryString.fromString("20221208")),
+                        rowData(1, 101, 16, 
BinaryString.fromString("20221208")),
+                        rowData(1, 101, 15, 
BinaryString.fromString("20221209")));
+
+                // second full compaction for new tables
+                validateResult(
+                        table,
+                        ROW_TYPE,
+                        scan,
+                        Arrays.asList(
+                                "+U[1, 101, 15, 20221208]",
+                                "+U[1, 101, 15, 20221209]",
+                                "+U[1, 101, 16, 20221208]",
+                                "-U[1, 100, 15, 20221208]",
+                                "-U[1, 100, 15, 20221209]",
+                                "-U[1, 100, 16, 20221208]"),
+                        60_000);
+
+                // assert dedicated compact job will expire snapshots
+                CommonTestUtils.waitUtil(
+                        () ->
+                                snapshotManager.latestSnapshotId() - 2
+                                        == 
snapshotManager.earliestSnapshotId(),
+                        Duration.ofSeconds(60_000),
+                        Duration.ofSeconds(100),
+                        String.format(
+                                "Cannot validate snapshot expiration in %s 
milliseconds.", 60_000));
+            }
+        }
+
+        client.cancel();
+    }
+
+    @ParameterizedTest(name = "mode = {0}")
+    @ValueSource(strings = {"divided", "combined"})
     @Timeout(60)
-    public void includeTableCompaction() throws Exception {
+    public void includeTableCompaction(String mode) throws Exception {
         includingAndExcludingTablesImpl(
+                mode,
                 "db1.t1",
                 null,
                 Collections.singletonList(Identifier.fromString("db1.t1")),
@@ -176,10 +405,12 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                         Identifier.fromString("db2.t2")));
     }
 
-    @Test
+    @ParameterizedTest(name = "mode = {0}")
+    @ValueSource(strings = {"divided", "combined"})
     @Timeout(60)
-    public void excludeTableCompaction() throws Exception {
+    public void excludeTableCompaction(String mode) throws Exception {
         includingAndExcludingTablesImpl(
+                mode,
                 null,
                 "db2.t2",
                 Arrays.asList(
@@ -189,10 +420,12 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                 Collections.singletonList(Identifier.fromString("db2.t2")));
     }
 
-    @Test
+    @ParameterizedTest(name = "mode = {0}")
+    @ValueSource(strings = {"divided", "combined"})
     @Timeout(60)
-    public void includeAndExcludeTableCompaction() throws Exception {
+    public void includeAndExcludeTableCompaction(String mode) throws Exception 
{
         includingAndExcludingTablesImpl(
+                mode,
                 "db1.+|db2.t1",
                 "db1.t2",
                 Arrays.asList(Identifier.fromString("db1.t1"), 
Identifier.fromString("db2.t1")),
@@ -200,6 +433,7 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
     }
 
     private void includingAndExcludingTablesImpl(
+            String mode,
             String includingPattern,
             String excludesPattern,
             List<Identifier> includeTables,
@@ -208,6 +442,9 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         Map<String, String> options = new HashMap<>();
         options.put(CoreOptions.WRITE_ONLY.key(), "true");
 
+        Map<String, String> compactOptions = new HashMap<>();
+        compactOptions.put(CoreOptions.CONTINUOUS_DISCOVERY_INTERVAL.key(), 
"1s");
+
         List<FileStoreTable> compactionTables = new ArrayList<>();
         List<FileStoreTable> noCompactionTables = new ArrayList<>();
 
@@ -217,7 +454,7 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                         createTable(
                                 dbName,
                                 tableName,
-                                ROW_TYPE_MAP.get(tableName),
+                                ROW_TYPE,
                                 Arrays.asList("dt", "hh"),
                                 Arrays.asList("dt", "hh", "k"),
                                 options);
@@ -233,22 +470,15 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                 write = streamWriteBuilder.newWrite();
                 commit = streamWriteBuilder.newCommit();
 
-                Object value = null;
-                if (tableName.equals("t1")) {
-                    value = 100;
-                } else if (tableName.equals("t2")) {
-                    value = 100L;
-                }
-
                 writeData(
-                        rowData(1, value, 15, 
BinaryString.fromString("20221208")),
-                        rowData(1, value, 16, 
BinaryString.fromString("20221208")),
-                        rowData(1, value, 15, 
BinaryString.fromString("20221209")));
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 16, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221209")));
 
                 writeData(
-                        rowData(2, value, 15, 
BinaryString.fromString("20221208")),
-                        rowData(2, value, 16, 
BinaryString.fromString("20221208")),
-                        rowData(2, value, 15, 
BinaryString.fromString("20221209")));
+                        rowData(2, 100, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 100, 16, 
BinaryString.fromString("20221208")),
+                        rowData(2, 100, 15, 
BinaryString.fromString("20221209")));
 
                 Snapshot snapshot = 
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
                 assertThat(snapshot.id()).isEqualTo(2);
@@ -259,9 +489,22 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
         env.setRuntimeMode(RuntimeExecutionMode.BATCH);
         env.setParallelism(ThreadLocalRandom.current().nextInt(2) + 1);
-        new CompactDatabaseAction(
-                        warehouse, "db1|db2", includingPattern, 
excludesPattern, new HashMap<>())
-                .build(env);
+
+        if (mode.equals("divided")) {
+            new CompactDatabaseAction(warehouse, new HashMap<>())
+                    .includingDatabases(null)
+                    .includingTables(includingPattern)
+                    .excludingTables(excludesPattern)
+                    .build(env);
+        } else {
+            new CompactDatabaseAction(warehouse, new HashMap<>())
+                    .includingDatabases(null)
+                    .includingTables(includingPattern)
+                    .excludingTables(excludesPattern)
+                    .withDatabaseCompactMode("combined")
+                    .withCompactOptions(compactOptions)
+                    .build(env);
+        }
         env.execute();
 
         for (FileStoreTable table : compactionTables) {
@@ -295,130 +538,6 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         }
     }
 
-    @Test
-    public void testStreamingCompact() throws Exception {
-        Map<String, String> options = new HashMap<>();
-        options.put(CoreOptions.CHANGELOG_PRODUCER.key(), "full-compaction");
-        options.put(
-                
FlinkConnectorOptions.CHANGELOG_PRODUCER_FULL_COMPACTION_TRIGGER_INTERVAL.key(),
-                "1s");
-        options.put(CoreOptions.CONTINUOUS_DISCOVERY_INTERVAL.key(), "1s");
-        options.put(CoreOptions.WRITE_ONLY.key(), "true");
-        // test that dedicated compact job will expire snapshots
-        options.put(CoreOptions.SNAPSHOT_NUM_RETAINED_MIN.key(), "3");
-        options.put(CoreOptions.SNAPSHOT_NUM_RETAINED_MAX.key(), "3");
-
-        List<FileStoreTable> tables = new ArrayList<>();
-        for (String dbName : DATABASE_NAMES) {
-            for (String tableName : TABLE_NAMES) {
-                FileStoreTable table =
-                        createTable(
-                                dbName,
-                                tableName,
-                                ROW_TYPE_MAP.get(tableName),
-                                Arrays.asList("dt", "hh"),
-                                Arrays.asList("dt", "hh", "k"),
-                                options);
-                tables.add(table);
-                snapshotManager = table.snapshotManager();
-                StreamWriteBuilder streamWriteBuilder =
-                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
-                write = streamWriteBuilder.newWrite();
-                commit = streamWriteBuilder.newCommit();
-
-                Object value = null;
-                if (tableName.equals("t1")) {
-                    value = 100;
-                } else if (tableName.equals("t2")) {
-                    value = 100L;
-                }
-
-                // base records
-                writeData(
-                        rowData(1, value, 15, 
BinaryString.fromString("20221208")),
-                        rowData(1, value, 16, 
BinaryString.fromString("20221208")),
-                        rowData(1, value, 15, 
BinaryString.fromString("20221209")));
-
-                Snapshot snapshot = 
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
-                assertThat(snapshot.id()).isEqualTo(1);
-                
assertThat(snapshot.commitKind()).isEqualTo(Snapshot.CommitKind.APPEND);
-
-                // no full compaction has happened, so plan should be empty
-                StreamTableScan scan = table.newReadBuilder().newStreamScan();
-                TableScan.Plan plan = scan.plan();
-                assertThat(plan.splits()).isEmpty();
-            }
-        }
-
-        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
-        env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
-        
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
-        env.getCheckpointConfig().setCheckpointInterval(500);
-        env.setParallelism(ThreadLocalRandom.current().nextInt(2) + 1);
-        new CompactDatabaseAction(warehouse, "db1|db2", null, null, new 
HashMap<>()).build(env);
-        JobClient client = env.executeAsync();
-
-        for (FileStoreTable table : tables) {
-            StreamTableScan scan = table.newReadBuilder().newStreamScan();
-            // first full compaction
-            validateResult(
-                    table,
-                    ROW_TYPE_MAP.get(table.name()),
-                    scan,
-                    Arrays.asList(
-                            "+I[1, 100, 15, 20221208]",
-                            "+I[1, 100, 15, 20221209]",
-                            "+I[1, 100, 16, 20221208]"),
-                    60_000);
-
-            Object value = null;
-            String tName = table.name();
-            if (tName.equals("t1")) {
-                value = 101;
-            } else if (tName.equals("t2")) {
-                value = 101L;
-            }
-
-            snapshotManager = table.snapshotManager();
-            StreamWriteBuilder streamWriteBuilder =
-                    table.newStreamWriteBuilder().withCommitUser(commitUser);
-            write = streamWriteBuilder.newWrite();
-            commit = streamWriteBuilder.newCommit();
-
-            // incremental records
-            writeData(
-                    rowData(1, value, 15, BinaryString.fromString("20221208")),
-                    rowData(1, value, 16, BinaryString.fromString("20221208")),
-                    rowData(1, value, 15, 
BinaryString.fromString("20221209")));
-
-            // second full compaction
-            validateResult(
-                    table,
-                    ROW_TYPE_MAP.get(table.name()),
-                    scan,
-                    Arrays.asList(
-                            "+U[1, 101, 15, 20221208]",
-                            "+U[1, 101, 15, 20221209]",
-                            "+U[1, 101, 16, 20221208]",
-                            "-U[1, 100, 15, 20221208]",
-                            "-U[1, 100, 15, 20221209]",
-                            "-U[1, 100, 16, 20221208]"),
-                    60_000);
-
-            // assert dedicated compact job will expire snapshots
-            CommonTestUtils.waitUtil(
-                    () ->
-                            snapshotManager.latestSnapshotId() - 2
-                                    == snapshotManager.earliestSnapshotId(),
-                    Duration.ofSeconds(60_000),
-                    Duration.ofSeconds(100),
-                    String.format(
-                            "Cannot validate snapshot expiration in %s 
milliseconds.", 60_000));
-        }
-
-        client.cancel();
-    }
-
     @Test
     public void testUnawareBucketStreamingCompact() throws Exception {
         Map<String, String> options = new HashMap<>();
@@ -434,7 +553,7 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                     createTable(
                             database,
                             tableName,
-                            ROW_TYPE_MAP.get(tableName),
+                            ROW_TYPE,
                             Arrays.asList("k"),
                             Collections.emptyList(),
                             options);
@@ -445,23 +564,16 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
             write = streamWriteBuilder.newWrite();
             commit = streamWriteBuilder.newCommit();
 
-            Object value = null;
-            if (tableName.equals("t1")) {
-                value = 100;
-            } else if (tableName.equals("t2")) {
-                value = 100L;
-            }
-
             // base records
             writeData(
-                    rowData(1, value, 15, BinaryString.fromString("20221208")),
-                    rowData(1, value, 16, BinaryString.fromString("20221208")),
-                    rowData(1, value, 15, 
BinaryString.fromString("20221209")));
+                    rowData(1, 100, 15, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 16, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 15, BinaryString.fromString("20221209")));
 
             writeData(
-                    rowData(1, value, 15, BinaryString.fromString("20221208")),
-                    rowData(1, value, 16, BinaryString.fromString("20221208")),
-                    rowData(1, value, 15, 
BinaryString.fromString("20221209")));
+                    rowData(1, 100, 15, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 16, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 15, BinaryString.fromString("20221209")));
 
             Snapshot snapshot = 
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
             assertThat(snapshot.id()).isEqualTo(2);
@@ -473,7 +585,11 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
         env.getCheckpointConfig().setCheckpointInterval(500);
         env.setParallelism(ThreadLocalRandom.current().nextInt(2) + 1);
-        new CompactDatabaseAction(warehouse, database, null, null, new 
HashMap<>()).build(env);
+        new CompactDatabaseAction(warehouse, new HashMap<>())
+                .includingDatabases(null)
+                .includingTables(null)
+                .excludingTables(null)
+                .build(env);
         JobClient client = env.executeAsync();
 
         for (FileStoreTable table : tables) {
@@ -488,18 +604,10 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
             // first compaction, snapshot will be 3
             checkFileAndRowSize(storeScan, 3L, 30_000L, 1, 6);
 
-            Object value = null;
-            String tName = table.name();
-            if (tName.equals("t1")) {
-                value = 101;
-            } else if (tName.equals("t2")) {
-                value = 101L;
-            }
-
             writeData(
-                    rowData(1, value, 15, BinaryString.fromString("20221208")),
-                    rowData(1, value, 16, BinaryString.fromString("20221208")),
-                    rowData(1, value, 15, 
BinaryString.fromString("20221209")));
+                    rowData(1, 101, 15, BinaryString.fromString("20221208")),
+                    rowData(1, 101, 16, BinaryString.fromString("20221208")),
+                    rowData(1, 101, 15, BinaryString.fromString("20221209")));
 
             // second compaction, snapshot will be 5
             checkFileAndRowSize(storeScan, 5L, 30_000L, 1, 9);
@@ -522,7 +630,7 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
                     createTable(
                             database,
                             tableName,
-                            ROW_TYPE_MAP.get(tableName),
+                            ROW_TYPE,
                             Collections.singletonList("k"),
                             Collections.emptyList(),
                             options);
@@ -533,23 +641,16 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
             write = streamWriteBuilder.newWrite();
             commit = streamWriteBuilder.newCommit();
 
-            Object value = null;
-            if (tableName.equals("t1")) {
-                value = 100;
-            } else if (tableName.equals("t2")) {
-                value = 100L;
-            }
-
             // base records
             writeData(
-                    rowData(1, value, 15, BinaryString.fromString("20221208")),
-                    rowData(1, value, 16, BinaryString.fromString("20221208")),
-                    rowData(1, value, 15, 
BinaryString.fromString("20221209")));
+                    rowData(1, 100, 15, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 16, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 15, BinaryString.fromString("20221209")));
 
             writeData(
-                    rowData(1, value, 15, BinaryString.fromString("20221208")),
-                    rowData(1, value, 16, BinaryString.fromString("20221208")),
-                    rowData(1, value, 15, 
BinaryString.fromString("20221209")));
+                    rowData(1, 100, 15, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 16, BinaryString.fromString("20221208")),
+                    rowData(1, 100, 15, BinaryString.fromString("20221209")));
 
             Snapshot snapshot = 
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
             assertThat(snapshot.id()).isEqualTo(2);
@@ -559,7 +660,11 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
         StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
         env.setRuntimeMode(RuntimeExecutionMode.BATCH);
         env.setParallelism(ThreadLocalRandom.current().nextInt(2) + 1);
-        new CompactDatabaseAction(warehouse, database, null, null, new 
HashMap<>()).build(env);
+        new CompactDatabaseAction(warehouse, new HashMap<>())
+                .includingDatabases(null)
+                .includingTables(null)
+                .excludingTables(null)
+                .build(env);
         env.execute();
 
         for (FileStoreTable table : tables) {
@@ -569,4 +674,16 @@ public class CompactDatabaseActionITCase extends 
CompactActionITCaseBase {
             checkFileAndRowSize(storeScan, 3L, 0L, 1, 6);
         }
     }
+
+    private void writeData(
+            StreamTableWrite write,
+            StreamTableCommit commit,
+            long incrementalIdentifier,
+            GenericRow... data)
+            throws Exception {
+        for (GenericRow d : data) {
+            write.write(d);
+        }
+        commit.commit(incrementalIdentifier, write.prepareCommit(true, 
incrementalIdentifier));
+    }
 }
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/MultiTablesCompactorSourceBuilderITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/MultiTablesCompactorSourceBuilderITCase.java
new file mode 100644
index 000000000..1a7127305
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/MultiTablesCompactorSourceBuilderITCase.java
@@ -0,0 +1,553 @@
+/*
+ * 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.source;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.flink.FlinkCatalogFactory;
+import org.apache.paimon.flink.util.AbstractTestBase;
+import org.apache.paimon.io.DataFileMetaSerializer;
+import org.apache.paimon.options.CatalogOptions;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.StreamTableCommit;
+import org.apache.paimon.table.sink.StreamTableWrite;
+import org.apache.paimon.table.sink.StreamWriteBuilder;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.SnapshotManager;
+
+import org.apache.flink.api.common.RuntimeExecutionMode;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.util.CloseableIterator;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.regex.Pattern;
+
+import static org.apache.paimon.utils.SerializationUtils.deserializeBinaryRow;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** IT cases for {@link MultiTablesCompactorSourceBuilder}. */
+public class MultiTablesCompactorSourceBuilderITCase extends AbstractTestBase
+        implements Serializable {
+    private String warehouse;
+    private Options catalogOptions;
+
+    private String commitUser;
+
+    private static final String[] DATABASE_NAMES = new String[] {"db1", "db2"};
+    private static final String[] TABLE_NAMES = new String[] {"t1", "t2"};
+    private static final String[] New_DATABASE_NAMES = new String[] {"db3"};
+    private static final String[] New_TABLE_NAMES = new String[] {"t1", "t2"};
+    private static final Map<String, RowType> ROW_TYPE_MAP = new 
HashMap<>(TABLE_NAMES.length);
+
+    @BeforeAll
+    public static void beforeAll() {
+        // set different datatype and RowType
+        DataType[] dataTypes1 =
+                new DataType[] {
+                    DataTypes.INT(), DataTypes.INT(), DataTypes.INT(), 
DataTypes.STRING()
+                };
+        DataType[] dataTypes2 =
+                new DataType[] {
+                    DataTypes.INT(), DataTypes.INT(), DataTypes.INT(), 
DataTypes.STRING()
+                };
+
+        ROW_TYPE_MAP.put("t1", RowType.of(dataTypes1, new String[] {"k", "v", 
"hh", "dt"}));
+        ROW_TYPE_MAP.put("t2", RowType.of(dataTypes2, new String[] {"k", "v1", 
"hh", "dt"}));
+    }
+
+    @BeforeEach
+    public void before() throws IOException {
+        warehouse = getTempDirPath();
+        catalogOptions = new Options();
+        commitUser = UUID.randomUUID().toString();
+    }
+
+    @ParameterizedTest(name = "defaultOptions = {0}")
+    @ValueSource(booleans = {true, false})
+    public void testBatchRead(boolean defaultOptions) throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.WRITE_ONLY.key(), "true");
+        if (!defaultOptions) {
+            // change options to test whether CompactorSourceBuilder work 
normally
+            options.put(CoreOptions.SCAN_SNAPSHOT_ID.key(), "2");
+        }
+        long monitorInterval = 1000;
+
+        for (String dbName : DATABASE_NAMES) {
+            for (String tableName : TABLE_NAMES) {
+                FileStoreTable table =
+                        createTable(
+                                dbName,
+                                tableName,
+                                ROW_TYPE_MAP.get(tableName),
+                                Arrays.asList("dt", "hh"),
+                                Arrays.asList("dt", "hh", "k"),
+                                options);
+                SnapshotManager snapshotManager = table.snapshotManager();
+                StreamWriteBuilder streamWriteBuilder =
+                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                StreamTableWrite write = streamWriteBuilder.newWrite();
+                StreamTableCommit commit = streamWriteBuilder.newCommit();
+
+                writeData(
+                        write,
+                        commit,
+                        0,
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 16, 
BinaryString.fromString("20221208")),
+                        rowData(1, 100, 15, 
BinaryString.fromString("20221209")));
+
+                writeData(
+                        write,
+                        commit,
+                        1,
+                        rowData(2, 100, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 100, 16, 
BinaryString.fromString("20221208")),
+                        rowData(2, 100, 15, 
BinaryString.fromString("20221209")));
+
+                Snapshot snapshot = 
snapshotManager.snapshot(snapshotManager.latestSnapshotId());
+                assertThat(snapshot.id()).isEqualTo(2);
+                
assertThat(snapshot.commitKind()).isEqualTo(Snapshot.CommitKind.APPEND);
+
+                write.close();
+                commit.close();
+            }
+        }
+
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setRuntimeMode(RuntimeExecutionMode.BATCH);
+        env.setParallelism(ThreadLocalRandom.current().nextInt(2) + 1);
+        DataStream<RowData> source =
+                new MultiTablesCompactorSourceBuilder(
+                                catalogLoader(),
+                                Pattern.compile("db1|db2"),
+                                Pattern.compile(".*"),
+                                null,
+                                monitorInterval)
+                        .withContinuousMode(false)
+                        .withEnv(env)
+                        .build();
+        CloseableIterator<RowData> it = source.executeAndCollect();
+        List<String> actual = new ArrayList<>();
+        while (it.hasNext()) {
+            actual.add(toString(it.next()));
+        }
+        assertThat(actual)
+                .hasSameElementsAs(
+                        Arrays.asList(
+                                "+I 2|20221208|15|0|0|db1|t1",
+                                "+I 2|20221209|15|0|0|db1|t1",
+                                "+I 2|20221208|16|0|0|db1|t1",
+                                "+I 2|20221208|15|0|0|db1|t2",
+                                "+I 2|20221209|15|0|0|db1|t2",
+                                "+I 2|20221208|16|0|0|db1|t2",
+                                "+I 2|20221208|15|0|0|db2|t1",
+                                "+I 2|20221209|15|0|0|db2|t1",
+                                "+I 2|20221208|16|0|0|db2|t1",
+                                "+I 2|20221208|15|0|0|db2|t2",
+                                "+I 2|20221209|15|0|0|db2|t2",
+                                "+I 2|20221208|16|0|0|db2|t2"));
+        it.close();
+    }
+
+    @ParameterizedTest(name = "defaultOptions = {0}")
+    @ValueSource(booleans = {true, false})
+    public void testStreamingRead(boolean defaultOptions) throws Exception {
+        Map<String, String> options = new HashMap<>();
+        if (!defaultOptions) {
+            // change options to test whether CompactorSourceBuilder work 
normally
+            options.put(CoreOptions.SCAN_SNAPSHOT_ID.key(), "2");
+            options.put(
+                    CoreOptions.CHANGELOG_PRODUCER.key(),
+                    CoreOptions.ChangelogProducer.NONE.toString());
+            options.put(CoreOptions.SCAN_BOUNDED_WATERMARK.key(), "0");
+        }
+        long monitorInterval = 1000;
+        List<FileStoreTable> tables = new ArrayList<>();
+        for (String dbName : DATABASE_NAMES) {
+            for (String tableName : TABLE_NAMES) {
+                FileStoreTable table =
+                        createTable(
+                                dbName,
+                                tableName,
+                                ROW_TYPE_MAP.get(tableName),
+                                Arrays.asList("dt", "hh"),
+                                Arrays.asList("dt", "hh", "k"),
+                                options);
+                tables.add(table);
+                StreamWriteBuilder streamWriteBuilder =
+                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                StreamTableWrite write = streamWriteBuilder.newWrite();
+                StreamTableCommit commit = streamWriteBuilder.newCommit();
+
+                writeData(
+                        write,
+                        commit,
+                        0,
+                        rowData(1, 1510, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 1620, 16, 
BinaryString.fromString("20221208")));
+
+                write.write(rowData(1, 1511, 15, 
BinaryString.fromString("20221208")));
+                write.write(rowData(1, 1510, 15, 
BinaryString.fromString("20221209")));
+                write.compact(binaryRow("20221208", 15), 0, true);
+                write.compact(binaryRow("20221209", 15), 0, true);
+                commit.commit(1, write.prepareCommit(true, 1));
+
+                writeData(
+                        write,
+                        commit,
+                        2,
+                        rowData(2, 1520, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 1621, 16, 
BinaryString.fromString("20221208")));
+                writeData(
+                        write,
+                        commit,
+                        3,
+                        rowData(1, 1512, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 1620, 16, 
BinaryString.fromString("20221209")));
+
+                write.close();
+                commit.close();
+            }
+        }
+
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        DataStream<RowData> compactorSource =
+                new MultiTablesCompactorSourceBuilder(
+                                catalogLoader(),
+                                Pattern.compile(".*"),
+                                Pattern.compile(".*"),
+                                null,
+                                monitorInterval)
+                        .withContinuousMode(true)
+                        .withEnv(env)
+                        .build();
+        CloseableIterator<RowData> it = compactorSource.executeAndCollect();
+
+        List<String> actual = new ArrayList<>();
+        for (int i = 0; i < 16; i++) {
+            actual.add(toString(it.next()));
+        }
+        assertThat(actual)
+                .hasSameElementsAs(
+                        Arrays.asList(
+                                "+I 4|20221208|15|0|1|db1|t1",
+                                "+I 4|20221208|16|0|1|db1|t1",
+                                "+I 4|20221208|15|0|1|db1|t2",
+                                "+I 4|20221208|16|0|1|db1|t2",
+                                "+I 4|20221208|15|0|1|db2|t1",
+                                "+I 4|20221208|16|0|1|db2|t1",
+                                "+I 4|20221208|15|0|1|db2|t2",
+                                "+I 4|20221208|16|0|1|db2|t2",
+                                "+I 5|20221209|16|0|1|db1|t1",
+                                "+I 5|20221208|15|0|1|db1|t1",
+                                "+I 5|20221209|16|0|1|db1|t2",
+                                "+I 5|20221208|15|0|1|db1|t2",
+                                "+I 5|20221209|16|0|1|db2|t1",
+                                "+I 5|20221208|15|0|1|db2|t1",
+                                "+I 5|20221209|16|0|1|db2|t2",
+                                "+I 5|20221208|15|0|1|db2|t2"));
+
+        for (FileStoreTable table : tables) {
+            StreamWriteBuilder streamWriteBuilder =
+                    table.newStreamWriteBuilder().withCommitUser(commitUser);
+            StreamTableWrite write = streamWriteBuilder.newWrite();
+            StreamTableCommit commit = streamWriteBuilder.newCommit();
+            writeData(
+                    write,
+                    commit,
+                    4,
+                    rowData(2, 1520, 15, BinaryString.fromString("20221209")),
+                    rowData(1, 1510, 16, BinaryString.fromString("20221208")),
+                    rowData(1, 1511, 15, BinaryString.fromString("20221209")));
+        }
+        actual.clear();
+        for (int i = 0; i < 8; i++) {
+            actual.add(toString(it.next()));
+        }
+        assertThat(actual)
+                .hasSameElementsAs(
+                        Arrays.asList(
+                                "+I 6|20221209|15|0|1|db1|t1",
+                                "+I 6|20221208|16|0|1|db1|t1",
+                                "+I 6|20221209|15|0|1|db1|t2",
+                                "+I 6|20221208|16|0|1|db1|t2",
+                                "+I 6|20221209|15|0|1|db2|t1",
+                                "+I 6|20221208|16|0|1|db2|t1",
+                                "+I 6|20221209|15|0|1|db2|t2",
+                                "+I 6|20221208|16|0|1|db2|t2"));
+
+        // check if newly created tables can be detected
+        for (String dbName : New_DATABASE_NAMES) {
+            for (String tableName : New_TABLE_NAMES) {
+                FileStoreTable table =
+                        createTable(
+                                dbName,
+                                tableName,
+                                ROW_TYPE_MAP.get(tableName),
+                                Arrays.asList("dt", "hh"),
+                                Arrays.asList("dt", "hh", "k"),
+                                options);
+                tables.add(table);
+                StreamWriteBuilder streamWriteBuilder =
+                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                StreamTableWrite write = streamWriteBuilder.newWrite();
+                StreamTableCommit commit = streamWriteBuilder.newCommit();
+                writeData(
+                        write,
+                        commit,
+                        0,
+                        rowData(2, 1520, 15, 
BinaryString.fromString("20221209")),
+                        rowData(1, 1510, 16, 
BinaryString.fromString("20221208")));
+            }
+        }
+        actual.clear();
+        for (int i = 0; i < 4; i++) {
+            actual.add(toString(it.next()));
+        }
+        assertThat(actual)
+                .hasSameElementsAs(
+                        Arrays.asList(
+                                "+I 1|20221209|15|0|1|db3|t2",
+                                "+I 1|20221208|16|0|1|db3|t2",
+                                "+I 1|20221209|15|0|1|db3|t1",
+                                "+I 1|20221208|16|0|1|db3|t1"));
+        it.close();
+    }
+
+    @ParameterizedTest(name = "defaultOptions = {0}")
+    @ValueSource(booleans = {true, false})
+    public void testIncludeAndExcludeTableRead(boolean defaultOptions) throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        if (!defaultOptions) {
+            // change options to test whether CompactorSourceBuilder work 
normally
+            options.put(CoreOptions.SCAN_SNAPSHOT_ID.key(), "2");
+            options.put(
+                    CoreOptions.CHANGELOG_PRODUCER.key(),
+                    CoreOptions.ChangelogProducer.NONE.toString());
+            options.put(CoreOptions.SCAN_BOUNDED_WATERMARK.key(), "0");
+        }
+        long monitorInterval = 1000;
+
+        for (String dbName : DATABASE_NAMES) {
+            for (String tableName : TABLE_NAMES) {
+                FileStoreTable table =
+                        createTable(
+                                dbName,
+                                tableName,
+                                ROW_TYPE_MAP.get(tableName),
+                                Arrays.asList("dt", "hh"),
+                                Arrays.asList("dt", "hh", "k"),
+                                options);
+                StreamWriteBuilder streamWriteBuilder =
+                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                StreamTableWrite write = streamWriteBuilder.newWrite();
+                StreamTableCommit commit = streamWriteBuilder.newCommit();
+
+                writeData(
+                        write,
+                        commit,
+                        0,
+                        rowData(1, 1510, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 1620, 16, 
BinaryString.fromString("20221208")));
+
+                write.write(rowData(1, 1511, 15, 
BinaryString.fromString("20221208")));
+                write.write(rowData(1, 1510, 15, 
BinaryString.fromString("20221209")));
+                write.compact(binaryRow("20221208", 15), 0, true);
+                write.compact(binaryRow("20221209", 15), 0, true);
+                commit.commit(1, write.prepareCommit(true, 1));
+
+                writeData(
+                        write,
+                        commit,
+                        2,
+                        rowData(2, 1520, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 1621, 16, 
BinaryString.fromString("20221208")));
+                writeData(
+                        write,
+                        commit,
+                        3,
+                        rowData(1, 1512, 15, 
BinaryString.fromString("20221208")),
+                        rowData(2, 1620, 16, 
BinaryString.fromString("20221209")));
+
+                write.close();
+                commit.close();
+            }
+        }
+
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        DataStream<RowData> compactorSource =
+                new MultiTablesCompactorSourceBuilder(
+                                catalogLoader(),
+                                Pattern.compile(".*"),
+                                Pattern.compile("db1.+|db2.t1|db3.t1"),
+                                Pattern.compile("db1.t2"),
+                                monitorInterval)
+                        .withContinuousMode(true)
+                        .withEnv(env)
+                        .build();
+        CloseableIterator<RowData> it = compactorSource.executeAndCollect();
+
+        List<String> actual = new ArrayList<>();
+        for (int i = 0; i < 8; i++) {
+            actual.add(toString(it.next()));
+        }
+        assertThat(actual)
+                .hasSameElementsAs(
+                        Arrays.asList(
+                                "+I 4|20221208|15|0|1|db1|t1",
+                                "+I 4|20221208|16|0|1|db1|t1",
+                                "+I 4|20221208|15|0|1|db2|t1",
+                                "+I 4|20221208|16|0|1|db2|t1",
+                                "+I 5|20221209|16|0|1|db1|t1",
+                                "+I 5|20221208|15|0|1|db1|t1",
+                                "+I 5|20221209|16|0|1|db2|t1",
+                                "+I 5|20221208|15|0|1|db2|t1"));
+
+        // check if newly created tables conform to the pattern rule
+        for (String dbName : New_DATABASE_NAMES) {
+            for (String tableName : New_TABLE_NAMES) {
+                FileStoreTable table =
+                        createTable(
+                                dbName,
+                                tableName,
+                                ROW_TYPE_MAP.get(tableName),
+                                Arrays.asList("dt", "hh"),
+                                Arrays.asList("dt", "hh", "k"),
+                                options);
+                StreamWriteBuilder streamWriteBuilder =
+                        
table.newStreamWriteBuilder().withCommitUser(commitUser);
+                StreamTableWrite write = streamWriteBuilder.newWrite();
+                StreamTableCommit commit = streamWriteBuilder.newCommit();
+                writeData(
+                        write,
+                        commit,
+                        0,
+                        rowData(2, 1520, 15, 
BinaryString.fromString("20221209")),
+                        rowData(1, 1510, 16, 
BinaryString.fromString("20221208")));
+            }
+        }
+        actual.clear();
+        for (int i = 0; i < 2; i++) {
+            actual.add(toString(it.next()));
+        }
+        assertThat(actual)
+                .hasSameElementsAs(
+                        Arrays.asList(
+                                "+I 1|20221209|15|0|1|db3|t1", "+I 
1|20221208|16|0|1|db3|t1"));
+        it.close();
+    }
+
+    private FileStoreTable createTable(
+            String databaseName,
+            String tableName,
+            RowType rowType,
+            List<String> partitionKeys,
+            List<String> primaryKeys,
+            Map<String, String> options)
+            throws Exception {
+        Catalog catalog = catalogLoader().load();
+        Identifier identifier = Identifier.create(databaseName, tableName);
+        catalog.createDatabase(databaseName, true);
+        catalog.createTable(
+                identifier,
+                new Schema(rowType.getFields(), partitionKeys, primaryKeys, 
options, ""),
+                false);
+        return (FileStoreTable) catalog.getTable(identifier);
+    }
+
+    private GenericRow rowData(Object... values) {
+        return GenericRow.of(values);
+    }
+
+    private void writeData(
+            StreamTableWrite write,
+            StreamTableCommit commit,
+            long incrementalIdentifier,
+            GenericRow... data)
+            throws Exception {
+        for (GenericRow d : data) {
+            write.write(d);
+        }
+        commit.commit(incrementalIdentifier, write.prepareCommit(true, 
incrementalIdentifier));
+    }
+
+    private String toString(RowData rowData) {
+        int numFiles;
+        DataFileMetaSerializer dataFileMetaSerializer = new 
DataFileMetaSerializer();
+        try {
+            numFiles = 
dataFileMetaSerializer.deserializeList(rowData.getBinary(3)).size();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+
+        BinaryRow partition = deserializeBinaryRow(rowData.getBinary(1));
+
+        return String.format(
+                "%s %d|%s|%d|%d|%d|%s|%s",
+                rowData.getRowKind().shortString(),
+                rowData.getLong(0),
+                partition.getString(0),
+                partition.getInt(1),
+                rowData.getInt(2),
+                numFiles,
+                rowData.getString(4),
+                rowData.getString(5));
+    }
+
+    private BinaryRow binaryRow(String dt, int hh) {
+        BinaryRow b = new BinaryRow(2);
+        BinaryRowWriter writer = new BinaryRowWriter(b);
+        writer.writeString(0, BinaryString.fromString(dt));
+        writer.writeInt(1, hh);
+        writer.complete();
+        return b;
+    }
+
+    private Catalog.Loader catalogLoader() {
+        // to make the action workflow serializable
+        catalogOptions.set(CatalogOptions.WAREHOUSE, warehouse);
+        return () -> FlinkCatalogFactory.createPaimonCatalog(catalogOptions);
+    }
+}


Reply via email to