This is an automated email from the ASF dual-hosted git repository.
leaves12138 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 1886d2f4e2 [core] Improve row id reassign retry reuse (#8506)
1886d2f4e2 is described below
commit 1886d2f4e297cbb3e2f72b041387037488aebba7
Author: YeJunHao <[email protected]>
AuthorDate: Sat Jul 11 13:05:59 2026 +0800
[core] Improve row id reassign retry reuse (#8506)
---
.../DataEvolutionRowIdReassigner.java | 744 +++++++-----
.../append/dataevolution/RowRangeMappingIndex.java | 20 +
.../DataEvolutionRowIdReassignerTest.java | 1198 +++++++++++++++++++-
.../dataevolution/RowRangeMappingIndexTest.java | 29 +
4 files changed, 1689 insertions(+), 302 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
index b7261639b5..d81d98ed70 100644
---
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
+++
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
@@ -20,6 +20,7 @@ package org.apache.paimon.append.dataevolution;
import org.apache.paimon.CoreOptions;
import org.apache.paimon.Snapshot;
+import org.apache.paimon.annotation.VisibleForTesting;
import org.apache.paimon.codegen.CodeGenUtils;
import org.apache.paimon.codegen.RecordComparator;
import org.apache.paimon.data.BinaryRow;
@@ -39,6 +40,7 @@ import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.stats.SimpleStats;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.SpecialFields;
+import org.apache.paimon.utils.Filter;
import org.apache.paimon.utils.Pair;
import org.apache.paimon.utils.Range;
import org.apache.paimon.utils.RangeHelper;
@@ -69,9 +71,11 @@ public class DataEvolutionRowIdReassigner {
private static final Logger LOG =
LoggerFactory.getLogger(DataEvolutionRowIdReassigner.class);
private static final String COMMIT_USER_PREFIX = "reassign-row-id";
+ private static final int MAX_COMMIT_ATTEMPTS = 3;
private final FileStoreTable table;
private final @Nullable PartitionPredicate partitionPredicate;
+ private final Runnable beforeCommit;
public DataEvolutionRowIdReassigner(FileStoreTable table) {
this(table, null);
@@ -79,8 +83,17 @@ public class DataEvolutionRowIdReassigner {
public DataEvolutionRowIdReassigner(
FileStoreTable table, @Nullable PartitionPredicate
partitionPredicate) {
+ this(table, partitionPredicate, () -> {});
+ }
+
+ @VisibleForTesting
+ DataEvolutionRowIdReassigner(
+ FileStoreTable table,
+ @Nullable PartitionPredicate partitionPredicate,
+ Runnable beforeCommit) {
this.table = table;
this.partitionPredicate = partitionPredicate;
+ this.beforeCommit = beforeCommit;
}
public Result reassign() {
@@ -116,86 +129,77 @@ public class DataEvolutionRowIdReassigner {
ManifestFile manifestFile =
table.store().manifestFileFactory().create();
ManifestList manifestList =
table.store().manifestListFactory().create();
- List<ManifestFileMeta> manifestMetas =
manifestList.readDataManifests(latest);
- AssignmentPlan assignment = planAssignment(manifestMetas,
manifestFile, nextRowId);
- if (!assignment.hasCurrentFiles) {
- return Result.skipped(
- latest.id(),
- nextRowId,
- partitionFilterEnabled()
- ? "partition filter matches no current files"
- : "table has no current files");
- }
- if (assignment.reassignedFileCount == 0) {
+ Optional<AssignmentPlan> optionalPlan =
+ planAssignment(manifestList.readDataManifests(latest),
manifestFile);
+ if (!optionalPlan.isPresent()) {
LOG.info(
- "Skip reassigning row IDs for table {} because partition
row IDs are already contiguous.",
+ "Skip reassigning row IDs for table {} because no
partition requires reassignment.",
table.name());
return Result.skipped(
- latest.id(), nextRowId, "partition row IDs are already
contiguous");
+ latest.id(), nextRowId, "no partition requires row-id
reassignment");
}
+ AssignmentPlan assignmentPlan = optionalPlan.get();
+
+ for (int attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt++) {
+ Assignment assignment = assignmentPlan.createAssignment(latest);
+ CommitAssignmentResult commitResult =
+ commitAssignment(assignment, manifestFile, manifestList,
commitUser);
+ if (commitResult.success) {
+ LOG.info(
+ "Reassigned row IDs for table {} from {} to {},
partitions={}, files={}, rows={}.",
+ table.name(),
+ assignment.firstAssignedRowId,
+ assignment.nextRowId,
+ assignment.rowIdMappings.size(),
+ commitResult.fileCount,
+ assignment.logicalRowCount());
+ return new Result(
+ assignment.snapshot.id(),
+ assignment.snapshot.id() + 1,
+ commitResult.fileCount,
+ assignment.logicalRowCount(),
+ commitResult.indexFileCount,
+ assignment.firstAssignedRowId,
+ assignment.nextRowId);
+ }
- Pair<String, Long> baseManifestList =
- writeBaseManifestList(
- manifestMetas, assignment.rewrittenManifestMetas,
manifestList);
- Pair<String, Long> deltaManifestList =
manifestList.write(Collections.emptyList());
-
- RewrittenIndexManifest rewrittenIndexManifest =
rewriteIndexManifest(latest, assignment);
-
- try (FileStoreCommitImpl commit =
- (FileStoreCommitImpl) table.store().newCommit(commitUser,
table)) {
- boolean success =
- commit.replaceManifestList(
- latest,
- latest.totalRecordCount(),
- baseManifestList,
- deltaManifestList,
- rewrittenIndexManifest.indexManifest,
- assignment.nextRowId);
- if (!success) {
+ if (attempt == MAX_COMMIT_ATTEMPTS) {
throw new RuntimeException(
"Failed to reassign row IDs because a newer snapshot
has been committed.");
}
+
+ Snapshot newLatest = table.snapshotManager().latestSnapshot();
+ checkState(newLatest != null, "Latest snapshot disappeared while
reassigning row IDs.");
+ assignmentPlan =
+ advanceAssignmentPlan(
+ assignmentPlan, latest, newLatest, manifestFile,
manifestList);
+ LOG.info(
+ "Failed to commit row-id reassignment for table {} based
on snapshot {} because snapshot {} has been committed. Retrying {}/{} with the
updated assignment plan.",
+ table.name(),
+ latest.id(),
+ newLatest.id(),
+ attempt + 1,
+ MAX_COMMIT_ATTEMPTS);
+ latest = newLatest;
}
- LOG.info(
- "Reassigned row IDs for table {} from {} to {}, partitions={},
files={}, rows={}.",
- table.name(),
- nextRowId,
- assignment.nextRowId,
- assignment.rowIdMappings.size(),
- assignment.reassignedFileCount,
- assignment.logicalRowCount);
- return new Result(
- latest.id(),
- latest.id() + 1,
- assignment.reassignedFileCount,
- assignment.logicalRowCount,
- rewrittenIndexManifest.indexFileCount,
- nextRowId,
- assignment.nextRowId);
+ throw new IllegalStateException("Unreachable retry state while
reassigning row IDs.");
}
- private AssignmentPlan planAssignment(
- List<ManifestFileMeta> manifestMetas, ManifestFile manifestFile,
long firstRowId) {
+ private Optional<AssignmentPlan> planAssignment(
+ List<ManifestFileMeta> manifestMetas, ManifestFile manifestFile) {
List<List<ManifestFileMeta>> manifestGroups =
manifestGroupsByPartition(manifestMetas);
- Map<String, List<ManifestFileMeta>> rewrittenManifestMetas = new
HashMap<>();
- Map<BinaryRow, RowRangeMappingIndex> rowIdMappings = new
LinkedHashMap<>();
- long nextRowId = firstRowId;
- long logicalRowCount = 0;
- long reassignedFileCount = 0;
- boolean hasCurrentFiles = false;
+ Map<BinaryRow, List<ManifestEntry>> entriesToReassignByPartition = new
LinkedHashMap<>();
for (List<ManifestFileMeta> manifestGroup : manifestGroups) {
if (skipManifestGroupByPartitionFilter(manifestGroup)) {
continue;
}
- CurrentManifest currentManifest = currentManifest(manifestGroup,
manifestFile);
- List<ManifestEntry> currentEntries = currentManifest.entries();
+ List<ManifestEntry> currentEntries = currentEntries(manifestGroup,
manifestFile);
if (currentEntries.isEmpty()) {
continue;
}
- hasCurrentFiles = true;
Map<BinaryRow, List<ManifestEntry>> entriesByPartition =
entriesByPartition(currentEntries);
@@ -204,42 +208,26 @@ public class DataEvolutionRowIdReassigner {
continue;
}
- Assignment groupAssignment =
- assign(entriesByPartition, partitionsToReassign,
nextRowId);
- nextRowId = groupAssignment.nextRowId;
- logicalRowCount += groupAssignment.logicalRowCount;
- reassignedFileCount += groupAssignment.reassignedFileCount;
- for (Map.Entry<BinaryRow, RowRangeMappingIndex> mapping :
- groupAssignment.rowIdMappings.entrySet()) {
- RowRangeMappingIndex previous =
- rowIdMappings.put(mapping.getKey(),
mapping.getValue());
- checkState(
- previous == null,
- "Partition %s appears in multiple manifest groups.",
-
table.store().pathFactory().getPartitionString(mapping.getKey()));
- }
-
- Map<String, List<ManifestFileMeta>> groupRewrittenManifestMetas =
- writeManifestReplacements(
- currentManifest, groupAssignment,
partitionsToReassign, manifestFile);
- for (Map.Entry<String, List<ManifestFileMeta>> rewritten :
- groupRewrittenManifestMetas.entrySet()) {
- List<ManifestFileMeta> previous =
- rewrittenManifestMetas.put(rewritten.getKey(),
rewritten.getValue());
- checkState(
- previous == null,
- "Manifest file %s appears in multiple manifest
groups.",
- rewritten.getKey());
+ for (ManifestEntry entry : currentEntries) {
+ if (partitionsToReassign.contains(entry.partition())) {
+ entriesToReassignByPartition
+ .computeIfAbsent(entry.partition(), ignored -> new
ArrayList<>())
+ .add(entry);
+ }
}
}
- return new AssignmentPlan(
- rewrittenManifestMetas,
- rowIdMappings,
- nextRowId,
- logicalRowCount,
- reassignedFileCount,
- hasCurrentFiles);
+ if (entriesToReassignByPartition.isEmpty()) {
+ return Optional.empty();
+ }
+
+ RelativeRowIdMappings relativeRowIdMappings =
+ createRelativeRowIdMappings(entriesToReassignByPartition);
+ return Optional.of(
+ new AssignmentPlan(
+ findManifestMetasToRewrite(
+ manifestMetas, relativeRowIdMappings,
manifestFile),
+ relativeRowIdMappings));
}
private List<List<ManifestFileMeta>> manifestGroupsByPartition(
@@ -272,6 +260,7 @@ public class DataEvolutionRowIdReassigner {
manifestMeta,
manifestMeta.partitionStats().minValues(),
manifestMeta.partitionStats().maxValues(),
+ containsNullPartition(manifestMeta,
partitionFieldCount),
i));
}
Collections.sort(
@@ -304,6 +293,32 @@ public class DataEvolutionRowIdReassigner {
}
groupedManifestRanges.add(currentGroup);
+ // Partition min/max excludes nulls, so null-bearing ranges need an
extra shared group.
+ List<PartitionManifestRange> nullPartitionGroup = new ArrayList<>();
+ int nullPartitionGroupIndex = -1;
+ for (int i = 0; i < groupedManifestRanges.size(); ) {
+ List<PartitionManifestRange> group = groupedManifestRanges.get(i);
+ boolean containsNullPartition = false;
+ for (PartitionManifestRange range : group) {
+ if (range.containsNullPartition) {
+ containsNullPartition = true;
+ break;
+ }
+ }
+ if (containsNullPartition) {
+ if (nullPartitionGroupIndex < 0) {
+ nullPartitionGroupIndex = i;
+ }
+ nullPartitionGroup.addAll(group);
+ groupedManifestRanges.remove(i);
+ } else {
+ i++;
+ }
+ }
+ if (!nullPartitionGroup.isEmpty()) {
+ groupedManifestRanges.add(nullPartitionGroupIndex,
nullPartitionGroup);
+ }
+
List<List<ManifestFileMeta>> groups = new ArrayList<>();
for (List<PartitionManifestRange> group : groupedManifestRanges) {
Collections.sort(group, Comparator.comparingInt(left ->
left.originalIndex));
@@ -347,27 +362,35 @@ public class DataEvolutionRowIdReassigner {
&& partitionStats.nullCounts().size() == partitionFieldCount;
}
- private CurrentManifest currentManifest(
+ private boolean containsNullPartition(ManifestFileMeta manifestMeta, int
partitionFieldCount) {
+ for (int i = 0; i < partitionFieldCount; i++) {
+ if (manifestMeta.partitionStats().nullCounts().getLong(i) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<ManifestEntry> currentEntries(
List<ManifestFileMeta> manifestMetas, ManifestFile manifestFile) {
Set<FileEntry.Identifier> deletedIdentifiers =
deletedIdentifiers(manifestFile, manifestMetas);
- List<SourcedManifestEntry> currentEntries = new ArrayList<>();
+ List<ManifestEntry> currentEntries = new ArrayList<>();
for (ManifestFileMeta manifestMeta : manifestMetas) {
if (manifestMeta.numAddedFiles() <= 0) {
continue;
}
- List<ManifestEntry> entries =
- manifestFile.read(manifestMeta.fileName(),
manifestMeta.fileSize());
+ List<ManifestEntry> entries =
readPlanningManifestEntries(manifestFile, manifestMeta);
for (ManifestEntry entry : entries) {
if (entry.kind() == FileKind.ADD
&& partitionIncluded(entry.partition())
&& !deletedIdentifiers.contains(entry.identifier())) {
- currentEntries.add(new SourcedManifestEntry(manifestMeta,
entry));
+ currentEntries.add(entry);
}
}
}
- return new CurrentManifest(manifestMetas, currentEntries);
+ return currentEntries;
}
private Set<FileEntry.Identifier> deletedIdentifiers(
@@ -377,8 +400,7 @@ public class DataEvolutionRowIdReassigner {
if (manifestMeta.numDeletedFiles() <= 0) {
continue;
}
- List<ManifestEntry> entries =
- manifestFile.read(manifestMeta.fileName(),
manifestMeta.fileSize());
+ List<ManifestEntry> entries =
readPlanningManifestEntries(manifestFile, manifestMeta);
for (ManifestEntry entry : entries) {
if (entry.kind() == FileKind.DELETE &&
partitionIncluded(entry.partition())) {
deletedIdentifiers.add(entry.identifier());
@@ -396,6 +418,184 @@ public class DataEvolutionRowIdReassigner {
return partitionPredicate != null;
}
+ private CommitAssignmentResult commitAssignment(
+ Assignment assignment,
+ ManifestFile manifestFile,
+ ManifestList manifestList,
+ String commitUser) {
+ RewrittenDataManifests rewrittenDataManifests =
+ writeManifestReplacements(assignment, manifestFile);
+ Pair<String, Long> baseManifestList =
+ writeBaseManifestList(
+ manifestList.readDataManifests(assignment.snapshot),
+ rewrittenDataManifests.manifestMetas,
+ manifestList);
+ Pair<String, Long> deltaManifestList =
manifestList.write(Collections.emptyList());
+ RewrittenIndexManifest rewrittenIndexManifest =
rewriteIndexManifest(assignment);
+
+ boolean success;
+ try (FileStoreCommitImpl commit =
+ (FileStoreCommitImpl) table.store().newCommit(commitUser,
table)) {
+ beforeCommit.run();
+ success =
+ commit.replaceManifestList(
+ assignment.snapshot,
+ assignment.snapshot.totalRecordCount(),
+ baseManifestList,
+ deltaManifestList,
+ rewrittenIndexManifest.indexManifest,
+ assignment.nextRowId);
+ }
+ return new CommitAssignmentResult(
+ success, rewrittenDataManifests.fileCount,
rewrittenIndexManifest.indexFileCount);
+ }
+
+ private AssignmentPlan advanceAssignmentPlan(
+ AssignmentPlan assignmentPlan,
+ Snapshot previous,
+ Snapshot latest,
+ ManifestFile manifestFile,
+ ManifestList manifestList) {
+ checkState(
+ latest.id() > previous.id(),
+ "Cannot advance row-id assignment from snapshot %s to %s.",
+ previous.id(),
+ latest.id());
+
+ Map<String, ManifestFileMeta> manifestMetasToRewrite = new
LinkedHashMap<>();
+ for (ManifestFileMeta manifestMeta :
assignmentPlan.manifestMetasToRewrite) {
+ manifestMetasToRewrite.put(manifestMeta.fileName(), manifestMeta);
+ }
+ for (long id = previous.id() + 1; id <= latest.id(); id++) {
+ Snapshot snapshot;
+ try {
+ snapshot = table.snapshotManager().tryGetSnapshot(id);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ String.format(
+ "Abort row-id reassignment because snapshot %s
cannot be read.",
+ id),
+ e);
+ }
+
+ if (snapshot.commitKind() == Snapshot.CommitKind.COMPACT
+ || snapshot.commitKind() == Snapshot.CommitKind.OVERWRITE)
{
+ throw new RuntimeException(
+ String.format(
+ "Abort row-id reassignment because %s snapshot
%s was committed after snapshot %s.",
+ snapshot.commitKind(), snapshot.id(),
previous.id()));
+ }
+ if (snapshot.commitKind() == Snapshot.CommitKind.ANALYZE) {
+ continue;
+ }
+ checkState(
+ snapshot.commitKind() == Snapshot.CommitKind.APPEND,
+ "Unsupported snapshot kind %s while advancing row-id
assignment.",
+ snapshot.commitKind());
+
+ for (ManifestFileMeta manifestMeta :
manifestList.readDeltaManifests(snapshot)) {
+ boolean needsReassign = false;
+ for (ManifestEntry entry :
+ readPlanningManifestEntries(manifestFile,
manifestMeta)) {
+ checkState(
+ entry.kind() == FileKind.ADD,
+ "APPEND snapshot %s contains non-ADD manifest
entry %s.",
+ snapshot.id(),
+ entry);
+ if (appendedEntryNeedsReassign(assignmentPlan, entry)) {
+ needsReassign = true;
+ }
+ }
+ if (needsReassign) {
+ manifestMetasToRewrite.put(manifestMeta.fileName(),
manifestMeta);
+ }
+ }
+ }
+
+ List<ManifestFileMeta> latestManifestMetas =
manifestList.readDataManifests(latest);
+ Set<String> latestManifestFiles = new HashSet<>();
+ for (ManifestFileMeta manifestMeta : latestManifestMetas) {
+ latestManifestFiles.add(manifestMeta.fileName());
+ }
+ for (String plannedManifestFile : manifestMetasToRewrite.keySet()) {
+ checkState(
+ latestManifestFiles.contains(plannedManifestFile),
+ "Cannot advance row-id assignment because planned manifest
%s no longer exists after APPEND manifest merge.",
+ plannedManifestFile);
+ }
+ return new AssignmentPlan(
+ new ArrayList<>(manifestMetasToRewrite.values()),
+ assignmentPlan.relativeRowIdMappings);
+ }
+
+ private boolean appendedEntryNeedsReassign(
+ AssignmentPlan assignmentPlan, ManifestEntry appendedEntry) {
+ RowRangeMappingIndex mapping =
+
assignmentPlan.relativeRowIdMappings.mappings.get(appendedEntry.partition());
+ if (mapping == null) {
+ return false;
+ }
+
+ Range appendedRange = appendedEntry.file().nonNullRowIdRange();
+ if (mapping.map(appendedRange).isPresent()) {
+ return true;
+ }
+
+ checkState(
+ !mapping.overlaps(appendedRange),
+ "Cannot advance row-id assignment because appended row-id
range %s partially overlaps planned ranges in partition %s.",
+ appendedRange,
+ appendedEntry.partition());
+ return false;
+ }
+
+ private boolean manifestGroupMayContainPartitions(
+ List<ManifestFileMeta> manifestGroup, PartitionPredicate
effectivePartitionPredicate) {
+ int partitionFieldCount =
table.schema().logicalPartitionType().getFieldCount();
+ for (ManifestFileMeta manifestMeta : manifestGroup) {
+ if (!containsPartitionStats(manifestMeta, partitionFieldCount)) {
+ return true;
+ }
+
+ SimpleStats partitionStats = manifestMeta.partitionStats();
+ if (effectivePartitionPredicate.test(
+ manifestMeta.numAddedFiles() +
manifestMeta.numDeletedFiles(),
+ partitionStats.minValues(),
+ partitionStats.maxValues(),
+ partitionStats.nullCounts())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<ManifestFileMeta> findManifestMetasToRewrite(
+ List<ManifestFileMeta> manifestMetas,
+ RelativeRowIdMappings relativeRowIdMappings,
+ ManifestFile manifestFile) {
+ PartitionPredicate plannedPartitionPredicate =
+ PartitionPredicate.fromMultiple(
+ table.schema().logicalPartitionType(),
+ relativeRowIdMappings.mappings.keySet());
+ checkState(plannedPartitionPredicate != null, "Planned partition
predicate is null.");
+ List<ManifestFileMeta> result = new ArrayList<>();
+ for (ManifestFileMeta manifestMeta : manifestMetas) {
+ if (!manifestGroupMayContainPartitions(
+ Collections.singletonList(manifestMeta),
plannedPartitionPredicate)) {
+ continue;
+ }
+ for (ManifestEntry entry :
readPlanningManifestEntries(manifestFile, manifestMeta)) {
+ RowRangeMappingIndex mapping =
+ relativeRowIdMappings.mappings.get(entry.partition());
+ if (mapping != null &&
mapping.map(entry.file().nonNullRowIdRange()).isPresent()) {
+ result.add(manifestMeta);
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
private Pair<String, Long> writeBaseManifestList(
List<ManifestFileMeta> manifestMetas,
Map<String, List<ManifestFileMeta>> rewrittenManifestMetas,
@@ -413,77 +613,48 @@ public class DataEvolutionRowIdReassigner {
return manifestList.write(baseManifestMetas);
}
- private Map<String, List<ManifestFileMeta>> writeManifestReplacements(
- CurrentManifest currentManifest,
- Assignment assignment,
- Set<BinaryRow> partitionsToReassign,
- ManifestFile manifestFile) {
- Set<String> manifestsToRewrite =
- manifestsToRewrite(currentManifest.currentEntries,
partitionsToReassign);
- Map<FileEntry.Identifier, ManifestEntry> reassignedEntries =
- entriesByIdentifier(assignment.entries);
-
+ private RewrittenDataManifests writeManifestReplacements(
+ Assignment assignment, ManifestFile manifestFile) {
Map<String, List<ManifestFileMeta>> rewrittenManifestMetas = new
HashMap<>();
- for (ManifestFileMeta manifestMeta : currentManifest.manifestMetas) {
- if (!manifestsToRewrite.contains(manifestMeta.fileName())) {
- continue;
- }
-
- List<ManifestEntry> rewrittenEntries = new ArrayList<>();
+ long fileCount = 0L;
+ for (ManifestFileMeta manifestMeta :
assignment.manifestMetasToRewrite) {
List<ManifestEntry> entries =
manifestFile.read(manifestMeta.fileName(),
manifestMeta.fileSize());
- for (ManifestEntry entry : entries) {
- if (entry.kind() == FileKind.ADD) {
- ManifestEntry reassignedEntry =
reassignedEntries.get(entry.identifier());
- if (reassignedEntry != null) {
- entry = reassignedEntry;
+ long reassignedAddFileCount = 0L;
+ boolean hasRewrittenEntry = false;
+ for (int i = 0; i < entries.size(); i++) {
+ ManifestEntry entry = entries.get(i);
+ RowRangeMappingIndex mapping =
assignment.rowIdMappings.get(entry.partition());
+ if (mapping == null) {
+ continue;
+ }
+ Optional<Range> reassignedRange =
mapping.map(entry.file().nonNullRowIdRange());
+ if (reassignedRange.isPresent()) {
+ validatePlanningEntry(entry);
+ entries.set(i,
entry.assignFirstRowId(reassignedRange.get().from));
+ hasRewrittenEntry = true;
+ if (entry.kind() == FileKind.ADD) {
+ reassignedAddFileCount++;
}
}
- rewrittenEntries.add(entry);
- }
- rewrittenManifestMetas.put(
- manifestMeta.fileName(),
manifestFile.write(rewrittenEntries));
- }
- return rewrittenManifestMetas;
- }
-
- private Set<String> manifestsToRewrite(
- List<SourcedManifestEntry> currentEntries, Set<BinaryRow>
partitionsToReassign) {
- Set<String> manifestsToRewrite = new HashSet<>();
- for (SourcedManifestEntry currentEntry : currentEntries) {
- if (partitionsToReassign.contains(currentEntry.entry.partition()))
{
- manifestsToRewrite.add(currentEntry.manifest.fileName());
}
+ checkState(
+ hasRewrittenEntry,
+ "Cannot find entries to reassign in planned manifest %s.",
+ manifestMeta.fileName());
+ rewrittenManifestMetas.put(manifestMeta.fileName(),
manifestFile.write(entries));
+ fileCount += reassignedAddFileCount;
}
- return manifestsToRewrite;
- }
-
- private Map<FileEntry.Identifier, ManifestEntry> entriesByIdentifier(
- List<ManifestEntry> entries) {
- Map<FileEntry.Identifier, ManifestEntry> result = new HashMap<>();
- for (ManifestEntry entry : entries) {
- ManifestEntry previous = result.put(entry.identifier(), entry);
- checkState(previous == null, "Duplicate current manifest entry for
file %s.", entry);
- }
- return result;
+ return new RewrittenDataManifests(rewrittenManifestMetas, fileCount);
}
private Map<BinaryRow, List<ManifestEntry>>
entriesByPartition(List<ManifestEntry> entries) {
- List<ManifestEntry> sorted = new ArrayList<>(entries);
- Collections.sort(sorted, entryComparator());
+ Comparator<ManifestEntry> comparator = entryComparator();
+ Collections.sort(entries, comparator);
Map<BinaryRow, List<ManifestEntry>> entriesByPartition = new
LinkedHashMap<>();
- for (ManifestEntry entry : sorted) {
- List<String> writeCols = entry.file().writeCols();
- checkState(
- writeCols == null ||
!writeCols.contains(SpecialFields.ROW_ID.name()),
- "Cannot reassign row IDs for file '%s' because it
physically stores the row-id field.",
- entry.file().fileName());
- checkState(
- entry.file().firstRowId() != null,
- "File '%s' in table '%s' does not have first row id.",
- entry.file().fileName(),
- table.name());
+ for (ManifestEntry entry : entries) {
+ validatePlanningEntry(entry);
entriesByPartition
.computeIfAbsent(entry.partition(), k -> new ArrayList<>())
.add(entry);
@@ -491,6 +662,19 @@ public class DataEvolutionRowIdReassigner {
return entriesByPartition;
}
+ private void validatePlanningEntry(ManifestEntry entry) {
+ List<String> writeCols = entry.file().writeCols();
+ checkState(
+ writeCols == null ||
!writeCols.contains(SpecialFields.ROW_ID.name()),
+ "Cannot reassign row IDs for file '%s' because it physically
stores the row-id field.",
+ entry.file().fileName());
+ checkState(
+ entry.file().firstRowId() != null,
+ "File '%s' in table '%s' does not have first row id.",
+ entry.file().fileName(),
+ table.name());
+ }
+
private Set<BinaryRow> partitionsToReassign(
Map<BinaryRow, List<ManifestEntry>> entriesByPartition) {
Set<BinaryRow> partitionsToReassign = new HashSet<>();
@@ -503,6 +687,9 @@ public class DataEvolutionRowIdReassigner {
}
private boolean partitionRowIdsAreContiguous(List<ManifestEntry> entries) {
+ for (ManifestEntry entry : entries) {
+ validatePlanningEntry(entry);
+ }
List<Range> logicalRanges = logicalRanges(entries);
if (logicalRanges.size() <= 1) {
return true;
@@ -536,59 +723,42 @@ public class DataEvolutionRowIdReassigner {
return logicalRanges;
}
- private Assignment assign(
- Map<BinaryRow, List<ManifestEntry>> entriesByPartition,
- Set<BinaryRow> partitionsToReassign,
- long firstRowId) {
- List<ManifestEntry> entries = new ArrayList<>();
- Map<BinaryRow, RowRangeMappingIndex> rowIdMappings = new
LinkedHashMap<>();
- long nextRowId = firstRowId;
- long logicalRowCount = 0;
- long reassignedFileCount = 0;
- for (Map.Entry<BinaryRow, List<ManifestEntry>> entry :
entriesByPartition.entrySet()) {
- if (partitionsToReassign.contains(entry.getKey())) {
- long partitionFirstRowId = nextRowId;
- PartitionAssignment partitionAssignment =
- assignPartition(entry.getValue(), nextRowId);
- entries.addAll(partitionAssignment.entries);
- rowIdMappings.put(entry.getKey(),
partitionAssignment.rowIdMappings);
- nextRowId = partitionAssignment.nextRowId;
- logicalRowCount += nextRowId - partitionFirstRowId;
- reassignedFileCount += partitionAssignment.entries.size();
+ private RelativeRowIdMappings createRelativeRowIdMappings(
+ Map<BinaryRow, List<ManifestEntry>> entriesByPartition) {
+ Map<BinaryRow, List<Range>> logicalRangesByPartition = new
LinkedHashMap<>();
+ for (Map.Entry<BinaryRow, List<ManifestEntry>> partition :
entriesByPartition.entrySet()) {
+ for (ManifestEntry entry : partition.getValue()) {
+ validatePlanningEntry(entry);
}
+ logicalRangesByPartition.put(partition.getKey(),
logicalRanges(partition.getValue()));
}
-
- return new Assignment(
- entries, rowIdMappings, nextRowId, logicalRowCount,
reassignedFileCount);
+ return createRelativeRowIdMappingsFromRanges(logicalRangesByPartition);
}
- private PartitionAssignment assignPartition(List<ManifestEntry> entries,
long firstRowId) {
- RangeHelper<ManifestEntry> rangeHelper =
- new RangeHelper<>(entry -> entry.file().nonNullRowIdRange());
- List<List<ManifestEntry>> groups =
rangeHelper.mergeOverlappingRanges(entries);
-
- List<ManifestEntry> reassigned = new ArrayList<>(entries.size());
- List<RowRangeMappingIndex.Mapping> mappings = new ArrayList<>();
- long nextRowId = firstRowId;
-
- for (List<ManifestEntry> group : groups) {
- Collections.sort(group, entryComparatorWithoutPartition());
- Range oldLogicalRange = oldLogicalRange(group);
- mappings.add(
- RowRangeMappingIndex.mapping(
- oldLogicalRange.from, oldLogicalRange.to,
nextRowId));
-
- for (ManifestEntry entry : group) {
- long oldFirstRowId = entry.file().nonNullFirstRowId();
- long newFirstRowId = nextRowId + oldFirstRowId -
oldLogicalRange.from;
- reassigned.add(entry.assignFirstRowId(newFirstRowId));
+ private RelativeRowIdMappings createRelativeRowIdMappingsFromRanges(
+ Map<BinaryRow, List<Range>> logicalRangesByPartition) {
+ List<BinaryRow> partitions = new
ArrayList<>(logicalRangesByPartition.keySet());
+ RecordComparator partitionComparator = partitionComparator();
+ Collections.sort(partitions, partitionComparator);
+
+ Map<BinaryRow, RowRangeMappingIndex> result = new LinkedHashMap<>();
+ long nextOffset = 0L;
+ for (BinaryRow partition : partitions) {
+ List<Range> ranges = new
ArrayList<>(logicalRangesByPartition.get(partition));
+ Collections.sort(
+ ranges,
+ (left, right) -> {
+ int compare = Long.compare(left.from, right.from);
+ return compare == 0 ? Long.compare(left.to, right.to)
: compare;
+ });
+ List<RowRangeMappingIndex.Mapping> mappings = new
ArrayList<>(ranges.size());
+ for (Range range : ranges) {
+ mappings.add(RowRangeMappingIndex.mapping(range.from,
range.to, nextOffset));
+ nextOffset = Math.addExact(nextOffset, range.count());
}
-
- nextRowId += oldLogicalRange.count();
+ result.put(partition, RowRangeMappingIndex.create(mappings));
}
-
- return new PartitionAssignment(
- reassigned, RowRangeMappingIndex.create(mappings), nextRowId);
+ return new RelativeRowIdMappings(result, nextOffset);
}
private Range oldLogicalRange(List<ManifestEntry> group) {
@@ -637,14 +807,14 @@ public class DataEvolutionRowIdReassigner {
return new Range(min, max);
}
- private RewrittenIndexManifest rewriteIndexManifest(
- Snapshot latest, AssignmentPlan assignment) {
- if (latest.indexManifest() == null) {
+ private RewrittenIndexManifest rewriteIndexManifest(Assignment assignment)
{
+ if (assignment.snapshot.indexManifest() == null) {
return new RewrittenIndexManifest(null, 0);
}
IndexManifestFile indexManifestFile =
table.store().indexManifestFileFactory().create();
- List<IndexManifestEntry> indexEntries =
indexManifestFile.read(latest.indexManifest());
+ List<IndexManifestEntry> indexEntries =
+ indexManifestFile.read(assignment.snapshot.indexManifest());
if (indexEntries.isEmpty()) {
return new RewrittenIndexManifest(null, 0);
}
@@ -655,7 +825,7 @@ public class DataEvolutionRowIdReassigner {
checkState(
entry.kind() == FileKind.ADD,
"Index manifest '%s' contains non-current entry %s.",
- latest.indexManifest(),
+ assignment.snapshot.indexManifest(),
entry);
IndexFileMeta indexFile = entry.indexFile();
@@ -668,6 +838,10 @@ public class DataEvolutionRowIdReassigner {
Optional<Range> newRange =
mappingIndex.map(globalIndex.rowRange());
if (!newRange.isPresent()) {
+ if (!mappingIndex.overlaps(globalIndex.rowRange())) {
+ rewritten.add(entry);
+ continue;
+ }
LOG.warn(
"Drop global index file '{}' from table {} during
row-id reassignment because its row range {} cannot be rewritten safely.",
indexFile.fileName(),
@@ -702,6 +876,18 @@ public class DataEvolutionRowIdReassigner {
indexManifestFile.writeWithoutRolling(rewritten),
globalIndexFileCount);
}
+ private List<ManifestEntry> readPlanningManifestEntries(
+ ManifestFile manifestFile, ManifestFileMeta manifestMeta) {
+ return manifestFile.read(
+ manifestMeta.fileName(),
+ manifestMeta.fileSize(),
+ partitionPredicate,
+ null,
+ Filter.alwaysTrue(),
+ entry -> partitionPredicate == null ||
partitionPredicate.test(entry.partition()),
+ ManifestEntry::copyWithoutStats);
+ }
+
private Comparator<ManifestEntry> entryComparator() {
RecordComparator partitionComparator = partitionComparator();
Comparator<ManifestEntry> withoutPartition =
entryComparatorWithoutPartition();
@@ -842,77 +1028,95 @@ public class DataEvolutionRowIdReassigner {
}
}
- private static class Assignment {
- private final List<ManifestEntry> entries;
- private final Map<BinaryRow, RowRangeMappingIndex> rowIdMappings;
- private final long nextRowId;
- private final long logicalRowCount;
- private final long reassignedFileCount;
+ private static class RelativeRowIdMappings {
+ private final Map<BinaryRow, RowRangeMappingIndex> mappings;
+ private final long totalOffset;
- private Assignment(
- List<ManifestEntry> entries,
- Map<BinaryRow, RowRangeMappingIndex> rowIdMappings,
- long nextRowId,
- long logicalRowCount,
- long reassignedFileCount) {
- this.entries = entries;
- this.rowIdMappings = rowIdMappings;
- this.nextRowId = nextRowId;
- this.logicalRowCount = logicalRowCount;
- this.reassignedFileCount = reassignedFileCount;
+ private RelativeRowIdMappings(
+ Map<BinaryRow, RowRangeMappingIndex> mappings, long
totalOffset) {
+ this.mappings = Collections.unmodifiableMap(new
LinkedHashMap<>(mappings));
+ this.totalOffset = totalOffset;
}
}
private static class AssignmentPlan {
- private final Map<String, List<ManifestFileMeta>>
rewrittenManifestMetas;
+ private final List<ManifestFileMeta> manifestMetasToRewrite;
+ private final RelativeRowIdMappings relativeRowIdMappings;
+
+ private AssignmentPlan(
+ List<ManifestFileMeta> manifestMetasToRewrite,
+ RelativeRowIdMappings relativeRowIdMappings) {
+ this.manifestMetasToRewrite = new
ArrayList<>(manifestMetasToRewrite);
+ this.relativeRowIdMappings = relativeRowIdMappings;
+ }
+
+ private Assignment createAssignment(Snapshot snapshot) {
+ Long firstAssignedRowId = snapshot.nextRowId();
+ checkState(
+ firstAssignedRowId != null,
+ "Next row id cannot be null for snapshot %s.",
+ snapshot.id());
+ Map<BinaryRow, RowRangeMappingIndex> absoluteRowIdMappings = new
LinkedHashMap<>();
+ for (Map.Entry<BinaryRow, RowRangeMappingIndex> mapping :
+ relativeRowIdMappings.mappings.entrySet()) {
+ absoluteRowIdMappings.put(
+ mapping.getKey(),
mapping.getValue().shiftNewStarts(firstAssignedRowId));
+ }
+ return new Assignment(
+ snapshot,
+ manifestMetasToRewrite,
+ absoluteRowIdMappings,
+ firstAssignedRowId,
+ Math.addExact(firstAssignedRowId,
relativeRowIdMappings.totalOffset));
+ }
+ }
+
+ private static class Assignment {
+ private final Snapshot snapshot;
+ private final List<ManifestFileMeta> manifestMetasToRewrite;
private final Map<BinaryRow, RowRangeMappingIndex> rowIdMappings;
+ private final long firstAssignedRowId;
private final long nextRowId;
- private final long logicalRowCount;
- private final long reassignedFileCount;
- private final boolean hasCurrentFiles;
- private AssignmentPlan(
- Map<String, List<ManifestFileMeta>> rewrittenManifestMetas,
+ private Assignment(
+ Snapshot snapshot,
+ List<ManifestFileMeta> manifestMetasToRewrite,
Map<BinaryRow, RowRangeMappingIndex> rowIdMappings,
- long nextRowId,
- long logicalRowCount,
- long reassignedFileCount,
- boolean hasCurrentFiles) {
- this.rewrittenManifestMetas = rewrittenManifestMetas;
- this.rowIdMappings = rowIdMappings;
+ long firstAssignedRowId,
+ long nextRowId) {
+ this.snapshot = snapshot;
+ this.manifestMetasToRewrite =
+ Collections.unmodifiableList(new
ArrayList<>(manifestMetasToRewrite));
+ this.rowIdMappings = Collections.unmodifiableMap(new
LinkedHashMap<>(rowIdMappings));
+ this.firstAssignedRowId = firstAssignedRowId;
this.nextRowId = nextRowId;
- this.logicalRowCount = logicalRowCount;
- this.reassignedFileCount = reassignedFileCount;
- this.hasCurrentFiles = hasCurrentFiles;
+ }
+
+ private long logicalRowCount() {
+ return nextRowId - firstAssignedRowId;
}
}
- private static class CurrentManifest {
- private final List<ManifestFileMeta> manifestMetas;
- private final List<SourcedManifestEntry> currentEntries;
+ private static class RewrittenDataManifests {
+ private final Map<String, List<ManifestFileMeta>> manifestMetas;
+ private final long fileCount;
- private CurrentManifest(
- List<ManifestFileMeta> manifestMetas,
List<SourcedManifestEntry> currentEntries) {
+ private RewrittenDataManifests(
+ Map<String, List<ManifestFileMeta>> manifestMetas, long
fileCount) {
this.manifestMetas = manifestMetas;
- this.currentEntries = currentEntries;
- }
-
- private List<ManifestEntry> entries() {
- List<ManifestEntry> entries = new
ArrayList<>(currentEntries.size());
- for (SourcedManifestEntry currentEntry : currentEntries) {
- entries.add(currentEntry.entry);
- }
- return entries;
+ this.fileCount = fileCount;
}
}
- private static class SourcedManifestEntry {
- private final ManifestFileMeta manifest;
- private final ManifestEntry entry;
+ private static class CommitAssignmentResult {
+ private final boolean success;
+ private final long fileCount;
+ private final long indexFileCount;
- private SourcedManifestEntry(ManifestFileMeta manifest, ManifestEntry
entry) {
- this.manifest = manifest;
- this.entry = entry;
+ private CommitAssignmentResult(boolean success, long fileCount, long
indexFileCount) {
+ this.success = success;
+ this.fileCount = fileCount;
+ this.indexFileCount = indexFileCount;
}
}
@@ -920,30 +1124,20 @@ public class DataEvolutionRowIdReassigner {
private final ManifestFileMeta manifest;
private final BinaryRow minPartition;
private final BinaryRow maxPartition;
+ private final boolean containsNullPartition;
private final int originalIndex;
private PartitionManifestRange(
ManifestFileMeta manifest,
BinaryRow minPartition,
BinaryRow maxPartition,
+ boolean containsNullPartition,
int originalIndex) {
this.manifest = manifest;
this.minPartition = minPartition;
this.maxPartition = maxPartition;
+ this.containsNullPartition = containsNullPartition;
this.originalIndex = originalIndex;
}
}
-
- private static class PartitionAssignment {
- private final List<ManifestEntry> entries;
- private final RowRangeMappingIndex rowIdMappings;
- private final long nextRowId;
-
- private PartitionAssignment(
- List<ManifestEntry> entries, RowRangeMappingIndex
rowIdMappings, long nextRowId) {
- this.entries = entries;
- this.rowIdMappings = rowIdMappings;
- this.nextRowId = nextRowId;
- }
- }
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndex.java
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndex.java
index a24515c905..7977c9295c 100644
---
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndex.java
+++
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndex.java
@@ -70,6 +70,18 @@ final class RowRangeMappingIndex {
return new Mapping(oldStart, oldEnd, newStart);
}
+ RowRangeMappingIndex shiftNewStarts(long offset) {
+ List<Mapping> shifted = new ArrayList<>(mappings.size());
+ for (Mapping mapping : mappings) {
+ shifted.add(
+ mapping(
+ mapping.oldStart,
+ mapping.oldEnd,
+ Math.addExact(mapping.newStart, offset)));
+ }
+ return create(shifted);
+ }
+
Optional<Range> map(Range oldRange) {
checkArgument(oldRange != null, "Old row range cannot be null.");
checkArgument(oldRange.from <= oldRange.to, "Invalid old row range
%s.", oldRange);
@@ -108,6 +120,14 @@ final class RowRangeMappingIndex {
return Optional.of(new Range(newFrom, newTo));
}
+ boolean overlaps(Range oldRange) {
+ checkArgument(oldRange != null, "Old row range cannot be null.");
+ checkArgument(oldRange.from <= oldRange.to, "Invalid old row range
%s.", oldRange);
+
+ int index = lowerBound(oldEnds, oldRange.from);
+ return index < mappings.size() && mappings.get(index).oldStart <=
oldRange.to;
+ }
+
private static int lowerBound(long[] sorted, long target) {
int left = 0;
int right = sorted.length;
diff --git
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
index ffdc12539a..77cb9802f3 100644
---
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
@@ -26,11 +26,15 @@ import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.data.serializer.InternalRowSerializer;
+import org.apache.paimon.fs.Path;
import org.apache.paimon.globalindex.btree.BTreeIndexOptions;
import org.apache.paimon.globalindex.sorted.SortedGlobalIndexBuilder;
import org.apache.paimon.index.GlobalIndexMeta;
import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.io.CompactIncrement;
import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.io.DataFilePathFactory;
+import org.apache.paimon.io.DataIncrement;
import org.apache.paimon.manifest.FileEntry;
import org.apache.paimon.manifest.FileKind;
import org.apache.paimon.manifest.FileSource;
@@ -46,25 +50,28 @@ import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.stats.SimpleStats;
+import org.apache.paimon.stats.Statistics;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.TableTestBase;
import org.apache.paimon.table.sink.BatchTableCommit;
import org.apache.paimon.table.sink.BatchTableWrite;
import org.apache.paimon.table.sink.BatchWriteBuilder;
import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.CommitMessageImpl;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.ReadBuilder;
import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.Pair;
import org.apache.paimon.utils.Range;
import org.apache.paimon.utils.SnapshotManager;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -72,21 +79,26 @@ import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** Tests for {@link DataEvolutionRowIdReassigner}. */
public class DataEvolutionRowIdReassignerTest extends TableTestBase {
private static final int LARGE_ENTRY_COUNT = 10_000;
private static final int LARGE_MANIFEST_FILE_COUNT = 20;
- private static final int ONE_MILLION_ENTRY_COUNT = 1_000_000;
- private static final int ONE_MILLION_MANIFEST_FILE_COUNT = 200;
private static final long LARGE_ENTRY_ROW_COUNT = 2L;
private static final long RANDOM_PARTITION_SEED = 20260519L;
private static final String LARGE_FILE_PREFIX = "large-partition-";
private static final String LARGE_FILE_SUFFIX = ".parquet";
private static final int LARGE_PARTITION_ID_WIDTH = 7;
+ private static final String[] DATA_INTEGRITY_PARTITIONS = {"a", "b", "c",
"d"};
+ private static final int DATA_INTEGRITY_FILES_PER_PARTITION = 6;
+ private static final long SCRAMBLED_ROW_ID_GAP = 100_000_000L;
+ private static final long SCRAMBLED_ROW_ID_SEED = 20260710L;
@Override
protected Schema schemaDefault() {
@@ -136,6 +148,493 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
assertThat(table.snapshotManager().latestSnapshot().nextRowId()).isEqualTo(10L);
}
+ @Test
+ public void testReassignNullPartitionAcrossManifestGroups() throws
Exception {
+ createTableDefault();
+ FileStoreTable table =
+ getTableDefault()
+ .copy(
+ Collections.singletonMap(
+
CoreOptions.ROW_TRACKING_PARTITION_GROUP_ON_COMMIT.key(),
+ "false"));
+ writeRowsInPartitions(table, null, 0, "a", 1);
+ writeOneRow(table, "z", 2);
+ writeOneRow(table, null, 3);
+
+ BinaryRow nullPartition =
+ new
InternalRowSerializer(table.schema().logicalPartitionType())
+ .toBinaryRow(GenericRow.of((Object) null));
+ String nullPartitionPath =
table.store().pathFactory().getPartitionString(nullPartition);
+ Map<String, List<Long>> beforeRowIds = rowIdsByPartition(table);
+ assertThat(beforeRowIds)
+ .containsEntry(nullPartitionPath, Arrays.asList(1L, 3L))
+ .containsEntry("pt=a/", Collections.singletonList(0L))
+ .containsEntry("pt=z/", Collections.singletonList(2L));
+
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(table)
+ .reassign("test-reassign-null-partition-row-id");
+
+ assertThat(result.firstAssignedRowId).isEqualTo(4L);
+ assertThat(result.nextRowId).isEqualTo(6L);
+ assertThat(result.fileCount).isEqualTo(2L);
+ assertThat(result.rowCount).isEqualTo(2L);
+ assertThat(rowIdsByPartition(table))
+ .containsEntry(nullPartitionPath, Arrays.asList(4L, 5L))
+ .containsEntry("pt=a/", Collections.singletonList(0L))
+ .containsEntry("pt=z/", Collections.singletonList(2L));
+ }
+
+ @Test
+ public void testReassignRetriesWithLatestNextRowIdAfterConcurrentAppend()
throws Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+ Map<String, SimpleStats> valueStatsBefore = valueStatsByFile(table);
+ assertThat(before.nextRowId()).isEqualTo(5L);
+ assertThat(valueStatsBefore.values())
+ .allSatisfy(stats ->
assertThat(stats).isNotEqualTo(SimpleStats.EMPTY_STATS));
+
+ AtomicBoolean appended = new AtomicBoolean();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if (appended.compareAndSet(false, true)) {
+ try {
+ writeOneRow(table, "c", 100);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .reassign("test-reassign-conflict-retry");
+
+ assertThat(appended).isTrue();
+ assertThat(result.reassigned).isTrue();
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 2);
+ assertThat(result.firstAssignedRowId).isEqualTo(6L);
+ assertThat(result.nextRowId).isEqualTo(11L);
+ assertThat(result.fileCount).isEqualTo(5L);
+ assertThat(result.rowCount).isEqualTo(5L);
+ assertThat(rowIdsByPartition(table))
+ .containsEntry("pt=a/", Arrays.asList(6L, 7L, 8L))
+ .containsEntry("pt=b/", Arrays.asList(9L, 10L))
+ .containsEntry("pt=c/", Collections.singletonList(5L));
+
assertThat(valueStatsByFile(table)).containsAllEntriesOf(valueStatsBefore);
+
assertThat(table.snapshotManager().latestSnapshot().nextRowId()).isEqualTo(11L);
+ }
+
+ @Test
+ public void testReassignLeavesDisjointConcurrentAppendOutOfPlan() throws
Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean appended = new AtomicBoolean();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if (appended.compareAndSet(false, true)) {
+ try {
+ writeOneRow(table, "a", 100);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .reassign("test-reassign-append-planned-partition");
+
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 2);
+ assertThat(result.firstAssignedRowId).isEqualTo(6L);
+ assertThat(result.nextRowId).isEqualTo(11L);
+ assertThat(result.fileCount).isEqualTo(5L);
+ assertThat(result.rowCount).isEqualTo(5L);
+ assertThat(rowIdsByPartition(table))
+ .containsEntry("pt=a/", Arrays.asList(5L, 6L, 7L, 8L))
+ .containsEntry("pt=b/", Arrays.asList(9L, 10L));
+ }
+
+ @Test
+ public void testReassignAbortsForPartiallyOverlappingConcurrentAppend()
throws Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean appended = new AtomicBoolean();
+ assertThatThrownBy(
+ () ->
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if
(appended.compareAndSet(false, true)) {
+ try {
+
writePartialPayload(
+ table,
+ "a",
+ 4L,
+
"overlap-4",
+
"overlap-5");
+ } catch (Exception e) {
+ throw new
RuntimeException(e);
+ }
+ }
+ })
+
.reassign("test-reassign-partially-overlapping-append"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("partially overlaps planned range");
+
+ assertThat(appended).isTrue();
+
assertThat(table.snapshotManager().latestSnapshot().id()).isEqualTo(before.id()
+ 1);
+ }
+
+ @Test
+ public void testReassignAbortsWhenConcurrentAppendMergesPlannedManifests()
throws Exception {
+ FileStoreTable originalTable = createTableWithInterleavedPartitions();
+ List<String> plannedManifestFiles =
dataManifestFileNames(originalTable);
+ assertThat(plannedManifestFiles).hasSizeGreaterThan(1);
+
+ FileStoreTable table = withManifestMergeOnNextAppend(originalTable);
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean merged = new AtomicBoolean();
+ assertThatThrownBy(
+ () ->
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if
(merged.compareAndSet(false, true)) {
+ try {
+ writeOneRow(table,
"c", 100);
+ Snapshot
appendSnapshot =
+
table.snapshotManager()
+
.latestSnapshot();
+
assertThat(appendSnapshot.commitKind())
+ .isEqualTo(
+
Snapshot.CommitKind
+
.APPEND);
+
assertThat(dataManifestFileNames(table))
+
.doesNotContainAnyElementsOf(
+
plannedManifestFiles);
+ } catch (Exception e) {
+ throw new
RuntimeException(e);
+ }
+ }
+ })
+
.reassign("test-reassign-append-manifest-merge"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("planned manifest")
+ .hasMessageContaining("no longer exists");
+
+ assertThat(merged).isTrue();
+
assertThat(table.snapshotManager().latestSnapshot().id()).isEqualTo(before.id()
+ 1);
+ assertThat(rowIdsByPartition(table))
+ .containsEntry("pt=a/", Arrays.asList(0L, 2L, 4L))
+ .containsEntry("pt=b/", Arrays.asList(1L, 3L))
+ .containsEntry("pt=c/", Collections.singletonList(5L));
+ assertThat(readTableRows(table))
+ .containsExactly("0|a|v0", "100|c|v100", "1|b|v1", "2|a|v2",
"3|b|v3", "4|a|v4");
+ }
+
+ @Test
+ public void testReassignKeepsHistoricalAddDeleteEntriesConsistent() throws
Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ FileEntry.Identifier compactedFile = compactOneFile(table, "pt=a/");
+ long oldFirstRowId = assertAddDeleteEntriesConsistent(table,
compactedFile);
+
+ new DataEvolutionRowIdReassigner(table)
+ .reassign("test-reassign-historical-add-delete-without-retry");
+
+ assertThat(assertAddDeleteEntriesConsistent(table, compactedFile))
+ .isNotEqualTo(oldFirstRowId);
+ assertThat(readTableRows(table))
+ .containsExactly("0|a|v0", "1|b|v1", "2|a|v2", "3|b|v3",
"4|a|v4");
+ }
+
+ @Test
+ public void
testReassignKeepsHistoricalAddDeleteEntriesConsistentAfterRetry() throws
Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ FileEntry.Identifier compactedFile = compactOneFile(table, "pt=a/");
+ long oldFirstRowId = assertAddDeleteEntriesConsistent(table,
compactedFile);
+
+ AtomicBoolean appended = new AtomicBoolean();
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if (appended.compareAndSet(false, true)) {
+ try {
+ writeOneRow(table, "c", 100);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .reassign("test-reassign-historical-add-delete");
+
+ assertThat(appended).isTrue();
+ assertThat(assertAddDeleteEntriesConsistent(table, compactedFile))
+ .isNotEqualTo(oldFirstRowId);
+ assertThat(readTableRows(table))
+ .containsExactly("0|a|v0", "100|c|v100", "1|b|v1", "2|a|v2",
"3|b|v3", "4|a|v4");
+ }
+
+ @Test
+ public void testReassignRetriesAfterConcurrentPartialColumnAppend() throws
Exception {
+ createTableDefault();
+ FileStoreTable originalTable = getTableDefault();
+ writeRows(originalTable, "a", 0, 1);
+ writeOneRow(originalTable, "b", 2);
+ writeRows(originalTable, "a", 3, 4);
+ FileStoreTable table = originalTable;
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean appended = new AtomicBoolean();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if (appended.compareAndSet(false, true)) {
+ try {
+ writePartialPayload(
+ table, "a", 0L,
"updated-0", "updated-1");
+ List<ManifestEntry>
sharedRangeEntries =
+
currentEntriesAtRange(table, "pt=a/", 0L, 1L);
+
assertThat(sharedRangeEntries).hasSize(2);
+ assertThat(
+ sharedRangeEntries
+ .get(0)
+ .file()
+
.maxSequenceNumber())
+ .isNotEqualTo(
+ sharedRangeEntries
+ .get(1)
+ .file()
+
.maxSequenceNumber());
+ assertThat(
+
table.snapshotManager()
+
.latestSnapshot()
+
.nextRowId())
+ .isEqualTo(5L);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .reassign("test-reassign-partial-column-append");
+
+ assertThat(appended).isTrue();
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 2);
+ assertThat(result.firstAssignedRowId).isEqualTo(5L);
+ assertThat(result.nextRowId).isEqualTo(9L);
+ assertThat(result.fileCount).isEqualTo(3L);
+ assertThat(result.rowCount).isEqualTo(4L);
+ assertThat(rowIdsByPartition(table))
+ .containsEntry("pt=a/", Arrays.asList(5L, 5L, 7L))
+ .containsEntry("pt=b/", Collections.singletonList(2L));
+ assertThat(readTableRows(table))
+ .containsExactly("0|a|updated-0", "1|a|updated-1", "2|b|v2",
"3|a|v3", "4|a|v4");
+ }
+
+ @Test
+ public void testReassignAdvancesAcrossRepeatedAppendConflicts() throws
Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicInteger beforeCommits = new AtomicInteger();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ try {
+ int attempt =
beforeCommits.getAndIncrement();
+ if (attempt == 0) {
+ writeOneRow(table, "c", 100);
+ writeOneRow(table, "d", 101);
+ } else if (attempt == 1) {
+ writeOneRow(table, "a", 102);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .reassign("test-reassign-multiple-append-conflicts");
+
+ assertThat(beforeCommits).hasValue(3);
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 3);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 4);
+ assertThat(result.firstAssignedRowId).isEqualTo(8L);
+ assertThat(result.nextRowId).isEqualTo(13L);
+ assertThat(result.fileCount).isEqualTo(5L);
+ assertThat(result.rowCount).isEqualTo(5L);
+ assertThat(rowIdsByPartition(table))
+ .containsEntry("pt=a/", Arrays.asList(7L, 8L, 9L, 10L))
+ .containsEntry("pt=b/", Arrays.asList(11L, 12L))
+ .containsEntry("pt=c/", Collections.singletonList(5L))
+ .containsEntry("pt=d/", Collections.singletonList(6L));
+ }
+
+ @Test
+ public void
testReassignPartitionFilterAfterConcurrentAppendOutsideFilter() throws
Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean appended = new AtomicBoolean();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ partitionPredicate(table, "a"),
+ () -> {
+ if (appended.compareAndSet(false, true)) {
+ try {
+ writeOneRow(table, "c", 100);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+
.reassign("test-reassign-filter-after-append-conflict");
+
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 2);
+ assertThat(result.firstAssignedRowId).isEqualTo(6L);
+ assertThat(result.nextRowId).isEqualTo(9L);
+ assertThat(result.fileCount).isEqualTo(3L);
+ assertThat(result.rowCount).isEqualTo(3L);
+ assertThat(rowIdsByPartition(table))
+ .containsEntry("pt=a/", Arrays.asList(6L, 7L, 8L))
+ .containsEntry("pt=b/", Arrays.asList(1L, 3L))
+ .containsEntry("pt=c/", Collections.singletonList(5L));
+ }
+
+ @Test
+ public void testReassignDoesNotExpandPlanForNewlyNonContiguousPartition()
throws Exception {
+ FileStoreTable table = createTableWithPartiallyOverlappedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean appended = new AtomicBoolean();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if (appended.compareAndSet(false, true)) {
+ try {
+ writeOneRow(table, "c", 100);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .reassign("test-reassign-append-new-partition");
+
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 2);
+ assertThat(result.firstAssignedRowId).isEqualTo(6L);
+ assertThat(result.nextRowId).isEqualTo(8L);
+ assertThat(result.fileCount).isEqualTo(2L);
+ assertThat(result.rowCount).isEqualTo(2L);
+ assertThat(expandedRowIdsByPartition(table))
+ .containsEntry("pt=a/", Arrays.asList(6L, 7L))
+ .containsEntry("pt=b/", Collections.singletonList(3L))
+ .containsEntry("pt=c/", Arrays.asList(0L, 1L, 5L));
+ }
+
+ @Test
+ public void testReassignAbortsAfterConcurrentOverwrite() throws Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean overwritten = new AtomicBoolean();
+ assertThatThrownBy(
+ () ->
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if
(overwritten.compareAndSet(false, true)) {
+ try {
+
overwriteOneRow(table, "c", 100);
+ } catch (Exception e) {
+ throw new
RuntimeException(e);
+ }
+ }
+ })
+
.reassign("test-reassign-overwrite-conflict"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("OVERWRITE snapshot");
+
+
assertThat(table.snapshotManager().latestSnapshot().id()).isEqualTo(before.id()
+ 1);
+ assertThat(table.snapshotManager().latestSnapshot().commitKind())
+ .isEqualTo(Snapshot.CommitKind.OVERWRITE);
+ }
+
+ @Test
+ public void testReassignAbortsAfterConcurrentManifestCompaction() throws
Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean compacted = new AtomicBoolean();
+ assertThatThrownBy(
+ () ->
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if
(compacted.compareAndSet(false, true)) {
+ try {
+
compactManifests(table);
+ } catch (Exception e) {
+ throw new
RuntimeException(e);
+ }
+ }
+ })
+
.reassign("test-reassign-compact-conflict"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("COMPACT snapshot");
+
+
assertThat(table.snapshotManager().latestSnapshot().id()).isEqualTo(before.id()
+ 1);
+ assertThat(table.snapshotManager().latestSnapshot().commitKind())
+ .isEqualTo(Snapshot.CommitKind.COMPACT);
+ }
+
+ @Test
+ public void testReassignContinuesAfterConcurrentAnalyze() throws Exception
{
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean analyzed = new AtomicBoolean();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if (analyzed.compareAndSet(false, true)) {
+ try {
+ updateStatistics(table);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .reassign("test-reassign-analyze-conflict");
+
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 2);
+ assertThat(result.firstAssignedRowId).isEqualTo(5L);
+ assertThat(result.nextRowId).isEqualTo(10L);
+
assertThat(table.snapshotManager().latestSnapshot().statistics()).isNotNull();
+ assertThat(table.snapshotManager().latestSnapshot().commitKind())
+ .isEqualTo(Snapshot.CommitKind.OVERWRITE);
+ }
+
@Test
public void testReassignMultiRowFilesByPartition() throws Exception {
createTableDefault();
@@ -301,7 +800,7 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
new
DataEvolutionRowIdReassigner(table).reassign("test-skip-row-id");
assertThat(result.reassigned).isFalse();
- assertThat(result.skipReason).isEqualTo("partition row IDs are already
contiguous");
+ assertThat(result.skipReason).isEqualTo("no partition requires row-id
reassignment");
assertThat(result.previousSnapshotId).isEqualTo(3L);
assertThat(result.newSnapshotId).isEqualTo(3L);
assertThat(result.firstAssignedRowId).isEqualTo(5L);
@@ -388,6 +887,47 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
assertThat(readPayloads(table, predicate)).containsExactly("v4");
}
+ @Test
+ public void testReassignKeepsConcurrentDisjointGlobalIndexRange() throws
Exception {
+ FileStoreTable table = createTableWithInterleavedPartitions();
+ createBTreeIndex(table);
+ Snapshot before = table.snapshotManager().latestSnapshot();
+
+ AtomicBoolean indexed = new AtomicBoolean();
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table,
+ null,
+ () -> {
+ if (indexed.compareAndSet(false, true)) {
+ try {
+ writeOneRow(table, "a", 100);
+ appendGlobalIndexRange(table,
"pt=a/", new Range(5, 5));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .reassign("test-reassign-disjoint-global-index");
+
+ assertThat(indexed).isTrue();
+ assertThat(result.previousSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 2);
+ assertThat(result.firstAssignedRowId).isEqualTo(6L);
+ assertThat(result.nextRowId).isEqualTo(11L);
+ assertThat(result.indexFileCount).isEqualTo(5L);
+ assertThat(globalIndexRanges(table))
+ .containsExactly(
+ new Range(5, 5),
+ new Range(6, 6),
+ new Range(7, 7),
+ new Range(8, 8),
+ new Range(9, 9),
+ new Range(10, 10));
+ assertThat(readTableRows(table))
+ .containsExactly("0|a|v0", "100|a|v100", "1|b|v1", "2|a|v2",
"3|b|v3", "4|a|v4");
+ }
+
@Test
public void
testDropUnsafeGlobalIndexEntryWhenRangeCannotBeRewrittenByMetadataOnly()
throws Exception {
@@ -430,15 +970,141 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
}
@Test
- public void testReassignManyOutOfOrderPartitionEntries() throws Exception {
- verifyReassignOutOfOrderPartitionEntries(LARGE_ENTRY_COUNT,
LARGE_MANIFEST_FILE_COUNT);
+ public void testFullReassignPreservesDataInTableWithLargeRowIdGaps()
throws Exception {
+ FileStoreTable table = createDataIntegrityTable();
+ long scrambledNextRowId = scrambleCurrentRowIds(table);
+ createBTreeIndex(table);
+
+ Snapshot before = table.snapshotManager().latestSnapshot();
+ List<String> expectedRows = readTableRows(table);
+ Map<String, List<Object>> expectedFileMetadata =
fileMetadataWithoutRowId(table);
+ Map<String, SimpleStats> expectedValueStats = valueStatsByFile(table);
+ Map<String, Long> rowCountsByPartition = rowCountsByPartition(table);
+ Map<String, List<Long>> scrambledRowIds = rowIdsByPartition(table);
+ int expectedFileCount = expectedFileMetadata.size();
+ int expectedIndexFileCount = globalIndexRanges(table).size();
+
+ assertThat(before.nextRowId()).isEqualTo(scrambledNextRowId);
+ assertThat(before.totalRecordCount()).isEqualTo(expectedRows.size());
+ assertThat(expectedFileCount)
+ .isEqualTo(DATA_INTEGRITY_PARTITIONS.length *
DATA_INTEGRITY_FILES_PER_PARTITION);
+ assertThat(expectedIndexFileCount).isGreaterThan(0);
+ assertPartitionsHaveLargeRowIdGaps(scrambledRowIds);
+ assertTableDataQueryable(table, expectedRows);
+
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(table)
+ .reassign("test-full-reassign-data-integrity");
+
+ assertThat(result.reassigned).isTrue();
+ assertThat(result.previousSnapshotId).isEqualTo(before.id());
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.firstAssignedRowId).isEqualTo(scrambledNextRowId);
+ assertThat(result.fileCount).isEqualTo(expectedFileCount);
+ assertThat(result.rowCount).isEqualTo(expectedRows.size());
+ assertThat(result.indexFileCount).isEqualTo(expectedIndexFileCount);
+
+ long expectedNextRowId =
+ assertContiguousRowIds(
+ expandedRowIdsByPartition(table),
+ rowCountsByPartition,
+ Arrays.asList(DATA_INTEGRITY_PARTITIONS),
+ scrambledNextRowId);
+ assertThat(result.nextRowId).isEqualTo(expectedNextRowId);
+ assertThat(table.snapshotManager().latestSnapshot().nextRowId())
+ .isEqualTo(expectedNextRowId);
+ assertThat(table.snapshotManager().latestSnapshot().totalRecordCount())
+ .isEqualTo(before.totalRecordCount());
+
assertThat(fileMetadataWithoutRowId(table)).isEqualTo(expectedFileMetadata);
+ assertThat(valueStatsByFile(table)).isEqualTo(expectedValueStats);
+ List<Range> reassignedIndexRanges = globalIndexRanges(table);
+ assertThat(reassignedIndexRanges).hasSize(expectedIndexFileCount);
+ for (Range range : reassignedIndexRanges) {
+ assertThat(range.from).isGreaterThanOrEqualTo(scrambledNextRowId);
+ assertThat(range.to).isLessThan(expectedNextRowId);
+ }
+ assertTableDataQueryable(table, expectedRows);
}
- @Disabled("Writes one million manifest entries; run manually for large
metadata stress tests.")
@Test
- public void testReassignOneMillionOutOfOrderPartitionEntries() throws
Exception {
- verifyReassignOutOfOrderPartitionEntries(
- ONE_MILLION_ENTRY_COUNT, ONE_MILLION_MANIFEST_FILE_COUNT);
+ public void testPartitionReassignPreservesDataAndOtherPartitionRowIds()
throws Exception {
+ FileStoreTable table = createDataIntegrityTable();
+ long scrambledNextRowId = scrambleCurrentRowIds(table);
+ createBTreeIndex(table);
+
+ Snapshot before = table.snapshotManager().latestSnapshot();
+ List<String> expectedRows = readTableRows(table);
+ Map<String, List<Object>> expectedFileMetadata =
fileMetadataWithoutRowId(table);
+ Map<String, SimpleStats> expectedValueStats = valueStatsByFile(table);
+ Map<String, Long> rowCountsByPartition = rowCountsByPartition(table);
+ Map<String, List<Long>> beforeFirstRowIds = rowIdsByPartition(table);
+ Map<String, List<Long>> beforeExpandedRowIds =
expandedRowIdsByPartition(table);
+ Map<String, List<Range>> beforeIndexRanges =
globalIndexRangesByPartition(table);
+ String reassignedPartition = "c";
+ String reassignedPartitionPath = "pt=" + reassignedPartition + "/";
+
+ assertThat(before.nextRowId()).isEqualTo(scrambledNextRowId);
+ assertThat(before.totalRecordCount()).isEqualTo(expectedRows.size());
+ assertPartitionsHaveLargeRowIdGaps(beforeFirstRowIds);
+ assertTableDataQueryable(table, expectedRows);
+
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(
+ table, partitionPredicate(table,
reassignedPartition))
+ .reassign("test-partition-reassign-data-integrity");
+
+ long reassignedRowCount =
rowCountsByPartition.get(reassignedPartitionPath);
+ long expectedNextRowId = scrambledNextRowId + reassignedRowCount;
+ assertThat(result.reassigned).isTrue();
+ assertThat(result.previousSnapshotId).isEqualTo(before.id());
+ assertThat(result.newSnapshotId).isEqualTo(before.id() + 1);
+ assertThat(result.firstAssignedRowId).isEqualTo(scrambledNextRowId);
+ assertThat(result.nextRowId).isEqualTo(expectedNextRowId);
+
assertThat(result.fileCount).isEqualTo(DATA_INTEGRITY_FILES_PER_PARTITION);
+ assertThat(result.rowCount).isEqualTo(reassignedRowCount);
+ assertThat(result.indexFileCount)
+
.isEqualTo(beforeIndexRanges.get(reassignedPartitionPath).size());
+
+ Map<String, List<Long>> afterFirstRowIds = rowIdsByPartition(table);
+ Map<String, List<Long>> afterExpandedRowIds =
expandedRowIdsByPartition(table);
+ Map<String, List<Range>> afterIndexRanges =
globalIndexRangesByPartition(table);
+ assertContiguousRowIds(
+ afterExpandedRowIds,
+ rowCountsByPartition,
+ Collections.singletonList(reassignedPartition),
+ scrambledNextRowId);
+ assertThat(afterFirstRowIds.get(reassignedPartitionPath))
+ .isNotEqualTo(beforeFirstRowIds.get(reassignedPartitionPath));
+ for (String partition : DATA_INTEGRITY_PARTITIONS) {
+ String partitionPath = "pt=" + partition + "/";
+ if (!partition.equals(reassignedPartition)) {
+ assertThat(afterFirstRowIds.get(partitionPath))
+ .isEqualTo(beforeFirstRowIds.get(partitionPath));
+ assertThat(afterExpandedRowIds.get(partitionPath))
+ .isEqualTo(beforeExpandedRowIds.get(partitionPath));
+ assertThat(afterIndexRanges.get(partitionPath))
+ .isEqualTo(beforeIndexRanges.get(partitionPath));
+ }
+ }
+ assertThat(afterIndexRanges.get(reassignedPartitionPath))
+ .hasSameSizeAs(beforeIndexRanges.get(reassignedPartitionPath));
+ for (Range range : afterIndexRanges.get(reassignedPartitionPath)) {
+ assertThat(range.from).isGreaterThanOrEqualTo(scrambledNextRowId);
+ assertThat(range.to).isLessThan(expectedNextRowId);
+ }
+
+ assertThat(table.snapshotManager().latestSnapshot().nextRowId())
+ .isEqualTo(expectedNextRowId);
+ assertThat(table.snapshotManager().latestSnapshot().totalRecordCount())
+ .isEqualTo(before.totalRecordCount());
+
assertThat(fileMetadataWithoutRowId(table)).isEqualTo(expectedFileMetadata);
+ assertThat(valueStatsByFile(table)).isEqualTo(expectedValueStats);
+ assertTableDataQueryable(table, expectedRows);
+ }
+
+ @Test
+ public void testReassignManyOutOfOrderPartitionEntries() throws Exception {
+ verifyReassignOutOfOrderPartitionEntries(LARGE_ENTRY_COUNT,
LARGE_MANIFEST_FILE_COUNT);
}
private FileStoreTable createTableWithInterleavedPartitions() throws
Exception {
@@ -462,6 +1128,88 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
return table;
}
+ private FileStoreTable createDataIntegrityTable() throws Exception {
+ createTableDefault();
+ FileStoreTable table = getTableDefault();
+ int nextId = 0;
+ for (int file = 0; file < DATA_INTEGRITY_FILES_PER_PARTITION; file++) {
+ for (int partition = 0; partition <
DATA_INTEGRITY_PARTITIONS.length; partition++) {
+ int rowCount = 3 + (file + partition) % 5;
+ int[] ids = new int[rowCount];
+ for (int row = 0; row < rowCount; row++) {
+ ids[row] = nextId++;
+ }
+ writeRows(table, DATA_INTEGRITY_PARTITIONS[partition], ids);
+ }
+ }
+ return table;
+ }
+
+ private long scrambleCurrentRowIds(FileStoreTable table) throws Exception {
+ Snapshot latest = table.snapshotManager().latestSnapshot();
+ assertThat(latest.indexManifest()).isNull();
+ List<ManifestEntry> currentEntries = currentEntries(table);
+ List<Long> rowIdSlots = new ArrayList<>(currentEntries.size());
+ for (int i = 0; i < currentEntries.size(); i++) {
+ rowIdSlots.add((i + 1L) * SCRAMBLED_ROW_ID_GAP);
+ }
+ Collections.shuffle(rowIdSlots, new Random(SCRAMBLED_ROW_ID_SEED));
+
+ Map<FileEntry.Identifier, Long> assignments = new HashMap<>();
+ for (int i = 0; i < currentEntries.size(); i++) {
+ assignments.put(currentEntries.get(i).identifier(),
rowIdSlots.get(i));
+ }
+
+ ManifestFile manifestFile =
table.store().manifestFileFactory().create();
+ ManifestList manifestList =
table.store().manifestListFactory().create();
+ List<ManifestFileMeta> rewrittenManifestMetas = new ArrayList<>();
+ int rewrittenEntryCount = 0;
+ for (ManifestFileMeta manifestMeta :
manifestList.readDataManifests(latest)) {
+ List<ManifestEntry> entries =
+ manifestFile.read(manifestMeta.fileName(),
manifestMeta.fileSize());
+ boolean rewritten = false;
+ for (int i = 0; i < entries.size(); i++) {
+ ManifestEntry entry = entries.get(i);
+ Long firstRowId =
+ entry.kind() == FileKind.ADD ?
assignments.get(entry.identifier()) : null;
+ if (firstRowId != null) {
+ entries.set(i, entry.assignFirstRowId(firstRowId));
+ rewritten = true;
+ rewrittenEntryCount++;
+ }
+ }
+ if (rewritten) {
+ rewrittenManifestMetas.addAll(manifestFile.write(entries));
+ } else {
+ rewrittenManifestMetas.add(manifestMeta);
+ }
+ }
+ assertThat(rewrittenEntryCount).isEqualTo(currentEntries.size());
+
+ Pair<String, Long> baseManifestList =
manifestList.write(rewrittenManifestMetas);
+ Pair<String, Long> deltaManifestList =
manifestList.write(Collections.emptyList());
+ long scrambledNextRowId = (currentEntries.size() + 10L) *
SCRAMBLED_ROW_ID_GAP;
+ try (FileStoreCommitImpl commit =
+ (FileStoreCommitImpl)
table.store().newCommit("test-scramble-row-ids", table)) {
+ assertThat(
+ commit.replaceManifestList(
+ latest,
+ latest.totalRecordCount(),
+ baseManifestList,
+ deltaManifestList,
+ null,
+ scrambledNextRowId))
+ .isTrue();
+ }
+
+ Map<FileEntry.Identifier, Long> committedAssignments = new HashMap<>();
+ for (ManifestEntry entry : currentEntries(table)) {
+ committedAssignments.put(entry.identifier(),
entry.file().nonNullFirstRowId());
+ }
+ assertThat(committedAssignments).isEqualTo(assignments);
+ return scrambledNextRowId;
+ }
+
private void verifyReassignOutOfOrderPartitionEntries(int entryCount, int
manifestFileCount)
throws Exception {
assertThat(entryCount % 2).isEqualTo(0);
@@ -791,16 +1539,184 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
try (BatchTableWrite write = builder.newWrite();
BatchTableCommit commit = builder.newCommit()) {
for (int id : ids) {
+ write.write(row(partition, id));
+ }
+ commit.commit(write.prepareCommit());
+ }
+ }
+
+ private void writePartialPayload(
+ FileStoreTable table, String partition, long firstRowId, String...
payloads)
+ throws Exception {
+ RowType writeType = table.rowType().project(Arrays.asList("pt",
"payload"));
+ BatchWriteBuilder builder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write =
builder.newWrite().withWriteType(writeType);
+ BatchTableCommit commit = builder.newCommit()) {
+ for (String payload : payloads) {
write.write(
GenericRow.of(
BinaryString.fromString(partition),
- id,
- BinaryString.fromString("v" + id)));
+ BinaryString.fromString(payload)));
+ }
+ List<CommitMessage> messages = write.prepareCommit();
+ assignFirstRowId(messages, firstRowId);
+ commit.commit(messages);
+ }
+ }
+
+ private FileStoreTable withManifestMergeOnNextAppend(FileStoreTable table)
{
+ Map<String, String> mergeOptions = new HashMap<>();
+ mergeOptions.put(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1GB");
+ mergeOptions.put(CoreOptions.MANIFEST_MERGE_MIN_COUNT.key(), "2");
+ mergeOptions.put(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(),
"1GB");
+ mergeOptions.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), "false");
+ return table.copy(mergeOptions);
+ }
+
+ private void assignFirstRowId(List<CommitMessage> messages, long
firstRowId) {
+ for (CommitMessage message : messages) {
+ CommitMessageImpl commitMessage = (CommitMessageImpl) message;
+ List<DataFileMeta> files =
+ new
ArrayList<>(commitMessage.newFilesIncrement().newFiles());
+ commitMessage.newFilesIncrement().newFiles().clear();
+ for (DataFileMeta file : files) {
+
commitMessage.newFilesIncrement().newFiles().add(file.assignFirstRowId(firstRowId));
}
+ }
+ }
+
+ private void writeRowsInPartitions(
+ FileStoreTable table,
+ String firstPartition,
+ int firstId,
+ String secondPartition,
+ int secondId)
+ throws Exception {
+ BatchWriteBuilder builder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write = builder.newWrite();
+ BatchTableCommit commit = builder.newCommit()) {
+ write.write(row(firstPartition, firstId));
+ write.write(row(secondPartition, secondId));
commit.commit(write.prepareCommit());
}
}
+ private GenericRow row(String partition, int id) {
+ return GenericRow.of(
+ partition == null ? null : BinaryString.fromString(partition),
+ id,
+ BinaryString.fromString("v" + id));
+ }
+
+ private void overwriteOneRow(FileStoreTable table, String partition, int
id) throws Exception {
+ BatchWriteBuilder builder =
table.newBatchWriteBuilder().withOverwrite();
+ try (BatchTableWrite write = builder.newWrite();
+ BatchTableCommit commit = builder.newCommit()) {
+ write.write(
+ GenericRow.of(
+ BinaryString.fromString(partition),
+ id,
+ BinaryString.fromString("v" + id)));
+ commit.commit(write.prepareCommit());
+ }
+ }
+
+ private void compactManifests(FileStoreTable table) throws Exception {
+ try (BatchTableCommit commit =
table.newBatchWriteBuilder().newCommit()) {
+ commit.compactManifests();
+ }
+ }
+
+ private FileEntry.Identifier compactOneFile(FileStoreTable table, String
partitionPath)
+ throws Exception {
+ ManifestEntry compactBefore =
+ currentEntries(table).stream()
+ .filter(
+ entry ->
+ table.store()
+ .pathFactory()
+
.getPartitionString(entry.partition())
+ .equals(partitionPath))
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ "Cannot find file in partition
" + partitionPath));
+ DataFilePathFactory pathFactory =
+ table.store()
+ .pathFactory()
+ .createDataFilePathFactory(
+ compactBefore.partition(),
compactBefore.bucket());
+ Path compactAfterPath = pathFactory.newPath();
+ table.fileIO().copyFile(pathFactory.toPath(compactBefore.file()),
compactAfterPath, false);
+ DataFileMeta compactAfter =
compactBefore.file().rename(compactAfterPath.getName());
+ CommitMessage message =
+ new CommitMessageImpl(
+ compactBefore.partition(),
+ compactBefore.bucket(),
+ compactBefore.totalBuckets(),
+ DataIncrement.emptyIncrement(),
+ new CompactIncrement(
+
Collections.singletonList(compactBefore.file()),
+ Collections.singletonList(compactAfter),
+ Collections.emptyList()));
+ try (BatchTableCommit commit =
table.newBatchWriteBuilder().newCommit()) {
+ commit.commit(Collections.singletonList(message));
+ }
+ return compactBefore.identifier();
+ }
+
+ private long assertAddDeleteEntriesConsistent(
+ FileStoreTable table, FileEntry.Identifier identifier) {
+ ManifestFile manifestFile =
table.store().manifestFileFactory().create();
+ ManifestList manifestList =
table.store().manifestListFactory().create();
+ Snapshot latest = table.snapshotManager().latestSnapshot();
+ List<ManifestEntry> matchingEntries = new ArrayList<>();
+ Set<String> matchingManifestFiles = new HashSet<>();
+ for (ManifestFileMeta manifestMeta :
manifestList.readDataManifests(latest)) {
+ for (ManifestEntry entry :
+ manifestFile.read(manifestMeta.fileName(),
manifestMeta.fileSize())) {
+ if (entry.identifier().equals(identifier)) {
+ matchingEntries.add(entry);
+ matchingManifestFiles.add(manifestMeta.fileName());
+ }
+ }
+ }
+
+ assertThat(matchingEntries).hasSize(2);
+ assertThat(matchingManifestFiles).hasSize(2);
+ ManifestEntry add =
+ matchingEntries.stream()
+ .filter(entry -> entry.kind() == FileKind.ADD)
+ .findFirst()
+ .orElseThrow(
+ () -> new AssertionError("Missing ADD entry
for " + identifier));
+ ManifestEntry delete =
+ matchingEntries.stream()
+ .filter(entry -> entry.kind() == FileKind.DELETE)
+ .findFirst()
+ .orElseThrow(
+ () -> new AssertionError("Missing DELETE entry
for " + identifier));
+ assertThat(delete.partition()).isEqualTo(add.partition());
+ assertThat(delete.bucket()).isEqualTo(add.bucket());
+ assertThat(delete.totalBuckets()).isEqualTo(add.totalBuckets());
+ assertThat(delete.file()).isEqualTo(add.file());
+ return add.file().nonNullFirstRowId();
+ }
+
+ private void updateStatistics(FileStoreTable table) throws Exception {
+ Snapshot latest = table.snapshotManager().latestSnapshot();
+ Statistics statistics =
+ new Statistics(
+ latest.id(),
+ latest.schemaId(),
+ latest.totalRecordCount(),
+ latest.totalRecordCount() * 100L);
+ try (BatchTableCommit commit =
table.newBatchWriteBuilder().newCommit()) {
+ commit.updateStatistics(statistics);
+ }
+ }
+
private void writeUnpartitionedRows(FileStoreTable table, int... ids)
throws Exception {
BatchWriteBuilder builder = table.newBatchWriteBuilder();
try (BatchTableWrite write = builder.newWrite();
@@ -819,15 +1735,100 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
table.coreOptions().partitionDefaultName());
}
+ private List<ManifestEntry> currentEntries(FileStoreTable table) {
+ return table.store()
+ .newScan()
+ .withSnapshot(table.snapshotManager().latestSnapshot())
+ .plan()
+ .files();
+ }
+
+ private List<ManifestEntry> currentEntriesAtRange(
+ FileStoreTable table, String partitionPath, long firstRowId, long
lastRowId) {
+ List<ManifestEntry> result = new ArrayList<>();
+ for (ManifestEntry entry : currentEntries(table)) {
+ String partition =
table.store().pathFactory().getPartitionString(entry.partition());
+ Range range = entry.file().nonNullRowIdRange();
+ if (partitionPath.equals(partition)
+ && range.from == firstRowId
+ && range.to == lastRowId) {
+ result.add(entry);
+ }
+ }
+ return result;
+ }
+
+ private Map<String, Long> rowCountsByPartition(FileStoreTable table) {
+ Map<String, Long> result = new LinkedHashMap<>();
+ for (ManifestEntry entry : currentEntries(table)) {
+ String partition =
table.store().pathFactory().getPartitionString(entry.partition());
+ result.merge(partition, entry.file().rowCount(), Long::sum);
+ }
+ return result;
+ }
+
+ private Map<String, List<Object>> fileMetadataWithoutRowId(FileStoreTable
table) {
+ Map<String, List<Object>> result = new LinkedHashMap<>();
+ for (ManifestEntry entry : currentEntries(table)) {
+ DataFileMeta file = entry.file();
+ String partition =
table.store().pathFactory().getPartitionString(entry.partition());
+ String key = partition + entry.bucket() + "/" + file.fileName();
+ List<Object> metadata =
+ Arrays.asList(
+ entry.kind(),
+ entry.totalBuckets(),
+ file.fileSize(),
+ file.rowCount(),
+ file.minSequenceNumber(),
+ file.maxSequenceNumber(),
+ file.schemaId(),
+ file.level(),
+ file.extraFiles(),
+ file.deleteRowCount(),
+ file.fileSource(),
+ file.valueStatsCols(),
+ file.externalPath(),
+ file.writeCols());
+ assertThat(result.put(key, metadata)).isNull();
+ }
+ return result;
+ }
+
+ private void assertPartitionsHaveLargeRowIdGaps(
+ Map<String, List<Long>> firstRowIdsByPartition) {
+
assertThat(firstRowIdsByPartition).hasSize(DATA_INTEGRITY_PARTITIONS.length);
+ for (String partition : DATA_INTEGRITY_PARTITIONS) {
+ List<Long> firstRowIds = firstRowIdsByPartition.get("pt=" +
partition + "/");
+
assertThat(firstRowIds).hasSize(DATA_INTEGRITY_FILES_PER_PARTITION);
+ for (int i = 1; i < firstRowIds.size(); i++) {
+ assertThat(firstRowIds.get(i) - firstRowIds.get(i - 1))
+ .isGreaterThanOrEqualTo(SCRAMBLED_ROW_ID_GAP);
+ }
+ }
+ }
+
+ private long assertContiguousRowIds(
+ Map<String, List<Long>> rowIdsByPartition,
+ Map<String, Long> rowCountsByPartition,
+ List<String> partitions,
+ long firstRowId) {
+ long nextRowId = firstRowId;
+ for (String partition : partitions) {
+ String partitionPath = "pt=" + partition + "/";
+ long rowCount = rowCountsByPartition.get(partitionPath);
+ List<Long> rowIds = rowIdsByPartition.get(partitionPath);
+ assertThat(rowIds).hasSize(Math.toIntExact(rowCount));
+ for (int i = 0; i < rowIds.size(); i++) {
+ assertThat(rowIds.get(i)).isEqualTo(nextRowId + i);
+ }
+ nextRowId += rowCount;
+ }
+ return nextRowId;
+ }
+
private Map<String, List<Long>> rowIdsByPartition(FileStoreTable table) {
- List<ManifestEntry> entries =
- table.store()
- .newScan()
- .withSnapshot(table.snapshotManager().latestSnapshot())
- .plan()
- .files();
Map<String, List<Long>> result = new LinkedHashMap<>();
- for (ManifestEntry entry : entries) {
+ for (ManifestEntry entry : currentEntries(table)) {
String partition =
table.store().pathFactory().getPartitionString(entry.partition());
result.computeIfAbsent(partition, k -> new ArrayList<>())
.add(entry.file().nonNullFirstRowId());
@@ -838,15 +1839,18 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
return result;
}
+ private Map<String, SimpleStats> valueStatsByFile(FileStoreTable table) {
+ Map<String, SimpleStats> result = new LinkedHashMap<>();
+ for (ManifestEntry entry : currentEntries(table)) {
+ SimpleStats previous = result.put(entry.file().fileName(),
entry.file().valueStats());
+ assertThat(previous).isNull();
+ }
+ return result;
+ }
+
private Map<String, List<Long>> expandedRowIdsByPartition(FileStoreTable
table) {
- List<ManifestEntry> entries =
- table.store()
- .newScan()
- .withSnapshot(table.snapshotManager().latestSnapshot())
- .plan()
- .files();
Map<String, List<Long>> result = new LinkedHashMap<>();
- for (ManifestEntry entry : entries) {
+ for (ManifestEntry entry : currentEntries(table)) {
String partition =
table.store().pathFactory().getPartitionString(entry.partition());
List<Long> rowIds = result.computeIfAbsent(partition, k -> new
ArrayList<>());
Range range = entry.file().nonNullRowIdRange();
@@ -971,6 +1975,52 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
}
String staleIndexManifest =
indexManifestFile.writeWithoutRolling(rewritten);
+ replaceLatestSnapshotIndexManifest(table, latest, staleIndexManifest);
+ }
+
+ private void appendGlobalIndexRange(FileStoreTable table, String
partition, Range rowRange)
+ throws Exception {
+ Snapshot latest = table.snapshotManager().latestSnapshot();
+ IndexManifestFile indexManifestFile =
table.store().indexManifestFileFactory().create();
+ List<IndexManifestEntry> entries =
indexManifestFile.read(latest.indexManifest());
+ IndexManifestEntry template = null;
+ for (IndexManifestEntry entry : entries) {
+ if (table.store()
+ .pathFactory()
+ .getPartitionString(entry.partition())
+ .equals(partition)) {
+ template = entry;
+ break;
+ }
+ }
+ assertThat(template).isNotNull();
+ IndexFileMeta indexFile = template.indexFile();
+ GlobalIndexMeta globalIndex = indexFile.globalIndexMeta();
+ assertThat(globalIndex).isNotNull();
+ entries.add(
+ new IndexManifestEntry(
+ template.kind(),
+ template.partition(),
+ template.bucket(),
+ new IndexFileMeta(
+ indexFile.indexType(),
+ indexFile.fileName(),
+ indexFile.fileSize(),
+ indexFile.rowCount(),
+ indexFile.dvRanges(),
+ indexFile.externalPath(),
+ new GlobalIndexMeta(
+ rowRange.from,
+ rowRange.to,
+ globalIndex.indexFieldId(),
+ globalIndex.extraFieldIds(),
+ globalIndex.indexMeta()))));
+ replaceLatestSnapshotIndexManifest(
+ table, latest, indexManifestFile.writeWithoutRolling(entries));
+ }
+
+ private void replaceLatestSnapshotIndexManifest(
+ FileStoreTable table, Snapshot latest, String indexManifest)
throws Exception {
Snapshot staleSnapshot =
new Snapshot(
latest.version(),
@@ -982,7 +2032,7 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
latest.deltaManifestListSize(),
latest.changelogManifestList(),
latest.changelogManifestListSize(),
- staleIndexManifest,
+ indexManifest,
latest.commitUser(),
latest.commitIdentifier(),
latest.commitKind(),
@@ -1025,6 +2075,100 @@ public class DataEvolutionRowIdReassignerTest extends
TableTestBase {
return ranges;
}
+ private Map<String, List<Range>>
globalIndexRangesByPartition(FileStoreTable table) {
+ Map<String, List<Range>> result = new LinkedHashMap<>();
+ List<IndexManifestEntry> entries =
+ table.store()
+ .indexManifestFileFactory()
+ .create()
+
.read(table.snapshotManager().latestSnapshot().indexManifest());
+ for (IndexManifestEntry entry : entries) {
+ GlobalIndexMeta globalIndex = entry.indexFile().globalIndexMeta();
+ if (globalIndex == null) {
+ continue;
+ }
+ String partition =
table.store().pathFactory().getPartitionString(entry.partition());
+ result.computeIfAbsent(partition, ignored -> new ArrayList<>())
+ .add(globalIndex.rowRange());
+ }
+ for (List<Range> ranges : result.values()) {
+ Collections.sort(
+ ranges,
+ (left, right) -> {
+ int compare = Long.compare(left.from, right.from);
+ return compare == 0 ? Long.compare(left.to, right.to)
: compare;
+ });
+ }
+ return result;
+ }
+
+ private void assertTableDataQueryable(FileStoreTable table, List<String>
expectedRows)
+ throws Exception {
+
assertThat(readTableRows(table)).containsExactlyElementsOf(expectedRows);
+
+ PredicateBuilder predicateBuilder = new
PredicateBuilder(table.rowType());
+ int partitionField = table.rowType().getFieldIndex("pt");
+ for (String partition : DATA_INTEGRITY_PARTITIONS) {
+ Predicate predicate =
+ predicateBuilder.equal(partitionField,
BinaryString.fromString(partition));
+ assertThat(readTableRows(table, predicate))
+ .containsExactlyElementsOf(rowsForPartition(expectedRows,
partition));
+ }
+
+ int idField = table.rowType().getFieldIndex("id");
+ int[] pointIds = {0, expectedRows.size() / 2, expectedRows.size() - 1};
+ for (int id : pointIds) {
+ Predicate predicate = predicateBuilder.equal(idField, id);
+ assertThat(readTableRows(table,
predicate)).containsAll(rowsForId(expectedRows, id));
+ }
+ }
+
+ private List<String> rowsForPartition(List<String> rows, String partition)
{
+ List<String> result = new ArrayList<>();
+ for (String row : rows) {
+ if (row.split("\\|", 3)[1].equals(partition)) {
+ result.add(row);
+ }
+ }
+ return result;
+ }
+
+ private List<String> rowsForId(List<String> rows, int id) {
+ List<String> result = new ArrayList<>();
+ String prefix = id + "|";
+ for (String row : rows) {
+ if (row.startsWith(prefix)) {
+ result.add(row);
+ }
+ }
+ return result;
+ }
+
+ private List<String> readTableRows(FileStoreTable table) throws Exception {
+ return readTableRows(table.newReadBuilder());
+ }
+
+ private List<String> readTableRows(FileStoreTable table, Predicate
predicate) throws Exception {
+ return readTableRows(table.newReadBuilder().withFilter(predicate));
+ }
+
+ private List<String> readTableRows(ReadBuilder readBuilder) throws
Exception {
+ List<String> result = new ArrayList<>();
+ readBuilder
+ .newRead()
+ .createReader(readBuilder.newScan().plan())
+ .forEachRemaining(
+ row -> {
+ InternalRow projected = row;
+ String partition =
projected.getString(0).toString();
+ int id = projected.getInt(1);
+ String payload = projected.getString(2).toString();
+ result.add(id + "|" + partition + "|" + payload);
+ });
+ Collections.sort(result);
+ return result;
+ }
+
private List<String> readPayloads(FileStoreTable table, Predicate
predicate) throws Exception {
ReadBuilder readBuilder = table.newReadBuilder().withFilter(predicate);
List<String> result = new ArrayList<>();
diff --git
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndexTest.java
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndexTest.java
index baeae7d34e..266574106c 100644
---
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndexTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/RowRangeMappingIndexTest.java
@@ -71,4 +71,33 @@ public class RowRangeMappingIndexTest {
assertThat(index.map(new Range(12, 17))).isEmpty();
}
+
+ @Test
+ public void testRelativeMappingCanBeShiftedToAbsoluteRowIds() {
+ RowRangeMappingIndex relative =
+ RowRangeMappingIndex.create(
+ Arrays.asList(
+ RowRangeMappingIndex.mapping(10, 14, 0),
+ RowRangeMappingIndex.mapping(20, 24, 5)));
+
+ RowRangeMappingIndex absolute = relative.shiftNewStarts(100L);
+ assertThat(absolute.map(new Range(10, 14))).hasValue(new Range(100,
104));
+ assertThat(absolute.map(new Range(20, 24))).hasValue(new Range(105,
109));
+ }
+
+ @Test
+ public void testOverlaps() {
+ RowRangeMappingIndex index =
+ RowRangeMappingIndex.create(
+ Arrays.asList(
+ RowRangeMappingIndex.mapping(10, 14, 100),
+ RowRangeMappingIndex.mapping(20, 24, 105)));
+
+ assertThat(index.overlaps(new Range(5, 9))).isFalse();
+ assertThat(index.overlaps(new Range(5, 10))).isTrue();
+ assertThat(index.overlaps(new Range(15, 19))).isFalse();
+ assertThat(index.overlaps(new Range(14, 20))).isTrue();
+ assertThat(index.overlaps(new Range(24, 30))).isTrue();
+ assertThat(index.overlaps(new Range(25, 30))).isFalse();
+ }
}