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 */