Skip to content

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

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

rich7420 wants to merge 2 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 using statistics, select rows using the page index and filter rows during decode. The filter above the scan cannot restore rows dropped there. On main and branch-1.1, three kinds of residual cause the native scan to drop rows that Spark returns:

These issues were found while reviewing #6847. This PR splits out their fixes so they can be backported and listed as fixes.

What changes are included in this PR?

The residual serde now 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 it is combined with an IS NOT NULL check on that column using AND. Spark infers that check for b = false, which it rewrites to NOT b, so that filter is still pushed.
  • On columns whose type changed in the schema history: everything except null checks and = outside NOT. If snapshot expiration has 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 predicates excluded from pushdown and that known issue.

How are these changes tested?

Serializer tests cover each excluded predicate form, including under AND, OR and nested NOT, and the schema-history detection. Native tests compare against Spark for signed zeros and NaN, negated <=>, and promoted columns before and after snapshot expiration. They also check that b = false is 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 / Scala 2.13 / JDK 21): CometIcebergNativeScanSuite, CometIcebergNativeSuite and CometFuzzIcebergSuite passed (179 passed, 2 canceled because of version assumptions). The Rust test also passed. -Pspark-3.5,delta -Pstrict-warnings test-compile passed. Other Spark profiles and the Iceberg Spark SQL suites were not run locally; run-iceberg-tests is needed.

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.
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