Skip to content

Support hive partitioned parquet files (except HybridScan) in cudf-polars - #23966

Open
mroeschke wants to merge 28 commits into
NVIDIA:mainfrom
mroeschke:cudf_polars/feat/hive_partitioning
Open

Support hive partitioned parquet files (except HybridScan) in cudf-polars#23966
mroeschke wants to merge 28 commits into
NVIDIA:mainfrom
mroeschke:cudf_polars/feat/hive_partitioning

Conversation

@mroeschke

@mroeschke mroeschke commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

Towards #17832

This PR enables reading hive partitioned parquet files in pl.scan_parquet. This PR doesn't intentionally omits support for the hybrid scan reader as a follow up.

Checklist

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

Polars parses hive keys out of file paths and hands cudf-polars the
surviving file list plus the partition values for those files. Those
values are not stored in the parquet files, so they have to be
materialized onto the rows we read.

HivePartitions holds the per-path values and knows how to turn them into
columns, either by broadcasting when every path shares a value or by
gathering with the reader's source index column. It is frozen and
hashable so it can take part in Scan's IR identity.
Thread HivePartitions through Scan so the parquet reader can produce the
hive columns polars expects. The projection polars gives us may name hive
columns, so those are removed before asking the reader for file columns
and added back afterwards from the partition values.

Rows are matched to their partition values with the reader's prepended
source index column, which is synthesized before filtering and so stays
correct when a predicate is pushed down. When every path shares the same
values the index is not read at all and the values are broadcast instead.

Predicates over hive columns are held back from the parquet filter, since
those columns do not exist in the files, and applied post-read once the
columns have been materialized.

Nothing constructs a hive-partitioned Scan yet; translation still raises.
Selecting only hive columns leaves the parquet reader with an empty
projection, and it then returns no rows at all, so neither the row count
nor a source index can come from the read.

Take the per-path row counts from the file metadata instead and repeat
each path's partition values that many times. This is only sound because
polars pushes a predicate down alongside the file columns it references,
so an empty projection always means no filter was pushed and the metadata
counts are exact.
Translate polars' hive_parts frame into HivePartitions instead of
refusing the scan. Polars has already pruned the file list against any
predicate it could evaluate on the partition keys by this point, so what
arrives is the surviving paths and the partition values for them.

Tests cover the shapes polars produces: hive columns projected alone or
mixed with file columns, predicates over hive columns, over file columns
and over both, pruning down to a single file or to none, a slice or row
index pushed into a hive-only projection, dtype overrides via
hive_schema, null partition values, and a file column shadowed by a hive
key.

These run on the in-memory executor; the streaming executors still drop
hive columns and are enabled next.
SplitScan and FusedScan each read a subset of the base scan's files, so
they need the partition values for just those files. Slice them when the
StreamingScan is built: a split always covers one file, so its values are
uniform and get broadcast, while a fused group needs the source index.

The hybrid reader builds its own projection and filter and has no notion
of columns that are absent from the file, so hive scans fall back to the
regular reader for now.

The hive scan tests now run against the full engine matrix rather than
just the in-memory executor.
libcudf clears its per-source row counts once the reader applies a
filter, so repeating each path by its row count produced a length
mismatch and the read failed outright:

    CUDF failure at repeat.cu:101: in and count must have equal size

Gather the paths with the source index instead, which the reader
synthesizes before filtering. The row counts are still used when nothing
is filtered, to avoid reading a column that is not needed.
Add the cases the earlier commits left untested: the chunked reader,
partition keys of each dtype polars can parse out of a path, and several
files under one partition so the values are uniform across more than one
path.

Four delta tests were xfailed as needing hive partitioning. They still
fail with hive scans enabled, and identically so on main:

    CUDF failure: All non-empty sources must have the same number of
    columns

These tables evolve their schema between versions, so the files have
differing column counts, which the reader cannot handle. Correct the
reasons rather than removing the entries.

Polars' own tests/unit/io/test_hive.py goes from 7 to 33 passing, and
the rest of tests/unit/io fails identically to main.
Scan is only ever constructed in translate.py, where the paths and the
partition values come from the same polars node, and reconstruct() and
__reduce__() carry both through together. Nothing could trip this
without editing that one call site, and no test reached the line.

Unlike the neighbouring cached parquet info check, which guards state
attached by a later pass and does have a test behind it.
Give _parquet_rows_per_path and _pop_source_index the Parameters and
Returns sections the rest of the module uses.
Once the budget reaches zero every later path takes zero rows, so the
loop can leave the pre-filled zeros in place and stop. This also covers
the cases that start with no budget at all, where skip_rows swallows the
whole dataset or n_rows is zero.
Keep what polars hands over rather than unpacking it into parallel name,
dtype and value tuples, and derive the properties from it on demand.
Conversion to device goes through Table.from_arrow, which also lets
broadcast reuse filling.repeat instead of building a scalar per key, so
nulls and temporal types need no special handling.

Identity needs care. The default dataclass __eq__ would compare two
frames with ==, which yields a frame rather than a bool, and
DataFrame.equals ignores the schema, so Int32 and Int64 partition values
would compare equal. Worse, node digests fall back to repr() and polars
elides the middle of a tall frame's repr, so two scans differing only in
a middle partition value would digest identically. All three dunders are
therefore driven off a cached key holding the schema and every row.
These three tests translate a query to inspect the IR, and were passing
a locally built GPUEngine rather than the shared fixture. Take the engine
fixture instead so they see each real backend, and skip the in-memory
variant, which never builds a SplitScan or FusedScan.
The row counts were asserted by calling the private helper directly.
Slicing a hive-only projection reaches it through the public API with the
same skip_rows and n_rows combinations, and checks the rows that come
back rather than just the counts, so the whole helper stays covered
including the paths where the budget starts empty.
__repr__ still unpacked the removed _key, so every repr() raised
AttributeError. __hash__ hashed the result of list.append, which is None,
so every instance hashed alike; hash the schema alongside a tuple of the
row hashes instead.

Since __repr__ now defers to the frame, which elides the middle of a tall
one, the stable node digest no longer separates partition values that
differ only in an elided row. That digest feeds explain output and actor
ids rather than result reuse, and is truncated to 32 bits anyway, while
CSE goes through __hash__ and __eq__, which do see every row. The test
that covered the digest now covers those instead, joined by cases for
column names and path order, which hash_rows alone does not distinguish.
@mroeschke mroeschke self-assigned this Sep 3, 2026
@mroeschke mroeschke added improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Sep 3, 2026
@copy-pr-bot

copy-pr-bot Bot commented Sep 3, 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 Python Affects Python cuDF API. cudf-polars Issues specific to cudf-polars labels Sep 3, 2026
Rename cudf_polars/dsl/utils/hive.py to per_path.py and HivePartitions to
PerPathValues, then use it in Scan.add_file_paths so the gather/repeat
expansion of per-path values lives in one place.
@mroeschke
mroeschke marked this pull request as ready for review September 3, 2026 21:23
@mroeschke
mroeschke requested a review from a team as a code owner September 3, 2026 21:23
@mroeschke
mroeschke requested a review from pentschev September 3, 2026 21:23
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review Change Stack

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: 49c293c6-de01-48b5-9f99-34dae7c8c91e

📥 Commits

Reviewing files that changed from the base of the PR and between 3f5225b and 79f9cb1.

📒 Files selected for processing (1)
  • python/cudf_polars/cudf_polars/testing/inject_gpu_engine.py

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


📝 Summary

Summary by CodeRabbit

  • New Features

    • Added support for reading Hive-partitioned Parquet data, including partition columns, filtering, projections, slicing, and multiple files.
    • Added partition metadata support for streaming scans, including fused and split execution.
    • Improved file-path column handling across Parquet and CSV scans, including filtered and partitioned data.
    • Added support for Hive-only projections and null or shadowed partition columns.
  • Bug Fixes

    • Improved predicate handling when partition columns cannot be pushed down to the file reader.
  • Tests

    • Expanded coverage for partitioned scans, streaming execution, file paths, and metadata.

Walkthrough

Hive-partitioned Parquet scans now preserve per-path partition metadata, defer filters on unreadable Hive columns, reconstruct partition and file-path columns, and support split and fused streaming scans. Tests cover filtering, projections, slicing, partition types, nulls, and fallback behavior.

Changes

Hive-partitioned Parquet scans

Layer / File(s) Summary
Per-path metadata and filter contracts
python/cudf_polars/cudf_polars/dsl/utils/per_path.py, python/cudf_polars/cudf_polars/dsl/to_ast.py, python/cudf_polars/cudf_polars/dsl/translate.py
Adds immutable PerPathValues operations. Hive metadata is translated into scans. Filters on unreadable Hive columns remain residual filters.
Parquet read and materialization
python/cudf_polars/cudf_polars/dsl/ir.py
Scan stores Hive metadata and reconstructs Hive and file-path columns across chunked and non-chunked reads.
Streaming scan propagation
python/cudf_polars/cudf_polars/streaming/io.py
SplitScan and FusedScan preserve and forward Hive metadata. Hybrid scanning is excluded when Hive values are present.
Scan behavior validation
python/cudf_polars/tests/dsl/test_per_path.py, python/cudf_polars/tests/test_scan.py, python/cudf_polars/tests/streaming/test_scan.py, python/cudf_polars/cudf_polars/testing/inject_gpu_engine.py
Adds unit and integration coverage for per-path values, Hive scans, file paths, projections, filters, slicing, data types, nulls, streaming fallback, and updated Delta failure reasons.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: ⚪ Minimal · up to 79f9c

Delta tests now classify mismatched source-schema cases as unsupported schema evolution rather than Hive partitioning. No current merge-blocking behavior or production risk is identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.68% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 88 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: support for Hive-partitioned Parquet files in cudf-polars, with the intentional HybridScan limitation.
Description check ✅ Passed The description directly explains the added Hive-partitioned Parquet support and identifies HybridScan support as deferred.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@vyasr vyasr changed the title Support hive partitioned parqet files (except HybridScan) in cudf-polars Support hive partitioned parquet files (except HybridScan) in cudf-polars Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cudf-polars Issues specific to cudf-polars improvement Improvement / enhancement to an existing function non-breaking Non-breaking change Python Affects Python cuDF API.

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant