@@ -11,13 +11,15 @@ use std::{
1111 hash:: { Hash , Hasher } ,
1212} ;
1313
14- use crate :: expr:: Sort ;
14+ use crate :: expr:: { Exists , InSubquery , SetComparison , Sort } ;
1515#[ cfg( not( feature = "sql" ) ) ]
1616use crate :: sql:: Ident ;
1717use arrow:: datatypes:: DataType ;
18- use datafusion_common:: tree_node:: { Transformed , TreeNodeContainer , TreeNodeRecursion } ;
18+ use datafusion_common:: tree_node:: {
19+ Transformed , TreeNode , TreeNodeContainer , TreeNodeRecursion ,
20+ } ;
1921use datafusion_common:: {
20- Constraints , DFSchema , DFSchemaRef , Result , SchemaReference , TableReference ,
22+ Constraints , DFSchema , DFSchemaRef , Result , SchemaReference , TableReference , plan_err ,
2123} ;
2224#[ cfg( feature = "sql" ) ]
2325use sqlparser:: ast:: Ident ;
@@ -904,6 +906,95 @@ pub struct CreateFunction {
904906 pub schema : DFSchemaRef ,
905907}
906908
909+ impl CreateFunction {
910+ /// Creates a function definition, validating placeholders in its body and
911+ /// argument defaults, including expressions inside subqueries.
912+ ///
913+ /// Placeholders must be positional (`$1`, `$2`, ...) and reference a
914+ /// declared argument. SQL argument names must be resolved before calling
915+ /// this constructor.
916+ pub fn try_new (
917+ or_replace : bool ,
918+ temporary : bool ,
919+ name : String ,
920+ args : Option < Vec < OperateFunctionArg > > ,
921+ return_type : Option < DataType > ,
922+ params : CreateFunctionBody ,
923+ ) -> Result < Self > {
924+ let arg_count = args. as_ref ( ) . map_or ( 0 , |declared| declared. len ( ) ) ;
925+ for default_expr in args
926+ . iter ( )
927+ . flatten ( )
928+ . filter_map ( |arg| arg. default_expr . as_ref ( ) )
929+ {
930+ validate_function_body_placeholders ( default_expr, arg_count) ?;
931+ }
932+ if let Some ( body) = & params. function_body {
933+ validate_function_body_placeholders ( body, arg_count) ?;
934+ }
935+ Ok ( Self {
936+ or_replace,
937+ temporary,
938+ name,
939+ args,
940+ return_type,
941+ params,
942+ schema : DFSchemaRef :: new ( DFSchema :: empty ( ) ) ,
943+ } )
944+ }
945+ }
946+
947+ /// Rejects placeholders in a `CREATE FUNCTION` body or argument default
948+ /// expression that do not reference a declared argument (e.g. `$3` for a
949+ /// two-argument function) at definition time, instead of deferring the error
950+ /// to function invocation.
951+ fn validate_function_body_placeholders ( expr : & Expr , arg_count : usize ) -> Result < ( ) > {
952+ expr. apply ( |expr| {
953+ if let Expr :: Placeholder ( placeholder) = expr {
954+ match placeholder
955+ . id
956+ . strip_prefix ( '$' )
957+ . and_then ( |id| id. parse :: < usize > ( ) . ok ( ) )
958+ {
959+ // In range, e.g. `$2` with two declared arguments
960+ Some ( idx) if ( 1 ..=arg_count) . contains ( & idx) => { }
961+ // Out of range, e.g. `$3` with two declared arguments
962+ Some ( _) => {
963+ return plan_err ! (
964+ "Invalid placeholder, out of range: {}" ,
965+ placeholder. id
966+ ) ;
967+ }
968+ // Named or malformed placeholder
969+ None => {
970+ return plan_err ! ( "Unknown placeholder: {}" , placeholder. id) ;
971+ }
972+ }
973+ }
974+ // `Expr::apply` does not descend into subqueries, so walk their
975+ // plans explicitly to validate placeholders inside them
976+ if let Some ( subquery) = match expr {
977+ Expr :: ScalarSubquery ( subquery) => Some ( & subquery. subquery ) ,
978+ Expr :: Exists ( Exists { subquery, .. } ) => Some ( & subquery. subquery ) ,
979+ Expr :: InSubquery ( InSubquery { subquery, .. } ) => Some ( & subquery. subquery ) ,
980+ Expr :: SetComparison ( SetComparison { subquery, .. } ) => {
981+ Some ( & subquery. subquery )
982+ }
983+ _ => None ,
984+ } {
985+ // Nested subqueries are handled by this expression visitor
986+ subquery. apply ( |plan| {
987+ plan. apply_expressions ( |e| {
988+ validate_function_body_placeholders ( e, arg_count) ?;
989+ Ok ( TreeNodeRecursion :: Continue )
990+ } )
991+ } ) ?;
992+ }
993+ Ok ( TreeNodeRecursion :: Continue )
994+ } ) ?;
995+ Ok ( ( ) )
996+ }
997+
907998// Manual implementation needed because of `schema` field. Comparison excludes this field.
908999impl PartialOrd for CreateFunction {
9091000 fn partial_cmp ( & self , other : & Self ) -> Option < Ordering > {
@@ -1074,13 +1165,69 @@ impl PartialOrd for CreateIndex {
10741165
10751166#[ cfg( test) ]
10761167mod test {
1168+ use super :: { CreateFunction , CreateFunctionBody , OperateFunctionArg } ;
1169+ use crate :: expr_fn:: placeholder;
10771170 use crate :: {
1078- CreateCatalog , CreateExternalCatalog , DdlStatement , DropCatalog , DropView ,
1171+ CreateCatalog , CreateExternalCatalog , DdlStatement , DropCatalog , DropView , Expr ,
1172+ LogicalPlanBuilder , Subquery ,
10791173 } ;
1174+ use arrow:: datatypes:: DataType ;
10801175 use datafusion_common:: { DFSchema , DFSchemaRef , TableReference } ;
1081- use std:: cmp:: Ordering ;
1082- use std:: collections:: HashMap ;
1083- use std:: sync:: Arc ;
1176+ use std:: { cmp:: Ordering , collections:: HashMap , sync:: Arc } ;
1177+
1178+ #[ test]
1179+ fn create_function_try_new_validates_placeholders ( ) {
1180+ let create = |body, default_expr| {
1181+ CreateFunction :: try_new (
1182+ false ,
1183+ false ,
1184+ "f" . to_string ( ) ,
1185+ Some ( vec ! [ OperateFunctionArg {
1186+ name: None ,
1187+ data_type: DataType :: Int64 ,
1188+ default_expr,
1189+ } ] ) ,
1190+ Some ( DataType :: Int64 ) ,
1191+ CreateFunctionBody {
1192+ language : None ,
1193+ behavior : None ,
1194+ function_body : body,
1195+ } ,
1196+ )
1197+ } ;
1198+
1199+ assert ! ( create( Some ( placeholder( "$1" ) ) , Some ( placeholder( "$1" ) ) ) . is_ok( ) ) ;
1200+ assert ! ( create( None , None ) . is_ok( ) ) ;
1201+ for ( body, default_expr) in [
1202+ ( Some ( placeholder ( "$2" ) ) , None ) ,
1203+ ( None , Some ( placeholder ( "$2" ) ) ) ,
1204+ ] {
1205+ let err = create ( body, default_expr) . unwrap_err ( ) ;
1206+ assert_eq ! (
1207+ err. strip_backtrace( ) ,
1208+ "Error during planning: Invalid placeholder, out of range: $2"
1209+ ) ;
1210+ }
1211+
1212+ let mut nested_body = placeholder ( "$2" ) ;
1213+ for _ in 0 ..16 {
1214+ let plan = LogicalPlanBuilder :: empty ( true )
1215+ . project ( vec ! [ nested_body. alias( "v" ) ] )
1216+ . unwrap ( )
1217+ . build ( )
1218+ . unwrap ( ) ;
1219+ nested_body = Expr :: ScalarSubquery ( Subquery {
1220+ subquery : Arc :: new ( plan) ,
1221+ outer_ref_columns : vec ! [ ] ,
1222+ spans : Default :: default ( ) ,
1223+ } ) ;
1224+ }
1225+ let err = create ( Some ( nested_body) , None ) . unwrap_err ( ) ;
1226+ assert_eq ! (
1227+ err. strip_backtrace( ) ,
1228+ "Error during planning: Invalid placeholder, out of range: $2"
1229+ ) ;
1230+ }
10841231
10851232 #[ test]
10861233 fn test_partial_ord ( ) {
0 commit comments