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 44c5113b8b [core] Extract injectable data evolution commit preparation
(#10686)
44c5113b8b is described below
commit 44c5113b8b37e0310175df7a2582eface500d45c
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Oct 20 14:15:26 2026 +0800
[core] Extract injectable data evolution commit preparation (#10686)
---
.../ClusteringCommitChangesRewriter.java | 59 +++
.../DataEvolutionCommitChangesRewriter.java | 125 ++++++
...writer.java => DataEvolutionIndexRewriter.java} | 25 +-
...ializeDeletionVectorsCommitChangesRewriter.java | 42 ++
.../apache/paimon/operation/FileStoreCommit.java | 4 +
.../paimon/operation/FileStoreCommitImpl.java | 167 ++------
.../operation/commit/CommitChangesRewriter.java | 69 ++++
.../commit/ReassignCompactChangesProvider.java | 21 +
.../apache/paimon/table/sink/TableCommitImpl.java | 6 +
.../dataevolution/DataEvolutionClusterTest.java | 52 ++-
.../operation/FileStoreCommitRewriterTest.java | 453 +++++++++++++++++++++
.../paimon/operation/FileStoreCommitTest.java | 32 +-
.../table/DataEvolutionDeletionVectorTest.java | 135 +++++-
.../flink/compact/IncrementalClusterCompact.java | 5 +-
.../flink/sink/DataEvolutionClusterSink.java | 49 +++
...DataEvolutionDeletionVectorMaterializeSink.java | 3 +
.../paimon/spark/procedure/CompactProcedure.java | 2 +
.../MaterializeDeletionVectorsProcedure.java | 3 +
18 files changed, 1090 insertions(+), 162 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/ClusteringCommitChangesRewriter.java
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/ClusteringCommitChangesRewriter.java
new file mode 100644
index 0000000000..932362bb29
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/ClusteringCommitChangesRewriter.java
@@ -0,0 +1,59 @@
+/*
+ * 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.append.dataevolution;
+
+import org.apache.paimon.Snapshot.CommitKind;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.operation.commit.CommitChanges;
+import org.apache.paimon.table.FileStoreTable;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** Remaps scalar indexes after clustering outputs receive their final row
IDs. */
+public final class ClusteringCommitChangesRewriter extends
DataEvolutionCommitChangesRewriter {
+
+ public ClusteringCommitChangesRewriter(FileStoreTable table) {
+ super(table);
+ }
+
+ @Override
+ public Attempt createAttempt(CommitKind kind, CommitChanges changes) {
+ if (kind != CommitKind.COMPACT) {
+ return Attempt.IDENTITY;
+ }
+
+ // Side files remain attached to committed data. Only outputs awaiting
their first row-ID
+ // assignment carry a permutation for this commit; later metadata
rewrites must ignore it.
+ Set<String> mappedClusteringOutputs =
+ changes.tableFiles.stream()
+ .filter(entry -> entry.kind() == FileKind.ADD)
+ .filter(entry -> entry.file().firstRowId() == null)
+ .filter(
+ entry ->
+ entry.file()
+ .extraFiles()
+ .contains(
+
ClusteringRowIdMapping.fileName(
+
entry.file().fileName())))
+ .map(entry -> entry.file().fileName())
+ .collect(Collectors.toSet());
+ return indexRewriteAttempt(changes, mappedClusteringOutputs);
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionCommitChangesRewriter.java
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionCommitChangesRewriter.java
new file mode 100644
index 0000000000..3adcc63ba5
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionCommitChangesRewriter.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.append.dataevolution;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.IndexManifestEntry;
+import org.apache.paimon.manifest.IndexManifestFile;
+import org.apache.paimon.operation.commit.CommitChanges;
+import org.apache.paimon.operation.commit.CommitChangesProvider;
+import org.apache.paimon.operation.commit.CommitChangesRewriter;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.CommitMessageImpl;
+import org.apache.paimon.utils.Pair;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static
org.apache.paimon.utils.DataEvolutionUtils.materializedRowIdRanges;
+
+/** Maintains index changes for operations which replace data-evolution row
IDs. */
+public abstract class DataEvolutionCommitChangesRewriter implements
CommitChangesRewriter {
+
+ private final FileStoreTable table;
+ private final IndexManifestFile indexManifestFile;
+
+ protected DataEvolutionCommitChangesRewriter(FileStoreTable table) {
+ this.table = table;
+ this.indexManifestFile =
table.store().indexManifestFileFactory().create();
+ }
+
+ @Override
+ public CommitChangesProvider compactChangesProvider(
+ CommitChanges changes, List<CommitMessage> messages) {
+ Set<Pair<BinaryRow, Integer>> materializedBuckets = new HashSet<>();
+ for (CommitMessage message : messages) {
+ CommitMessageImpl commitMessage = (CommitMessageImpl) message;
+ if (!materializedRowIdRanges(
+ commitMessage.compactIncrement().compactBefore(),
+ commitMessage.compactIncrement().compactAfter())
+ .isEmpty()) {
+ materializedBuckets.add(Pair.of(commitMessage.partition(),
commitMessage.bucket()));
+ }
+ }
+ if (materializedBuckets.isEmpty()) {
+ return CommitChangesRewriter.super.compactChangesProvider(changes,
messages);
+ }
+
+ return latestSnapshot -> {
+ List<IndexManifestEntry> indexFiles =
+ changes.indexFiles.stream()
+ // Replace global-index deletions prepared against
an older snapshot.
+ .filter(
+ entry ->
+ entry.kind() != FileKind.DELETE
+ ||
entry.indexFile().globalIndexMeta() == null
+ ||
!materializedBuckets.contains(
+ Pair.of(
+
entry.partition(),
+
entry.bucket())))
+ .collect(Collectors.toList());
+
+ // Each optimistic retry must include concurrently created indexes
before assigning
+ // final row IDs to this operation's outputs.
+ if (latestSnapshot != null && latestSnapshot.indexManifest() !=
null) {
+ for (IndexManifestEntry entry :
+
indexManifestFile.read(latestSnapshot.indexManifest())) {
+ if (entry.indexFile().globalIndexMeta() != null
+ && materializedBuckets.contains(
+ Pair.of(entry.partition(),
entry.bucket()))) {
+ indexFiles.add(entry.toDeleteEntry());
+ }
+ }
+ }
+
+ return new CommitChanges(changes.tableFiles,
changes.changelogFiles, indexFiles);
+ };
+ }
+
+ protected Attempt indexRewriteAttempt(CommitChanges changes, Set<String>
mappedOutputs) {
+ // Capture this before assignment makes newly allocated outputs
indistinguishable from
+ // ordinary compaction outputs which preserve their existing row IDs.
+ Set<String> rowIdPreservingOutputs =
+ changes.tableFiles.stream()
+ .filter(entry -> entry.kind() == FileKind.ADD)
+ .filter(entry -> entry.file().firstRowId() != null)
+ .map(entry -> entry.file().fileName())
+ .collect(Collectors.toSet());
+ return (newSnapshotId, assignedFiles, indexFiles, ioManager) -> {
+ if (assignedFiles.stream().noneMatch(entry -> entry.kind() ==
FileKind.DELETE)) {
+ return indexFiles;
+ }
+ return DataEvolutionIndexRewriter.rewrite(
+ table.fileIO(),
+ table.store().pathFactory(),
+ table.schemaManager(),
+ table.coreOptions(),
+ ioManager,
+ assignedFiles,
+ indexFiles,
+ mappedOutputs,
+ rowIdPreservingOutputs,
+ newSnapshotId);
+ };
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/ClusteringIndexRewriter.java
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionIndexRewriter.java
similarity index 95%
rename from
paimon-core/src/main/java/org/apache/paimon/append/dataevolution/ClusteringIndexRewriter.java
rename to
paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionIndexRewriter.java
index 36d360dac4..d9ec534abc 100644
---
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/ClusteringIndexRewriter.java
+++
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionIndexRewriter.java
@@ -22,8 +22,6 @@ import org.apache.paimon.CoreOptions;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.disk.IOManager;
import org.apache.paimon.fs.FileIO;
-import org.apache.paimon.fs.Path;
-import org.apache.paimon.fs.PositionOutputStream;
import org.apache.paimon.globalindex.DataEvolutionGlobalIndexRefreshPlanner;
import org.apache.paimon.globalindex.GlobalIndexBuilderUtils;
import org.apache.paimon.globalindex.GlobalIndexFileReadWrite;
@@ -71,14 +69,14 @@ import java.util.stream.Collectors;
import static
org.apache.paimon.utils.DataEvolutionUtils.materializedRowIdRanges;
import static org.apache.paimon.utils.Preconditions.checkArgument;
-/** Rewrites scalar index postings after clustering output files receive their
final row IDs. */
-public final class ClusteringIndexRewriter {
+/** Maintains scalar index coverage when data evolution replaces row IDs. */
+public final class DataEvolutionIndexRewriter {
// The current metadata protocol represents coverage with contiguous
ranges. Bound both
// tiny-file fan-out and repeated source scans until sparse coverage has
its own protocol.
private static final int MAX_COVERAGE_FRAGMENTS = 64;
- private ClusteringIndexRewriter() {}
+ private DataEvolutionIndexRewriter() {}
public static List<IndexManifestEntry> rewrite(
FileIO fileIO,
@@ -90,8 +88,7 @@ public final class ClusteringIndexRewriter {
List<IndexManifestEntry> indexFiles,
Set<String> mappedClusteringOutputs,
Set<String> rowIdPreservingOutputs,
- long newSnapshotId,
- List<Path> createdFiles)
+ long newSnapshotId)
throws IOException {
if (indexFiles.stream()
.noneMatch(
@@ -284,7 +281,7 @@ public final class ClusteringIndexRewriter {
GlobalIndexer.create(type, fields,
Options.fromMap(options.toMap()));
IndexPathFactory indexPath =
pathFactory.globalIndexFileFactory();
GlobalIndexFileReadWrite indexIO =
- trackingIndexIO(fileIO, indexPath, createdFiles);
+ new GlobalIndexFileReadWrite(fileIO, indexPath);
for (IndexManifestEntry source : sources) {
GlobalIndexMeta meta =
source.indexFile().globalIndexMeta();
List<Range> fragments = new ArrayList<>();
@@ -382,18 +379,6 @@ public final class ClusteringIndexRewriter {
fragments.add(fragment);
}
- private static GlobalIndexFileReadWrite trackingIndexIO(
- FileIO fileIO, IndexPathFactory indexPath, List<Path>
createdFiles) {
- return new GlobalIndexFileReadWrite(fileIO, indexPath) {
- @Override
- public PositionOutputStream newOutputStream(String fileName)
throws IOException {
- // Track before opening: failed writes can leave a partial
file without a result.
- createdFiles.add(indexPath.toPath(fileName));
- return super.newOutputStream(fileName);
- }
- };
- }
-
private static void readMapping(
FileIO fileIO,
DataFilePathFactory pathFactory,
diff --git
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/MaterializeDeletionVectorsCommitChangesRewriter.java
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/MaterializeDeletionVectorsCommitChangesRewriter.java
new file mode 100644
index 0000000000..6f2d3502c5
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/MaterializeDeletionVectorsCommitChangesRewriter.java
@@ -0,0 +1,42 @@
+/*
+ * 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.append.dataevolution;
+
+import org.apache.paimon.Snapshot.CommitKind;
+import org.apache.paimon.operation.commit.CommitChanges;
+import org.apache.paimon.table.FileStoreTable;
+
+import java.util.Collections;
+
+/** Maintains index coverage for deletion-vector materialization. */
+public final class MaterializeDeletionVectorsCommitChangesRewriter
+ extends DataEvolutionCommitChangesRewriter {
+
+ public MaterializeDeletionVectorsCommitChangesRewriter(FileStoreTable
table) {
+ super(table);
+ }
+
+ @Override
+ public Attempt createAttempt(CommitKind kind, CommitChanges changes) {
+ if (kind != CommitKind.COMPACT) {
+ return Attempt.IDENTITY;
+ }
+ return indexRewriteAttempt(changes, Collections.emptySet());
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
index 050a345a0e..ca4f96527f 100644
--- a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
+++ b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
@@ -23,6 +23,7 @@ import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.disk.IOManager;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.operation.commit.CommitChangesRewriter;
import org.apache.paimon.operation.metrics.CommitMetrics;
import org.apache.paimon.stats.Statistics;
import org.apache.paimon.table.sink.CommitMessage;
@@ -36,6 +37,9 @@ public interface FileStoreCommit extends AutoCloseable {
FileStoreCommit withIOManager(IOManager ioManager);
+ /** Injects metadata preparation into the existing commit attempt
lifecycle. */
+ FileStoreCommit withCommitChangesRewriter(CommitChangesRewriter rewriter);
+
FileStoreCommit ignoreEmptyCommit(boolean ignoreEmptyCommit);
FileStoreCommit withPartitionExpire(PartitionExpire partitionExpire);
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
index 70a4f2ac95..464f2ec3d6 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
@@ -22,8 +22,6 @@ import org.apache.paimon.CoreOptions;
import org.apache.paimon.Snapshot;
import org.apache.paimon.Snapshot.CommitKind;
import org.apache.paimon.annotation.VisibleForTesting;
-import org.apache.paimon.append.dataevolution.ClusteringIndexRewriter;
-import org.apache.paimon.append.dataevolution.ClusteringRowIdMapping;
import org.apache.paimon.catalog.SnapshotCommit;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.InternalRow;
@@ -47,6 +45,7 @@ import org.apache.paimon.manifest.PartitionEntry;
import org.apache.paimon.manifest.SimpleFileEntry;
import org.apache.paimon.operation.commit.CommitChanges;
import org.apache.paimon.operation.commit.CommitChangesProvider;
+import org.apache.paimon.operation.commit.CommitChangesRewriter;
import org.apache.paimon.operation.commit.CommitCleaner;
import org.apache.paimon.operation.commit.CommitResult;
import org.apache.paimon.operation.commit.CommitRollback;
@@ -118,7 +117,6 @@ import static
org.apache.paimon.operation.commit.ManifestEntryChanges.changedPar
import static
org.apache.paimon.operation.commit.RowTrackingCommitUtils.assignRowTracking;
import static
org.apache.paimon.partition.PartitionPredicate.createBinaryPartitions;
import static
org.apache.paimon.partition.PartitionPredicate.createPartitionPredicate;
-import static
org.apache.paimon.utils.DataEvolutionUtils.materializedRowIdRanges;
import static org.apache.paimon.utils.Preconditions.checkArgument;
import static org.apache.paimon.utils.Preconditions.checkNotNull;
@@ -170,6 +168,8 @@ public class FileStoreCommitImpl implements FileStoreCommit
{
private final ConflictDetection conflictDetection;
private final CommitCleaner commitCleaner;
+ @Nullable private CommitChangesRewriter commitChangesRewriter;
+
private boolean ignoreEmptyCommit;
private CommitMetrics commitMetrics;
private boolean appendCommitCheckConflict = false;
@@ -253,6 +253,12 @@ public class FileStoreCommitImpl implements
FileStoreCommit {
}
}
+ @Override
+ public FileStoreCommitImpl withCommitChangesRewriter(CommitChangesRewriter
rewriter) {
+ this.commitChangesRewriter = checkNotNull(rewriter);
+ return this;
+ }
+
@Override
public FileStoreCommit withIOManager(IOManager ioManager) {
this.ioManager = ioManager;
@@ -348,7 +354,6 @@ public class FileStoreCommitImpl implements FileStoreCommit
{
List<CommitMessage> commitMessages = committable.fileCommittables();
configureRowIdCheckFromMessages(commitMessages);
ManifestEntryChanges changes = collectChanges(commitMessages);
- Set<Pair<BinaryRow, Integer>> materializedBuckets =
materializedBuckets(commitMessages);
try {
List<SimpleFileEntry> appendSimpleEntries =
SimpleFileEntry.from(changes.appendTableFiles);
@@ -406,7 +411,7 @@ public class FileStoreCommitImpl implements FileStoreCommit
{
changes.compactTableFiles,
changes.compactChangelog,
changes.compactIndexFiles),
- materializedBuckets),
+ commitMessages),
committable.identifier(),
committable.watermark(),
committable.properties(),
@@ -595,7 +600,7 @@ public class FileStoreCommitImpl implements FileStoreCommit
{
changes.compactTableFiles,
emptyList(),
changes.compactIndexFiles),
- Collections.emptySet()),
+ Collections.emptyList()),
committable.identifier(),
committable.watermark(),
committable.properties(),
@@ -836,70 +841,18 @@ public class FileStoreCommitImpl implements
FileStoreCommit {
}
}
- private Set<Pair<BinaryRow, Integer>>
materializedBuckets(List<CommitMessage> commitMessages) {
- if (!options.dataEvolutionEnabled()) {
- return Collections.emptySet();
- }
-
- Set<Pair<BinaryRow, Integer>> result = new HashSet<>();
- for (CommitMessage message : commitMessages) {
- CommitMessageImpl commitMessage = (CommitMessageImpl) message;
- if (!materializedRowIdRanges(
- commitMessage.compactIncrement().compactBefore(),
- commitMessage.compactIncrement().compactAfter())
- .isEmpty()) {
- result.add(Pair.of(commitMessage.partition(),
commitMessage.bucket()));
- }
- }
- return result;
- }
-
- @VisibleForTesting
- CommitChangesProvider compactChangesProvider(
- CommitChanges changes, Set<Pair<BinaryRow, Integer>>
materializedBuckets) {
- if (materializedBuckets.isEmpty()) {
- Long lastSafeSnapshot =
options.commitLastSafeSnapshot().orElse(null);
- if (options.dataEvolutionEnabled()
- &&
!conflictDetection.shouldCheckRowIdFromSnapshot(CommitKind.COMPACT)
- && lastSafeSnapshot != null
- && lastSafeSnapshot >= 0) {
- return new ReassignCompactChangesProvider(
- fileIO, pathFactory, snapshotManager,
lastSafeSnapshot, changes);
- }
- return CommitChangesProvider.provider(
- changes.tableFiles, changes.changelogFiles,
changes.indexFiles);
- }
-
- return latestSnapshot -> {
- List<IndexManifestEntry> indexFiles =
- changes.indexFiles.stream()
- // Replace global-index deletions prepared against
an older snapshot.
- .filter(
- entry ->
- entry.kind() != FileKind.DELETE
- ||
entry.indexFile().globalIndexMeta() == null
- ||
!materializedBuckets.contains(
- Pair.of(
-
entry.partition(),
-
entry.bucket())))
- .collect(Collectors.toList());
-
- // This provider is invoked again after every optimistic-commit
conflict. Scanning the
- // latest snapshot includes concurrently created indexes in this
attempt's changes.
- // Clustering can then remap their postings after the final row
IDs have been assigned.
- if (latestSnapshot != null && latestSnapshot.indexManifest() !=
null) {
- for (IndexManifestEntry entry :
-
indexManifestFile.read(latestSnapshot.indexManifest())) {
- if (entry.indexFile().globalIndexMeta() != null
- && materializedBuckets.contains(
- Pair.of(entry.partition(),
entry.bucket()))) {
- indexFiles.add(entry.toDeleteEntry());
- }
- }
- }
-
- return new CommitChanges(changes.tableFiles,
changes.changelogFiles, indexFiles);
- };
+ private CommitChangesProvider compactChangesProvider(
+ CommitChanges changes, List<CommitMessage> messages) {
+ if (commitChangesRewriter != null) {
+ return commitChangesRewriter.compactChangesProvider(changes,
messages);
+ }
+ return ReassignCompactChangesProvider.create(
+ fileIO,
+ pathFactory,
+ snapshotManager,
+ options,
+ changes,
+
conflictDetection.shouldCheckRowIdFromSnapshot(CommitKind.COMPACT));
}
private int tryCommit(
@@ -1150,31 +1103,15 @@ public class FileStoreCommitImpl implements
FileStoreCommit {
List<ManifestEntry> deltaFiles = changes.tableFiles;
List<ManifestEntry> changelogFiles = changes.changelogFiles;
List<IndexManifestEntry> indexFiles = changes.indexFiles;
- // Side files remain attached to committed data. Only outputs awaiting
their first row-ID
- // assignment carry a permutation for this commit; later metadata
rewrites must ignore it.
- Set<String> mappedClusteringOutputs =
- commitKind == CommitKind.COMPACT &&
options.dataEvolutionEnabled()
- ? deltaFiles.stream()
- .filter(entry -> entry.kind() == FileKind.ADD)
- .filter(entry -> entry.file().firstRowId() ==
null)
- .filter(
- entry ->
- entry.file()
- .extraFiles()
- .contains(
-
ClusteringRowIdMapping.fileName(
-
entry.file().fileName())))
- .map(entry -> entry.file().fileName())
- .collect(Collectors.toSet())
- : Collections.emptySet();
- Set<String> rowIdPreservingOutputs =
- commitKind == CommitKind.COMPACT &&
options.dataEvolutionEnabled()
- ? deltaFiles.stream()
- .filter(entry -> entry.kind() == FileKind.ADD)
- .filter(entry -> entry.file().firstRowId() !=
null)
- .map(entry -> entry.file().fileName())
- .collect(Collectors.toSet())
- : Collections.emptySet();
+ CommitChangesRewriter.Attempt rewriteAttempt =
+ commitChangesRewriter == null
+ ? CommitChangesRewriter.Attempt.IDENTITY
+ : commitChangesRewriter.createAttempt(
+ commitKind,
+ new CommitChanges(
+
Collections.unmodifiableList(changes.tableFiles),
+
Collections.unmodifiableList(changes.changelogFiles),
+
Collections.unmodifiableList(changes.indexFiles)));
Set<Long> rebasedReassignments =
changesProvider.rebasedReassignments();
long newSnapshotId = Snapshot.FIRST_SNAPSHOT_ID;
@@ -1283,7 +1220,6 @@ public class FileStoreCommitImpl implements
FileStoreCommit {
List<ManifestFileMeta> mergeAfterManifests = new ArrayList<>();
boolean skipManifestMerge = false;
long nextRowIdStart = firstRowIdStart;
- List<Path> clusteringIndexOutputs = new ArrayList<>();
try {
long previousTotalRecordCount = 0L;
Long currentWatermark = watermark;
@@ -1349,23 +1285,13 @@ public class FileStoreCommitImpl implements
FileStoreCommit {
deltaFiles = assigned.assignedEntries;
}
- if (commitKind == CommitKind.COMPACT
- && options.dataEvolutionEnabled()
- && deltaFiles.stream().anyMatch(entry -> entry.kind() ==
FileKind.DELETE)) {
- indexFiles =
- ClusteringIndexRewriter.rewrite(
- fileIO,
- pathFactory,
- schemaManager,
- options,
- ioManager,
- deltaFiles,
- indexFiles,
- mappedClusteringOutputs,
- rowIdPreservingOutputs,
- newSnapshotId,
- clusteringIndexOutputs);
- }
+ indexFiles =
+ checkNotNull(
+ rewriteAttempt.rewriteIndexFiles(
+ newSnapshotId,
+ Collections.unmodifiableList(deltaFiles),
+ Collections.unmodifiableList(indexFiles),
+ ioManager));
if (options.snapshotSequenceOrdering()) {
deltaFiles = stampSequenceWithSnapshotId(newSnapshotId,
commitKind, deltaFiles);
@@ -1447,7 +1373,6 @@ public class FileStoreCommitImpl implements
FileStoreCommit {
operation);
} catch (Throwable e) {
// fails when preparing for commit, we should clean up
- clusteringIndexOutputs.forEach(fileIO::deleteQuietly);
commitCleaner.cleanUpReuseTmpManifests(
deltaManifestList, changelogManifestList,
oldIndexManifest, indexManifest);
commitCleaner.cleanUpNoReuseTmpManifests(
@@ -1464,16 +1389,10 @@ public class FileStoreCommitImpl implements
FileStoreCommit {
final List<SimpleFileEntry> finalBaseFiles = baseDataFiles;
final List<ManifestEntry> finalDeltaFiles = deltaFiles;
final List<IndexManifestEntry> finalIndexFiles = indexFiles;
- try {
- commitPreCallbacks.forEach(
- callback ->
- callback.call(
- finalBaseFiles, finalDeltaFiles,
finalIndexFiles, newSnapshot));
- } catch (RuntimeException | Error failure) {
- // No snapshot publication has been attempted, so these new
payloads are unreferenced.
- clusteringIndexOutputs.forEach(fileIO::deleteQuietly);
- throw failure;
- }
+ commitPreCallbacks.forEach(
+ callback ->
+ callback.call(
+ finalBaseFiles, finalDeltaFiles,
finalIndexFiles, newSnapshot));
try {
success = commitSnapshotImpl(latestSnapshot, newSnapshot,
deltaPartitionEntries);
} catch (Exception e) {
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/commit/CommitChangesRewriter.java
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/CommitChangesRewriter.java
new file mode 100644
index 0000000000..00f31f2d74
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/CommitChangesRewriter.java
@@ -0,0 +1,69 @@
+/*
+ * 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.operation.commit;
+
+import org.apache.paimon.Snapshot.CommitKind;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.manifest.IndexManifestEntry;
+import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.table.sink.CommitMessage;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.util.List;
+
+/** Prepares commit metadata while reusing the committer's conflict and
publication lifecycle. */
+public interface CommitChangesRewriter {
+
+ CommitChangesRewriter IDENTITY = new CommitChangesRewriter() {};
+
+ /** Creates one provider whose state is retained across optimistic retries
of this compact. */
+ default CommitChangesProvider compactChangesProvider(
+ CommitChanges changes, List<CommitMessage> messages) {
+ return CommitChangesProvider.provider(
+ changes.tableFiles, changes.changelogFiles,
changes.indexFiles);
+ }
+
+ /**
+ * Creates isolated state for one attempt, after resolving its latest
changes and before row-ID
+ * assignment. The changes are read-only; this method must not create
files.
+ */
+ default Attempt createAttempt(CommitKind kind, CommitChanges changes) {
+ return Attempt.IDENTITY;
+ }
+
+ /** Index-only preparation after conflict checking and final row-ID
assignment. */
+ @FunctionalInterface
+ interface Attempt {
+
+ Attempt IDENTITY = (snapshotId, files, indexes, ioManager) -> indexes;
+
+ /**
+ * Returns index changes for this candidate snapshot. Inputs are
read-only. Generated index
+ * files are retained on failure; unreferenced files can be removed by
orphan-file cleanup.
+ */
+ List<IndexManifestEntry> rewriteIndexFiles(
+ long newSnapshotId,
+ List<ManifestEntry> assignedFiles,
+ List<IndexManifestEntry> indexFiles,
+ @Nullable IOManager ioManager)
+ throws IOException;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/commit/ReassignCompactChangesProvider.java
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/ReassignCompactChangesProvider.java
index 88d3dcf906..0a66a6b488 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/commit/ReassignCompactChangesProvider.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/ReassignCompactChangesProvider.java
@@ -18,6 +18,7 @@
package org.apache.paimon.operation.commit;
+import org.apache.paimon.CoreOptions;
import org.apache.paimon.Snapshot;
import org.apache.paimon.append.dataevolution.SerializationAssignment;
import org.apache.paimon.fs.FileIO;
@@ -67,6 +68,26 @@ public final class ReassignCompactChangesProvider implements
CommitChangesProvid
this.changes = changes;
}
+ /** Selects the ordinary compaction provider, allowing reuse across
concurrent reassignment. */
+ public static CommitChangesProvider create(
+ FileIO fileIO,
+ FileStorePathFactory paths,
+ SnapshotManager snapshots,
+ CoreOptions options,
+ CommitChanges changes,
+ boolean checkRowIdsFromSnapshot) {
+ Long lastSafeSnapshot = options.commitLastSafeSnapshot().orElse(null);
+ if (options.dataEvolutionEnabled()
+ && !checkRowIdsFromSnapshot
+ && lastSafeSnapshot != null
+ && lastSafeSnapshot >= 0) {
+ return new ReassignCompactChangesProvider(
+ fileIO, paths, snapshots, lastSafeSnapshot, changes);
+ }
+ return CommitChangesProvider.provider(
+ changes.tableFiles, changes.changelogFiles,
changes.indexFiles);
+ }
+
@Override
public CommitChanges provide(@Nullable Snapshot latest) {
checkState(
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
index 23f1132fd7..ee5fcd2eb9 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
@@ -31,6 +31,7 @@ import org.apache.paimon.manifest.ManifestCommittable;
import org.apache.paimon.metrics.MetricRegistry;
import org.apache.paimon.operation.FileStoreCommit;
import org.apache.paimon.operation.PartitionExpire;
+import org.apache.paimon.operation.commit.CommitChangesRewriter;
import org.apache.paimon.operation.metrics.CommitMetrics;
import org.apache.paimon.stats.Statistics;
import org.apache.paimon.tag.Tag;
@@ -181,6 +182,11 @@ public class TableCommitImpl implements InnerTableCommit {
return this;
}
+ public TableCommitImpl withCommitChangesRewriter(CommitChangesRewriter
rewriter) {
+ commit.withCommitChangesRewriter(rewriter);
+ return this;
+ }
+
@Override
public TableCommitImpl withOperation(Snapshot.Operation operation) {
commit.withOperation(operation);
diff --git
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionClusterTest.java
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionClusterTest.java
index adb355dfc9..9070a9a05e 100644
---
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionClusterTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionClusterTest.java
@@ -544,6 +544,35 @@ class DataEvolutionClusterTest extends TableTestBase {
"a|50|y|9"));
}
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testIndexRemappingRequiresClusteringRewriter(boolean
materializeDeletionVectors)
+ throws Exception {
+ FileStoreTable table = createTable(false);
+ append(table, "a", 30, 10, 20);
+ commit(table, prepareGlobalIndex(table, table.latestSnapshot().get(),
"id"));
+ Snapshot snapshot = table.latestSnapshot().get();
+ List<CommitMessage> messages = prepareMappedCluster(table, snapshot,
true);
+ // An ordinary caller explicitly invalidates its old indexes. Only a
clustering committer
+ // should replace those deletions with remapped postings from the
permutation sidecar.
+ messages.addAll(
+ new DataEvolutionCompactionCommitPreparation(table,
snapshot).prepare(messages));
+ messages =
+ IncrementalClusterRewriter.create(table,
snapshot).prepareCommitMessages(messages);
+ try (TableCommitImpl commit =
table.newCommit("test-scoped-index-rewriter")) {
+ commit.materializeDvRowIdCheck();
+ if (materializeDeletionVectors) {
+ commit.withCommitChangesRewriter(
+ new
MaterializeDeletionVectorsCommitChangesRewriter(table));
+ }
+ commit.commit(messages);
+ }
+
+ assertThat(globalIndexNames(table)).isEmpty();
+ assertThat(readRows(table))
+ .containsExactly("a|10|base-10|3", "a|20|base-20|4",
"a|30|base-30|5");
+ }
+
@ParameterizedTest
@ValueSource(strings = {"btree", "bitmap"})
void testClusterPreservesExactPartialIndexCoverage(String indexType)
throws Exception {
@@ -707,8 +736,10 @@ class DataEvolutionClusterTest extends TableTestBase {
assertMinorClusterResult(table, deletionVectors, untouchedRows,
untouchedFiles, 21L, 4);
}
- @Test
- void testMinorClusterRemapsIndexWhenAllSelectedRowsAreDeleted() throws
Exception {
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testFullyDeletedRewritePreservesIndexForUnchangedRows(boolean
clustering)
+ throws Exception {
FileStoreTable table = createMinorClusterTable(true);
List<String> untouchedRows = rowsInRange(table, new Range(12, 14));
List<String> untouchedFiles = filesInRange(table, new Range(12, 14));
@@ -726,7 +757,18 @@ class DataEvolutionClusterTest extends TableTestBase {
.compactIncrement()
.compactAfter())
.isEmpty());
- commitCluster(table, snapshot, messages);
+ if (clustering) {
+ commitCluster(table, snapshot, messages);
+ } else {
+ try (TableCommitImpl commit =
table.newCommit("test-materialize-deleted-rows")) {
+ commit.materializeDvRowIdCheck()
+ .withCommitChangesRewriter(
+ new
MaterializeDeletionVectorsCommitChangesRewriter(table))
+ .commit(
+ IncrementalClusterRewriter.create(table,
snapshot)
+ .prepareCommitMessages(messages));
+ }
+ }
PredicateBuilder predicates = new PredicateBuilder(table.rowType());
int id = table.rowType().getFieldIndex("id");
@@ -2077,7 +2119,9 @@ class DataEvolutionClusterTest extends TableTestBase {
private void commitChecked(FileStoreTable table, List<CommitMessage>
messages)
throws Exception {
try (TableCommitImpl commit = table.newCommit("test-cluster")) {
- commit.materializeDvRowIdCheck().commit(messages);
+ commit.materializeDvRowIdCheck()
+ .withCommitChangesRewriter(new
ClusteringCommitChangesRewriter(table))
+ .commit(messages);
}
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitRewriterTest.java
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitRewriterTest.java
new file mode 100644
index 0000000000..8880bb9887
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitRewriterTest.java
@@ -0,0 +1,453 @@
+/*
+ * 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.operation;
+
+import org.apache.paimon.AppendOnlyFileStore;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.catalog.RenamingSnapshotCommit;
+import org.apache.paimon.catalog.SnapshotCommit;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.index.GlobalIndexMeta;
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.IndexManifestEntry;
+import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.manifest.SimpleFileEntry;
+import org.apache.paimon.operation.commit.CommitChanges;
+import org.apache.paimon.operation.commit.CommitChangesRewriter;
+import org.apache.paimon.operation.commit.ConflictDetection;
+import org.apache.paimon.partition.PartitionStatistics;
+import org.apache.paimon.schema.FileSystemSchemaManager;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.TableTestBase;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.CommitPreCallback;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.utils.Range;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Commit-attempt isolation and index payload retention for injected index
rewriters. */
+class FileStoreCommitRewriterTest extends TableTestBase {
+
+ @Test
+ void testRetryCreatesAttemptFromLatestSnapshotAndFinalRowIds() throws
Exception {
+ FileStoreTable table = createTable();
+ write(table, GenericRow.of(1));
+ ManifestCommittable pending = prepareAppend(table, 10, 20);
+ List<Long> snapshots = new ArrayList<>();
+ List<Long> firstRowIds = new ArrayList<>();
+ List<Path> outputs = new ArrayList<>();
+ List<CommitChangesRewriter.Attempt> attempts = new ArrayList<>();
+ AtomicInteger publications = new AtomicInteger();
+ SnapshotCommit publisher =
+ new DelegatingSnapshotCommit(table) {
+ @Override
+ protected boolean commitImpl(
+ String baseUuid,
+ Snapshot snapshot,
+ String branch,
+ List<PartitionStatistics> statistics)
+ throws Exception {
+ if (publications.incrementAndGet() == 1) {
+ write(table, GenericRow.of(2), GenericRow.of(3));
+ assertThat(delegate.commit(baseUuid, snapshot,
branch, statistics))
+ .isFalse();
+ return false;
+ }
+ return delegate.commit(baseUuid, snapshot, branch,
statistics);
+ }
+ };
+ CommitChangesRewriter rewriter =
+ new CommitChangesRewriter() {
+ @Override
+ public Attempt createAttempt(Snapshot.CommitKind kind,
CommitChanges changes) {
+ assertThat(kind).isEqualTo(Snapshot.CommitKind.APPEND);
+ assertThat(changes.tableFiles).hasSize(1);
+
assertThat(changes.tableFiles.get(0).file().firstRowId()).isNull();
+ assertReadOnly(changes.tableFiles);
+ assertReadOnly(changes.changelogFiles);
+ assertReadOnly(changes.indexFiles);
+ AtomicInteger calls = new AtomicInteger();
+ Attempt attempt =
+ (snapshotId, assigned, indexes, ioManager) -> {
+
assertThat(calls.incrementAndGet()).isEqualTo(1);
+ snapshots.add(snapshotId);
+ assertReadOnly(assigned);
+ assertReadOnly(indexes);
+
firstRowIds.add(assigned.get(0).file().firstRowId());
+ return writeIndex(table, assigned.get(0),
outputs);
+ };
+ attempts.add(attempt);
+ return attempt;
+ }
+ };
+ try (FileStoreCommit commit =
+ newCommit(table, publisher, Collections.emptyList())
+ .withCommitChangesRewriter(rewriter)) {
+ commit.commit(pending, false);
+ }
+
+ assertThat(publications).hasValue(2);
+ assertThat(snapshots).containsExactly(2L, 3L);
+ assertThat(firstRowIds).containsExactly(1L, 3L);
+ assertThat(attempts).hasSize(2);
+ assertThat(attempts.get(0)).isNotSameAs(attempts.get(1));
+ assertThat(table.latestSnapshot().get().nextRowId()).isEqualTo(5L);
+ assertThat(read(table))
+ .extracting(row -> row.getInt(0))
+ .containsExactlyInAnyOrder(1, 2, 3, 10, 20);
+ List<IndexManifestEntry> indexes =
table.store().newIndexFileHandler().scanEntries();
+ assertThat(indexes).hasSize(1);
+
assertThat(indexes.get(0).indexFile().fileName()).isEqualTo(outputs.get(1).getName());
+ assertThat(indexes.get(0).indexFile().globalIndexMeta().rowRange())
+ .isEqualTo(new Range(3, 4));
+ // A false publication response is uncertain, even when this test's
publisher knows it lost.
+ for (Path output : outputs) {
+ assertThat(table.fileIO().exists(output)).isTrue();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testPreparationFailureRetainsOrphanIndex(boolean fatalError) throws
Exception {
+ FileStoreTable table = createTable();
+ ManifestCommittable pending = prepareAppend(table, 10);
+ List<Path> outputs = new ArrayList<>();
+ Throwable failure =
+ fatalError
+ ? new AssertionError("rewrite failed")
+ : new IOException("rewrite failed");
+ AtomicInteger publications = new AtomicInteger();
+ CommitChangesRewriter rewriter =
+ new CommitChangesRewriter() {
+ @Override
+ public Attempt createAttempt(Snapshot.CommitKind kind,
CommitChanges changes) {
+ return (snapshotId, assigned, indexes, ioManager) -> {
+ writeIndex(table, assigned.get(0), outputs);
+ if (fatalError) {
+ throw (AssertionError) failure;
+ }
+ throw (IOException) failure;
+ };
+ }
+ };
+ try (FileStoreCommit commit =
+ newCommit(table, countingPublisher(table, publications),
Collections.emptyList())
+ .withCommitChangesRewriter(rewriter)) {
+ assertThatThrownBy(() -> commit.commit(pending, false))
+ .hasMessageContaining("preparing snapshot #1")
+ .hasCauseReference(failure);
+ }
+ assertThat(outputs).hasSize(1);
+
assertThat(table.fileIO().readFileUtf8(outputs.get(0))).isEqualTo("index
payload");
+ assertThat(publications).hasValue(0);
+ assertThat(table.latestSnapshot()).isEmpty();
+ }
+
+ @Test
+ void testManifestPreparationFailureRetainsOrphanIndex() throws Exception {
+ FileStoreTable table = createTable();
+ ManifestCommittable pending = prepareAppend(table, 10);
+ List<Path> outputs = new ArrayList<>();
+ AtomicInteger publications = new AtomicInteger();
+ CommitChangesRewriter rewriter =
+ new CommitChangesRewriter() {
+ @Override
+ public Attempt createAttempt(Snapshot.CommitKind kind,
CommitChanges changes) {
+ return (snapshotId, assigned, indexes, ioManager) -> {
+ IndexManifestEntry index =
+ writeIndex(table, assigned.get(0),
outputs).get(0);
+ // Fail after the extension returns, while merging
the index manifest.
+ return
Collections.singletonList(index.toDeleteEntry());
+ };
+ }
+ };
+ try (FileStoreCommit commit =
+ newCommit(table, countingPublisher(table, publications),
Collections.emptyList())
+ .withCommitChangesRewriter(rewriter)) {
+ assertThatThrownBy(() -> commit.commit(pending, false))
+ .hasMessageContaining("preparing snapshot #1")
+ .hasRootCauseInstanceOf(IllegalStateException.class)
+ .hasStackTraceContaining("which does not exist.");
+ }
+ assertThat(outputs).hasSize(1);
+
assertThat(table.fileIO().readFileUtf8(outputs.get(0))).isEqualTo("index
payload");
+ assertThat(publications).hasValue(0);
+ assertThat(table.latestSnapshot()).isEmpty();
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testPreCallbackFailureRetainsOrphanIndex(boolean fatalError) throws
Exception {
+ FileStoreTable table = createTable();
+ ManifestCommittable pending = prepareAppend(table, 10);
+ List<Path> outputs = new ArrayList<>();
+ Throwable failure =
+ fatalError
+ ? new AssertionError("validation failed")
+ : new IllegalStateException("validation failed");
+ AtomicInteger publications = new AtomicInteger();
+ CommitPreCallback callback =
+ new CommitPreCallback() {
+ @Override
+ public void call(
+ List<SimpleFileEntry> base,
+ List<ManifestEntry> delta,
+ List<IndexManifestEntry> indexes,
+ Snapshot snapshot) {
+ assertThat(indexes).hasSize(1);
+ if (fatalError) {
+ throw (AssertionError) failure;
+ }
+ throw (IllegalStateException) failure;
+ }
+
+ @Override
+ public void close() {}
+ };
+ try (FileStoreCommit commit =
+ newCommit(
+ table,
+ countingPublisher(table, publications),
+ Collections.singletonList(callback))
+ .withCommitChangesRewriter(indexRewriter(table,
outputs))) {
+ assertThatThrownBy(() -> commit.commit(pending,
false)).isSameAs(failure);
+ }
+ assertThat(outputs).hasSize(1);
+
assertThat(table.fileIO().readFileUtf8(outputs.get(0))).isEqualTo("index
payload");
+ assertThat(publications).hasValue(0);
+ assertThat(table.latestSnapshot()).isEmpty();
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testUncertainPublicationRetainsVisibleIndex(boolean
publicationThrows) throws Exception {
+ FileStoreTable table = createTable();
+ ManifestCommittable pending = prepareAppend(table, 10, 20);
+ List<Path> outputs = new ArrayList<>();
+ AtomicInteger attempts = new AtomicInteger();
+ AtomicInteger publications = new AtomicInteger();
+ SnapshotCommit publisher =
+ new DelegatingSnapshotCommit(table) {
+ @Override
+ protected boolean commitImpl(
+ String baseUuid,
+ Snapshot snapshot,
+ String branch,
+ List<PartitionStatistics> statistics)
+ throws Exception {
+ publications.incrementAndGet();
+ assertThat(delegate.commit(baseUuid, snapshot, branch,
statistics))
+ .isTrue();
+ if (publicationThrows) {
+ throw new IOException("publication response lost");
+ }
+ return false;
+ }
+ };
+ CommitChangesRewriter rewriter =
+ new CommitChangesRewriter() {
+ @Override
+ public Attempt createAttempt(Snapshot.CommitKind kind,
CommitChanges changes) {
+ attempts.incrementAndGet();
+ return (snapshotId, assigned, indexes, ioManager) ->
+ writeIndex(table, assigned.get(0), outputs);
+ }
+ };
+ try (FileStoreCommit commit =
+ newCommit(table, publisher, Collections.emptyList())
+ .withCommitChangesRewriter(rewriter)) {
+ commit.commit(pending, false);
+ }
+
+ assertThat(publications).hasValue(1);
+ assertThat(attempts).hasValue(1);
+ // Reconciliation must happen before constructing another attempt or
rewriting another
+ // index.
+ assertThat(outputs).hasSize(1);
+
assertThat(table.fileIO().readFileUtf8(outputs.get(0))).isEqualTo("index
payload");
+ assertThat(table.latestSnapshot().get().id()).isEqualTo(1L);
+ assertThat(table.latestSnapshot().get().nextRowId()).isEqualTo(2L);
+ assertThat(read(table)).extracting(row ->
row.getInt(0)).containsExactlyInAnyOrder(10, 20);
+ List<IndexManifestEntry> indexes =
table.store().newIndexFileHandler().scanEntries();
+ assertThat(indexes).hasSize(1);
+
assertThat(indexes.get(0).indexFile().fileName()).isEqualTo(outputs.get(0).getName());
+ assertThat(indexes.get(0).indexFile().globalIndexMeta().rowRange())
+ .isEqualTo(new Range(0, 1));
+ }
+
+ private FileStoreTable createTable() throws Exception {
+ catalog.createTable(
+ identifier(),
+ Schema.newBuilder()
+ .column("id", DataTypes.INT())
+ .option(CoreOptions.ROW_TRACKING_ENABLED.key(), "true")
+ .option(CoreOptions.WRITE_ONLY.key(), "true")
+ .option(CoreOptions.COMMIT_MIN_RETRY_WAIT.key(), "0ms")
+ .option(CoreOptions.COMMIT_MAX_RETRY_WAIT.key(), "0ms")
+ .build(),
+ false);
+ return getTableDefault();
+ }
+
+ private ManifestCommittable prepareAppend(FileStoreTable table, int...
values)
+ throws Exception {
+ ManifestCommittable pending = new ManifestCommittable(17L);
+ try (BatchTableWrite write = table.newBatchWriteBuilder().newWrite()) {
+ for (int value : values) {
+ write.write(GenericRow.of(value));
+ }
+ write.prepareCommit().forEach(pending::addFileCommittable);
+ }
+ return pending;
+ }
+
+ private CommitChangesRewriter indexRewriter(FileStoreTable table,
List<Path> outputs) {
+ return new CommitChangesRewriter() {
+ @Override
+ public Attempt createAttempt(Snapshot.CommitKind kind,
CommitChanges changes) {
+ return (snapshotId, assigned, indexes, ioManager) ->
+ writeIndex(table, assigned.get(0), outputs);
+ }
+ };
+ }
+
+ private List<IndexManifestEntry> writeIndex(
+ FileStoreTable table, ManifestEntry assigned, List<Path> outputs)
throws IOException {
+ Path path =
table.store().pathFactory().globalIndexFileFactory().newPath();
+ outputs.add(path);
+ table.fileIO().writeFile(path, "index payload", false);
+ IndexFileMeta index =
+ new IndexFileMeta(
+ "test-index",
+ path.getName(),
+ 13,
+ assigned.file().rowCount(),
+ new GlobalIndexMeta(
+ assigned.file().nonNullFirstRowId(),
+ assigned.file().nonNullRowIdRange().to,
+ 0,
+ null,
+ null),
+ null);
+ return Collections.singletonList(
+ new IndexManifestEntry(
+ FileKind.ADD, assigned.partition(), assigned.bucket(),
index));
+ }
+
+ private SnapshotCommit countingPublisher(FileStoreTable table,
AtomicInteger publications) {
+ return new DelegatingSnapshotCommit(table) {
+ @Override
+ protected boolean commitImpl(
+ String baseUuid,
+ Snapshot snapshot,
+ String branch,
+ List<PartitionStatistics> statistics)
+ throws Exception {
+ publications.incrementAndGet();
+ return delegate.commit(baseUuid, snapshot, branch, statistics);
+ }
+ };
+ }
+
+ private FileStoreCommitImpl newCommit(
+ FileStoreTable table, SnapshotCommit publisher,
List<CommitPreCallback> callbacks) {
+ AppendOnlyFileStore store = (AppendOnlyFileStore) table.store();
+ CoreOptions options = store.options();
+ return new FileStoreCommitImpl(
+ publisher,
+ table.fileIO(),
+ new FileSystemSchemaManager(table.fileIO(), table.location()),
+ "rewriter-test",
+ "rewriter-test-user",
+ store.partitionType(),
+ options,
+ store.pathFactory(),
+ store.snapshotManager(),
+ store.manifestFileFactory(),
+ store.manifestListFactory(),
+ store.indexManifestFileFactory(),
+ store::newScan,
+ store.newStatsFileHandler(),
+ store.bucketMode(),
+ callbacks,
+ Collections.emptyList(),
+ scanner ->
+ ConflictDetection.create(
+ "rewriter-test",
+ "rewriter-test-user",
+ store.partitionType(),
+ store.pathFactory(),
+ store.newKeyComparator(),
+ store.bucketMode(),
+ options.deletionVectorsEnabled(),
+ false,
+ false,
+ options.pkClusteringOverride(),
+ store.newIndexFileHandler(),
+ store.snapshotManager(),
+ scanner),
+ null);
+ }
+
+ private static void assertReadOnly(List<?> entries) {
+
assertThatThrownBy(entries::clear).isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ private static class DelegatingSnapshotCommit extends SnapshotCommit {
+
+ protected final SnapshotCommit delegate;
+
+ private DelegatingSnapshotCommit(FileStoreTable table) {
+ super(Lock.empty());
+ delegate = new RenamingSnapshotCommit(table.snapshotManager(),
Lock.empty());
+ }
+
+ @Override
+ protected boolean commitImpl(
+ String baseUuid,
+ Snapshot snapshot,
+ String branch,
+ List<PartitionStatistics> statistics)
+ throws Exception {
+ return delegate.commit(baseUuid, snapshot, branch, statistics);
+ }
+
+ @Override
+ protected void closeImpl() throws Exception {
+ delegate.close();
+ }
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
index 4c9291b25b..110d81faaa 100644
---
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
@@ -24,6 +24,7 @@ import org.apache.paimon.Snapshot;
import org.apache.paimon.TestAppendFileStore;
import org.apache.paimon.TestFileStore;
import org.apache.paimon.TestKeyValueGenerator;
+import
org.apache.paimon.append.dataevolution.MaterializeDeletionVectorsCommitChangesRewriter;
import org.apache.paimon.catalog.RenamingSnapshotCommit;
import org.apache.paimon.catalog.SnapshotCommit;
import org.apache.paimon.data.BinaryRow;
@@ -1021,16 +1022,27 @@ public class FileStoreCommitTest {
otherBucketDelete,
nonGlobalDelete));
- try (FileStoreCommitImpl commit = store.newCommit()) {
- CommitChanges refreshed =
- commit.compactChangesProvider(
- changes,
Collections.singleton(Pair.of(partition, 0)))
- .provide(null);
-
- // Global-index removals from earlier preparation are replaced for
the entire bucket,
- // even if their ranges do not overlap the materialized input.
-
assertThat(refreshed.indexFiles).containsExactly(otherBucketDelete,
nonGlobalDelete);
- }
+ CommitMessage message =
+ new CommitMessageImpl(
+ partition,
+ 0,
+ 2,
+ DataIncrement.emptyIncrement(),
+ new CompactIncrement(
+ Collections.singletonList(
+ addFile(partition, 0, 2,
0).file().assignFirstRowId(0)),
+ Collections.emptyList(),
+ Collections.emptyList()));
+ MaterializeDeletionVectorsCommitChangesRewriter rewriter =
+ new MaterializeDeletionVectorsCommitChangesRewriter(
+ FileStoreTableFactory.create(store.fileIO(),
store.options().path()));
+ CommitChanges refreshed =
+ rewriter.compactChangesProvider(changes,
Collections.singletonList(message))
+ .provide(null);
+
+ // Global-index removals from earlier preparation are replaced for the
entire bucket,
+ // even if their ranges do not overlap the materialized input.
+ assertThat(refreshed.indexFiles).containsExactly(otherBucketDelete,
nonGlobalDelete);
}
private static IndexManifestEntry globalIndexDeleteEntry(
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionDeletionVectorTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionDeletionVectorTest.java
index 1ec1343ab1..2620f51746 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionDeletionVectorTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionDeletionVectorTest.java
@@ -26,6 +26,9 @@ import
org.apache.paimon.append.dataevolution.DataEvolutionCompactTaskSerializer
import
org.apache.paimon.append.dataevolution.DataEvolutionCompactionCommitPreparation;
import
org.apache.paimon.append.dataevolution.DataEvolutionDeletionVectorMaterializeCoordinator;
import org.apache.paimon.append.dataevolution.DataEvolutionRowIdReassigner;
+import
org.apache.paimon.append.dataevolution.MaterializeDeletionVectorsCommitChangesRewriter;
+import org.apache.paimon.catalog.RenamingSnapshotCommit;
+import org.apache.paimon.catalog.SnapshotCommit;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.BlobData;
@@ -39,7 +42,9 @@ import org.apache.paimon.format.blob.BlobFileFormat;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.SeekableInputStream;
import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.globalindex.DataEvolutionGlobalIndexScanner;
import org.apache.paimon.globalindex.IndexedSplit;
+import org.apache.paimon.globalindex.sorted.SortedGlobalIndexTestUtils;
import org.apache.paimon.index.DeletionVectorMeta;
import org.apache.paimon.index.GlobalIndexMeta;
import org.apache.paimon.index.IndexFileMeta;
@@ -50,7 +55,11 @@ import org.apache.paimon.manifest.FileKind;
import org.apache.paimon.manifest.IndexManifestEntry;
import org.apache.paimon.manifest.ManifestEntry;
import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.operation.Lock;
import org.apache.paimon.partition.PartitionPredicate;
+import org.apache.paimon.partition.PartitionStatistics;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaChange;
@@ -71,6 +80,7 @@ import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.Range;
import org.apache.paimon.utils.RangeHelper;
+import org.apache.paimon.utils.SnapshotManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -87,6 +97,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static
org.apache.paimon.deletionvectors.DeletionVectorsIndexFile.DELETION_VECTORS_INDEX;
@@ -895,6 +906,125 @@ public class DataEvolutionDeletionVectorTest extends
DataEvolutionTestBase {
.doesNotContainAnyElementsOf(oldAnchorFiles);
}
+ @ParameterizedTest
+ @ValueSource(strings = {"btree", "bitmap"})
+ public void testMaterializeRetryRefreshesConcurrentScalarIndexes(String
indexType)
+ throws Exception {
+ Map<String, String> retryOptions = new HashMap<>();
+ retryOptions.put(CoreOptions.COMMIT_MIN_RETRY_WAIT.key(), "0ms");
+ retryOptions.put(CoreOptions.COMMIT_MAX_RETRY_WAIT.key(), "0ms");
+ FileStoreTable original =
+
createPartitionedReassignTable("materialize_retry_index_table", false)
+ .copy(retryOptions);
+ writePartitionRows(original, "a", 0, 1, 2);
+ writePartitionRows(original, "a", 3, 4, 5);
+ commitDeletionVectors(
+ original,
+ partition(original, "a"),
+ Collections.singletonList(new DvSpec(new Range(0, 2), 1, 2)));
+ Snapshot planningSnapshot = original.latestSnapshot().get();
+ List<CommitMessage> materialized =
+ prepareMaterializeDeletionVectors(original, planningSnapshot,
null);
+ AtomicInteger attempts = new AtomicInteger();
+ List<String> untouchedIndexes = new ArrayList<>();
+ CatalogEnvironment environment =
+ new CatalogEnvironment(null, null, null, null, null, null,
false, false) {
+ @Override
+ public SnapshotCommit snapshotCommit(
+ SnapshotManager manager, CoreOptions options,
String commitUser) {
+ SnapshotCommit delegate = new
RenamingSnapshotCommit(manager, Lock.empty());
+ return new SnapshotCommit(Lock.empty()) {
+ @Override
+ protected boolean commitImpl(
+ String baseUuid,
+ Snapshot snapshot,
+ String branch,
+ List<PartitionStatistics> statistics)
+ throws Exception {
+ int attempt = attempts.incrementAndGet();
+ if (attempt == 1) {
+ List<CommitMessage> indexes = new
ArrayList<>();
+ for (DataSplit split :
+ original.newSnapshotReader()
+
.withSnapshot(planningSnapshot)
+ .read()
+ .dataSplits()) {
+ for (DataFileMeta file :
split.dataFiles()) {
+ indexes.addAll(
+
SortedGlobalIndexTestUtils.buildIndex(
+ original,
+ indexType,
+ "f0",
+
split.filterDataFile(file::equals)
+ .get(),
+
planningSnapshot.id()));
+ }
+ }
+
DataEvolutionDeletionVectorTest.commit(original, indexes);
+
assertThat(liveGlobalIndexFileNames(original)).hasSize(2);
+ for (IndexManifestEntry entry :
+
original.store().newIndexFileHandler().scanEntries()) {
+ GlobalIndexMeta meta =
entry.indexFile().globalIndexMeta();
+ if (meta != null
+ && meta.rowRange().equals(new
Range(3, 5))) {
+
untouchedIndexes.add(entry.indexFile().fileName());
+ }
+ }
+ assertThat(untouchedIndexes).hasSize(1);
+ // Move the next available row ID after
the first candidate has
+ // already assigned its output. The retry
must assign it again.
+ writePartitionRows(original, "a", 6);
+ }
+ boolean committed =
+ delegate.commit(baseUuid, snapshot,
branch, statistics);
+ assertThat(committed).isEqualTo(attempt == 2);
+ return committed;
+ }
+
+ @Override
+ protected void closeImpl() throws Exception {
+ delegate.close();
+ }
+ };
+ }
+ };
+ FileStoreTable committingTable =
+ FileStoreTableFactory.create(
+ original.fileIO(), original.location(),
original.schema(), environment);
+ commitMaterializeDeletionVectors(
+ committingTable, planningSnapshot, materialized,
"test-materialize-index-retry");
+
+ assertThat(attempts).hasValue(2);
+
assertThat(original.latestSnapshot().get().id()).isEqualTo(planningSnapshot.id()
+ 3);
+ assertThat(original.latestSnapshot().get().nextRowId()).isEqualTo(8L);
+ assertThat(readPartitionedRowsWithRowIds(original))
+ .containsExactly("a|3|3", "a|4|4", "a|5|5", "a|6|6", "a|0|7");
+ assertThat(readPartitionedRows(original))
+ .containsExactlyInAnyOrder(
+ "a|0|base-0", "a|3|base-3", "a|4|base-4",
"a|5|base-5", "a|6|base-6");
+ assertThat(liveDeletionVectorDataFileNames(original)).isEmpty();
+
assertThat(liveGlobalIndexFileNames(original)).containsExactlyElementsOf(untouchedIndexes);
+ assertThat(
+
original.store().newIndexFileHandler().scanEntries().stream()
+ .filter(entry ->
entry.indexFile().globalIndexMeta() != null)
+ .map(entry ->
entry.indexFile().globalIndexMeta().rowRange()))
+ .containsExactly(new Range(3, 5));
+ Predicate predicate = new
PredicateBuilder(original.rowType()).equal(1, 4);
+ try (DataEvolutionGlobalIndexScanner scanner =
+ DataEvolutionGlobalIndexScanner.create(
+ original, PartitionPredicate.ALWAYS_TRUE,
predicate)
+ .get()) {
+ assertThat(scanner.scan(predicate).get().results().toRangeList())
+ .containsExactly(new Range(4, 4));
+ assertThat(
+ scanner.scan(new
PredicateBuilder(original.rowType()).equal(1, 1))
+ .get()
+ .results()
+ .toRangeList())
+ .isEmpty();
+ }
+ }
+
@Test
public void testStaleMaterializeRejectedAfterConcurrentUpdate() throws
Exception {
createTableDefault();
@@ -1492,7 +1622,10 @@ public class DataEvolutionDeletionVectorTest extends
DataEvolutionTestBase {
((CommitMessageImpl) message)
.withCheckFromSnapshot(snapshot.id()))
.collect(Collectors.toList());
- commit.materializeDvRowIdCheck().commit(checkedMessages);
+ commit.materializeDvRowIdCheck()
+ .withCommitChangesRewriter(
+ new
MaterializeDeletionVectorsCommitChangesRewriter(table))
+ .commit(checkedMessages);
}
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/compact/IncrementalClusterCompact.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/compact/IncrementalClusterCompact.java
index 3a986dca07..45afd6c040 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/compact/IncrementalClusterCompact.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/compact/IncrementalClusterCompact.java
@@ -35,10 +35,10 @@ import
org.apache.paimon.flink.cluster.IncrementalClusterSplitSource;
import
org.apache.paimon.flink.cluster.RewriteIncrementalClusterCommittableOperator;
import org.apache.paimon.flink.sink.Committable;
import org.apache.paimon.flink.sink.CommittableTypeInfo;
+import org.apache.paimon.flink.sink.DataEvolutionClusterSink;
import org.apache.paimon.flink.sink.DataEvolutionCommitPreparationOperator;
import org.apache.paimon.flink.sink.DataEvolutionCompactionTaskTypeInfo;
import org.apache.paimon.flink.sink.DataEvolutionCompactionWorkerOperator;
-import org.apache.paimon.flink.sink.DataEvolutionDeletionVectorMaterializeSink;
import org.apache.paimon.flink.sink.FlinkSinkBuilder;
import org.apache.paimon.flink.sink.RowAppendTableSink;
import org.apache.paimon.flink.sorter.SortOperator;
@@ -405,8 +405,7 @@ public class IncrementalClusterCompact {
DataEvolutionCommitPreparationOperator.Factory.forClustering(
table, snapshot))
.forceNonParallel();
- new DataEvolutionDeletionVectorMaterializeSink(table, snapshot)
- .doCommit(dataStream, commitUser);
+ new DataEvolutionClusterSink(table).doCommit(dataStream,
commitUser);
} else {
sink.doCommit(dataStream, commitUser);
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/DataEvolutionClusterSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/DataEvolutionClusterSink.java
new file mode 100644
index 0000000000..a53fa4abba
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/DataEvolutionClusterSink.java
@@ -0,0 +1,49 @@
+/*
+ * 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.append.dataevolution.ClusteringCommitChangesRewriter;
+import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.TableCommitImpl;
+
+/** Sink which commits data-evolution clustering and accompanying
row-preserving compaction. */
+public class DataEvolutionClusterSink extends RowAppendTableSink {
+
+ private static final long serialVersionUID = 1L;
+
+ public DataEvolutionClusterSink(FileStoreTable table) {
+ super(table, null, null);
+ }
+
+ @Override
+ protected Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory() {
+ return context -> {
+ TableCommitImpl commit = table.newCommit(context.commitUser());
+ commit.materializeDvRowIdCheck();
+ commit.withCommitChangesRewriter(new
ClusteringCommitChangesRewriter(table));
+ return new StoreCommitter(table, commit, context);
+ };
+ }
+
+ @Override
+ protected CommittableStateManager<ManifestCommittable>
createCommittableStateManager() {
+ return new NoopCommittableStateManager();
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/DataEvolutionDeletionVectorMaterializeSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/DataEvolutionDeletionVectorMaterializeSink.java
index 3ca51a69d5..553989ac38 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/DataEvolutionDeletionVectorMaterializeSink.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/DataEvolutionDeletionVectorMaterializeSink.java
@@ -20,6 +20,7 @@ package org.apache.paimon.flink.sink;
import org.apache.paimon.Snapshot;
import org.apache.paimon.append.dataevolution.DataEvolutionCompactTask;
+import
org.apache.paimon.append.dataevolution.MaterializeDeletionVectorsCommitChangesRewriter;
import org.apache.paimon.manifest.ManifestCommittable;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.sink.TableCommitImpl;
@@ -75,6 +76,8 @@ public class DataEvolutionDeletionVectorMaterializeSink
return context -> {
TableCommitImpl commit = table.newCommit(context.commitUser());
commit.materializeDvRowIdCheck();
+ commit.withCommitChangesRewriter(
+ new
MaterializeDeletionVectorsCommitChangesRewriter(table));
return new StoreCommitter(table, commit, context);
};
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
index c69c789361..fbed108b36 100644
---
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
@@ -25,6 +25,7 @@ import org.apache.paimon.append.AppendCompactCoordinator;
import org.apache.paimon.append.AppendCompactTask;
import org.apache.paimon.append.cluster.DataEvolutionClusterCompaction;
import org.apache.paimon.append.cluster.IncrementalClusterManager;
+import org.apache.paimon.append.dataevolution.ClusteringCommitChangesRewriter;
import org.apache.paimon.append.dataevolution.DataEvolutionCompactCoordinator;
import org.apache.paimon.append.dataevolution.DataEvolutionCompactTask;
import org.apache.paimon.compact.CompactUnit;
@@ -987,6 +988,7 @@ public class CompactProcedure extends BaseProcedure {
if (table.coreOptions().dataEvolutionEnabled()) {
String commitUser =
createCommitUser(table.coreOptions().toConfiguration());
try (TableCommitImpl commit =
table.newCommit(commitUser).materializeDvRowIdCheck()) {
+ commit.withCommitChangesRewriter(new
ClusteringCommitChangesRewriter(table));
clusterMessages.addAll(
DataEvolutionRewriteExecutor.executeTasks(
table,
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/MaterializeDeletionVectorsProcedure.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/MaterializeDeletionVectorsProcedure.java
index 7377d8fb6e..c742a673bf 100644
---
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/MaterializeDeletionVectorsProcedure.java
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/MaterializeDeletionVectorsProcedure.java
@@ -22,6 +22,7 @@ import org.apache.paimon.CoreOptions;
import org.apache.paimon.Snapshot;
import org.apache.paimon.append.dataevolution.DataEvolutionCompactTask;
import
org.apache.paimon.append.dataevolution.DataEvolutionDeletionVectorMaterializeCoordinator;
+import
org.apache.paimon.append.dataevolution.MaterializeDeletionVectorsCommitChangesRewriter;
import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.spark.utils.SparkProcedureUtils;
import org.apache.paimon.table.BucketMode;
@@ -176,6 +177,8 @@ public class MaterializeDeletionVectorsProcedure extends
BaseProcedure {
@Override
public void configure(TableCommitImpl commit) {
commit.materializeDvRowIdCheck();
+ commit.withCommitChangesRewriter(
+ new
MaterializeDeletionVectorsCommitChangesRewriter(table));
}
@Override