From 08491beb76162e0918ccaaf62ecec5df2c7c335f Mon Sep 17 00:00:00 2001 From: Mohamed Date: Tue, 19 May 2026 01:21:58 +0300 Subject: [PATCH] MDEV-38329 Named parameters in invocation of stored routines Add support for named parameter syntax in stored routine calls, allowing callers to specify arguments by name and in any order. Stored procedures use the => syntax: CALL proc(a => 1, b => 2); CALL proc(c => 3, a => 1, b => 2); CALL proc(1, b => 2, c => 3); Stored functions use the existing AS alias syntax: SELECT func(1 AS a, 2 AS b); SELECT func(3 AS c, 1 AS a, 2 AS b); Parameters with default values can be omitted: CALL proc(a => 1); -- b,c use defaults SELECT func(1 AS a, 3 AS c); -- b uses default Parser: added sp_cparam rule in sql_yacc.yy to accept ident ARROW_SYM expr in CALL argument lists. Named args set IS_EXPLICIT_NAME and store the name in Item::name, reusing the UDF named argument mechanism. Positional args after named args are rejected at parse time. Reordering for procedures: in sp_head::execute_procedure(), named arguments are matched against sp_pcontext formal parameters and reordered to declared positions. Omitted params with defaults are filled from sp_variable::default_value. Reordering for functions: in Item_func_sp::fix_fields(), after resolving the sp_head, named arguments (marked via IS_EXPLICIT_NAME by the AS alias syntax) are matched and reordered the same way. The has_named_parameters() rejection in Create_sp_func::create_with_db() is removed. Error handling: - Unknown parameter name: ER_SP_UNDECLARED_VAR - Duplicate parameter name: ER_SP_DUP_PARAM - Missing required parameter: ER_SP_WRONG_NO_OF_ARGS --- mysql-test/main/sp_named_params.result | 120 ++++++++++++++++++++++ mysql-test/main/sp_named_params.test | 131 +++++++++++++++++++++++++ sql/item_create.cc | 15 --- sql/item_func.cc | 71 ++++++++++++++ sql/sp_head.cc | 84 +++++++++++++++- sql/sql_lex.cc | 2 + sql/sql_lex.h | 1 + sql/sql_yacc.yy | 24 ++++- 8 files changed, 429 insertions(+), 19 deletions(-) create mode 100644 mysql-test/main/sp_named_params.result create mode 100644 mysql-test/main/sp_named_params.test diff --git a/mysql-test/main/sp_named_params.result b/mysql-test/main/sp_named_params.result new file mode 100644 index 0000000000000..d34a1c823352e --- /dev/null +++ b/mysql-test/main/sp_named_params.result @@ -0,0 +1,120 @@ +# +# MDEV-38329: Named Parameters in Invocation of Stored Routines +# +# Test setup +CREATE PROCEDURE p1(a INT, b INT, c INT) +BEGIN +SELECT a, b, c; +END; +$$ +# All positional (existing behavior) +CALL p1(1, 2, 3); +a b c +1 2 3 +# All named +CALL p1(a => 1, b => 2, c => 3); +a b c +1 2 3 +# Named in different order +CALL p1(c => 3, a => 1, b => 2); +a b c +1 2 3 +# Mixed positional and named +CALL p1(1, b => 2, c => 3); +a b c +1 2 3 +# Mixed: first two positional, last named +CALL p1(1, 2, c => 3); +a b c +1 2 3 +# Positional after named should fail +CALL p1(a => 1, 2, 3); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ' 3)' at line 1 +# Unknown parameter name +CALL p1(a => 1, b => 2, x => 3); +ERROR 42000: Undeclared variable: x +# Duplicate parameter name +CALL p1(a => 1, a => 2, b => 3); +ERROR 42000: Duplicate parameter: a +# Positional fills 'a', then 'a' again by name +CALL p1(1, a => 2, c => 3); +ERROR 42000: Duplicate parameter: a +DROP PROCEDURE p1; +# +# Test with default values +# +CREATE PROCEDURE p2(a INT, b INT DEFAULT 20, c INT DEFAULT 30) +BEGIN +SELECT a, b, c; +END; +$$ +# Skip middle param (use default for b) +CALL p2(a => 1, c => 3); +a b c +1 20 3 +# Only required param +CALL p2(a => 1); +a b c +1 20 30 +# All named with defaults overridden +CALL p2(a => 10, b => 20, c => 30); +a b c +10 20 30 +# Missing required param should fail +CALL p2(b => 2, c => 3); +ERROR 42000: Incorrect number of arguments for PROCEDURE test.p2; expected 3, got 2 +DROP PROCEDURE p2; +# +# Stored functions with named parameters via AS syntax +# +CREATE FUNCTION f1(a INT, b INT, c INT) RETURNS INT +BEGIN +RETURN a * 10000 + b * 100 + c; +END; +$$ +# Positional function call +SELECT f1(1, 2, 3); +f1(1, 2, 3) +10203 +# All named via AS +SELECT f1(1 AS a, 2 AS b, 3 AS c); +f1(1 AS a, 2 AS b, 3 AS c) +10203 +# Named in different order via AS +SELECT f1(3 AS c, 1 AS a, 2 AS b); +f1(3 AS c, 1 AS a, 2 AS b) +10203 +# Mixed positional and named +SELECT f1(1, 2 AS b, 3 AS c); +f1(1, 2 AS b, 3 AS c) +10203 +# Unknown parameter name +SELECT f1(1 AS a, 2 AS b, 3 AS x); +ERROR 42000: Undeclared variable: x +# Duplicate parameter name +SELECT f1(1 AS a, 2 AS a, 3 AS b); +ERROR 42000: Duplicate parameter: a +DROP FUNCTION f1; +# +# Stored function with default values +# +CREATE FUNCTION f2(a INT, b INT DEFAULT 20, c INT DEFAULT 30) RETURNS INT +BEGIN +RETURN a * 10000 + b * 100 + c; +END; +$$ +# Skip middle param (use default for b) +SELECT f2(1 AS a, 3 AS c); +f2(1 AS a, 3 AS c) +12003 +# Only required param +SELECT f2(1 AS a); +f2(1 AS a) +12030 +# Missing required param should fail +SELECT f2(2 AS b, 3 AS c); +ERROR 42000: Incorrect number of arguments for FUNCTION test.f2; expected 3, got 2 +DROP FUNCTION f2; +# +# End of tests +# diff --git a/mysql-test/main/sp_named_params.test b/mysql-test/main/sp_named_params.test new file mode 100644 index 0000000000000..add3ac08abfd5 --- /dev/null +++ b/mysql-test/main/sp_named_params.test @@ -0,0 +1,131 @@ +--echo # +--echo # MDEV-38329: Named Parameters in Invocation of Stored Routines +--echo # + +--echo # Test setup +delimiter $$; +CREATE PROCEDURE p1(a INT, b INT, c INT) +BEGIN + SELECT a, b, c; +END; +$$ +delimiter ;$$ + +--echo # All positional (existing behavior) +CALL p1(1, 2, 3); + +--echo # All named +CALL p1(a => 1, b => 2, c => 3); + +--echo # Named in different order +CALL p1(c => 3, a => 1, b => 2); + +--echo # Mixed positional and named +CALL p1(1, b => 2, c => 3); + +--echo # Mixed: first two positional, last named +CALL p1(1, 2, c => 3); + +--echo # Positional after named should fail +--error ER_PARSE_ERROR +CALL p1(a => 1, 2, 3); + +--echo # Unknown parameter name +--error ER_SP_UNDECLARED_VAR +CALL p1(a => 1, b => 2, x => 3); + +--echo # Duplicate parameter name +--error ER_SP_DUP_PARAM +CALL p1(a => 1, a => 2, b => 3); + +--echo # Positional fills 'a', then 'a' again by name +--error ER_SP_DUP_PARAM +CALL p1(1, a => 2, c => 3); + +DROP PROCEDURE p1; + +--echo # +--echo # Test with default values +--echo # +delimiter $$; +CREATE PROCEDURE p2(a INT, b INT DEFAULT 20, c INT DEFAULT 30) +BEGIN + SELECT a, b, c; +END; +$$ +delimiter ;$$ + +--echo # Skip middle param (use default for b) +CALL p2(a => 1, c => 3); + +--echo # Only required param +CALL p2(a => 1); + +--echo # All named with defaults overridden +CALL p2(a => 10, b => 20, c => 30); + +--echo # Missing required param should fail +--error ER_SP_WRONG_NO_OF_ARGS +CALL p2(b => 2, c => 3); + +DROP PROCEDURE p2; + +--echo # +--echo # Stored functions with named parameters via AS syntax +--echo # +delimiter $$; +CREATE FUNCTION f1(a INT, b INT, c INT) RETURNS INT +BEGIN + RETURN a * 10000 + b * 100 + c; +END; +$$ +delimiter ;$$ + +--echo # Positional function call +SELECT f1(1, 2, 3); + +--echo # All named via AS +SELECT f1(1 AS a, 2 AS b, 3 AS c); + +--echo # Named in different order via AS +SELECT f1(3 AS c, 1 AS a, 2 AS b); + +--echo # Mixed positional and named +SELECT f1(1, 2 AS b, 3 AS c); + +--echo # Unknown parameter name +--error ER_SP_UNDECLARED_VAR +SELECT f1(1 AS a, 2 AS b, 3 AS x); + +--echo # Duplicate parameter name +--error ER_SP_DUP_PARAM +SELECT f1(1 AS a, 2 AS a, 3 AS b); + +DROP FUNCTION f1; + +--echo # +--echo # Stored function with default values +--echo # +delimiter $$; +CREATE FUNCTION f2(a INT, b INT DEFAULT 20, c INT DEFAULT 30) RETURNS INT +BEGIN + RETURN a * 10000 + b * 100 + c; +END; +$$ +delimiter ;$$ + +--echo # Skip middle param (use default for b) +SELECT f2(1 AS a, 3 AS c); + +--echo # Only required param +SELECT f2(1 AS a); + +--echo # Missing required param should fail +--error ER_SP_WRONG_NO_OF_ARGS +SELECT f2(2 AS b, 3 AS c); + +DROP FUNCTION f2; + +--echo # +--echo # End of tests +--echo # diff --git a/sql/item_create.cc b/sql/item_create.cc index f2716e643668a..b3c2339611bf0 100644 --- a/sql/item_create.cc +++ b/sql/item_create.cc @@ -2996,21 +2996,6 @@ Create_sp_func::create_with_db(THD *thd, const Sp_handler *sph= &sp_handler_function; Database_qualified_name pkgname; - if (unlikely(has_named_parameters(item_list))) - { - /* - The syntax "db.foo(expr AS p1, expr AS p2, ...) is invalid, - and has been rejected during syntactic parsing already, - because a stored function call may not have named parameters. - - The syntax "foo(expr AS p1, expr AS p2, ...)" is correct, - because it can refer to a User Defined Function call. - For a Stored Function however, this has no semantic. - */ - my_error(ER_WRONG_PARAMETERS_TO_STORED_FCT, MYF(0), name.str); - return NULL; - } - if (item_list != NULL) arg_count= item_list->elements; diff --git a/sql/item_func.cc b/sql/item_func.cc index 594a98b3e3138..0d3e459ddae0e 100644 --- a/sql/item_func.cc +++ b/sql/item_func.cc @@ -6884,6 +6884,77 @@ Item_func_sp::fix_fields(THD *thd, Item **ref) DBUG_RETURN(TRUE); } + if (arg_count && args[0]->is_explicit_name()) + { + sp_pcontext *pcont= m_sp->get_parse_context(); + uint params= pcont->context_var_count(); + Item **arg_array= (Item**) thd->calloc(sizeof(Item*) * params); + bool *param_assigned= (bool*) thd->calloc(sizeof(bool) * params); + if (!arg_array || !param_assigned) + DBUG_RETURN(TRUE); + + uint positional_count= 0; + for (uint i= 0; i < arg_count; i++) + { + Item *item= args[i]; + if (item->is_explicit_name()) + { + bool found= false; + for (uint j= 0; j < params; j++) + { + sp_variable *spvar= pcont->get_context_variable(j); + if (spvar->name.streq(item->name)) + { + if (param_assigned[j]) + { + my_error(ER_SP_DUP_PARAM, MYF(0), item->name.str); + DBUG_RETURN(TRUE); + } + arg_array[j]= item; + param_assigned[j]= true; + found= true; + break; + } + } + if (!found) + { + my_error(ER_SP_UNDECLARED_VAR, MYF(0), item->name.str); + DBUG_RETURN(TRUE); + } + } + else + { + if (positional_count >= params) + { + my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "FUNCTION", + ErrConvDQName(m_sp).ptr(), params, arg_count); + DBUG_RETURN(TRUE); + } + arg_array[positional_count]= item; + param_assigned[positional_count]= true; + positional_count++; + } + } + + for (uint j= 0; j < params; j++) + { + if (!param_assigned[j]) + { + sp_variable *spvar= pcont->get_context_variable(j); + if (!spvar->default_value) + { + my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "FUNCTION", + ErrConvDQName(m_sp).ptr(), params, arg_count); + DBUG_RETURN(TRUE); + } + arg_array[j]= spvar->default_value; + } + } + + args= arg_array; + arg_count= params; + } + Query_arena *arena, backup; /* Allocation an instance of Item_func_sp used for initialization of diff --git a/sql/sp_head.cc b/sql/sp_head.cc index 5d52b7f109dac..6b926c77250fb 100644 --- a/sql/sp_head.cc +++ b/sql/sp_head.cc @@ -2177,14 +2177,94 @@ sp_head::execute_procedure(THD *thd, List *args) if (m_parent && m_parent->instantiate_if_needed(thd)) DBUG_RETURN(true); - if (args->elements < (params - default_params) || - args->elements > params) + if (!thd->lex->has_named_call_param && + (args->elements < (params - default_params) || + args->elements > params)) { my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "PROCEDURE", ErrConvDQName(this).ptr(), params, args->elements); DBUG_RETURN(TRUE); } + /* Reorder named arguments to match formal parameter positions */ + List reordered_args; + if (thd->lex->has_named_call_param) + { + Item **arg_array= (Item**) thd->calloc(sizeof(Item*) * params); + bool *param_assigned= (bool*) thd->calloc(sizeof(bool) * params); + if (!arg_array || !param_assigned) + DBUG_RETURN(TRUE); + + List_iterator it(*args); + uint positional_count= 0; + Item *item; + + while ((item= it++)) + { + if (item->is_explicit_name()) + { + bool found= false; + for (uint j= 0; j < params; j++) + { + sp_variable *spvar= m_pcont->get_context_variable(j); + if (spvar->name.streq(item->name)) + { + if (param_assigned[j]) + { + my_error(ER_SP_DUP_PARAM, MYF(0), item->name.str); + DBUG_RETURN(TRUE); + } + arg_array[j]= item; + param_assigned[j]= true; + found= true; + break; + } + } + if (!found) + { + my_error(ER_SP_UNDECLARED_VAR, MYF(0), item->name.str); + DBUG_RETURN(TRUE); + } + } + else + { + /* + Positional args come before named args (enforced by the parser), + so the slot at positional_count is always free. Guard only + against too many positional args overflowing the params array. + */ + if (positional_count >= params) + { + my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "PROCEDURE", + ErrConvDQName(this).ptr(), params, args->elements); + DBUG_RETURN(TRUE); + } + arg_array[positional_count]= item; + param_assigned[positional_count]= true; + positional_count++; + } + } + + for (uint j= 0; j < params; j++) + { + if (!param_assigned[j]) + { + sp_variable *spvar= m_pcont->get_context_variable(j); + if (!spvar->default_value) + { + my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "PROCEDURE", + ErrConvDQName(this).ptr(), params, args->elements); + DBUG_RETURN(TRUE); + } + arg_array[j]= spvar->default_value; + } + } + + for (uint j= 0; j < params; j++) + reordered_args.push_back(arg_array[j], thd->mem_root); + args= &reordered_args; + } + save_spcont= octx= thd->spcont; if (! octx) { diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 18f1ca7bb11e6..d9ebfa09facf1 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -10398,6 +10398,7 @@ bool LEX::call_statement_start(THD *thd, sp_name *name) const Sp_handler *sph= &sp_handler_procedure; sql_command= SQLCOM_CALL; value_list.empty(); + has_named_call_param= false; thd->variables.path.resolve(thd, sphead, name, &sph, &pkgname); @@ -10437,6 +10438,7 @@ bool LEX::call_statement_start(THD *thd, Identifier_chain2 q_pkg_proc(*pkg, *proc); sp_name *spname; value_list.empty(); + has_named_call_param= false; sql_command= SQLCOM_CALL; const Lex_ident_db_normalized dbn= thd->to_ident_db_normalized_with_error(*db); diff --git a/sql/sql_lex.h b/sql/sql_lex.h index e86650716896d..6da145f8c9f21 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -3441,6 +3441,7 @@ struct LEX: public Query_tables_list bool verbose:1, no_write_to_binlog:1; bool safe_to_cache_query:1; bool ignore:1; + bool has_named_call_param:1; bool next_is_main:1; // use "main" SELECT_LEX for nrxt allocation; bool next_is_down:1; // use "main" SELECT_LEX for nrxt allocation; /* diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 269d497058c2f..39c0f9b55a985 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1594,6 +1594,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); boolean_test predicate bit_expr parenthesized_expr table_wild simple_expr column_default_non_parenthesized_expr udf_expr + sp_cparam primary_expr string_factor_expr mysql_concatenation_expr select_sublist_qualified_asterisk expr_or_ignore expr_or_ignore_or_default @@ -3497,16 +3498,35 @@ opt_sp_cparams: ; sp_cparams: - sp_cparams ',' expr + sp_cparams ',' sp_cparam { ($$= $1)->push_back($3, thd->mem_root); } - | expr + | sp_cparam { ($$= &Lex->value_list)->push_back($1, thd->mem_root); } ; +sp_cparam: + expr + { + if (Lex->has_named_call_param) + { + thd->parse_error(); + MYSQL_YYABORT; + } + $$= $1; + } + | ident ARROW_SYM expr + { + Lex->has_named_call_param= true; + $3->base_flags|= item_base_t::IS_EXPLICIT_NAME; + $3->set_name(thd, $1); + $$= $3; + } + ; + /* Stored FUNCTION parameter declaration list */ sp_fdparam_list: /* Empty */