Add an ASSERT statement in plpgsql.
authorTom Lane
Wed, 25 Mar 2015 23:05:20 +0000 (19:05 -0400)
committerTom Lane
Wed, 25 Mar 2015 23:05:32 +0000 (19:05 -0400)
This is meant to make it easier to insert simple debugging cross-checks
in plpgsql functions.

Pavel Stehule, reviewed by Jim Nasby

doc/src/sgml/plpgsql.sgml
src/backend/utils/errcodes.txt
src/pl/plpgsql/src/pl_exec.c
src/pl/plpgsql/src/pl_funcs.c
src/pl/plpgsql/src/pl_gram.y
src/pl/plpgsql/src/pl_handler.c
src/pl/plpgsql/src/pl_scanner.c
src/pl/plpgsql/src/plpgsql.h
src/test/regress/expected/plpgsql.out
src/test/regress/sql/plpgsql.sql

index 9fc2a2f498bb6ee4f30eaf7474fe7a1e7e7dd395..d36acf6d99650e992a01a5f5ea8883520de4d7e9 100644 (file)
@@ -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 
-    linkend="plpgsql-porting-appendix"> 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
index 28c8c400b95efdf605bb0d547c99c6485b10911a..6a113b8f74cae012ec2234df18940b70f6b1ccac 100644 (file)
@@ -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
 
index 6a9354092b35fc4240153c82e7746e32666f3291..deefb1f9de8db63c85f31ca8393906c56f5cac72 100644 (file)
@@ -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
index b6023cc0144e7f02744f7733bf7ab271cc6c2a73..7b26970f46848d538630715d1cea8016cad9f75f 100644 (file)
@@ -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)
 {
index 46217fd64bd7a2c109d7190152e53b69ad90b2b5..4026e417a1273801dea0eee65d47d452214a5c4d 100644 (file)
@@ -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
index 93b703418b2a148787eed759f487b4541f55c0fd..266c314068648c92eaf6f793a60ffaccb69cb778 100644 (file)
@@ -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,
index f9323771e69814ebc48318dcaab4207880d892e1..dce56ce55b96b73b5ad22c87e71ccfdb005ce79c 100644 (file)
@@ -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;
index 66d4da61d100a5884e68ed0cdb4a3e140802ecaf..f630ff822fbdc1f85ec99e4b605599709f7bf3c6 100644 (file)
@@ -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
index 2c0b2e5e2b19e582d7157318f1f8a553b36f880e..78e5a85810e26ec23ee98a219393e53e4834813d 100644 (file)
@@ -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
index 001138eea28ee0d17637ff0111836750574d8186..e19e415386775a4c3221ba180f795ce446f016ba 100644 (file)
@@ -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;
+$$;