Add an ASSERT statement in plpgsql.
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 25 Mar 2015 23:05:20 +0000 (19:05 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
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 <xref linkend="errcodes-appendix">.  A category
      name matches any error within its category.  The special
      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
+     <literal>QUERY_CANCELED</> and <literal>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 <literal>SQLSTATE</> code; for example these are equivalent:
 <programlisting>
@@ -3387,8 +3388,12 @@ END LOOP <optional> <replaceable>label</replaceable> </optional>;
   <sect1 id="plpgsql-errors-and-messages">
    <title>Errors and Messages</title>
 
+  <sect2 id="plpgsql-statements-raise">
+   <title>Reporting Errors and Messages</title>
+
    <indexterm>
     <primary>RAISE</primary>
+    <secondary>in PL/pgSQL</secondary>
    </indexterm>
 
    <indexterm>
@@ -3580,6 +3585,67 @@ RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id;
     </para>
    </note>
 
+  </sect2>
+
+  <sect2 id="plpgsql-statements-assert">
+   <title>Checking Assertions</title>
+
+   <indexterm>
+    <primary>ASSERT</primary>
+    <secondary>in PL/pgSQL</secondary>
+   </indexterm>
+
+   <indexterm>
+    <primary>assertions</primary>
+    <secondary>in PL/pgSQL</secondary>
+   </indexterm>
+
+   <indexterm>
+    <primary><varname>plpgsql.check_asserts</> configuration parameter</primary>
+   </indexterm>
+
+   <para>
+    The <command>ASSERT</command> statement is a convenient shorthand for
+    inserting debugging checks into <application>PL/pgSQL</application>
+    functions.
+
+<synopsis>
+ASSERT <replaceable class="parameter">condition</replaceable> <optional> , <replaceable class="parameter">message</replaceable> </optional>;
+</synopsis>
+
+    The <replaceable class="parameter">condition</replaceable> is a boolean
+    expression that is expected to always evaluate to TRUE; if it does,
+    the <command>ASSERT</command> statement does nothing further.  If the
+    result is FALSE or NULL, then an <literal>ASSERT_FAILURE</> exception
+    is raised.  (If an error occurs while evaluating
+    the <replaceable class="parameter">condition</replaceable>, it is
+    reported as a normal error.)
+   </para>
+
+   <para>
+    If the optional <replaceable class="parameter">message</replaceable> is
+    provided, it is an expression whose result (if not null) replaces the
+    default error message text <quote>assertion failed</>, should
+    the <replaceable class="parameter">condition</replaceable> fail.
+    The <replaceable class="parameter">message</replaceable> expression is
+    not evaluated in the normal case where the assertion succeeds.
+   </para>
+
+   <para>
+    Testing of assertions can be enabled or disabled via the configuration
+    parameter <literal>plpgsql.check_asserts</>, which takes a boolean
+    value; the default is <literal>on</>.  If this parameter
+    is <literal>off</> then <command>ASSERT</> statements do nothing.
+   </para>
+
+   <para>
+    Note that <command>ASSERT</command> is meant for detecting program
+    bugs, not for reporting ordinary error conditions.  Use
+    the <command>RAISE</> statement, described above, for that.
+   </para>
+
+  </sect2>
+
  </sect1>
 
  <sect1 id="plpgsql-trigger">
@@ -5075,8 +5141,7 @@ $func$ LANGUAGE plpgsql;
     <productname>PostgreSQL</> does not have a built-in
     <function>instr</function> function, but you can create one
     using a combination of other
-    functions.<indexterm><primary>instr</></indexterm> In <xref
-    linkend="plpgsql-porting-appendix"> there is a
+    functions. In <xref linkend="plpgsql-porting-appendix"> there is a
     <application>PL/pgSQL</application> implementation of
     <function>instr</function> that you can use to make your porting
     easier.
@@ -5409,6 +5474,10 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE;
     your porting efforts.
    </para>
 
+   <indexterm>
+    <primary><function>instr</> function</primary>
+   </indexterm>
+
 <programlisting>
 --
 -- 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>      loop_body
 %type <stmt>   proc_stmt pl_block
 %type <stmt>   stmt_assign stmt_if stmt_loop stmt_while stmt_exit
-%type <stmt>   stmt_return stmt_raise stmt_execsql
+%type <stmt>   stmt_return stmt_raise stmt_assert stmt_execsql
 %type <stmt>   stmt_dynexecute stmt_for stmt_perform stmt_getdiag
 %type <stmt>   stmt_open stmt_fetch stmt_move stmt_close stmt_null
 %type <stmt>   stmt_case stmt_foreach_a
@@ -247,6 +247,7 @@ static      void                    check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %token <keyword>       K_ALIAS
 %token <keyword>       K_ALL
 %token <keyword>       K_ARRAY
+%token <keyword>       K_ASSERT
 %token <keyword>       K_BACKWARD
 %token <keyword>       K_BEGIN
 %token <keyword>       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;
+$$;