fix: preserve projection and cast target field metadata - #24831
Draft
adriangb wants to merge 2 commits into
Draft
Conversation
Field metadata on a ProjectionExec's output schema could silently disappear when the physical optimizer removed or rewrote projections: 1. A metadata-only identity projection was treated as removable, because the check only compared column indices, aliases, and counts. 2. Collapsing a projection across a metadata boundary substituted the outer expression through the inner projection, so metadata-reading expressions saw the scan field instead of the projected field. 3. `make_with_child` rebuilt the projection with `try_new`, rederiving the output schema and dropping the original metadata. This commit is taken verbatim from @gene-bordegaray's work in apache#24670. Co-Authored-By: Gene Bordegaray <gene.bordegaray@datadoghq.com>
The logical `Expr::Cast`/`Expr::TryCast` carry a `FieldRef` target so a cast
can express a destination that is more than a `DataType` (for example an
extension type produced by a `TypePlanner`). `cast_output_field` ignored that
field's metadata entirely and always inherited the source's, so
`Expr::to_field()` disagreed with the physical `CastExpr`, which already treats
a non-synthesized target field as authoritative.
The divergence was masked because the physical optimizer rederives a
projection's schema from its expressions, repairing the logical schema on the
way through. Once projections preserve their metadata faithfully (previous
commit) the underlying bug surfaces, and a cast to an extension type loses it:
SELECT arrow_metadata(CAST(raw AS UUID), 'ARROW:extension:name')
-- 'arrow.uuid' before, NULL after
Take the target's metadata when it carries any, and otherwise inherit the
source's. A plain `CAST(expr AS type)` synthesizes a target with no metadata,
so its long-standing behaviour is unchanged.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24831 +/- ##
========================================
Coverage 81.58% 81.58%
========================================
Files 1123 1123
Lines 406610 406748 +138
Branches 406610 406748 +138
========================================
+ Hits 331719 331836 +117
- Misses 55453 55460 +7
- Partials 19438 19452 +14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Which issue does this PR close?
Rationale for this change
This is an alternative to #24670 by @gene-bordegaray, opened as a draft to show the diff and CI for the smaller shape rather than to compete with it. The projection commit here is his work, taken verbatim.
Field metadata on a
ProjectionExec's output schema can silently disappear when the physical optimizer removes or rewrites projections, so query results lose declared field metadata and metadata-reading expressions return wrong answers. That is #24721, and @gene-bordegaray diagnosed it in three parts (identity-projection removal, collapsing across a metadata boundary, andmake_with_childrederiving the schema).Fixing those exposes a second, pre-existing bug. The logical
Expr::Cast/Expr::TryCastcarry aFieldReftarget so a cast can express a destination that is more than aDataType— for example an extension type produced by aTypePlanner.cast_output_fieldignored that field's metadata entirely and always inherited the source's, soExpr::to_field()disagreed with the physicalCastExpr, which already treats a non-synthesized target field as authoritative. The disagreement was masked because the optimizer rederives a projection's schema from its expressions, repairing the logical schema on the way through. Once projections preserve metadata faithfully, the repair stops and a cast to an extension type loses it:#24670 currently addresses that by introducing a
CastTargetenum (stacked from #24725), which changes the publicCast.field/TryCast.fieldtypes and fails the semver check, blocking a 55.1 backport. This PR takes the target's metadata when it carries any and otherwise inherits the source's, which resolves the same divergence with no API change.What changes are included in this PR?
Two commits, each green on its own:
preserve projection field metadata during physical optimization— @gene-bordegaray'sphysical-plan/src/projection.rsfrom fix: preserve projection metadata during optimization #24670, unmodified, applied to currentmain.use the cast target's metadata when it carries any— 20 lines inexpr/src/expr_schema.rs.No public types change, so
cargo-semver-checksstays clean.The rule is deliberately narrower than the three-part sentinel (
name.is_empty() && is_nullable && metadata.is_empty()) used by the physical cast and by #23169. That sentinel reclassifies a protobuf-roundtrippedCAST(x AS Utf8)withnullable = falseas an explicit target and drops its source metadata; an emptiness check cannot change behaviour for any cast whose target carries no metadata, which seems the safer property for a patch release.Deliberately not included, to keep this reviewable and backportable:
ARROW:extension:name/:metadatawhen a cast changes the storage type (Propagation of metadata through casts can strips extension type from the destination field and can result in invalid extension types #22079). Real bug, user-visible behaviour change, belongs on its own.to_protowritesfield.metadata()andfrom_protorebuilds fromarrow_type+nullableonly (Physical-plan serialization drops ProjectionExec metadata #24695), already tracked separately by @gene-bordegaray.CastTargetenum, the existing sentinel, or dropping source-metadata inheritance entirely is a real discussion, but it is a design question for the next major rather than a blocker on a correctness fix.What is the testing strategy for this PR?
The existing
datafusion/sqllogictest/test_files/cast_extension_type_metadata.sltis the regression test: with commit 1 alone it reproduces the CI failure @gabotechs reported on #24670, byte for byte and at the same line, and commit 2 makes it pass.Commit 2 also adds
expr_schema::tests::test_cast_output_field_metadata, covering both directions of the rule forCastandTryCast. Reverting the rule while keeping the test makes it fail, so it is load-bearing rather than decorative.Locally: the full sqllogictest suite (504/504 files) and the
datafusion-expr,-physical-expr,-physical-plan,-sql,-protoand-optimizerunit tests (5495 passed, 0 failed) are green.Are there any user-facing changes?
Yes, both are bug fixes to schemas that were already wrong:
to_field(), matching what the physical plan already produces. A plainCAST(expr AS type)synthesizes a target with no metadata, so its behaviour is unchanged.No breaking changes to public APIs.