Skip to content

[python] Support selected-key projection for shared-shredding MAP - #9789

Open
XiaoHongbo-Hope wants to merge 7 commits into
apache:masterfrom
XiaoHongbo-Hope:codex/map-selected-key-projection
Open

[python] Support selected-key projection for shared-shredding MAP#9789
XiaoHongbo-Hope wants to merge 7 commits into
apache:masterfrom
XiaoHongbo-Hope:codex/map-selected-key-projection

Conversation

@XiaoHongbo-Hope

@XiaoHongbo-Hope XiaoHongbo-Hope commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Purpose

Project literal keys from top-level MAP<STRING, ...> columns through the existing API:

read_builder.with_projection([
    "id",
    "attributes['subkey']",
])

Nested ROW paths remain profile.name. attributes['sub.key'] treats sub.key as one literal MAP key.

Behavior

  • Shared-shredding files read selected physical key columns and overflow.
  • Other layouts or unencodable keys read the complete MAP and extract the key.
  • Parquet, ORC, ROW, row sidecars, data evolution, column rename, and value-type evolution are supported.
  • No new method or table option is introduced.

Scope

This PR only adds projection. Physical-column filters continue to work. Filtering projected MAP keys remains rejected. Query authorization also remains rejected because parent-MAP filters and masks must run before key extraction.

Tests

Focused current-PyArrow and PyArrow 6 tests, Flake8, and git diff --check pass.

@XiaoHongbo-Hope
XiaoHongbo-Hope marked this pull request as ready for review September 14, 2026 10:17
@XiaoHongbo-Hope

Copy link
Copy Markdown
Contributor Author

@JingsongLi Could you please review the public API choice?

PyPaimon has no expression layer, so this PR keeps the existing projection entry point and uses Spark-style MAP access:

read_builder.with_projection([
    "profile.name",
    "attributes['key.with.dots']",
])

We considered adding with_read_type() or a Python-specific projection object, but both expose more low-level API. Internally, this still uses the same selected-key RowType metadata as Java/Spark. Does this approach look reasonable?

Comment thread docs/docs/pypaimon/reading.md Outdated

```python
read_builder = read_builder.with_projection([
'id', "attributes['key.with.dots']"

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.

...the doc is super strange, you should introduce a simple one. And explain key with dots...

@XiaoHongbo-Hope
XiaoHongbo-Hope force-pushed the codex/map-selected-key-projection branch from 1a69325 to e12cb2c Compare September 14, 2026 11:28
@XiaoHongbo-Hope
XiaoHongbo-Hope force-pushed the codex/map-selected-key-projection branch from e12cb2c to 12cdaaa Compare September 14, 2026 11:30

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

Two correctness issues in projection resolution and the fallback for ROW files, both reproduced with end-to-end table reads.

]
if not candidates:
continue
top = max(candidates, key=len)

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.

[P2] Preserve the existing ROW-path resolution priority

Choosing the longest top-level ROW prefix changes the meaning of an already valid projection. For example, with top-level fields a: ROW<b: ROW<c: BIGINT>>, a.b: ROW<d: BIGINT>, and id, with_projection(['a.b.c', 'id']) previously returned a_b_c and id. This code selects the top-level a.b first, fails to find its child c, and silently drops the projection instead of resolving a -> b -> c. If the top-level a.b also has a child c, the same projection silently reads a different column. I reproduced both cases against the base and PR implementations using actual table writes and reads.

Could we preserve the original valid ROW path first, and only try dotted top-level prefixes when that path cannot be resolved? Exact top-level name matches can still retain their existing precedence.

if path[1] not in keys:
keys.append(path[1])
try:
field = map_selected_keys_field(field, keys)

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.

[P2] Preserve the full-MAP fallback when the physical reader is ROW

This replaces the MAP read type with a selected-key ROW before the physical format is known, but FormatRowReader still returns a MAP array. DataFileBatchReader then raises ArrowNotImplementedError: Unsupported cast from map<string, int64> to struct using function cast_struct.

This also affects the explicitly supported Parquet data-evolution path: enable row-tracking.enabled, data-evolution.enabled, and data-evolution.row-sidecar.enabled, write 100 rows with attributes = {'first': row_id}, and read _ROW_ID = 5 while projecting only attributes['first']. The sparse read selects the ROW sidecar and fails. Projecting both the complete attributes MAP and the key succeeds and returns 5; a table using file.format = row exhibits the same failure.

Please retain the complete-MAP fallback for readers that do not assemble selected-key ROWs, or add that conversion to the ROW reader. Checking only the table's file.format would miss Parquet tables that select a ROW sidecar.

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.

The ROW and row-sidecar cases now pass. I think key projection should support MAP<STRING, BLOB> as well, since this is an existing supported table type.

There is still a failure on 88cafac: create a data-evolution table with payload MAP<STRING, BLOB> and write {'k': b'hello', 'v': b'world'}. Reading the complete MAP succeeds, but with_projection(["payload['k']"]) raises ArrowTypeError. Projecting both the complete MAP and the key also succeeds.

The selected-key ROW type reaches FormatBlobReader, so it no longer recognizes the field as a MAP of BLOBs and tries to read it as a scalar BLOB. The failure happens before the new conversion in DataFileBatchReader. Please preserve the physical MAP schema for BLOB decoding and then extract the selected key, or use the complete-MAP fallback for this layout.

Please also add an end-to-end regression test that writes a real MAP-of-BLOBs table and verifies key-only projection returns the expected bytes. Cover missing keys, null values, and null/empty MAPs, and compare the results with complete-MAP reads so the test exercises the actual BLOB reader path.

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.

2 participants