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 5dacde516b [core][spark] Fix format table INSERT OVERWRITE ignoring
dynamic-partition-overwrite (#9353)
5dacde516b is described below
commit 5dacde516bf6e29b583aff8b32bab321bfc6e64f
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Sat Aug 22 17:32:37 2026 +0800
[core][spark] Fix format table INSERT OVERWRITE ignoring
dynamic-partition-overwrite (#9353)
---
docs/docs/spark/sql-write.md | 8 ++
.../table/format/FormatBatchWriteBuilder.java | 3 +-
.../paimon/table/format/FormatTableCommit.java | 113 +++++++++++++-----
.../format/FormatTableCommitStatisticsTest.java | 87 +++++++++++++-
.../paimon/table/format/FormatTableCommitTest.java | 131 +++++++++++++++++++--
.../spark/format/FormatTableBatchWrite.scala | 3 +-
.../apache/spark/sql/paimon/shims/Spark4Shim.scala | 3 +-
.../spark/format/FormatTableBatchWriteBase.scala | 11 +-
.../paimon/spark/format/PaimonFormatTable.scala | 30 +++--
.../apache/spark/sql/paimon/shims/SparkShim.scala | 1 -
.../paimon/spark/sql/FormatTableTestBase.scala | 48 ++++++++
.../paimon/spark/table/PaimonFormatTableTest.scala | 12 +-
.../spark/format/FormatTableBatchWrite.scala | 3 +-
.../apache/spark/sql/paimon/shims/Spark3Shim.scala | 3 +-
.../spark/format/FormatTableBatchWrite.scala | 3 +-
.../apache/spark/sql/paimon/shims/Spark4Shim.scala | 3 +-
16 files changed, 394 insertions(+), 68 deletions(-)
diff --git a/docs/docs/spark/sql-write.md b/docs/docs/spark/sql-write.md
index 5ac34f0834..8c0f8f8531 100644
--- a/docs/docs/spark/sql-write.md
+++ b/docs/docs/spark/sql-write.md
@@ -123,6 +123,14 @@ SELECT * FROM my_table;
*/
```
+A Format Table read through Paimon (`format-table.implementation = paimon`,
the default) follows
+the same rule. An `INSERT OVERWRITE` that names no partition replaces the
whole table, so a
+partition the query does not write is replaced too, and a query that returns
no rows leaves the
+table empty; `dynamic` mode replaces only the partitions written, and writing
nothing then replaces
+nothing. With `metastore.partitioned-table = true` the catalog is the answer
to which partitions
+the table has, so overwriting the whole table empties those and leaves a
directory still waiting
+for `MSCK REPAIR TABLE` alone.
+
## Truncate Table
The `TRUNCATE TABLE` statement removes all the rows from a table or
partition(s).
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatBatchWriteBuilder.java
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatBatchWriteBuilder.java
index b14ab00890..73c11d7741 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatBatchWriteBuilder.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatBatchWriteBuilder.java
@@ -89,7 +89,8 @@ public class FormatBatchWriteBuilder implements
BatchWriteBuilder {
staticPartition,
syncHiveUri,
table.catalogContext(),
- table.partitionManager());
+ table.partitionManager(),
+ options.dynamicPartitionOverwrite());
}
@Override
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableCommit.java
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableCommit.java
index aec5f73e15..be2105930a 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableCommit.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableCommit.java
@@ -74,6 +74,7 @@ public class FormatTableCommit implements BatchTableCommit {
private Catalog hiveCatalog;
private Identifier tableIdentifier;
@Nullable private final FormatTablePartitionManager partitionManager;
+ private final boolean dynamicPartitionOverwrite;
public FormatTableCommit(
String location,
@@ -86,7 +87,8 @@ public class FormatTableCommit implements BatchTableCommit {
@Nullable Map<String, String> staticPartitions,
@Nullable String syncHiveUri,
CatalogContext catalogContext,
- @Nullable FormatTablePartitionManager partitionManager) {
+ @Nullable FormatTablePartitionManager partitionManager,
+ boolean dynamicPartitionOverwrite) {
this.location = location;
this.fileIO = fileIO;
this.formatTablePartitionOnlyValueInPath =
formatTablePartitionOnlyValueInPath;
@@ -97,6 +99,7 @@ public class FormatTableCommit implements BatchTableCommit {
this.partitionKeys = partitionKeys;
this.tableIdentifier = tableIdentifier;
this.partitionManager = partitionManager;
+ this.dynamicPartitionOverwrite = dynamicPartitionOverwrite;
if (syncHiveUri != null) {
try {
Options options = new Options();
@@ -155,15 +158,25 @@ public class FormatTableCommit implements
BatchTableCommit {
fileIO.mkdirs(partitionPath);
}
} else if (overwrite) {
- Set<Path> partitionPaths = new HashSet<>();
- for (TwoPhaseCommitMessage message : messages) {
-
partitionPaths.add(message.getCommitter().targetPath().getParent());
- }
- for (Path p : partitionPaths) {
- // The parent of a written file is a complete partition
directory - the table
- // directory itself when the table is unpartitioned - so
there is no partition
- // level below it to descend, and it is a partition this
commit writes anyway.
- deletePreviousDataFile(p, 0);
+ if (replacesOnlyWrittenPartitions()) {
+ Set<Path> partitionPaths = new HashSet<>();
+ for (TwoPhaseCommitMessage message : messages) {
+
partitionPaths.add(message.getCommitter().targetPath().getParent());
+ }
+ for (Path p : partitionPaths) {
+ // The parent of a written file is a complete
partition directory - the
+ // table directory itself when the table is
unpartitioned - so there is no
+ // partition level below it to descend, and it is a
partition this commit
+ // writes anyway.
+ deletePreviousDataFile(p, 0);
+ }
+ } else {
+ // Overwriting without naming a partition replaces the
table, so what has to go
+ // is everything the table holds rather than the files
this commit happens to
+ // write: a statement whose query returns nothing still
empties the table.
+ for (Path dataDirectory : tableDataDirectories()) {
+
clearedPartitionPaths.addAll(deletePreviousDataFile(dataDirectory, 0));
+ }
}
}
@@ -238,11 +251,12 @@ public class FormatTableCommit implements
BatchTableCommit {
}
/**
- * Registers the partitions this commit touched, carrying the statistics
of what it wrote. A
- * static prefix overwrite also empties partitions it writes nothing to;
those report an exact
- * zero and are registered with the rest, since a statistic can only be
reported for a partition
- * its own request registers. A truncation writes nothing and reports
every partition it
- * emptied.
+ * Registers the partitions this commit touched, carrying the statistics
of what it wrote. An
+ * overwrite also empties partitions it writes nothing to - those below a
static prefix, and
+ * every partition the table has when the statement names none and dynamic
partition overwrite
+ * is off; those report an exact zero and are registered with the rest,
since a statistic can
+ * only be reported for a partition its own request registers. A
truncation writes nothing and
+ * reports every partition it emptied.
*/
private void reportPartitions(
Set<Map<String, String>> writtenPartitionSpecs,
@@ -433,8 +447,62 @@ public class FormatTableCommit implements BatchTableCommit
{
public void close() throws Exception {}
/**
- * Deletes the data files below a path and returns the directories they
sat in, which for a
- * static prefix overwrite can be partitions this commit never writes.
+ * Whether an overwrite that names no partition replaces only the
partitions this commit wrote
+ * rather than everything the table holds. Same condition a data table
commit applies: the
+ * option is about which partitions to replace, so an unpartitioned table
has nothing for it to
+ * select.
+ */
+ private boolean replacesOnlyWrittenPartitions() {
+ return partitionKeys != null && !partitionKeys.isEmpty() &&
dynamicPartitionOverwrite;
+ }
+
+ /**
+ * The directories this table's data sits in: the table directory itself
when the table is
+ * unpartitioned, and one per partition otherwise, taken from wherever the
table reads its
+ * partitions. A directory no scan of the table reads holds none of its
data - one the catalog
+ * has not registered, or one whose name does not parse into the partition
keys - and replacing
+ * what the table holds leaves it alone, the way {@link #truncateTable()}
does.
+ */
+ private List<Path> tableDataDirectories() {
+ if (partitionKeys == null || partitionKeys.isEmpty()) {
+ return Collections.singletonList(new Path(location));
+ }
+ List<Path> directories = new ArrayList<>();
+ if (partitionManager != null) {
+ for (Map<String, String> spec :
registeredPartitions(Collections.emptyMap())) {
+ directories.add(
+ buildPartitionPath(
+ location,
+ spec,
+ formatTablePartitionOnlyValueInPath,
+ partitionKeys));
+ }
+ return directories;
+ }
+ for (Pair<LinkedHashMap<String, String>, Path> partition :
partitionsInTheFileSystem()) {
+ directories.add(partition.getRight());
+ }
+ return directories;
+ }
+
+ /** The partition directories a table that discovers its partitions from
the files has. */
+ private List<Pair<LinkedHashMap<String, String>, Path>>
partitionsInTheFileSystem() {
+ return PartitionPathUtils.searchPartSpecAndPaths(
+ fileIO,
+ new Path(location),
+ partitionKeys.size(),
+ partitionKeys,
+ formatTablePartitionOnlyValueInPath,
+ null,
+ null,
+ defaultPartName);
+ }
+
+ /**
+ * Deletes the data files below a path and returns the directories they
sat in. Those can be
+ * partitions this commit never writes: a static prefix overwrite clears
the partitions sitting
+ * below the prefix, and an overwrite that names no partition clears every
partition the table
+ * has.
*/
private Set<Path> deletePreviousDataFile(Path partitionPath, int
partitionLevels)
throws IOException {
@@ -498,16 +566,7 @@ public class FormatTableCommit implements BatchTableCommit
{
// Filesystem partition discovery: the partition directories the scan
reads are the table.
// A directory that does not parse into the partition keys is not one
of them, so
// truncating leaves it alone.
- for (Pair<LinkedHashMap<String, String>, Path> partition :
- PartitionPathUtils.searchPartSpecAndPaths(
- fileIO,
- new Path(location),
- partitionKeys.size(),
- partitionKeys,
- formatTablePartitionOnlyValueInPath,
- null,
- null,
- defaultPartName)) {
+ for (Pair<LinkedHashMap<String, String>, Path> partition :
partitionsInTheFileSystem()) {
try {
deletePreviousDataFile(partition.getRight(), 0);
} catch (IOException e) {
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitStatisticsTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitStatisticsTest.java
index 199b7b446b..f5ac146070 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitStatisticsTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitStatisticsTest.java
@@ -223,6 +223,66 @@ class FormatTableCommitStatisticsTest {
});
}
+ @Test
+ void testOverwritingTheWholeTableZeroesAPartitionItDidNotRewrite() throws
Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path tablePath = new Path(tempDir.toUri());
+ FormatTablePartitionManager partitionManager =
mock(FormatTablePartitionManager.class);
+ registered(partitionManager, spec("2025", "10"), spec("2025", "11"));
+ writeDataFile(fileIO, tablePath, "year=2025/month=10", "old-data.csv",
4096);
+ writeDataFile(fileIO, tablePath, "year=2025/month=11", "old-data.csv",
2048);
+ // The statement names no partition, and its query wrote only one of
the two.
+ CommitMessage message = writtenFile(fileIO, tablePath,
"year=2025/month=10", 3, 128);
+
+ overwritingTheWholeTable(tablePath, fileIO, partitionManager)
+ .commit(Collections.singletonList(message));
+
+ Reported reported = capture(partitionManager);
+ assertThat(reported.replaceStatistics).isTrue();
+ assertThat(reported.specs)
+ .containsExactlyInAnyOrder(spec("2025", "10"), spec("2025",
"11"));
+ assertThat(reported.statistics)
+ .anySatisfy(
+ statistics -> {
+
assertThat(statistics.spec()).isEqualTo(spec("2025", "10"));
+ assertThat(statistics.recordCount()).isEqualTo(3);
+
assertThat(statistics.fileSizeInBytes()).isEqualTo(128);
+ assertThat(statistics.fileCount()).isEqualTo(1);
+ })
+ // Emptied and not written to: the whole table was replaced,
so an exact zero
+ // rather than the numbers of the files this commit deleted.
+ .anySatisfy(
+ statistics -> {
+
assertThat(statistics.spec()).isEqualTo(spec("2025", "11"));
+ assertThat(statistics.recordCount()).isZero();
+ assertThat(statistics.fileSizeInBytes()).isZero();
+ assertThat(statistics.fileCount()).isZero();
+ });
+ }
+
+ @Test
+ void
testOverwritingTheWholeTableLeavesADirectoryTheCatalogHasNotRegistered() throws
Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path tablePath = new Path(tempDir.toUri());
+ FormatTablePartitionManager partitionManager =
mock(FormatTablePartitionManager.class);
+ registered(partitionManager, spec("2025", "10"));
+ writeDataFile(fileIO, tablePath, "year=2025/month=10", "data.csv",
4096);
+ // A directory MSCK REPAIR TABLE has not registered yet: no scan of
this table reads it,
+ // so replacing what the table holds is not this directory's business
either.
+ writeDataFile(fileIO, tablePath, "year=2025/month=11",
"unregistered.csv", 2048);
+
+ overwritingTheWholeTable(tablePath, fileIO, partitionManager)
+ .commit(Collections.emptyList());
+
+ assertThat(fileIO.exists(new Path(tablePath,
"year=2025/month=10/data.csv"))).isFalse();
+ assertThat(fileIO.exists(new Path(tablePath,
"year=2025/month=11/unregistered.csv")))
+ .isTrue();
+ // Nor does the overwrite register it: reporting a zero for it would
make a partition the
+ // catalog never had, out of a directory that still holds rows.
+ Reported reported = capture(partitionManager);
+ assertThat(reported.specs).containsExactly(spec("2025", "10"));
+ }
+
@Test
void testTruncatingPartitionsReportsAnExactZeroAsTheTotal() throws
Exception {
LocalFileIO fileIO = LocalFileIO.create();
@@ -584,6 +644,24 @@ class FormatTableCommitStatisticsTest {
boolean overwrite,
Map<String, String> staticPartitions,
boolean onlyValueInPath) {
+ return commit(
+ tablePath,
+ fileIO,
+ partitionManager,
+ overwrite,
+ staticPartitions,
+ onlyValueInPath,
+ /* dynamicPartitionOverwrite */ true);
+ }
+
+ private FormatTableCommit commit(
+ Path tablePath,
+ FileIO fileIO,
+ FormatTablePartitionManager partitionManager,
+ boolean overwrite,
+ Map<String, String> staticPartitions,
+ boolean onlyValueInPath,
+ boolean dynamicPartitionOverwrite) {
return new FormatTableCommit(
tablePath.toString(),
PARTITION_KEYS,
@@ -595,7 +673,14 @@ class FormatTableCommitStatisticsTest {
staticPartitions,
null,
null,
- partitionManager);
+ partitionManager,
+ dynamicPartitionOverwrite);
+ }
+
+ /** An overwrite that names no partition: INSERT OVERWRITE without a
PARTITION clause. */
+ private FormatTableCommit overwritingTheWholeTable(
+ Path tablePath, FileIO fileIO, FormatTablePartitionManager
partitionManager) {
+ return commit(tablePath, fileIO, partitionManager, true, null, false,
false);
}
/** A file this commit wrote, with the counts its writer took. */
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitTest.java
index db12129814..75b141d947 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableCommitTest.java
@@ -85,7 +85,8 @@ class FormatTableCommitTest {
null,
null,
null,
- partitionManager);
+ partitionManager,
+ /* dynamicPartitionOverwrite */ true);
CommitMessage message = new TwoPhaseCommitMessage(committer);
assertThatThrownBy(() ->
commit.commit(Collections.singletonList(message)))
@@ -117,7 +118,8 @@ class FormatTableCommitTest {
null,
null,
null,
- partitionManager);
+ partitionManager,
+ /* dynamicPartitionOverwrite */ true);
CommitMessage message = new TwoPhaseCommitMessage(committer);
assertThatThrownBy(() ->
commit.commit(Collections.singletonList(message)))
@@ -212,7 +214,8 @@ class FormatTableCommitTest {
staticPartition,
null,
null,
- null);
+ null,
+ /* dynamicPartitionOverwrite */ true);
commit.commit(Collections.singletonList(new
TwoPhaseCommitMessage(committer)));
@@ -260,7 +263,8 @@ class FormatTableCommitTest {
Collections.singletonMap("year", "2025"),
null,
null,
- null);
+ null,
+ /* dynamicPartitionOverwrite */ true);
commit.commit(Collections.emptyList());
@@ -303,7 +307,8 @@ class FormatTableCommitTest {
Collections.singletonMap("year", "2025"),
null,
null,
- null);
+ null,
+ /* dynamicPartitionOverwrite */ true);
commit.commit(Collections.emptyList());
@@ -350,7 +355,8 @@ class FormatTableCommitTest {
staticPartition,
null,
null,
- null);
+ null,
+ /* dynamicPartitionOverwrite */ true);
assertThatThrownBy(() -> commit.commit(Collections.emptyList()))
.isInstanceOf(RuntimeException.class)
@@ -542,6 +548,113 @@ class FormatTableCommitTest {
assertThat(fileIO.exists(defaultPartition)).isTrue();
}
+ @Test
+ void testOverwritingWithoutAStaticPartitionEmptiesAnUnpartitionedTable()
throws Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path tablePath = new Path(tempDir.toUri());
+ Path previousData = new Path(tablePath, "data-1.csv");
+ fileIO.writeFile(previousData, "1", false);
+ // Another writer is mid-write; its staging tree is not table data.
+ Path stagingFile = new Path(tablePath,
"_temporary/attempt/part-00000.csv");
+ fileIO.writeFile(stagingFile, "2", false);
+ FormatTableCommit commit = overwritingCommit(tablePath, fileIO, true);
+
+ // Nothing to write: the query behind the statement returned no rows.
+ commit.commit(Collections.emptyList());
+
+ // An unpartitioned overwrite is about the table, so the files it
replaces are the table's,
+ // not the ones this commit happens to write.
+ assertThat(fileIO.exists(previousData)).isFalse();
+ assertThat(fileIO.exists(stagingFile)).isTrue();
+ }
+
+ @Test
+ void testOverwritingAPartitionedTableFollowsDynamicPartitionOverwrite()
throws Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path dynamicTable = new Path(new Path(tempDir.toUri()), "dynamic");
+ Path staticTable = new Path(new Path(tempDir.toUri()), "static");
+ for (Path table : Arrays.asList(dynamicTable, staticTable)) {
+ fileIO.writeFile(new Path(table, "year=2025/month=10/data-1.csv"),
"1", false);
+ fileIO.writeFile(new Path(table, "year=2025/month=11/data-2.csv"),
"2", false);
+ }
+
+ overwritingCommit(dynamicTable, fileIO, true, "year", "month")
+ .commit(Collections.emptyList());
+ overwritingCommit(staticTable, fileIO, false, "year", "month")
+ .commit(Collections.emptyList());
+
+ // Dynamic overwrite selects the partitions written, and this commit
wrote none.
+ assertThat(fileIO.exists(new Path(dynamicTable,
"year=2025/month=10/data-1.csv"))).isTrue();
+ assertThat(fileIO.exists(new Path(dynamicTable,
"year=2025/month=11/data-2.csv"))).isTrue();
+ // With it off, the statement is about the whole table, whatever the
query returned.
+ assertThat(fileIO.exists(new Path(staticTable,
"year=2025/month=10/data-1.csv"))).isFalse();
+ assertThat(fileIO.exists(new Path(staticTable,
"year=2025/month=11/data-2.csv"))).isFalse();
+ }
+
+ @Test
+ void testDynamicOverwriteReplacesOnlyThePartitionsItWrites() throws
Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path tablePath = new Path(tempDir.toUri());
+ Path rewritten = new Path(tablePath, "year=2025/month=10/data-1.csv");
+ Path untouched = new Path(tablePath, "year=2025/month=11/data-2.csv");
+ fileIO.writeFile(rewritten, "1", false);
+ fileIO.writeFile(untouched, "2", false);
+ RenamingTwoPhaseOutputStream outputStream =
+ new RenamingTwoPhaseOutputStream(
+ fileIO, new Path(tablePath,
"year=2025/month=10/data-new.csv"), false);
+ outputStream.write(1);
+ TwoPhaseOutputStream.Committer committer =
outputStream.closeForCommit();
+
+ overwritingCommit(tablePath, fileIO, true, "year", "month")
+ .commit(Collections.singletonList(new
TwoPhaseCommitMessage(committer)));
+
+ // The partitions a dynamic overwrite replaces are the ones it writes,
and only those.
+ assertThat(fileIO.exists(rewritten)).isFalse();
+ assertThat(fileIO.exists(new Path(tablePath,
"year=2025/month=10/data-new.csv"))).isTrue();
+ assertThat(fileIO.exists(untouched)).isTrue();
+ }
+
+ @Test
+ void testOverwritingTheWholeTableLeavesADirectoryThatIsNoPartitionOfIt()
throws Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path tablePath = new Path(tempDir.toUri());
+ fileIO.writeFile(new Path(tablePath, "year=2025/month=10/data-1.csv"),
"1", false);
+ // Neither of these parses into the partition keys, so no scan of the
table reads them:
+ // replacing what the table holds is none of their business.
+ fileIO.writeFile(new Path(tablePath, "year=2025/nomonth/data-2.csv"),
"2", false);
+ fileIO.writeFile(new Path(tablePath, "loose.csv"), "3", false);
+
+ overwritingCommit(tablePath, fileIO, false, "year", "month")
+ .commit(Collections.emptyList());
+
+ assertThat(fileIO.exists(new Path(tablePath,
"year=2025/month=10/data-1.csv"))).isFalse();
+ assertThat(fileIO.exists(new Path(tablePath,
"year=2025/nomonth/data-2.csv"))).isTrue();
+ assertThat(fileIO.exists(new Path(tablePath, "loose.csv"))).isTrue();
+ }
+
+ /**
+ * An overwrite that names no partition: what INSERT OVERWRITE without a
PARTITION clause is.
+ */
+ private FormatTableCommit overwritingCommit(
+ Path tableLocation,
+ LocalFileIO fileIO,
+ boolean dynamicPartitionOverwrite,
+ String... partitionKeys) {
+ return new FormatTableCommit(
+ tableLocation.toString(),
+ Arrays.asList(partitionKeys),
+ fileIO,
+ false,
+ PARTITION_DEFAULT_NAME.defaultValue(),
+ true,
+ Identifier.create("overwrite_db", "overwrite_table"),
+ null,
+ null,
+ null,
+ null,
+ dynamicPartitionOverwrite);
+ }
+
private static Map<String, String> partitionSpec(String year, String
month) {
LinkedHashMap<String, String> spec = new LinkedHashMap<>();
spec.put("year", year);
@@ -567,7 +680,8 @@ class FormatTableCommitTest {
null,
null,
null,
- partitionManager);
+ partitionManager,
+ /* dynamicPartitionOverwrite */ true);
}
private FormatTablePartitionManager commitPartitionedFile(
@@ -591,7 +705,8 @@ class FormatTableCommitTest {
null,
null,
null,
- partitionManager);
+ partitionManager,
+ /* dynamicPartitionOverwrite */ true);
commit.commit(Collections.singletonList(new
TwoPhaseCommitMessage(committer)));
return partitionManager;
}
diff --git
a/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
b/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
index c05349c504..430dbb4b2c 100644
---
a/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
+++
b/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
@@ -31,10 +31,9 @@ import org.apache.spark.sql.types.StructType
*/
class FormatTableBatchWrite(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType)
- extends FormatTableBatchWriteBase(table, overwriteDynamic,
overwritePartitions, writeSchema)
+ extends FormatTableBatchWriteBase(table, overwritePartitions, writeSchema)
with BatchWrite
with Serializable {
diff --git
a/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
b/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
index 12dddf4db5..8cdc6e453d 100644
---
a/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
+++
b/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
@@ -232,10 +232,9 @@ class Spark4Shim extends SparkShim {
override def createFormatTableBatchWrite(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType): BatchWrite =
- new FormatTableBatchWrite(table, overwriteDynamic, overwritePartitions,
writeSchema)
+ new FormatTableBatchWrite(table, overwritePartitions, writeSchema)
override def createCTERelationRef(
cteId: Long,
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWriteBase.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWriteBase.scala
index 853c7f4146..c7ab592a1b 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWriteBase.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWriteBase.scala
@@ -43,7 +43,6 @@ import scala.collection.JavaConverters._
*/
abstract class FormatTableBatchWriteBase(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType)
extends Logging
@@ -51,12 +50,10 @@ abstract class FormatTableBatchWriteBase(
protected val batchWriteBuilder: BatchWriteBuilder = {
val builder = table.newBatchWriteBuilder()
- // todo: add test for static overwrite the whole table
- if (overwriteDynamic.contains(true)) {
- builder.withOverwrite()
- } else {
- overwritePartitions.foreach(partitions =>
builder.withOverwrite(partitions.asJava))
- }
+ // Which partitions an overwrite that names none replaces is the table's
+ // `dynamic-partition-overwrite` to answer, and the builder carries the
mode Spark resolved
+ // in that option, so an empty spec here means the statement, not the mode.
+ overwritePartitions.foreach(partitions =>
builder.withOverwrite(partitions.asJava))
builder
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/PaimonFormatTable.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/PaimonFormatTable.scala
index fea982207d..baa7d21e40 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/PaimonFormatTable.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/format/PaimonFormatTable.scala
@@ -367,20 +367,30 @@ case class PaimonFormatTableWriterBuilder(table:
FormatTable, writeSchema: Struc
override def partitionRowType(): RowType = table.partitionType
- override def build: Write = new Write with RequiresDistributionAndOrdering {
- private val writeRequirement = PaimonWriteRequirement(table)
+ override def build: Write = {
+ // Which partitions an overwrite replaces is the table option's call, the
same as for a data
+ // table. Carrying the mode Spark resolved into that option is what keeps
a `STATIC` overwrite
+ // from being served as if it were `DYNAMIC`.
+ val writeTable = overwriteDynamic match {
+ case Some(dynamic) =>
+ table.copy(Map(CoreOptions.DYNAMIC_PARTITION_OVERWRITE.key ->
dynamic.toString).asJava)
+ case None => table
+ }
+ new Write with RequiresDistributionAndOrdering {
+ private val writeRequirement = PaimonWriteRequirement(writeTable)
- override def requiredDistribution(): Distribution =
writeRequirement.distribution
+ override def requiredDistribution(): Distribution =
writeRequirement.distribution
- override def requiredOrdering(): Array[SortOrder] =
writeRequirement.ordering
+ override def requiredOrdering(): Array[SortOrder] =
writeRequirement.ordering
- override def toBatch: BatchWrite = {
- SparkShimLoader.shim
- .createFormatTableBatchWrite(table, overwriteDynamic,
overwritePartitions, writeSchema)
- }
+ override def toBatch: BatchWrite = {
+ SparkShimLoader.shim
+ .createFormatTableBatchWrite(writeTable, overwritePartitions,
writeSchema)
+ }
- override def toStreaming: StreamingWrite = {
- throw new UnsupportedOperationException("FormatTable doesn't support
streaming write")
+ override def toStreaming: StreamingWrite = {
+ throw new UnsupportedOperationException("FormatTable doesn't support
streaming write")
+ }
}
}
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/paimon/shims/SparkShim.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/paimon/shims/SparkShim.scala
index 690c20b85b..8d0cd71579 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/paimon/shims/SparkShim.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/paimon/shims/SparkShim.scala
@@ -151,7 +151,6 @@ trait SparkShim {
/** Same `BatchWrite` mixin problem as [[createPaimonBatchWrite]], but for
`FormatTable` writes. */
def createFormatTableBatchWrite(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType): BatchWrite
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
index dc523284bb..998c7c591c 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
@@ -576,6 +576,54 @@ abstract class FormatTableTestBase extends
PaimonHiveTestBase with AdaptiveSpark
}
}
+ test(
+ "Format table: INSERT OVERWRITE empties an unpartitioned table when the
query returns nothing") {
+ withTable("t") {
+ sql("CREATE TABLE t (id INT, payload STRING) USING CSV")
+ sql("INSERT INTO t VALUES (1, 'a'), (2, 'b')")
+
+ sql("INSERT OVERWRITE t SELECT * FROM t WHERE false")
+
+ // An unpartitioned overwrite replaces the table, and it replaces it
with nothing here. Left
+ // to the files this commit wrote, an empty query would leave the old
rows readable.
+ checkAnswer(sql("SELECT * FROM t"), Seq.empty)
+ }
+ }
+
+ test("Format table: static INSERT OVERWRITE replaces every partition of the
table") {
+ withTable("t") {
+ withSQLConf("spark.sql.sources.partitionOverwriteMode" -> "STATIC") {
+ sql("CREATE TABLE t (id INT, dt STRING) USING CSV PARTITIONED BY (dt)")
+ sql("INSERT INTO t VALUES (1, '20260101'), (2, '20260102')")
+
+ sql("INSERT OVERWRITE t VALUES (9, '20260101')")
+
+ // The statement names no partition and the mode is STATIC, so it is
about the whole
+ // table: the partition it does not write is replaced too, not left as
it was.
+ checkAnswer(sql("SELECT * FROM t"), Seq(Row(9, "20260101")))
+
+ sql("INSERT OVERWRITE t SELECT * FROM t WHERE false")
+ checkAnswer(sql("SELECT * FROM t"), Seq.empty)
+ }
+ }
+ }
+
+ test("Format table: dynamic INSERT OVERWRITE replaces only the partitions it
writes") {
+ withTable("t") {
+ withSQLConf("spark.sql.sources.partitionOverwriteMode" -> "DYNAMIC") {
+ sql("CREATE TABLE t (id INT, dt STRING) USING CSV PARTITIONED BY (dt)")
+ sql("INSERT INTO t VALUES (1, '20260101'), (2, '20260102')")
+
+ sql("INSERT OVERWRITE t VALUES (9, '20260101')")
+ checkAnswer(sql("SELECT * FROM t ORDER BY id"), Seq(Row(2,
"20260102"), Row(9, "20260101")))
+
+ // Nothing written means no partition selected, which is not the same
as selecting all.
+ sql("INSERT OVERWRITE t SELECT * FROM t WHERE false")
+ checkAnswer(sql("SELECT * FROM t ORDER BY id"), Seq(Row(2,
"20260102"), Row(9, "20260101")))
+ }
+ }
+ }
+
def collectFilteredInputSplits(plan: SparkPlan, tableName: String):
Seq[Split] = {
flatMap(plan) {
case s: BatchScanExec =>
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
index fedf8363d0..e554c72322 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
@@ -203,11 +203,21 @@ class PaimonFormatTableTest extends
PaimonSparkTestWithRestCatalogBase {
Row(1, 5, "Jerry") :: Row(1, 7, "Tom") :: Nil
)
spark.sql(s"INSERT INTO $tableName PARTITION (id = 3) VALUES (5,
'Alice')")
+ // No PARTITION clause under Spark's default STATIC mode: the statement
is about the whole
+ // table, so the partition it does not write is replaced too.
spark.sql(s"INSERT OVERWRITE $tableName VALUES (5, 'Jerry', 1), (7,
'Tom', 2)")
checkAnswer(
spark.sql(s"SELECT id, age, name FROM $tableName ORDER BY id, age"),
- Row(1, 5, "Jerry") :: Row(2, 7, "Tom") :: Row(3, 5, "Alice") :: Nil
+ Row(1, 5, "Jerry") :: Row(2, 7, "Tom") :: Nil
)
+ withSparkSQLConf("spark.sql.sources.partitionOverwriteMode" ->
"dynamic") {
+ spark.sql(s"INSERT INTO $tableName PARTITION (id = 3) VALUES (5,
'Alice')")
+ spark.sql(s"INSERT OVERWRITE $tableName VALUES (9, 'Jerry', 1)")
+ checkAnswer(
+ spark.sql(s"SELECT id, age, name FROM $tableName ORDER BY id, age"),
+ Row(1, 9, "Jerry") :: Row(2, 7, "Tom") :: Row(3, 5, "Alice") :: Nil
+ )
+ }
}
}
diff --git
a/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
b/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
index d13c737370..a00b8dd284 100644
---
a/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
+++
b/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
@@ -29,10 +29,9 @@ import org.apache.spark.sql.types.StructType
*/
class FormatTableBatchWrite(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType)
- extends FormatTableBatchWriteBase(table, overwriteDynamic,
overwritePartitions, writeSchema)
+ extends FormatTableBatchWriteBase(table, overwritePartitions, writeSchema)
with BatchWrite
with Serializable {
diff --git
a/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark3Shim.scala
b/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark3Shim.scala
index b568ce16c3..3ea437da35 100644
---
a/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark3Shim.scala
+++
b/paimon-spark/paimon-spark3-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark3Shim.scala
@@ -218,10 +218,9 @@ class Spark3Shim extends SparkShim {
override def createFormatTableBatchWrite(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType): BatchWrite =
- new FormatTableBatchWrite(table, overwriteDynamic, overwritePartitions,
writeSchema)
+ new FormatTableBatchWrite(table, overwritePartitions, writeSchema)
override def createCTERelationRef(
cteId: Long,
diff --git
a/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
b/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
index 4fa58c95a7..5a114f1300 100644
---
a/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
+++
b/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/paimon/spark/format/FormatTableBatchWrite.scala
@@ -31,10 +31,9 @@ import org.apache.spark.sql.types.StructType
*/
class FormatTableBatchWrite(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType)
- extends FormatTableBatchWriteBase(table, overwriteDynamic,
overwritePartitions, writeSchema)
+ extends FormatTableBatchWriteBase(table, overwritePartitions, writeSchema)
with BatchWrite
with Serializable {
diff --git
a/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
b/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
index eea503d7d5..4ae3bb1892 100644
---
a/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
+++
b/paimon-spark/paimon-spark4-common/src/main/scala/org/apache/spark/sql/paimon/shims/Spark4Shim.scala
@@ -216,10 +216,9 @@ class Spark4Shim extends SparkShim {
override def createFormatTableBatchWrite(
table: FormatTable,
- overwriteDynamic: Option[Boolean],
overwritePartitions: Option[Map[String, String]],
writeSchema: StructType): BatchWrite =
- new FormatTableBatchWrite(table, overwriteDynamic, overwritePartitions,
writeSchema)
+ new FormatTableBatchWrite(table, overwritePartitions, writeSchema)
override def createCTERelationRef(
cteId: Long,