GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [iceberg] Read deletion vectors in follow mode by swanandx · Pull Request #7590 · feldera/feldera

Conversation

@swanandx

@swanandx swanandx commented Oct 22, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Follow mode now reads Iceberg V3 deletion vectors (DVs), so merge-on-read DELETE, UPDATE and MERGE stream as changes. Before, the connector stopped with an error.

for each snapshot, pair every DV with its data file:
  added file         → insert its rows, minus its DV
  removed file       → delete its rows, minus its old DV
  kept file, new DV  → delete only the rows in (new DV − old DV)
  V2 delete file     → error

Writers before Iceberg 1.10 leave the DV of a removed file in the table. The connector applies that DV to the removed file, as the Java reader does, and ignores it after the file is gone.

Cost: for a new DV, the connector reads the whole data file once and keeps only the newly deleted rows. A copy-on-write change reads two files: the old file and its rewrite. On MinIO, a DELETE of 1% of 10M rows in 100 files took 4.5 s and 113 MB with DVs, and 19.6 s and 2.0 GB with copy-on-write. The connector keeps a small summary of each delete manifest that it read, so a snapshot reads only new delete manifests and those that name a removed file. When a snapshot drops a leftover DV, the connector reads the data manifests old enough to hold its data file, to make sure that the file is gone.

Manifest and deletion vector reads now retry like data-file reads, and each retry marks the connector unhealthy. A read that a retry cannot fix, such as a read of a missing file in follow mode, stops at once. On a table on the local disk, an invalid-data error still retries, because iceberg-rust's local reader reports a temporary I/O error the same way.

Linked Issue

Part of #6165.

Describe Manual Test Plan

A Spark fixture writes a partitioned V3 table, whose partition spec changes after the first insert, with merge-on-read DELETE, UPDATE and MERGE, a copy-on-write DELETE and a compaction. Each data file has many row groups. A second fixture writes with Iceberg 1.9.2 and then 1.10.0, so the table has a leftover DV. It also writes a V2 table with a position delete file. The e2e tests check the final rows and the number of rows read. If any branch above breaks, a test fails. A third fixture writes every type that Spark can write, then deletes and updates rows. That test checks that follow mode gives the same rows as a snapshot read of the last snapshot. Spark cannot write time or fixed, and a unit test runs the row filter on every type family.

Checklist

  • Unit tests added/updated
  • Integration tests added/updated
  • Documentation updated
  • Changelog updated

Breaking Changes?

Mark if you think the answer is yes for any of these components:

  • OpenAPI / REST HTTP API / feldera-types / manager (What is a breaking change?)
  • Feldera SQL (Syntax, Semantics)
  • feldera-sqllib (incl. dependencies fxp, etc.) (What is a breaking change?)
  • Python SDK (What is a breaking change?)
  • fda (CLI arguments)
  • Adapters (including configuration)
  • Storage Format / Checkpoints
  • Others (specify)

Describe Incompatible Changes

Follow mode now stops with an error when a snapshot deletes rows and the table still has a V2 delete file anywhere. Before, it went on, and emitted rows that were already deleted whenever that delete file applied to a removed file.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/adapters/src/test/iceberg.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

What I ran

Check Result
cargo test --locked -p feldera-iceberg deletion_vector ✅ 2/2 pass
pre-commit run --files <changed files> (clippy, fmt, machete, crypto deps) ✅ pass
2 extra QA unit tests (diff below) ✅ pass
e2e iceberg-tests-follow (needs a REST catalog, object storage and Spark) not run

missing (medium): unit coverage. The happy path of pair_deletion_vectors (replaced DV, added file with its DV, removed file with its DV) and select_positions are tested only by the Spark e2e test. That test is the one most likely to flake (see the inline comment). Suggested tests, which pass locally:

diff
@@ -3723,6 +3723,53 @@ mod deletion_vector_tests {
         })
         .contains("two deletion vectors"));
     }
+
+    #[test]
+    fn pairs_deletion_vectors_with_their_data_files() {
+        let paired = pair_deletion_vectors(
+            7,
+            SnapshotFiles {
+                added: vec![data_file("new.parquet")],
+                removed: vec![data_file("old.parquet")],
+                added_dvs: vec![
+                    dv("n.puffin", "new.parquet"),
+                    dv("k2.puffin", "kept.parquet"),
+                ],
+                removed_dvs: vec![
+                    dv("o.puffin", "old.parquet"),
+                    dv("k1.puffin", "kept.parquet"),
+                ],
+            },
+        )
+        .unwrap();
+        assert_eq!(paired.added.len(), 1);
+        assert!(paired.added[0].deletion_vector.is_some());
+        assert_eq!(paired.removed.len(), 1);
+        assert_eq!(
+            paired.removed[0].deletion_vector.as_ref().unwrap().file_path,
+            "o.puffin"
+        );
+        assert_eq!(paired.new_deletes.len(), 1);
+        let nd = &paired.new_deletes[0];
+        assert_eq!(nd.data_file_path, "kept.parquet");
+        assert_eq!(nd.replaced.as_ref().unwrap().data_file.file_path(), "k1.puffin");
+    }
+
+    #[test]
+    fn select_positions_keeps_only_selected_rows() {
+        use datafusion::arrow::array::{Int64Array, StringArray, ArrayRef};
+        let batch = RecordBatch::try_from_iter([
+            ("v", Arc::new(StringArray::from(vec!["a", "b", "c", "d"])) as ArrayRef),
+            ("_file", Arc::new(StringArray::from(vec!["f"; 4])) as ArrayRef),
+            ("_pos", Arc::new(Int64Array::from(vec![10, 11, 12, 13])) as ArrayRef),
+        ])
+        .unwrap();
+        let selected = HashMap::from([("f".to_string(), Arc::new(RoaringTreemap::from_iter([11u64, 13])))]);
+        let out = select_positions(batch, &selected).unwrap();
+        assert_eq!(out.num_columns(), 1);
+        let v = out.column(0).as_string::<i32>();
+        assert_eq!(v.iter().flatten().collect::<Vec<_>>(), ["b", "d"]);
+    }
 }
 
 #[cfg(test)]

Uncovered cases (medium):

  • A followed snapshot that adds or removes a V2 delete file (the error path in snapshot_files). Only the "live V2 file at check time" path has a test.
  • The error for equality delete files.
  • A data file that is not Parquet behind a new DV: new_deletes_read hardcodes DataFileFormat::Parquet, so such a file fails with an unclear reader error.

Gate: SQL type coverage (medium). The new _file/_pos projection and filter path runs only on BIGINT/STRING/TIMESTAMP (S3TablesTestStruct). filter_record_batch is type-generic, so the risk is low. Still, one MoR DELETE on a table with the full type set (including nested types) would satisfy the connector type-coverage rule.

The other gates are fine: docs are updated, the new deps are MIT/Apache, there is no unsafe, and the commit message is good.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/connectors/src/test/iceberg.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

What I ran

Check Result
cargo test --locked -p feldera-iceberg deletion_vector ✅ 3/3
New local select_positions tests (plain and run-end-encoded _file, unknown file, empty batch) ✅ 4/4 (diff inline)
cargo check --locked -p dbsp_adapters --features iceberg-tests-follow --tests ✅
pre-commit run --files <changed> ✅
e2e iceberg_rest_v3_* / iceberg_rest_follow_rejects_v2_delete_files ⚠️ not run: they need the REST catalog and object store. Only the Main CI check has run so far.

Gates

  • SQL type coverage (medium): the new selected-positions path (projecting _file/_pos, then filter_record_batch) only runs on id BIGINT, name STRING, created_at TIMESTAMP. Under the connector rule, this needs a merge-on-read DELETE against a table with the full IcebergTestStruct type set (decimal, binary, nested struct/array/map, nullable columns).
  • Flaky tests (medium): see the inline comment on the Spark fixture's network downloads.

Missing test cases (medium)

  1. new_deletes_read error paths: a new DV that isn't a superset of the old one, and a record_count that doesn't match the decoded positions.
  2. Resuming from a checkpoint in the middle of a run of DV snapshots, so no rows are double-deleted after restart.
  3. One snapshot that adds both a data file and its DV, with a row-count check (FileRead::whole with a DV on the insert side).
  4. The error path for an equality delete file.

Logic, docs, licences (roaring and crc32fast are MIT/Apache-2.0) and the breaking-change note all look good. The fixes I'd ask for are the retry pitfall (inline) and the type-coverage gate.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from d368867 to 864fe23 Compare October 22, 2026 10:46
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

What I ran

Check Result
cargo test --locked -p feldera-iceberg deletion_vector_tests (5 tests) ✅ pass
pre-commit run --files <changed> (clippy, fmt, machete, crypto deps, docs) ✅ pass
iceberg_rest_v3_* / iceberg_rest_follow_rejects_v2_delete_files e2e ⏭️ not run (needs a REST catalog, object store and Spark)

Gates: tests ✅, docs ✅, breaking change described in the PR and docs ✅. New deps roaring and crc32fast are both MIT/Apache-2.0 and were already in the lockfile ✅. Flakiness: the Spark fixture retries Maven downloads 3× and uses a separate namespace per run, so this looks OK. The e2e test schema has only 3 columns. I accept that here: the new code path only filters batches that the existing reader produces, and select_positions has a unit test over every Arrow type family.

Test cases the PR does not cover (medium):

  1. A DV on a data file that has several row groups. All fixture files hold about 25 rows, so _pos is never checked past the first row group (iceberg-rust gets it from the Parquet RowNumber, so the risk is low).
  2. DVs across partition-spec evolution: the selected read takes the DV's partition_spec_id as the data file's.
  3. A V3 table that was upgraded from V2 and still has live V2 position deletes, which is the main real-world case for the new error.
  4. A snapshot whose DV deletes no new rows (positions.is_empty() → None).
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 864fe23 to 39c2047 Compare October 22, 2026 11:21
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

What I ran

Check Result
cargo test --locked -p feldera-iceberg deletion_vector_tests ✅ 5/5 pass
cargo check --tests -p dbsp_adapters --features iceberg-tests-follow ✅ compiles. I did not run the Spark/REST e2e tests (they need the catalog and object store)
pre-commit run --files <changed> ✅ all pass
Extra QA unit tests (diff below) ✅ pass

The pairing logic, spec reasoning, docs and breaking-change note look right. Licenses are fine: roaring and crc32fast are both MIT/Apache.

  • pitfall (medium, flakiness): Every CI run of spark_tables() downloads PySpark from PyPI and the Iceberg Spark/GCP bundles from Maven Central. The 3× retry helps, but red CI will still depend on two external registries. Consider baking the jars and wheel into the CI image, or caching them under ~/.ivy2 and the uv cache.
  • missing (medium): Nothing tests the checked_for_v2_deletes caching. Untested case: a V2 table where the first snapshot that removes files has no live V2 deletes, followed by a later snapshot with a removal. Also see the inline comment on new_deletes_read.
QA tests I added locally (pass)
+    #[test]
+    fn qa_rejects_trailing_bytes_after_bitmap() {
+        let mut body = vec![0xD1, 0xD3, 0x39, 0x64];
+        RoaringTreemap::from_iter([1, 2]).serialize_into(&mut body).unwrap();
+        body.push(0);
+        let mut blob = (body.len() as u32).to_be_bytes().to_vec();
+        blob.extend_from_slice(&body);
+        blob.extend_from_slice(&crc32fast::hash(&body).to_be_bytes());
+        let e = decode_deletion_vector(&blob).unwrap_err().to_string();
+        assert!(e.contains("follow the bitmap"), "{e}");
+    }
+
+    #[test]
+    fn qa_pairs_plain_files_and_first_dv() {
+        let paired = pair_deletion_vectors(1, SnapshotFiles {
+            added: vec![data_file("a.parquet")],
+            removed: vec![data_file("r.parquet")],
+            added_dvs: vec![dv("x.puffin", "kept.parquet")],
+            removed_dvs: vec![],
+        }).unwrap();
+        assert!(paired.added[0].deletion_vector.is_none());
+        assert!(paired.removed[0].deletion_vector.is_none());
+        assert!(paired.new_deletes[0].replaced.is_none());
+        pair_deletion_vectors(2, SnapshotFiles {
+            removed: vec![data_file("f.parquet")],
+            removed_dvs: vec![dv("x.puffin", "f.parquet")],
+            ..Default::default()
+        }).unwrap();
+    }

No hard gate fails. LGTM once the new_deletes_read error paths have tests.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 39c2047 to 9ed66a3 Compare October 22, 2026 11:44
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Oct 22, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

What I ran

Check Result
cargo test --locked -p feldera-iceberg deletion_vector_tests ✅ 6/6 pass
cargo check --locked -p dbsp_adapters --features iceberg-tests-follow --tests ✅ compiles
e2e iceberg_rest_v3_* / iceberg_rest_follow_rejects_v2_delete_files ⚠️ not run here (needs a REST catalog, object store and Spark). CI runs them through iceberg-tests-follow.

Gates: tests, docs, breaking-change notice, licences (roaring, crc32fast are MIT/Apache-2.0) and flakiness all look fine. The fixture uses a per-run namespace, and its Maven retry with sleep has a good reason. SQL type coverage: the e2e schema has only 3 columns, but the new path only filters rows, and select_positions gets a unit test on every type family, so I accept that.

Missing tests (medium):

  1. new_deletes_read with replaced: None (a file's first DV). Only the e2e covers it.
  2. A transient object-store error during DV or HEAD reads. See the inline comment: these reads are not retried.
  3. A V2 delete file removed by a followed snapshot (the snapshot_files error path). The V2 e2e reaches only find_v2_delete_file.

Pitfall (medium): a writer that removes a data file but keeps its DV breaks the spec. The connector then silently emits deletes for rows it never inserted (negative weights downstream), and nothing fails. The docs call this out. Nothing cheap can detect it, so please describe this symptom (negative weights after a copy-on-write change) in the docs, so users can diagnose it.

Otherwise LGTM once the retry question is resolved.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 9ed66a3 to 8eab915 Compare October 22, 2026 12:07
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 22, 2026
View reviewed changes
Comment thread crates/adapters/src/test/iceberg.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Ran

Step Result
cargo test --locked -p feldera-iceberg (all 31, including 7 new DV tests) ✅ pass
cargo check --locked -p dbsp_adapters --tests --features iceberg-tests-follow ✅ pass
pre-commit run --files <changed> ✅ pass
e2e iceberg_rest_v3_* / iceberg_rest_follow_rejects_v2_delete_files ⚠️ not run: they need the REST catalog, object storage and Spark. CI was still pending.

Gates

  • missing (medium), SQL type coverage: the e2e table has only id BIGINT, name STRING, created_at TIMESTAMP_NTZ. The unit test runs select_positions on every Arrow type family, which supports the argument that type handling is unchanged. But the DV path is new: it adds _file/_pos to the projection, reads without a manifest record_count, and fills partition constants from the DV's partition. Spark can write the wide schema the other follow tests use, so please give v3_dv those columns. That would meet the gate outright.
  • Tests, docs, breaking-change note, commit message, dependency licences (roaring, crc32fast: MIT/Apache) and unsafe (none): OK.

Missing coverage (medium): new_deletes_read is never unit-tested with replaced: None, which is the first DV on a file and the most common case. Only the e2e test covers it. I wrote this test and it passes:

-            replaced: Some(write_dv("old.puffin", old, old.len() as u64)),
+            replaced: (!old.is_empty()).then(|| write_dv("old.puffin", old, old.len() as u64)),
...
+    #[tokio::test]
+    async fn new_deletes_read_without_old_vector_selects_all_positions() {
+        let selected = newly_deleted(&[], &[0, 2, 1 << 33], 3).await.unwrap();
+        assert_eq!(selected, Some(vec![0, 2, 1 << 33]));
+        assert_eq!(newly_deleted(&[], &[], 0).await.unwrap(), None);
+    }

Other cases with no test:

  • A snapshot that adds a V2 delete file to a V3 table after checked_for_v2_deletes is set.
  • A connector restart in the middle of a run of DV snapshots: a checkpoint resume followed by a DV replacement.
  • An equality-delete snapshot in a V3 table, which should hit the error path.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 8eab915 to 870511b Compare October 22, 2026 12:51
@swanandx
swanandx marked this pull request as draft October 22, 2026 13:03
@swanandx
swanandx temporarily deployed to ci October 22, 2026 13:21 — with GitHub Actions Inactive
@swanandx
swanandx marked this pull request as ready for review October 24, 2026 07:17
@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 870511b to 61cbc61 Compare October 24, 2026 07:17
@swanandx
swanandx marked this pull request as draft October 24, 2026 07:21
@mythical-fred-oss

mythical-fred-oss Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

LGTM. I traced the DV pairing (added / removed / kept file with a replaced DV) against the spec and found no correctness bugs. Docs are updated, the commit message is good, and the new deps (crc32fast, plus the existing workspace dep roaring) are MIT/Apache.

What I ran

Command Result
cargo test --locked -p feldera-iceberg deletion_vector_tests 7/7 pass
cargo test --locked -p feldera-iceberg 31/31 pass
cargo check --locked -p dbsp_adapters --tests --features iceberg-tests-follow clean
e2e iceberg_rest_v3_* / iceberg_rest_follow_rejects_v2_delete_files not run: needs the REST catalog, object storage and Spark

pitfall (medium): the e2e fixture depends on the network. create_v3_tables.py downloads PySpark (via uv) and the Iceberg Spark and bundle jars from Maven Central at test time. The 3× retry helps, but a Maven outage will still fail CI. Consider pre-fetching the jars into the CI image or caching the Ivy directory.

Cases the PR's tests don't cover (none of them block):

  • An equality delete file only has a unit test (error text); no e2e test rejects one.
  • A data file added in the same snapshot as its DV only has a unit test of the pairing logic; the e2e test doesn't guarantee one.
  • A writer that leaves a dangling DV when it removes a data file would make the connector delete those rows twice. The docs state the spec requirement, and nothing detects it at runtime. That is acceptable, but worth knowing if users run non-Java writers.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 61cbc61 to 466a6e0 Compare October 24, 2026 07:40
@swanandx
swanandx changed the base branch from main to iceberg-bump-4dd8c94 November 4, 2026 14:50
@mythical-fred-oss

mythical-fred-oss Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Review: LGTM. I found no correctness bugs, and every feldera-pr-checks gate passes: tests, docs, commit message, deps (roaring and crc32fast are MIT/Apache) and error messages that tell the user what to do.

What I ran

Check Result
cargo test --locked -p feldera-iceberg deletion_vector 5/5 pass
pre-commit run --files <changed files> (clippy, fmt, machete, ruff-format, docs) pass
Spark e2e (rest_follow_v3_*, rest_follow_rejects_v2_*) not run (needs S3/REST catalog); CI image has JDK 21 and uv

Cases I checked by reading the code: added file + DV, removed file + old DV, kept file with first / replacement DV, DV that shrinks (rejected), DV removed without replacement (rejected), CoW rewrite of a file with a DV, compaction skipped, V2 file left over from before the start snapshot (caught on the first delete). Type coverage is fine: the new path only filters batches, and select_positions_keeps_selected_rows_of_any_type covers every type family.

Missing tests (medium):

  1. Restart from a checkpoint in the middle of DV snapshots. No test resumes a follow across DELETE/UPDATE snapshots.
  2. Manifest or DV read retries, and the connector going unhealthy and then healthy again. The new retry() has no test.
  3. The equality-delete error and the V2/DV classification have no test. Here is one I wrote and ran (passes):
+    #[test]
+    fn classifies_delete_files() {
+        let file = |content, format| {
+            DataFileBuilder::default().file_path("d".to_string()).file_format(format)
+                .content(content).record_count(1).partition(Struct::empty())
+                .file_size_in_bytes(1).build().unwrap()
+        };
+        let pos_v2 = file(DataContentType::PositionDeletes, DataFileFormat::Parquet);
+        let eq = file(DataContentType::EqualityDeletes, DataFileFormat::Parquet);
+        let dv = file(DataContentType::PositionDeletes, DataFileFormat::Puffin);
+        assert!(is_deletion_vector(&dv));
+        assert!(!is_deletion_vector(&pos_v2) && !is_deletion_vector(&eq));
+        assert!(v2_delete_file_error(&eq).to_string().contains("equality delete file ('d')"));
+        assert!(v2_delete_file_error(&pos_v2).to_string().contains("V2 position delete file ('d')"));
+    }

Pitfall (medium, flakiness): at test time, the e2e fixture downloads pyspark from PyPI and the Iceberg jars from Maven Central. The 3× retry helps, but the jobs still depend on the network. Consider caching the uv and ivy directories in test-adapters.yml, or baking the jars into the CI image.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 01720d9 to 3d00f4a Compare November 4, 2026 15:25
@ryzhyk

ryzhyk commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

I asked the SI whether the previously found issues are fixed.

Lines 2 and 4 below should be easy fixes.
Line 1 - much harder, but please do evaluate claude's suggestion above: "- Fix: a new-DV read yields few rows, so collect each file's rows before queuing them; a failure then leaves nothing partial. The general fix is to record which files are fully pushed and retry only the rest."

┌───────────────────────┬─────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Point in your comment │ Status  │                                                           Evidence                                                           │
├───────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ a. Replays become     │ Not     │ The only change in IcebergInputEndpointInner::push_changed_files is where it reports health. It still pushes                 │
│ double deletes        │ fixed   │ QueueEntry::Rollback and re-reads every file, and IcebergInputEndpointInner::drain_batch_stream is unchanged. I compared the │
│                       │         │  code; I did not run it.                                                                                                     │
├───────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Errors that can never │ Not     │ input::is_permanent_read_error still matches only FeatureUnsupported, and IcebergReaderConfig::max_retries() still defaults  │
│  succeed are retried  │ fixed   │ to u32::MAX.                                                                                                                 │
│ forever               │         │                                                                                                                              │
├───────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ b. Never reports a    │ Fixed   │ The new IcebergInputEndpointInner::retry marks the connector unhealthy on every failed attempt. push_changed_files now does  │
│ problem               │         │ too; before, it did only when it gave up. The follow loop marks the connector healthy once a snapshot's changes are read.    │
├───────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ b. Never gives up;    │ Not     │ Errors are still turned into strings before the check. I tested a missing deletion vector with default settings: after 8 s   │
│ discards the error    │ fixed   │ it was still retrying (4 attempts, 4 unhealthy reports). The error DataInvalid => Failed to open file … No such file or      │
│ kind                  │         │ directory is not treated as permanent.                                                                                       │
├───────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ c. Manifest reads     │ Fixed   │ read_manifest_list and read_manifest now go through retry. Manifest, deletion-vector, file-size and data-file reads now      │
│ never retried         │         │ follow one retry policy.                                                                                                     │
└───────────────────────┴─────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-bump-4dd8c94 branch from 75d0c64 to 9291b9f Compare November 4, 2026 17:30
@swanandx
swanandx force-pushed the iceberg-v3-follow branch 2 times, most recently from 48f1c8d to 2e3a140 Compare November 4, 2026 17:54
@swanandx

swanandx commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

@ryzhyk for 1, I think based on what i evaluated, it should be follow-up instead of making this big PR even bigger

from my friend:
by itself it does not stop repeated rows, and a full fix costs a lot of memory

A retry reads all files of the snapshot again, also the files that it already
queued. A full fix must remember which files it queued and retry only the
others. It must also hold each file in memory until it reads the file to the
end. That is cheap for a new deletion vector, because it keeps only the deleted
rows. But a removed file can be large, and holding it costs as much memory as
the file. Inserts and copy-on-write deletes on main already repeat rows the same
way, so the follow-up will fix all of them, with a test that makes a read fail.

for 2 i initially thought let's not categorize errors and it is safe to keep retrying if we're marking connector unhealthy. but now it is fixed, same for 4, fixed it!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx

swanandx commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

if u don't mind bigger PR to review, i can add 1 in this PR as well :P

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ryzhyk

ryzhyk commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

A separate PR is fine by me.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ryzhyk

ryzhyk commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Since you asked for a detailed review, I've got one for you.

I'd hold this PR: the fresh review found a new high-severity bug, I reproduced it through the connector, and a second finding comes with it. The two fixes you asked for landed (a missing file now stops at once), but the new check has a flaw of its own.

Your table (comment 6636088929), re-checked at 2e3a140c9

The only code change since my last review: input::is_permanent_read_error now also matches "datainvalid" and "notfound". The author also added a test and one docs sentence.

┌───────────────────────┬─────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│         Point         │       Status        │                                                     Evidence                                                     │
├───────────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ a. Replays become     │ Not fixed; the      │ His main argument is right: on main, a retry already re-queues inserts and copy-on-write deletes; the            │
│ double deletes        │ author wants a      │ QueueEntry::Rollback doc comment says so. His cost argument is wrong: a per-file count of rows already queued    │
│                       │ follow-up           │ would let a retry skip them without holding any file in memory (my inference; I haven't built it).               │
├───────────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Errors that can never │                     │ A missing deletion vector stops after 1 attempt with 1 unhealthy report, on the local filesystem, opendal's      │
│  succeed are retried  │ Fixed               │ filesystem and S3 (MinIO). I ran all three.                                                                      │
│ forever               │                     │                                                                                                                  │
├───────────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ b. Never reports a    │ Fixed (unchanged)   │                                                                                                                  │
│ problem               │                     │                                                                                                                  │
├───────────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ b. Never gives up;    │                     │                                                                                                                  │
│ discards the error    │ Partly fixed        │ It gives up now, but it still decides by matching words anywhere in the message, file path included (finding 3). │
│ kind                  │                     │                                                                                                                  │
├───────────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ c. Manifest reads     │ Fixed (unchanged)   │                                                                                                                  │
│ never retried         │                     │                                                                                                                  │
└───────────────────────┴─────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

New findings

1. High: deletion vectors left behind by Iceberg Java 1.9 or earlier cause double deletes.
- Cause: before 1.10.0, Iceberg Java left a data file's deletion vector in place when a copy-on-write change removed the file. The spec rule ("When removing a data file, writers must also remove any deletion vector that applies to that data file") and Java's removeDanglingDeletesFor both arrived in 1.10.0. I checked the apache/iceberg 1.9.0, 1.9.2 and 1.10.0 tags.
- Effect: input::pair_deletion_vectors finds no removed vector for the file (/Users/leonid/projects/feldera.pr7590/crates/iceberg/src/input.rs:669). The connector then deletes every row of the file, including rows the vector had already deleted.
- Reproduced: I ran the PR's own e2e test on its own fixture, written by Spark 3.5.7 with Iceberg 1.9.2 instead of 1.10.0. The connector emits weight −1 for ids 50, 51, 52, 60, 61, 62, 70, 71 and 72. The read-count check still passes (104 = 104); only the row comparison catches it.
- Keyed tables: a delete removes the key's current value (per add_input_map's documented semantics). So the live rows u52, u62 and u72 would disappear from Feldera. This is inference; I didn't run a keyed table.

2. Medium: a valid later commit stops follow mode with a fatal error that blames the writer.
- Cause: Java drops a leftover vector (dropDeleteFilesOlderThan, present in both 1.9 and 1.10) once every live data file is newer than it. So an ordinary later DELETE removes the vector, and input::pair_deletion_vectors treats that as a spec violation (/Users/leonid/projects/feldera.pr7590/crates/iceberg/src/input.rs:692).
- Reproduced on the REST catalog:
  - Iceberg 1.9.2 runs a merge-on-read DELETE, then a copy-on-write DELETE.
  - Iceberg 1.10.0 then appends and runs a copy-on-write DELETE of another row.
  - The connector stops with: FATAL error … removes the deletion vector of data file '…' without replacing it or removing the file; the table breaks the Iceberg spec … Fix the writer and rewrite the table …
- Why the message misleads: the 1.10 writer is following the spec, and the writer that left the vector behind was following the spec of its time. Telling the user to fix the writer doesn't help.

An independent reviewer that I ran in parallel found 1 and 2; I reproduced both myself.

3. Medium-low: the new error check matches words inside file paths.
- input::is_permanent_read_error (/Users/leonid/projects/feldera.pr7590/crates/iceberg/src/input.rs:383) searches the whole message, which contains the path twice.
- Reproduced: I pointed opendal's S3 client at a closed port and read three paths:
  - code=Timeout: still retrying after 30 s.
  - code=NotFound or result=DataInvalid: gave up after the first attempt (7 s).
- Partition values are user data, so a table partitioned by an error code fails on its first temporary error.
- The local-filesystem storage labels every I/O error DataInvalid, so temporary local errors are now permanent too. I found this by reading local_fs.rs; it affects only metadata_location tables.
- Fix: check iceberg::Error::kind() and the source error's kind, not the formatted text.

4. Low: docs and tests.
- There's no changelog entry, though the PR marks the V2 rejection as a breaking change.
- The README's local catalog, tabulario/iceberg-rest:1.6.0, cannot run the new V3 test: Spark fails with "Unsupported format version: v3 (supported: v2)". The image CI uses works.
- The fixture pins Iceberg 1.10.0, so the tests cannot catch findings 1 and 2.
- The fixture's comment that the reader fills created_at from the partition is wrong (/Users/leonid/projects/feldera.pr7590/crates/iceberg/src/test/create_v3_tables.py:82). Spark writes created_at into every file, and iceberg-rust uses the partition value only when the column is missing. So the partition values the PR passes for deletion-vector reads are never exercised.
- The max_retries doc still describes snapshot reads only.

Tests I ran

┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────┐
│                                                                  Test                                                                   │        Result        │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ feldera-iceberg unit tests                                                                                                              │ 32/32 pass           │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ rest_follow_v3_reads_only_changed_rows and rest_follow_rejects_v2_delete_files, against CI's catalog image                              │ both pass            │
│ (apache/iceberg-rest-fixture:1.9.1) with MinIO                                                                                          │                      │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ Same V3 test, table written by Iceberg 1.9.2                                                                                            │ fails (finding 1)    │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ Same harness, 1.9.2 writes then 1.10.0 writes                                                                                           │ fatal error (finding │
│                                                                                                                                         │  2)                  │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────┘

On GitHub, still only the main check has run on this commit.

Possible fixes for 1 and 2 (the author's choice)

- Index the live vectors. IcebergInputEndpointInner::find_v2_delete_file already reads every delete manifest once. That same scan could build a map from data-file path to live deletion vector, updated per snapshot. The connector would apply the live vector when it reads a removed file, and ignore a removed vector whose data file is no longer live.
- Cheaper partial fix for 2. Ignore an unmatched removed vector whose sequence number is below the smallest min_sequence_number among the snapshot's data manifests, which mirrors the rule Java uses to drop it. Such a vector can't apply to any live file. This is my inference; I haven't built it.
- At the least, document that the table's writers must be Iceberg Java 1.10 or later.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Base automatically changed from iceberg-bump-4dd8c94 to main November 4, 2026 22:21
@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 2e3a140 to 9350f8a Compare November 5, 2026 08:10
@swanandx

swanandx commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

@ryzhyk thanks! fixed the findings! 🫡

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 5, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Outdated
deletion_vectors: live_dvs,
v2_delete_file,
} = self
.live_deletes(table, &manifest_list, &unpaired_removed, find_v2)

@mythical-fred-oss mythical-fred-oss Bot Nov 5, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

perf, medium: unpaired_removed is non-empty for every snapshot that removes a data file which never had a DV (a plain copy-on-write DELETE/overwrite, the common case after 1.10), so each such snapshot re-reads every live delete manifest of the table. On a large merge-on-read table with many delete manifests, follow cost becomes proportional to table size per snapshot, not to the change. Cheap mitigations: skip when the snapshot summary has total-delete-files == 0, prune delete manifests by partition summary of the removed files, or cache the live-DV index across snapshots (it only changes by each snapshot's added/removed DVs).

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 5, 2026
View reviewed changes
Comment thread crates/connectors/src/test/iceberg.rs Outdated
// Delta Spark fixtures.
let mut attempt = 1;
let output = loop {
let output = std::process::Command::new("uv")

@mythical-fred-oss mythical-fred-oss Bot Nov 5, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

flaky-risk, medium: each case downloads a pyspark wheel from PyPI (uv run --with pyspark==…) and the Iceberg runtime/bundle jars from Maven Central at test time, and cases run as parallel processes sharing ~/.ivy2. That is an unmocked network dependency in the adapters CI job; the 3× retry papers over it rather than removing it. Consider pre-fetching pyspark (e.g. requirements.ci.txt) and the jars in a CI step (or giving each process its own spark.jars.ivy) so the test itself never hits the network.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

What I ran

Step Result
cargo check --locked -p feldera-iceberg ✅
cargo test --locked -p feldera-iceberg --lib (35 tests, including 9 new DV tests) ✅
pre-commit run --files <changed files> (clippy, fmt, machete, crypto deps, docs) ✅
Spark/S3 e2e tests (rest_follow_v3_*, rest_follow_rejects_v2_delete_files) not run here (they need the REST catalog, object store, JDK and Spark)

Gates: tests, docs, changelog, breaking-change note and licences (roaring, opendal and crc32fast are MIT/Apache, and opendal uses default-features = false) all pass. There is no unsafe code. SQL type coverage: the e2e schema only has BIGINT/STRING/TIMESTAMP, but the new path only filters rows, and select_positions_keeps_selected_rows_of_any_type covers that, so I accept it.

Issues (see the inline comments)

  • flaky-risk, medium: the e2e fixtures download pyspark and Maven jars when the tests run.
  • perf, medium: each copy-on-write removal re-reads every live delete manifest.
  • nit, commit: the subject ends with a period (…in follow mode.).

Gaps in QA coverage

  • missing, low: there were no tests for deletion_vector_range errors (encrypted DV, missing or negative offset) or for select_positions on an empty batch or an unknown _file. I wrote these tests locally and they pass:
    +    #[test]
    +    fn deletion_vector_range_errors() {
    +        assert_eq!(deletion_vector_range(&dv_file(Some(4), Some(10), None)).unwrap(), 4..14);
    +        assert!(deletion_vector_range(&dv_file(None, Some(10), None)).unwrap_err().to_string().contains("no content offset or size"));
    +        assert!(deletion_vector_range(&dv_file(Some(4), Some(10), Some(vec![1]))).unwrap_err().to_string().contains("encrypted"));
    +        assert!(deletion_vector_range(&dv_file(Some(-1), Some(10), None)).is_err());
    +    }
    +    #[test]
    +    fn select_positions_empty_and_unknown_file() { /* 0-row batch drops _file/_pos; unknown _file → "no selected positions" */ }
  • missing, medium: no test covers the following:
    • A restart from a checkpoint between snapshots that carry DVs. This resets checked_for_v2_deletes and re-pairs the DVs.
    • A transient failure while reading a DV or manifest, followed by recovery, end to end.
    • A snapshot that removes a DV without replacing it while its data file is still live. The pairing unit test covers this, but nothing tests the error through snapshot_delta and live_data_files.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 9350f8a to 9d60f1a Compare November 5, 2026 13:10
ryzhyk
ryzhyk approved these changes Nov 5, 2026
View reviewed changes

@ryzhyk ryzhyk left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

No more bugs found. There's a performance issue for you to assess. Sounds like it's time to merge this.


New findings

  1. Medium (cost, not correctness): the leftover lookup rereads every delete manifest of the table.
    • When: every snapshot that removes a data file without its deletion vector triggers it. That covers copy-on-write DML, INSERT OVERWRITE and partition deletes on any table that has deletion vectors.
    • Wasted on new writers: this includes tables written only by Iceberg 1.10+, where the lookup can never find anything, because 1.10 removes a file's vector in the same commit.
    • Where: IcebergInputEndpointInner::live_deletes (input.rs:2058), called from snapshot_delta (:1932-1947). iceberg-rust's ManifestReader::read has no cache.
    • Observed: in the reviewer's trace, which I checked, a table with 39 delete manifests and 4 followed copy-on-write deletes fetched each delete manifest 5 times: once for the initial read and once per snapshot.
    • Cheap fix: Java writes its version into every snapshot summary; our test table shows iceberg-version: "Apache Iceberg 1.10.0 (commit …)". Skip the lookup for Java 1.10+ writers. Pruning manifests by partition, or caching them per poll, would also help.
    • Wrong comment: the comment in IcebergInputEndpointInner::live_data_files (:2113-2116) says this check "reads no manifest". In the reviewer's table history, a 1.9.2 partition delete and a 1.10 INSERT OVERWRITE each dropped a leftover vector while an older data file stayed in the table, so the method did read manifests. The code handles this correctly; only the comment is wrong.
  2. Low (test gap, confirmed by mutation): nothing tests whether a vector's data file is still in the table.
    • The check: when a snapshot removes a vector on its own, snapshot_delta asks live_data_files whether the vector's data file is still in the table. If it is, the connector stops with a spec-violation error.
    • Mutation: I made live_data_files report every data file as gone. All three Spark e2e tests still passed. No unit test reaches snapshot_delta; that code isn't even built into the unit-test binary.
    • Risk: a regression here would silently ignore a vector removed from a file still in the table, so rows the table now has again would stay deleted in Feldera.
    • Fix: move the check into a plain function with unit tests, or add a 1.10 INSERT OVERWRITE to the test table that drops a leftover vector while an older file stays.
    • Minor: both deletion-vector e2e tests start before the first vector, so the initial table read never meets one.
  3. Low (observed): on local tables, a temporary I/O error now stops the connector at once.
    • Cause: local tables are read through FileIO::new_with_fs(). That storage reports every I/O error as DataInvalid, keeping only the message text, and input::is_permanent_read_error treats DataInvalid as permanent.
    • Observed: with all file descriptors in use, a vector read gave up after one attempt in 1 ms, with Too many open files (os error 24).
    • Before this PR: these errors were retried, in snapshot mode too.
    • Fix: in the iceberg-rust fork, keep the original I/O error attached and classify by its kind.
  4. Low (inferred, not run): retries can't recover from an error that needs a fresh table.
    • What changed: manifest and vector reads now retry; before, one failure was fatal. The retries reuse the table opened for the current poll, and max_retries has no limit by default.
    • Scenario: a REST catalog hands out short-lived S3 credentials, and a catch-up runs past their expiry. The connector would then retry forever instead of failing. Data-file reads already behaved this way.
    • Suggestion: handle this in the retry follow-up PR (item a).
  5. Low (inferred, not run): stopping at once on a missing file is new for most reads. It could misfire where S3 replication or Multi-Region Access Points briefly return "not found" for objects that exist. That is a rare setup.
  6. Low (observed): the changelog entries land in a released section. After merging into current main, the three entries end up under ## v0.377.0 instead of ## Unreleased.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@swanandx
swanandx force-pushed the iceberg-v3-follow branch from 9d60f1a to a614c9a Compare November 5, 2026 20:28
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 5, 2026
View reviewed changes
Comment thread crates/connectors/src/test/iceberg.rs
.output()
.unwrap_or_else(|e| panic!("failed to run '{script}' (is `uv` on PATH?): {e}"))
};
// Jar downloads from Maven Central fail now and then; retry like the

@mythical-fred-oss mythical-fred-oss Bot Nov 5, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

pitfall (medium): flakiness. Every CI run of these four tests resolves PyPI (pyspark 4.0.1 and 3.5.7) and Maven Central (Spark 4/3.5 Iceberg runtimes plus AWS bundles), and only three attempts with 5 s and 10 s back-off protect it. A Maven or PyPI outage longer than about 15 s turns test-adapters red for reasons the PR does not control. Could CI cache the Ivy dir (~/.ivy2) and the uv cache (actions/cache keyed on the script hash)? Or could the fixture tables be pre-built in a pinned image, as iceberg-rest already is?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Nov 5, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Review: Iceberg follow mode reads deletion vectors

What I ran

Step Result
cargo check --locked -p feldera-iceberg --tests ✅
cargo test --locked -p feldera-iceberg --lib ✅ 39/39, including the new deletion_vector_tests and the permanent-error tests
Spark/MinIO e2e (rest_follow_v3_*, rest_follow_rejects_v2_delete_files) not run: needs Docker, so I could not run them here

Gates: tests ✅, docs and changelog ✅, breaking change declared ✅, new deps roaring and opendal (Apache-2.0, default-features = false) and crc32fast (dev, MIT/Apache) ✅, no unsafe ✅. SQL type coverage: Spark cannot write TIME, FIXED, unsigned or interval types, and the row-filter unit test covers every type family, so this is acceptable.

Correctness checks: I checked that a transient S3 or Parquet error does not become permanent. iceberg-rust maps ParquetError and OpenDAL errors to Unexpected, not DataInvalid, so the new is_permanent_read_error keeps retrying object-store failures as before.

Missing test (medium): no test covers live_deletes reusing a cached DeleteManifestSummary across snapshots: skipping manifests with no hash match, re-reading one after a hash collision, and pruning summaries of manifests that are gone. A bug in this cache would silently miss a leftover DV, and the deleted rows would be emitted twice.

Flakiness (medium): see the inline comment about the Maven/PyPI downloads in the e2e fixture.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
[iceberg] Read deletion vectors in follow mode.
859afe7
Follow mode now reads V3 deletion vectors, so merge-on-read DELETE,
UPDATE and MERGE stream as changes. For a new vector it reads the data
file and keeps only the newly deleted rows. It also applies the vectors
that writers before Iceberg 1.10 leave in the table. V2 delete files
still stop the connector with an error. A summary of each delete
manifest is kept, so a snapshot reads only new delete manifests.

Manifest and deletion vector reads now retry like data-file reads, and
each retry marks the connector unhealthy. A read that a retry cannot
fix, such as a read of a missing file, stops at once.

Signed-off-by: Swanand Mulay <[email protected]>
@swanandx
swanandx force-pushed the iceberg-v3-follow branch from b3aec76 to 859afe7 Compare November 5, 2026 20:55
@mythical-fred-oss

mythical-fred-oss Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review: LGTM, with one gap to note.

What I ran

Check Result
cargo check --locked -p feldera-iceberg ✅
cargo test --locked -p feldera-iceberg --lib ✅ 40/40 (includes the 12 new deletion_vector_tests)
pre-commit run --files <changed files> (clippy, fmt, machete, crypto deps, ruff-format) ✅
iceberg-tests-follow e2e (Spark, MinIO, REST catalog) ⏭️ not run, needs services

I also checked that the IcebergError(<kind>) label matched in is_permanent_read_error is the exact text that iceberg-datafusion's to_datafusion_error produces. num_parsers == 0 is rejected at startup, so the new buffer_unordered(num_parsers) calls can't hang. The new deps are fine: opendal (Apache-2.0, default-features = false) and crc32fast (MIT/Apache-2.0, dev-only).

tech-debt (medium): the new e2e tests don't run in CI. No workflow enables iceberg-tests-follow. The four new rest_follow_v3_* / rest_follow_rejects_v2_delete_files tests are the only coverage for the pieces that cross the reader boundary: _file/_pos projection over many row groups, leftover-DV pairing against a real 1.9 writer, and the V2 rejection. They also download PySpark and the Iceberg jars from PyPI and Maven Central on every run, so they can fail for reasons unrelated to the code. That's acceptable for an opt-in suite, but it shouldn't become a gating job as it is. Please follow up with a CI job that caches the jars, or run these in a nightly job.

Cases the PR's tests don't cover

  1. A pipeline restarting from a checkpoint in the middle of a run of DV snapshots. The DeleteManifests cache and checked_for_v2_deletes are in memory only, so after a restart they start empty and the code reads the manifests again. I expect this to be correct, but no test covers it.
  2. An equality delete file in a V3 table. Only the classification is unit-tested. The e2e test only covers a V2 position delete file.
  3. One snapshot that adds a DV to a kept file and also removes a different file that has a leftover DV, so the new_deletes read and the whole-file read go through the same stream.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 983cb7e Nov 6, 2026
1 check passed

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx deleted the iceberg-v3-follow branch November 6, 2026 00:32
@swanandx swanandx mentioned this pull request Nov 6, 2026
[iceberg] Skip already queued rows on a follow-mode retry #7869
Merged
4 of 12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@mythical-fred-oss mythical-fred-oss[bot] mythical-fred-oss[bot] left review comments
@ryzhyk ryzhyk ryzhyk approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@swanandx @ryzhyk
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.