From 4107478d374fae8299b6eb17fd13e65b7d8e026a Mon Sep 17 00:00:00 2001 From: Tom Lane <tgl@sss.pgh.pa.us> Date: Tue, 13 May 2008 22:10:30 +0000 Subject: [PATCH] Improve plpgsql's RAISE command. It is now possible to attach DETAIL and HINT fields to a user-thrown error message, and to specify the SQLSTATE error code to use. The syntax has also been tweaked so that the Oracle-compatible case "RAISE exception_name" works (though you won't get a very nice error message if you just write that much). Lastly, support the Oracle-compatible syntax "RAISE" with no parameters to re-throw the current error from within an EXCEPTION block. In passing, allow the syntax SQLSTATE 'nnnnn' within EXCEPTION lists, so that there is a way to trap errors with custom SQLSTATE codes. Pavel Stehule and Tom Lane --- doc/src/sgml/plpgsql.sgml | 126 +++++++++--- src/pl/plpgsql/src/gram.y | 274 ++++++++++++++++++++------ src/pl/plpgsql/src/pl_comp.c | 38 +++- src/pl/plpgsql/src/pl_exec.c | 241 ++++++++++++++++------ src/pl/plpgsql/src/pl_funcs.c | 39 +++- src/pl/plpgsql/src/plpgsql.h | 31 ++- src/pl/plpgsql/src/scan.l | 7 +- src/test/regress/expected/plpgsql.out | 131 +++++++++++- src/test/regress/sql/plpgsql.sql | 131 +++++++++++- 9 files changed, 859 insertions(+), 159 deletions(-) diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 1065eddc74e..09ad6944dba 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -1,4 +1,4 @@ -<!-- $PostgreSQL: pgsql/doc/src/sgml/plpgsql.sgml,v 1.128 2008/05/03 00:11:36 tgl Exp $ --> +<!-- $PostgreSQL: pgsql/doc/src/sgml/plpgsql.sgml,v 1.129 2008/05/13 22:10:29 tgl Exp $ --> <chapter id="plpgsql"> <title><application>PL/pgSQL</application> - <acronym>SQL</acronym> Procedural Language</title> @@ -2133,7 +2133,12 @@ END; condition name <literal>OTHERS</> matches every error type except <literal>QUERY_CANCELED</>. (It is possible, but often unwise, to trap <literal>QUERY_CANCELED</> by name.) Condition names are - not case-sensitive. + not case-sensitive. Also, an error condition can be specified + by <literal>SQLSTATE</> code; for example these are equivalent: +<programlisting> + WHEN division_by_zero THEN ... + WHEN SQLSTATE '22012' THEN ... +</programlisting> </para> <para> @@ -2750,13 +2755,19 @@ END LOOP <optional> <replaceable>label</replaceable> </optional>; raise errors. <synopsis> -RAISE <replaceable class="parameter">level</replaceable> '<replaceable class="parameter">format</replaceable>' <optional>, <replaceable class="parameter">expression</replaceable> <optional>, ...</optional></optional>; +RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> '<replaceable class="parameter">format</replaceable>' <optional>, <replaceable class="parameter">expression</replaceable> <optional>, ...</optional></optional> <optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional> </optional>; +RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> <replaceable class="parameter">condition_name</> <optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional> </optional>; +RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> SQLSTATE '<replaceable class="parameter">sqlstate</>' <optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional> </optional>; +RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional>; +RAISE ; </synopsis> - Possible levels are <literal>DEBUG</literal>, + The <replaceable class="parameter">level</replaceable> option specifies + the error severity. Allowed levels are <literal>DEBUG</literal>, <literal>LOG</literal>, <literal>INFO</literal>, <literal>NOTICE</literal>, <literal>WARNING</literal>, - and <literal>EXCEPTION</literal>. + and <literal>EXCEPTION</literal>, with <literal>EXCEPTION</literal> + being the default. <literal>EXCEPTION</literal> raises an error (which normally aborts the current transaction); the other levels only generate messages of different priority levels. @@ -2769,19 +2780,17 @@ RAISE <replaceable class="parameter">level</replaceable> '<replaceable class="pa </para> <para> + After <replaceable class="parameter">level</replaceable> if any, + you can write a <replaceable class="parameter">format</replaceable> + (which must be a simple string literal, not an expression). The + format string specifies the error message text to be reported. + The format string can be followed + by optional argument expressions to be inserted into the message. Inside the format string, <literal>%</literal> is replaced by the - next optional argument's string representation. Write + string representation of the next optional argument's value. Write <literal>%%</literal> to emit a literal <literal>%</literal>. - Arguments can be simple variables or expressions, - but the format must be a simple string literal. </para> - <!-- - This example should work, but does not: - RAISE NOTICE 'Id number ' || key || ' not found!'; - Put it back when we allow non-string-literal formats. - --> - <para> In this example, the value of <literal>v_job_id</> will replace the <literal>%</literal> in the string: @@ -2791,19 +2800,90 @@ RAISE NOTICE 'Calling cs_create_job(%)', v_job_id; </para> <para> - This example will abort the transaction with the given error message: + You can attach additional information to the error report by writing + <literal>USING</> followed by <replaceable + class="parameter">option</replaceable> = <replaceable + class="parameter">expression</replaceable> items. The allowed + <replaceable class="parameter">option</replaceable> keywords are + <literal>MESSAGE</>, <literal>DETAIL</>, <literal>HINT</>, and + <literal>ERRCODE</>, while each <replaceable + class="parameter">expression</replaceable> can be any string-valued + expression. + <literal>MESSAGE</> sets the error message text (this option can't + be used in the form of <command>RAISE</> that includes a format + string before <literal>USING</>). + <literal>DETAIL</> supplies an error detail message, while + <literal>HINT</> supplies a hint message. + <literal>ERRCODE</> specifies the error code (SQLSTATE) to report, + either by condition name as shown in <xref linkend="errcodes-appendix">, + or directly as a five-character SQLSTATE code. + </para> + + <para> + This example will abort the transaction with the given error message + and hint: +<programlisting> +RAISE EXCEPTION 'Nonexistent ID --> %', user_id USING HINT = 'Please check your user id'; +</programlisting> + </para> + + <para> + These two examples show equivalent ways of setting the SQLSTATE: +<programlisting> +RAISE 'Duplicate user ID: %', user_id USING ERRCODE = 'unique_violation'; +RAISE 'Duplicate user ID: %', user_id USING ERRCODE = '23505'; +</programlisting> + </para> + + <para> + There is a second <command>RAISE</> syntax in which the main argument + is the condition name or SQLSTATE to be reported, for example: +<programlisting> +RAISE division_by_zero; +RAISE SQLSTATE '22012'; +</programlisting> + In this syntax, <literal>USING</> can be used to supply a custom + error message, detail, or hint. Another way to do the earlier + example is <programlisting> -RAISE EXCEPTION 'Nonexistent ID --> %', user_id; +RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id; </programlisting> </para> + <para> + Still another variant is to write <literal>RAISE USING</> or <literal>RAISE + <replaceable class="parameter">level</replaceable> USING</> and put + everything else into the <literal>USING</> list. + </para> + + <para> + The last variant of <command>RAISE</> has no parameters at all. + This form can only be used inside a <literal>BEGIN</> block's + <literal>EXCEPTION</> clause; + it causes the error currently being handled to be re-thrown to the + next enclosing block. + </para> + + <para> + If no condition name nor SQLSTATE is specified in a + <command>RAISE EXCEPTION</command> command, the default is to use + <literal>RAISE_EXCEPTION</> (<literal>P0001</>). If no message + text is specified, the default is to use the condition name or + SQLSTATE as message text. + </para> + + <note> <para> - <command>RAISE EXCEPTION</command> presently always generates - the same <varname>SQLSTATE</varname> code, <literal>P0001</>, no matter what message - it is invoked with. It is possible to trap this exception with - <literal>EXCEPTION ... WHEN RAISE_EXCEPTION THEN ...</> but there - is no way to tell one <command>RAISE</> from another. + When specifying an error code by SQLSTATE code, you are not + limited to the predefined error codes, but can select any + error code consisting of five digits and/or upper-case ASCII + letters, other than <literal>00000</>. It is recommended that + you avoid throwing error codes that end in three zeroes, because + these are category codes and can only be trapped by trapping + the whole category. </para> + </note> + </sect1> <sect1 id="plpgsql-trigger"> @@ -4307,7 +4387,9 @@ $$ LANGUAGE plpgsql; <callout arearefs="co.plpgsql-porting-raise"> <para> The syntax of <literal>RAISE</> is considerably different from - Oracle's similar statement. + Oracle's statement, although the basic case <literal>RAISE</> + <replaceable class="parameter">exception_name</replaceable> works + similarly. </para> </callout> <callout arearefs="co.plpgsql-porting-exception"> diff --git a/src/pl/plpgsql/src/gram.y b/src/pl/plpgsql/src/gram.y index 979ebee3ca3..b67a8bba776 100644 --- a/src/pl/plpgsql/src/gram.y +++ b/src/pl/plpgsql/src/gram.y @@ -9,7 +9,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/pl/plpgsql/src/gram.y,v 1.111 2008/05/03 00:11:36 tgl Exp $ + * $PostgreSQL: pgsql/src/pl/plpgsql/src/gram.y,v 1.112 2008/05/13 22:10:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -52,6 +52,7 @@ static void check_labels(const char *start_label, const char *end_label); static PLpgSQL_expr *read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected); +static List *read_raise_options(void); %} @@ -138,11 +139,7 @@ static PLpgSQL_expr *read_cursor_args(PLpgSQL_var *cursor, %type <list> proc_exceptions %type <exception_block> exception_sect %type <exception> proc_exception -%type <condition> proc_conditions - - -%type <ival> raise_level -%type <str> raise_msg +%type <condition> proc_conditions proc_condition %type <list> getdiag_list %type <diagitem> getdiag_list_item @@ -164,7 +161,6 @@ static PLpgSQL_expr *read_cursor_args(PLpgSQL_var *cursor, %token K_CONSTANT %token K_CONTINUE %token K_CURSOR -%token K_DEBUG %token K_DECLARE %token K_DEFAULT %token K_DIAGNOSTICS @@ -181,16 +177,13 @@ static PLpgSQL_expr *read_cursor_args(PLpgSQL_var *cursor, %token K_GET %token K_IF %token K_IN -%token K_INFO %token K_INSERT %token K_INTO %token K_IS -%token K_LOG %token K_LOOP %token K_MOVE %token K_NOSCROLL %token K_NOT -%token K_NOTICE %token K_NULL %token K_OPEN %token K_OR @@ -207,7 +200,6 @@ static PLpgSQL_expr *read_cursor_args(PLpgSQL_var *cursor, %token K_TO %token K_TYPE %token K_USING -%token K_WARNING %token K_WHEN %token K_WHILE @@ -1246,7 +1238,7 @@ stmt_return : K_RETURN lno } ; -stmt_raise : K_RAISE lno raise_level raise_msg +stmt_raise : K_RAISE lno { PLpgSQL_stmt_raise *new; int tok; @@ -1255,66 +1247,130 @@ stmt_raise : K_RAISE lno raise_level raise_msg new->cmd_type = PLPGSQL_STMT_RAISE; new->lineno = $2; - new->elog_level = $3; - new->message = $4; + new->elog_level = ERROR; /* default */ + new->condname = NULL; + new->message = NULL; new->params = NIL; + new->options = NIL; tok = yylex(); + if (tok == 0) + yyerror("unexpected end of function definition"); /* - * We expect either a semi-colon, which - * indicates no parameters, or a comma that - * begins the list of parameter expressions + * We could have just RAISE, meaning to re-throw + * the current error. */ - if (tok != ',' && tok != ';') - yyerror("syntax error"); - - if (tok == ',') + if (tok != ';') { - do + /* + * First is an optional elog severity level. + * Most of these are not plpgsql keywords, + * so we rely on examining yytext. + */ + if (pg_strcasecmp(yytext, "exception") == 0) + { + new->elog_level = ERROR; + tok = yylex(); + } + else if (pg_strcasecmp(yytext, "warning") == 0) + { + new->elog_level = WARNING; + tok = yylex(); + } + else if (pg_strcasecmp(yytext, "notice") == 0) + { + new->elog_level = NOTICE; + tok = yylex(); + } + else if (pg_strcasecmp(yytext, "info") == 0) + { + new->elog_level = INFO; + tok = yylex(); + } + else if (pg_strcasecmp(yytext, "log") == 0) + { + new->elog_level = LOG; + tok = yylex(); + } + else if (pg_strcasecmp(yytext, "debug") == 0) { - PLpgSQL_expr *expr; + new->elog_level = DEBUG1; + tok = yylex(); + } + if (tok == 0) + yyerror("unexpected end of function definition"); - expr = read_sql_expression2(',', ';', - ", or ;", - &tok); - new->params = lappend(new->params, expr); - } while (tok == ','); - } + /* + * Next we can have a condition name, or + * equivalently SQLSTATE 'xxxxx', or a string + * literal that is the old-style message format, + * or USING to start the option list immediately. + */ + if (tok == T_STRING) + { + /* old style message and parameters */ + new->message = plpgsql_get_string_value(); + /* + * We expect either a semi-colon, which + * indicates no parameters, or a comma that + * begins the list of parameter expressions, + * or USING to begin the options list. + */ + tok = yylex(); + if (tok != ',' && tok != ';' && tok != K_USING) + yyerror("syntax error"); - $$ = (PLpgSQL_stmt *)new; - } - ; + while (tok == ',') + { + PLpgSQL_expr *expr; -raise_msg : T_STRING - { - $$ = plpgsql_get_string_value(); - } - ; + expr = read_sql_construct(',', ';', K_USING, + ", or ; or USING", + "SELECT ", + true, true, &tok); + new->params = lappend(new->params, expr); + } + } + else if (tok != K_USING) + { + /* must be condition name or SQLSTATE */ + if (pg_strcasecmp(yytext, "sqlstate") == 0) + { + /* next token should be a string literal */ + char *sqlstatestr; + + if (yylex() != T_STRING) + yyerror("syntax error"); + sqlstatestr = plpgsql_get_string_value(); + + if (strlen(sqlstatestr) != 5) + yyerror("invalid SQLSTATE code"); + if (strspn(sqlstatestr, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != 5) + yyerror("invalid SQLSTATE code"); + new->condname = sqlstatestr; + } + else + { + char *cname; + + if (tok != T_WORD) + yyerror("syntax error"); + plpgsql_convert_ident(yytext, &cname, 1); + plpgsql_recognize_err_condition(cname, + false); + new->condname = cname; + } + tok = yylex(); + if (tok != ';' && tok != K_USING) + yyerror("syntax error"); + } -raise_level : K_EXCEPTION - { - $$ = ERROR; - } - | K_WARNING - { - $$ = WARNING; - } - | K_NOTICE - { - $$ = NOTICE; - } - | K_INFO - { - $$ = INFO; - } - | K_LOG - { - $$ = LOG; - } - | K_DEBUG - { - $$ = DEBUG1; + if (tok == K_USING) + new->options = read_raise_options(); + } + + $$ = (PLpgSQL_stmt *)new; } ; @@ -1592,20 +1648,61 @@ proc_exception : K_WHEN lno proc_conditions K_THEN proc_sect } ; -proc_conditions : proc_conditions K_OR opt_lblname +proc_conditions : proc_conditions K_OR proc_condition { PLpgSQL_condition *old; for (old = $1; old->next != NULL; old = old->next) /* skip */ ; - old->next = plpgsql_parse_err_condition($3); - + old->next = $3; $$ = $1; } - | opt_lblname + | proc_condition + { + $$ = $1; + } + ; + +proc_condition : opt_lblname { $$ = plpgsql_parse_err_condition($1); } + | T_SCALAR + { + /* + * Because we know the special sqlstate variable + * is at the top of the namestack (see the + * exception_sect production), the SQLSTATE + * keyword will always lex as T_SCALAR. This + * might not be true in other parsing contexts! + */ + PLpgSQL_condition *new; + char *sqlstatestr; + + if (pg_strcasecmp(yytext, "sqlstate") != 0) + yyerror("syntax error"); + + /* next token should be a string literal */ + if (yylex() != T_STRING) + yyerror("syntax error"); + sqlstatestr = plpgsql_get_string_value(); + + if (strlen(sqlstatestr) != 5) + yyerror("invalid SQLSTATE code"); + if (strspn(sqlstatestr, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != 5) + yyerror("invalid SQLSTATE code"); + + new = palloc(sizeof(PLpgSQL_condition)); + new->sqlerrstate = MAKE_SQLSTATE(sqlstatestr[0], + sqlstatestr[1], + sqlstatestr[2], + sqlstatestr[3], + sqlstatestr[4]); + new->condname = sqlstatestr; + new->next = NULL; + + $$ = new; + } ; expr_until_semi : @@ -2658,6 +2755,55 @@ read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected) return expr; } +/* + * Parse RAISE ... USING options + */ +static List * +read_raise_options(void) +{ + List *result = NIL; + + for (;;) + { + PLpgSQL_raise_option *opt; + int tok; + + if ((tok = yylex()) == 0) + yyerror("unexpected end of function definition"); + + opt = (PLpgSQL_raise_option *) palloc(sizeof(PLpgSQL_raise_option)); + + if (pg_strcasecmp(yytext, "errcode") == 0) + opt->opt_type = PLPGSQL_RAISEOPTION_ERRCODE; + else if (pg_strcasecmp(yytext, "message") == 0) + opt->opt_type = PLPGSQL_RAISEOPTION_MESSAGE; + else if (pg_strcasecmp(yytext, "detail") == 0) + opt->opt_type = PLPGSQL_RAISEOPTION_DETAIL; + else if (pg_strcasecmp(yytext, "hint") == 0) + opt->opt_type = PLPGSQL_RAISEOPTION_HINT; + else + { + plpgsql_error_lineno = plpgsql_scanner_lineno(); + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized RAISE statement option \"%s\"", + yytext))); + } + + if (yylex() != K_ASSIGN) + yyerror("syntax error, expected \"=\""); + + opt->expr = read_sql_expression2(',', ';', ", or ;", &tok); + + result = lappend(result, opt); + + if (tok == ';') + break; + } + + return result; +} + /* Needed to avoid conflict between different prefix settings: */ #undef yylex diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c index 2252618ad88..737bac58881 100644 --- a/src/pl/plpgsql/src/pl_comp.c +++ b/src/pl/plpgsql/src/pl_comp.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_comp.c,v 1.125 2008/05/12 00:00:54 alvherre Exp $ + * $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_comp.c,v 1.126 2008/05/13 22:10:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1749,6 +1749,42 @@ build_datatype(HeapTuple typeTup, int32 typmod) return typ; } +/* + * plpgsql_recognize_err_condition + * Check condition name and translate it to SQLSTATE. + * + * Note: there are some cases where the same condition name has multiple + * entries in the table. We arbitrarily return the first match. + */ +int +plpgsql_recognize_err_condition(const char *condname, bool allow_sqlstate) +{ + int i; + + if (allow_sqlstate) + { + if (strlen(condname) == 5 && + strspn(condname, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 5) + return MAKE_SQLSTATE(condname[0], + condname[1], + condname[2], + condname[3], + condname[4]); + } + + for (i = 0; exception_label_map[i].label != NULL; i++) + { + if (strcmp(condname, exception_label_map[i].label) == 0) + return exception_label_map[i].sqlerrstate; + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("unrecognized exception condition \"%s\"", + condname))); + return 0; /* keep compiler quiet */ +} + /* * plpgsql_parse_err_condition * Generate PLpgSQL_condition entry(s) for an exception condition name diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index b248384af41..2ba45befb71 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_exec.c,v 1.213 2008/05/12 20:02:02 alvherre Exp $ + * $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_exec.c,v 1.214 2008/05/13 22:10:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -316,13 +316,17 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo) estate.err_text = NULL; /* - * Provide a more helpful message if a CONTINUE has been used outside - * a loop. + * Provide a more helpful message if a CONTINUE or RAISE has been used + * outside the context it can work in. */ if (rc == PLPGSQL_RC_CONTINUE) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("CONTINUE cannot be used outside a loop"))); + else if (rc == PLPGSQL_RC_RERAISE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("RAISE without parameters cannot be used outside an exception handler"))); else ereport(ERROR, (errcode(ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT), @@ -662,13 +666,17 @@ plpgsql_exec_trigger(PLpgSQL_function *func, estate.err_text = NULL; /* - * Provide a more helpful message if a CONTINUE has been used outside - * a loop. + * Provide a more helpful message if a CONTINUE or RAISE has been used + * outside the context it can work in. */ if (rc == PLPGSQL_RC_CONTINUE) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("CONTINUE cannot be used outside a loop"))); + else if (rc == PLPGSQL_RC_RERAISE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("RAISE without parameters cannot be used outside an exception handler"))); else ereport(ERROR, (errcode(ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT), @@ -1109,6 +1117,11 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block) free_var(errm_var); errm_var->value = (Datum) 0; errm_var->isnull = true; + + /* re-throw error if requested by handler */ + if (rc == PLPGSQL_RC_RERAISE) + ReThrowError(edata); + break; } } @@ -1139,8 +1152,9 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block) switch (rc) { case PLPGSQL_RC_OK: - case PLPGSQL_RC_CONTINUE: case PLPGSQL_RC_RETURN: + case PLPGSQL_RC_CONTINUE: + case PLPGSQL_RC_RERAISE: return rc; case PLPGSQL_RC_EXIT: @@ -1469,7 +1483,8 @@ exec_stmt_loop(PLpgSQL_execstate *estate, PLpgSQL_stmt_loop *stmt) break; case PLPGSQL_RC_RETURN: - return PLPGSQL_RC_RETURN; + case PLPGSQL_RC_RERAISE: + return rc; default: elog(ERROR, "unrecognized rc: %d", rc); @@ -1532,7 +1547,8 @@ exec_stmt_while(PLpgSQL_execstate *estate, PLpgSQL_stmt_while *stmt) break; case PLPGSQL_RC_RETURN: - return PLPGSQL_RC_RETURN; + case PLPGSQL_RC_RERAISE: + return rc; default: elog(ERROR, "unrecognized rc: %d", rc); @@ -1650,8 +1666,9 @@ exec_stmt_fori(PLpgSQL_execstate *estate, PLpgSQL_stmt_fori *stmt) */ rc = exec_stmts(estate, stmt->body); - if (rc == PLPGSQL_RC_RETURN) - break; /* return from function */ + if (rc == PLPGSQL_RC_RETURN || + rc == PLPGSQL_RC_RERAISE) + break; /* break out of the loop */ else if (rc == PLPGSQL_RC_EXIT) { if (estate->exitlabel == NULL) @@ -2267,64 +2284,163 @@ exec_init_tuple_store(PLpgSQL_execstate *estate) static int exec_stmt_raise(PLpgSQL_execstate *estate, PLpgSQL_stmt_raise *stmt) { - char *cp; - PLpgSQL_dstring ds; - ListCell *current_param; + int err_code = 0; + char *condname = NULL; + char *err_message = NULL; + char *err_detail = NULL; + char *err_hint = NULL; + ListCell *lc; - plpgsql_dstring_init(&ds); - current_param = list_head(stmt->params); + /* RAISE with no parameters: re-throw current exception */ + if (stmt->condname == NULL && stmt->message == NULL && + stmt->options == NIL) + return PLPGSQL_RC_RERAISE; - for (cp = stmt->message; *cp; cp++) + if (stmt->condname) { - /* - * Occurrences of a single % are replaced by the next parameter's - * external representation. Double %'s are converted to one %. - */ - if (cp[0] == '%') - { - Oid paramtypeid; - Datum paramvalue; - bool paramisnull; - char *extval; + err_code = plpgsql_recognize_err_condition(stmt->condname, true); + condname = pstrdup(stmt->condname); + } + + if (stmt->message) + { + PLpgSQL_dstring ds; + ListCell *current_param; + char *cp; + + plpgsql_dstring_init(&ds); + current_param = list_head(stmt->params); - if (cp[1] == '%') + for (cp = stmt->message; *cp; cp++) + { + /* + * Occurrences of a single % are replaced by the next parameter's + * external representation. Double %'s are converted to one %. + */ + if (cp[0] == '%') { - plpgsql_dstring_append_char(&ds, cp[1]); - cp++; - continue; - } + Oid paramtypeid; + Datum paramvalue; + bool paramisnull; + char *extval; - if (current_param == NULL) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("too few parameters specified for RAISE"))); + if (cp[1] == '%') + { + plpgsql_dstring_append_char(&ds, cp[1]); + cp++; + continue; + } - paramvalue = exec_eval_expr(estate, - (PLpgSQL_expr *) lfirst(current_param), - ¶misnull, - ¶mtypeid); + if (current_param == NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("too few parameters specified for RAISE"))); + + paramvalue = exec_eval_expr(estate, + (PLpgSQL_expr *) lfirst(current_param), + ¶misnull, + ¶mtypeid); - if (paramisnull) - extval = "<NULL>"; + if (paramisnull) + extval = "<NULL>"; + else + extval = convert_value_to_string(paramvalue, paramtypeid); + plpgsql_dstring_append(&ds, extval); + current_param = lnext(current_param); + exec_eval_cleanup(estate); + } else - extval = convert_value_to_string(paramvalue, paramtypeid); - plpgsql_dstring_append(&ds, extval); - current_param = lnext(current_param); - exec_eval_cleanup(estate); - continue; + plpgsql_dstring_append_char(&ds, cp[0]); } - plpgsql_dstring_append_char(&ds, cp[0]); + /* + * If more parameters were specified than were required to process the + * format string, throw an error + */ + if (current_param != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("too many parameters specified for RAISE"))); + + err_message = plpgsql_dstring_get(&ds); + /* No dstring_free here, the pfree(err_message) does it */ } - /* - * If more parameters were specified than were required to process the - * format string, throw an error - */ - if (current_param != NULL) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("too many parameters specified for RAISE"))); + foreach(lc, stmt->options) + { + PLpgSQL_raise_option *opt = (PLpgSQL_raise_option *) lfirst(lc); + Datum optionvalue; + bool optionisnull; + Oid optiontypeid; + char *extval; + + optionvalue = exec_eval_expr(estate, opt->expr, + &optionisnull, + &optiontypeid); + if (optionisnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("RAISE statement option cannot be NULL"))); + + extval = convert_value_to_string(optionvalue, optiontypeid); + + switch (opt->opt_type) + { + case PLPGSQL_RAISEOPTION_ERRCODE: + if (err_code) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("RAISE option already specified: %s", + "ERRCODE"))); + err_code = plpgsql_recognize_err_condition(extval, true); + condname = pstrdup(extval); + break; + case PLPGSQL_RAISEOPTION_MESSAGE: + if (err_message) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("RAISE option already specified: %s", + "MESSAGE"))); + err_message = pstrdup(extval); + break; + case PLPGSQL_RAISEOPTION_DETAIL: + if (err_detail) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("RAISE option already specified: %s", + "DETAIL"))); + err_detail = pstrdup(extval); + break; + case PLPGSQL_RAISEOPTION_HINT: + if (err_hint) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("RAISE option already specified: %s", + "HINT"))); + err_hint = pstrdup(extval); + break; + default: + elog(ERROR, "unrecognized raise option: %d", opt->opt_type); + } + + exec_eval_cleanup(estate); + } + + /* Default code if nothing specified */ + if (err_code == 0 && stmt->elog_level >= ERROR) + err_code = ERRCODE_RAISE_EXCEPTION; + + /* Default error message if nothing specified */ + if (err_message == NULL) + { + if (condname) + { + err_message = condname; + condname = NULL; + } + else + err_message = pstrdup(unpack_sql_state(err_code)); + } /* * Throw the error (may or may not come back) @@ -2332,12 +2448,21 @@ exec_stmt_raise(PLpgSQL_execstate *estate, PLpgSQL_stmt_raise *stmt) estate->err_text = raise_skip_msg; /* suppress traceback of raise */ ereport(stmt->elog_level, - ((stmt->elog_level >= ERROR) ? errcode(ERRCODE_RAISE_EXCEPTION) : 0, - errmsg_internal("%s", plpgsql_dstring_get(&ds)))); + (err_code ? errcode(err_code) : 0, + errmsg_internal("%s", err_message), + (err_detail != NULL) ? errdetail(err_detail) : 0, + (err_hint != NULL) ? errhint(err_hint) : 0)); estate->err_text = NULL; /* un-suppress... */ - plpgsql_dstring_free(&ds); + if (condname != NULL) + pfree(condname); + if (err_message != NULL) + pfree(err_message); + if (err_detail != NULL) + pfree(err_detail); + if (err_hint != NULL) + pfree(err_hint); return PLPGSQL_RC_OK; } diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index f7624a6c33d..cb9e9c99ec3 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_funcs.c,v 1.70 2008/05/03 00:11:36 tgl Exp $ + * $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_funcs.c,v 1.71 2008/05/13 22:10:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1003,7 +1003,12 @@ dump_raise(PLpgSQL_stmt_raise *stmt) int i = 0; dump_ind(); - printf("RAISE '%s'\n", stmt->message); + printf("RAISE level=%d", stmt->elog_level); + if (stmt->condname) + printf(" condname='%s'", stmt->condname); + if (stmt->message) + printf(" message='%s'", stmt->message); + printf("\n"); dump_indent += 2; foreach(lc, stmt->params) { @@ -1012,6 +1017,36 @@ dump_raise(PLpgSQL_stmt_raise *stmt) dump_expr((PLpgSQL_expr *) lfirst(lc)); printf("\n"); } + if (stmt->options) + { + dump_ind(); + printf(" USING\n"); + dump_indent += 2; + foreach(lc, stmt->options) + { + PLpgSQL_raise_option *opt = (PLpgSQL_raise_option *) lfirst(lc); + + dump_ind(); + switch (opt->opt_type) + { + case PLPGSQL_RAISEOPTION_ERRCODE: + printf(" ERRCODE = "); + break; + case PLPGSQL_RAISEOPTION_MESSAGE: + printf(" MESSAGE = "); + break; + case PLPGSQL_RAISEOPTION_DETAIL: + printf(" DETAIL = "); + break; + case PLPGSQL_RAISEOPTION_HINT: + printf(" HINT = "); + break; + } + dump_expr(opt->expr); + printf("\n"); + } + dump_indent -= 2; + } dump_indent -= 2; } diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index f1206932bb5..53d691a596a 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/pl/plpgsql/src/plpgsql.h,v 1.98 2008/05/03 00:11:36 tgl Exp $ + * $PostgreSQL: pgsql/src/pl/plpgsql/src/plpgsql.h,v 1.99 2008/05/13 22:10:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -106,7 +106,8 @@ enum PLPGSQL_RC_OK, PLPGSQL_RC_EXIT, PLPGSQL_RC_RETURN, - PLPGSQL_RC_CONTINUE + PLPGSQL_RC_CONTINUE, + PLPGSQL_RC_RERAISE }; /* ---------- @@ -119,6 +120,18 @@ enum PLPGSQL_GETDIAG_RESULT_OID }; +/* -------- + * RAISE statement options + * -------- + */ +enum +{ + PLPGSQL_RAISEOPTION_ERRCODE, + PLPGSQL_RAISEOPTION_MESSAGE, + PLPGSQL_RAISEOPTION_DETAIL, + PLPGSQL_RAISEOPTION_HINT +}; + /********************************************************************** * Node and structure definitions @@ -539,10 +552,18 @@ typedef struct int cmd_type; int lineno; int elog_level; - char *message; - List *params; /* list of expressions */ + char *condname; /* condition name, SQLSTATE, or NULL */ + char *message; /* old-style message format literal, or NULL */ + List *params; /* list of expressions for old-style message */ + List *options; /* list of PLpgSQL_raise_option */ } PLpgSQL_stmt_raise; +typedef struct +{ /* RAISE statement option */ + int opt_type; + PLpgSQL_expr *expr; +} PLpgSQL_raise_option; + typedef struct { /* Generic SQL statement to execute */ @@ -772,6 +793,8 @@ extern PLpgSQL_variable *plpgsql_build_variable(const char *refname, int lineno, bool add2namespace); extern PLpgSQL_rec *plpgsql_build_record(const char *refname, int lineno, bool add2namespace); +extern int plpgsql_recognize_err_condition(const char *condname, + bool allow_sqlstate); extern PLpgSQL_condition *plpgsql_parse_err_condition(char *condname); extern void plpgsql_adddatum(PLpgSQL_datum *new); extern int plpgsql_add_initdatums(int **varnos); diff --git a/src/pl/plpgsql/src/scan.l b/src/pl/plpgsql/src/scan.l index 38bc8a9f98d..73258ec3648 100644 --- a/src/pl/plpgsql/src/scan.l +++ b/src/pl/plpgsql/src/scan.l @@ -9,7 +9,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/pl/plpgsql/src/scan.l,v 1.62 2008/05/09 15:36:31 petere Exp $ + * $PostgreSQL: pgsql/src/pl/plpgsql/src/scan.l,v 1.63 2008/05/13 22:10:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -120,7 +120,6 @@ close { return K_CLOSE; } constant { return K_CONSTANT; } continue { return K_CONTINUE; } cursor { return K_CURSOR; } -debug { return K_DEBUG; } declare { return K_DECLARE; } default { return K_DEFAULT; } diagnostics { return K_DIAGNOSTICS; } @@ -137,16 +136,13 @@ from { return K_FROM; } get { return K_GET; } if { return K_IF; } in { return K_IN; } -info { return K_INFO; } insert { return K_INSERT; } into { return K_INTO; } is { return K_IS; } -log { return K_LOG; } loop { return K_LOOP; } move { return K_MOVE; } no{space}+scroll { return K_NOSCROLL; } not { return K_NOT; } -notice { return K_NOTICE; } null { return K_NULL; } open { return K_OPEN; } or { return K_OR; } @@ -163,7 +159,6 @@ then { return K_THEN; } to { return K_TO; } type { return K_TYPE; } using { return K_USING; } -warning { return K_WARNING; } when { return K_WHEN; } while { return K_WHILE; } diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index da987b22e46..9f18f45126a 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -3267,7 +3267,7 @@ end; $$ language plpgsql; ERROR: cursor FOR loop must use a bound cursor variable CONTEXT: compile of PL/pgSQL function "forc_bad" near line 4 --- return query execute +-- test RETURN QUERY EXECUTE create or replace function return_dquery() returns setof int as $$ begin @@ -3285,3 +3285,132 @@ select * from return_dquery(); (4 rows) drop function return_dquery(); +-- Tests for 8.4's new RAISE features +create or replace function raise_test() returns void as $$ +begin + raise notice '% % %', 1, 2, 3 + using errcode = '55001', detail = 'some detail info', hint = 'some hint'; + raise '% % %', 1, 2, 3 + using errcode = 'division_by_zero', detail = 'some detail info'; +end; +$$ language plpgsql; +select raise_test(); +NOTICE: 1 2 3 +DETAIL: some detail info +HINT: some hint +ERROR: 1 2 3 +DETAIL: some detail info +-- Since we can't actually see the thrown SQLSTATE in default psql output, +-- test it like this; this also tests re-RAISE +create or replace function raise_test() returns void as $$ +begin + raise 'check me' + using errcode = 'division_by_zero', detail = 'some detail info'; + exception + when others then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; +select raise_test(); +NOTICE: SQLSTATE: 22012 SQLERRM: check me +ERROR: check me +DETAIL: some detail info +create or replace function raise_test() returns void as $$ +begin + raise 'check me' + using errcode = '1234F', detail = 'some detail info'; + exception + when others then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; +select raise_test(); +NOTICE: SQLSTATE: 1234F SQLERRM: check me +ERROR: check me +DETAIL: some detail info +-- SQLSTATE specification in WHEN +create or replace function raise_test() returns void as $$ +begin + raise 'check me' + using errcode = '1234F', detail = 'some detail info'; + exception + when sqlstate '1234F' then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; +select raise_test(); +NOTICE: SQLSTATE: 1234F SQLERRM: check me +ERROR: check me +DETAIL: some detail info +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero using detail = 'some detail info'; + exception + when others then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; +select raise_test(); +NOTICE: SQLSTATE: 22012 SQLERRM: division_by_zero +ERROR: division_by_zero +DETAIL: some detail info +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero; +end; +$$ language plpgsql; +select raise_test(); +ERROR: division_by_zero +create or replace function raise_test() returns void as $$ +begin + raise sqlstate '1234F'; +end; +$$ language plpgsql; +select raise_test(); +ERROR: 1234F +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero using message = 'custom' || ' message'; +end; +$$ language plpgsql; +select raise_test(); +ERROR: custom message +create or replace function raise_test() returns void as $$ +begin + raise using message = 'custom' || ' message', errcode = '22012'; +end; +$$ language plpgsql; +select raise_test(); +ERROR: custom message +-- conflict on message +create or replace function raise_test() returns void as $$ +begin + raise notice 'some message' using message = 'custom' || ' message', errcode = '22012'; +end; +$$ language plpgsql; +select raise_test(); +ERROR: RAISE option already specified: MESSAGE +CONTEXT: PL/pgSQL function "raise_test" line 2 at RAISE +-- conflict on errcode +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero using message = 'custom' || ' message', errcode = '22012'; +end; +$$ language plpgsql; +select raise_test(); +ERROR: RAISE option already specified: ERRCODE +CONTEXT: PL/pgSQL function "raise_test" line 2 at RAISE +-- nothing to re-RAISE +create or replace function raise_test() returns void as $$ +begin + raise; +end; +$$ language plpgsql; +select raise_test(); +ERROR: RAISE without parameters cannot be used outside an exception handler +CONTEXT: PL/pgSQL function "raise_test" +drop function raise_test(); diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index b0799dcdc70..f64bfabc1e5 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -2670,7 +2670,7 @@ begin end; $$ language plpgsql; --- return query execute +-- test RETURN QUERY EXECUTE create or replace function return_dquery() returns setof int as $$ @@ -2683,3 +2683,132 @@ $$ language plpgsql; select * from return_dquery(); drop function return_dquery(); + +-- Tests for 8.4's new RAISE features + +create or replace function raise_test() returns void as $$ +begin + raise notice '% % %', 1, 2, 3 + using errcode = '55001', detail = 'some detail info', hint = 'some hint'; + raise '% % %', 1, 2, 3 + using errcode = 'division_by_zero', detail = 'some detail info'; +end; +$$ language plpgsql; + +select raise_test(); + +-- Since we can't actually see the thrown SQLSTATE in default psql output, +-- test it like this; this also tests re-RAISE + +create or replace function raise_test() returns void as $$ +begin + raise 'check me' + using errcode = 'division_by_zero', detail = 'some detail info'; + exception + when others then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; + +select raise_test(); + +create or replace function raise_test() returns void as $$ +begin + raise 'check me' + using errcode = '1234F', detail = 'some detail info'; + exception + when others then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; + +select raise_test(); + +-- SQLSTATE specification in WHEN +create or replace function raise_test() returns void as $$ +begin + raise 'check me' + using errcode = '1234F', detail = 'some detail info'; + exception + when sqlstate '1234F' then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; + +select raise_test(); + +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero using detail = 'some detail info'; + exception + when others then + raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm; + raise; +end; +$$ language plpgsql; + +select raise_test(); + +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero; +end; +$$ language plpgsql; + +select raise_test(); + +create or replace function raise_test() returns void as $$ +begin + raise sqlstate '1234F'; +end; +$$ language plpgsql; + +select raise_test(); + +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero using message = 'custom' || ' message'; +end; +$$ language plpgsql; + +select raise_test(); + +create or replace function raise_test() returns void as $$ +begin + raise using message = 'custom' || ' message', errcode = '22012'; +end; +$$ language plpgsql; + +select raise_test(); + +-- conflict on message +create or replace function raise_test() returns void as $$ +begin + raise notice 'some message' using message = 'custom' || ' message', errcode = '22012'; +end; +$$ language plpgsql; + +select raise_test(); + +-- conflict on errcode +create or replace function raise_test() returns void as $$ +begin + raise division_by_zero using message = 'custom' || ' message', errcode = '22012'; +end; +$$ language plpgsql; + +select raise_test(); + +-- nothing to re-RAISE +create or replace function raise_test() returns void as $$ +begin + raise; +end; +$$ language plpgsql; + +select raise_test(); + +drop function raise_test(); -- GitLab