Skip to content

Align scalar UDF literal args with coerced return fields - #23232

Open
yinli-systems wants to merge 6 commits into
apache:mainfrom
yinli-systems:kevin/return-field-scalar-arg-types
Open

Align scalar UDF literal args with coerced return fields#23232
yinli-systems wants to merge 6 commits into
apache:mainfrom
yinli-systems:kevin/return-field-scalar-arg-types

Conversation

@yinli-systems

@yinli-systems yinli-systems commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

ReturnFieldArgs exposes both coerced arg_fields and optional scalar literal arguments. A coercible UDF could receive an Int16 field but an original Int64 literal, or lose the literal behind an implicit cast. That makes return-field decisions inconsistent and forced UDF implementations to compensate for planner internals.

What changes are included in this PR?

  • Materialize successful implicit casts of scalar-function literals during type coercion, so literal values and arg_fields always have the same type.
  • Keep user-written Cast and TryCast expressions as expressions rather than reporting them as literals.
  • Preserve execution-time errors for invalid literal casts.
  • Simplify return-field scalar extraction to accept only genuine post-coercion literals.
  • Update round to use the strengthened contract and document it on ReturnFieldArgs.
  • Add unit and integration regressions for coerced integers, typed nulls, list literals, invalid values, explicit casts, expression arguments, and arrow_cast's required literal type argument.

This follows the approach discussed in #20012: materialize implicit literal coercion in the type-coercion pass without changing explicit-cast semantics.

Are these changes tested?

  • cargo fmt --all --check
  • cargo test -p datafusion --test user_defined_integration test_return_field_args_scalar_argument_types_match_arg_fields (1 passed)
  • cargo test -p datafusion --test core_integration test_fn_arrow_cast_requires_literal_type_arg (1 passed)
  • cargo test -p datafusion-expr scalar_arguments_ (2 passed)
  • cargo clippy --all-targets --all-features -- -D warnings
  • git diff --check

The validation process caught and fixed two semantic regressions before push: explicit casts being folded into scalar literals, and arrow_cast reporting its existing error during planning rather than collection.

AI assistance

AI tooling assisted with implementation and test iteration. I reviewed the final diff, the related #20012 design discussion, and the scalar-function coercion/return-field flow end to end, and ran the validations listed above.

Signed-off-by: Kevin-Li-2025 <2242139@qq.com>
@github-actions github-actions Bot added logical-expr Logical plan and expressions core Core DataFusion crate labels Jun 29, 2026
@yinli-systems
yinli-systems marked this pull request as ready for review June 29, 2026 07:00
@Jefffrey

Jefffrey commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

This seems to follow a similar approach to a previous PR that attempted this issue:

There was some good discussion on there, perhaps can double check that since we found some difficulties with such an approach

@yinli-systems

Copy link
Copy Markdown
Contributor Author

Thanks for pointing me back to #20012. I went through that discussion and tightened this PR so it does not take the broad approach that caused the earlier test churn.

The latest commit narrows scalar literal recovery to the implicit-cast shape inserted by type coercion, Cast(Literal -> expected arg type). It deliberately does not recursively unwrap user-written casts/try_casts, so cases such as arrow_cast(1, cast('Utf8' as varchar)) still fail as before instead of treating the cast expression as a constant type argument. It also preserves the original scalar value when the planning-time scalar cast fails, so builtins that validate literal values themselves keep their existing error path instead of surfacing an early Arrow cast error.

Local checks run:

  • cargo test -p datafusion --test user_defined_integration test_return_field_args_scalar_argument_types_match_arg_fields
  • cargo test -p datafusion --test core_integration dataframe::dataframe_functions::test_fn_arrow_cast_requires_literal_type_arg
  • cargo run -p datafusion-examples --example builtin_functions -- date_time
  • git diff --check

New CI is now queued on head 053b1c0e8.

@github-actions github-actions Bot added optimizer Optimizer rules functions Changes to functions implementation labels Jul 23, 2026

@Jefffrey Jefffrey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like some tests are affected 🤔

Comment on lines +1139 to +1142
// A failed value cast remains an expression cast so execution produces the
// same error as before. Since it is no longer a literal at the coerced type,
// it is reported as `None` in `ReturnFieldArgs::scalar_arguments`.
Expr::Literal(value, metadata).cast_to(data_type, schema)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this to maintain backwards compatibility?

}

#[tokio::test]
async fn test_fn_arrow_cast_requires_literal_type_arg() -> Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can probably just put this as an SLT which is less verbose

Comment on lines +2132 to +2134
if let Some(scalar) = args.scalar_arguments[0] {
assert_eq!(args.arg_fields[0].data_type(), &scalar.data_type());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if let Some(scalar) = args.scalar_arguments[0] {
assert_eq!(args.arg_fields[0].data_type(), &scalar.data_type());
}
assert_eq!(args.arg_fields[0].data_type(), &args.scalar_arguments[0].unwrap().data_type());

nit: we already assert it is_some() above


fn scalar_argument_for_field(expr: &Expr, arg_field: &FieldRef) -> Option<ScalarValue> {
match expr {
Expr::Literal(sv, _) => sv.cast_to(arg_field.data_type()).ok(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suppose this is an easier way to ensure proper type without a wider refactor 🤔

Comment thread datafusion/functions/src/math/round.rs Outdated
// Note: `scalar_arguments` contains the original literal values (pre-coercion), so
// integer literals may appear as Int64 even though the signature coerces them to Int32.
// `scalar_arguments` uses the coerced argument type, so an integer literal
// is Int32 here when the signature coerces it to Int32.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can just remove the note here if we fix this behaviour, no need to explain it again (since scalar_arguments docstring should explain it)

Comment thread datafusion/expr/src/udf.rs Outdated
///
/// When present, the scalar value has the same data type as the corresponding
/// entry in [`Self::arg_fields`], including after implicit type coercion.
/// User-written `Cast` and `TryCast` expressions are not scalar arguments.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// User-written `Cast` and `TryCast` expressions are not scalar arguments.

this just seems confusing, can remove it

@github-actions github-actions Bot added the sqllogictest SQL Logic Tests (.slt) label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Core DataFusion crate functions Changes to functions implementation logical-expr Logical plan and expressions optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ReturnFieldArgs.scalar_arguments type doesn't match with arg_fields

2 participants