Skip to content

Do not refuse a manifest-only Iceberg rewrite over an absent parent total - #117012

Open
groeneai wants to merge 1 commit into
ClickHouse:masterfrom
groeneai:groeneai/iceberg-manifest-only-keeps-absent-parent-total
Open

Do not refuse a manifest-only Iceberg rewrite over an absent parent total#117012
groeneai wants to merge 1 commit into
ClickHouse:masterfrom
groeneai:groeneai/iceberg-manifest-only-keeps-absent-parent-total

Conversation

@groeneai

@groeneai groeneai commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fixes OPTIMIZE TABLE ... MANIFEST failing with BAD_ARGUMENTS on an Iceberg table whose current snapshot summary omits an optional total-records, total-files-size or total-data-files metric, which made manifest compaction permanently unavailable on tables written by engines that omit those metrics. Closes #117000.

Description

Closes: #117000

The total-* snapshot summary metrics are optional per the Iceberg spec, so a table written by another engine may omit them and still read correctly. setSnapshotTotals refused to derive a new total whenever the parent summary omitted one, regardless of the delta being applied. generateManifestOnlySnapshot passes 0 for every delta, so OPTIMIZE TABLE ... MANIFEST was refused outright:

Code: 36. DB::Exception: Cannot derive Iceberg snapshot total 'total-records':
the parent snapshot's summary omits it. (BAD_ARGUMENTS)

That also contradicts the documented contract of generateManifestOnlySnapshot, which is to carry the total-* counters forward so the rewrite is idempotent.

I made the refusal depend on the delta rather than on the caller. A zero delta leaves the total exactly as the parent had it, so an absent counter is carried forward by staying absent, as the sibling delete-family totals already tolerate absence. A non-zero delta still cannot be derived from an unknown total, so the INSERT path keeps refusing.

The new stateless test strips total-records from an IcebergLocal table's current snapshot summary, reloads the table and runs the real statement. Before, OPTIMIZE TABLE t MANIFEST failed as above while the table read correctly; after, it succeeds and the new snapshot omits total-records while forwarding total-files-size and total-data-files unchanged. Widening the new condition to accept any delta instead fails the INSERT arm, so each arm covers a different defect. The existing test_optimize_manifest_parent_summary_missing_totals strips only the three delete-family counters, which set_delete_total already tolerated, which is why this case was uncovered.

Note for review: on this path an absent delete-family counter is still published as 0 even when delete files are live. Master behaves identically, and that test pins the 0, so I left it out of scope rather than widen this fix.


Workflow [PR]
Sync PR [sync-upstream/pr/117012]

@clickhouse-gh

clickhouse-gh Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [0572538]


AI Review

Summary

This PR changes MetadataGenerator::setSnapshotTotals so zero-delta Iceberg manifest rewrites preserve absent parent data totals instead of throwing BAD_ARGUMENTS. The implementation looks consistent with the surrounding optional-summary handling, but the new coverage stops at the helper boundary and does not yet prove the user-visible OPTIMIZE TABLE ... MANIFEST regression from #117000 end-to-end.

Missing context / blind spots
  • ⚠️ Most GitHub CI jobs were still pending at review time; only Fast test and Style check had completed. A full green CI run would close that gap.
Tests
  • ⚠️ src/Storages/ObjectStorage/DataLakes/Iceberg/tests/gtest_iceberg_metadata_generator.cpp:232 The new gtests validate the MetadataGenerator branch, but the reported bug reproduces on the SQL OPTIMIZE TABLE ... MANIFEST path against on-disk Iceberg metadata. Please extend the existing manifest-compaction integration coverage, or add a focused stateless IcebergLocal test, that strips total-records / total-files-size / total-data-files from the latest metadata file and runs the actual OPTIMIZE.
Final Verdict

⚠️ One review concern: the fix itself looks plausible, but the PR still needs end-to-end regression evidence for the exact user-visible path it claims to fix.

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Aug 29, 2026
@groeneai groeneai added can be tested Allows running workflows for external contributors groeneai-origin-review-bot PR origin: review-bot finding on a groeneai PR labels Aug 29, 2026
…otal

The `total-*` snapshot summary metrics are optional per the Iceberg spec, so a table
written by another engine may omit them and still read correctly. `set_data_total` threw
BAD_ARGUMENTS whenever the parent summary omitted one, regardless of the delta being
applied. `generateManifestOnlySnapshot` passes 0 for every delta, so `OPTIMIZE TABLE ...
MANIFEST` was permanently unavailable on such a table:

  Code: 36. DB::Exception: Cannot derive Iceberg snapshot total 'total-records':
  the parent snapshot's summary omits it. (BAD_ARGUMENTS)

That contradicts the documented contract of `generateManifestOnlySnapshot`, which is to
carry the `total-*` counters forward so the rewrite is idempotent.

Condition the refusal on the delta rather than on the caller. A zero delta leaves the
table-wide total exactly as the parent had it, so an absent counter is carried forward by
staying absent, the way the sibling `set_delete_total` already tolerates absence. A
non-zero delta still cannot be derived from an unknown total, so the INSERT path keeps
refusing: writing the delta alone would report a total that omits the parent's rows.

Verified at SQL level on an `IcebergLocal` table whose current snapshot summary has
`total-records` removed: before, `OPTIMIZE TABLE t MANIFEST` failed with the error above
while the table read correctly; after, it succeeds and the new snapshot omits
`total-records` while forwarding `total-files-size` and `total-data-files` unchanged. An
untouched control table compacts identically in both cases, which is what shows the
fixture reaches this code rather than an earlier threshold check.

Two tests are added to the existing gtest module, one per branch of the new condition.
Making the carry-forward unconditional instead does not compile
(-Werror,-Wunreachable-code), so the over-broad form of this change is rejected by the
build.

Closes: ClickHouse#117000
@groeneai
groeneai force-pushed the groeneai/iceberg-manifest-only-keeps-absent-parent-total branch from 6edf795 to 0572538 Compare August 29, 2026 10:39
/// engine may omit them. A manifest-only rewrite adds no data, so every such total is unchanged and
/// an absent one stays absent; refusing the rewrite would leave OPTIMIZE ... MANIFEST permanently
/// unavailable on a table that reads correctly. Totals the parent does carry are still forwarded.
TEST(IcebergMetadataGenerator, ManifestOnlySnapshotWhenParentTotalRecordsMissing)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation change looks reasonable, but this only exercises MetadataGenerator in isolation. The regression in #117000 reproduces on the SQL OPTIMIZE TABLE ... MANIFEST path against on-disk Iceberg metadata, and the repo already has manifest-compaction integration coverage in tests/integration/test_storage_iceberg_with_spark/test_manifest_compaction.py. Please add a focused end-to-end case there (or a stateless IcebergLocal test) that strips total-records / total-files-size / total-data-files from the latest metadata file and runs the actual OPTIMIZE, so future caller-side regressions are caught too.

@clickhouse-gh

clickhouse-gh Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Build profile diff (arm_release)

Comparing 0572538c6 with master ab9ebb053 (stripped binary size, per-symbol sizes and ThinLTO time; compile times per translation unit against the most recent warmup build that recompiled it).

✅ No significant changes.

Binary sizes
Binary Master PR Δ
programs/clickhouse-stripped 710.14 MiB 707.08 MiB -3.05 MiB (-0.43%)

Only the stripped binary is compared: the official master build keeps debug symbols while PR builds strip them, so the other binaries differ by construction.

Compile time of recompiled translation units

7 translation units recompiled, 12 s compile time in total, 7 of them have a recent master baseline.

Job report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors groeneai-origin-review-bot PR origin: review-bot finding on a groeneai PR pr-bugfix Pull request with bugfix, not backported by default

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OPTIMIZE MANIFEST refused when snapshot summary omits total-records

1 participant