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 184d68bd91 [flink] Commit format table overwrites once (#9433)
184d68bd91 is described below
commit 184d68bd91b4bd40b97b2150b20b2240cb2430b3
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Fri Aug 28 13:48:06 2026 +0800
[flink] Commit format table overwrites once (#9433)
---
.../flink/sink/FlinkFormatTableDataStreamSink.java | 36 +++++-
.../flink/sink/FlinkFormatTableSinkBase.java | 6 +
.../sink/FlinkFormatTableDataStreamSinkTest.java | 143 +++++++++++++++++++--
.../paimon/flink/source/FormatTableITCase.java | 46 +++++++
4 files changed, 214 insertions(+), 17 deletions(-)
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
index a223ad5129..029ffe5fac 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
@@ -55,10 +55,16 @@ public class FlinkFormatTableDataStreamSink {
}
public DataStreamSink<?> sinkFrom(DataStream<RowData> dataStream) {
- return dataStream.sinkTo(new FormatTableSink(table, overwrite,
staticPartitions));
+ DataStreamSink<?> sink =
+ dataStream.sinkTo(new FormatTableSink(table, overwrite,
staticPartitions));
+ if (overwrite) {
+ // Parallel overwrite commits could delete files produced by one
another.
+ sink.setParallelism(1);
+ }
+ return sink;
}
- private static class FormatTableSink implements Sink<RowData>,
LineageVertexProvider {
+ static class FormatTableSink implements Sink<RowData>,
LineageVertexProvider {
private final FormatTable table;
private final boolean overwrite;
@@ -93,14 +99,17 @@ public class FlinkFormatTableDataStreamSink {
}
/** Sink writer for format tables using Flink v2 API. */
- private static class FormatTableSinkWriter implements
SinkWriter<RowData> {
+ static class FormatTableSinkWriter implements SinkWriter<RowData> {
+ private final boolean overwrite;
+ private boolean reachedEndOfInput;
private transient BatchWriteBuilder writeBuilder;
private transient FormatTableWrite tableWrite;
private transient BatchTableCommit tableCommit;
public FormatTableSinkWriter(
FormatTable table, boolean overwrite, Map<String, String>
staticPartitions) {
+ this.overwrite = overwrite;
this.writeBuilder = table.newBatchWriteBuilder();
this.tableWrite = (FormatTableWrite) writeBuilder.newWrite();
if (overwrite) {
@@ -121,21 +130,34 @@ public class FlinkFormatTableDataStreamSink {
}
@Override
- public void flush(boolean endOfInput) {}
+ public void flush(boolean endOfInput) {
+ if (endOfInput) {
+ reachedEndOfInput = true;
+ }
+ }
@Override
public void close() throws Exception {
if (tableWrite != null) {
List<CommitMessage> commitMessages = null;
+ boolean shouldCommit = false;
try {
// Prepare commit and commit the data
commitMessages = tableWrite.prepareCommit();
- if (!commitMessages.isEmpty()) {
+ // A normally completed overwrite replaces its target
even with no rows.
+ shouldCommit =
+ !commitMessages.isEmpty() || (overwrite &&
reachedEndOfInput);
+ if (shouldCommit) {
tableCommit.commit(commitMessages);
}
} catch (Exception e) {
- if (commitMessages != null &&
!commitMessages.isEmpty()) {
- tableCommit.abort(commitMessages);
+ if (commitMessages != null && shouldCommit) {
+ try {
+ tableCommit.abort(commitMessages);
+ } catch (Exception abortFailure) {
+ // Report the commit failure, not the cleanup
that followed it.
+ e.addSuppressed(abortFailure);
+ }
}
throw new RuntimeException(e);
} finally {
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableSinkBase.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableSinkBase.java
index 7add7f64c1..749cfbbdf8 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableSinkBase.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableSinkBase.java
@@ -57,6 +57,12 @@ public abstract class FlinkFormatTableSinkBase
@Override
public SinkRuntimeProvider getSinkRuntimeProvider(Context context) {
+ if (overwrite && !context.isBounded()) {
+ // An overwrite replaces its target once the input ends, which an
unbounded input
+ // never does.
+ throw new UnsupportedOperationException(
+ "Paimon doesn't support streaming INSERT OVERWRITE.");
+ }
return new PaimonDataStreamSinkProvider(
(dataStream) ->
new FlinkFormatTableDataStreamSink(table, overwrite,
staticPartitions)
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSinkTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSinkTest.java
index 9a838bea68..15fb041159 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSinkTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSinkTest.java
@@ -24,25 +24,136 @@ import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.local.LocalFileIO;
import org.apache.paimon.options.Options;
import org.apache.paimon.table.FormatTable;
+import org.apache.paimon.table.format.FormatTableWrite;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
import org.apache.paimon.types.IntType;
import org.apache.paimon.types.RowType;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.lineage.LineageVertex;
import org.apache.flink.streaming.api.lineage.LineageVertexProvider;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
-import java.lang.reflect.Constructor;
import java.util.Collections;
-import java.util.Map;
+import static org.apache.paimon.flink.LogicalTypeConversion.toLogicalType;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
/** Tests for {@link FlinkFormatTableDataStreamSink}. */
class FlinkFormatTableDataStreamSinkTest {
@TempDir java.nio.file.Path temp;
+ @Test
+ void testOverwriteUsesOneSinkWriterWhileAppendKeepsParallelism() {
+ int parallelism = 4;
+ StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
+ env.setParallelism(parallelism);
+ RowType rowType = RowType.of(new IntType());
+ DataStream<RowData> input =
+ env.fromCollection(
+ Collections.singletonList((RowData)
GenericRowData.of(1)),
+ InternalTypeInfo.of(toLogicalType(rowType)));
+ FormatTable table = mock(FormatTable.class);
+ when(table.options()).thenReturn(Collections.singletonMap("path",
temp.toUri().toString()));
+ when(table.partitionKeys()).thenReturn(Collections.emptyList());
+ when(table.primaryKeys()).thenReturn(Collections.emptyList());
+ when(table.fullName()).thenReturn("test_db.test_table");
+ when(table.rowType()).thenReturn(rowType);
+
+ DataStreamSink<?> overwriteSink =
+ new FlinkFormatTableDataStreamSink(table, true,
Collections.emptyMap())
+ .sinkFrom(input);
+ DataStreamSink<?> appendSink =
+ new FlinkFormatTableDataStreamSink(table, false,
Collections.emptyMap())
+ .sinkFrom(input);
+
+ assertThat(overwriteSink.getTransformation().getParallelism()).isOne();
+ // Without this the adaptive batch scheduler is free to pick the
parallelism back up.
+
assertThat(overwriteSink.getTransformation().isParallelismConfigured()).isTrue();
+
assertThat(appendSink.getTransformation().getParallelism()).isEqualTo(parallelism);
+ }
+
+ @Test
+ void testEmptyMessagesAreCommittedOnlyForOverwrite() throws Exception {
+ FormatTableWrite overwriteWrite = mock(FormatTableWrite.class);
+ BatchTableCommit overwriteCommit = mock(BatchTableCommit.class);
+
when(overwriteWrite.prepareCommit()).thenReturn(Collections.emptyList());
+
+ SinkWriter<?> overwriteWriter = createWriter(true, overwriteWrite,
overwriteCommit);
+ overwriteWriter.flush(true);
+ overwriteWriter.close();
+
+ verify(overwriteCommit).commit(Collections.emptyList());
+
+ FormatTableWrite appendWrite = mock(FormatTableWrite.class);
+ BatchTableCommit appendCommit = mock(BatchTableCommit.class);
+ when(appendWrite.prepareCommit()).thenReturn(Collections.emptyList());
+
+ SinkWriter<?> appendWriter = createWriter(false, appendWrite,
appendCommit);
+ appendWriter.flush(true);
+ appendWriter.close();
+
+ verify(appendCommit, never()).commit(anyList());
+ }
+
+ @Test
+ void testEmptyOverwriteIsNotCommittedBeforeEndOfInput() throws Exception {
+ FormatTableWrite tableWrite = mock(FormatTableWrite.class);
+ BatchTableCommit tableCommit = mock(BatchTableCommit.class);
+ when(tableWrite.prepareCommit()).thenReturn(Collections.emptyList());
+
+ SinkWriter<?> writer = createWriter(true, tableWrite, tableCommit);
+ // A checkpoint is not the end of the input, and a job that fails
after one must not have
+ // replaced the target.
+ writer.flush(false);
+ writer.close();
+
+ verify(tableCommit, never()).commit(anyList());
+ verify(tableCommit, never()).abort(anyList());
+ }
+
+ @Test
+ void testFailedEmptyOverwriteCommitIsAborted() throws Exception {
+ FormatTableWrite tableWrite = mock(FormatTableWrite.class);
+ BatchTableCommit tableCommit = mock(BatchTableCommit.class);
+ when(tableWrite.prepareCommit()).thenReturn(Collections.emptyList());
+ doThrow(new RuntimeException("commit failed"))
+ .when(tableCommit)
+ .commit(Collections.emptyList());
+ doThrow(new RuntimeException("abort failed"))
+ .when(tableCommit)
+ .abort(Collections.emptyList());
+ SinkWriter<?> writer = createWriter(true, tableWrite, tableCommit);
+ writer.flush(true);
+
+ assertThatThrownBy(writer::close)
+ .isInstanceOf(RuntimeException.class)
+ .hasRootCauseMessage("commit failed")
+ .rootCause()
+ .satisfies(
+ cause ->
+ assertThat(cause.getSuppressed())
+ .extracting(Throwable::getMessage)
+ .containsExactly("abort failed"));
+ verify(tableCommit).abort(Collections.emptyList());
+ }
+
@Test
void testFormatTableSinkLineageVertex() throws Exception {
FormatTable table =
@@ -57,17 +168,29 @@ class FlinkFormatTableDataStreamSinkTest {
.catalogContext(CatalogContext.create(new Options()))
.build();
- Class<?> sinkClass =
- Class.forName(
-
"org.apache.paimon.flink.sink.FlinkFormatTableDataStreamSink$FormatTableSink");
- Constructor<?> constructor =
- sinkClass.getDeclaredConstructor(FormatTable.class,
boolean.class, Map.class);
- constructor.setAccessible(true);
- Object sink = constructor.newInstance(table, false,
Collections.emptyMap());
+ FlinkFormatTableDataStreamSink.FormatTableSink sink =
+ new FlinkFormatTableDataStreamSink.FormatTableSink(
+ table, false, Collections.emptyMap());
assertThat(sink).isInstanceOf(LineageVertexProvider.class);
- LineageVertex vertex = ((LineageVertexProvider)
sink).getLineageVertex();
+ LineageVertex vertex = sink.getLineageVertex();
assertThat(vertex.datasets()).hasSize(1);
assertThat(vertex.datasets().get(0).name()).isEqualTo("paimon." +
table.fullName());
}
+
+ private SinkWriter<?> createWriter(
+ boolean overwrite, FormatTableWrite tableWrite, BatchTableCommit
tableCommit)
+ throws Exception {
+ FormatTable table = mock(FormatTable.class);
+ BatchWriteBuilder writeBuilder = mock(BatchWriteBuilder.class);
+ when(table.newBatchWriteBuilder()).thenReturn(writeBuilder);
+ when(writeBuilder.newWrite()).thenReturn(tableWrite);
+ when(writeBuilder.newCommit()).thenReturn(tableCommit);
+ if (overwrite) {
+
when(writeBuilder.withOverwrite(Collections.emptyMap())).thenReturn(writeBuilder);
+ }
+
+ return new
FlinkFormatTableDataStreamSink.FormatTableSink.FormatTableSinkWriter(
+ table, overwrite, Collections.emptyMap());
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FormatTableITCase.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FormatTableITCase.java
index 63da32b3fe..ba8dc66aea 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FormatTableITCase.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FormatTableITCase.java
@@ -133,6 +133,52 @@ public class FormatTableITCase extends
RESTCatalogITCaseBase {
sql("Drop TABLE %s", tableName);
}
+ @Test
+ public void testInsertOverwriteWithoutRowsReplacesItsTarget() {
+ String tableName = "format_table_empty_overwrite";
+ sql(
+ "CREATE TABLE %s (a INT, b INT) WITH ('file.format'='parquet',
'type'='format-table')",
+ tableName);
+ setDataToken(tableName);
+
+ sql("INSERT INTO %s VALUES (1, 11), (2, 22)", tableName);
+ assertThat(sql("SELECT * FROM %s", tableName))
+ .containsExactlyInAnyOrder(Row.of(1, 11), Row.of(2, 22));
+
+ sql("INSERT OVERWRITE %s SELECT a, b FROM %s WHERE a < 0", tableName,
tableName);
+ assertThat(sql("SELECT * FROM %s", tableName)).isEmpty();
+
+ sql("Drop TABLE %s", tableName);
+ }
+
+ @Test
+ public void testInsertOverwriteWithoutRowsEmptiesOnlyTheNamedPartition() {
+ String tableName = "format_table_empty_overwrite_partitioned";
+ sql(
+ "CREATE TABLE %s (a INT, b INT, c INT) PARTITIONED BY (c) WITH
('file.format'='parquet', 'type'='format-table')",
+ tableName);
+ setDataToken(tableName);
+
+ sql("INSERT INTO %s PARTITION (c = 1) VALUES (1, 11)", tableName);
+ sql("INSERT INTO %s PARTITION (c = 2) VALUES (2, 22)", tableName);
+ assertThat(sql("SELECT a, b, c FROM %s", tableName))
+ .containsExactlyInAnyOrder(Row.of(1, 11, 1), Row.of(2, 22, 2));
+
+ sql(
+ "INSERT OVERWRITE %s PARTITION (c = 1) SELECT a, b FROM %s
WHERE a < 0",
+ tableName, tableName);
+ assertThat(sql("SELECT a, b, c FROM %s", tableName))
+ .containsExactlyInAnyOrder(Row.of(2, 22, 2));
+
+ // Without a PARTITION clause the statement replaces the partitions it
wrote, and it wrote
+ // none, so both engines leave the table alone.
+ sql("INSERT OVERWRITE %s SELECT a, b, c FROM %s WHERE a < 0",
tableName, tableName);
+ assertThat(sql("SELECT a, b, c FROM %s", tableName))
+ .containsExactlyInAnyOrder(Row.of(2, 22, 2));
+
+ sql("Drop TABLE %s", tableName);
+ }
+
@Test
public void testTruncateTable() {
String tableName = "format_table_truncate";