Skip to content

feat: keep extension types of CREATE FUNCTION arguments and return type - #27102

Open
cblp wants to merge 1 commit into
apache:mainfrom
cblp:create-function-field-types
Open

cblp wants to merge 1 commit into
apache:mainfrom
cblp:create-function-field-types

Conversation

@cblp

@cblp cblp commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

CREATE FUNCTION drops the metadata of argument and return types, so a FunctionFactory cannot tell an extension type from its storage type. For example, for

CREATE FUNCTION same_uuid(id UUID) RETURNS UUID RETURN $1

the factory receives FixedSizeBinary(16) for the argument and the return type, without ARROW:extension:name = arrow.uuid. The placeholder $1 in the body also has the type FixedSizeBinary(16).

The SQL planner already resolves these types to fields with metadata (convert_data_type_to_field), but CreateFunction stores them as DataTypes, so the planner kept only the data type.

What changes are included in this PR?

  • OperateFunctionArg::data_type: DataType is replaced by OperateFunctionArg::field: FieldRef, and CreateFunction::return_type: Option<DataType> by CreateFunction::return_field: Option<FieldRef>. This follows how Placeholder (Enable placeholders with extension types #17986) and ScalarVariable (Allow Logical expression ScalarVariable to represent an extension type or metadata #18243) moved from DataType to FieldRef. CreateFunction::try_new takes the return type as Option<FieldRef>.
  • The SQL planner keeps the fields it resolves. The argument field is named after the argument, or "" if it has no name. Before, the planner built the fields for the placeholders in the body from the data types, with the same names. Now it reuses the argument fields, so the placeholders keep the metadata too.
  • The function_factory example and the existing FunctionFactory tests read the data type from the fields.
  • An entry in the 56.0.0 upgrade guide.

What is the testing strategy for this PR?

create_function_keeps_extension_types in datafusion/core/tests/user_defined/user_defined_scalar_functions.rs plans CREATE FUNCTION same_uuid(id UUID) RETURNS UUID RETURN $1 with a recording FunctionFactory. It checks that the argument field, the return field and the placeholder in the body all have the UUID extension type. The test fails if the planner drops the metadata.

This is a Rust test rather than a sqllogictest because DataFusion has no built-in FunctionFactory, so what CREATE FUNCTION passes to it cannot be observed from SQL.

The existing CREATE FUNCTION tests are updated for the new fields.

Are there any user-facing changes?

Yes, this is a breaking API change, so it needs the api change label:

  • OperateFunctionArg::data_type is renamed to field and is now a FieldRef;
  • CreateFunction::return_type is renamed to return_field and is now an Option<FieldRef>.

FunctionFactory implementations that read these fields need to call .data_type() on them, or can use the fields to support extension types. The upgrade guide describes the migration.

There is no change in SQL behavior without a custom FunctionFactory.

`CREATE FUNCTION` stored its argument and return types as `DataType`s, so the
SQL planner dropped the field metadata of types such as `UUID`, and a
`FunctionFactory` could not tell an extension type from its storage type. The
placeholders in the function body lost the metadata too.

Replace `OperateFunctionArg::data_type` with `field: FieldRef` and
`CreateFunction::return_type` with `return_field: Option<FieldRef>`, and keep
the fields resolved by the planner. Placeholders in the body use the argument
fields.

Closes apache#18235.
@github-actions github-actions Bot added documentation Improvements or additions to documentation sql SQL Planner logical-expr Logical plan and expressions core Core DataFusion crate labels Nov 4, 2026

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This would benefit from a second look from somebody more familiar with SQL functions than I am, but this seems reasonable to me.

The open question is how hard we want to try to actually match the input based on the input fields. My sense is that it's not worth it for this PR but I'm happy to hear other opinions.

Comment on lines +1345 to +1348
field: Arc::new(Field::new("name", DataType::Utf8View, true)),
default_expr: None,
}]),
return_type: Some(DataType::Int32),
return_field: Some(Arc::new(Field::new("", DataType::Int32, true))),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we use into_nullable_field_ref() here?

Comment on lines 1018 to +1022
type_signatures.push(TypeSignature::Exact(
args.iter().take(n).map(|a| a.data_type.clone()).collect(),
args.iter()
.take(n)
.map(|a| a.field.data_type().clone())
.collect(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The Signature has no way to match an extension type here and so I think this is OK (the alternative would be to use Any and enforce in return_field()).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Core DataFusion crate documentation Improvements or additions to documentation logical-expr Logical plan and expressions sql SQL Planner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CreateFunction statement can't represent input or output types that require metadata

2 participants