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 b1f678b6d6 [core] Do not count inherited index entries in the
strict-mode partition check (#10218)
b1f678b6d6 is described below
commit b1f678b6d67519781deb2ca23a287febb012ae64
Author: YangJie <[email protected]>
AuthorDate: Mon Oct 5 10:35:38 2026 -0400
[core] Do not count inherited index entries in the strict-mode partition
check (#10218)
---
.../paimon/operation/commit/StrictModeChecker.java | 38 ++++-
.../apache/paimon/table/sink/TableCommitTest.java | 164 +++++++++++++++++++++
2 files changed, 194 insertions(+), 8 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/commit/StrictModeChecker.java
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/StrictModeChecker.java
index dc93bf8dc9..69fcef0bc9 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/commit/StrictModeChecker.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/StrictModeChecker.java
@@ -29,9 +29,12 @@ import org.apache.paimon.operation.FileStoreScan;
import org.apache.paimon.table.source.ScanMode;
import org.apache.paimon.utils.SnapshotManager;
+import javax.annotation.Nullable;
+
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
@@ -139,23 +142,42 @@ public class StrictModeChecker {
return false;
}
String indexManifest = snapshot.indexManifest();
- if (indexManifest == null) {
- return false;
- }
// Fast exit: if this snapshot's indexManifest file name equals the
// previous snapshot's, no index file was added/removed by this commit
// (writeIndexFiles reuses the previous file when newIndexFiles is
empty).
+ String prevIndexManifest = null;
long prevId = snapshot.id() - 1;
- if (snapshotManager.snapshotExists(prevId)
- &&
indexManifest.equals(snapshotManager.snapshot(prevId).indexManifest())) {
+ if (snapshotManager.snapshotExists(prevId)) {
+ prevIndexManifest =
snapshotManager.snapshot(prevId).indexManifest();
+ }
+ if (Objects.equals(indexManifest, prevIndexManifest)) {
return false;
}
- for (IndexManifestEntry entry : indexManifestFile.read(indexManifest))
{
- if (newPartitions.contains(entry.partition())) {
+ // Count only the index entries this commit added, replaced or removed
relative to
+ // the previous snapshot (every entry if that snapshot is gone).
Removals matter
+ // because an OVERWRITE such as rollbackToAsLatest can drop a DV entry
without any
+ // data-file delta.
+ Set<IndexManifestEntry> previousEntries =
+ readEntriesInPartitions(prevIndexManifest, newPartitions);
+ for (IndexManifestEntry entry : readEntriesInPartitions(indexManifest,
newPartitions)) {
+ if (!previousEntries.remove(entry)) {
return true;
}
}
- return false;
+ return !previousEntries.isEmpty();
+ }
+
+ private Set<IndexManifestEntry> readEntriesInPartitions(
+ @Nullable String indexManifest, Set<BinaryRow> partitions) {
+ Set<IndexManifestEntry> entries = new HashSet<>();
+ if (indexManifest != null) {
+ for (IndexManifestEntry entry :
indexManifestFile.read(indexManifest)) {
+ if (partitions.contains(entry.partition())) {
+ entries.add(entry);
+ }
+ }
+ }
+ return entries;
}
private boolean hasOverlappedPartition(
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/sink/TableCommitTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/sink/TableCommitTest.java
index 00eb30d2f9..d9652ccf68 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/sink/TableCommitTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/sink/TableCommitTest.java
@@ -19,10 +19,12 @@
package org.apache.paimon.table.sink;
import org.apache.paimon.CoreOptions;
+import org.apache.paimon.Snapshot;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.BinaryRowWriter;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.deletionvectors.BucketedDvMaintainer;
+import org.apache.paimon.disk.IOManagerImpl;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.local.LocalFileIO;
import org.apache.paimon.index.GlobalIndexMeta;
@@ -33,6 +35,7 @@ import org.apache.paimon.io.DataFileMeta;
import org.apache.paimon.io.DataFilePathFactory;
import org.apache.paimon.io.DataIncrement;
import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.operation.FileStoreCommit;
import org.apache.paimon.options.Options;
import org.apache.paimon.schema.FileSystemSchemaManager;
import org.apache.paimon.schema.Schema;
@@ -42,6 +45,7 @@ import org.apache.paimon.stats.SimpleStats;
import org.apache.paimon.table.CatalogEnvironment;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.FileStoreTableFactory;
+import org.apache.paimon.table.source.ScanMode;
import org.apache.paimon.types.DataType;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowType;
@@ -724,6 +728,166 @@ public class TableCommitTest {
commit2.close();
}
+ @Test
+ public void testStrictModeIgnoresDeletionVectorsInheritedByCompaction()
throws Exception {
+ // A streaming upsert job on a primary-key table with deletion vectors
keeps compacting
+ // pt=2 while a job pinned to the snapshot it started from (e.g.
rescale) writes pt=1.
+ FileStoreTable table = createPkDvTable();
+ String streamingUser = UUID.randomUUID().toString();
+ try (TableWriteImpl<?> write =
+ table.newWrite(streamingUser)
+ .withIOManager(new
IOManagerImpl(tempDir.toString()));
+ TableCommitImpl commit = table.newCommit(streamingUser)) {
+ for (int k = 0; k < 3; k++) {
+ write.write(GenericRow.of(1, k, 0L));
+ write.write(GenericRow.of(2, k, 0L));
+ }
+ commit.commit(0, write.prepareCommit(true, 0));
+ // updating a key of pt=1 gives pt=1 a deletion vector before the
pinned job starts
+ write.write(GenericRow.of(1, 0, 1L));
+ commit.commit(1, write.prepareCommit(true, 1));
+ long jobStart = table.snapshotManager().latestSnapshotId();
+
+ // the compaction of this pt=2 update changes only pt=2's deletion
vectors, while
+ // its index manifest still lists pt=1's
+ write.write(GenericRow.of(2, 0, 2L));
+ commit.commit(2, write.prepareCommit(true, 2));
+ assertThat(table.snapshotManager().latestSnapshot().commitKind())
+ .isEqualTo(Snapshot.CommitKind.COMPACT);
+
+ FileStoreTable pinned =
+ table.copy(
+ singletonMap(
+ COMMIT_LAST_SAFE_SNAPSHOT.key(),
String.valueOf(jobStart)));
+ String pinnedUser = UUID.randomUUID().toString();
+ try (TableWriteImpl<?> pinnedWrite =
+ pinned.newWrite(pinnedUser)
+ .withIOManager(new
IOManagerImpl(tempDir.toString()));
+ TableCommitImpl pinnedCommit =
pinned.newCommit(pinnedUser)) {
+ pinnedWrite.write(GenericRow.of(1, 9, 9L));
+ assertThatCode(() -> pinnedCommit.commit(0,
pinnedWrite.prepareCommit(false, 0)))
+ .doesNotThrowAnyException();
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testStrictModeDetectsIndexRemovalWithoutDataDelta(boolean
targetHasIndexManifest)
+ throws Exception {
+ // Rolling back a DV-only commit is an OVERWRITE that drops the DV
entry without any
+ // data-file delta, so the index check must also flag partitions whose
entries the
+ // checked snapshot removed, including when its index manifest becomes
null.
+ FileStoreTable table = createDvAppendTable();
+ String user1 = UUID.randomUUID().toString();
+ commitRow(table, user1, GenericRow.of(1, 0, 0L), GenericRow.of(2, 0,
0L));
+ long commitId = 2;
+ if (targetHasIndexManifest) {
+ commitDvOnly(table, user1, partitionRow(2), commitId++);
+ }
+ SnapshotManager snapshotManager = table.snapshotManager();
+ Snapshot target = snapshotManager.latestSnapshot();
+ commitDvOnly(table, user1, partitionRow(1), commitId);
+ long lastSafe = snapshotManager.latestSnapshotId();
+
+ try (FileStoreCommit commit = table.store().newCommit(user1, table)) {
+ assertThat(commit.rollbackToAsLatest(target)).isTrue();
+ }
+ Snapshot rollback = snapshotManager.latestSnapshot();
+ assertThat(rollback.indexManifest() !=
null).isEqualTo(targetHasIndexManifest);
+ assertThat(
+ table.store()
+ .newScan()
+ .withSnapshot(rollback)
+ .withKind(ScanMode.DELTA)
+ .plan()
+ .files())
+ .isEmpty();
+
+ FileStoreTable tableWithStrict =
+ table.copy(singletonMap(COMMIT_LAST_SAFE_SNAPSHOT.key(),
String.valueOf(lastSafe)));
+ // pt=2's index entries are untouched by the rollback -> no error
+ assertThatCode(
+ () ->
+ commitRow(
+ tableWithStrict,
+ UUID.randomUUID().toString(),
+ GenericRow.of(2, 1, 1L)))
+ .doesNotThrowAnyException();
+ // the rollback removed pt=1's DV entry -> must throw
+ assertThatThrownBy(
+ () ->
+ commitRow(
+ tableWithStrict,
+ UUID.randomUUID().toString(),
+ GenericRow.of(1, 1, 1L)))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("found a OVERWRITE snapshot (id: " +
rollback.id() + ")")
+ .hasMessageContaining(
+ "Giving up committing as commit.strict-mode.enabled is
true.");
+ }
+
+ private FileStoreTable createPkDvTable() throws Exception {
+ String path = tempDir.toString();
+ RowType rowType =
+ RowType.of(
+ new DataType[] {DataTypes.INT(), DataTypes.INT(),
DataTypes.BIGINT()},
+ new String[] {"pt", "k", "v"});
+
+ Options options = new Options();
+ options.set(CoreOptions.PATH, path);
+ options.set(CoreOptions.BUCKET, 1);
+ options.set(CoreOptions.DELETION_VECTORS_ENABLED, true);
+ TableSchema tableSchema =
+ SchemaUtils.forceCommit(
+ new FileSystemSchemaManager(LocalFileIO.create(), new
Path(path)),
+ new Schema(
+ rowType.getFields(),
+ Collections.singletonList("pt"),
+ Arrays.asList("pt", "k"),
+ options.toMap(),
+ ""));
+ return FileStoreTableFactory.create(
+ LocalFileIO.create(), new Path(path), tableSchema,
CatalogEnvironment.empty());
+ }
+
+ private FileStoreTable createDvAppendTable() throws Exception {
+ String path = tempDir.toString();
+ RowType rowType =
+ RowType.of(
+ new DataType[] {DataTypes.INT(), DataTypes.INT(),
DataTypes.BIGINT()},
+ new String[] {"pt", "k", "v"});
+
+ Options options = new Options();
+ options.set(CoreOptions.PATH, path);
+ options.set(CoreOptions.BUCKET, 1);
+ options.set(CoreOptions.BUCKET_KEY, "k");
+ options.set(CoreOptions.NUM_SORTED_RUNS_COMPACTION_TRIGGER, 10);
+ options.set(CoreOptions.DELETION_VECTORS_ENABLED, true);
+ TableSchema tableSchema =
+ SchemaUtils.forceCommit(
+ new FileSystemSchemaManager(LocalFileIO.create(), new
Path(path)),
+ new Schema(
+ rowType.getFields(),
+ Collections.singletonList("pt"),
+ Collections.emptyList(),
+ options.toMap(),
+ ""));
+ return FileStoreTableFactory.create(
+ LocalFileIO.create(), new Path(path), tableSchema,
CatalogEnvironment.empty());
+ }
+
+ private static void commitRow(FileStoreTable table, String user,
GenericRow... rows)
+ throws Exception {
+ try (TableWriteImpl<?> write = table.newWrite(user);
+ TableCommitImpl commit = table.newCommit(user)) {
+ for (GenericRow row : rows) {
+ write.write(row);
+ }
+ commit.commit(1, write.prepareCommit(false, 1));
+ }
+ }
+
@Test
public void testStrictModeShortCircuitsWhenIndexManifestUnchanged() throws
Exception {
String path = tempDir.toString();