Skip to content

Iceberg deletion vector support - #2119

Open
ianton-ru wants to merge 5 commits into
antalya-26.6from
feature/antalya-26.6/iceberg-puffin-deletion-vectors-read
Open

Iceberg deletion vector support#2119
ianton-ru wants to merge 5 commits into
antalya-26.6from
feature/antalya-26.6/iceberg-puffin-deletion-vectors-read

Conversation

@ianton-ru

@ianton-ru ianton-ru commented Jul 27, 2026

Copy link
Copy Markdown

Changelog category (leave one):

  • New Feature

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

Iceberg deletion vector support

Documentation entry for user-facing changes

Iceberg deletion vector support.
Based on uncommited Puffin file reader ClickHouse#109990.

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

@ianton-ru

Copy link
Copy Markdown
Author

@codex review

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Workflow [PR], commit [4b30a8f]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5bd1ca373d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const bool effective_need_only_count = need_only_count;
#endif

const bool can_use_count_cache = effective_need_only_count && headers_requested

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Negate the _headers guard before using the count cache

When a count-style query requests _headers and a cached row count exists, this predicate enables the ConstChunkGenerator shortcut, so no data GET occurs and the result can contain metadata-probe/HEAD headers instead of the requested GET headers. Conversely, ordinary count queries without _headers can no longer use the cache. Require !headers_requested here so the shortcut is skipped rather than falling back to different headers.

AGENTS.md reference: AGENTS.md:L153-L153

Useful? React with 👍 / 👎.

@@ -0,0 +1,46 @@
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add mandatory sidebar fields to the new Puffin docs

Both this new page and docs/en/interfaces/formats/Puffin/PuffinMetadata.md omit the mandatory sidebar_label and sidebar_position frontmatter fields, leaving the new documentation pages noncompliant with the repository's required metadata. Add both fields to each page's frontmatter.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

ianton-ru and others added 4 commits July 28, 2026 09:49
Restore `!headers_requested` after a merge-conflict resolution inverted the condition, which disabled the cache for ordinary counts and allowed wrong headers on cache hits.

Co-authored-by: Cursor <cursoragent@cursor.com>
Include `sidebar_label`, `sidebar_position`, and input/output flags so the new pages match AGENTS.md and peer format docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Without the guard, Fast test builds that disable Avro fail to compile because IcebergPathFromMetadata is only visible when Avro is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ianton-ru

ianton-ru commented Jul 28, 2026

Copy link
Copy Markdown
Author

Reviewer notes: Altinity PR #2119

Title: Iceberg deletion vector support
Base: antalya-26.6
Branch: feature/antalya-26.6/iceberg-puffin-deletion-vectors-read
Scope: read-only Iceberg v3 deletion vectors (deletion-vector-v1 in Puffin files). No DV write path.


Motivation

Iceberg v3 replaces classic position-delete files with deletion vectors stored as roaring bitmaps inside Puffin files. Without this, ClickHouse can return rows that engines like Spark already treat as deleted.
This PR ports and hardens that read path onto Antalya 26.6, based on upstream work around the Puffin reader (ClickHouse#109990) plus follow-up correctness fixes.


What landed (architecture)

1. SQL formats Puffin / PuffinMetadata

  • Puffin — materializes deleted row positions from deletion-vector-v1 blobs (referenced_data_file, deleted_rows).
  • PuffinMetadata — one row per footer blob entry (type, offsets, fields, etc.).
  • Shared low-level reader: PuffinDeletionVectorReader (envelope peek, CRC, cardinality checks, materialization ceiling before large allocates).
  • Docs under docs/en/interfaces/formats/Puffin/.

2. Iceberg table / table-function read path

  • Manifest parsing recognizes deletion-vector content and attaches DV metadata to data files.
  • IcebergDeletionVector loads the bitmap (optionally via PuffinFilesCache).
  • DeletionVectorTransform filters rows by file-local positions carried in excluded_rows.
  • Wired in StorageObjectStorageSource::createReader.

3. Cache and settings

  • Setting use_puffin_files_cache (default true) caches parsed deletion vectors keyed by path/etag/offset/size/referenced file.
  • Server setting + SYSTEM flush support for the cache.
  • Settings history entry for Antalya: 26.6.1.20001.altinityantalya (cherry-pick cleaned upstream 26.7/26.8 noise).

4. Correctness / fail-closed behavior (important for review)

Area Behavior
Cluster protocol Refuse to serialize tasks with non-empty excluded_rows / Iceberg eq+pos deletes / file_bucket_info when worker protocol is too old (would otherwise silently return deleted or duplicated rows).
DV vs equality deletes Apply DeletionVectorTransform before equality FilterTransform so file row numbers stay valid.
Count shortcuts Skip count-from-files cache when DVs / buckets / Iceberg equality+position deletes are present; require explicit total-equality-deletes == 0 for snapshot totalRows shortcut; disable need_only_count when equality deletes exist.
_headers + count cache Count-cache shortcut skipped when _headers is requested (otherwise cache hit returns wrong headers).
Data format Reject DVs on non-Parquet data files.
Avro-disabled builds IcebergDeletionVector.cpp wrapped in #if USE_AVRO (Fast test without Avro).

Tests

  • GTests: Puffin envelope/cardinality/bounds, files cache, DV-before-equality order, Iceberg count shortcuts, data-object clone, Parquet row-delete guards, ParquetV3 need_only_count + buckets.
  • Stateless: 04077 / 04257 / 04260 / 04548 / 04549 / 04612 / 04613 (Puffin formats); 0426104263 (Iceberg DV + cache) with MinIO fixtures under tests/queries/0_stateless/data_minio/dv_puffin_warehouse/ and data_puffin/.
  • Integration: tests/integration/test_storage_iceberg_with_spark/test_deletion_vectors.py.

Follow-up commits on top of the main cherry-pick

These are small fixes after the initial Iceberg deletion vector commit / review / CI:

  1. !headers_requested polarity — merge-conflict typo had inverted the count-cache guard.
  2. Puffin docs frontmattersidebar_label / sidebar_position (+ input/output flags) for AGENTS.md compliance.
  3. USE_AVRO guard on IcebergDeletionVector.cpp — fixes Fast test compile without Avro.
  4. Settings history — Antalya version key only for use_puffin_files_cache.

Out of scope / intentional limits

  • No writing Iceberg deletion vectors.
  • Non-deletion-vector-v1 Puffin blob types are skipped by the SQL Puffin format.
  • Selecting only referenced_data_file skips payload I/O (and thus envelope/CRC/materialization checks) by design.
  • Poco JSON Int64 wrap of values near 2^63 in footer parsing was reviewed and deferred (not treated as a blocker in this PR).

Suggested review focus

  1. Manifest → excluded_rows attachment and path matching (referenced_data_file vs data file).
  2. Transform order (DV before equality) and cluster fail-closed protocol checks.
  3. Count / totalRows / cache interactions — easy to silently over-count.
  4. Resource bounds in PuffinDeletionVectorReader (cardinality ceiling, envelope peek before allocate).
  5. Antalya-specific settings history entry vs upstream versioning.

Links

@ianton-ru

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b30a8f3e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +63 to +64
for (UInt64 position : deleted_positions)
bitmap->add(static_cast<size_t>(position));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject positions beyond the data file

When a deletion vector contains a position greater than or equal to the referenced data file's manifest record_count, this loop inserts it without validation; the transform never encounters that position and silently returns rows that the manifest claims were deleted. This can also make a full scan disagree with Iceberg's trivial count() calculation, which subtracts the deletion vector's record_count. Pass the data file row count into the loader and reject out-of-range positions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the note. Agreed that we do not currently bound-check positions against the data file’s manifest record_count (that field lives on IcebergDataObjectInfo / the data-file entry and is not passed into loadDeletionVector).

A few clarifications on impact:

  1. In-range deletes are unaffected. DeletionVectorTransform only filters rows whose file-local indices appear in the bitmap (rb_contains on ChunkInfoRowNumbers). Positions past the real file never match a produced row, so they are no-ops for the scan — they do not revive deleted in-range rows.
  2. What “manifest claims deleted” means here. The DV entry’s own record_count is already checked exactly against the roaring bitmap cardinality in PuffinDeletionVectorReader. The gap is only vs the data file row count. OOB positions mean some declared deletes are ineffective, not that valid deletes are skipped.
  3. Count disagreement with the snapshot totalRows shortcut (total_records - total_position_deletes) would require the table metadata/summary itself to be inconsistent with the on-disk DV (corrupt or non-spec writer). For normal Spark/Iceberg-written tables, writers keep positions within the file.

So this is a reasonable fail-closed hardening against bad metadata (and we could pass object_info->info.record_count into the loader and reject OOB positions), but we do not treat it as a correctness bug for well-formed Iceberg v3 deletion vectors. Prefer to leave it out of this PR unless reviewers want the extra check now.

if (etag.empty())
return std::nullopt;

return PuffinFilesCacheKey{file_path, etag, content_offset, content_size_in_bytes, referenced_data_file};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include declared cardinality in the cache key

When two manifest versions reference the same Puffin path, ETag, slice, and data file but declare different record_count values, this key aliases them even though deserialization validates the bitmap against that declaration. After one version populates the cache, the other receives the cached bitmap without running its cardinality check, so malformed or inconsistent metadata is accepted depending on query order. Include the expected cardinality in the key or revalidate it on every cache hit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks. Confirmed: tryCreateKey / PuffinFilesCacheKey currently use (file_path, etag, content_offset, content_size_in_bytes, referenced_data_file) and omit the manifest record_count passed as expected_cardinality.

Why this is weaker than it sounds for query correctness:

  1. A cache entry is only created after a successful uncached load. That path always runs readDeletionVectorFromPuffin(..., expected_cardinality), which requires the roaring bitmap cardinality to exactly match the declared value. A mismatched declaration never populates the cache.
  2. Same path + etag + slice ⇒ same blob bytes. The true bitmap (and thus true cardinality) is fixed. The only realistic “two declarations, one key” case is inconsistent metadata for the same physical object — not two different bitmaps aliased together.
  3. On a later hit with a wrong declaration, we skip re-checking cardinality and return the already-validated bitmap. Scan filtering still matches the on-disk DV; we fail to detect that this manifest entry’s record_count disagrees with the blob.

So the finding is a real gap for fail-closed metadata validation (and adding cardinality to the key, or checking bitmap->size() == expected_cardinality on hit, would close it). It is not a silent wrong-bitmap / wrong-answer bug for consistent Iceberg tables.

Prefer to leave the key as-is for this PR unless reviewers want the stricter check now; happy to follow up with cardinality in the key (and a small gtest) if desired.

@ianton-ru

Copy link
Copy Markdown
Author

Failed tests looks unrelated to PR.
SQLLogic fails with not enough memory error.

Stateless test 04033_tpc_ds_q63 has timeout, marked as flaky
Stateless test 00071_merge_tree_optimize_aio also has a timeout.
Both are not related to puffin files.

Regression tests are useless unless all Antalya features are merged in the branch. Were turned on by default, I have turned them off only right now.

@ianton-ru ianton-ru changed the title [WIP] Iceberg deletion vector support Iceberg deletion vector support Jul 29, 2026
@ilejn

ilejn commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Doesn't it make sense to emphasize differences between this PR and 109990 (unless you are totally skeptical about its chances to be accepted to upstream)?
Is cache introduced by this PR?

@ianton-ru

Copy link
Copy Markdown
Author

109990 Is only a Puffin reader.
Current PR is a deletion vector implementation using that reader, cause deletion vectors for Iceberg are in puffin files.
Cache is only in current PR.

@ianton-ru

Copy link
Copy Markdown
Author

By the way I want to create PR with this changes in upstream too, but it is possible only after accept of puffin reader.
Other option is to close 109990 and create a new one with all together, but I think first way has more chances.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants