*** a/doc/src/sgml/plpgsql.sgml --- b/doc/src/sgml/plpgsql.sgml *************** *** 3528,3533 **** RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id; --- 3528,3596 ---- + + Assertions + + + Assertions provide a way to check that the + internal state of a function is as expected. For example: + + CREATE FUNCTION delete_user(username text) RETURNS VOID AS $$ + BEGIN + DELETE FROM users WHERE users.username = delete_user.username; + ASSERT FOUND; + END + $$ LANGUAGE plpgsql; + + SELECT delete_user('mia'); + ERROR: Assertion on line 4 failed + CONTEXT: PL/pgSQL function delete_user(text) line 4 at ASSERT + + + One could implement the equivalent functionality with a conditional + RAISE EXCEPTION statement, but assertions have two major differences: + + + + They're a lot faster to write than the equivalent IF .. THEN RAISE EXCEPTION .. END IF constructs. + + + + + They can be (and are by default) disabled in production + environments. A disabled assertion only incurs a negligible + compile-time overhead and no execution time overhead, so you + can write complex checks for development environments without + having to worry about performance. + + + + + + + The configuration parameter plpgsql.enable_assertions + controls whether assertions are enabled. Note that the value of + this parameter only affects subsequent compilations of + PL/pgSQL functions, but not statements already + compiled in the current session. + + + + It is also possible to enable assertions in a single function + (possibly overriding the global setting) by using a compile + option: + + CREATE FUNCTION delete_user(username text) RETURNS VOID AS $$ + #enable_assertions + BEGIN + DELETE FROM users WHERE users.username = delete_user.username; + ASSERT FOUND; + END + $$ LANGUAGE plpgsql; + + + + *** a/src/backend/utils/errcodes.txt --- b/src/backend/utils/errcodes.txt *************** *** 454,459 **** P0000 E ERRCODE_PLPGSQL_ERROR plp --- 454,460 ---- 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_ASSERTION_FAILURE assertion_failure Section: Class XX - Internal Error *** a/src/pl/plpgsql/src/pl_comp.c --- b/src/pl/plpgsql/src/pl_comp.c *************** *** 351,356 **** do_compile(FunctionCallInfo fcinfo, --- 351,357 ---- function->fn_cxt = func_cxt; function->out_param_varno = -1; /* set up for no OUT param */ function->resolve_option = plpgsql_variable_conflict; + function->enable_assertions = plpgsql_enable_assertions; if (is_dml_trigger) function->fn_is_trigger = PLPGSQL_DML_TRIGGER; *************** *** 847,852 **** plpgsql_compile_inline(char *proc_source) --- 848,854 ---- function->fn_cxt = func_cxt; function->out_param_varno = -1; /* set up for no OUT param */ function->resolve_option = plpgsql_variable_conflict; + function->enable_assertions = plpgsql_enable_assertions; plpgsql_ns_init(); plpgsql_ns_push(func_name); *** a/src/pl/plpgsql/src/pl_exec.c --- b/src/pl/plpgsql/src/pl_exec.c *************** *** 133,138 **** static int exec_stmt_dynexecute(PLpgSQL_execstate *estate, --- 133,140 ---- PLpgSQL_stmt_dynexecute *stmt); static int exec_stmt_dynfors(PLpgSQL_execstate *estate, PLpgSQL_stmt_dynfors *stmt); + static int exec_stmt_assert(PLpgSQL_execstate *estate, + PLpgSQL_stmt_assert *stmt); static void plpgsql_estate_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, *************** *** 1466,1471 **** exec_stmt(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt) --- 1468,1477 ---- rc = exec_stmt_close(estate, (PLpgSQL_stmt_close *) stmt); break; + case PLPGSQL_STMT_ASSERT: + rc = exec_stmt_assert(estate, (PLpgSQL_stmt_assert *) stmt); + break; + default: estate->err_stmt = save_estmt; elog(ERROR, "unrecognized cmdtype: %d", stmt->cmd_type); *************** *** 3629,3634 **** exec_stmt_dynfors(PLpgSQL_execstate *estate, PLpgSQL_stmt_dynfors *stmt) --- 3635,3655 ---- return rc; } + static int + exec_stmt_assert(PLpgSQL_execstate *estate, PLpgSQL_stmt_assert *stmt) + { + bool value; + bool isnull; + + value = exec_eval_boolean(estate, stmt->expr, &isnull); + exec_eval_cleanup(estate); + if (isnull || !value) + ereport(ERROR, + (errcode(ERRCODE_ASSERTION_FAILURE), + errmsg("Assertion on line %d failed", stmt->lineno))); + + return PLPGSQL_RC_OK; + } /* ---------- * exec_stmt_open Execute an OPEN cursor statement *** a/src/pl/plpgsql/src/pl_funcs.c --- b/src/pl/plpgsql/src/pl_funcs.c *************** *** 260,265 **** plpgsql_stmt_typename(PLpgSQL_stmt *stmt) --- 260,267 ---- return "CLOSE"; case PLPGSQL_STMT_PERFORM: return "PERFORM"; + case PLPGSQL_STMT_ASSERT: + return "ASSERT"; } return "unknown"; *************** *** 338,343 **** static void free_open(PLpgSQL_stmt_open *stmt); --- 340,346 ---- static void free_fetch(PLpgSQL_stmt_fetch *stmt); static void free_close(PLpgSQL_stmt_close *stmt); static void free_perform(PLpgSQL_stmt_perform *stmt); + static void free_assert(PLpgSQL_stmt_assert *stmt); static void free_expr(PLpgSQL_expr *expr); *************** *** 415,420 **** free_stmt(PLpgSQL_stmt *stmt) --- 418,426 ---- case PLPGSQL_STMT_PERFORM: free_perform((PLpgSQL_stmt_perform *) stmt); break; + case PLPGSQL_STMT_ASSERT: + free_assert((PLpgSQL_stmt_assert *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; *************** *** 563,568 **** free_perform(PLpgSQL_stmt_perform *stmt) --- 569,580 ---- } static void + free_assert(PLpgSQL_stmt_assert *stmt) + { + free_expr(stmt->expr); + } + + static void free_exit(PLpgSQL_stmt_exit *stmt) { free_expr(stmt->cond); *************** *** 742,747 **** static void dump_cursor_direction(PLpgSQL_stmt_fetch *stmt); --- 754,760 ---- static void dump_close(PLpgSQL_stmt_close *stmt); static void dump_perform(PLpgSQL_stmt_perform *stmt); static void dump_expr(PLpgSQL_expr *expr); + static void dump_assert(PLpgSQL_stmt_assert *stmt); static void *************** *** 828,833 **** dump_stmt(PLpgSQL_stmt *stmt) --- 841,849 ---- case PLPGSQL_STMT_PERFORM: dump_perform((PLpgSQL_stmt_perform *) stmt); break; + case PLPGSQL_STMT_ASSERT: + dump_assert((PLpgSQL_stmt_assert *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; *************** *** 1205,1210 **** dump_perform(PLpgSQL_stmt_perform *stmt) --- 1221,1235 ---- } static void + dump_assert(PLpgSQL_stmt_assert *stmt) + { + dump_ind(); + printf("ASSERT expr = "); + dump_expr(stmt->expr); + printf("\n"); + } + + static void dump_exit(PLpgSQL_stmt_exit *stmt) { dump_ind(); *** a/src/pl/plpgsql/src/pl_gram.y --- b/src/pl/plpgsql/src/pl_gram.y *************** *** 193,198 **** static List *read_raise_options(void); --- 193,199 ---- %type stmt_assign stmt_if stmt_loop stmt_while stmt_exit %type stmt_return stmt_raise stmt_execsql %type stmt_dynexecute stmt_for stmt_perform stmt_getdiag + %type stmt_assert %type stmt_open stmt_fetch stmt_move stmt_close stmt_null %type stmt_case stmt_foreach_a *************** *** 245,250 **** static List *read_raise_options(void); --- 246,252 ---- %token K_ALIAS %token K_ALL %token K_ARRAY + %token K_ASSERT %token K_BACKWARD %token K_BEGIN %token K_BY *************** *** 268,273 **** static List *read_raise_options(void); --- 270,276 ---- %token K_DUMP %token K_ELSE %token K_ELSIF + %token K_ENABLE_ASSERTIONS %token K_END %token K_ERRCODE %token K_ERROR *************** *** 354,359 **** comp_option : '#' K_OPTION K_DUMP --- 357,366 ---- { plpgsql_DumpExecTree = true; } + | '#' K_ENABLE_ASSERTIONS + { + plpgsql_curr_compile->enable_assertions = true; + } | '#' K_VARIABLE_CONFLICT K_ERROR { plpgsql_curr_compile->resolve_option = PLPGSQL_RESOLVE_ERROR; *************** *** 833,838 **** proc_stmt : pl_block ';' --- 840,847 ---- { $$ = $1; } | stmt_getdiag { $$ = $1; } + | stmt_assert + { $$ = $1; } | stmt_open { $$ = $1; } | stmt_fetch *************** *** 934,939 **** stmt_getdiag : K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';' --- 943,969 ---- } ; + stmt_assert : K_ASSERT expr_until_semi + { + if (plpgsql_curr_compile->enable_assertions) + { + PLpgSQL_stmt_assert *new; + + new = palloc0(sizeof(PLpgSQL_stmt_assert)); + new->cmd_type = PLPGSQL_STMT_ASSERT; + new->lineno = plpgsql_location_to_lineno(@1); + new->expr = $2; + + $$ = (PLpgSQL_stmt *)new; + } + else + { + /* skip if assertions are disabled */ + $$ = NULL; + } + } + ; + getdiag_area_opt : { $$ = false; *** a/src/pl/plpgsql/src/pl_handler.c --- b/src/pl/plpgsql/src/pl_handler.c *************** *** 36,41 **** static const struct config_enum_entry variable_conflict_options[] = { --- 36,42 ---- }; int plpgsql_variable_conflict = PLPGSQL_RESOLVE_ERROR; + bool plpgsql_enable_assertions = false; /* Hook for plugins */ PLpgSQL_plugin **plugin_ptr = NULL; *************** *** 66,71 **** _PG_init(void) --- 67,81 ---- PGC_SUSET, 0, NULL, NULL, NULL); + DefineCustomBoolVariable("plpgsql.enable_assertions", + gettext_noop("Enables assertions in PL/PgSQL functions."), + NULL, + &plpgsql_enable_assertions, + false, + PGC_USERSET, 0, + NULL, NULL, NULL); + + EmitWarningsOnPlaceholders("plpgsql"); plpgsql_HashTableInit(); *** a/src/pl/plpgsql/src/pl_scanner.c --- b/src/pl/plpgsql/src/pl_scanner.c *************** *** 60,65 **** IdentifierLookup plpgsql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL; --- 60,66 ---- static const ScanKeyword reserved_keywords[] = { PG_KEYWORD("all", K_ALL, RESERVED_KEYWORD) + PG_KEYWORD("assert", K_ASSERT, RESERVED_KEYWORD) PG_KEYWORD("begin", K_BEGIN, RESERVED_KEYWORD) PG_KEYWORD("by", K_BY, RESERVED_KEYWORD) PG_KEYWORD("case", K_CASE, RESERVED_KEYWORD) *************** *** 120,125 **** static const ScanKeyword unreserved_keywords[] = { --- 121,127 ---- PG_KEYWORD("debug", K_DEBUG, UNRESERVED_KEYWORD) PG_KEYWORD("detail", K_DETAIL, UNRESERVED_KEYWORD) PG_KEYWORD("dump", K_DUMP, UNRESERVED_KEYWORD) + PG_KEYWORD("enable_assertions", K_ENABLE_ASSERTIONS, UNRESERVED_KEYWORD) PG_KEYWORD("errcode", K_ERRCODE, UNRESERVED_KEYWORD) PG_KEYWORD("error", K_ERROR, UNRESERVED_KEYWORD) PG_KEYWORD("first", K_FIRST, UNRESERVED_KEYWORD) *** a/src/pl/plpgsql/src/plpgsql.h --- b/src/pl/plpgsql/src/plpgsql.h *************** *** 100,106 **** enum PLpgSQL_stmt_types PLPGSQL_STMT_OPEN, PLPGSQL_STMT_FETCH, PLPGSQL_STMT_CLOSE, ! PLPGSQL_STMT_PERFORM }; --- 100,107 ---- PLPGSQL_STMT_OPEN, PLPGSQL_STMT_FETCH, PLPGSQL_STMT_CLOSE, ! PLPGSQL_STMT_PERFORM, ! PLPGSQL_STMT_ASSERT }; *************** *** 403,408 **** typedef struct --- 404,416 ---- List *diag_items; /* List of PLpgSQL_diag_item */ } PLpgSQL_stmt_getdiag; + typedef struct + { /* ASSERT statement */ + int cmd_type; + int lineno; + PLpgSQL_expr *expr; + } PLpgSQL_stmt_assert; + typedef struct { /* IF statement */ *************** *** 736,741 **** typedef struct PLpgSQL_function --- 744,750 ---- int tg_tag_varno; PLpgSQL_resolve_option resolve_option; + bool enable_assertions; int ndatums; PLpgSQL_datum **datums; *************** *** 873,878 **** extern IdentifierLookup plpgsql_IdentifierLookup; --- 882,889 ---- extern int plpgsql_variable_conflict; + extern bool plpgsql_enable_assertions; + extern bool plpgsql_check_syntax; extern bool plpgsql_DumpExecTree; *** a/src/test/regress/expected/plpgsql.out --- b/src/test/regress/expected/plpgsql.out *************** *** 5085,5087 **** NOTICE: outer_func() done --- 5085,5161 ---- drop function outer_outer_func(int); drop function outer_func(int); drop function inner_func(int); + -- check assertions + set plpgsql.enable_assertions to true; + create function foof() + returns void + as $$ + begin + -- failure + assert 1 > 2; + end + $$ language plpgsql; + select foof(); + ERROR: Assertion on line 4 failed + CONTEXT: PL/pgSQL function foof() line 4 at ASSERT + drop function foof(); + create function foof() + returns void + as $$ + begin + -- no failure + assert 1 < 2; + end + $$ language plpgsql; + select foof(); + foof + ------ + + (1 row) + + drop function foof(); + create function foof() + returns void + as $$ + begin + -- NULL is failure + assert 1 < NULL; + end + $$ language plpgsql; + select foof(); + ERROR: Assertion on line 4 failed + CONTEXT: PL/pgSQL function foof() line 4 at ASSERT + reset plpgsql.enable_assertions; + -- still enabled for foof + select foof(); + ERROR: Assertion on line 4 failed + CONTEXT: PL/pgSQL function foof() line 4 at ASSERT + drop function foof(); + create function foof() + returns void + as $$ + #enable_assertions + begin + -- failure + assert 1 > 2; + end + $$ language plpgsql; + select foof(); + ERROR: Assertion on line 5 failed + CONTEXT: PL/pgSQL function foof() line 5 at ASSERT + drop function foof(); + create function foof() + returns void + as $$ + begin + -- failure, but assertions are disabled + assert 1 > 2; + end + $$ language plpgsql; + select foof(); + foof + ------ + + (1 row) + + drop function foof(); *** a/src/test/regress/sql/plpgsql.sql --- b/src/test/regress/sql/plpgsql.sql *************** *** 3983,3985 **** drop function outer_outer_func(int); --- 3983,4055 ---- drop function outer_func(int); drop function inner_func(int); + -- check assertions + + set plpgsql.enable_assertions to true; + + create function foof() + returns void + as $$ + begin + -- failure + assert 1 > 2; + end + $$ language plpgsql; + + select foof(); + drop function foof(); + + create function foof() + returns void + as $$ + begin + -- no failure + assert 1 < 2; + end + $$ language plpgsql; + + select foof(); + drop function foof(); + + create function foof() + returns void + as $$ + begin + -- NULL is failure + assert 1 < NULL; + end + $$ language plpgsql; + + select foof(); + + reset plpgsql.enable_assertions; + -- still enabled for foof + select foof(); + + drop function foof(); + + create function foof() + returns void + as $$ + #enable_assertions + begin + -- failure + assert 1 > 2; + end + $$ language plpgsql; + + select foof(); + drop function foof(); + + create function foof() + returns void + as $$ + begin + -- failure, but assertions are disabled + assert 1 > 2; + end + $$ language plpgsql; + + select foof(); + drop function foof(); +