Skip to content

Prepend row index column in Parquet reader - #23077

Merged
rapids-bot[bot] merged 9 commits into
NVIDIA:mainfrom
mhaseeb123:fea/prepend-row-index-column
Jul 9, 2026
Merged

Prepend row index column in Parquet reader#23077
rapids-bot[bot] merged 9 commits into
NVIDIA:mainfrom
mhaseeb123:fea/prepend-row-index-column

Conversation

@mhaseeb123

Copy link
Copy Markdown
Contributor

Description

Contributes to #22849

This PR enables optionally prepending a file-local row index (i.e. the absolute index of each row in the file it came from) column to the read table.

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 Jul 2, 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 the libcudf Affects libcudf (C++/CUDA) code. label Jul 2, 2026
@mhaseeb123 mhaseeb123 changed the title Prepend file-local row index column in Parquet reader Prepend row index column in Parquet reader Jul 2, 2026
@mhaseeb123 mhaseeb123 added feature request New feature or request 2 - In Progress Currently a work in progress non-breaking Non-breaking change Spark Functionality that helps Spark RAPIDS Velox Functionality that helps Velox-cudf labels Jul 2, 2026
@mhaseeb123 mhaseeb123 added 3 - Ready for Review Ready for review by team and removed 2 - In Progress Currently a work in progress labels Jul 7, 2026
@mhaseeb123
mhaseeb123 marked this pull request as ready for review July 7, 2026 00:31
@mhaseeb123
mhaseeb123 requested a review from a team as a code owner July 7, 2026 00:31
@coderabbitai

coderabbitai Bot commented Jul 7, 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: 482c990c-372f-40be-bc61-8a5627a04a10

📥 Commits

Reviewing files that changed from the base of the PR and between 9f6acb5 and 2574b9d.

📒 Files selected for processing (1)
  • cpp/src/io/parquet/reader_impl_preprocess.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/io/parquet/reader_impl_preprocess.cu

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a Parquet reader option to prepend a file-local "row_index" column to the output.
    • When enabled alongside "source_index", output columns are ordered as "source_index" then "row_index".
  • Bug Fixes

    • "row_index" now consistently reflects the original file-local position across skips, row-group selection, and filtering (including chunked multi-source reads).
  • Tests

    • Expanded Parquet and chunked Parquet reader tests to validate multi-source, filtered, skipped, and explicit row-group selection behavior for both index columns.

Walkthrough

Adds a prepend_row_index_column Parquet reader option, threads it through reader finalization, computes file-local row offsets per row group, synthesizes row_index and source_index columns, and expands reader tests for multi-source and selected reads.

Changes

Row index column feature

Layer / File(s) Summary
Public API option for row index column
cpp/include/cudf/io/parquet.hpp
Adds the row-index option flag, getter, setter, and builder method on parquet_reader_options.
Reader wiring and output columns
cpp/src/io/parquet/reader_impl.hpp, cpp/src/io/parquet/reader_impl.cpp
Adds row-index support to reader_impl options, computes the active read range before advancing output state, prepends row_index and source_index columns to the output schema, and adjusts expression-column offsets.
Row-group offset tracking
cpp/src/io/parquet/reader_impl_helpers.hpp, cpp/src/io/parquet/reader_impl_helpers.cpp
Adds per-source row-group start offsets to row_group_info and computes them during row-group selection with a new metadata helper.
Device synthesis of index columns
cpp/src/io/parquet/reader_impl_preprocess.cu
Adds the device-side row-index mapping functor and implements synthesis of row_index and source_index columns from row ranges and per-source row counts.
Row index tests
cpp/tests/io/parquet_chunked_reader_test.cu, cpp/tests/io/parquet_reader_test.cpp
Adds tests for row_index behavior in chunked multi-source reads, filtered reads, skip_rows/num_rows reads, and row-group selection scenarios.

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

Possibly related PRs

  • rapidsai/cudf#22879: Prior Parquet reader work on synthesized source-index columns and output-schema offset handling that this change extends with row_index.

Suggested labels: cuIO, tests, doc

Suggested reviewers: qbacpey, PointKernel, bdice, vuule, vyasr

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding an optional row index column to the Parquet reader.
Description check ✅ Passed The description directly describes the row index column feature and matches the implemented changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
cpp/tests/io/parquet_chunked_reader_test.cu (1)

1986-2126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM! Correct row-index/source-index arithmetic across chunked multi-source reads.

Verified the i / num_rows / i % num_rows reconstruction for unfiltered, skip/num_rows, and filtered cases against the multi-source concatenation and per-source filtering semantics — all consistent with the documented prepend_row_index_column/prepend_source_index_column contract.

One gap: none of the three scenarios exercise a source with null values in the data column, or a source fully excluded by the filter (zero surviving rows). Given this is new device-synthesis logic for row/source index columns interacting with filtering and chunked multi-pass reads, adding at least one case with nulls and one with a fully-filtered-out source would strengthen coverage.

As per coding guidelines, "Tests missing edge cases: empty input, null values, sliced columns, boundary sizes, multi-block sizes" should be covered for changed/critical paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/io/parquet_chunked_reader_test.cu` around lines 1986 - 2126, The
new multi-source chunked parquet test covers the happy paths but misses two
important edge cases: nulls in the data column and a source that produces zero
surviving rows after filtering. Update TestRowIndexColumnMultipleSources to add
a scenario using null-containing input and another where one source is fully
filtered out, and verify prepend_source_index_column and
prepend_row_index_column still produce correct results for those cases.

Source: Coding guidelines

cpp/tests/io/parquet_reader_test.cpp (1)

4981-5146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM! Solid coverage of row-index semantics, including the tricky out-of-order row-group case.

Traced through both tests: RowIndexColumn correctly validates per-source reset behavior, column ordering (row_idx alone vs. src_idx, row_idx, data), and filtered-row-index recomputation. RowIndexSelectedRead's out-of-order row-group test (Lines 5108-5119) is a particularly valuable check that file-local row index reflects absolute file position rather than selection/read order.

As with the chunked-reader tests, coverage is missing for a data column containing nulls and for a source that is entirely filtered out (zero surviving rows) when both index columns are enabled. Worth adding given this is new logic mapping filtered/selected rows back to absolute file-local positions.

As per coding guidelines, "Tests missing edge cases: empty input, null values, sliced columns, boundary sizes, multi-block sizes" should be covered for changed/critical paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/io/parquet_reader_test.cpp` around lines 4981 - 5146, Add missing
edge-case coverage in the ParquetReaderTest row-index tests: extend
RowIndexColumn and/or RowIndexSelectedRead to verify row-index behavior when the
data column contains nulls and when a source is completely filtered out with
both prepend_source_index_column and prepend_row_index_column enabled. Use the
existing test helpers and symbols like RowIndexColumn, RowIndexSelectedRead,
read_parquet, parquet_reader_options::builder, and the filter/row_groups setup
to assert the index columns still map to file-local positions or produce an
empty result as expected.

Source: Coding guidelines

cpp/include/cudf/io/parquet.hpp (1)

316-329: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the row index column's data type.

The doc explains ordering but not the resulting column type. Per the device-side implementation (synthesize_row_index_column in reader_impl_preprocess.cu), the row index column uses size_t (UINT64), whereas the sibling source index column uses cudf::size_type (INT32). Since callers rely on this doc to know what type to expect downstream, documenting the dtype asymmetry here would help avoid confusion.

📝 Proposed doc addition
   /**
    * `@brief` Returns whether to prepend a file-local row index column to the output.
    *
    * The row index column contains, for each output row, the row's index within its parquet
    * source file. If the source index column is also enabled, the column order is: source index,
    * row index, data columns.
    *
+   * `@note` The row index column has type `size_t` (mapped to `UINT64`), unlike the source index
+   * column which uses `cudf::size_type` (`INT32`).
+   *
    * `@return` `true` if a row index column should be prepended
    */
   [[nodiscard]] bool is_enabled_prepend_row_index_column() const
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/cudf/io/parquet.hpp` around lines 316 - 329, The getter is
documented for ordering but not for the row index column’s actual dtype, which
is different from the source index column. Update the doc comment on
is_enabled_prepend_row_index_column to explicitly state that the synthesized row
index column uses size_t/UINT64, while the source index column uses
cudf::size_type/INT32, so callers know what type to expect downstream.
cpp/src/io/parquet/reader_impl.cpp (1)

880-896: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate pass-in-bounds condition.

_file_itm_data._current_input_pass < _file_itm_data.num_passes() is evaluated twice back-to-back (once for read_info, once for the chunk-advance guard). Hoisting into a local avoids risk of the two checks drifting apart in future edits.

♻️ Proposed dedup
-  // Row-range of the current output chunk relative to the current row group selection.
-  auto const read_info =
-    (_file_itm_data._current_input_pass < _file_itm_data.num_passes())
-      ? _pass_itm_data->subpass
-          ->output_chunk_read_info[_pass_itm_data->subpass->current_output_chunk]
-      : row_range{0, 0};
-
-  // advance output chunk/subpass/pass info for non-empty tables if and only if we are in bounds
-  if (_file_itm_data._current_input_pass < _file_itm_data.num_passes()) {
+  auto const has_current_pass = _file_itm_data._current_input_pass < _file_itm_data.num_passes();
+
+  // Row-range of the current output chunk relative to the current row group selection.
+  auto const read_info =
+    has_current_pass
+      ? _pass_itm_data->subpass
+          ->output_chunk_read_info[_pass_itm_data->subpass->current_output_chunk]
+      : row_range{0, 0};
+
+  // advance output chunk/subpass/pass info for non-empty tables if and only if we are in bounds
+  if (has_current_pass) {
     auto& pass    = *_pass_itm_data;
     auto& subpass = *pass.subpass;
     subpass.current_output_chunk++;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/io/parquet/reader_impl.cpp` around lines 880 - 896, The
pass-in-bounds check in reader_impl.cpp is duplicated for both computing
read_info and advancing the output chunk state, so hoist
`_file_itm_data._current_input_pass < _file_itm_data.num_passes()` into a single
local and reuse it in this section. Update the logic around `read_info`,
`pass.subpass`, and `subpass.current_output_chunk` to rely on that shared
boolean so the guard stays consistent and does not drift in future edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/io/parquet/reader_impl_preprocess.cu`:
- Around line 1219-1231: The row-offset staging buffer in the preprocess path
can be destroyed before the async H2D copy finishes. Update the block that
builds `host_row_offsets` and calls `cudf::detail::make_device_uvector_async` /
`cudf::detail::label_segments` to keep the pinned buffer alive until the stream
work is complete, matching the `synthesize_row_index_column` pattern by
synchronizing `_stream` before the block exits.

---

Nitpick comments:
In `@cpp/include/cudf/io/parquet.hpp`:
- Around line 316-329: The getter is documented for ordering but not for the row
index column’s actual dtype, which is different from the source index column.
Update the doc comment on is_enabled_prepend_row_index_column to explicitly
state that the synthesized row index column uses size_t/UINT64, while the source
index column uses cudf::size_type/INT32, so callers know what type to expect
downstream.

In `@cpp/src/io/parquet/reader_impl.cpp`:
- Around line 880-896: The pass-in-bounds check in reader_impl.cpp is duplicated
for both computing read_info and advancing the output chunk state, so hoist
`_file_itm_data._current_input_pass < _file_itm_data.num_passes()` into a single
local and reuse it in this section. Update the logic around `read_info`,
`pass.subpass`, and `subpass.current_output_chunk` to rely on that shared
boolean so the guard stays consistent and does not drift in future edits.

In `@cpp/tests/io/parquet_chunked_reader_test.cu`:
- Around line 1986-2126: The new multi-source chunked parquet test covers the
happy paths but misses two important edge cases: nulls in the data column and a
source that produces zero surviving rows after filtering. Update
TestRowIndexColumnMultipleSources to add a scenario using null-containing input
and another where one source is fully filtered out, and verify
prepend_source_index_column and prepend_row_index_column still produce correct
results for those cases.

In `@cpp/tests/io/parquet_reader_test.cpp`:
- Around line 4981-5146: Add missing edge-case coverage in the ParquetReaderTest
row-index tests: extend RowIndexColumn and/or RowIndexSelectedRead to verify
row-index behavior when the data column contains nulls and when a source is
completely filtered out with both prepend_source_index_column and
prepend_row_index_column enabled. Use the existing test helpers and symbols like
RowIndexColumn, RowIndexSelectedRead, read_parquet,
parquet_reader_options::builder, and the filter/row_groups setup to assert the
index columns still map to file-local positions or produce an empty result as
expected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0ab2afa8-f5b7-4504-ac18-2eaff3111df1

📥 Commits

Reviewing files that changed from the base of the PR and between 6d8ebac and 5dc873f.

📒 Files selected for processing (8)
  • cpp/include/cudf/io/parquet.hpp
  • cpp/src/io/parquet/reader_impl.cpp
  • cpp/src/io/parquet/reader_impl.hpp
  • cpp/src/io/parquet/reader_impl_helpers.cpp
  • cpp/src/io/parquet/reader_impl_helpers.hpp
  • cpp/src/io/parquet/reader_impl_preprocess.cu
  • cpp/tests/io/parquet_chunked_reader_test.cu
  • cpp/tests/io/parquet_reader_test.cpp

Comment thread cpp/src/io/parquet/reader_impl_preprocess.cu
@vuule
vuule self-requested a review July 7, 2026 18:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cpp/tests/io/parquet_reader_test.cpp (1)

4981-5155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add edge-case coverage for the new row_index tests.

The new RowIndexColumn, SourceAndRowIndexColumns, and RowIndexSelectedRead tests exercise multi-source, filtered, and row-group-selection scenarios well, but none cover an empty source/table, a source containing nulls, or a sliced input column with prepend_row_index_column. Consider adding at least one case for an empty parquet file/read and one with nullable data columns to confirm the row_index column is still correctly synthesized (or trivially empty) in those situations.

Based on path instructions ("Test suites should cover edge cases such as empty input, null values, sliced columns, boundary sizes, and multi-block sizes.").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/io/parquet_reader_test.cpp` around lines 4981 - 5155, The new
row_index coverage is missing edge cases for empty input and nullable/sliced
data, so extend the existing ParquetReaderTest cases in RowIndexColumn,
SourceAndRowIndexColumns, or RowIndexSelectedRead to include at least one empty
parquet read and one nullable column scenario, plus a sliced input with
prepend_row_index_column. Verify the synthesized row_index column is empty when
appropriate and remains correctly offset/restarted for null-containing and
sliced inputs.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cpp/tests/io/parquet_reader_test.cpp`:
- Around line 4981-5155: The new row_index coverage is missing edge cases for
empty input and nullable/sliced data, so extend the existing ParquetReaderTest
cases in RowIndexColumn, SourceAndRowIndexColumns, or RowIndexSelectedRead to
include at least one empty parquet read and one nullable column scenario, plus a
sliced input with prepend_row_index_column. Verify the synthesized row_index
column is empty when appropriate and remains correctly offset/restarted for
null-containing and sliced inputs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d5d215cb-88ff-45e3-8687-2a129f8a1d57

📥 Commits

Reviewing files that changed from the base of the PR and between 18b2e21 and 15cc4fb.

📒 Files selected for processing (3)
  • cpp/src/io/parquet/reader_impl.cpp
  • cpp/src/io/parquet/reader_impl_preprocess.cu
  • cpp/tests/io/parquet_reader_test.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • cpp/src/io/parquet/reader_impl.cpp
  • cpp/src/io/parquet/reader_impl_preprocess.cu

Comment thread cpp/src/io/parquet/reader_impl.cpp
column_name_info{.name = "src_idx", .is_nullable = false});
// Prepend the source and row index columns if requested
{
if (_options.prepend_row_index_column) {

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.

Synthesize and prepend row index column

std::vector<size_t> aggregate_reader_metadata::compute_source_row_group_offsets(
size_type src_idx) const
{
CUDF_EXPECTS(src_idx >= 0 && std::cmp_less(src_idx, per_file_metadata.size()),

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.

Compute absolute (file-local) row offset for each row group


} // namespace

std::unique_ptr<column> reader_impl::synthesize_row_index_column(row_range const& read_info)

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.

This function is identical to synthesize_source_index_column except the computation functor above. I did consider a common function taking in a templated functor but it didn't look any better than duplicate looking functions

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.

can you at least factor out common code into utility functions?

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.

Common code is barely a couple lines. Not sure if it's worth it.

mhaseeb123 added a commit to mhaseeb123/cudf that referenced this pull request Jul 7, 2026
…snapshot]

Backport of the effective diff of rapidsai/cudf PR NVIDIA#23077
("Prepend row index column in Parquet reader"), which was still OPEN at
the time this branch was created.

This captures the PR's net change as a single squashed commit computed as
the diff between its merge-base with main and its head:
  merge-base: 52d322c
  head:       2574b9d

Applied on top of the 26.06.01 pin + NVIDIA#22773 + NVIDIA#22879 backport.

NOTE: This is a PRE-MERGE snapshot; re-sync to the squashed merge commit
once NVIDIA#23077 lands upstream. Unrelated main-only tests present as diff
context (MismatchedSchema*) were intentionally excluded.
Comment thread cpp/src/io/parquet/reader_impl.hpp
Comment thread cpp/src/io/parquet/reader_impl_preprocess.cu
@mhaseeb123 mhaseeb123 added 5 - Ready to Merge Testing and reviews complete, ready to merge and removed 3 - Ready for Review Ready for review by team labels Jul 9, 2026
@mhaseeb123

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit fa37c3b into NVIDIA:main Jul 9, 2026
138 checks passed
@mhaseeb123
mhaseeb123 deleted the fea/prepend-row-index-column branch July 9, 2026 18:36
rapids-bot Bot pushed a commit that referenced this pull request Jul 10, 2026
Follow up #23077

Supply stream and mr to column synthesizers which are used to produce the output columns

Authors:
  - Muhammad Haseeb (https://github.com/mhaseeb123)

Approvers:
  - Bradley Dice (https://github.com/bdice)
  - David Wendt (https://github.com/davidwendt)

URL: #23209
rapids-bot Bot pushed a commit that referenced this pull request Jul 14, 2026
Closes #22849. 
Follow up of #22879 + #23077

This PR enables prepending source and row index columns in hybrid scan. For two-step materialization, the extra index columns are prepended to filter columns only. Single-step materialization is identical to the main parquet reader

Authors:
  - Muhammad Haseeb (https://github.com/mhaseeb123)

Approvers:
  - Yunsong Wang (https://github.com/PointKernel)
  - Vyas Ramasubramani (https://github.com/vyasr)

URL: #22878
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 feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change Spark Functionality that helps Spark RAPIDS Velox Functionality that helps Velox-cudf

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants