Skip to content

Fix Parquet statistics pruning for predicates satisfied by NaN - #23735

Merged
rapids-bot[bot] merged 29 commits into
NVIDIA:mainfrom
mhaseeb123:nan-ordering-negation
Aug 31, 2026
Merged

Fix Parquet statistics pruning for predicates satisfied by NaN#23735
rapids-bot[bot] merged 29 commits into
NVIDIA:mainfrom
mhaseeb123:nan-ordering-negation

Conversation

@mhaseeb123

@mhaseeb123 mhaseeb123 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

This PR relaxes Parquet statistics based pruning for floating-point types to compensate for NaNs. This is necessary because Arrow and parquet-mr writers omit NaNs from min/max statistics, which could cause stats transforms for NOT(col < lit) and col != lit to incorrectly prune row groups.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@copy-pr-bot

copy-pr-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions github-actions Bot added libcudf Affects libcudf (C++/CUDA) code. Python Affects Python cuDF API. pylibcudf Issues specific to the pylibcudf package labels Aug 20, 2026
@mhaseeb123 mhaseeb123 added bug Something isn't working 3 - Ready for Review Ready for review by team non-breaking Non-breaking change labels Aug 20, 2026
The negation pushdown deliberately refuses to complement ordering
comparisons, because IEEE-754 makes every ordered comparison against a NaN
false, so NOT(a < b) is true exactly where a >= b is false. The stats
converter still complemented them one layer down: NOT(col < lit) became
col >= lit and then vmax >= lit.

For a row group holding {NaN, 1.0, 2.0} and lit = 50, the NaN row satisfies
NOT(col < 50), yet vmax >= 50 is false and the row group is pruned. cudf
files are immune because the writer drops min/max entirely when a NaN is
seen (PARQUET-1246), but Arrow writes min/max that merely exclude NaN -
pyarrow 23 yields min=1.0, max=2.0 for that chunk - so the hole is live for
Arrow-written files.

Give the converter the column data types and skip the rewrite for floating
point columns, relaxing instead. Equality is unaffected: NaN == x is false
and NaN != x is true, so those stay exact complements.

Costs pruning only for negated ordering comparisons on float columns, which
is what the existing ParquetPredicatePushdownTestAST expectation for
NOT(col0 < 100) OR IS_NULL(col0) now records.

Reported by @vuule.
The guard added for negated ordering comparisons was not sufficient. The
NOT_EQUAL leaf is unsound for the same reason and involves no negation at
all:

  col != val  -->  vmin != vmax OR vmax != val

A chunk of {NaN, val} reports min == max == val, because Arrow and
parquet-mr both skip NaN when updating min/max, and is therefore
indistinguishable from a constant-val chunk. The transform prunes exactly
that shape - but NaN != val is true, so its NaN rows do satisfy the filter
and are dropped. This also reaches NOT(col == val), which the normalizer
complements into col != val.

Reproduced on a two row group pyarrow file [NaN, 5.0 | 7.0, 8.0] filtered
by x != 5.0: the first row group is pruned and only [7.0, 8.0] comes back.

Relax the leaf for floating point columns. The reader cannot be more
precise: the Parquet Statistics struct carries null_count, distinct_count
and the min/max exactness flags, but nothing about NaN, so a NaN-free chunk
is indistinguishable from one whose NaN was skipped. Costs pruning for
col != val on float columns only.

The other leaves stay sound because NaN never satisfies them: col < v,
col > v and col == v are all false for NaN, so excluding it from min/max
cannot make them prune a matching row. The unsound cases are exactly the
predicates NaN satisfies.

parquet-mr's DoubleStatistics.updateStats behaving like Arrow here was
confirmed by Paul Mattione, widening this from Arrow-written files to
Spark-written ones as well.
Inlining can_negate_ordering() hoisted the column lookup out of the
short-circuit that protected it. extract_binary_operands() only reports a
column reference for the `col op lit` and `lit op col` forms; for anything
else it returns nullptr, so the unconditional

  _output_dtypes[binary_operands.col_ref->get_column_index()]

dereferences null for any NOT wrapping a comparison neither of whose
operands is a bare column, such as NOT((col + 1) > 5).

The read is now nested inside the `col op lit` check rather than sitting in
the condition alongside it, which is what the short-circuit was doing
before. Behaviour is otherwise unchanged.

Reproduced with the filter (col_a < 150) AND NOT((col_a + 10) > 50); the
first conjunct is what makes a column stats-usable, so the converter is
built at all. Covered by ParquetReaderTest.FilterNegationPushdown, which
segfaults without this.
@mhaseeb123
mhaseeb123 force-pushed the nan-ordering-negation branch from 4f3162e to 8d83b2f Compare August 21, 2026 17:59
Two follow-ups from Lawrence's review of NVIDIA#23580.

Call the rewrite by its name. "Negation normal form" is the standard term
in mathematical logic for an expression whose negations appear only on
atoms, reached by eliminating double negations and applying De Morgan's
laws, which is exactly what the normalizer produces. Both class docs now
say so.

Make transform_operator's mode dispatch exhaustive, so that adding a slot
to operator_transform and calling with it fails to compile rather than
silently taking the NEGATE branch. The static_assert condition mentions
`mode` deliberately: cudf builds as C++20, where a bare
static_assert(false) in a discarded if-constexpr branch is ill-formed and
fires unconditionally. Keeping the condition value-dependent defers it to
instantiation, which is what makes the check fire only for an unhandled
mode.
@mhaseeb123

mhaseeb123 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@pmattione-nvidia: #23709 also updated NOT_EQUAL statistics transforms for null handling. Please retain the floating point changes from this PR whenever you are reopening a new split-PR off it.

@mhaseeb123
mhaseeb123 marked this pull request as ready for review August 21, 2026 19:48
@mhaseeb123
mhaseeb123 requested review from a team as code owners August 21, 2026 19:48
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f7711842-2f9f-4d59-bc8a-6b61da617b86

📥 Commits

Reviewing files that changed from the base of the PR and between 8d18b6e and 76f3e8c.

📒 Files selected for processing (1)
  • cpp/src/io/parquet/stats_filter_helpers.hpp
💤 Files with no reviewable changes (1)
  • cpp/src/io/parquet/stats_filter_helpers.hpp

Included review availability: Your plan provides up to 12 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved Parquet predicate filtering for floating-point columns containing NaN values.
    • Prevented statistics-based filtering from incorrectly excluding rows for negated or not-equal conditions.
    • Improved handling of complex negated predicates and literal-left comparisons during row-group selection.
    • Ensured predicate normalization consistently supports negation and column references.
  • Tests

    • Added regression coverage for NaN handling and complex predicate pushdown.

Walkthrough

The PR passes output data types to Parquet statistics conversion, restricts unsafe floating-point predicate negation, and adds regression tests for NaN handling and negated expressions.

Changes

Parquet predicate filtering

Layer / File(s) Summary
Converter data-type wiring
cpp/src/io/parquet/stats_filter_helpers.*, cpp/src/io/parquet/experimental/page_index_filter.cu, cpp/src/io/parquet/predicate_pushdown.cpp, cpp/src/io/parquet/expression_transform_helpers.*, cpp/src/io/parquet/experimental/hybrid_scan_helpers.hpp
stats_expression_converter now accepts output data-type spans and derives the column count from them. Call sites pass the spans. Predicate normalization documentation and operator-mode validation are updated.
Floating-point negation handling
cpp/src/io/parquet/stats_filter_helpers.cpp
Ordering operators are not negated for floating-point columns. Floating-point NOT_EQUAL predicates produce always-true statistics expressions.
Predicate regression coverage
cpp/tests/io/parquet_reader_test.cpp, python/cudf/cudf/tests/input_output/test_parquet.py
Tests cover literal-left comparisons, negated arithmetic expressions, row-group retention, and NaN behavior for NOT_EQUAL and negated ordering predicates.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟠 High · up to 76f3e

The PR changes Parquet predicate pruning, but the current implementation still contains undefined behavior in the statistics conversion path, which can make filtering unpredictable or cause runtime failures. This issue should be fixed before merging.

Possibly related PRs

  • NVIDIA/cudf#23580: Extends related Parquet predicate normalization and statistics-pruning logic.

Suggested reviewers: nirandaperera, matt711, pmattione-nvidia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: correcting Parquet statistics pruning for predicates that must account for NaN values.
Description check ✅ Passed The description directly explains the floating-point NaN pruning issue and identifies the related tests and documentation updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@mhaseeb123

Copy link
Copy Markdown
Contributor Author

Thanks @igorpeshansky for such a detailed and thorough review. I have addressed your concerns in aa395d7 :)

@mhaseeb123 mhaseeb123 added 5 - Ready to Merge Testing and reviews complete, ready to merge and removed 4 - Needs Review Waiting for reviewer to review or respond labels Aug 27, 2026
@mhaseeb123 mhaseeb123 added 4 - Needs Review Waiting for reviewer to review or respond and removed 5 - Ready to Merge Testing and reviews complete, ready to merge labels Aug 27, 2026

@igorpeshansky igorpeshansky left a comment

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.

One test needs fixing.

Comment thread cpp/src/io/parquet/stats_filter_helpers.cpp Outdated
Comment thread cpp/src/io/parquet/stats_filter_helpers.cpp Outdated
Comment thread cpp/src/io/parquet/stats_filter_helpers.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_test.cpp
Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
// Signed numeric types pass RGs 1,2,3, others pass RGs 2,3. Floats keep all 4: NaN makes
// every ordered comparison false, so `NOT(col0 < 100)` is not `col0 >= 100` and gets relaxed.
// Signed integral types pass RGs 1,2,3, others pass RGs 2,3. Floats keep all 4 as they may
// hold NaNs making every ordered comparison false and get relaxed instead.

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.

Nit: I think this sentence needs at least a comma before the "and", and possibly another one before "making"…

Suggested change
// hold NaNs making every ordered comparison false and get relaxed instead.
// hold NaNs, making every ordered comparison false, and get relaxed instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 205559d.

Comment thread cpp/src/io/parquet/stats_filter_helpers.hpp Outdated
Comment thread cpp/src/io/parquet/stats_filter_helpers.cpp Outdated

@igorpeshansky igorpeshansky left a comment

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.

LGTM :shipit: modulo a couple of proposed readability and test tweaks.

Comment thread cpp/src/io/parquet/stats_filter_helpers.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
@mhaseeb123

Copy link
Copy Markdown
Contributor Author

/merge

@mhaseeb123 mhaseeb123 added 5 - Ready to Merge Testing and reviews complete, ready to merge and removed 4 - Needs Review Waiting for reviewer to review or respond labels Aug 28, 2026
@rapids-bot
rapids-bot Bot merged commit 7385730 into NVIDIA:main Aug 31, 2026
155 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in cuDF Python Aug 31, 2026
@mhaseeb123
mhaseeb123 deleted the nan-ordering-negation branch August 31, 2026 20:53
@GregoryKimball GregoryKimball moved this from Burndown to Landed in libcudf Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to Merge Testing and reviews complete, ready to merge bug Something isn't working libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change pylibcudf Issues specific to the pylibcudf package Python Affects Python cuDF API.

Projects

Status: Done
Status: Landed

Development

Successfully merging this pull request may close these issues.

6 participants