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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 558765aa0a [flink] Support FLIP-510 key-only deletes on primary-key 
table sink (#8452)
558765aa0a is described below

commit 558765aa0aaec9552142ae601cf35e0da5e14eac
Author: IRC <[email protected]>
AuthorDate: Wed Jul 15 02:39:28 2026 +0200

    [flink] Support FLIP-510 key-only deletes on primary-key table sink (#8452)
---
 docs/generated/flink_connector_configuration.html  |   6 +
 .../apache/paimon/flink/FlinkConnectorOptions.java |  14 ++
 .../paimon/flink/sink/FlinkTableSinkBase.java      |  25 +++
 .../org/apache/paimon/flink/ChangelogModeTest.java |  25 +++
 .../apache/paimon/flink/KeyOnlyDeletesITCase.java  | 183 +++++++++++++++++++++
 .../paimon/flink/utils/ChangelogModeUtils.java     |  30 ++++
 .../paimon/flink/utils/ChangelogModeUtils.java     |  56 +++++++
 7 files changed, 339 insertions(+)

diff --git a/docs/generated/flink_connector_configuration.html 
b/docs/generated/flink_connector_configuration.html
index a80c819be0..83e9c0bb25 100644
--- a/docs/generated/flink_connector_configuration.html
+++ b/docs/generated/flink_connector_configuration.html
@@ -278,6 +278,12 @@ under the License.
             <td>MemorySize</td>
             <td>Weight of managed memory for RocksDB in cross-partition 
update, Flink will compute the memory size according to the weight, the actual 
memory used depends on the running environment.</td>
         </tr>
+        <tr>
+            <td><h5>sink.key-only-deletes.enabled</h5></td>
+            <td style="word-wrap: break-word;">false</td>
+            <td>Boolean</td>
+            <td>If true, a primary-key table sink advertises the key-only 
(partial) deletes capability, allowing the Flink planner to drop the upstream 
ChangelogNormalize node when the source produces deletes by key. Requires Flink 
2.1+; no effect on Flink 1.x or 2.0. Does not apply when the table has no 
primary key, when 'changelog-producer' is 'input', or when 'merge-engine' is 
'aggregation' or 'partial-update' with aggregation functions; in those cases a 
warning is logged. Disabled by [...]
+        </tr>
         <tr>
             <td><h5>sink.managed.writer-buffer-memory</h5></td>
             <td style="word-wrap: break-word;">256 mb</td>
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
index 7799acc2d2..bc61b8bbab 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
@@ -504,6 +504,20 @@ public class FlinkConnectorOptions {
                     .withDescription(
                             "Enable sink writer coordinator to plan data files 
in Job Manager.");
 
+    public static final ConfigOption<Boolean> SINK_KEY_ONLY_DELETES_ENABLED =
+            key("sink.key-only-deletes.enabled")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription(
+                            "If true, a primary-key table sink advertises the 
key-only (partial) "
+                                    + "deletes capability, allowing the Flink 
planner to drop the "
+                                    + "upstream ChangelogNormalize node when 
the source produces "
+                                    + "deletes by key. Requires Flink 2.1+; no 
effect on Flink 1.x or 2.0. "
+                                    + "Does not apply when the table has no 
primary key, when "
+                                    + "'changelog-producer' is 'input', or 
when 'merge-engine' is "
+                                    + "'aggregation' or 'partial-update' with 
aggregation functions; "
+                                    + "in those cases a warning is logged. 
Disabled by default.");
+
     public static final ConfigOption<MemorySize> 
SINK_WRITER_COORDINATOR_CACHE_MEMORY =
             key("sink.writer-coordinator.cache-memory")
                     .memoryType()
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkTableSinkBase.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkTableSinkBase.java
index 2790a8a926..da5b8b58eb 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkTableSinkBase.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkTableSinkBase.java
@@ -21,7 +21,9 @@ package org.apache.paimon.flink.sink;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.CoreOptions.ChangelogProducer;
 import org.apache.paimon.CoreOptions.MergeEngine;
+import org.apache.paimon.flink.FlinkConnectorOptions;
 import org.apache.paimon.flink.PaimonDataStreamSinkProvider;
+import org.apache.paimon.flink.utils.ChangelogModeUtils;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.table.FormatTable;
 import org.apache.paimon.table.Table;
@@ -34,6 +36,8 @@ import 
org.apache.flink.table.connector.sink.abilities.SupportsOverwrite;
 import org.apache.flink.table.connector.sink.abilities.SupportsPartitioning;
 import org.apache.flink.table.factories.DynamicTableFactory;
 import org.apache.flink.types.RowKind;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -57,6 +61,8 @@ public abstract class FlinkTableSinkBase
 
     protected final Table table;
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(FlinkTableSinkBase.class);
+
     protected Map<String, String> staticPartitions = new HashMap<>();
     protected boolean overwrite = false;
 
@@ -72,19 +78,24 @@ public abstract class FlinkTableSinkBase
         if (table.primaryKeys().isEmpty()) {
             // Don't check this, for example, only inserts are available from 
the database, but the
             // plan phase contains all changelogs
+            warnKeyOnlyDeletesIgnored("the table has no primary key");
             return requestedMode;
         } else {
             Options options = Options.fromMap(table.options());
             if (options.get(CHANGELOG_PRODUCER) == ChangelogProducer.INPUT) {
+                warnKeyOnlyDeletesIgnored("'changelog-producer' is 'input'");
                 return requestedMode;
             }
 
             if (options.get(MERGE_ENGINE) == MergeEngine.AGGREGATE) {
+                warnKeyOnlyDeletesIgnored("'merge-engine' is 'aggregation'");
                 return requestedMode;
             }
 
             if (options.get(MERGE_ENGINE) == MergeEngine.PARTIAL_UPDATE
                     && new CoreOptions(options).definedAggFunc()) {
+                warnKeyOnlyDeletesIgnored(
+                        "'merge-engine' is 'partial-update' with aggregation 
functions");
                 return requestedMode;
             }
 
@@ -95,10 +106,24 @@ public abstract class FlinkTableSinkBase
                     builder.addContainedKind(kind);
                 }
             }
+            if 
(options.get(FlinkConnectorOptions.SINK_KEY_ONLY_DELETES_ENABLED)) {
+                ChangelogModeUtils.enableKeyOnlyDeletes(builder);
+            }
             return builder.build();
         }
     }
 
+    private void warnKeyOnlyDeletesIgnored(String reason) {
+        if (Options.fromMap(table.options())
+                .get(FlinkConnectorOptions.SINK_KEY_ONLY_DELETES_ENABLED)) {
+            LOG.warn(
+                    "'{}' is set to true for table {}, but it has no effect 
because {}.",
+                    FlinkConnectorOptions.SINK_KEY_ONLY_DELETES_ENABLED.key(),
+                    tableIdentifier.asSummaryString(),
+                    reason);
+        }
+    }
+
     @Override
     public SinkRuntimeProvider getSinkRuntimeProvider(Context context) {
         if (overwrite && !context.isBounded()) {
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ChangelogModeTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ChangelogModeTest.java
index b968df4746..6a068b2ee3 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ChangelogModeTest.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ChangelogModeTest.java
@@ -20,6 +20,7 @@ package org.apache.paimon.flink;
 
 import org.apache.paimon.flink.sink.FlinkTableSink;
 import org.apache.paimon.flink.source.DataTableSource;
+import org.apache.paimon.flink.utils.ChangelogModeUtils;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.options.Options;
@@ -89,6 +90,21 @@ public class ChangelogModeTest {
                         .build());
     }
 
+    @Test
+    public void testKeyOnlyDeletesEnabled() throws Exception {
+        Options options = new Options();
+        options.set(FlinkConnectorOptions.SINK_KEY_ONLY_DELETES_ENABLED, true);
+        // keyOnlyDeletes is a no-op on Flink 1.x and set on Flink 2.1+, so 
build the expected
+        // mode through the same adapter the sink uses.
+        ChangelogMode.Builder expectSink =
+                ChangelogMode.newBuilder()
+                        .addContainedKind(RowKind.INSERT)
+                        .addContainedKind(RowKind.UPDATE_AFTER)
+                        .addContainedKind(RowKind.DELETE);
+        ChangelogModeUtils.enableKeyOnlyDeletes(expectSink);
+        test(options, ChangelogMode.upsert(), expectSink.build());
+    }
+
     @Test
     public void testInputChangelogProducer() throws Exception {
         Options options = new Options();
@@ -96,6 +112,15 @@ public class ChangelogModeTest {
         test(options, ChangelogMode.all(), ChangelogMode.all());
     }
 
+    @Test
+    public void testKeyOnlyDeletesIgnoredWithInputChangelogProducer() throws 
Exception {
+        Options options = new Options();
+        options.set(CHANGELOG_PRODUCER, INPUT);
+        options.set(FlinkConnectorOptions.SINK_KEY_ONLY_DELETES_ENABLED, true);
+        // The input changelog producer forces ChangelogMode.all(), so 
key-only deletes is ignored.
+        test(options, ChangelogMode.all(), ChangelogMode.all());
+    }
+
     @Test
     public void testLookupChangelogProducer() throws Exception {
         Options options = new Options();
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/KeyOnlyDeletesITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/KeyOnlyDeletesITCase.java
new file mode 100644
index 0000000000..dc8a5052dd
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/KeyOnlyDeletesITCase.java
@@ -0,0 +1,183 @@
+/*
+ * 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;
+
+import org.apache.paimon.utils.BlockingIterator;
+
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
+
+import org.apache.flink.table.planner.factories.TestValuesTableFactory;
+import org.apache.flink.types.Row;
+import org.apache.flink.types.RowKind;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * ITCase for FLIP-510 key-only (partial) deletes on a primary-key table with 
{@code merge-engine =
+ * deduplicate}, {@code changelog-producer = none} and {@code 
sink.key-only-deletes.enabled = true}.
+ *
+ * <p>Verifies data integrity end-to-end when the upstream source emits:
+ *
+ * <ul>
+ *   <li>updates as {@code +U} only (no {@code -U}/UPDATE_BEFORE), and
+ *   <li>deletes carrying only the primary key ({@code -D} with the value 
columns null).
+ * </ul>
+ *
+ * <p>The delete-by-key and update-without-before shapes are guaranteed by how 
the input is built (a
+ * {@code values} source with {@code changelog-mode = 'I,UA,D'} and DELETE 
rows populated only on
+ * the key). Enabling {@code sink.key-only-deletes.enabled} lets the Flink 
planner drop the upstream
+ * ChangelogNormalize so those records reach the Paimon sink as-is. The API 
backing this capability
+ * only exists in Flink 2.1+, so the test is gated on that.
+ */
+public class KeyOnlyDeletesITCase extends CatalogITCaseBase {
+
+    /**
+     * Streams +I / +U (no -U) / key-only -D over two keys:
+     *
+     * <ul>
+     *   <li>k1: +I (1,1) → +U (1,2) → -D (1) — deleted, must be gone;
+     *   <li>k2: +I (2,20) → -D (2) → +I (2,21) — deleted then re-inserted, 
latest value kept.
+     * </ul>
+     *
+     * <p>Final state must be exactly (2,21), proving the key-only delete 
removes the row and leaves
+     * no tombstone stuck to the key on re-insert.
+     */
+    @Test
+    @EnabledIf("isFlink2_1OrAbove")
+    public void testKeyOnlyDeletesIntegrity() throws Exception {
+        sql(
+                "CREATE TABLE T (k INT PRIMARY KEY NOT ENFORCED, v INT) WITH ("
+                        + " 'merge-engine' = 'deduplicate',"
+                        + " 'changelog-producer' = 'none',"
+                        + " 'sink.key-only-deletes.enabled' = 'true')");
+
+        List<Row> input =
+                ImmutableList.of(
+                        Row.ofKind(RowKind.INSERT, 1, 1), // +I (k1, v1)
+                        Row.ofKind(RowKind.UPDATE_AFTER, 1, 2), // +U (k1, 
v2), no -U
+                        Row.ofKind(RowKind.DELETE, 1, null), // -D (k1), 
key-only → gone
+                        Row.ofKind(RowKind.INSERT, 2, 20), // +I (k2, v20)
+                        Row.ofKind(RowKind.DELETE, 2, null), // -D (k2), 
key-only
+                        Row.ofKind(RowKind.INSERT, 2, 21)); // +I (k2, v21), 
re-insert
+
+        String id = TestValuesTableFactory.registerData(input);
+        streamSqlIter(
+                        "CREATE TEMPORARY TABLE input (k INT PRIMARY KEY NOT 
ENFORCED, v INT) WITH ("
+                                + " 'connector' = 'values',"
+                                + " 'bounded' = 'true',"
+                                + " 'data-id' = '%s',"
+                                + " 'changelog-mode' = 'I,UA,D')",
+                        id)
+                .close();
+        sEnv.executeSql("INSERT INTO T SELECT * FROM input").await();
+
+        assertThat(sql("SELECT * FROM T")).containsExactlyInAnyOrder(Row.of(2, 
21));
+    }
+
+    private static Stream<Arguments> reconstructParameters() {
+        // parameters: mergeEngine, changelogProducer
+        return Stream.of(
+                Arguments.of("deduplicate", "lookup"),
+                Arguments.of("deduplicate", "full-compaction"),
+                Arguments.of("partial-update", "lookup"),
+                Arguments.of("partial-update", "full-compaction"));
+    }
+
+    /**
+     * Same key-only-delete / no-before-update write path as {@link 
#testKeyOnlyDeletesIntegrity()},
+     * but with a changelog-producer ({@code lookup} / {@code 
full-compaction}) that materializes a
+     * changelog. Uses a partial schema (k, a, b) so partial-update exercises 
real column merging.
+     *
+     * <p>Input over key 1: {@code +I (1,1,1)} → {@code +U (1,2,1)} (no {@code 
-U}) → {@code -D (1)}
+     * (key-only, value columns null). Each phase is written in its own commit 
(multiple changes to
+     * the same key within a single commit would be merged before the producer 
runs, so the
+     * intermediate transition would not surface). Two verifications:
+     *
+     * <ul>
+     *   <li>Batch read: final state is empty (the key-only delete removed the 
row).
+     *   <li>Streaming read: even though the write was key-only / before-less, 
the producer
+     *       reconstructs a full changelog — the update surfaces as a {@code 
-U}/{@code +U} pair
+     *       carrying the before-image, and the delete surfaces as a {@code 
-D} with the full row
+     *       (all columns), not just the key.
+     * </ul>
+     */
+    @ParameterizedTest(name = "mergeEngine={0}, changelogProducer={1}")
+    @MethodSource("reconstructParameters")
+    @EnabledIf("isFlink2_1OrAbove")
+    public void testKeyOnlyDeletesReconstructChangelog(String mergeEngine, 
String changelogProducer)
+            throws Exception {
+        sql(
+                "CREATE TABLE R (k INT PRIMARY KEY NOT ENFORCED, a INT, b INT) 
WITH ("
+                        + " 'merge-engine' = '%s',"
+                        + " 'partial-update.remove-record-on-delete' = 'true',"
+                        + " 'changelog-producer' = '%s',"
+                        + " 'changelog-producer.compaction-interval' = '1s',"
+                        + " 'sink.key-only-deletes.enabled' = 'true')",
+                mergeEngine, changelogProducer);
+
+        // Open the streaming changelog read before writing so we observe the 
reconstructed stream.
+        BlockingIterator<Row, Row> iterator = 
BlockingIterator.of(streamSqlIter("SELECT * FROM R"));
+
+        // Commit 1: insert.
+        streamInto(ImmutableList.of(Row.ofKind(RowKind.INSERT, 1, 1, 1))); // 
+I (1, a=1, b=1)
+        
assertThat(iterator.collect(1)).containsExactly(Row.ofKind(RowKind.INSERT, 1, 
1, 1));
+
+        // Commit 2: update as +U only (no -U); reconstructed as a -U/+U pair 
with the before-image.
+        streamInto(ImmutableList.of(Row.ofKind(RowKind.UPDATE_AFTER, 1, 2, 
1))); // +U (1, a=2)
+        assertThat(iterator.collect(2))
+                .containsExactlyInAnyOrder(
+                        Row.ofKind(RowKind.UPDATE_BEFORE, 1, 1, 1),
+                        Row.ofKind(RowKind.UPDATE_AFTER, 1, 2, 1));
+
+        // Commit 3: key-only delete (only the primary key is populated).
+        streamInto(ImmutableList.of(Row.ofKind(RowKind.DELETE, 1, null, 
null)));
+
+        // The reconstructed delete carries the full row (all columns), not 
just the key.
+        
assertThat(iterator.collect(1)).containsExactly(Row.ofKind(RowKind.DELETE, 1, 
2, 1));
+        iterator.close();
+
+        // Batch read: the key-only delete removed the row.
+        assertThat(sql("SELECT * FROM R")).isEmpty();
+    }
+
+    /**
+     * Streams {@code rows} into table {@code R} through a bounded {@code 
values} changelog source
+     * that carries only INSERT / UPDATE_AFTER / DELETE (no UPDATE_BEFORE).
+     */
+    private void streamInto(List<Row> rows) throws Exception {
+        String id = TestValuesTableFactory.registerData(rows);
+        streamSqlIter(
+                        "CREATE TEMPORARY TABLE src_%s (k INT PRIMARY KEY NOT 
ENFORCED, a INT, b INT) WITH ("
+                                + " 'connector' = 'values',"
+                                + " 'bounded' = 'true',"
+                                + " 'data-id' = '%s',"
+                                + " 'changelog-mode' = 'I,UA,D')",
+                        id, id)
+                .close();
+        sEnv.executeSql(String.format("INSERT INTO R SELECT * FROM src_%s", 
id)).await();
+    }
+}
diff --git 
a/paimon-flink/paimon-flink1-common/src/main/java/org/apache/paimon/flink/utils/ChangelogModeUtils.java
 
b/paimon-flink/paimon-flink1-common/src/main/java/org/apache/paimon/flink/utils/ChangelogModeUtils.java
new file mode 100644
index 0000000000..4aece06d44
--- /dev/null
+++ 
b/paimon-flink/paimon-flink1-common/src/main/java/org/apache/paimon/flink/utils/ChangelogModeUtils.java
@@ -0,0 +1,30 @@
+/*
+ * 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.flink.table.connector.ChangelogMode;
+
+/** Utility methods about Flink {@link ChangelogMode} to resolve compatibility 
issues. */
+public class ChangelogModeUtils {
+
+    /** FLIP-510 key-only deletes: not available before Flink 2.1, so this is 
a no-op. */
+    public static ChangelogMode.Builder 
enableKeyOnlyDeletes(ChangelogMode.Builder builder) {
+        return builder;
+    }
+}
diff --git 
a/paimon-flink/paimon-flink2-common/src/main/java/org/apache/paimon/flink/utils/ChangelogModeUtils.java
 
b/paimon-flink/paimon-flink2-common/src/main/java/org/apache/paimon/flink/utils/ChangelogModeUtils.java
new file mode 100644
index 0000000000..7798c5b551
--- /dev/null
+++ 
b/paimon-flink/paimon-flink2-common/src/main/java/org/apache/paimon/flink/utils/ChangelogModeUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.flink.runtime.util.EnvironmentInformation;
+import org.apache.flink.table.connector.ChangelogMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Utility methods about Flink {@link ChangelogMode} to resolve compatibility 
issues. */
+public class ChangelogModeUtils {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ChangelogModeUtils.class);
+
+    public static ChangelogMode.Builder 
enableKeyOnlyDeletes(ChangelogMode.Builder builder) {
+        if (isFlink21OrAbove()) {
+            return builder.keyOnlyDeletes(true);
+        }
+        LOG.warn(
+                "'sink.key-only-deletes.enabled' requires Flink 2.1+ (current 
version: {}); "
+                        + "key-only deletes are not enabled.",
+                EnvironmentInformation.getVersion());
+        return builder;
+    }
+
+    private static boolean isFlink21OrAbove() {
+        String version = EnvironmentInformation.getVersion();
+        try {
+            String[] parts = version.split("\\.");
+            int major = Integer.parseInt(parts[0]);
+            int minor = Integer.parseInt(parts[1]);
+            return major > 2 || (major == 2 && minor >= 1);
+        } catch (RuntimeException e) {
+            LOG.warn(
+                    "Could not parse Flink version '{}'; key-only deletes are 
not enabled.",
+                    version);
+            return false;
+        }
+    }
+}

Reply via email to