Skip to content

fix: keep Iceberg residual pushdown within Spark semantics - #7156

Draft
rich7420 wants to merge 3 commits into
apache:mainfrom
rich7420:fix/7136-iceberg-residual-semantics
Draft

rich7420 wants to merge 3 commits into
apache:mainfrom
rich7420:fix/7136-iceberg-residual-semantics

Conversation

@rich7420

@rich7420 rich7420 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Closes #7136.
Closes #7137.
Part of #7138.

Rationale for this change

The native Iceberg scan pushes each task's residual into iceberg-rust, which uses it to prune row groups and pages and to filter rows during decode. The filter above the scan cannot restore rows dropped there. On main and branch-1.1, three kinds of residual drop rows that Spark returns:

These were found while reviewing #6847 and split out so the fixes can be backported.

What changes are included in this PR?

The residual serde leaves these predicates to the filter above the scan:

  • On FLOAT/DOUBLE columns: range comparisons, comparisons with 0.0, -0.0 or NaN, and FLOAT literals that do not narrow exactly. Equality, != and IN with other literals are still pushed.
  • Equality under NOT on a nullable column, unless an enclosing AND checks that column with IS NOT NULL. Spark infers that check for b = false, which it rewrites to NOT b, so that filter is still pushed. != reaches Iceberg as a separate comparison and is not affected.
  • On columns whose type changed in the schema history: everything except null checks and = outside NOT. If snapshot expiration removed older schemas, every BIGINT, DOUBLE and DECIMAL column counts as promoted.

This removes some pushdown that main has today. Files imported by add_files, migrate or snapshot, and promotions made before Iceberg 0.12, leave no schema history, so they remain a known issue under #7138. The Iceberg guide lists the excluded predicates and that known issue.

How are these changes tested?

Serde tests cover each excluded form, including under AND, OR and nested NOT, and the schema-history detection. Native tests compare with Spark for signed zeros and NaN, negated <=>, and promoted columns before and after snapshot expiration. They also assert which negated equalities are still pushed. A Rust test covers the bind-failure fallback, which the float tests no longer reach. The comparison and logical pushdown fixtures move from DOUBLE to BIGINT so their predicates are still pushed.

Local, Spark 4.1 / JDK 21: CometIcebergNativeScanSuite, CometIcebergNativeSuite and CometFuzzIcebergSuite pass. Iceberg's Spark SQL tests (Iceberg 1.8 to 1.11) and the Comet suites on the other Spark profiles have not run yet. They need the run-iceberg-tests and run-all-spark-profiles labels.

Decline FLOAT and DOUBLE range comparisons and zero or NaN literals.
Spark treats -0.0 as equal to 0.0 and sorts every NaN above all
numbers, while iceberg-rust compares by IEEE 754 total order, and its
row-group pruning trusts statistics that can omit NaN
(apache/iceberg-rust#3494).

Decline equality under NOT on nullable columns. Iceberg passes `<=>`
as `=`, so the native reader dropped NULL rows that Spark keeps for
`NOT (id <=> 1)`.

On columns whose type changed in the schema history, push only
equality outside NOT and null checks, since iceberg-rust compares
older, narrower files incorrectly (apache/iceberg-rust#3122). An
incomplete history treats every BIGINT, DOUBLE and DECIMAL column as
promoted.

Closes apache#7136. Closes apache#7137. Part of apache#7138.
@github-actions github-actions Bot added bug Something isn't working area:scan Parquet scan / data reading area:Iceberg labels Oct 22, 2026
Spark rewrites `b = false` on a nullable boolean as `b IS NOT NULL AND
NOT b`, which Iceberg passes as `not_null(b) AND not(b = true)`. Declining
equality under NOT on a nullable column left that residual unpushed, so
CometFuzzIcebergSuite found no residual for its boolean column.

A NULL value fails a NOT_NULL conjunct, and with it the whole AND, in
Spark and in the native reader alike. Treat a column checked that way as
non-nullable within the AND, so the null-safe concern no longer applies.
…cans

Extract referencedColumn so the residual converter and notNullColumns share
the bare-reference check. Share the residual predicate loop of the new native
tests, and assert that `id != 1` and `b = false` are still pushed while
`NOT (id <=> 1)` and `b IS NOT TRUE` are not.

CometFuzzIcebergSuite samples its equality literal from the data. Skip zero
as well as NaN, since floating-point comparisons with zero are no longer
pushed.

Note in the Iceberg guide that `!=` is not affected by the negated-equality
rule.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Iceberg area:scan Parquet scan / data reading bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native Iceberg scan drops NULL rows for NOT (col <=> value) Native Iceberg scan drops -0.0 rows for floating-point predicates

1 participant