Skip to content

fix(spark): resolve pruned nested fields against each file's struct - #11083

Merged
robert3005 merged 3 commits into
vortex-data:developfrom
gitRasheed:spark-nested-resolution-per-file
Nov 2, 2026
Merged

robert3005 merged 3 commits into
vortex-data:developfrom
gitRasheed:spark-nested-resolution-per-file

Conversation

@gitRasheed

Copy link
Copy Markdown
Contributor

Summary

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.

Changes

  • Build the projection per file from its Arrow schema, matching columns and struct fields by name under spark.sql.caseSensitive at every struct level. A nullable field the file lacks reads as null, and its struct keeps its own nulls.
  • Missing top-level columns go through the same code, which replaces the reader's separate top-level matching.
  • Push a filter only when the file has every field on its path under the same name. Spark evaluates the rest, as it already does for every data filter.

Known gap: structs inside arrays and maps are still read whole and by position, and so are extension types that export as Arrow structs, such as Variant.

Tests

VortexNestedSchemaResolutionTest covers the four cases and compares each struct query with Parquet, with nested schema pruning off and on. It also reads a file with an unsigned field the query doesn't select. These 8 tests fail on develop. A ninth test covers partition columns spelled in another case and a partition-only read. Spark handles those itself for a file format, so it passes on develop and guards the reader change.

Checked locally: :vortex-spark-3.5_2.12:test, :vortex-spark-3.5_2.13:test, :vortex-spark-4.0_2.13:test, spark41CompatTest, spark42CompatTest and spotlessCheck pass.

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

gitRasheed and others added 2 commits November 1, 2026 23:46
The reader matched top-level columns against each file, but built the
nested projection from the table schema and trusted that a read struct
had the file's fields, in the file's order and case. When a supplied
schema did not, a read returned another field's values, failed on a
field named in another case, indexed past the struct's children, or
swapped fields stored in another order, where Parquet returns the right
rows.

The projection is now built per file against that file's Arrow schema.
Columns and struct fields resolve by name under Spark's case
sensitivity, through structs nested in structs, and are read under the
file's names in Spark's order. A nullable column or field the file does
not carry reads as null, and its struct keeps its own nulls. Pushed
filters bind only paths the file carries under the same names, and
Spark evaluates the rest.

Signed-off-by: gitRasheed <[email protected]>
…other type

Resolving a read schema scanned every file field for each read field, at
every struct level, and filter restriction did the same. Both now build a
name lookup once per struct, so wide schemas resolve in linear time.

A read struct over a file field that is neither a struct nor an extension
type now fails with the file and field path, instead of handing Spark a
column of the wrong type.

Test two files with different struct layouts in one query, and the new
error. Use Arrow's extension-name constant, and document what the class
resolves and what it still reads by position.

Signed-off-by: Robert Kruszewski <[email protected]>
@robert3005 robert3005 added the changelog/fix A bug fix label Nov 2, 2026
@codspeed

codspeed Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 19.95%

1 warning

⚠️ Walltime results may be unreliable

Some benchmarks used the Walltime instrument outside CodSpeed Macro Runners, which can make the results inconsistent.

Learn more about Macro Runners

See affected benchmarks

❌ 4 regressed benchmarks
✅ 2314 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Walltime threshold_neon[64] 2.6 µs 3.7 µs -29.35%
❌ Walltime filter_neon[64] 1.8 µs 2.3 µs -21.13%
❌ Walltime filter_neon[80] 2.6 µs 3 µs -14.8%
❌ Walltime threshold_neon[80] 3.5 µs 4 µs -13.51%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing gitRasheed:spark-nested-resolution-per-file (b2ef14e) with develop (b9b02f6)

Open in CodSpeed

@robert3005
robert3005 enabled auto-merge (squash) November 2, 2026 14:30
@robert3005
robert3005 merged commit 23bb75b into vortex-data:develop Nov 2, 2026
75 of 76 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants