Repository navigation
perf(spark): push nested schema pruning down to the native scan (FileFormat connector) - #40
Merged
Conversation
…he field Inside a nullable struct, `is_not_null($.x)` expanded to `is_not_null(mask($.x, is_not_null($)))`, so the partitioner handed the field's reader a bare `$` and all of `x` was read just for its validity. Check the path's own nulls and the parent's validity separately, which reads only validity. A pruned projection of a struct inside a nullable struct, such as `mask(pack(q: $.t.x.q), is_not_null($.t.x))`, needs this. Reading `s.t.x.q` from Spark with `s`, `t` and `x` nullable read 6.8 MB before and 39 KB after. Signed-off-by: gitRasheed <[email protected]>
The V2 scan left Spark's nested schema pruning off and the reader projected whole top-level columns, so reading `s.a` from a struct `s` read all of `s`. The scan builder now opts in, and the reader turns the pruned read schema into a native projection: `select` when only leaf fields are pruned, which keeps the struct's nulls, and `mask(pack(...), is_not_null(...))` when a child struct is pruned too. Structs inside arrays and maps keep their full type, as the native scan cannot project list elements yet. This adds the `Expression.mask` JNI binding. The V1 `FileFormat` path is unchanged: Spark's V1 schema pruning rule only applies to Parquet and ORC. Signed-off-by: gitRasheed <[email protected]>
The reader trusted that a read struct had the file's fields, in the file's order and case. With a supplied schema that did not, it read the wrong field, failed in the native scan, or indexed past the struct's children, where Parquet returns the right value or null. Nested fields now resolve by name against each file's own schema under Spark's case sensitivity, at every level, and are read under the file's names. A nullable field the file does not carry reads as null; a non-nullable one fails naming the field. A struct the file carries none of the read fields of keeps its own nulls, as Parquet does from Spark 4.1. Pushed filters only bind paths the file carries under the same names, and Spark evaluates the rest. Signed-off-by: gitRasheed <[email protected]>
The reader converted every column of a file to a Spark type before projecting, though only names, nullability and struct shapes are matched against. A column Spark cannot represent, such as DECIMAL(50, 2), failed a query that never read it. An unsigned integer inside a struct turned the whole struct into an opaque column, so reading a sibling field read the whole struct and failed on the unsigned field. The file schema now keeps struct shapes and leaves every other type unconverted, so a column Spark cannot read only fails a query that reads it. Signed-off-by: gitRasheed <[email protected]>
Signed-off-by: gitRasheed <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pushes Spark's nested schema pruning down to the native scan on the FileFormat connector. Spark asks for only the nested fields a query uses, but the connector read every nested column whole:
SELECT s.t.x.qread the whole ofs. Now the scan projects just the requested struct fields, at any depth.Changes
perf(layout): checking a nullable struct field's nulls no longer reads the field. Without it, a projection below a nullable struct still decoded the struct's other fields.feat(spark): the pruned read schema becomes a native projection (selectwhen only leaves are dropped,packplus a null mask when a child struct is pruned too).fix(spark): Spark's pruned schema is resolved against each file's schema, so case-insensitive names, files with reordered or missing nested fields, and a narrower user schema all read the right fields. Missing nullable fields read as null.fix(spark): files are matched by shape and names without converting leaf types, so an unselected column Spark can't represent (DECIMAL(50,2), unsigned ints) no longer breaks the scan or disables pruning for its parent struct.Structs inside arrays and maps are still read whole here; a follow-up PR handles arrays.
Testing
s.t.x.qreads 39 KB instead of 10.8 MB,s.a34 KB.DECIMAL_PRECISION_EXCEEDS_MAX_PRECISION,Unsupported Arrow unsigned integer type).checkpasses: 325 tests in each of Spark 3.5/2.12, 3.5/2.13, 4.0, and the 4.1 and 4.2 compat runs.Checked locally.
I used AI tools to help write and review this change.