Skip to content

perf(layout): read a path below a nullable struct's field without the rest of the field - #10952

Open
gitRasheed wants to merge 1 commit into
vortex-data:developfrom
gitRasheed:struct-path-below-nullable-field
Open

gitRasheed wants to merge 1 commit into
vortex-data:developfrom
gitRasheed:struct-path-below-nullable-field

Conversation

@gitRasheed

@gitRasheed gitRasheed commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Reading a field two levels into nullable structs read the whole inner struct. For t: {x: {p, q}, r} with t and x nullable, projecting $.t.x.q or filtering on $.t.x.q > n read 6.05 MB of a 23.7 MB file, as much as reading all of x. With this change it reads 160 KB. One level, $.s.b, was already pruned.

expand_struct_root rewrites a field access $.x on a nullable struct to mask($.x, is_not_null($)), so the field keeps the struct's nulls. For $.x.q that gives get_item(q, mask($.x, ...)), and the mask hides the path from the partitioner: x's reader gets $ and reads every field. A Spark connector that pushes nested schema pruning down runs into this for any struct inside a nullable struct, since Spark structs are nearly always nullable.

Changes

A get_item/select chain that starts at $.field is rewritten as a whole to mask(chain_on_field, is_not_null($)), so the field's reader gets the path and reads only what it needs.

test_path_below_nullable_struct_field reads a leaf, a select, is_not_null of a leaf and a predicate through two nullable structs. It checks the results against in-memory evaluation and that no segment of the other fields is requested. Every case fails without the change.

Checked locally: cargo test -p vortex-layout --lib (696 passed), clippy with -D warnings, nightly fmt, and the byte counts above from a throwaway scan through a counting reader.

I used AI tools to help write and review this change.

… rest of the field

A nullable struct expands a field access `$.x` to `mask($.x, is_not_null($))`. For a
path below the field, such as `$.x.q`, the mask then sits between the path and the
field, so the partitioner hands the field's reader a bare `$` and the whole of `x`
is read. Mask the whole path instead.

On a file with `t: {x: {p, q}, r}`, both nullable, reading `$.t.x.q` or filtering on
`$.t.x.q > n` read 6.0 MB before and 160 KB after.

Signed-off-by: gitRasheed <99349713+gitRasheed@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

1 participant