diff --git a/mysql-test/main/call_named_param.result b/mysql-test/main/call_named_param.result new file mode 100644 index 0000000000000..3df1d03d72252 --- /dev/null +++ b/mysql-test/main/call_named_param.result @@ -0,0 +1,105 @@ +# MDEV-38329 CALL with named parameters. +# Create procedure with 2 params +CREATE PROCEDURE p1(a INT, b INT) SELECT a, b; +# Positional call +CALL p1(1, 2); +a b +1 2 +# Named call - same order as declaration +CALL p1(a => 10, b => 20); +a b +10 20 +# Named call - reversed order (tests resolution) +CALL p1(b => 200, a => 100); +a b +100 200 +# Mixed: positional first, then named +CALL p1(100, b => 200); +a b +100 200 +# User variables as named parameters +SET @v1= 5, @v2= 10; +CALL p1(a => @v1, b => @v2); +a b +5 10 +# Expressions as named parameters +CALL p1(a => 1+1, b => 3*4); +a b +2 12 +# Error: unknown parameter names +CALL p1(x => 0, y => 3); +ERROR 42000: Undeclared variable: x +# 3-param procedure +CREATE PROCEDURE p2(x INT, y INT, z INT) SELECT x, y, z; +# Named call all 3 +CALL p2(x => 1, y => 2, z => 3); +x y z +1 2 3 +# Named out-of-order +CALL p2(z => 30, x => 10, y => 20); +x y z +10 20 30 +# Mixed: 1 positional + 2 named +CALL p2(1, z => 30, y => 20); +x y z +1 20 30 +DROP PROCEDURE p1; +DROP PROCEDURE p2; +# Error: unknown parameter name +CREATE PROCEDURE p3(a INT) SELECT a; +CALL p3(x => 1); +ERROR 42000: Undeclared variable: x +# Error: duplicate parameter +CALL p3(a => 1, a => 2); +ERROR 42000: Duplicate parameter: a +DROP PROCEDURE p3; +# OUT parameters: named call out-of-order, writeback to correct variables +CREATE PROCEDURE p_out(OUT a INT, OUT b INT) +BEGIN +SELECT 10, 20 INTO a, b; +END| +CALL p_out(b => @bout, a => @aout); +SELECT @aout, @bout; +@aout @bout +10 20 +DROP PROCEDURE p_out; +# INOUT parameters with named call +CREATE PROCEDURE p_inout(INOUT x INT, INOUT y INT) +BEGIN +SET x= x * 10, y= y + 100; +END| +SET @xin= 1, @yin= 2; +CALL p_inout(y => @yin, x => @xin); +SELECT @xin, @yin; +@xin @yin +10 102 +DROP PROCEDURE p_inout; +# Default parameters: omitted params get default value +CREATE PROCEDURE p_def(a INT, b INT DEFAULT 10) SELECT a, b; +# Positional: one arg, second gets default +CALL p_def(1); +a b +1 10 +# Positional: two args +CALL p_def(1, 2); +a b +1 2 +# Named: omit b, b gets default (2e) +CALL p_def(a => 5); +a b +5 10 +# Named: both given +CALL p_def(a => 1, b => 20); +a b +1 20 +# Named: reversed order, give both +CALL p_def(b => 100, a => 7); +a b +7 100 +DROP PROCEDURE p_def; +# Error: required param omitted (no default) +CREATE PROCEDURE p_req(a INT, b INT) SELECT a, b; +CALL p_req(a => 1); +ERROR 42000: Incorrect number of arguments for PROCEDURE test.p_req; expected 2, got 1 +DROP PROCEDURE p_req; +# End of 13.0 tests diff --git a/mysql-test/main/call_named_param.test b/mysql-test/main/call_named_param.test new file mode 100644 index 0000000000000..566f5750ea8e0 --- /dev/null +++ b/mysql-test/main/call_named_param.test @@ -0,0 +1,101 @@ +# MDEV-38329 CALL with named parameters. + +--echo # MDEV-38329 CALL with named parameters. +--echo # Create procedure with 2 params +CREATE PROCEDURE p1(a INT, b INT) SELECT a, b; + +--echo # Positional call +CALL p1(1, 2); + +--echo # Named call - same order as declaration +CALL p1(a => 10, b => 20); + +--echo # Named call - reversed order (tests resolution) +CALL p1(b => 200, a => 100); + +--echo # Mixed: positional first, then named +CALL p1(100, b => 200); + +--echo # User variables as named parameters +SET @v1= 5, @v2= 10; +CALL p1(a => @v1, b => @v2); + +--echo # Expressions as named parameters +CALL p1(a => 1+1, b => 3*4); + +--echo # Error: unknown parameter names +--error ER_SP_UNDECLARED_VAR +CALL p1(x => 0, y => 3); + +--echo # 3-param procedure +CREATE PROCEDURE p2(x INT, y INT, z INT) SELECT x, y, z; + +--echo # Named call all 3 +CALL p2(x => 1, y => 2, z => 3); + +--echo # Named out-of-order +CALL p2(z => 30, x => 10, y => 20); + +--echo # Mixed: 1 positional + 2 named +CALL p2(1, z => 30, y => 20); + +DROP PROCEDURE p1; +DROP PROCEDURE p2; + +--echo # Error: unknown parameter name +CREATE PROCEDURE p3(a INT) SELECT a; + +--error ER_SP_UNDECLARED_VAR +CALL p3(x => 1); + +--echo # Error: duplicate parameter +--error ER_SP_DUP_PARAM +CALL p3(a => 1, a => 2); + +DROP PROCEDURE p3; + +--echo # OUT parameters: named call out-of-order, writeback to correct variables +delimiter |; +CREATE PROCEDURE p_out(OUT a INT, OUT b INT) +BEGIN + SELECT 10, 20 INTO a, b; +END| +delimiter ;| +CALL p_out(b => @bout, a => @aout); +SELECT @aout, @bout; +DROP PROCEDURE p_out; + +--echo # INOUT parameters with named call +delimiter |; +CREATE PROCEDURE p_inout(INOUT x INT, INOUT y INT) +BEGIN + SET x= x * 10, y= y + 100; +END| +delimiter ;| +SET @xin= 1, @yin= 2; +CALL p_inout(y => @yin, x => @xin); +SELECT @xin, @yin; +DROP PROCEDURE p_inout; + +--echo # Default parameters: omitted params get default value +CREATE PROCEDURE p_def(a INT, b INT DEFAULT 10) SELECT a, b; + +--echo # Positional: one arg, second gets default +CALL p_def(1); +--echo # Positional: two args +CALL p_def(1, 2); +--echo # Named: omit b, b gets default (2e) +CALL p_def(a => 5); +--echo # Named: both given +CALL p_def(a => 1, b => 20); +--echo # Named: reversed order, give both +CALL p_def(b => 100, a => 7); +DROP PROCEDURE p_def; + +--echo # Error: required param omitted (no default) +CREATE PROCEDURE p_req(a INT, b INT) SELECT a, b; +--error ER_SP_WRONG_NO_OF_ARGS +CALL p_req(a => 1); +DROP PROCEDURE p_req; + +--echo # End of 13.0 tests diff --git a/mysql-test/run_call_tests.sh b/mysql-test/run_call_tests.sh new file mode 100755 index 0000000000000..344adc2d55d69 --- /dev/null +++ b/mysql-test/run_call_tests.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Run tests relevant to CALL / named params. Run from repo root. +# Usage: ./mysql-test/run_call_tests.sh [--quick|--main|--main-ps|--push] + +set -e +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MTR="$ROOT/build/mysql-test/mtr" + +if [ ! -x "$MTR" ]; then + echo "Build or MTR not found. Run: cd build && ninja" + exit 1 +fi + +cd "$ROOT/build/mysql-test" + +case "${1:-}" in + --quick) + echo "=== Quick: call_named_param (default + ps-protocol) ===" + ./mtr --force call_named_param + ./mtr --force --ps-protocol call_named_param + ;; + --main) + echo "=== Main suite ===" + ./mtr --force --suite-timeout=120 --max-test-fail=10 --retry=3 --suite=main + ;; + --main-ps) + echo "=== Main suite with --ps-protocol ===" + ./mtr --force --ps-protocol --suite-timeout=120 --max-test-fail=10 --retry=3 --suite=main + ;; + --push) + echo "=== default.push: n_mix ===" + ./mtr --timer --force --parallel=auto --comment=n_mix --vardir=var-n_mix --mysqld=--binlog-format=mixed --skip-test-list=collections/disabled-per-push.list + echo "=== default.push: ps_row ===" + ./mtr --timer --force --parallel=auto --comment=ps_row --vardir=var-ps_row --ps-protocol --mysqld=--binlog-format=row --skip-test-list=collections/disabled-per-push.list + ;; + *) + echo "Usage: $0 --quick | --main | --main-ps | --push" + echo " --quick call_named_param only (default + ps-protocol)" + echo " --main full main suite" + echo " --main-ps main suite with prepared statements" + echo " --push first two default.push runs (n_mix, ps_row)" + exit 1 + ;; +esac diff --git a/sql/sp_head.cc b/sql/sp_head.cc index 6aa501521116c..14efb652d7983 100644 --- a/sql/sp_head.cc +++ b/sql/sp_head.cc @@ -2237,10 +2237,21 @@ sp_head::execute_procedure(THD *thd, List *args) for (uint i= 0 ; i < params ; i++) { - Item *arg_item= it_args++; - + Item *arg_item; + if (i < args->elements) + arg_item= it_args++; + else + { + sp_variable *spvar= m_pcont->get_context_variable(i); + arg_item= spvar->default_value; + } if (!arg_item) + { + my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "PROCEDURE", + ErrConvDQName(this).ptr(), params, args->elements); + err_status= TRUE; break; + } err_status= bind_input_param(thd, arg_item, i, octx, nctx, FALSE); if (err_status) diff --git a/sql/sp_rcontext.cc b/sql/sp_rcontext.cc index 84b8748e23bd4..24eccc32708c5 100644 --- a/sql/sp_rcontext.cc +++ b/sql/sp_rcontext.cc @@ -227,9 +227,13 @@ bool Row_definition_list:: List_iterator it_args(*args); DBUG_ASSERT(elements >= args->elements ); Spvar_definition *def; - Item *arg; - while ((def= it++) && (arg= it_args++)) + uint i= 0; + while ((def= it++)) { + Item *arg= (i < args->elements) ? it_args++ : nullptr; + i++; + if (!arg) + continue; if (def->type_handler()->adjust_spparam_type(def, arg)) return true; } diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index b492dbe8411a2..a56754758b055 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -1258,6 +1258,7 @@ void LEX::start(THD *thd_arg) update_list.empty(); set_var_list.empty(); param_list.empty(); + call_param_list.empty(); view_list.empty(); with_persistent_for_clause= FALSE; column_list= NULL; @@ -10274,6 +10275,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(); + call_param_list.empty(); thd->variables.path.resolve(thd, sphead, name, &sph, &pkgname); @@ -10313,6 +10315,7 @@ bool LEX::call_statement_start(THD *thd, Identifier_chain2 q_pkg_proc(*pkg, *proc); sp_name *spname; value_list.empty(); + call_param_list.empty(); sql_command= SQLCOM_CALL; const Lex_ident_db_normalized dbn= thd->to_ident_db_normalized_with_error(*db); @@ -10377,12 +10380,29 @@ bool LEX::call_statement_start_or_lvalue_assign(THD *thd, } +void LEX::build_value_list_from_call_params(THD *thd) +{ + if (call_param_list.elements == 0) + return; + value_list.empty(); + List_iterator_fast it(call_param_list); + Call_param *cp; + while ((cp= it++)) + value_list.push_back(cp->value, thd->mem_root); +} + + bool LEX::direct_call(THD *thd, const Qualified_ident *ident, List *args) { DBUG_ASSERT(ident); if (!ident->spvar()) return false; // A procedure call + /* + Populate value_list from call_param_list so that args is ready for + SP variable method calls such as assoc_array_var.delete('key'). + */ + build_value_list_from_call_params(thd); /* ident->part(0) is a known SP variable. diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 438f8824ab99f..6db4bd887967c 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -3305,6 +3305,12 @@ struct LEX: public Query_tables_list Table_type table_type; /* Used for SHOW CREATE */ List ref_list; List users_list; + /** One argument to CALL: optional parameter name (empty if positional) and value. */ + struct Call_param { + LEX_CSTRING name; /* empty (str==NULL or length==0) if positional */ + Item *value; + }; + List call_param_list; List *insert_list= nullptr,field_list,value_list,update_list; List many_values; List var_list; @@ -3997,6 +4003,8 @@ struct LEX: public Query_tables_list bool call_statement_start(THD *thd, const Qualified_ident *ident); bool call_statement_start_or_lvalue_assign(THD *thd, Qualified_ident *ident); + /** Build value_list from call_param_list (for execution/prepare). No-op if call_param_list is empty. */ + void build_value_list_from_call_params(THD *thd); /* Create instructions for a direct call (without the CALL keyword): sp1; - a schema procedure call diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index 1fce0eb6014c2..2c5415df623d4 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -3066,6 +3066,116 @@ static bool do_execute_sp(THD *thd, sp_head *sp) thd->lex->in_sum_func= 0; // For Item_field::fix_fields() + /* + Rebuild value_list from call_param_list so execution always sees the + current arguments (needed for prepared statements where value_list may + not have been preserved from prepare). Then, if any parameter is named, + resolve names to formal positions and rebuild value_list in declaration order. + */ + if (thd->lex->call_param_list.elements > 0) + { + thd->lex->build_value_list_from_call_params(thd); + + bool has_named= false; + { + List_iterator_fast it(thd->lex->call_param_list); + LEX::Call_param *cp; + while ((cp= it++)) + if (cp->name.length) { has_named= true; break; } + } + + if (has_named) + { + sp_pcontext *pctx= sp->get_parse_context(); + const uint param_count= pctx->context_var_count(); + const uint call_count= thd->lex->call_param_list.elements; + + Item **arg_array= (Item **)thd->alloc(param_count * sizeof(Item *)); + if (!arg_array) + return 1; + memset(arg_array, 0, param_count * sizeof(Item *)); + + /* Positional pass: leading positional arguments fill slots 0..N-1. */ + uint positional_idx= 0; + bool seen_named= false; + { + List_iterator_fast it(thd->lex->call_param_list); + LEX::Call_param *cp; + while ((cp= it++)) + { + if (!cp->name.length) + { + if (seen_named) + { + my_error(ER_WRONG_ARGUMENTS, MYF(0), "CALL"); + return 1; + } + if (positional_idx >= param_count) + { + my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "PROCEDURE", + ErrConvDQName(sp).ptr(), param_count, call_count); + return 1; + } + arg_array[positional_idx++]= cp->value; + } + else + seen_named= true; + } + } + + /* Named pass: resolve each name to its formal-parameter slot. */ + { + List_iterator_fast it(thd->lex->call_param_list); + LEX::Call_param *cp; + while ((cp= it++)) + { + if (!cp->name.length) + continue; + sp_variable *spvar= pctx->find_variable(&cp->name, false); + if (!spvar) + { + my_error(ER_SP_UNDECLARED_VAR, MYF(0), cp->name.str); + return 1; + } + if (arg_array[spvar->offset]) + { + my_error(ER_SP_DUP_PARAM, MYF(0), cp->name.str); + return 1; + } + arg_array[spvar->offset]= cp->value; + } + } + + /* Defaults pass: fill any omitted slot from the parameter's default. */ + for (uint i= 0; i < param_count; i++) + { + if (!arg_array[i]) + { + sp_variable *spvar= pctx->get_context_variable(i); + arg_array[i]= spvar->default_value; + } + } + + /* Validate: every required slot must be filled (no default = error). */ + for (uint i= 0; i < param_count; i++) + { + if (!arg_array[i]) + { + sp_variable *spvar= pctx->get_context_variable(i); + my_error(ER_SP_WRONG_NO_OF_ARGS, MYF(0), "PROCEDURE", + ErrConvDQName(sp).ptr(), param_count, call_count); + (void)spvar; + return 1; + } + } + + /* Rebuild value_list in formal-parameter order. */ + thd->lex->value_list.empty(); + for (uint i= 0; i < param_count; i++) + thd->lex->value_list.push_back(arg_array[i], thd->mem_root); + } + } + /* We never write CALL statements into binlog: - If the mode is non-prelocked, each statement will be logged diff --git a/sql/sql_prepare.cc b/sql/sql_prepare.cc index c572cb3d79970..cf25525b127d4 100644 --- a/sql/sql_prepare.cc +++ b/sql/sql_prepare.cc @@ -2484,6 +2484,7 @@ static bool check_prepared_statement(Prepared_statement *stmt) break; case SQLCOM_CALL: + lex->build_value_list_from_call_params(thd); res= mysql_test_call_fields(stmt, tables, &lex->value_list); break; case SQLCOM_SET_OPTION: diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 21525c7990ccf..1473604a991b0 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -3497,17 +3497,34 @@ opt_sp_cparams: | sp_cparams { $$= $1; } ; -sp_cparams: - sp_cparams ',' expr +/* One CALL parameter: positional (expr) or named (ident => expr). */ +sp_call_param: + expr { - ($$= $1)->push_back($3, thd->mem_root); + LEX::Call_param *cp= (LEX::Call_param*)thd->alloc(sizeof(LEX::Call_param)); + cp->name= null_clex_str; + cp->value= $1; + Lex->call_param_list.push_back(cp, thd->mem_root); + Lex->value_list.push_back($1, thd->mem_root); } - | expr + | ident ARROW_SYM expr { - ($$= &Lex->value_list)->push_back($1, thd->mem_root); + LEX::Call_param *cp= (LEX::Call_param*)thd->alloc(sizeof(LEX::Call_param)); + cp->name.str= thd->strmake($1.str, $1.length); + cp->name.length= cp->name.str ? $1.length : 0; + cp->value= $3; + Lex->call_param_list.push_back(cp, thd->mem_root); + Lex->value_list.push_back($3, thd->mem_root); } ; +sp_cparams: + sp_cparams ',' sp_call_param + { $$= &Lex->value_list; } + | sp_call_param + { $$= &Lex->value_list; } + ; + /* Stored FUNCTION parameter declaration list */ sp_fdparam_list: /* Empty */