This is an automated email from the ASF dual-hosted git repository.
rdblue pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 729e650b9e Core: Support manifest DVs in V4ManifestReader, remove
includeAll (#18248)
729e650b9e is described below
commit 729e650b9e0a9f706ea8bea795b3a6dfb58e306b
Author: Ryan Blue <[email protected]>
AuthorDate: Fri Sep 25 14:22:32 2026 -0700
Core: Support manifest DVs in V4ManifestReader, remove includeAll (#18248)
---
.../java/org/apache/iceberg/V4ManifestReader.java | 38 +-
.../org/apache/iceberg/TestV4ManifestReader.java | 390 ++++++++++++++-------
.../java/org/apache/iceberg/V4TestComparators.java | 4 +-
.../apache/iceberg/mumbling/MumblingTestUtil.java | 10 +-
4 files changed, 286 insertions(+), 156 deletions(-)
diff --git a/core/src/main/java/org/apache/iceberg/V4ManifestReader.java
b/core/src/main/java/org/apache/iceberg/V4ManifestReader.java
index 1122cb5750..09515b3f11 100644
--- a/core/src/main/java/org/apache/iceberg/V4ManifestReader.java
+++ b/core/src/main/java/org/apache/iceberg/V4ManifestReader.java
@@ -68,12 +68,12 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
}
private final ManifestFile manifest;
+ private final ManifestBitmap dv;
private final FileIO io;
private final Schema readSchema;
private final String tableLocation;
private final InclusiveStatsEvaluator statsFilter;
private final Map<Integer, Pair<Evaluator, StructProjection>>
partitionFilters; // by spec ID
- private final boolean includeAll;
private final Set<Integer> requestedStatsFieldIds;
private final boolean isUncommitted;
private final ScanMetrics scanMetrics;
@@ -87,17 +87,16 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
String tableLocation,
InclusiveStatsEvaluator statsFilter,
Map<Integer, Pair<Evaluator, StructProjection>> partitionFilters,
- boolean includeAll,
Set<Integer> requestedStatsFieldIds,
boolean isUncommitted,
ScanMetrics scanMetrics) {
this.manifest = manifest;
+ this.dv = manifest.manifestDeletionVector();
this.io = io;
this.readSchema = readSchema;
this.tableLocation = tableLocation;
this.statsFilter = statsFilter;
this.partitionFilters = partitionFilters;
- this.includeAll = includeAll;
this.requestedStatsFieldIds = requestedStatsFieldIds;
this.isUncommitted = isUncommitted;
this.scanMetrics = scanMetrics;
@@ -113,10 +112,16 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
@Override
public CloseableIterator<TrackedFile> iterator() {
+ // first row ID assignment must happen first to maintain consistent
assignment when files are
+ // removed or filtered
CloseableIterable<TrackedFile> files =
CloseableIterable.transform(open(), this::applyInheritance);
- if (!includeAll) {
+ if (dv != null) {
+ files =
+ CloseableIterable.filter(
+ files, file -> file.tracking().isLive() &&
!isDeletedByMDV(file));
+ } else {
files = CloseableIterable.filter(files, file ->
file.tracking().isLive());
}
@@ -156,6 +161,10 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
return file;
}
+ private boolean isDeletedByMDV(TrackedFile file) {
+ return dv.isSet(Math.toIntExact(file.tracking().manifestPos()));
+ }
+
private boolean matchesPartition(TrackedFile trackedFile) {
Integer specId = trackedFile.specId();
if (specId == null) {
@@ -237,10 +246,10 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
copy.setLocation(LocationUtil.resolveLocation(tableLocation,
copy.location()));
}
- DeletionVector dv = copy.deletionVector();
- if (dv != null && dv.location() != null) {
- ((DeletionVectorStruct) dv)
- .setLocation(LocationUtil.resolveLocation(tableLocation,
dv.location()));
+ DeletionVector deletionVector = copy.deletionVector();
+ if (deletionVector != null && deletionVector.location() != null) {
+ ((DeletionVectorStruct) deletionVector)
+ .setLocation(LocationUtil.resolveLocation(tableLocation,
deletionVector.location()));
}
return copy;
@@ -261,7 +270,6 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
private String tableLocation = null;
private Expression rowFilter = Expressions.alwaysTrue();
private boolean caseSensitive = true;
- private boolean includeAll = false;
private boolean scanPlanning = false;
private Set<String> requestedColumns = null;
private Schema requestedProjection = null;
@@ -291,11 +299,6 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
Preconditions.checkArgument(
format != null, "Cannot determine format of manifest: %s",
manifest.path());
- if (manifest.manifestDeletionVector() != null) {
- throw new UnsupportedOperationException(
- "Cannot read manifest with a deletion vector: " + manifest.path());
- }
-
this.manifest = manifest;
this.io = io;
this.tableSchema = tableSchema;
@@ -328,12 +331,6 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
return this;
}
- /** Returns all entries without filtering by {@link Tracking#isLive()
liveness}. */
- Builder includeAll() {
- this.includeAll = true;
- return this;
- }
-
/** Configures the reader to select the minimal fields needed for scan
planning. */
Builder forScanPlanning() {
Preconditions.checkState(
@@ -420,7 +417,6 @@ class V4ManifestReader extends CloseableGroup implements
CloseableIterable<Track
tableLocation,
statsFilter(),
partitionFilters,
- includeAll,
requestedStatsFieldIds,
isUncommitted,
scanMetrics);
diff --git a/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java
b/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java
index 0a9c55ced6..94e448e8b8 100644
--- a/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java
+++ b/core/src/test/java/org/apache/iceberg/TestV4ManifestReader.java
@@ -40,6 +40,7 @@ import org.apache.iceberg.io.FileAppender;
import org.apache.iceberg.io.OutputFile;
import org.apache.iceberg.metrics.DefaultMetricsContext;
import org.apache.iceberg.metrics.ScanMetrics;
+import org.apache.iceberg.mumbling.MumblingTestUtil;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
@@ -117,6 +118,8 @@ class TestV4ManifestReader {
V4TestComparators.trackedFileStatusOnly(ID_PARTITIONED_TYPE);
private static final Comparator<TrackedFile> UNPARTITIONED_FILE_COMPARATOR =
V4TestComparators.trackedFileStatusOnly(UNPARTITIONED_TYPE);
+ private static final Comparator<TrackedFile> FILE_AND_TRACKING_COMPARATOR =
+ V4TestComparators.trackedFile(UNPARTITIONED_TYPE);
// shared data files: FILE_A is in partition id=1, FILE_B in partition id=2
private static final TrackedFile UNPARTITIONED_FILE =
@@ -318,49 +321,137 @@ class TestV4ManifestReader {
@FieldSource("MANIFEST_FORMATS")
public void statusFilter(FileFormat format) throws IOException {
List<TrackedFile> files =
- ImmutableList.of(
- unpartitionedFileWithStatus(EntryStatus.ADDED,
"s3://bucket/added.parquet"),
- unpartitionedFileWithStatus(EntryStatus.MODIFIED,
"s3://bucket/modified.parquet"),
- unpartitionedFileWithStatus(EntryStatus.DELETED,
"s3://bucket/deleted.parquet"),
- unpartitionedFileWithStatus(EntryStatus.EXISTING,
"s3://bucket/existing.parquet"),
- unpartitionedFileWithStatus(EntryStatus.REPLACED,
"s3://bucket/replaced.parquet"));
+ List.of(
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 42L, null, null, null,
null, null, null),
+ "s3://bucket/added.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.MODIFIED, 40L, 5L, 5L, 42L,
5_000L, null, null),
+ "s3://bucket/modified.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.DELETED, 42L, 2L, 2L, null,
1_000L, null, null),
+ "s3://bucket/deleted.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.EXISTING, 38L, 4L, 4L, null,
3_000L, null, null),
+ "s3://bucket/existing.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.REPLACED, 42L, 2L, 2L, 40L,
2_000L, null, null),
+ "s3://bucket/replaced.parquet"));
ManifestFile manifest = writeManifest(format, UNPARTITIONED_TYPE, files);
+ when(manifest.firstRowId()).thenReturn(10_000L);
- List<TrackedFile> liveFiles =
- read(
- V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
UNPARTITIONED_SPECS)
- .metricsConfig(METRICS_CONFIG));
+ List<TrackedFile> expectedFiles =
+ List.of(
+ // inherits seq number and assigned first row ID
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, 42L, MANIFEST_SEQ, MANIFEST_SEQ, null,
10_000L, null, null),
+ "s3://bucket/added.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.MODIFIED, 40L, 5L, 5L, 42L,
5_000L, null, null),
+ "s3://bucket/modified.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.DELETED, 42L, 2L, 2L, null,
1_000L, null, null),
+ "s3://bucket/deleted.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.EXISTING, 38L, 4L, 4L, null,
3_000L, null, null),
+ "s3://bucket/existing.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.REPLACED, 42L, 2L, 2L, 40L,
2_000L, null, null),
+ "s3://bucket/replaced.parquet"));
+
+ V4ManifestReader.Builder builder =
+ V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
UNPARTITIONED_SPECS)
+ .metricsConfig(METRICS_CONFIG);
+
+ List<TrackedFile> liveFiles = read(builder);
assertThat(liveFiles)
- .usingComparatorForType(FILE_COMPARATOR, TrackedFile.class)
- .containsExactly(files.get(0), files.get(1), files.get(3));
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .containsExactly(expectedFiles.get(0), expectedFiles.get(1),
expectedFiles.get(3));
+ }
- List<TrackedFile> allFiles =
- read(
- V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
UNPARTITIONED_SPECS)
- .metricsConfig(METRICS_CONFIG)
- .includeAll());
- assertThat(allFiles)
- .usingComparatorForType(FILE_COMPARATOR, TrackedFile.class)
- .containsExactlyElementsOf(files);
+ @ParameterizedTest
+ @FieldSource("MANIFEST_FORMATS")
+ public void mdvFilter(FileFormat format) throws IOException {
+ List<TrackedFile> files =
+ List.of(
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 42L, null, null, null,
null, null, null),
+ "s3://bucket/added.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.MODIFIED, 40L, 5L, 5L, 42L,
5_000L, null, null),
+ "s3://bucket/modified.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.DELETED, 42L, 2L, 2L, null,
1_000L, null, null),
+ "s3://bucket/deleted.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.EXISTING, 38L, 4L, 4L, null,
3_000L, null, null),
+ "s3://bucket/existing.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.REPLACED, 42L, 2L, 2L, 40L,
2_000L, null, null),
+ "s3://bucket/replaced.parquet"));
+
+ ManifestFile manifest = writeManifest(format, UNPARTITIONED_TYPE, files);
+
+ // this bitmap deletes a MODIFIED entry and an already DELETED entry
+ when(manifest.manifestDeletionVector())
+ .thenReturn(MumblingTestUtil.bitmap(MumblingTestUtil.sparse(1, 2)));
+ when(manifest.firstRowId()).thenReturn(10_000L);
+
+ List<TrackedFile> expectedFiles =
+ List.of(
+ // inherits seq number and assigned first row ID
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, 42L, MANIFEST_SEQ, MANIFEST_SEQ, null,
10_000L, null, null),
+ "s3://bucket/added.parquet"),
+ // status changed to DELETED, delete snapshot ID is unknown
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.DELETED, null, 5L, 5L, 42L,
5_000L, null, null),
+ "s3://bucket/modified.parquet"),
+ // status already DELETED, no modification
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.DELETED, 42L, 2L, 2L, null,
1_000L, null, null),
+ "s3://bucket/deleted.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.EXISTING, 38L, 4L, 4L, null,
3_000L, null, null),
+ "s3://bucket/existing.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.REPLACED, 42L, 2L, 2L, 40L,
2_000L, null, null),
+ "s3://bucket/replaced.parquet"));
+
+ V4ManifestReader.Builder builder =
+ V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
UNPARTITIONED_SPECS)
+ .metricsConfig(METRICS_CONFIG);
+
+ List<TrackedFile> liveFiles = read(builder);
+ assertThat(liveFiles)
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .containsExactly(expectedFiles.get(0), expectedFiles.get(3));
}
@ParameterizedTest
@FieldSource("MANIFEST_FORMATS")
public void inheritanceUncommittedOnlyInheritsSnapshotId(FileFormat format)
throws IOException {
- Tracking trackingWithSnapshotId =
- new TrackingStruct(EntryStatus.ADDED, 1234567L, null, null, null,
null, null, null);
TrackedFile withSnapshotId =
- unpartitionedDataFile(trackingWithSnapshotId,
"s3://bucket/table/file-b.parquet");
- Tracking trackingWithoutSnapshotId =
- new TrackingStruct(EntryStatus.ADDED, null, null, null, null, null,
null, null);
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 1234567L, null, null, null,
null, null, null),
+ "s3://bucket/table/file-b.parquet");
TrackedFile withoutSnapshotId =
- unpartitionedDataFile(trackingWithoutSnapshotId,
"s3://bucket/table/file-a.parquet");
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, null, null, null, null,
null, null, null),
+ "s3://bucket/table/file-a.parquet");
ManifestFile manifest =
writeManifest(
format, UNPARTITIONED_TYPE, ImmutableList.of(withSnapshotId,
withoutSnapshotId));
+ // inheritable values can be set but only snapshot ID is inherited
+ when(manifest.firstRowId()).thenReturn(10_000L);
+ when(manifest.snapshotId()).thenReturn(34L);
+ when(manifest.sequenceNumber()).thenReturn(5L);
+
assertThat(manifest.sequenceNumber())
.as("Manifest metadata has a sequence number that will not be
inherited")
.isNotNull();
@@ -370,136 +461,162 @@ class TestV4ManifestReader {
.metricsConfig(METRICS_CONFIG);
List<TrackedFile> actual = read(builder);
+ List<TrackedFile> expected =
+ List.of(
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 1234567L, null, null,
null, null, null, null),
+ "s3://bucket/table/file-b.parquet"),
+ // inherits only snapshot ID
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 34L, null, null, null,
null, null, null),
+ "s3://bucket/table/file-a.parquet"));
+
assertThat(actual)
- .extracting(file -> file.tracking().snapshotId())
- .containsExactly(1234567L, SNAPSHOT_ID);
- assertThat(actual)
- .as("Uncommitted files do not inherit data sequence number")
- .extracting(file -> file.tracking().dataSequenceNumber())
- .containsOnlyNulls();
- assertThat(actual)
- .as("Uncommitted files do not inherit file sequence number")
- .extracting(file -> file.tracking().fileSequenceNumber())
- .containsOnlyNulls();
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .isEqualTo(expected);
}
@ParameterizedTest
@FieldSource("MANIFEST_FORMATS")
public void inheritanceSnapshotId(FileFormat format) throws IOException {
- Tracking trackingWithSnapshotId =
- new TrackingStruct(EntryStatus.ADDED, 1234567L, null, null, null,
null, null, null);
TrackedFile withSnapshotId =
- unpartitionedDataFile(trackingWithSnapshotId,
"s3://bucket/table/file-b.parquet");
- Tracking trackingWithoutSnapshotId =
- new TrackingStruct(EntryStatus.ADDED, null, null, null, null, null,
null, null);
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 1234567L, 5L, 5L, null,
5_000L, null, null),
+ "s3://bucket/table/file-b.parquet");
TrackedFile withoutSnapshotId =
- unpartitionedDataFile(trackingWithoutSnapshotId,
"s3://bucket/table/file-a.parquet");
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, null, 5L, 5L, null, 5_100L,
null, null),
+ "s3://bucket/table/file-a.parquet");
ManifestFile manifest =
writeManifest(
format, UNPARTITIONED_TYPE, ImmutableList.of(withSnapshotId,
withoutSnapshotId));
+ when(manifest.firstRowId()).thenReturn(10_000L);
+ when(manifest.snapshotId()).thenReturn(34L);
+
V4ManifestReader.Builder builder =
V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
ID_PARTITIONING_SPECS)
.metricsConfig(METRICS_CONFIG);
List<TrackedFile> actual = read(builder);
+ List<TrackedFile> expected =
+ List.of(
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 1234567L, 5L, 5L, null,
5_000L, null, null),
+ "s3://bucket/table/file-b.parquet"),
+ // inherits only snapshot ID
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, 34L, 5L, 5L, null,
5_100L, null, null),
+ "s3://bucket/table/file-a.parquet"));
+
assertThat(actual)
- .extracting(file -> file.tracking().snapshotId())
- .containsExactly(1234567L, SNAPSHOT_ID);
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .isEqualTo(expected);
}
@ParameterizedTest
@FieldSource("MANIFEST_FORMATS")
public void inheritanceDVSnapshotIdNotInherited(FileFormat format) throws
IOException {
- Tracking trackingWithDVSnapshotId =
- new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, null, null,
1234567L, null, null, null);
TrackedFile withDVSnapshotId =
- unpartitionedDataFile(trackingWithDVSnapshotId,
"s3://bucket/table/file-b.parquet");
- Tracking trackingWithoutDVSnapshotId =
- new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, null, null, null,
null, null, null);
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, 1234567L, 5_000L,
null, null),
+ "s3://bucket/table/file-b.parquet");
TrackedFile withoutDVSnapshotId =
- unpartitionedDataFile(trackingWithoutDVSnapshotId,
"s3://bucket/table/file-a.parquet");
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null,
5_100L, null, null),
+ "s3://bucket/table/file-a.parquet");
ManifestFile manifest =
writeManifest(
format, UNPARTITIONED_TYPE, ImmutableList.of(withDVSnapshotId,
withoutDVSnapshotId));
+ when(manifest.firstRowId()).thenReturn(10_000L);
+ when(manifest.snapshotId()).thenReturn(34L);
+
V4ManifestReader.Builder builder =
V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
ID_PARTITIONING_SPECS)
.metricsConfig(METRICS_CONFIG);
List<TrackedFile> actual = read(builder);
+ List<TrackedFile> expected =
+ List.of(
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, 1234567L, 5_000L,
null, null),
+ "s3://bucket/table/file-b.parquet"),
+ // inherits only snapshot ID
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null, 5_100L,
null, null),
+ "s3://bucket/table/file-a.parquet"));
+
assertThat(actual)
- .extracting(file -> file.tracking().dvSnapshotId())
- .containsExactly(1234567L, null);
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .isEqualTo(expected);
}
@ParameterizedTest
@FieldSource("MANIFEST_FORMATS")
- public void inheritanceAddedDataSequenceNumber(FileFormat format) throws
IOException {
- Tracking trackingWithoutSeq =
- new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, null, null, null,
null, null, null);
+ public void inheritanceAddedSequenceNumbers(FileFormat format) throws
IOException {
TrackedFile withoutSeq =
- unpartitionedDataFile(trackingWithoutSeq,
"s3://bucket/table/file-a.parquet");
- Tracking trackingWithDataSeq =
- new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, 500L, null, null,
null, null, null);
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, null, null, null, 5_000L,
null, null),
+ "s3://bucket/table/file-a.parquet");
TrackedFile withDataSeq =
- unpartitionedDataFile(trackingWithDataSeq,
"s3://bucket/table/file-b.parquet");
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 500L, 500L, null, 5_100L,
null, null),
+ "s3://bucket/table/file-b.parquet");
ManifestFile manifest =
writeManifest(format, UNPARTITIONED_TYPE, ImmutableList.of(withoutSeq,
withDataSeq));
- V4ManifestReader.Builder builder =
- V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
ID_PARTITIONING_SPECS)
- .metricsConfig(METRICS_CONFIG);
- List<TrackedFile> actual = read(builder);
-
- assertThat(actual)
- .extracting(file -> file.tracking().dataSequenceNumber())
- .containsExactly(MANIFEST_SEQ, 500L);
- }
-
- @ParameterizedTest
- @FieldSource("MANIFEST_FORMATS")
- public void inheritanceAddedFileSequenceNumber(FileFormat format) throws
IOException {
- Tracking trackingWithoutSeq =
- new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, null, null, null,
null, null, null);
- TrackedFile withoutSeq =
- unpartitionedDataFile(trackingWithoutSeq,
"s3://bucket/table/file-a.parquet");
- Tracking trackingWithFileSeq =
- new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, null, 500L, null,
null, null, null);
- TrackedFile withFileSeq =
- unpartitionedDataFile(trackingWithFileSeq,
"s3://bucket/table/file-c.parquet");
-
- ManifestFile manifest =
- writeManifest(format, UNPARTITIONED_TYPE, ImmutableList.of(withoutSeq,
withFileSeq));
+ when(manifest.firstRowId()).thenReturn(10_000L);
+ when(manifest.snapshotId()).thenReturn(34L);
+ when(manifest.sequenceNumber()).thenReturn(5L);
V4ManifestReader.Builder builder =
V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
ID_PARTITIONING_SPECS)
.metricsConfig(METRICS_CONFIG);
List<TrackedFile> actual = read(builder);
+ List<TrackedFile> expected =
+ List.of(
+ // inherits sequence numbers
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null, 5_000L,
null, null),
+ "s3://bucket/table/file-a.parquet"),
+ // does not inherit sequence numbers
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 500L, 500L, null, 5_100L,
null, null),
+ "s3://bucket/table/file-b.parquet"));
+
assertThat(actual)
- .extracting(file -> file.tracking().fileSequenceNumber())
- .containsExactly(MANIFEST_SEQ, 500L);
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .isEqualTo(expected);
}
@ParameterizedTest
@FieldSource("MANIFEST_FORMATS")
public void inheritanceFirstRowId(FileFormat format) throws IOException {
- Tracking trackingWithoutFirstRowId =
- new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, null, null, null,
null, null, null);
TrackedFile withoutFirstRowId =
- unpartitionedDataFile(trackingWithoutFirstRowId,
"s3://bucket/table/file-a.parquet");
- Tracking trackingWithFirstRowId =
- new TrackingStruct(EntryStatus.EXISTING, SNAPSHOT_ID, null, null,
null, 5_000L, null, null);
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null,
null, null, null),
+ "s3://bucket/table/file-a.parquet");
TrackedFile withFirstRowId =
- unpartitionedDataFile(trackingWithFirstRowId,
"s3://bucket/table/file-c.parquet");
+ unpartitionedDataFile(
+ new TrackingStruct(EntryStatus.EXISTING, SNAPSHOT_ID, 5L, 5L,
null, 5_000L, null, null),
+ "s3://bucket/table/file-c.parquet");
ManifestFile manifest =
writeManifest(
format, UNPARTITIONED_TYPE, ImmutableList.of(withoutFirstRowId,
withFirstRowId));
+
when(manifest.firstRowId()).thenReturn(10_000L);
V4ManifestReader.Builder builder =
@@ -507,9 +624,22 @@ class TestV4ManifestReader {
.metricsConfig(METRICS_CONFIG);
List<TrackedFile> actual = read(builder);
+ List<TrackedFile> expected =
+ List.of(
+ // assigns a new first row ID
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null, 10_000L,
null, null),
+ "s3://bucket/table/file-a.parquet"),
+ // does not modify first row ID
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.EXISTING, SNAPSHOT_ID, 5L, 5L, null, 5_000L,
null, null),
+ "s3://bucket/table/file-c.parquet"));
+
assertThat(actual)
- .extracting(file -> file.tracking().firstRowId())
- .containsExactly(10_000L, 5_000L);
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .isEqualTo(expected);
}
@ParameterizedTest
@@ -518,7 +648,7 @@ class TestV4ManifestReader {
Tracking trackingWithoutFirstRowId =
new TrackingStruct(EntryStatus.ADDED, SNAPSHOT_ID, null, null, null,
null, null, null);
Tracking trackingWithFirstRowId =
- new TrackingStruct(EntryStatus.EXISTING, SNAPSHOT_ID, null, null,
null, 5_000L, null, null);
+ new TrackingStruct(EntryStatus.EXISTING, SNAPSHOT_ID, 2L, 2L, null,
5_000L, null, null);
TrackedFile fileA =
unpartitionedDataFile(trackingWithoutFirstRowId,
"s3://bucket/table/file-a.parquet");
@@ -534,17 +664,42 @@ class TestV4ManifestReader {
ManifestFile manifest =
writeManifest(
format, UNPARTITIONED_TYPE, ImmutableList.of(fileA, fileB, fileC,
fileD, fileE));
+
when(manifest.firstRowId()).thenReturn(10_000L);
+ when(manifest.snapshotId()).thenReturn(34L);
+ when(manifest.sequenceNumber()).thenReturn(5L);
V4ManifestReader.Builder builder =
V4ManifestReader.builder(manifest, IO, TABLE_SCHEMA,
ID_PARTITIONING_SPECS)
.metricsConfig(METRICS_CONFIG);
List<TrackedFile> actual = read(builder);
+ List<TrackedFile> expected =
+ List.of(
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null, 10_000L,
null, null),
+ "s3://bucket/table/file-a.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null, 10_100L,
null, null),
+ "s3://bucket/table/file-b.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null, 10_200L,
null, null),
+ "s3://bucket/table/file-c.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.EXISTING, SNAPSHOT_ID, 2L, 2L, null, 5_000L,
null, null),
+ "s3://bucket/table/file-d.parquet"),
+ unpartitionedDataFile(
+ new TrackingStruct(
+ EntryStatus.ADDED, SNAPSHOT_ID, 5L, 5L, null, 10_300L,
null, null),
+ "s3://bucket/table/file-e.parquet"));
+
assertThat(actual)
- .as("Each assignment should increment by rocord_count=100")
- .extracting(file -> file.tracking().firstRowId())
- .containsExactly(10_000L, 10_100L, 10_200L, 5_000L, 10_300L);
+ .usingComparatorForType(FILE_AND_TRACKING_COMPARATOR,
TrackedFile.class)
+ .isEqualTo(expected);
}
@ParameterizedTest
@@ -1450,21 +1605,6 @@ class TestV4ManifestReader {
.hasMessageContaining("Cannot read manifest with content DELETES");
}
- @Test
- public void validationManifestDeletionVectorFailure() {
- ManifestFile fileWithMDV = Mockito.mock(ManifestFile.class);
-
Mockito.when(fileWithMDV.path()).thenReturn(FileFormat.PARQUET.addExtension("manifest"));
- Mockito.when(fileWithMDV.formatVersion()).thenReturn(4);
- Mockito.when(fileWithMDV.content()).thenReturn(ManifestContent.DATA);
- Mockito.when(fileWithMDV.manifestDeletionVector())
- .thenReturn(Mockito.mock(ManifestBitmap.class));
-
- assertThatThrownBy(
- () -> V4ManifestReader.builder(fileWithMDV, IO, TABLE_SCHEMA,
UNPARTITIONED_SPECS))
- .isInstanceOf(UnsupportedOperationException.class)
- .hasMessageContaining("Cannot read manifest with a deletion vector");
- }
-
@Test
public void validationChecksBuilderArguments() {
assertThatThrownBy(
@@ -1616,7 +1756,17 @@ class TestV4ManifestReader {
}
private static TrackedFile unpartitionedFileWithoutStats(String location) {
- return unpartitionedFileWithStatus(EntryStatus.ADDED, location);
+ Tracking tracking =
+ new TrackingStruct(
+ EntryStatus.ADDED,
+ SNAPSHOT_ID,
+ 3L, // data sequence number
+ 3L, // file sequence number
+ null, // dv snapshot id
+ null, // first row id
+ null, // deleted positions
+ null); // replaced positions
+ return unpartitionedDataFile(tracking, location, null /* no stats */, null
/* no DV */);
}
private static TrackedFile idPartitionedDataFileWithoutStats(
@@ -1702,20 +1852,6 @@ class TestV4ManifestReader {
return unpartitionedDataFile(tracking, location, null /* no stats */, null
/* no DV */);
}
- private static TrackedFile unpartitionedFileWithStatus(EntryStatus status,
String location) {
- Tracking tracking =
- new TrackingStruct(
- status,
- SNAPSHOT_ID,
- 3L, // data sequence number
- 3L, // file sequence number
- null, // dv snapshot id
- null, // first row id
- null, // deleted positions
- null); // replaced positions
- return unpartitionedDataFile(tracking, location, null /* no stats */, null
/* no DV */);
- }
-
private static TrackedFile unpartitionedDataFile(
Tracking tracking, String location, ContentStats stats, DeletionVector
dv) {
return new TrackedFileStruct(
diff --git a/core/src/test/java/org/apache/iceberg/V4TestComparators.java
b/core/src/test/java/org/apache/iceberg/V4TestComparators.java
index 2f79fc3907..4905d6032f 100644
--- a/core/src/test/java/org/apache/iceberg/V4TestComparators.java
+++ b/core/src/test/java/org/apache/iceberg/V4TestComparators.java
@@ -94,9 +94,7 @@ class V4TestComparators {
.thenComparing(Tracking::dvSnapshotId, natural())
.thenComparing(Tracking::firstRowId, natural())
.thenComparing(Tracking::deletedPositions, BYTES)
- .thenComparing(Tracking::replacedPositions, BYTES)
- .thenComparing(Tracking::manifestLocation, natural())
- .thenComparingLong(Tracking::manifestPos));
+ .thenComparing(Tracking::replacedPositions, BYTES));
// compare only status, ignoring inherited fields and fields set during a
write
static final Comparator<Tracking> STATUS_ONLY_TRACKING =
diff --git
a/core/src/test/java/org/apache/iceberg/mumbling/MumblingTestUtil.java
b/core/src/test/java/org/apache/iceberg/mumbling/MumblingTestUtil.java
index 6c941fec6d..38c8efcdf0 100644
--- a/core/src/test/java/org/apache/iceberg/mumbling/MumblingTestUtil.java
+++ b/core/src/test/java/org/apache/iceberg/mumbling/MumblingTestUtil.java
@@ -30,11 +30,11 @@ public class MumblingTestUtil {
return ByteBuffers.toByteArray(build(sparse(0)));
}
- static MumblingBitmap bitmap(Container... containers) {
+ public static MumblingBitmap bitmap(Container... containers) {
return new MumblingBitmap(build(containers));
}
- static ByteBuffer build(Container... containers) {
+ public static ByteBuffer build(Container... containers) {
Preconditions.checkArgument(
containers.length <= 8192, "Invalid container count (max 8192): %s",
containers.length);
@@ -77,7 +77,7 @@ public class MumblingTestUtil {
return buf;
}
- static Container sparse(int... positions) {
+ public static Container sparse(int... positions) {
byte[] bytes = new byte[positions.length];
for (int i = 0; i < positions.length; i += 1) {
if (i > 0) {
@@ -99,12 +99,12 @@ public class MumblingTestUtil {
}
/** Descriptor + bytes for a dense container. */
- static Container dense(byte[] container) {
+ public static Container dense(byte[] container) {
Preconditions.checkArgument(container.length == 32, "Dense container must
be 32 bytes");
return new Container(container);
}
- static class Container {
+ public static class Container {
private final byte[] bytes;
private final int descriptor;
private final int cardinality;