Repository navigation
feat(spark): prune struct fields inside arrays (FileFormat connector) - #42
Merged
gitRasheed merged 9 commits intoOct 30, 2026
Merged
Conversation
Signed-off-by: Matt Katz <[email protected]>
…ept lists skip Signed-off-by: Matt Katz <[email protected]>
A struct reader over a nullable struct expands `$.g` to `mask($.g, is_not_null($))`. In `list_transform($.g, x -> ...)` that mask sat between the transform and the field, so the partitioner gave field `g` only `$.g` and the list reader read every element column. A list inside nullable struct elements, which is how Spark writes arrays of structs, never had its elements pruned. An infallible transform yields the same values whether its list is masked before or after it, so the reader now masks the whole transform, as it already did for `get_item` and `select` chains, and the transform reaches the list reader. `mask` is now declared infallible: it cannot fail on any input its dtype check accepts. That lets a transform whose body masks a packed element struct take this path too. It also lets other infallibility checks, such as dictionary push-down, evaluate `mask` over values no row uses. The new list reader cases cover the expression shapes the Spark reader builds, and nullable elements at both levels of a nested list. Signed-off-by: gitRasheed <[email protected]>
Adds `Expression.variable` and `Expression.listTransform`, so a Java caller can apply an expression to every element of a list. The body may read only its own parameter; the native lambda rejects anything else, and the error reaches Java as a RuntimeException. Signed-off-by: gitRasheed <[email protected]>
A stable write stores each list chunk as one segment, so reading one field of the structs in an array reads all of them. The list layout stores offsets, validity and elements apart and splits struct elements into one column per field, but only the unstable edition permits it. `vortex.listLayout` (default false) writes it, through `VortexWriter.Builder.listLayout`. Enabled editions are shared by every clone of a session, so enabling the unstable edition on the JVM-wide session would change every later write. The JNI writer instead writes through a session of its own with that edition enabled. The guide says these files may hold other unstable encodings and that future versions may not read them. Signed-off-by: gitRasheed <[email protected]>
Reading `arr.a` from an `array<struct<...>>` read the whole array: the scan builder restored each array's full table type, and the reader projected only through structs. Pruning now recurses through arrays. Each array whose elements are pruned is read through `list_transform(arr, e -> ...)`, whose body keeps the element fields Spark reads in the same way a struct is pruned: `select` when only leaves are dropped, `pack` when a child is pruned too, masked with the element's own nulls when the elements are nullable. Fields resolve against each file's schema as before, which now keeps array shapes as well as struct shapes, and a nullable element field the file lacks reads as null. A file in the list layout then reads only the offsets, validity and element columns the query uses. A stable file still reads the whole array. Signed-off-by: gitRasheed <[email protected]>
A list-layout write ran in a new default session, so it lost every encoding, layout or function registered on the session the caller supplied. It now runs in a fork of that session. `VortexSession::fork` copies the session's variables into a store of its own, so a variable registered on the fork is not seen by the original. The enabled editions are then replaced with a new set holding the session's editions plus the unstable one; the copied set would still share its contents with the original. Signed-off-by: gitRasheed <[email protected]>
Spark prunes fields inside arrays only on the V2 path, and keeps whole arrays after `inline` or `transform` and the inner arrays' fields after `explode`. Signed-off-by: gitRasheed <[email protected]>
`NullFilledArrayVector.getArray` read the length of the wrapped column's array without checking the row, so a null array threw a NullPointerException where Spark's `ColumnVector` contract returns null. 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
Spark can now read a few fields inside arrays of structs without reading the whole array. Before,
SELECT arr.aread every field of every element. This needs files written with the list layout, which stores element fields separately; a newvortex.listLayoutwrite option turns it on per write.Changes
list_transformbodies into element reads.fix(layout): pushlist_transformthrough a nullable struct's fields. Without it, the inner array ofarr.g[0].hwas read whole, because the struct reader wrapped the field in a null mask that hid the transform.maskis now declared infallible.feat(jni):Expression.variableandExpression.listTransform.feat(spark): avortex.listLayoutwrite option (default off). The writer forks its session for that one file with the UNSTABLE edition enabled, so the shared session and its custom registrations are untouched.fix(jni): keep the supplied session's registrations for a list-layout write; addsVortexSession::fork().feat(spark): onelist_transformper array level in the native projection, returning the pruned element struct; a missing nullable field inside elements reads as null, like Parquet.fix(spark): a null array in a pruned column returns null fromgetArray.inline/transformread the whole array, andexplodeprunes only the exploded struct's own fields.Testing
Bytes read, 16,384 rows (Hadoop FS statistics):
arrarr.aarr.g[0].hexplode(arr)thene.g.hvortex-layout765,vortex-arraylist/mask 1,024,vortex-session23,vortex-jni9 pass; clippy clean.checkpasses: 352 tests in each of Spark 3.5/2.12, 3.5/2.13, 4.0, and the 4.1 and 4.2 compat runs.list_transformmay evaluate elements hidden under null lists, which costs work but not correctness).Checked locally.
I used AI tools to help write and review this change.