Repository navigation
fix: propagate field metadata through coalesce, nvl, and nvl2 - #24865
james-willis wants to merge 9 commits into
Conversation
|
In this PR, if the branches of the coalesce, etc don't agree on the metadata, we drop extension metadata defensively. My opinion is that this should actually be a planning time error unless all rows will hit only one branch but wanted to take the more conservative approach for now. I want to solicit opinions from the reviewers on what the desried behavior is here. |
These functions built their return Field from argument data types only, dropping field metadata (e.g. Arrow extension types) from the planned schema. Attach the metadata shared by every value-contributing argument instead. Part of apache#24860.
82761e8 to
1ea88c0
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24865 +/- ##
==========================================
+ Coverage 82.49% 82.58% +0.09%
==========================================
Files 1140 1142 +2
Lines 439034 440994 +1960
Branches 439034 440994 +1960
==========================================
+ Hits 362165 364182 +2017
+ Misses 54962 54813 -149
- Partials 21907 21999 +92 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
paleolimbot
left a comment
There was a problem hiding this comment.
Thank you for this fix! I'm not a committer so I can't help with the CI but I'll review from a metadata perspective.
I think the family of functions that are affected here are ones that have >1 "compatible" (castable to a common type) inputs. While there are some extension aware type systems that can handle this (notably: vctrs via vec_ptype2() and vec_cast()), datafusion has no mechanism for that and so probably the best we can do for now is to propagate agressively and rely on engines to write analyzer or optimizer rules to enforce other behaviour.
One other place I've seen this pattern is combining values in a values literal clause. I would personally love if the logic were the same there but that might insert too much scope here.
datafusion/datafusion/expr/src/logical_plan/builder.rs
Lines 309 to 320 in d02bd78
I am curious what @timsaucer would expect to happen to metadata in CASE WHEN since that is a metadata usage I'm less familiar with!
…on-common Requiring the arguments to agree on metadata was too strict: extension types with JSON-encoded parameters can differ byte-wise, and unrelated metadata that arrived with the data should not drop an extension type from the result. Propagate the first non-empty metadata among the value-contributing arguments instead, and move the helper next to the related utilities in datafusion_common::metadata so the upcoming CASE fix in datafusion-expr can reuse it.
paleolimbot
left a comment
There was a problem hiding this comment.
I know you're still working here, just making sure I didn't forget to review!
Adopt the suggested compromise: merge metadata across the value-contributing arguments (earlier arguments win for repeated keys) instead of taking only the first non-empty metadata, but refuse differing values of 'ARROW:extension:name' with a planning error, keeping the extension name and its parameters paired. Also accept impl IntoIterator and inline the field construction in the datafusion-common tests.
|
@james-willis Can you address the merge conflicts? I can bring up this PR in the weekly sync tomorrow to get a second review if it's ready. |
# Conflicts: # datafusion/functions/src/core/nvl2.rs
Drop the field/ext_field/return_field test helpers in favor of Field::new().with_metadata() and direct return_field_from_args calls, matching the datafusion-common tests in this PR and the existing tests in with_metadata.rs.
paleolimbot
left a comment
There was a problem hiding this comment.
Thank you!
This would benefit from a second review to double check my take on metadata merging here, but in general I think this a standalone improvement and fixes a hard to work around bug.
| /// [`Expr::Cast`]: https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.Expr.html#variant.Cast | ||
| pub fn coerced_fields_metadata<'a>( |
There was a problem hiding this comment.
I recently discovered intersect_metadata_for_union(), which silently discards non-matching metadata keys. I like this version better (I want a louder error when the types don't match), but either is OK with me if we want to keep the existing logic for consistency.
| // Propagate field metadata (e.g. Arrow extension types) from the | ||
| // arguments that can supply a value; see `coerced_fields_metadata` | ||
| // for the merge semantics. | ||
| let metadata = | ||
| coerced_fields_metadata(args.arg_fields.iter().map(|f| f.as_ref()))?; | ||
| Ok(metadata | ||
| .add_to_field(Field::new(self.name(), return_type, nullable)) | ||
| .into()) |
There was a problem hiding this comment.
Happy to hear another opinion, but I do think this is a bug worth fixing regardless of whether #26145 is a good idea.
Which issue does this PR close?
CASEexpressions drop field metadata, silently stripping Arrow extension types and breaking metadata-aware UDFs #24860.Rationale for this change
COALESCE,NVL/IFNULL, andNVL2drop the field metadata of their arguments when computing their return field:return_field_from_argsbuilds the outputFieldfrom the argument data types only. For columns carrying Arrow extension types (ARROW:extension:name/ARROW:extension:metadata— e.g.arrow.uuid,geoarrow.wkb), the planned schema of any expression containing these functions silently loses the extension-type identity, and metadata-aware UDFs that dispatch on their argumentFields fail to plan over them (see #24860 for a full reproducer).This is one of several independent metadata drop sites listed in #24860. It is the plan-time schema fix for the conditional functions. These functions simplify into
CASE, whose logical and physical drop sites are addressed separately in #26145 and #26158.What changes are included in this PR?
datafusion_common::metadata::coerced_fields_metadata: computes the metadata for an expression whose result may come from any one of several inputs coerced to a common type. It lives next to the related metadata utilities so thatCASE(indatafusion-expr) can share it:Null(an untyped NULL literal carries no metadata and cannot contribute a typed value), with earlier inputs taking precedence for repeated keys. Unrelated keys are not required to agree, so metadata that arrived with the data (e.g. from an Arrow file) cannot cause an extension type to be dropped.Expr::Castoutput field derivation: differing values ofARROW:extension:nameare a planning error, andARROW:extension:metadataalways comes from the input that supplied the extension name (byte-wise differences in it are tolerated, since extension parameters are often JSON).coalesce:return_field_from_argsattaches the merged metadata of its arguments. Return type and nullability are unchanged.NVL/IFNULLdelegate tocoalesceand are fixed by the same change.nvl2: same, over the second and third arguments only — the first argument is only tested for NULL and never contributes a value, so its metadata is excluded.What is the testing strategy for this PR?
coerced_fields_metadatainmetadata.rs: merge precedence, untyped NULL inputs skipped, empty result, error on differing extension names, and extension name/metadata kept paired.coalesce.rsandnvl2.rsexercisereturn_field_from_args: metadata propagated, untyped NULL arguments not blocking propagation, conflicting extension types rejected, NVL2's test argument excluded, and nullability unchanged.There is deliberately no sqllogictest yet:
arrow_metadata()reads its argument field at execution time, and by then these functions have been simplified intoCASE, so end-to-end coverage depends on theCASEfixes (#26145, #26158). This ordering is safe: the optimizer's schema invariant ignores metadata (assert_expected_schema/logically_equivalent_names_and_types), so a plan whosecoalescecarries metadata that the simplifiedCASEdoes not still plans.Are there any user-facing changes?
DataFrame::schema()) of expressions containingCOALESCE/NVL/IFNULL/NVL2over metadata-bearing columns now preserves that metadata.COALESCE/NVL/IFNULL/NVL2over arguments with differingARROW:extension:namevalues (e.g. anarrow.uuidcolumn and anarrow.jsoncolumn) is now a planning error. Previously these planned and silently dropped the extension type.datafusion_common::metadata::coerced_fields_metadata. No other API changes.