)]}'
{
  "commit": "48e710d20ceeeaa637d5aeae7746b787410859f8",
  "tree": "e3457c608031fc471a25a20777a7a7e8be2c26ee",
  "parents": [
    "8a31e79228d9a8da93c162ce22f1ce044ad11273"
  ],
  "author": {
    "name": "Anas Khan",
    "email": "anxkhn28@gmail.com",
    "time": "Fri Jul 17 09:22:41 2026 +0530"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Fri Jul 17 05:52:41 2026 +0200"
  },
  "message": "fix: preserve manifest min sequence number of 0 (#3660)\n\n# Rationale for this change\n\n`ManifestWriter.to_manifest_file()` computed the manifest\u0027s minimum data\nsequence number with a truthiness fallback:\n\n```python\nmin_sequence_number \u003d self._min_sequence_number or UNASSIGNED_SEQ\n```\n\nA data sequence number of `0` is a legitimate minimum for a live file\n(files\nfrom a v1 table, or the initial commit of a v2 table). Because `0` is\nfalsy,\n`or` collapses it to `UNASSIGNED_SEQ` (`-1`), i.e. it treats a real `0`\nthe same\nas \"unset\".\n\nThis diverges from the Java reference implementation, which falls back\nto\n`UNASSIGNED_SEQ` only when the value is actually unset:\n\n```java\n// ManifestWriter#toManifestFile (apache/iceberg)\nlong minSeqNumber \u003d minDataSequenceNumber !\u003d null ? minDataSequenceNumber : UNASSIGNED_SEQ;\n```\n\nThe `-1` is not harmless. When the manifest is produced by a\nmerge/compaction\n(so its `added_snapshot_id` equals the current commit snapshot id),\n`ManifestListWriter.prepare_manifest()` treats `min_sequence_number \u003d\u003d\nUNASSIGNED_SEQ` as \"no file had an assigned sequence number\" and\noverwrites it\nwith the current, higher commit sequence number\n(`pyiceberg/manifest.py`, the `if\nwrapped_manifest_file.min_sequence_number \u003d\u003d\nUNASSIGNED_SEQ:` branch). The manifest\u0027s minimum data sequence number is\nthereby\nsilently raised, which affects sequence-number-based delete-file\napplication and\nscans.\n\nThe fix uses an explicit `None` check instead of a truthiness fallback,\nmirroring the Java writer:\n\n```python\nmin_sequence_number \u003d self._min_sequence_number if self._min_sequence_number is not None else UNASSIGNED_SEQ\n```\n\n## Are these changes tested?\n\nYes. A new parametrized regression test,\n\n`tests/utils/test_manifest.py::test_write_manifest_min_sequence_number_zero`\n(format versions 1 and 2), drives a live `EXISTING` entry with\n`sequence_number\u003d0` through `ManifestWriter.existing()` and asserts that\n`to_manifest_file().min_sequence_number \u003d\u003d 0`.\n\nThe test fails before the fix (`assert -1 \u003d\u003d 0`) and passes after it.\nThis path\nhad no prior coverage: no existing test drove a live sequence-0 entry\nthrough\n`to_manifest_file()`, which is why the defect was not caught.\n\nLocal runs (unit tests):\n- `pytest tests/utils/test_manifest.py -q` -\u003e passes.\n- `pytest tests/table/test_snapshots.py\ntests/table/test_manage_snapshots.py -q`\n  -\u003e passes.\n- `make lint` (ruff, ruff-format, mypy, pydocstyle, codespell) -\u003e clean\non the\n  changed files.\n\nThe merge/compaction integration path exercised by `prepare_manifest()`\nis\ncovered by the integration suite, which requires Docker and Spark; those\nwere\nnot run locally. The new unit test exercises the same\n`to_manifest_file()` write\npath that feeds it.\n\n## Are there any user-facing changes?\n\nNo API changes. It is a correctness fix: manifests written for live\nfiles whose\nminimum data sequence number is `0` now record `0` instead of `-1`, so a\nsubsequent merge/compaction no longer silently raises the manifest\u0027s\nminimum\ndata sequence number.\n\nSigned-off-by: Anas Khan \u003c83116240+anxkhn@users.noreply.github.com\u003e",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "9842f79d8efac88096ea94150f3e576e89dd0449",
      "old_mode": 33188,
      "old_path": "pyiceberg/manifest.py",
      "new_id": "37dbd04b139a651921ee2932a3c357e78f9be7f9",
      "new_mode": 33188,
      "new_path": "pyiceberg/manifest.py"
    },
    {
      "type": "modify",
      "old_id": "f2ae1e05ad02290a254d227f247e7f4053009211",
      "old_mode": 33188,
      "old_path": "tests/utils/test_manifest.py",
      "new_id": "eac4d520bc879de4d61dcb11d8552e8e1d07b72e",
      "new_mode": 33188,
      "new_path": "tests/utils/test_manifest.py"
    }
  ]
}
