Skip to content

Fix Iceberg trivial count bug#111617

Open
melvynator wants to merge 2 commits into
masterfrom
fix-iceberg-trivial-count
Open

Fix Iceberg trivial count bug#111617
melvynator wants to merge 2 commits into
masterfrom
fix-iceberg-trivial-count

Conversation

@melvynator

Copy link
Copy Markdown
Member

This is addressing the bug of trivial count mentioned in #92861

When the summary in the snapshot get corrupted by a bad commit it might get out of sync compared to the real count.

Changelog category (leave one):

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

Changelog entry (a [user-readable short description]

Fix Iceberg trivial count in the case where the snapshot summary is not accurate by referring to the manifest list.

… summary

IcebergMetadata::totalRows() returned the total-records hint from the
snapshot summary unconditionally. Writers maintain that hint as
parent_total + added, so it silently diverges from the actual data when
the table history contains a lossy or corrupted commit, making
SELECT count() disagree with a full scan of the same table.

Derive the count from the manifest list instead: in format v2+ every
entry carries added_rows_count/existing_rows_count, whose sum over data
manifests is the exact live row count. Fall back to the summary hint
only for v1 manifest lists without counts or when delete manifests are
present. Log a warning when the summary disagrees with the manifest
list.
@melvynator melvynator changed the title Fix Iceberg trivial count Fix Iceberg trivial count bug Jul 23, 2026
@clickhouse-gh

clickhouse-gh Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [96694c9]

Summary:

job_name test_name status info comment
Stateless tests (arm_asan_ubsan, targeted) FAIL
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
Too many test failures FAIL cidb
Stateless tests (amd_asan_ubsan, flaky check) FAIL
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
Too many test failures FAIL cidb
Stateless tests (amd_tsan, flaky check) FAIL
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
2 more test cases not shown
Stateless tests (amd_msan, flaky check) FAIL
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
Too many test failures FAIL cidb
Stateless tests (amd_debug, flaky check) FAIL
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
Too many test failures FAIL cidb
Stateless tests (amd_debug, parallel) FAIL
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb
04538_iceberg_multi_command_alter_mutation FAIL cidb
Stateless tests (arm_binary, parallel) FAIL
04611_iceberg_count_ignores_corrupted_snapshot_summary FAIL cidb

AI Review

Summary

This PR makes Iceberg totalRows() prefer manifest-list row counts over the snapshot total-records hint and adds regression coverage for corrupted snapshot summaries. The position-delete case from the earlier thread is fixed, but the trivial-count path is still not correct for equality-delete snapshots, and malformed v2 manifest-list row counts still fall back to the stale summary hint the patch is trying to distrust.

Findings

❌ Blockers

  • [src/Storages/ObjectStorage/DataLakes/Iceberg/IcebergMetadata.cpp:1167-1188] Delete manifests are handled as one bucket, but the fallback count logic only subtracts POSITION_DELETE rows. Equality deletes still go through the trivial-count path even though their delete files do not encode how many table rows they actually remove. The existing deletes_db/eq_deletes_table fixture already demonstrates the gap: 03595_equality_deletes_simple.reference shows count(name) = 881, while the current snapshot metadata has total-records = 1010 and total-position-deletes = 30, so this code path would return 980. Suggested fix: detect EQUALITY_DELETE entries while scanning delete manifests and return {} so SELECT count() falls back to a real scan instead of an incorrect trivial count.

⚠️ Majors

  • [src/Storages/ObjectStorage/DataLakes/Iceberg/StatelessMetadataFileGetter.cpp:220-235] The new row-count fast path silently collapses malformed v2 added_rows_count/existing_rows_count values to std::nullopt, and IcebergMetadata::totalRows() then falls straight back to summary_total_rows. That reintroduces the same stale summary hint this PR is trying to avoid for corrupted metadata, even though the manifest-file scan below can still compute the exact count in the no-delete case. Suggested fix: reject malformed v2 row counts with ICEBERG_SPECIFICATION_VIOLATION, or at minimum carry an "invalid v2 counts" flag so totalRows() skips the summary fast path and continues to the manifest scan.
Final Verdict

❌ Changes requested.

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Jul 23, 2026
@scanhex12 scanhex12 self-assigned this Jul 23, 2026
/// status ADDED and EXISTING respectively. Required in format v2+, optional in v1
/// (std::nullopt when absent or null). Their sum over all data manifests is the exact
/// live row count of a snapshot without delete files, see IcebergMetadata::totalRows().
std::optional<Int64> added_rows_count;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

UInt64

}
],
"properties": {
"owner": "divanik",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Legacy via copy-paste

if (entry.content_type == Iceberg::ManifestFileContentType::DATA)
{
if (entry.added_rows_count.has_value() && entry.existing_rows_count.has_value())
manifest_list_rows += *entry.added_rows_count + *entry.existing_rows_count;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why

manifest_list_rows += *entry.added_rows_count + *entry.existing_rows_count

?

I think

manifest_list_rows += *entry.added_rows_count

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.

Because EXISTING manifests are still part of the current snapshot. After the first commit, a snapshot can inherit manifests from earlier snapshots (plain append, manifest-only rewrite, compaction, etc.), and those rows remain live even though they were not newly added in this snapshot. Summing only added_rows_count would undercount every snapshot that carries forward pre-existing manifests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

makes sense

Comment thread src/Storages/ObjectStorage/DataLakes/Iceberg/IcebergMetadata.cpp Outdated
@Diskein

Diskein commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@melvynator we merged some PRs about summary when we do different operations on iceberg tables, and another one on review #109288, with some tests for mutations as well. I think we are testing summary snapshot consistency much better now and the corruption chance by clickhouse is less then before, but what if counters are wrong in manifests lists? should we check the data files then? We would again ended up with broken count.

WDYT about adding some metadata validation on iceberg tables creation/attaching:

  • check totals of the summary snapshot
  • correct totals (or just throw) if totals are wrong
  • CHECK TABLE command should also check this totals

cc: @scanhex12

…ests

- Use UInt64 for the manifest-list row counts (review comment), treating
  negative values as absent since they violate the spec.
- Do not fall back to the summary total-records hint when the snapshot
  has delete manifests: the hint is maintained incrementally by writers
  and can be corrupted the same way, so go to the manifest scan, which
  derives both data and position-delete rows from the actual manifests.
- Add a stateless test covering position deletes with a corrupted
  summary: 5 rows, 2 deleted via merge-on-read, total-records corrupted
  from 5 to 100; count() must return 3, not 98.
/// poisons it the same way, and with delete manifests present the manifest scan below
/// is the only source that derives both data and position-delete rows from the actual
/// manifests instead of writer-provided hints.
if (!has_delete_manifests && summary_total_rows.has_value())

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.

This branch sends every delete-manifest snapshot into the manifest scan below, but that scan only subtracts POSITION_DELETE rows. We already have a fixture showing why that is not enough: 03595_equality_deletes_simple.reference has count(name) = 881 for deletes_db/eq_deletes_table, while the current snapshot metadata says total-records = 1010 and total-position-deletes = 30. If totalRows() runs through the code below, it comes back as 980 because equality deletes are never accounted for.

Please bail out of trivial count when any manifest file contains EQUALITY_DELETE entries (return {} and force a real scan), or add logic that can prove the exact number of rows removed by equality deletes.


/// Row counts are required in v2+ manifest lists and optional in v1.
/// They let totalRows() compute an exact count without opening manifest files.
/// Negative values violate the spec, treat them as absent.

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.

For v2 manifest lists these row-count fields are required, so treating a negative or NULL value as std::nullopt is not just "count unavailable" - it changes the control flow in IcebergMetadata::totalRows(). A corrupted v2 entry lands in all_data_manifests_have_counts = false, and the next branch at IcebergMetadata.cpp:1172 goes straight back to summary_total_rows, which is exactly the stale writer-provided hint this PR is trying to stop trusting.

Please either throw ICEBERG_SPECIFICATION_VIOLATION for malformed v2 row counts, or at least make the invalid-v2 case bypass the summary fast path and fall through to the manifest-file scan.

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

Labels

pr-bugfix Pull request with bugfix, not backported by default

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants