[SPARK-57499][SQL] Fix column pruning and invalid plans in variant extraction pushdown on DSv2 scans#56556
Open
qlong wants to merge 1 commit into
Open
Conversation
5abbc88 to
34f3787
Compare
Contributor
|
can you fix merge conflicts? |
34f3787 to
0a76001
Compare
Contributor
Author
|
@cloud-fan rebased. I will keep an eye on the CI build |
…g on DSv2 scans Three fixes in `pushVariantExtractions` (called by `V2ScaqqnRelationPushDown.pushDownVariants`): 1. **Guard against double-visit**: Add a `pushedVariants.isEmpty` sentinel check so the inner `ScanBuilderHolder` leaf visit (caused by `transformDown` recursing into the child after returning the plan unchanged) returns immediately. This ensures `builder.pushVariantExtractions` is called exactly once per holder. 2. **Eager column pruning**: While `projectList` and `filters` are in scope, call `builder.pruneColumns(requiredSchema)` for builders implementing `SupportsPushDownRequiredColumns` and trim `sHolder.output` to the required columns. By the time `buildScanWithPushedVariants` calls `build()`, the builder already has the correct pruned schema. This is similiar to how buildScanWithPushedAggregate works. 3. **Keep whole-variant reads raw**: pushdown is for extractions, not whole-variant reads. A bare variant reference -- `SELECT v`, or a column lifted to feed a `variant_get` above a Join/Sort/Aggregate barrier the local rewrite cannot see -- is recorded as `fullVariant` (path `$`), meaning "the entire value." Shredding that to a lone full-variant slot saves no I/O and is mishandled: the Parquet reader collapses it to a boolean placeholder, and above a barrier the re-exposed `GetStructField AS v#orig` alias is dropped by `RemoveRedundantAliases`, giving wrong results or an invalid plan. So when fullVariant is a variant's only requested field, leave the column raw; when it coexists with real extractions (e.g. `SELECT v, variant_get(v, '$.a')`) the >=2-slot struct is not collapsed and keeps its pushdown. This subsumes the join-key case and shreds automatically once barrier-aware pushdown makes the `variant_get` visible as a typed path. Jira: https://issues.apache.org/jira/browse/SPARK-57499
0a76001 to
81c15e5
Compare
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.
What changes were proposed in this pull request?
Three fixes in
pushVariantExtractions(called byV2ScaqqnRelationPushDown.pushDownVariants):pushedVariants.isEmptysentinel check so the inner
ScanBuilderHolderleaf visit (caused bytransformDownrecursing into the child after returning the planunchanged) returns immediately. This ensures
builder.pushVariantExtractionsis called exactly once per holder.projectListandfiltersare inscope, call
builder.pruneColumns(requiredSchema)for buildersimplementing
SupportsPushDownRequiredColumnsand trimsHolder.outputto the required columns. By the timebuildScanWithPushedVariantscallsbuild(), the builder alreadyhas the correct pruned schema. This is similiar to how
buildScanWithPushedAggregate works.
whole-variant reads. A bare variant reference --
SELECT v, or acolumn lifted to feed a
variant_getabove a Join/Sort/Aggregatebarrier the local rewrite cannot see -- is recorded as
fullVariant(path
$), meaning "the entire value." Shredding that to a lonefull-variant slot saves no I/O and is mishandled: the Parquet reader
collapses it to a boolean placeholder, and above a barrier the
re-exposed
GetStructField AS v#origalias is dropped byRemoveRedundantAliases, giving wrong results or an invalid plan. Sowhen fullVariant is a variant's only requested field, leave the
column raw; when it coexists with real extractions (e.g.
SELECT v, variant_get(v, '$.a')) the >=2-slot struct is not collapsed andkeeps its pushdown. This subsumes the join-key case and shreds
automatically once barrier-aware pushdown makes the
variant_getvisible as a typed path.
Jira: https://issues.apache.org/jira/browse/SPARK-57499
Why are the changes needed?
Three bugs on the accepted variant pushdown path:
Issue 1 — column pruning is skipped.
buildScanWithPushedVariantscallsbuilder.build()and replaces theScanBuilderHolderwith aDataSourceV2ScanRelation.The subsequent
pruneColumnsrule matches onlyScanBuilderHoldernodes, so it is ano-op and
builder.pruneColumns()is never called. The scan reads the full table schemaincluding unreferenced columns. For unreferenced
VARIANTcolumns this is especiallycostly — each is fully reconstructed from its shredded Parquet tree on every row.
Issue 2 — invalid plan / crash on tables with >=2 VARIANT columns
pushDownVariants uses transformDown, which recurses into the child ScanBuilderHolder after returning the plan unchanged. The bare ScanBuilderHolder matches PhysicalOperation a second time, collecting an unreferenced sibling VARIANT column as a full-variant request and pushing it to the builder again. ParquetScanBuilder overwrites its state on every call, so the second push clobbers the correct extraction from the first. The rewritten scan then emits a fresh ExprId for the variant while the projection still references the original, and binding fails.
This affects any extraction shape — projection, ORDER BY, aggregate, join — on a table with two or more VARIANT columns. Single-VARIANT tables are unaffected.
Reproduce on stock spark-4.1.x (path-based views force DSv2):
Issue 3 -- wrong results / crash when a whole-variant read is shredded. A bare
variant reference (a plain
SELECT v, or a column lifted to feed a variant_getabove a Join/Sort/Aggregate barrier the local rewrite cannot see) is recorded as a
full-variant request (path "$"). Shredding it to a lone full-variant slot is both
useless (the whole value is read regardless) and mishandled:
so ORDER BY variant_get(v, '$.price') sorts on the placeholder and silently
returns the wrong order, and max(variant_get(v, '$.price')) fails to codegen.
RemoveRedundantAliases collapses the alias and the condition references a
dropped ExprId, failing plan validation.
Does this PR introduce any user-facing change?
No
How was this patch tested?
Was this patch authored or co-authored using generative AI tooling?
Co-authored with Claude code (Sonnet 4.6)