Hi,

Attached is a patch for supporting assertions in PL/PgSQL. These are similar to the Assert() backend macro: they can be disabled during compile time, but when enabled, abort execution if the passed expression is not true.

A simple 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


Again, I'll add this to the open commitfest, but feedback is greatly appreciated.


Regards,
Marko Tiikkaja
*** 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 ----
      </para>
     </note>
  
+   <sect2 id="plpgsql-assert">
+    <title>Assertions</title>
+ 
+    <para>
+     <literal>Assertions</literal> provide a way to check that the
+     internal state of a function is as expected.  For example:
+ <programlisting>
+ 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
+ </programlisting>
+ 
+     One could implement the equivalent functionality with a conditional
+     RAISE EXCEPTION statement, but assertions have two major differences:
+         <itemizedlist>
+          <listitem>
+            <para>
+             They're a lot faster to write than the equivalent IF .. THEN 
RAISE EXCEPTION .. END IF constructs.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+             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.
+            </para>
+          </listitem>
+         </itemizedlist> 
+    </para>
+ 
+    <para>
+     The configuration parameter <varname>plpgsql.enable_assertions</>
+     controls whether assertions are enabled.  Note that the value of
+     this parameter only affects subsequent compilations of
+     <application>PL/pgSQL</> functions, but not statements already
+     compiled in the current session.
+    </para>
+ 
+    <para>
+     It is also possible to enable assertions in a single function
+     (possibly overriding the global setting) by using a compile
+     option:
+ <programlisting>
+ 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;
+ </programlisting>
+    </para>
+   </sect2>
+ 
   </sect1>
  
   <sect1 id="plpgsql-trigger">
*** 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>  stmt_assign stmt_if stmt_loop stmt_while stmt_exit
  %type <stmt>  stmt_return stmt_raise stmt_execsql
  %type <stmt>  stmt_dynexecute stmt_for stmt_perform stmt_getdiag
+ %type <stmt>  stmt_assert
  %type <stmt>  stmt_open stmt_fetch stmt_move stmt_close stmt_null
  %type <stmt>  stmt_case stmt_foreach_a
  
***************
*** 245,250 **** static List                    *read_raise_options(void);
--- 246,252 ----
  %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
***************
*** 268,273 **** static List                    *read_raise_options(void);
--- 270,276 ----
  %token <keyword>      K_DUMP
  %token <keyword>      K_ELSE
  %token <keyword>      K_ELSIF
+ %token <keyword>      K_ENABLE_ASSERTIONS
  %token <keyword>      K_END
  %token <keyword>      K_ERRCODE
  %token <keyword>      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();
+ 
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to