diff --git a/mysql-test/main/sp_named_params.result b/mysql-test/main/sp_named_params.result new file mode 100644 index 0000000000000..883c1970614c0 --- /dev/null +++ b/mysql-test/main/sp_named_params.result @@ -0,0 +1,66 @@ +# +# 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: Undeclared variable: 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; +# +# 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..4749b0fcdbfc4 --- /dev/null +++ b/mysql-test/main/sp_named_params.test @@ -0,0 +1,71 @@ +--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_UNDECLARED_VAR +CALL p1(a => 1, a => 2, b => 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 # End of tests +--echo # diff --git a/sql/sp_head.cc b/sql/sp_head.cc index 6aa501521116c..5fe0021d092c7 100644 --- a/sql/sp_head.cc +++ b/sql/sp_head.cc @@ -2177,14 +2177,89 @@ 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_UNDECLARED_VAR, 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 (param_assigned[positional_count]) + { + my_error(ER_SP_UNDECLARED_VAR, MYF(0), + m_pcont->get_context_variable(positional_count)->name.str); + 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 b492dbe8411a2..370ed233aac2a 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -10274,6 +10274,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); @@ -10313,6 +10314,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 438f8824ab99f..7eeb41ed56d8c 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -3438,6 +3438,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 21525c7990ccf..bd8d3efaa1340 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1592,6 +1592,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 @@ -3498,16 +3499,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 */