diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 9fc2a2f498b..d36acf6d996 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -2562,8 +2562,9 @@ END; those shown in . A category name matches any error within its category. The special condition name OTHERS matches every error type except - QUERY_CANCELED. (It is possible, but often unwise, - to trap QUERY_CANCELED by name.) Condition names are + QUERY_CANCELED and ASSERT_FAILURE. + (It is possible, but often unwise, to trap those two error types + by name.) Condition names are not case-sensitive. Also, an error condition can be specified by SQLSTATE code; for example these are equivalent: @@ -3387,8 +3388,12 @@ END LOOP label ; Errors and Messages + + Reporting Errors and Messages + RAISE + in PL/pgSQL @@ -3580,6 +3585,67 @@ RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id; + + + + Checking Assertions + + + ASSERT + in PL/pgSQL + + + + assertions + in PL/pgSQL + + + + plpgsql.check_asserts configuration parameter + + + + The ASSERT statement is a convenient shorthand for + inserting debugging checks into PL/pgSQL + functions. + + +ASSERT condition , message ; + + + The condition is a boolean + expression that is expected to always evaluate to TRUE; if it does, + the ASSERT statement does nothing further. If the + result is FALSE or NULL, then an ASSERT_FAILURE exception + is raised. (If an error occurs while evaluating + the condition, it is + reported as a normal error.) + + + + If the optional message is + provided, it is an expression whose result (if not null) replaces the + default error message text assertion failed, should + the condition fail. + The message expression is + not evaluated in the normal case where the assertion succeeds. + + + + Testing of assertions can be enabled or disabled via the configuration + parameter plpgsql.check_asserts, which takes a boolean + value; the default is on. If this parameter + is off then ASSERT statements do nothing. + + + + Note that ASSERT is meant for detecting program + bugs, not for reporting ordinary error conditions. Use + the RAISE statement, described above, for that. + + + + @@ -5075,8 +5141,7 @@ $func$ LANGUAGE plpgsql; PostgreSQL does not have a built-in instr function, but you can create one using a combination of other - functions.instr In there is a + functions. In there is a PL/pgSQL implementation of instr that you can use to make your porting easier. @@ -5409,6 +5474,10 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; your porting efforts. + + instr function + + -- -- instr functions that mimic Oracle's counterpart diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt index 28c8c400b95..6a113b8f74c 100644 --- a/src/backend/utils/errcodes.txt +++ b/src/backend/utils/errcodes.txt @@ -454,6 +454,7 @@ P0000 E ERRCODE_PLPGSQL_ERROR plp P0001 E ERRCODE_RAISE_EXCEPTION raise_exception P0002 E ERRCODE_NO_DATA_FOUND no_data_found P0003 E ERRCODE_TOO_MANY_ROWS too_many_rows +P0004 E ERRCODE_ASSERT_FAILURE assert_failure Section: Class XX - Internal Error diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 6a9354092b3..deefb1f9de8 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -153,6 +153,8 @@ static int exec_stmt_return_query(PLpgSQL_execstate *estate, PLpgSQL_stmt_return_query *stmt); static int exec_stmt_raise(PLpgSQL_execstate *estate, PLpgSQL_stmt_raise *stmt); +static int exec_stmt_assert(PLpgSQL_execstate *estate, + PLpgSQL_stmt_assert *stmt); static int exec_stmt_execsql(PLpgSQL_execstate *estate, PLpgSQL_stmt_execsql *stmt); static int exec_stmt_dynexecute(PLpgSQL_execstate *estate, @@ -363,8 +365,8 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo, estate.err_text = NULL; /* - * Provide a more helpful message if a CONTINUE or RAISE has been used - * outside the context it can work in. + * Provide a more helpful message if a CONTINUE has been used outside + * the context it can work in. */ if (rc == PLPGSQL_RC_CONTINUE) ereport(ERROR, @@ -730,8 +732,8 @@ plpgsql_exec_trigger(PLpgSQL_function *func, estate.err_text = NULL; /* - * Provide a more helpful message if a CONTINUE or RAISE has been used - * outside the context it can work in. + * Provide a more helpful message if a CONTINUE has been used outside + * the context it can work in. */ if (rc == PLPGSQL_RC_CONTINUE) ereport(ERROR, @@ -862,8 +864,8 @@ plpgsql_exec_event_trigger(PLpgSQL_function *func, EventTriggerData *trigdata) estate.err_text = NULL; /* - * Provide a more helpful message if a CONTINUE or RAISE has been used - * outside the context it can work in. + * Provide a more helpful message if a CONTINUE has been used outside + * the context it can work in. */ if (rc == PLPGSQL_RC_CONTINUE) ereport(ERROR, @@ -1027,12 +1029,14 @@ exception_matches_conditions(ErrorData *edata, PLpgSQL_condition *cond) int sqlerrstate = cond->sqlerrstate; /* - * OTHERS matches everything *except* query-canceled; if you're - * foolish enough, you can match that explicitly. + * OTHERS matches everything *except* query-canceled and + * assert-failure. If you're foolish enough, you can match those + * explicitly. */ if (sqlerrstate == 0) { - if (edata->sqlerrcode != ERRCODE_QUERY_CANCELED) + if (edata->sqlerrcode != ERRCODE_QUERY_CANCELED && + edata->sqlerrcode != ERRCODE_ASSERT_FAILURE) return true; } /* Exact match? */ @@ -1471,6 +1475,10 @@ exec_stmt(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt) rc = exec_stmt_raise(estate, (PLpgSQL_stmt_raise *) stmt); break; + case PLPGSQL_STMT_ASSERT: + rc = exec_stmt_assert(estate, (PLpgSQL_stmt_assert *) stmt); + break; + case PLPGSQL_STMT_EXECSQL: rc = exec_stmt_execsql(estate, (PLpgSQL_stmt_execsql *) stmt); break; @@ -3117,6 +3125,48 @@ exec_stmt_raise(PLpgSQL_execstate *estate, PLpgSQL_stmt_raise *stmt) return PLPGSQL_RC_OK; } +/* ---------- + * exec_stmt_assert Assert statement + * ---------- + */ +static int +exec_stmt_assert(PLpgSQL_execstate *estate, PLpgSQL_stmt_assert *stmt) +{ + bool value; + bool isnull; + + /* do nothing when asserts are not enabled */ + if (!plpgsql_check_asserts) + return PLPGSQL_RC_OK; + + value = exec_eval_boolean(estate, stmt->cond, &isnull); + exec_eval_cleanup(estate); + + if (isnull || !value) + { + char *message = NULL; + + if (stmt->message != NULL) + { + Datum val; + Oid typeid; + int32 typmod; + + val = exec_eval_expr(estate, stmt->message, + &isnull, &typeid, &typmod); + if (!isnull) + message = convert_value_to_string(estate, val, typeid); + /* we mustn't do exec_eval_cleanup here */ + } + + ereport(ERROR, + (errcode(ERRCODE_ASSERT_FAILURE), + message ? errmsg_internal("%s", message) : + errmsg("assertion failed"))); + } + + return PLPGSQL_RC_OK; +} /* ---------- * Initialize a mostly empty execution state diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index b6023cc0144..7b26970f468 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -244,6 +244,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt) return "RETURN QUERY"; case PLPGSQL_STMT_RAISE: return "RAISE"; + case PLPGSQL_STMT_ASSERT: + return "ASSERT"; case PLPGSQL_STMT_EXECSQL: return _("SQL statement"); case PLPGSQL_STMT_DYNEXECUTE: @@ -330,6 +332,7 @@ static void free_return(PLpgSQL_stmt_return *stmt); static void free_return_next(PLpgSQL_stmt_return_next *stmt); static void free_return_query(PLpgSQL_stmt_return_query *stmt); static void free_raise(PLpgSQL_stmt_raise *stmt); +static void free_assert(PLpgSQL_stmt_assert *stmt); static void free_execsql(PLpgSQL_stmt_execsql *stmt); static void free_dynexecute(PLpgSQL_stmt_dynexecute *stmt); static void free_dynfors(PLpgSQL_stmt_dynfors *stmt); @@ -391,6 +394,9 @@ free_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_RAISE: free_raise((PLpgSQL_stmt_raise *) stmt); break; + case PLPGSQL_STMT_ASSERT: + free_assert((PLpgSQL_stmt_assert *) stmt); + break; case PLPGSQL_STMT_EXECSQL: free_execsql((PLpgSQL_stmt_execsql *) stmt); break; @@ -610,6 +616,13 @@ free_raise(PLpgSQL_stmt_raise *stmt) } } +static void +free_assert(PLpgSQL_stmt_assert *stmt) +{ + free_expr(stmt->cond); + free_expr(stmt->message); +} + static void free_execsql(PLpgSQL_stmt_execsql *stmt) { @@ -732,6 +745,7 @@ static void dump_return(PLpgSQL_stmt_return *stmt); static void dump_return_next(PLpgSQL_stmt_return_next *stmt); static void dump_return_query(PLpgSQL_stmt_return_query *stmt); static void dump_raise(PLpgSQL_stmt_raise *stmt); +static void dump_assert(PLpgSQL_stmt_assert *stmt); static void dump_execsql(PLpgSQL_stmt_execsql *stmt); static void dump_dynexecute(PLpgSQL_stmt_dynexecute *stmt); static void dump_dynfors(PLpgSQL_stmt_dynfors *stmt); @@ -804,6 +818,9 @@ dump_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_RAISE: dump_raise((PLpgSQL_stmt_raise *) stmt); break; + case PLPGSQL_STMT_ASSERT: + dump_assert((PLpgSQL_stmt_assert *) stmt); + break; case PLPGSQL_STMT_EXECSQL: dump_execsql((PLpgSQL_stmt_execsql *) stmt); break; @@ -1353,6 +1370,25 @@ dump_raise(PLpgSQL_stmt_raise *stmt) dump_indent -= 2; } +static void +dump_assert(PLpgSQL_stmt_assert *stmt) +{ + dump_ind(); + printf("ASSERT "); + dump_expr(stmt->cond); + printf("\n"); + + dump_indent += 2; + if (stmt->message != NULL) + { + dump_ind(); + printf(" MESSAGE = "); + dump_expr(stmt->message); + printf("\n"); + } + dump_indent -= 2; +} + static void dump_execsql(PLpgSQL_stmt_execsql *stmt) { diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index 46217fd64bd..4026e417a12 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -192,7 +192,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %type loop_body %type proc_stmt pl_block %type stmt_assign stmt_if stmt_loop stmt_while stmt_exit -%type stmt_return stmt_raise stmt_execsql +%type stmt_return stmt_raise stmt_assert stmt_execsql %type stmt_dynexecute stmt_for stmt_perform stmt_getdiag %type stmt_open stmt_fetch stmt_move stmt_close stmt_null %type stmt_case stmt_foreach_a @@ -247,6 +247,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %token K_ALIAS %token K_ALL %token K_ARRAY +%token K_ASSERT %token K_BACKWARD %token K_BEGIN %token K_BY @@ -871,6 +872,8 @@ proc_stmt : pl_block ';' { $$ = $1; } | stmt_raise { $$ = $1; } + | stmt_assert + { $$ = $1; } | stmt_execsql { $$ = $1; } | stmt_dynexecute @@ -1847,6 +1850,29 @@ stmt_raise : K_RAISE } ; +stmt_assert : K_ASSERT + { + PLpgSQL_stmt_assert *new; + int tok; + + new = palloc(sizeof(PLpgSQL_stmt_assert)); + + new->cmd_type = PLPGSQL_STMT_ASSERT; + new->lineno = plpgsql_location_to_lineno(@1); + + new->cond = read_sql_expression2(',', ';', + ", or ;", + &tok); + + if (tok == ',') + new->message = read_sql_expression(';', ";"); + else + new->message = NULL; + + $$ = (PLpgSQL_stmt *) new; + } + ; + loop_body : proc_sect K_END K_LOOP opt_label ';' { $$.stmts = $1; @@ -2315,6 +2341,7 @@ unreserved_keyword : K_ABSOLUTE | K_ALIAS | K_ARRAY + | K_ASSERT | K_BACKWARD | K_CLOSE | K_COLLATE diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c index 93b703418b2..266c3140686 100644 --- a/src/pl/plpgsql/src/pl_handler.c +++ b/src/pl/plpgsql/src/pl_handler.c @@ -44,6 +44,8 @@ int plpgsql_variable_conflict = PLPGSQL_RESOLVE_ERROR; bool plpgsql_print_strict_params = false; +bool plpgsql_check_asserts = true; + char *plpgsql_extra_warnings_string = NULL; char *plpgsql_extra_errors_string = NULL; int plpgsql_extra_warnings; @@ -160,6 +162,14 @@ _PG_init(void) PGC_USERSET, 0, NULL, NULL, NULL); + DefineCustomBoolVariable("plpgsql.check_asserts", + gettext_noop("Perform checks given in ASSERT statements."), + NULL, + &plpgsql_check_asserts, + true, + PGC_USERSET, 0, + NULL, NULL, NULL); + DefineCustomStringVariable("plpgsql.extra_warnings", gettext_noop("List of programming constructs that should produce a warning."), NULL, diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c index f9323771e69..dce56ce55b9 100644 --- a/src/pl/plpgsql/src/pl_scanner.c +++ b/src/pl/plpgsql/src/pl_scanner.c @@ -98,6 +98,7 @@ static const ScanKeyword unreserved_keywords[] = { PG_KEYWORD("absolute", K_ABSOLUTE, UNRESERVED_KEYWORD) PG_KEYWORD("alias", K_ALIAS, UNRESERVED_KEYWORD) PG_KEYWORD("array", K_ARRAY, UNRESERVED_KEYWORD) + PG_KEYWORD("assert", K_ASSERT, UNRESERVED_KEYWORD) PG_KEYWORD("backward", K_BACKWARD, UNRESERVED_KEYWORD) PG_KEYWORD("close", K_CLOSE, UNRESERVED_KEYWORD) PG_KEYWORD("collate", K_COLLATE, UNRESERVED_KEYWORD) @@ -607,8 +608,7 @@ plpgsql_scanner_errposition(int location) * Beware of using yyerror for other purposes, as the cursor position might * be misleading! */ -void -pg_attribute_noreturn +void pg_attribute_noreturn plpgsql_yyerror(const char *message) { char *yytext = core_yy.scanbuf + plpgsql_yylloc; diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index 66d4da61d10..f630ff822fb 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -94,6 +94,7 @@ enum PLpgSQL_stmt_types PLPGSQL_STMT_RETURN_NEXT, PLPGSQL_STMT_RETURN_QUERY, PLPGSQL_STMT_RAISE, + PLPGSQL_STMT_ASSERT, PLPGSQL_STMT_EXECSQL, PLPGSQL_STMT_DYNEXECUTE, PLPGSQL_STMT_DYNFORS, @@ -630,6 +631,13 @@ typedef struct PLpgSQL_expr *expr; } PLpgSQL_raise_option; +typedef struct +{ /* ASSERT statement */ + int cmd_type; + int lineno; + PLpgSQL_expr *cond; + PLpgSQL_expr *message; +} PLpgSQL_stmt_assert; typedef struct { /* Generic SQL statement to execute */ @@ -889,6 +897,8 @@ extern int plpgsql_variable_conflict; extern bool plpgsql_print_strict_params; +extern bool plpgsql_check_asserts; + /* extra compile-time checks */ #define PLPGSQL_XCHECK_NONE 0 #define PLPGSQL_XCHECK_SHADOWVAR 1 diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 2c0b2e5e2b1..78e5a85810e 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -5377,3 +5377,52 @@ NOTICE: outer_func() done drop function outer_outer_func(int); drop function outer_func(int); drop function inner_func(int); +-- +-- Test ASSERT +-- +do $$ +begin + assert 1=1; -- should succeed +end; +$$; +do $$ +begin + assert 1=0; -- should fail +end; +$$; +ERROR: assertion failed +CONTEXT: PL/pgSQL function inline_code_block line 3 at ASSERT +do $$ +begin + assert NULL; -- should fail +end; +$$; +ERROR: assertion failed +CONTEXT: PL/pgSQL function inline_code_block line 3 at ASSERT +-- check controlling GUC +set plpgsql.check_asserts = off; +do $$ +begin + assert 1=0; -- won't be tested +end; +$$; +reset plpgsql.check_asserts; +-- test custom message +do $$ +declare var text := 'some value'; +begin + assert 1=0, format('assertion failed, var = "%s"', var); +end; +$$; +ERROR: assertion failed, var = "some value" +CONTEXT: PL/pgSQL function inline_code_block line 4 at ASSERT +-- ensure assertions are not trapped by 'others' +do $$ +begin + assert 1=0, 'unhandled assertion'; +exception when others then + null; -- do nothing +end; +$$; +ERROR: unhandled assertion +CONTEXT: PL/pgSQL function inline_code_block line 3 at ASSERT diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index 001138eea28..e19e4153867 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -4217,3 +4217,51 @@ select outer_outer_func(20); drop function outer_outer_func(int); drop function outer_func(int); drop function inner_func(int); + +-- +-- Test ASSERT +-- + +do $$ +begin + assert 1=1; -- should succeed +end; +$$; + +do $$ +begin + assert 1=0; -- should fail +end; +$$; + +do $$ +begin + assert NULL; -- should fail +end; +$$; + +-- check controlling GUC +set plpgsql.check_asserts = off; +do $$ +begin + assert 1=0; -- won't be tested +end; +$$; +reset plpgsql.check_asserts; + +-- test custom message +do $$ +declare var text := 'some value'; +begin + assert 1=0, format('assertion failed, var = "%s"', var); +end; +$$; + +-- ensure assertions are not trapped by 'others' +do $$ +begin + assert 1=0, 'unhandled assertion'; +exception when others then + null; -- do nothing +end; +$$;