Skip to content

feat: add support for _pos metadata column#2746

Merged
CTTY merged 11 commits into
apache:mainfrom
hsiang-c:metadata_col_pos
Jul 20, 2026
Merged

feat: add support for _pos metadata column#2746
CTTY merged 11 commits into
apache:mainfrom
hsiang-c:metadata_col_pos

Conversation

@hsiang-c

@hsiang-c hsiang-c commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

What changes are included in this PR?

Are these changes tested?

Sample plan

== Physical Plan ==
AdaptiveSparkPlan isFinalPlan=false
+- CometSort [id#863L, data#864, _pos#868L], [id#863L ASC NULLS FIRST]
   +- CometExchange rangepartitioning(id#863L ASC NULLS FIRST, 200), ENSURE_REQUIREMENTS, CometNativeShuffle, [plan_id=2634]
      +- CometFilter [id#863L, data#864, _pos#868L], (id#863L >= 10)
         +- CometIcebergNativeScan [id#863L, data#864, _pos#868L], file:/var/folders/d2/b93h6k7174ddqxltrxgb51040000gn/T/hive2622216829356851627/table/metadata/00004-95932e2b-bbe6-444e-81b8-069c5e748a20.metadata.json, spark_catalog.default.table (branch=null) [filters=id IS NOT NULL, id >= 10, groupedBy=], 1

@mbutrovich mbutrovich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I like this approach a lot. Delegating to the parquet reader's RowNumber virtual column is essentially what Java does with SupportsRowPosition / PositionReader, and it gets correct absolute file positions through row-group pruning and row selection without us hand-rolling a counter. The virtual_fields plus PassThrough wiring is clean. My only real ask is around tests that exercise _pos where it's actually load-bearing. The current tests read contiguous rows, which would pass even if selection/deletes mis-numbered positions:

  1. _pos with row selection that genuinely prunes rows;
  2. _pos with a delete file applied;
  3. _pos on a split task (start/length not equal to the whole file);
  4. _pos interleaved with other metadata columns.

Curious whether any of these already pass as-is. If so, even better, and they'd lock in the behavior.

.table
.scan()
.select(["x", RESERVED_COL_NAME_POS])
.with_row_selection_enabled(true)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With no filter, all 1024 rows are read contiguously, so this passes whether or not _pos survives selection correctly. The interesting case, and the reason to use the reader's row-number column rather than a counter, is a predicate that prunes row groups/rows, where _pos must still report absolute file positions (for example non-contiguous like [0,1,2, 500,501,...]). Could we add that? It's the test that proves the design.

assert!(result.is_ok(), "Scan timed out - deadlock detected");
}

#[tokio::test]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The spec defines _pos as the ordinal position in the source data file (https://iceberg.apache.org/spec/#reserved-field-ids). Two behaviors I'd love a test for, next to the ones you added here: (a) applying a delete filter doesn't renumber _pos; (b) a task scanning only [start, start+length) still emits absolute positions, matching Java's findStartingRowPos. Both look reachable with the existing TableTestFixture. Do these hold today?


let project_pos = task.project_field_ids().contains(&RESERVED_FIELD_ID_POS);

let arrow_metadata = if project_pos {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There are now up to three sequential ArrowReaderMetadata::try_new rebuilds per file (field-ID schema, INT96 coercion, virtual columns), each re-deriving from parquet metadata. Could the final ArrowReaderOptions (schema plus virtual columns) be assembled once and try_new called a single time? Minor and not blocking; it just compounds with the two existing passes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

+1, the code is getting harder to follow. I have reviewed this part code several times in the past two weeks but still need to refresh my memory every time I see it :)

I think we can create a tracking issue and have AI check if there is any opportunity to refactor it

Comment thread crates/iceberg/src/arrow/reader/pipeline.rs
Comment thread crates/iceberg/src/arrow/record_batch_transformer.rs
arrow_metadata
};

let project_pos = task.project_field_ids().contains(&RESERVED_FIELD_ID_POS);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reusing RowNumber instead of a manual counter is the right, idiomatic call. It inherits the parquet reader's correctness for selection and row-group boundaries and matches Java's SupportsRowPosition semantics. Nice.

@hsiang-c
hsiang-c force-pushed the metadata_col_pos branch from bfa0ae3 to e16e307 Compare July 8, 2026 16:53
@hsiang-c
hsiang-c force-pushed the metadata_col_pos branch from e16e307 to 3450222 Compare July 8, 2026 20:34
@hsiang-c
hsiang-c marked this pull request as ready for review July 8, 2026 20:41
@mbutrovich
mbutrovich self-requested a review July 10, 2026 17:06

@mbutrovich mbutrovich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Delegating _pos to the Parquet reader's RowNumber virtual column is the right call: it inherits the reader's correctness through row-group pruning and row selection, and matches Java's SupportsRowPosition / PositionReader semantics rather than hand-rolling a counter.

The tests requested in the prior round have all landed and prove the design point — test_pos_across_row_groups_via_table_scan, test_pos_reads_only_middle_row_group_via_filter, and test_pos_with_positional_deletes_via_table_scan confirm absolute file positions survive pruning and are not renumbered by deletes. Production footprint is small (~107 lines); the rest is tests.

No blocking comments. The downstream Comet PR (#4752), which exercises this against Iceberg-Java-written data, is green including the Iceberg 1.11 Spark SQL suite.

Merge order

Suggest #2746#2695#2668. #2746 is independent (virtual-column path; doesn't touch the constant-map logic the other two rewrite), so it can go first cleanly.

@hsiang-c

Copy link
Copy Markdown
Contributor Author

Thank you for your suggestions and review @mbutrovich

@hsiang-c

Copy link
Copy Markdown
Contributor Author

@CTTY if you could review it when you have time, thanks again for your help.

@CTTY CTTY left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM! just some minor comments

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since we added some good behavior changes/fixes to this function, can we update the comment here to say that we now explicitly handle reordered schema and mark them as different?

Also I think it would nice to have a test for this function

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.

I'll add another test for it.

@hsiang-c hsiang-c Jul 20, 2026

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.

Added a test for reordered schema (it will lead to SchemaComparison::NameChangesOnly, which is incorrect) and updated comment accordingly.


let project_pos = task.project_field_ids().contains(&RESERVED_FIELD_ID_POS);

let arrow_metadata = if project_pos {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

+1, the code is getting harder to follow. I have reviewed this part code several times in the past two weeks but still need to refresh my memory every time I see it :)

I think we can create a tracking issue and have AI check if there is any opportunity to refactor it

@hsiang-c

hsiang-c commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@CTTY I created a tracking issue and will refactor the process function later.

@CTTY CTTY left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, thanks for the contribution!

@CTTY
CTTY merged commit 8b76dbe into apache:main Jul 20, 2026
21 checks passed
@hsiang-c
hsiang-c deleted the metadata_col_pos branch July 20, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for projecting metadata columns _pos, _spec_id, and _partition in table scan

3 participants