You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by st...@apache.org on 2026/06/03 18:29:48 UTC
(iceberg) branch main updated: Core: Refactor v4 struct builders to improve validation (#16408)
This is an automated email from the ASF dual-hosted git repository.
stevenzwu 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 c646404008 Core: Refactor v4 struct builders to improve validation (#16408)
c646404008 is described below
commit c6464040081ef6c578a59a08f9d668466486dd56
Author: Anoop Johnson <an...@apache.org>
AuthorDate: Wed Jun 3 11:29:41 2026 -0700
Core: Refactor v4 struct builders to improve validation (#16408)
---
.../org/apache/iceberg/DeletionVectorStruct.java | 24 +-
.../org/apache/iceberg/ManifestInfoStruct.java | 107 +++--
.../java/org/apache/iceberg/TrackingBuilder.java | 179 ++++++++
.../java/org/apache/iceberg/TrackingStruct.java | 97 +----
.../apache/iceberg/TestDeletionVectorStruct.java | 47 ++-
.../org/apache/iceberg/TestManifestInfoStruct.java | 257 ++++++++++--
.../org/apache/iceberg/TestTrackedFileStruct.java | 20 +-
.../org/apache/iceberg/TestTrackingStruct.java | 464 +++++++++++++++++----
8 files changed, 934 insertions(+), 261 deletions(-)
diff --git a/core/src/main/java/org/apache/iceberg/DeletionVectorStruct.java b/core/src/main/java/org/apache/iceberg/DeletionVectorStruct.java
index 0eb7c2fe1e..04d23fa33a 100644
--- a/core/src/main/java/org/apache/iceberg/DeletionVectorStruct.java
+++ b/core/src/main/java/org/apache/iceberg/DeletionVectorStruct.java
@@ -51,7 +51,7 @@ class DeletionVectorStruct extends SupportsIndexProjection implements DeletionVe
}
private DeletionVectorStruct(String location, long offset, long sizeInBytes, long cardinality) {
- super(BASE_TYPE, BASE_TYPE);
+ super(BASE_TYPE.fields().size());
this.location = location;
this.offset = offset;
this.sizeInBytes = sizeInBytes;
@@ -140,37 +140,41 @@ class DeletionVectorStruct extends SupportsIndexProjection implements DeletionVe
static class Builder {
private String location = null;
- private long offset = -1L;
- private long sizeInBytes = -1L;
- private long cardinality = -1L;
+ private Long offset = null;
+ private Long sizeInBytes = null;
+ private Long cardinality = null;
Builder location(String dvLocation) {
+ Preconditions.checkArgument(dvLocation != null, "Invalid location: null");
this.location = dvLocation;
return this;
}
Builder offset(long dvOffset) {
+ Preconditions.checkArgument(dvOffset >= 0, "Invalid offset: %s (must be >= 0)", dvOffset);
this.offset = dvOffset;
return this;
}
Builder sizeInBytes(long dvSizeInBytes) {
+ Preconditions.checkArgument(
+ dvSizeInBytes >= 0, "Invalid size in bytes: %s (must be >= 0)", dvSizeInBytes);
this.sizeInBytes = dvSizeInBytes;
return this;
}
Builder cardinality(long dvCardinality) {
+ Preconditions.checkArgument(
+ dvCardinality >= 0, "Invalid cardinality: %s (must be >= 0)", dvCardinality);
this.cardinality = dvCardinality;
return this;
}
DeletionVectorStruct build() {
- Preconditions.checkArgument(location != null, "Invalid location: null");
- Preconditions.checkArgument(offset >= 0, "Invalid offset: %s (must be >= 0)", offset);
- Preconditions.checkArgument(
- sizeInBytes >= 0, "Invalid size in bytes: %s (must be >= 0)", sizeInBytes);
- Preconditions.checkArgument(
- cardinality >= 0, "Invalid cardinality: %s (must be >= 0)", cardinality);
+ Preconditions.checkArgument(location != null, "Missing required value: location");
+ Preconditions.checkArgument(offset != null, "Missing required value: offset");
+ Preconditions.checkArgument(sizeInBytes != null, "Missing required value: size in bytes");
+ Preconditions.checkArgument(cardinality != null, "Missing required value: cardinality");
return new DeletionVectorStruct(location, offset, sizeInBytes, cardinality);
}
}
diff --git a/core/src/main/java/org/apache/iceberg/ManifestInfoStruct.java b/core/src/main/java/org/apache/iceberg/ManifestInfoStruct.java
index 922047bffe..6a7ccea6b6 100644
--- a/core/src/main/java/org/apache/iceberg/ManifestInfoStruct.java
+++ b/core/src/main/java/org/apache/iceberg/ManifestInfoStruct.java
@@ -74,7 +74,7 @@ class ManifestInfoStruct extends SupportsIndexProjection implements ManifestInfo
this.dvCardinality = toCopy.dvCardinality;
}
- private ManifestInfoStruct(
+ ManifestInfoStruct(
int addedFilesCount,
int existingFilesCount,
int deletedFilesCount,
@@ -86,7 +86,7 @@ class ManifestInfoStruct extends SupportsIndexProjection implements ManifestInfo
long minSequenceNumber,
byte[] dv,
Long dvCardinality) {
- super(BASE_TYPE, BASE_TYPE);
+ super(BASE_TYPE.fields().size());
this.addedFilesCount = addedFilesCount;
this.existingFilesCount = existingFilesCount;
this.deletedFilesCount = deletedFilesCount;
@@ -252,121 +252,140 @@ class ManifestInfoStruct extends SupportsIndexProjection implements ManifestInfo
.add("replaced_rows_count", replacedRowsCount)
.add("min_sequence_number", minSequenceNumber)
.add("dv", dv == null ? "null" : "(binary)")
- .add("dv_cardinality", dvCardinality == null ? "null" : dvCardinality)
+ .add("dv_cardinality", dvCardinality)
.toString();
}
static class Builder {
- private int addedFilesCount = -1;
- private int existingFilesCount = -1;
- private int deletedFilesCount = -1;
- private int replacedFilesCount = -1;
- private long addedRowsCount = -1L;
- private long existingRowsCount = -1L;
- private long deletedRowsCount = -1L;
- private long replacedRowsCount = -1L;
- private long minSequenceNumber = -1L;
+ private Integer addedFilesCount = null;
+ private Integer existingFilesCount = null;
+ private Integer deletedFilesCount = null;
+ private Integer replacedFilesCount = null;
+ private Long addedRowsCount = null;
+ private Long existingRowsCount = null;
+ private Long deletedRowsCount = null;
+ private Long replacedRowsCount = null;
+ private Long minSequenceNumber = null;
private byte[] dv = null;
private Long dvCardinality = null;
Builder addedFilesCount(int count) {
+ Preconditions.checkArgument(
+ count >= 0, "Invalid added files count: %s (must be >= 0)", count);
this.addedFilesCount = count;
return this;
}
Builder existingFilesCount(int count) {
+ Preconditions.checkArgument(
+ count >= 0, "Invalid existing files count: %s (must be >= 0)", count);
this.existingFilesCount = count;
return this;
}
Builder deletedFilesCount(int count) {
+ Preconditions.checkArgument(
+ count >= 0, "Invalid deleted files count: %s (must be >= 0)", count);
this.deletedFilesCount = count;
return this;
}
Builder replacedFilesCount(int count) {
+ Preconditions.checkArgument(
+ count >= 0, "Invalid replaced files count: %s (must be >= 0)", count);
this.replacedFilesCount = count;
return this;
}
Builder addedRowsCount(long count) {
+ Preconditions.checkArgument(count >= 0, "Invalid added rows count: %s (must be >= 0)", count);
this.addedRowsCount = count;
return this;
}
Builder existingRowsCount(long count) {
+ Preconditions.checkArgument(
+ count >= 0, "Invalid existing rows count: %s (must be >= 0)", count);
this.existingRowsCount = count;
return this;
}
Builder deletedRowsCount(long count) {
+ Preconditions.checkArgument(
+ count >= 0, "Invalid deleted rows count: %s (must be >= 0)", count);
this.deletedRowsCount = count;
return this;
}
Builder replacedRowsCount(long count) {
+ Preconditions.checkArgument(
+ count >= 0, "Invalid replaced rows count: %s (must be >= 0)", count);
this.replacedRowsCount = count;
return this;
}
Builder minSequenceNumber(long sequenceNumber) {
+ Preconditions.checkArgument(
+ sequenceNumber >= 0, "Invalid min sequence number: %s (must be >= 0)", sequenceNumber);
this.minSequenceNumber = sequenceNumber;
return this;
}
Builder dv(ByteBuffer buffer) {
- this.dv = buffer != null ? ByteBuffers.toByteArray(buffer) : null;
- return this;
- }
-
- Builder dv(byte[] buffer) {
- this.dv = buffer;
+ Preconditions.checkArgument(buffer != null, "Invalid DV: null");
+ this.dv = ByteBuffers.toByteArray(buffer);
return this;
}
- Builder dvCardinality(Long cardinality) {
+ Builder dvCardinality(long cardinality) {
+ Preconditions.checkArgument(
+ cardinality >= 0, "Invalid DV cardinality: %s (must be >= 0)", cardinality);
this.dvCardinality = cardinality;
return this;
}
ManifestInfoStruct build() {
Preconditions.checkArgument(
- addedFilesCount >= 0, "Invalid added files count: %s (must be >= 0)", addedFilesCount);
+ addedFilesCount != null, "Missing required value: added files count");
Preconditions.checkArgument(
- existingFilesCount >= 0,
- "Invalid existing files count: %s (must be >= 0)",
- existingFilesCount);
+ existingFilesCount != null, "Missing required value: existing files count");
Preconditions.checkArgument(
- deletedFilesCount >= 0,
- "Invalid deleted files count: %s (must be >= 0)",
- deletedFilesCount);
+ deletedFilesCount != null, "Missing required value: deleted files count");
Preconditions.checkArgument(
- replacedFilesCount >= 0,
- "Invalid replaced files count: %s (must be >= 0)",
- replacedFilesCount);
+ replacedFilesCount != null, "Missing required value: replaced files count");
+ Preconditions.checkArgument(
+ addedRowsCount != null, "Missing required value: added rows count");
+ Preconditions.checkArgument(
+ existingRowsCount != null, "Missing required value: existing rows count");
+ Preconditions.checkArgument(
+ deletedRowsCount != null, "Missing required value: deleted rows count");
Preconditions.checkArgument(
- addedRowsCount >= 0, "Invalid added rows count: %s (must be >= 0)", addedRowsCount);
+ replacedRowsCount != null, "Missing required value: replaced rows count");
Preconditions.checkArgument(
- existingRowsCount >= 0,
- "Invalid existing rows count: %s (must be >= 0)",
- existingRowsCount);
+ minSequenceNumber != null, "Missing required value: min sequence number");
Preconditions.checkArgument(
- deletedRowsCount >= 0, "Invalid deleted rows count: %s (must be >= 0)", deletedRowsCount);
+ addedRowsCount == 0 || addedFilesCount > 0,
+ "Invalid added counts: %s rows in %s files",
+ addedRowsCount,
+ addedFilesCount);
+ Preconditions.checkArgument(
+ existingRowsCount == 0 || existingFilesCount > 0,
+ "Invalid existing counts: %s rows in %s files",
+ existingRowsCount,
+ existingFilesCount);
Preconditions.checkArgument(
- replacedRowsCount >= 0,
- "Invalid replaced rows count: %s (must be >= 0)",
- replacedRowsCount);
+ deletedRowsCount == 0 || deletedFilesCount > 0,
+ "Invalid deleted counts: %s rows in %s files",
+ deletedRowsCount,
+ deletedFilesCount);
Preconditions.checkArgument(
- minSequenceNumber >= 0,
- "Invalid min sequence number: %s (must be >= 0)",
- minSequenceNumber);
+ replacedRowsCount == 0 || replacedFilesCount > 0,
+ "Invalid replaced counts: %s rows in %s files",
+ replacedRowsCount,
+ replacedFilesCount);
Preconditions.checkArgument(
(dv == null) == (dvCardinality == null),
"Invalid DV and cardinality: must both be null or non-null");
- Preconditions.checkArgument(
- dvCardinality == null || dvCardinality > 0,
- "Invalid DV cardinality: %s (must be positive)",
- dvCardinality);
return new ManifestInfoStruct(
addedFilesCount,
existingFilesCount,
diff --git a/core/src/main/java/org/apache/iceberg/TrackingBuilder.java b/core/src/main/java/org/apache/iceberg/TrackingBuilder.java
new file mode 100644
index 0000000000..bf3cb3a066
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/TrackingBuilder.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg;
+
+import java.nio.ByteBuffer;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.util.ByteBuffers;
+
+class TrackingBuilder {
+ private final EntryStatus status;
+ private final Long snapshotId;
+ private final Long dataSequenceNumber;
+ private final Long fileSequenceNumber;
+ private final Long firstRowId;
+ // ID of the snapshot in which the new Tracking instance will be committed.
+ private final long newSnapshotId;
+ private Long dvSnapshotId;
+ private byte[] deletedPositions;
+ private byte[] replacedPositions;
+
+ /**
+ * Creates a builder for a newly added file.
+ *
+ * @param newSnapshotId the snapshot ID in which the new tracking instance will be committed
+ */
+ static TrackingBuilder added(long newSnapshotId) {
+ return new TrackingBuilder(newSnapshotId);
+ }
+
+ /**
+ * Creates a builder for a tracking row derived from {@code source}.
+ *
+ * @param source source tracking from a manifest entry
+ * @param newSnapshotId the snapshot ID in which the new tracking instance will be committed
+ */
+ static TrackingBuilder from(Tracking source, long newSnapshotId) {
+ return new TrackingBuilder(source, newSnapshotId);
+ }
+
+ /**
+ * Returns a DELETED tracking row derived from {@code source}.
+ *
+ * @param source source tracking from a manifest entry
+ * @param newSnapshotId the snapshot ID in which the new tracking instance will be committed
+ */
+ static Tracking deleted(Tracking source, long newSnapshotId) {
+ return terminal(EntryStatus.DELETED, source, newSnapshotId);
+ }
+
+ /**
+ * Returns a REPLACED tracking row derived from {@code source}.
+ *
+ * @param source source tracking from a manifest entry
+ * @param newSnapshotId the snapshot ID in which the new tracking instance will be committed
+ */
+ static Tracking replaced(Tracking source, long newSnapshotId) {
+ return terminal(EntryStatus.REPLACED, source, newSnapshotId);
+ }
+
+ private TrackingBuilder(long newSnapshotId) {
+ this.status = EntryStatus.ADDED;
+ this.snapshotId = newSnapshotId;
+ this.newSnapshotId = newSnapshotId;
+ this.dataSequenceNumber = null;
+ this.fileSequenceNumber = null;
+ this.firstRowId = null;
+ this.dvSnapshotId = null;
+ this.deletedPositions = null;
+ this.replacedPositions = null;
+ }
+
+ private TrackingBuilder(Tracking source, long newSnapshotId) {
+ validateSource(source);
+ validateStatusTransition(source.status(), EntryStatus.EXISTING);
+ this.status = EntryStatus.EXISTING;
+ this.snapshotId = source.snapshotId();
+ this.newSnapshotId = newSnapshotId;
+ this.dataSequenceNumber = source.dataSequenceNumber();
+ this.fileSequenceNumber = source.fileSequenceNumber();
+ this.firstRowId = source.firstRowId();
+ this.dvSnapshotId = source.dvSnapshotId();
+ this.deletedPositions = null;
+ this.replacedPositions = null;
+ }
+
+ /** Indicates that the DV has been updated for the new Tracking. */
+ TrackingBuilder dvUpdated() {
+ // DV applies to data files; deleted/replaced positions apply to manifest files
+ Preconditions.checkState(
+ deletedPositions == null && replacedPositions == null,
+ "Cannot mark DV updated on a manifest entry (deleted/replaced positions are set)");
+ this.dvSnapshotId = newSnapshotId;
+ return this;
+ }
+
+ TrackingBuilder deletedPositions(ByteBuffer positions) {
+ Preconditions.checkState(
+ status == EntryStatus.EXISTING, "Cannot set deleted positions on %s entry", status);
+ // DV applies to data files; deleted positions apply to manifest files
+ Preconditions.checkState(
+ dvSnapshotId == null,
+ "Cannot set deleted positions on a data file entry (DV snapshot ID is set)");
+ this.deletedPositions = ByteBuffers.toByteArray(positions);
+ return this;
+ }
+
+ TrackingBuilder replacedPositions(ByteBuffer positions) {
+ Preconditions.checkState(
+ status == EntryStatus.EXISTING, "Cannot set replaced positions on %s entry", status);
+ // DV applies to data files; replaced positions apply to manifest files
+ Preconditions.checkState(
+ dvSnapshotId == null,
+ "Cannot set replaced positions on a data file entry (DV snapshot ID is set)");
+ this.replacedPositions = ByteBuffers.toByteArray(positions);
+ return this;
+ }
+
+ Tracking build() {
+ return new TrackingStruct(
+ status,
+ snapshotId,
+ dataSequenceNumber,
+ fileSequenceNumber,
+ dvSnapshotId,
+ firstRowId,
+ deletedPositions,
+ replacedPositions);
+ }
+
+ private static Tracking terminal(EntryStatus to, Tracking source, long newSnapshotId) {
+ validateSource(source);
+ validateStatusTransition(source.status(), to);
+ return new TrackingStruct(
+ to,
+ newSnapshotId,
+ source.dataSequenceNumber(),
+ source.fileSequenceNumber(),
+ source.dvSnapshotId(),
+ source.firstRowId(),
+ null,
+ null);
+ }
+
+ private static void validateSource(Tracking source) {
+ Preconditions.checkArgument(source != null, "Invalid source tracking: null");
+ Preconditions.checkArgument(
+ source.dataSequenceNumber() != null,
+ "Invalid tracking source: data sequence number is null");
+ Preconditions.checkArgument(
+ source.fileSequenceNumber() != null,
+ "Invalid tracking source: file sequence number is null");
+ }
+
+ private static void validateStatusTransition(EntryStatus from, EntryStatus to) {
+ Preconditions.checkState(from != null, "Invalid tracking source: status is null");
+ Preconditions.checkState(
+ from != EntryStatus.DELETED && from != EntryStatus.REPLACED,
+ "Cannot revive non-live entry with status %s",
+ from);
+ Preconditions.checkState(
+ to != EntryStatus.ADDED, "Cannot transition to ADDED: ADDED is the starting status");
+ }
+}
diff --git a/core/src/main/java/org/apache/iceberg/TrackingStruct.java b/core/src/main/java/org/apache/iceberg/TrackingStruct.java
index 65513c8d4a..8ae4b7e4ce 100644
--- a/core/src/main/java/org/apache/iceberg/TrackingStruct.java
+++ b/core/src/main/java/org/apache/iceberg/TrackingStruct.java
@@ -59,6 +59,11 @@ class TrackingStruct extends SupportsIndexProjection implements Tracking, Serial
super(BASE_TYPE, type);
}
+ /** Constructor for Java serialization. */
+ TrackingStruct() {
+ super(BASE_TYPE.fields().size());
+ }
+
private TrackingStruct(TrackingStruct toCopy) {
super(toCopy);
this.status = toCopy.status;
@@ -79,7 +84,7 @@ class TrackingStruct extends SupportsIndexProjection implements Tracking, Serial
this.manifestPos = toCopy.manifestPos;
}
- private TrackingStruct(
+ TrackingStruct(
EntryStatus status,
Long snapshotId,
Long dataSequenceNumber,
@@ -88,7 +93,7 @@ class TrackingStruct extends SupportsIndexProjection implements Tracking, Serial
Long firstRowId,
byte[] deletedPositions,
byte[] replacedPositions) {
- super(BASE_TYPE, BASE_TYPE);
+ super(BASE_TYPE.fields().size());
this.status = status;
this.snapshotId = snapshotId;
this.dataSequenceNumber = dataSequenceNumber;
@@ -249,95 +254,17 @@ class TrackingStruct extends SupportsIndexProjection implements Tracking, Serial
}
}
- static Builder builder() {
- return new Builder();
- }
-
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("status", status)
- .add("snapshot_id", snapshotId == null ? "null" : snapshotId)
- .add("data_sequence_number", dataSequenceNumber == null ? "null" : dataSequenceNumber)
- .add("file_sequence_number", fileSequenceNumber == null ? "null" : fileSequenceNumber)
- .add("dv_snapshot_id", dvSnapshotId == null ? "null" : dvSnapshotId)
- .add("first_row_id", firstRowId == null ? "null" : firstRowId)
+ .add("snapshot_id", snapshotId)
+ .add("data_sequence_number", dataSequenceNumber)
+ .add("file_sequence_number", fileSequenceNumber)
+ .add("dv_snapshot_id", dvSnapshotId)
+ .add("first_row_id", firstRowId)
.add("deleted_positions", deletedPositions == null ? "null" : "(binary)")
.add("replaced_positions", replacedPositions == null ? "null" : "(binary)")
.toString();
}
-
- static class Builder {
- private EntryStatus status = null;
- private Long snapshotId = null;
- private Long dataSequenceNumber = null;
- private Long fileSequenceNumber = null;
- private Long dvSnapshotId = null;
- private Long firstRowId = null;
- private byte[] deletedPositions = null;
- private byte[] replacedPositions = null;
-
- Builder status(EntryStatus entryStatus) {
- this.status = entryStatus;
- return this;
- }
-
- Builder snapshotId(Long id) {
- this.snapshotId = id;
- return this;
- }
-
- Builder dataSequenceNumber(Long sequenceNumber) {
- this.dataSequenceNumber = sequenceNumber;
- return this;
- }
-
- Builder fileSequenceNumber(Long sequenceNumber) {
- this.fileSequenceNumber = sequenceNumber;
- return this;
- }
-
- Builder dvSnapshotId(Long id) {
- this.dvSnapshotId = id;
- return this;
- }
-
- Builder firstRowId(Long rowId) {
- this.firstRowId = rowId;
- return this;
- }
-
- Builder deletedPositions(ByteBuffer positions) {
- this.deletedPositions = positions != null ? ByteBuffers.toByteArray(positions) : null;
- return this;
- }
-
- Builder deletedPositions(byte[] positions) {
- this.deletedPositions = positions;
- return this;
- }
-
- Builder replacedPositions(ByteBuffer positions) {
- this.replacedPositions = positions != null ? ByteBuffers.toByteArray(positions) : null;
- return this;
- }
-
- Builder replacedPositions(byte[] positions) {
- this.replacedPositions = positions;
- return this;
- }
-
- TrackingStruct build() {
- Preconditions.checkArgument(status != null, "Invalid status: null");
- return new TrackingStruct(
- status,
- snapshotId,
- dataSequenceNumber,
- fileSequenceNumber,
- dvSnapshotId,
- firstRowId,
- deletedPositions,
- replacedPositions);
- }
- }
}
diff --git a/core/src/test/java/org/apache/iceberg/TestDeletionVectorStruct.java b/core/src/test/java/org/apache/iceberg/TestDeletionVectorStruct.java
index 325f9afd9c..8242be38e9 100644
--- a/core/src/test/java/org/apache/iceberg/TestDeletionVectorStruct.java
+++ b/core/src/test/java/org/apache/iceberg/TestDeletionVectorStruct.java
@@ -87,6 +87,26 @@ class TestDeletionVectorStruct {
assertThat(dv.get(1, Long.class)).isEqualTo(42L);
}
+ @Test
+ void testInternalSetIgnoresUnknownOrdinal() {
+ DeletionVectorStruct dv =
+ DeletionVectorStruct.builder()
+ .location("s3://bucket/data/dv.puffin")
+ .offset(100L)
+ .sizeInBytes(512L)
+ .cardinality(42L)
+ .build();
+
+ // unknown ordinals from a newer format version are silently ignored
+ dv.internalSet(99, "value from a newer format");
+
+ // every field is unchanged
+ assertThat(dv.location()).isEqualTo("s3://bucket/data/dv.puffin");
+ assertThat(dv.offset()).isEqualTo(100L);
+ assertThat(dv.sizeInBytes()).isEqualTo(512L);
+ assertThat(dv.cardinality()).isEqualTo(42L);
+ }
+
@Test
void testJavaSerializationRoundTrip() throws IOException, ClassNotFoundException {
DeletionVectorStruct dv =
@@ -106,11 +126,11 @@ class TestDeletionVectorStruct {
}
@Test
- void testBuilderValidation() {
+ void testBuilderMissingRequiredFields() {
assertThatThrownBy(
() -> DeletionVectorStruct.builder().offset(0).sizeInBytes(1).cardinality(1).build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid location: null");
+ .hasMessage("Missing required value: location");
assertThatThrownBy(
() ->
@@ -120,7 +140,7 @@ class TestDeletionVectorStruct {
.cardinality(1)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid offset: -1 (must be >= 0)");
+ .hasMessage("Missing required value: offset");
assertThatThrownBy(
() ->
@@ -130,7 +150,7 @@ class TestDeletionVectorStruct {
.cardinality(1)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid size in bytes: -1 (must be >= 0)");
+ .hasMessage("Missing required value: size in bytes");
assertThatThrownBy(
() ->
@@ -140,6 +160,25 @@ class TestDeletionVectorStruct {
.sizeInBytes(1)
.build())
.isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Missing required value: cardinality");
+ }
+
+ @Test
+ void testBuilderRejectsInvalidValuesAtSetter() {
+ assertThatThrownBy(() -> DeletionVectorStruct.builder().location(null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid location: null");
+
+ assertThatThrownBy(() -> DeletionVectorStruct.builder().offset(-1))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid offset: -1 (must be >= 0)");
+
+ assertThatThrownBy(() -> DeletionVectorStruct.builder().sizeInBytes(-1))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid size in bytes: -1 (must be >= 0)");
+
+ assertThatThrownBy(() -> DeletionVectorStruct.builder().cardinality(-1))
+ .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid cardinality: -1 (must be >= 0)");
}
diff --git a/core/src/test/java/org/apache/iceberg/TestManifestInfoStruct.java b/core/src/test/java/org/apache/iceberg/TestManifestInfoStruct.java
index 3a694f1a38..9d78dca65f 100644
--- a/core/src/test/java/org/apache/iceberg/TestManifestInfoStruct.java
+++ b/core/src/test/java/org/apache/iceberg/TestManifestInfoStruct.java
@@ -30,19 +30,8 @@ class TestManifestInfoStruct {
@Test
void testFieldAccess() {
- ManifestInfoStruct info = new ManifestInfoStruct(ManifestInfo.schema());
-
- info.set(0, 10);
- info.set(1, 20);
- info.set(2, 3);
- info.set(3, 2);
- info.set(4, 1000L);
- info.set(5, 2000L);
- info.set(6, 300L);
- info.set(7, 200L);
- info.set(8, 5L);
- info.set(9, ByteBuffer.wrap(new byte[] {0xF}));
- info.set(10, 1L);
+ ManifestInfoStruct info =
+ new ManifestInfoStruct(10, 20, 3, 2, 1000L, 2000L, 300L, 200L, 5L, new byte[] {0xF}, 1L);
assertThat(info.addedFilesCount()).isEqualTo(10);
assertThat(info.existingFilesCount()).isEqualTo(20);
@@ -70,7 +59,7 @@ class TestManifestInfoStruct {
.deletedRowsCount(300L)
.replacedRowsCount(200L)
.minSequenceNumber(5L)
- .dv(new byte[] {0xF})
+ .dv(ByteBuffer.wrap(new byte[] {0xF}))
.dvCardinality(1L)
.build();
@@ -130,6 +119,40 @@ class TestManifestInfoStruct {
assertThat(info.get(1, Long.class)).isEqualTo(5L);
}
+ @Test
+ void testInternalSetIgnoresUnknownOrdinal() {
+ ManifestInfoStruct info =
+ ManifestInfoStruct.builder()
+ .addedFilesCount(10)
+ .existingFilesCount(20)
+ .deletedFilesCount(3)
+ .replacedFilesCount(2)
+ .addedRowsCount(1000L)
+ .existingRowsCount(2000L)
+ .deletedRowsCount(300L)
+ .replacedRowsCount(200L)
+ .minSequenceNumber(5L)
+ .dv(ByteBuffer.wrap(new byte[] {0xF}))
+ .dvCardinality(1L)
+ .build();
+
+ // unknown ordinals from a newer format version are silently ignored
+ info.internalSet(99, "value from a newer format");
+
+ // every field is unchanged
+ assertThat(info.addedFilesCount()).isEqualTo(10);
+ assertThat(info.existingFilesCount()).isEqualTo(20);
+ assertThat(info.deletedFilesCount()).isEqualTo(3);
+ assertThat(info.replacedFilesCount()).isEqualTo(2);
+ assertThat(info.addedRowsCount()).isEqualTo(1000L);
+ assertThat(info.existingRowsCount()).isEqualTo(2000L);
+ assertThat(info.deletedRowsCount()).isEqualTo(300L);
+ assertThat(info.replacedRowsCount()).isEqualTo(200L);
+ assertThat(info.minSequenceNumber()).isEqualTo(5L);
+ assertThat(info.dv()).isEqualTo(ByteBuffer.wrap(new byte[] {0xF}));
+ assertThat(info.dvCardinality()).isEqualTo(1L);
+ }
+
@Test
void testJavaSerializationRoundTrip() throws IOException, ClassNotFoundException {
ManifestInfoStruct info =
@@ -143,7 +166,7 @@ class TestManifestInfoStruct {
.deletedRowsCount(300L)
.replacedRowsCount(200L)
.minSequenceNumber(5L)
- .dv(new byte[] {0xF})
+ .dv(ByteBuffer.wrap(new byte[] {0xF}))
.dvCardinality(1L)
.build();
@@ -163,7 +186,7 @@ class TestManifestInfoStruct {
}
@Test
- void testBuilderValidation() {
+ void testBuilderMissingAddedFilesCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -177,8 +200,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid added files count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: added files count");
+ }
+ @Test
+ void testBuilderMissingExistingFilesCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -192,8 +218,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid existing files count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: existing files count");
+ }
+ @Test
+ void testBuilderMissingDeletedFilesCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -207,8 +236,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid deleted files count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: deleted files count");
+ }
+ @Test
+ void testBuilderMissingReplacedFilesCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -222,8 +254,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid replaced files count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: replaced files count");
+ }
+ @Test
+ void testBuilderMissingAddedRowsCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -237,8 +272,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid added rows count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: added rows count");
+ }
+ @Test
+ void testBuilderMissingExistingRowsCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -252,8 +290,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid existing rows count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: existing rows count");
+ }
+ @Test
+ void testBuilderMissingDeletedRowsCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -267,8 +308,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid deleted rows count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: deleted rows count");
+ }
+ @Test
+ void testBuilderMissingReplacedRowsCount() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -282,8 +326,11 @@ class TestManifestInfoStruct {
.minSequenceNumber(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid replaced rows count: -1 (must be >= 0)");
+ .hasMessage("Missing required value: replaced rows count");
+ }
+ @Test
+ void testBuilderMissingMinSequenceNumber() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -297,11 +344,81 @@ class TestManifestInfoStruct {
.replacedRowsCount(0L)
.build())
.isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Missing required value: min sequence number");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeAddedFilesCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().addedFilesCount(-1))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid added files count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeExistingFilesCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().existingFilesCount(-1))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid existing files count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeDeletedFilesCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().deletedFilesCount(-1))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid deleted files count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeReplacedFilesCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().replacedFilesCount(-1))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid replaced files count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeAddedRowsCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().addedRowsCount(-1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid added rows count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeExistingRowsCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().existingRowsCount(-1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid existing rows count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeDeletedRowsCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().deletedRowsCount(-1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid deleted rows count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeReplacedRowsCount() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().replacedRowsCount(-1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid replaced rows count: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsNegativeMinSequenceNumber() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().minSequenceNumber(-1L))
+ .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid min sequence number: -1 (must be >= 0)");
}
@Test
- void testBuilderDvPairingValidation() {
+ void testBuilderRejectsNegativeDvCardinality() {
+ assertThatThrownBy(() -> ManifestInfoStruct.builder().dvCardinality(-1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid DV cardinality: -1 (must be >= 0)");
+ }
+
+ @Test
+ void testBuilderRejectsRowsWithoutFiles() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -309,16 +426,91 @@ class TestManifestInfoStruct {
.existingFilesCount(0)
.deletedFilesCount(0)
.replacedFilesCount(0)
- .addedRowsCount(0L)
+ .addedRowsCount(10L)
.existingRowsCount(0L)
.deletedRowsCount(0L)
.replacedRowsCount(0L)
.minSequenceNumber(0L)
- .dv(new byte[] {0xF})
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid DV and cardinality: must both be null or non-null");
+ .hasMessage("Invalid added counts: 10 rows in 0 files");
+
+ assertThatThrownBy(
+ () ->
+ ManifestInfoStruct.builder()
+ .addedFilesCount(0)
+ .existingFilesCount(0)
+ .deletedFilesCount(0)
+ .replacedFilesCount(0)
+ .addedRowsCount(0L)
+ .existingRowsCount(5L)
+ .deletedRowsCount(0L)
+ .replacedRowsCount(0L)
+ .minSequenceNumber(0L)
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid existing counts: 5 rows in 0 files");
+
+ assertThatThrownBy(
+ () ->
+ ManifestInfoStruct.builder()
+ .addedFilesCount(0)
+ .existingFilesCount(0)
+ .deletedFilesCount(0)
+ .replacedFilesCount(0)
+ .addedRowsCount(0L)
+ .existingRowsCount(0L)
+ .deletedRowsCount(3L)
+ .replacedRowsCount(0L)
+ .minSequenceNumber(0L)
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid deleted counts: 3 rows in 0 files");
+ assertThatThrownBy(
+ () ->
+ ManifestInfoStruct.builder()
+ .addedFilesCount(0)
+ .existingFilesCount(0)
+ .deletedFilesCount(0)
+ .replacedFilesCount(0)
+ .addedRowsCount(0L)
+ .existingRowsCount(0L)
+ .deletedRowsCount(0L)
+ .replacedRowsCount(7L)
+ .minSequenceNumber(0L)
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid replaced counts: 7 rows in 0 files");
+ }
+
+ @Test
+ void testBuilderAllowsFilesWithoutRows() {
+ ManifestInfoStruct info =
+ ManifestInfoStruct.builder()
+ .addedFilesCount(5)
+ .existingFilesCount(5)
+ .deletedFilesCount(5)
+ .replacedFilesCount(5)
+ .addedRowsCount(0L)
+ .existingRowsCount(0L)
+ .deletedRowsCount(0L)
+ .replacedRowsCount(0L)
+ .minSequenceNumber(0L)
+ .build();
+
+ assertThat(info.addedFilesCount()).isEqualTo(5);
+ assertThat(info.existingFilesCount()).isEqualTo(5);
+ assertThat(info.deletedFilesCount()).isEqualTo(5);
+ assertThat(info.replacedFilesCount()).isEqualTo(5);
+ assertThat(info.addedRowsCount()).isEqualTo(0L);
+ assertThat(info.existingRowsCount()).isEqualTo(0L);
+ assertThat(info.deletedRowsCount()).isEqualTo(0L);
+ assertThat(info.replacedRowsCount()).isEqualTo(0L);
+ }
+
+ @Test
+ void testBuilderDvPairingValidation() {
assertThatThrownBy(
() ->
ManifestInfoStruct.builder()
@@ -331,7 +523,7 @@ class TestManifestInfoStruct {
.deletedRowsCount(0L)
.replacedRowsCount(0L)
.minSequenceNumber(0L)
- .dvCardinality(1L)
+ .dv(ByteBuffer.wrap(new byte[] {0xF}))
.build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid DV and cardinality: must both be null or non-null");
@@ -348,11 +540,10 @@ class TestManifestInfoStruct {
.deletedRowsCount(0L)
.replacedRowsCount(0L)
.minSequenceNumber(0L)
- .dv(new byte[] {0xF})
- .dvCardinality(0L)
+ .dvCardinality(1L)
.build())
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid DV cardinality: 0 (must be positive)");
+ .hasMessage("Invalid DV and cardinality: must both be null or non-null");
}
@Test
@@ -368,7 +559,7 @@ class TestManifestInfoStruct {
.deletedRowsCount(300L)
.replacedRowsCount(200L)
.minSequenceNumber(5L)
- .dv(new byte[] {0xF})
+ .dv(ByteBuffer.wrap(new byte[] {0xF}))
.dvCardinality(1L)
.build();
diff --git a/core/src/test/java/org/apache/iceberg/TestTrackedFileStruct.java b/core/src/test/java/org/apache/iceberg/TestTrackedFileStruct.java
index 3abb36aa51..8891db408b 100644
--- a/core/src/test/java/org/apache/iceberg/TestTrackedFileStruct.java
+++ b/core/src/test/java/org/apache/iceberg/TestTrackedFileStruct.java
@@ -35,11 +35,14 @@ class TestTrackedFileStruct {
Types.NestedField.optional(1000, "id_bucket", Types.IntegerType.get()),
Types.NestedField.optional(1001, "category", Types.StringType.get()));
+ // Ordinal of MetadataColumns.ROW_POSITION within TrackingStruct's BASE_TYPE,
+ // which appends ROW_POSITION after the Tracking schema fields.
+ private static final int MANIFEST_POS_ORDINAL = Tracking.schema().fields().size();
+
@Test
void testFieldAccess() {
TrackedFileStruct file = new TrackedFileStruct();
- TrackingStruct tracking =
- TrackingStruct.builder().status(EntryStatus.ADDED).snapshotId(42L).build();
+ Tracking tracking = TrackingBuilder.added(42L).build();
DeletionVectorStruct dv =
DeletionVectorStruct.builder()
.location("s3://bucket/dv.puffin")
@@ -98,9 +101,9 @@ class TestTrackedFileStruct {
void testReaderSideFields() {
TrackedFileStruct file = new TrackedFileStruct();
- TrackingStruct tracking = TrackingStruct.builder().status(EntryStatus.ADDED).build();
+ TrackingStruct tracking = new TrackingStruct();
tracking.setManifestLocation("s3://bucket/metadata/manifest.avro");
- tracking.set(8, 7L);
+ tracking.set(MANIFEST_POS_ORDINAL, 7L);
file.set(0, tracking);
file.set(1, FileContent.DATA.id());
@@ -328,14 +331,9 @@ class TestTrackedFileStruct {
}
static TrackedFileStruct createFullTrackedFile() {
- TrackingStruct tracking =
- TrackingStruct.builder()
- .status(EntryStatus.ADDED)
- .snapshotId(42L)
- .dataSequenceNumber(10L)
- .build();
+ TrackingStruct tracking = (TrackingStruct) TrackingBuilder.added(42L).build();
tracking.setManifestLocation("s3://bucket/manifest.avro");
- tracking.set(8, 3L);
+ tracking.set(MANIFEST_POS_ORDINAL, 3L);
DeletionVectorStruct dv =
DeletionVectorStruct.builder()
diff --git a/core/src/test/java/org/apache/iceberg/TestTrackingStruct.java b/core/src/test/java/org/apache/iceberg/TestTrackingStruct.java
index 98a7eff2af..0bbd8b3480 100644
--- a/core/src/test/java/org/apache/iceberg/TestTrackingStruct.java
+++ b/core/src/test/java/org/apache/iceberg/TestTrackingStruct.java
@@ -23,23 +23,44 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
import org.apache.iceberg.types.Types;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
class TestTrackingStruct {
+ // Ordinals looked up from Tracking.schema() so tests don't hard-code positions.
+ private static final List<Types.NestedField> TRACKING_FIELDS = Tracking.schema().fields();
+ private static final int STATUS_ORDINAL = TRACKING_FIELDS.indexOf(Tracking.STATUS);
+ private static final int SNAPSHOT_ID_ORDINAL = TRACKING_FIELDS.indexOf(Tracking.SNAPSHOT_ID);
+ private static final int DATA_SEQUENCE_NUMBER_ORDINAL =
+ TRACKING_FIELDS.indexOf(Tracking.SEQUENCE_NUMBER);
+ private static final int FILE_SEQUENCE_NUMBER_ORDINAL =
+ TRACKING_FIELDS.indexOf(Tracking.FILE_SEQUENCE_NUMBER);
+ private static final int DV_SNAPSHOT_ID_ORDINAL =
+ TRACKING_FIELDS.indexOf(Tracking.DV_SNAPSHOT_ID);
+ private static final int FIRST_ROW_ID_ORDINAL = TRACKING_FIELDS.indexOf(Tracking.FIRST_ROW_ID);
+ private static final int DELETED_POSITIONS_ORDINAL =
+ TRACKING_FIELDS.indexOf(Tracking.DELETED_POSITIONS);
+ private static final int REPLACED_POSITIONS_ORDINAL =
+ TRACKING_FIELDS.indexOf(Tracking.REPLACED_POSITIONS);
+
@Test
void testFieldAccess() {
TrackingStruct tracking = new TrackingStruct(Tracking.schema());
- tracking.set(0, EntryStatus.ADDED.id());
- tracking.set(1, 42L);
- tracking.set(2, 10L);
- tracking.set(3, 11L);
- tracking.set(4, 43L);
- tracking.set(5, 1000L);
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+ tracking.set(SNAPSHOT_ID_ORDINAL, 42L);
+ tracking.set(DATA_SEQUENCE_NUMBER_ORDINAL, 10L);
+ tracking.set(FILE_SEQUENCE_NUMBER_ORDINAL, 11L);
+ tracking.set(DV_SNAPSHOT_ID_ORDINAL, 43L);
+ tracking.set(FIRST_ROW_ID_ORDINAL, 1000L);
assertThat(tracking.status()).isEqualTo(EntryStatus.ADDED);
assertThat(tracking.snapshotId()).isEqualTo(42L);
@@ -53,30 +74,33 @@ class TestTrackingStruct {
@Test
void testCopy() {
- TrackingStruct tracking =
- TrackingStruct.builder()
- .status(EntryStatus.ADDED)
- .snapshotId(42L)
- .dataSequenceNumber(10L)
- .deletedPositions(new byte[] {1, 2})
+ Tracking tracking =
+ TrackingBuilder.from(manifestSourceTracking(), 1L)
+ .deletedPositions(ByteBuffer.wrap(new byte[] {1, 2}))
+ .replacedPositions(ByteBuffer.wrap(new byte[] {3, 4}))
.build();
- TrackingStruct copy = tracking.copy();
+ Tracking copy = tracking.copy();
- assertThat(copy.status()).isEqualTo(EntryStatus.ADDED);
- assertThat(copy.snapshotId()).isEqualTo(42L);
- assertThat(copy.dataSequenceNumber()).isEqualTo(10L);
- assertThat(copy.deletedPositions()).isNotNull();
+ assertThat(copy.status()).isEqualTo(EntryStatus.EXISTING);
+ assertThat(copy.snapshotId()).isEqualTo(tracking.snapshotId());
+ assertThat(copy.dataSequenceNumber()).isEqualTo(tracking.dataSequenceNumber());
+ assertThat(copy.fileSequenceNumber()).isEqualTo(tracking.fileSequenceNumber());
+ assertThat(copy.dvSnapshotId()).isNull();
+ assertThat(copy.firstRowId()).isEqualTo(tracking.firstRowId());
+ assertThat(copy.deletedPositions()).isEqualTo(tracking.deletedPositions());
+ assertThat(copy.replacedPositions()).isEqualTo(tracking.replacedPositions());
- // verify deep copy of ByteBuffer
- assertThat(copy.deletedPositions()).isNotSameAs(tracking.deletedPositions());
+ // verify deep copy of ByteBuffer backing arrays
+ assertThat(copy.deletedPositions().array()).isNotSameAs(tracking.deletedPositions().array());
+ assertThat(copy.replacedPositions().array()).isNotSameAs(tracking.replacedPositions().array());
}
@ParameterizedTest
@EnumSource(EntryStatus.class)
void testAllStatuses(EntryStatus status) {
TrackingStruct tracking = new TrackingStruct(Tracking.schema());
- tracking.set(0, status.id());
+ tracking.set(STATUS_ORDINAL, status.id());
assertThat(tracking.status()).isEqualTo(status);
}
@@ -84,22 +108,24 @@ class TestTrackingStruct {
void testIsLive() {
TrackingStruct tracking = new TrackingStruct(Tracking.schema());
- tracking.set(0, EntryStatus.ADDED.id());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
assertThat(tracking.isLive()).isTrue();
- tracking.set(0, EntryStatus.EXISTING.id());
+ tracking.set(STATUS_ORDINAL, EntryStatus.EXISTING.id());
assertThat(tracking.isLive()).isTrue();
- tracking.set(0, EntryStatus.DELETED.id());
+ tracking.set(STATUS_ORDINAL, EntryStatus.DELETED.id());
assertThat(tracking.isLive()).isFalse();
- tracking.set(0, EntryStatus.REPLACED.id());
+ tracking.set(STATUS_ORDINAL, EntryStatus.REPLACED.id());
assertThat(tracking.isLive()).isFalse();
}
@Test
void testInheritSnapshotId() {
- TrackingStruct tracking = TrackingStruct.builder().status(EntryStatus.ADDED).build();
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+
tracking.inheritFrom(createManifestTracking(100L, 60L));
// snapshotId is null, should inherit from manifest
@@ -108,7 +134,9 @@ class TestTrackingStruct {
@Test
void testInheritSequenceNumberForAddedEntries() {
- TrackingStruct tracking = TrackingStruct.builder().status(EntryStatus.ADDED).build();
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+
tracking.inheritFrom(createManifestTracking(100L, 60L));
// sequence numbers are null and status is ADDED, should inherit
@@ -118,12 +146,11 @@ class TestTrackingStruct {
@Test
void testDoNotInheritSequenceNumberForExistingEntries() {
- TrackingStruct tracking =
- TrackingStruct.builder()
- .status(EntryStatus.EXISTING)
- .dataSequenceNumber(5L)
- .fileSequenceNumber(6L)
- .build();
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.EXISTING.id());
+ tracking.set(DATA_SEQUENCE_NUMBER_ORDINAL, 5L);
+ tracking.set(FILE_SEQUENCE_NUMBER_ORDINAL, 6L);
+
tracking.inheritFrom(createManifestTracking(100L, 60L));
// sequence numbers are not inherited for EXISTING entries
@@ -133,13 +160,12 @@ class TestTrackingStruct {
@Test
void testExplicitValuesOverrideInheritance() {
- TrackingStruct tracking =
- TrackingStruct.builder()
- .status(EntryStatus.ADDED)
- .snapshotId(200L)
- .dataSequenceNumber(75L)
- .fileSequenceNumber(76L)
- .build();
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+ tracking.set(SNAPSHOT_ID_ORDINAL, 200L);
+ tracking.set(DATA_SEQUENCE_NUMBER_ORDINAL, 75L);
+ tracking.set(FILE_SEQUENCE_NUMBER_ORDINAL, 76L);
+
tracking.inheritFrom(createManifestTracking(100L, 60L));
// explicit values should take precedence
@@ -151,13 +177,13 @@ class TestTrackingStruct {
@Test
void testInheritFromRejectsUnequalSequenceNumbers() {
TrackingStruct tracking = new TrackingStruct(Tracking.schema());
- tracking.set(0, EntryStatus.ADDED.id());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
TrackingStruct manifestTracking = new TrackingStruct(Tracking.schema());
- manifestTracking.set(0, EntryStatus.ADDED.id());
- manifestTracking.set(1, 100L);
- manifestTracking.set(2, 50L);
- manifestTracking.set(3, 60L);
+ manifestTracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+ manifestTracking.set(SNAPSHOT_ID_ORDINAL, 100L);
+ manifestTracking.set(DATA_SEQUENCE_NUMBER_ORDINAL, 50L);
+ manifestTracking.set(FILE_SEQUENCE_NUMBER_ORDINAL, 60L);
assertThatThrownBy(() -> tracking.inheritFrom(manifestTracking))
.isInstanceOf(IllegalArgumentException.class)
@@ -166,7 +192,8 @@ class TestTrackingStruct {
@Test
void testNoDefaultingWithoutInheritance() {
- TrackingStruct tracking = TrackingStruct.builder().status(EntryStatus.ADDED).build();
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
// no inheritance, nulls stay null
assertThat(tracking.snapshotId()).isNull();
@@ -174,20 +201,262 @@ class TestTrackingStruct {
assertThat(tracking.fileSequenceNumber()).isNull();
}
+ @Test
+ void testInheritFromNullIsNoOp() {
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+
+ tracking.inheritFrom(null);
+
+ // null source is a no-op; all unset fields stay null
+ assertThat(tracking.snapshotId()).isNull();
+ assertThat(tracking.dataSequenceNumber()).isNull();
+ assertThat(tracking.fileSequenceNumber()).isNull();
+ }
+
private static Tracking createManifestTracking(long snapshotId, long sequenceNumber) {
- return TrackingStruct.builder()
- .status(EntryStatus.ADDED)
- .snapshotId(snapshotId)
- .dataSequenceNumber(sequenceNumber)
- .fileSequenceNumber(sequenceNumber)
- .build();
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+ tracking.set(SNAPSHOT_ID_ORDINAL, snapshotId);
+ tracking.set(DATA_SEQUENCE_NUMBER_ORDINAL, sequenceNumber);
+ tracking.set(FILE_SEQUENCE_NUMBER_ORDINAL, sequenceNumber);
+ return tracking;
}
@Test
- void testBuilderValidation() {
- assertThatThrownBy(() -> TrackingStruct.builder().build())
+ void testAddedBuilder() {
+ Tracking tracking = TrackingBuilder.added(42L).dvUpdated().build();
+
+ assertThat(tracking.status()).isEqualTo(EntryStatus.ADDED);
+ assertThat(tracking.snapshotId()).isEqualTo(42L);
+ assertThat(tracking.dvSnapshotId()).isEqualTo(42L);
+ assertThat(tracking.deletedPositions()).isNull();
+ assertThat(tracking.replacedPositions()).isNull();
+ // sequence numbers and firstRowId remain null; populated by inheritance
+ assertThat(tracking.dataSequenceNumber()).isNull();
+ assertThat(tracking.fileSequenceNumber()).isNull();
+ assertThat(tracking.firstRowId()).isNull();
+ }
+
+ @Test
+ void testExistingBuilderPreservesSourceFields() {
+ Tracking source = sourceTracking();
+
+ Tracking existing = TrackingBuilder.from(source, 1L).build();
+
+ assertThat(existing.status()).isEqualTo(EntryStatus.EXISTING);
+ assertThat(existing.snapshotId()).isEqualTo(source.snapshotId());
+ assertThat(existing.dataSequenceNumber()).isEqualTo(source.dataSequenceNumber());
+ assertThat(existing.fileSequenceNumber()).isEqualTo(source.fileSequenceNumber());
+ assertThat(existing.dvSnapshotId()).isEqualTo(source.dvSnapshotId());
+ assertThat(existing.firstRowId()).isEqualTo(source.firstRowId());
+ }
+
+ @Test
+ void testDeleteUpdatesSnapshotIdAndPreservesRest() {
+ Tracking source = sourceTracking();
+
+ Tracking deleted = TrackingBuilder.deleted(source, 999L);
+
+ assertThat(deleted.status()).isEqualTo(EntryStatus.DELETED);
+ assertThat(deleted.snapshotId()).isEqualTo(999L);
+ assertThat(deleted.dataSequenceNumber()).isEqualTo(source.dataSequenceNumber());
+ assertThat(deleted.fileSequenceNumber()).isEqualTo(source.fileSequenceNumber());
+ assertThat(deleted.dvSnapshotId()).isEqualTo(source.dvSnapshotId());
+ assertThat(deleted.firstRowId()).isEqualTo(source.firstRowId());
+ }
+
+ @Test
+ void testReplaceUpdatesSnapshotIdAndPreservesRest() {
+ Tracking source = sourceTracking();
+
+ Tracking replaced = TrackingBuilder.replaced(source, 999L);
+
+ assertThat(replaced.status()).isEqualTo(EntryStatus.REPLACED);
+ assertThat(replaced.snapshotId()).isEqualTo(999L);
+ assertThat(replaced.dataSequenceNumber()).isEqualTo(source.dataSequenceNumber());
+ assertThat(replaced.fileSequenceNumber()).isEqualTo(source.fileSequenceNumber());
+ assertThat(replaced.dvSnapshotId()).isEqualTo(source.dvSnapshotId());
+ assertThat(replaced.firstRowId()).isEqualTo(source.firstRowId());
+ }
+
+ @Test
+ void testSourceDvPositionsAreNotCarriedForward() {
+ TrackingStruct source = sourceTracking();
+ source.set(DELETED_POSITIONS_ORDINAL, ByteBuffer.wrap(new byte[] {1, 2}));
+ source.set(REPLACED_POSITIONS_ORDINAL, ByteBuffer.wrap(new byte[] {3, 4}));
+
+ Tracking existing = TrackingBuilder.from(source, 1L).build();
+ assertThat(existing.deletedPositions()).isNull();
+ assertThat(existing.replacedPositions()).isNull();
+
+ Tracking deleted = TrackingBuilder.deleted(source, 999L);
+ assertThat(deleted.deletedPositions()).isNull();
+ assertThat(deleted.replacedPositions()).isNull();
+
+ Tracking replaced = TrackingBuilder.replaced(source, 999L);
+ assertThat(replaced.deletedPositions()).isNull();
+ assertThat(replaced.replacedPositions()).isNull();
+ }
+
+ @Test
+ void testExistingBuilderAllowsDvMutation() {
+ Tracking existing = TrackingBuilder.from(sourceTracking(), 999L).dvUpdated().build();
+ assertThat(existing.dvSnapshotId()).isEqualTo(999L);
+ }
+
+ @Test
+ void testManifestDVMutatorsRejectedOnAdded() {
+ assertThatThrownBy(
+ () -> TrackingBuilder.added(42L).deletedPositions(ByteBuffer.wrap(new byte[] {1})))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Cannot set deleted positions on ADDED entry");
+
+ assertThatThrownBy(
+ () -> TrackingBuilder.added(42L).replacedPositions(ByteBuffer.wrap(new byte[] {1})))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Cannot set replaced positions on ADDED entry");
+ }
+
+ @Test
+ void testDvSnapshotIdAndManifestDVPositionsAreMutuallyExclusive() {
+ // sourceTracking has dvSnapshotId=43, inherited by existing(source)
+ assertThatThrownBy(
+ () ->
+ TrackingBuilder.from(sourceTracking(), 1L)
+ .deletedPositions(ByteBuffer.wrap(new byte[] {1})))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Cannot set deleted positions on a data file entry (DV snapshot ID is set)");
+
+ assertThatThrownBy(
+ () ->
+ TrackingBuilder.from(sourceTracking(), 1L)
+ .replacedPositions(ByteBuffer.wrap(new byte[] {1})))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Cannot set replaced positions on a data file entry (DV snapshot ID is set)");
+
+ // Setting MDV positions first then dvUpdated is also rejected
+ assertThatThrownBy(
+ () ->
+ TrackingBuilder.from(manifestSourceTracking(), 1L)
+ .deletedPositions(ByteBuffer.wrap(new byte[] {1}))
+ .dvUpdated())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage(
+ "Cannot mark DV updated on a manifest entry (deleted/replaced positions are set)");
+ }
+
+ @Test
+ void testBuilderRejectsNullSource() {
+ assertThatThrownBy(() -> TrackingBuilder.from(null, 1L))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid status: null");
+ .hasMessage("Invalid source tracking: null");
+ }
+
+ @Test
+ void testSourceBuildersRejectSourceWithoutSequenceNumbers() {
+ Tracking missingBoth = TrackingBuilder.added(42L).build();
+
+ assertThatThrownBy(() -> TrackingBuilder.from(missingBoth, 1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid tracking source: data sequence number is null");
+
+ assertThatThrownBy(() -> TrackingBuilder.deleted(missingBoth, 1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid tracking source: data sequence number is null");
+
+ assertThatThrownBy(() -> TrackingBuilder.replaced(missingBoth, 1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid tracking source: data sequence number is null");
+
+ TrackingStruct missingFileSeq = new TrackingStruct(Tracking.schema());
+ missingFileSeq.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+ missingFileSeq.set(SNAPSHOT_ID_ORDINAL, 42L);
+ missingFileSeq.set(DATA_SEQUENCE_NUMBER_ORDINAL, 10L);
+
+ assertThatThrownBy(() -> TrackingBuilder.from(missingFileSeq, 1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid tracking source: file sequence number is null");
+
+ assertThatThrownBy(() -> TrackingBuilder.deleted(missingFileSeq, 1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid tracking source: file sequence number is null");
+
+ assertThatThrownBy(() -> TrackingBuilder.replaced(missingFileSeq, 1L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid tracking source: file sequence number is null");
+ }
+
+ private static Stream<Arguments> terminalTransitionCases() {
+ Consumer<Tracking> builderCall = source -> TrackingBuilder.from(source, 1L);
+ Consumer<Tracking> deletedCall = source -> TrackingBuilder.deleted(source, 1L);
+ Consumer<Tracking> replacedCall = source -> TrackingBuilder.replaced(source, 1L);
+ return Stream.of(
+ Arguments.of(EntryStatus.DELETED, builderCall),
+ Arguments.of(EntryStatus.DELETED, deletedCall),
+ Arguments.of(EntryStatus.DELETED, replacedCall),
+ Arguments.of(EntryStatus.REPLACED, builderCall),
+ Arguments.of(EntryStatus.REPLACED, deletedCall),
+ Arguments.of(EntryStatus.REPLACED, replacedCall));
+ }
+
+ @ParameterizedTest
+ @MethodSource("terminalTransitionCases")
+ void testRejectsTransitionsFromTerminalStatus(
+ EntryStatus sourceStatus, Consumer<Tracking> factoryCall) {
+ Tracking source = sourceTrackingWithStatus(sourceStatus);
+ assertThatThrownBy(() -> factoryCall.accept(source))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Cannot revive non-live entry with status " + sourceStatus);
+ }
+
+ @Test
+ void testExistingToExistingIsAllowed() {
+ Tracking existingSource = sourceTrackingWithStatus(EntryStatus.EXISTING);
+
+ Tracking existing = TrackingBuilder.from(existingSource, 1L).build();
+
+ assertThat(existing.status()).isEqualTo(EntryStatus.EXISTING);
+ assertThat(existing.snapshotId()).isEqualTo(existingSource.snapshotId());
+ }
+
+ @Test
+ void testExistingToTerminalTransitions() {
+ Tracking existingSource = sourceTrackingWithStatus(EntryStatus.EXISTING);
+
+ Tracking deleted = TrackingBuilder.deleted(existingSource, 999L);
+ assertThat(deleted.status()).isEqualTo(EntryStatus.DELETED);
+ assertThat(deleted.snapshotId()).isEqualTo(999L);
+
+ Tracking replaced = TrackingBuilder.replaced(existingSource, 999L);
+ assertThat(replaced.status()).isEqualTo(EntryStatus.REPLACED);
+ assertThat(replaced.snapshotId()).isEqualTo(999L);
+ }
+
+ @Test
+ void testInternalSetIgnoresUnknownOrdinal() {
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+ tracking.set(SNAPSHOT_ID_ORDINAL, 42L);
+ tracking.set(DATA_SEQUENCE_NUMBER_ORDINAL, 10L);
+ tracking.set(FILE_SEQUENCE_NUMBER_ORDINAL, 11L);
+ tracking.set(DV_SNAPSHOT_ID_ORDINAL, 43L);
+ tracking.set(FIRST_ROW_ID_ORDINAL, 1000L);
+ tracking.set(DELETED_POSITIONS_ORDINAL, ByteBuffer.wrap(new byte[] {1, 2}));
+ tracking.set(REPLACED_POSITIONS_ORDINAL, ByteBuffer.wrap(new byte[] {3, 4}));
+
+ // unknown ordinals from a newer format version are silently ignored
+ tracking.internalSet(99, "value from a newer format");
+
+ // every field is unchanged
+ assertThat(tracking.status()).isEqualTo(EntryStatus.ADDED);
+ assertThat(tracking.snapshotId()).isEqualTo(42L);
+ assertThat(tracking.dataSequenceNumber()).isEqualTo(10L);
+ assertThat(tracking.fileSequenceNumber()).isEqualTo(11L);
+ assertThat(tracking.dvSnapshotId()).isEqualTo(43L);
+ assertThat(tracking.firstRowId()).isEqualTo(1000L);
+ assertThat(tracking.deletedPositions()).isEqualTo(ByteBuffer.wrap(new byte[] {1, 2}));
+ assertThat(tracking.replacedPositions()).isEqualTo(ByteBuffer.wrap(new byte[] {3, 4}));
}
@Test
@@ -210,38 +479,85 @@ class TestTrackingStruct {
}
@Test
- void testJavaSerializationRoundTrip() throws IOException, ClassNotFoundException {
- TrackingStruct tracking =
- TrackingStruct.builder()
- .status(EntryStatus.ADDED)
- .snapshotId(42L)
- .dataSequenceNumber(10L)
- .deletedPositions(new byte[] {1, 2})
- .build();
+ void testAddedWithDvSnapshotIdJavaSerializationRoundTrip()
+ throws IOException, ClassNotFoundException {
+ Tracking tracking = TrackingBuilder.added(42L).dvUpdated().build();
- TrackingStruct deserialized = TestHelpers.roundTripSerialize(tracking);
+ Tracking deserialized = TestHelpers.roundTripSerialize(tracking);
assertThat(deserialized.status()).isEqualTo(EntryStatus.ADDED);
assertThat(deserialized.snapshotId()).isEqualTo(42L);
- assertThat(deserialized.dataSequenceNumber()).isEqualTo(10L);
- assertThat(deserialized.deletedPositions()).isEqualTo(ByteBuffer.wrap(new byte[] {1, 2}));
+ assertThat(deserialized.dvSnapshotId()).isEqualTo(42L);
+ assertThat(deserialized.deletedPositions()).isNull();
+ assertThat(deserialized.replacedPositions()).isNull();
}
@Test
- void testKryoSerializationRoundTrip() throws IOException {
- TrackingStruct tracking =
- TrackingStruct.builder()
- .status(EntryStatus.ADDED)
- .snapshotId(42L)
- .dataSequenceNumber(10L)
- .deletedPositions(new byte[] {1, 2})
+ void testExistingWithManifestDVPositionsJavaSerializationRoundTrip()
+ throws IOException, ClassNotFoundException {
+ Tracking tracking =
+ TrackingBuilder.from(manifestSourceTracking(), 1L)
+ .deletedPositions(ByteBuffer.wrap(new byte[] {1, 2}))
+ .replacedPositions(ByteBuffer.wrap(new byte[] {3, 4}))
.build();
- TrackingStruct deserialized = TestHelpers.KryoHelpers.roundTripSerialize(tracking);
+ Tracking deserialized = TestHelpers.roundTripSerialize(tracking);
+
+ assertThat(deserialized.status()).isEqualTo(EntryStatus.EXISTING);
+ assertThat(deserialized.dvSnapshotId()).isNull();
+ assertThat(deserialized.deletedPositions()).isEqualTo(ByteBuffer.wrap(new byte[] {1, 2}));
+ assertThat(deserialized.replacedPositions()).isEqualTo(ByteBuffer.wrap(new byte[] {3, 4}));
+ }
+
+ @Test
+ void testAddedWithDvSnapshotIdKryoSerializationRoundTrip() throws IOException {
+ Tracking tracking = TrackingBuilder.added(42L).dvUpdated().build();
+
+ Tracking deserialized = TestHelpers.KryoHelpers.roundTripSerialize(tracking);
assertThat(deserialized.status()).isEqualTo(EntryStatus.ADDED);
assertThat(deserialized.snapshotId()).isEqualTo(42L);
- assertThat(deserialized.dataSequenceNumber()).isEqualTo(10L);
+ assertThat(deserialized.dvSnapshotId()).isEqualTo(42L);
+ assertThat(deserialized.deletedPositions()).isNull();
+ assertThat(deserialized.replacedPositions()).isNull();
+ }
+
+ @Test
+ void testExistingWithManifestDVPositionsKryoSerializationRoundTrip() throws IOException {
+ Tracking tracking =
+ TrackingBuilder.from(manifestSourceTracking(), 1L)
+ .deletedPositions(ByteBuffer.wrap(new byte[] {1, 2}))
+ .replacedPositions(ByteBuffer.wrap(new byte[] {3, 4}))
+ .build();
+
+ Tracking deserialized = TestHelpers.KryoHelpers.roundTripSerialize(tracking);
+
+ assertThat(deserialized.status()).isEqualTo(EntryStatus.EXISTING);
+ assertThat(deserialized.dvSnapshotId()).isNull();
assertThat(deserialized.deletedPositions()).isEqualTo(ByteBuffer.wrap(new byte[] {1, 2}));
+ assertThat(deserialized.replacedPositions()).isEqualTo(ByteBuffer.wrap(new byte[] {3, 4}));
+ }
+
+ private static TrackingStruct sourceTracking() {
+ TrackingStruct tracking = new TrackingStruct(Tracking.schema());
+ tracking.set(STATUS_ORDINAL, EntryStatus.ADDED.id());
+ tracking.set(SNAPSHOT_ID_ORDINAL, 42L);
+ tracking.set(DATA_SEQUENCE_NUMBER_ORDINAL, 10L);
+ tracking.set(FILE_SEQUENCE_NUMBER_ORDINAL, 10L);
+ tracking.set(DV_SNAPSHOT_ID_ORDINAL, 43L);
+ tracking.set(FIRST_ROW_ID_ORDINAL, 1000L);
+ return tracking;
+ }
+
+ private static TrackingStruct sourceTrackingWithStatus(EntryStatus status) {
+ TrackingStruct tracking = sourceTracking();
+ tracking.set(STATUS_ORDINAL, status.id());
+ return tracking;
+ }
+
+ private static TrackingStruct manifestSourceTracking() {
+ TrackingStruct tracking = sourceTracking();
+ tracking.set(DV_SNAPSHOT_ID_ORDINAL, null);
+ return tracking;
}
}