Skip to content

Commit 9af3f57

Browse files
committed
fix: preserve manifest min sequence number of 0
ManifestWriter.to_manifest_file() used `self._min_sequence_number or UNASSIGNED_SEQ`, which treats a legitimate minimum data sequence number of 0 as falsy and collapses it to UNASSIGNED_SEQ (-1). A data sequence number of 0 is valid for a live file (files from a v1 table, or the initial commit of a v2 table), so this diverges from the Java reference, which falls back to UNASSIGNED_SEQ only when the minimum is unset (null). When such a manifest is produced by a merge/compaction (the added snapshot id equals the current commit snapshot id), prepare_manifest() interprets the -1 as "no file had an assigned sequence number" and overwrites it with the current, higher commit sequence number, silently raising the manifest's min data sequence number. That in turn affects sequence-number-based delete-file application and scans. Use an explicit None check, mirroring the Java ManifestWriter, and add a regression test that drives a live existing entry with sequence number 0 through to_manifest_file() for both format versions. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent 2c75523 commit 9af3f57

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

pyiceberg/manifest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,9 @@ def to_manifest_file(self) -> ManifestFile:
11451145
"""Return the manifest file."""
11461146
# once the manifest file is generated, no more entries can be added
11471147
self.closed = True
1148-
min_sequence_number = self._min_sequence_number or UNASSIGNED_SEQ
1148+
# A min sequence number of 0 is legitimate (e.g. live files from a v1 table or the
1149+
# initial commit of a v2 table), so fall back to UNASSIGNED_SEQ only when it is unset.
1150+
min_sequence_number = self._min_sequence_number if self._min_sequence_number is not None else UNASSIGNED_SEQ
11491151
return ManifestFile.from_args(
11501152
manifest_path=self._output_file.location,
11511153
manifest_length=len(self._writer.output_file),

tests/utils/test_manifest.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,48 @@ def test_manifest_writer_tell(format_version: TableVersion) -> None:
938938
assert after_entry_bytes > initial_bytes, "Bytes should increase after adding entry"
939939

940940

941+
@pytest.mark.parametrize("format_version", [1, 2])
942+
def test_write_manifest_min_sequence_number_zero(format_version: TableVersion) -> None:
943+
# A data sequence number of 0 is a legitimate min for a live file (e.g. files from a
944+
# v1 table or the initial commit of a v2 table). It must be preserved in the manifest,
945+
# not collapsed to UNASSIGNED_SEQ (-1), which would let a merge/compaction silently
946+
# raise the manifest's min data sequence number. This mirrors the Java reference, which
947+
# only falls back to UNASSIGNED_SEQ when the min is unset (null).
948+
io = load_file_io()
949+
test_schema = Schema(NestedField(1, "foo", IntegerType(), False))
950+
951+
with TemporaryDirectory() as tmpdir:
952+
output_file = io.new_output(f"{tmpdir}/test-manifest.avro")
953+
with write_manifest(
954+
format_version=format_version,
955+
spec=UNPARTITIONED_PARTITION_SPEC,
956+
schema=test_schema,
957+
output_file=output_file,
958+
snapshot_id=1,
959+
avro_compression="null",
960+
) as writer:
961+
data_file = DataFile.from_args(
962+
content=DataFileContent.DATA,
963+
file_path=f"{tmpdir}/data.parquet",
964+
file_format=FileFormat.PARQUET,
965+
partition=Record(),
966+
record_count=100,
967+
file_size_in_bytes=1000,
968+
)
969+
writer.existing(
970+
ManifestEntry.from_args(
971+
status=ManifestEntryStatus.EXISTING,
972+
snapshot_id=1,
973+
sequence_number=0,
974+
file_sequence_number=0,
975+
data_file=data_file,
976+
)
977+
)
978+
manifest_file = writer.to_manifest_file()
979+
980+
assert manifest_file.min_sequence_number == 0
981+
982+
941983
def test_inherit_from_manifest_snapshot_id() -> None:
942984
entry = ManifestEntry.from_args(
943985
status=ManifestEntryStatus.ADDED,

0 commit comments

Comments
 (0)