Repository navigation
feat(spark): push nested struct pruning down to the native scan - #10957
Conversation
robert3005
left a comment
There was a problem hiding this comment.
Just the one thing about nullability
| } | ||
| Expression packed = Expression.pack(names, children, false); | ||
| // `pack` builds a struct with no nulls, so carry over the source struct's nulls. | ||
| return nullable ? Expression.mask(packed, Expression.isNotNull(struct)) : packed; |
There was a problem hiding this comment.
There's a chance that this differs from the file nullability, can you add a test where two files differ in nullability only and make sure they get read correctly?
There was a problem hiding this comment.
Good catch. I added a test with two files that differ only in nullability, reading a pruned nested struct through the masked path: a2a20e6. It passes, and fails if the mask is removed.
It only covers the order where the inferred file is the nullable one. In the other order, reads crash on develop too, with or without this PR, because schema inference reads one file and keeps its nullability (#11019). #11020 makes every column nullable, as Spark's file sources do. Once it lands I'll add the other order here.
…ullable Port the open Spark connector fixes to the FileFormat connector. From #10957, struct columns are now pruned to the nested fields that a query uses, at any depth. VortexScanBuilder keeps Spark's pruned type for struct columns. The partition reader projects them with select, or with pack masked by the struct's validity when a child struct is also pruned. Add the Expression.mask JNI binding that this needs. From #11020, add regression tests for files that differ only in nullability and for a user schema that declares a column non-nullable. FileTable and HadoopFsRelation already read every column as nullable, so no reader change is necessary. Signed-off-by: Robert Kruszewski <[email protected]>
|
You will need to resolve conflicts here as well |
Spark asks the scan for only the nested fields a query uses, but the connector read every nested column whole, so SELECT s.b decoded all of s. pruneColumns now keeps Spark's pruned type for struct columns, and NestedSchemaPruning turns the pruned read schema into a native projection: select([b], $.s) when only leaf fields are pruned, or mask(pack(..), is_not_null($.s)) when a child struct is pruned too. Fields inside arrays and maps are still read whole. Adds an Expression.mask JNI binding, and moves the duplicated CountingSegmentSource test helper into segments/test.rs. Signed-off-by: Christopher Wirt <[email protected]>
…llability, in both orders Signed-off-by: Christopher Wirt <[email protected]>
Head branch was pushed to by a user without write access
a2a20e6 to
7c4353b
Compare
|
Rebased on #11020. |
Merging this PR will improve performance by 21.12%1 warning
Performance Changes
Tip Curious why performance improved? Comment Comparing |
|
Looks like there is another merge conflict sorry |
Signed-off-by: Christopher Wirt <[email protected]>
Head branch was pushed to by a user without write access
Signed-off-by: Christopher Wirt <[email protected]>
…ullable Port the Spark connector fixes from develop to the FileFormat connector. From #10957, struct columns are now pruned to the nested fields that a query uses, at any depth. VortexScanBuilder keeps Spark's pruned type for struct columns. The partition reader projects them with select, or with pack masked by the struct's validity when a child struct is also pruned. The projection uses the file's own field names, which can differ in case from the Spark names. From #11020, keep the regression tests for files that differ only in nullability and for a user schema that declares a column non-nullable. FileTable and HadoopFsRelation already read every column as nullable, so no reader change is necessary. Signed-off-by: Robert Kruszewski <[email protected]>
…11083) Follow-up to #10957. The reader matches top-level columns against each file, but it builds the nested projection from Spark's schema and never checks it against the file's struct. When a file's struct differs from the read schema, Vortex returns wrong rows or fails where Parquet returns the right ones: 1. Narrower schema. A file with `c<x, y>` read as `c STRUCT<y>` returns x's values for `c.y`. 2. Name case. A file with `s<a>` read as `s STRUCT<A>` with `caseSensitive=false` throws `A not found`. 3. Missing field. Files with `s<a>` and `s<a, b>` read as `s<a, b>` throw instead of returning a null `b` for the first file. 4. Field order. Files with `c<x, y>` and `c<y, x>` swap x and y for the second file. Cases 3 and 4 need a supplied schema, because schema merging still requires the same struct fields in every file. --------- Signed-off-by: gitRasheed <[email protected]> Signed-off-by: Robert Kruszewski <[email protected]>
Summary
Spark asks the scan for only the nested fields a query uses, but the connector reads every nested column whole.
SELECT s.bdecodes all ofs.Struct columns are now pruned all the way down to the native scan, at any depth. Fields inside arrays and maps are still read whole.
#9658 rewrites the connector as a
FileFormatand will conflict with this. Since it's not clear when it will land, this change targets the current connector. I'm happy to port it onto #9658 afterwards.Changes
Spark connector
pruneColumnskeeps Spark's pruned type for struct columns. Other columns keep their table type, as in fix(spark): return the requested field when Spark prunes nested fields #10724.NestedSchemaPruningturns the pruned read schema into a native projection:select([b], $.s)when only leaf fields are pruned. This keeps the struct's own nulls.mask(pack(..), is_not_null($.s))when a child struct is pruned too.packhas no nulls, so the mask carries them over. Non-nullable structs skip the mask.JNI
Expression.maskbinding.vortex-layoutStructReaderreads these projections field by field.CountingSegmentSourcenow lives insegments/test.rs, replacing four copies.Tests
IS NULLnext to a pruned field, so they go through the pruned read, and assert the read schema Spark asks for.developtoo, and fix(spark): read every column as nullable #11020 fixes it.Tested with:
cargo test -p vortex-layout --lib: 696 passed./gradlew :vortex-jni:test :vortex-spark_2.13:test :vortex-spark_2.12:test: all passBenchmark
JMH, 2 forks × 10 iterations, release native library, Spark 4.1
local[4]. The table has 2M rows, with a nullable 20-field structsand the same struct nested atouter.inner.nestedSchemaPruning=falseis the old behaviour, where the whole struct is read.max(s.f3)max(outer.inner.f3)Known gaps
nullable_outer.inner.c,inner.aandinner.bare still read, but sibling fields ofinnerare skipped.StructReadermasks$.innerwith the parent's validity before theselect, so theselectisn't pushed intoinner.AI assistance
Disclosed per the AI policy: the investigation, code and tests were drafted with Claude (Claude Code) and reviewed by me.