From 4107478d374fae8299b6eb17fd13e65b7d8e026a Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Tue, 13 May 2008 22:10:30 +0000
Subject: [PATCH] Improve plpgsql's RAISE command.  It is now possible to
 attach DETAIL and HINT fields to a user-thrown error message, and to specify
 the SQLSTATE error code to use.  The syntax has also been tweaked so that the
 Oracle-compatible case "RAISE exception_name" works (though you won't get a
 very nice error message if you just write that much).  Lastly, support the
 Oracle-compatible syntax "RAISE" with no parameters to re-throw the current
 error from within an EXCEPTION block.

In passing, allow the syntax SQLSTATE 'nnnnn' within EXCEPTION lists,
so that there is a way to trap errors with custom SQLSTATE codes.

Pavel Stehule and Tom Lane
---
 doc/src/sgml/plpgsql.sgml             | 126 +++++++++---
 src/pl/plpgsql/src/gram.y             | 274 ++++++++++++++++++++------
 src/pl/plpgsql/src/pl_comp.c          |  38 +++-
 src/pl/plpgsql/src/pl_exec.c          | 241 ++++++++++++++++------
 src/pl/plpgsql/src/pl_funcs.c         |  39 +++-
 src/pl/plpgsql/src/plpgsql.h          |  31 ++-
 src/pl/plpgsql/src/scan.l             |   7 +-
 src/test/regress/expected/plpgsql.out | 131 +++++++++++-
 src/test/regress/sql/plpgsql.sql      | 131 +++++++++++-
 9 files changed, 859 insertions(+), 159 deletions(-)

diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 1065eddc74e..09ad6944dba 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/plpgsql.sgml,v 1.128 2008/05/03 00:11:36 tgl Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/plpgsql.sgml,v 1.129 2008/05/13 22:10:29 tgl Exp $ -->
 
 <chapter id="plpgsql">
   <title><application>PL/pgSQL</application> - <acronym>SQL</acronym> Procedural Language</title>
@@ -2133,7 +2133,12 @@ END;
      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
-     not case-sensitive.
+     not case-sensitive.  Also, an error condition can be specified
+     by <literal>SQLSTATE</> code; for example these are equivalent:
+<programlisting>
+        WHEN division_by_zero THEN ...
+        WHEN SQLSTATE '22012' THEN ...
+</programlisting>
     </para>
 
     <para>
@@ -2750,13 +2755,19 @@ END LOOP <optional> <replaceable>label</replaceable> </optional>;
     raise errors.
 
 <synopsis>
-RAISE <replaceable class="parameter">level</replaceable> '<replaceable class="parameter">format</replaceable>' <optional>, <replaceable class="parameter">expression</replaceable> <optional>, ...</optional></optional>;
+RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> '<replaceable class="parameter">format</replaceable>' <optional>, <replaceable class="parameter">expression</replaceable> <optional>, ...</optional></optional> <optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional> </optional>;
+RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> <replaceable class="parameter">condition_name</> <optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional> </optional>;
+RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> SQLSTATE '<replaceable class="parameter">sqlstate</>' <optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional> </optional>;
+RAISE <optional> <replaceable class="parameter">level</replaceable> </optional> USING <replaceable class="parameter">option</replaceable> = <replaceable class="parameter">expression</replaceable> <optional>, ... </optional>;
+RAISE ;
 </synopsis>
 
-    Possible levels are <literal>DEBUG</literal>,
+    The <replaceable class="parameter">level</replaceable> option specifies
+    the error severity.  Allowed levels are <literal>DEBUG</literal>,
     <literal>LOG</literal>, <literal>INFO</literal>,
     <literal>NOTICE</literal>, <literal>WARNING</literal>,
-    and <literal>EXCEPTION</literal>.
+    and <literal>EXCEPTION</literal>, with <literal>EXCEPTION</literal>
+    being the default.
     <literal>EXCEPTION</literal> raises an error (which normally aborts the
     current transaction); the other levels only generate messages of different
     priority levels.
@@ -2769,19 +2780,17 @@ RAISE <replaceable class="parameter">level</replaceable> '<replaceable class="pa
    </para>
 
    <para>
+    After <replaceable class="parameter">level</replaceable> if any,
+    you can write a <replaceable class="parameter">format</replaceable>
+    (which must be a simple string literal, not an expression).  The
+    format string specifies the error message text to be reported.
+    The format string can be followed
+    by optional argument expressions to be inserted into the message.
     Inside the format string, <literal>%</literal> is replaced by the
-    next optional argument's string representation. Write
+    string representation of the next optional argument's value. Write
     <literal>%%</literal> to emit a literal <literal>%</literal>.
-    Arguments can be simple variables or expressions,
-    but the format must be a simple string literal.
    </para>
 
-   <!--
-   This example should work, but does not:
-        RAISE NOTICE 'Id number ' || key || ' not found!';
-   Put it back when we allow non-string-literal formats.
-    -->
-
    <para>
     In this example, the value of <literal>v_job_id</> will replace the
     <literal>%</literal> in the string:
@@ -2791,19 +2800,90 @@ RAISE NOTICE 'Calling cs_create_job(%)', v_job_id;
    </para>
 
    <para>
-    This example will abort the transaction with the given error message:
+    You can attach additional information to the error report by writing
+    <literal>USING</> followed by <replaceable
+    class="parameter">option</replaceable> = <replaceable
+    class="parameter">expression</replaceable> items.  The allowed
+    <replaceable class="parameter">option</replaceable> keywords are
+    <literal>MESSAGE</>, <literal>DETAIL</>, <literal>HINT</>, and
+    <literal>ERRCODE</>, while each <replaceable
+    class="parameter">expression</replaceable> can be any string-valued
+    expression.
+    <literal>MESSAGE</> sets the error message text (this option can't
+    be used in the form of <command>RAISE</> that includes a format
+    string before <literal>USING</>).
+    <literal>DETAIL</> supplies an error detail message, while
+    <literal>HINT</> supplies a hint message.
+    <literal>ERRCODE</> specifies the error code (SQLSTATE) to report,
+    either by condition name as shown in <xref linkend="errcodes-appendix">,
+    or directly as a five-character SQLSTATE code.
+   </para>
+
+   <para>
+    This example will abort the transaction with the given error message
+    and hint:
+<programlisting>
+RAISE EXCEPTION 'Nonexistent ID --> %', user_id USING HINT = 'Please check your user id';
+</programlisting>
+   </para>
+
+   <para>
+    These two examples show equivalent ways of setting the SQLSTATE:
+<programlisting>
+RAISE 'Duplicate user ID: %', user_id USING ERRCODE = 'unique_violation';
+RAISE 'Duplicate user ID: %', user_id USING ERRCODE = '23505';
+</programlisting>
+   </para>
+
+   <para>
+    There is a second <command>RAISE</> syntax in which the main argument
+    is the condition name or SQLSTATE to be reported, for example:
+<programlisting>
+RAISE division_by_zero;
+RAISE SQLSTATE '22012';
+</programlisting>
+    In this syntax, <literal>USING</> can be used to supply a custom
+    error message, detail, or hint.  Another way to do the earlier
+    example is
 <programlisting>
-RAISE EXCEPTION 'Nonexistent ID --> %', user_id;
+RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id;
 </programlisting>
    </para>
 
+   <para>
+    Still another variant is to write <literal>RAISE USING</> or <literal>RAISE
+    <replaceable class="parameter">level</replaceable> USING</> and put
+    everything else into the <literal>USING</> list.
+   </para>
+
+   <para>
+    The last variant of <command>RAISE</> has no parameters at all.
+    This form can only be used inside a <literal>BEGIN</> block's
+    <literal>EXCEPTION</> clause;
+    it causes the error currently being handled to be re-thrown to the
+    next enclosing block.
+   </para>
+
+   <para>
+    If no condition name nor SQLSTATE is specified in a
+    <command>RAISE EXCEPTION</command> command, the default is to use
+    <literal>RAISE_EXCEPTION</> (<literal>P0001</>).  If no message
+    text is specified, the default is to use the condition name or
+    SQLSTATE as message text.
+   </para>
+
+   <note>
     <para>
-     <command>RAISE EXCEPTION</command> presently always generates
-     the same <varname>SQLSTATE</varname> code, <literal>P0001</>, no matter what message
-     it is invoked with.  It is possible to trap this exception with
-     <literal>EXCEPTION ... WHEN RAISE_EXCEPTION THEN ...</> but there
-     is no way to tell one <command>RAISE</> from another.
+     When specifying an error code by SQLSTATE code, you are not
+     limited to the predefined error codes, but can select any
+     error code consisting of five digits and/or upper-case ASCII
+     letters, other than <literal>00000</>.  It is recommended that
+     you avoid throwing error codes that end in three zeroes, because
+     these are category codes and can only be trapped by trapping
+     the whole category.
     </para>
+   </note>
+
  </sect1>
 
  <sect1 id="plpgsql-trigger">
@@ -4307,7 +4387,9 @@ $$ LANGUAGE plpgsql;
      <callout arearefs="co.plpgsql-porting-raise">
       <para>
        The syntax of <literal>RAISE</> is considerably different from
-       Oracle's similar statement.
+       Oracle's statement, although the basic case <literal>RAISE</>
+       <replaceable class="parameter">exception_name</replaceable> works
+       similarly.
       </para>
      </callout>
      <callout arearefs="co.plpgsql-porting-exception">
diff --git a/src/pl/plpgsql/src/gram.y b/src/pl/plpgsql/src/gram.y
index 979ebee3ca3..b67a8bba776 100644
--- a/src/pl/plpgsql/src/gram.y
+++ b/src/pl/plpgsql/src/gram.y
@@ -9,7 +9,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/gram.y,v 1.111 2008/05/03 00:11:36 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/gram.y,v 1.112 2008/05/13 22:10:29 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -52,6 +52,7 @@ static	void			 check_labels(const char *start_label,
 									  const char *end_label);
 static PLpgSQL_expr 	*read_cursor_args(PLpgSQL_var *cursor,
 										  int until, const char *expected);
+static List				*read_raise_options(void);
 
 %}
 
@@ -138,11 +139,7 @@ static PLpgSQL_expr 	*read_cursor_args(PLpgSQL_var *cursor,
 %type <list>	proc_exceptions
 %type <exception_block> exception_sect
 %type <exception>	proc_exception
-%type <condition>	proc_conditions
-
-
-%type <ival>	raise_level
-%type <str>		raise_msg
+%type <condition>	proc_conditions proc_condition
 
 %type <list>	getdiag_list
 %type <diagitem> getdiag_list_item
@@ -164,7 +161,6 @@ static PLpgSQL_expr 	*read_cursor_args(PLpgSQL_var *cursor,
 %token	K_CONSTANT
 %token	K_CONTINUE
 %token	K_CURSOR
-%token	K_DEBUG
 %token	K_DECLARE
 %token	K_DEFAULT
 %token	K_DIAGNOSTICS
@@ -181,16 +177,13 @@ static PLpgSQL_expr 	*read_cursor_args(PLpgSQL_var *cursor,
 %token	K_GET
 %token	K_IF
 %token	K_IN
-%token	K_INFO
 %token	K_INSERT
 %token	K_INTO
 %token	K_IS
-%token	K_LOG
 %token	K_LOOP
 %token	K_MOVE
 %token	K_NOSCROLL
 %token	K_NOT
-%token	K_NOTICE
 %token	K_NULL
 %token	K_OPEN
 %token	K_OR
@@ -207,7 +200,6 @@ static PLpgSQL_expr 	*read_cursor_args(PLpgSQL_var *cursor,
 %token	K_TO
 %token	K_TYPE
 %token	K_USING
-%token	K_WARNING
 %token	K_WHEN
 %token	K_WHILE
 
@@ -1246,7 +1238,7 @@ stmt_return		: K_RETURN lno
 					}
 				;
 
-stmt_raise		: K_RAISE lno raise_level raise_msg
+stmt_raise		: K_RAISE lno
 					{
 						PLpgSQL_stmt_raise		*new;
 						int	tok;
@@ -1255,66 +1247,130 @@ stmt_raise		: K_RAISE lno raise_level raise_msg
 
 						new->cmd_type	= PLPGSQL_STMT_RAISE;
 						new->lineno		= $2;
-						new->elog_level = $3;
-						new->message	= $4;
+						new->elog_level = ERROR;	/* default */
+						new->condname	= NULL;
+						new->message	= NULL;
 						new->params		= NIL;
+						new->options	= NIL;
 
 						tok = yylex();
+						if (tok == 0)
+							yyerror("unexpected end of function definition");
 
 						/*
-						 * We expect either a semi-colon, which
-						 * indicates no parameters, or a comma that
-						 * begins the list of parameter expressions
+						 * We could have just RAISE, meaning to re-throw
+						 * the current error.
 						 */
-						if (tok != ',' && tok != ';')
-							yyerror("syntax error");
-
-						if (tok == ',')
+						if (tok != ';')
 						{
-							do
+							/*
+							 * First is an optional elog severity level.
+							 * Most of these are not plpgsql keywords,
+							 * so we rely on examining yytext.
+							 */
+							if (pg_strcasecmp(yytext, "exception") == 0)
+							{
+								new->elog_level = ERROR;
+								tok = yylex();
+							}
+							else if (pg_strcasecmp(yytext, "warning") == 0)
+							{
+								new->elog_level = WARNING;
+								tok = yylex();
+							}
+							else if (pg_strcasecmp(yytext, "notice") == 0)
+							{
+								new->elog_level = NOTICE;
+								tok = yylex();
+							}
+							else if (pg_strcasecmp(yytext, "info") == 0)
+							{
+								new->elog_level = INFO;
+								tok = yylex();
+							}
+							else if (pg_strcasecmp(yytext, "log") == 0)
+							{
+								new->elog_level = LOG;
+								tok = yylex();
+							}
+							else if (pg_strcasecmp(yytext, "debug") == 0)
 							{
-								PLpgSQL_expr *expr;
+								new->elog_level = DEBUG1;
+								tok = yylex();
+							}
+							if (tok == 0)
+								yyerror("unexpected end of function definition");
 
-								expr = read_sql_expression2(',', ';',
-															", or ;",
-															&tok);
-								new->params = lappend(new->params, expr);
-							} while (tok == ',');
-						}
+							/*
+							 * Next we can have a condition name, or
+							 * equivalently SQLSTATE 'xxxxx', or a string
+							 * literal that is the old-style message format,
+							 * or USING to start the option list immediately.
+							 */
+							if (tok == T_STRING)
+							{
+								/* old style message and parameters */
+								new->message = plpgsql_get_string_value();
+								/*
+								 * We expect either a semi-colon, which
+								 * indicates no parameters, or a comma that
+								 * begins the list of parameter expressions,
+								 * or USING to begin the options list.
+								 */
+								tok = yylex();
+								if (tok != ',' && tok != ';' && tok != K_USING)
+									yyerror("syntax error");
 
-						$$ = (PLpgSQL_stmt *)new;
-					}
-				;
+								while (tok == ',')
+								{
+									PLpgSQL_expr *expr;
 
-raise_msg		: T_STRING
-					{
-						$$ = plpgsql_get_string_value();
-					}
-				;
+									expr = read_sql_construct(',', ';', K_USING,
+															  ", or ; or USING",
+															  "SELECT ",
+															  true, true, &tok);
+									new->params = lappend(new->params, expr);
+								}
+							}
+							else if (tok != K_USING)
+							{
+								/* must be condition name or SQLSTATE */
+								if (pg_strcasecmp(yytext, "sqlstate") == 0)
+								{
+									/* next token should be a string literal */
+									char   *sqlstatestr;
+
+									if (yylex() != T_STRING)
+										yyerror("syntax error");
+									sqlstatestr = plpgsql_get_string_value();
+
+									if (strlen(sqlstatestr) != 5)
+										yyerror("invalid SQLSTATE code");
+									if (strspn(sqlstatestr, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != 5)
+										yyerror("invalid SQLSTATE code");
+									new->condname = sqlstatestr;
+								}
+								else
+								{
+									char   *cname;
+
+									if (tok != T_WORD)
+										yyerror("syntax error");
+									plpgsql_convert_ident(yytext, &cname, 1);
+									plpgsql_recognize_err_condition(cname,
+																	false);
+									new->condname = cname;
+								}
+								tok = yylex();
+								if (tok != ';' && tok != K_USING)
+									yyerror("syntax error");
+							}
 
-raise_level		: K_EXCEPTION
-					{
-						$$ = ERROR;
-					}
-				| K_WARNING
-					{
-						$$ = WARNING;
-					}
-				| K_NOTICE
-					{
-						$$ = NOTICE;
-					}
-				| K_INFO
-					{
-						$$ = INFO;
-					}
-				| K_LOG
-					{
-						$$ = LOG;
-					}
-				| K_DEBUG
-					{
-						$$ = DEBUG1;
+							if (tok == K_USING)
+								new->options = read_raise_options();
+						}
+
+						$$ = (PLpgSQL_stmt *)new;
 					}
 				;
 
@@ -1592,20 +1648,61 @@ proc_exception	: K_WHEN lno proc_conditions K_THEN proc_sect
 					}
 				;
 
-proc_conditions	: proc_conditions K_OR opt_lblname
+proc_conditions	: proc_conditions K_OR proc_condition
 						{
 							PLpgSQL_condition	*old;
 
 							for (old = $1; old->next != NULL; old = old->next)
 								/* skip */ ;
-							old->next = plpgsql_parse_err_condition($3);
-
+							old->next = $3;
 							$$ = $1;
 						}
-				| opt_lblname
+				| proc_condition
+						{
+							$$ = $1;
+						}
+				;
+
+proc_condition	: opt_lblname
 						{
 							$$ = plpgsql_parse_err_condition($1);
 						}
+				| T_SCALAR
+						{
+							/*
+							 * Because we know the special sqlstate variable
+							 * is at the top of the namestack (see the
+							 * exception_sect production), the SQLSTATE
+							 * keyword will always lex as T_SCALAR.  This
+							 * might not be true in other parsing contexts!
+							 */
+							PLpgSQL_condition *new;
+							char   *sqlstatestr;
+
+							if (pg_strcasecmp(yytext, "sqlstate") != 0)
+								yyerror("syntax error");
+
+							/* next token should be a string literal */
+							if (yylex() != T_STRING)
+								yyerror("syntax error");
+							sqlstatestr = plpgsql_get_string_value();
+
+							if (strlen(sqlstatestr) != 5)
+								yyerror("invalid SQLSTATE code");
+							if (strspn(sqlstatestr, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != 5)
+								yyerror("invalid SQLSTATE code");
+
+							new = palloc(sizeof(PLpgSQL_condition));
+							new->sqlerrstate = MAKE_SQLSTATE(sqlstatestr[0],
+															 sqlstatestr[1],
+															 sqlstatestr[2],
+															 sqlstatestr[3],
+															 sqlstatestr[4]);
+							new->condname = sqlstatestr;
+							new->next = NULL;
+
+							$$ = new;
+						}
 				;
 
 expr_until_semi :
@@ -2658,6 +2755,55 @@ read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
 	return expr;
 }
 
+/*
+ * Parse RAISE ... USING options
+ */
+static List *
+read_raise_options(void)
+{
+	List	   *result = NIL;
+
+	for (;;)
+	{
+		PLpgSQL_raise_option *opt;
+		int		tok;
+
+		if ((tok = yylex()) == 0)
+			yyerror("unexpected end of function definition");
+
+		opt = (PLpgSQL_raise_option *) palloc(sizeof(PLpgSQL_raise_option));
+
+		if (pg_strcasecmp(yytext, "errcode") == 0)
+			opt->opt_type = PLPGSQL_RAISEOPTION_ERRCODE;
+		else if (pg_strcasecmp(yytext, "message") == 0)
+			opt->opt_type = PLPGSQL_RAISEOPTION_MESSAGE;
+		else if (pg_strcasecmp(yytext, "detail") == 0)
+			opt->opt_type = PLPGSQL_RAISEOPTION_DETAIL;
+		else if (pg_strcasecmp(yytext, "hint") == 0)
+			opt->opt_type = PLPGSQL_RAISEOPTION_HINT;
+		else
+		{
+			plpgsql_error_lineno = plpgsql_scanner_lineno();
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("unrecognized RAISE statement option \"%s\"",
+							yytext)));
+		}
+
+		if (yylex() != K_ASSIGN)
+			yyerror("syntax error, expected \"=\"");
+
+		opt->expr = read_sql_expression2(',', ';', ", or ;", &tok);
+
+		result = lappend(result, opt);
+
+		if (tok == ';')
+			break;
+	}
+
+	return result;
+}
+
 
 /* Needed to avoid conflict between different prefix settings: */
 #undef yylex
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index 2252618ad88..737bac58881 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_comp.c,v 1.125 2008/05/12 00:00:54 alvherre Exp $
+ *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_comp.c,v 1.126 2008/05/13 22:10:29 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -1749,6 +1749,42 @@ build_datatype(HeapTuple typeTup, int32 typmod)
 	return typ;
 }
 
+/*
+ *  plpgsql_recognize_err_condition
+ * 		Check condition name and translate it to SQLSTATE.
+ *
+ * Note: there are some cases where the same condition name has multiple
+ * entries in the table.  We arbitrarily return the first match.
+ */
+int
+plpgsql_recognize_err_condition(const char *condname, bool allow_sqlstate)
+{
+	int			i;
+
+	if (allow_sqlstate)
+	{
+		if (strlen(condname) == 5 &&
+			strspn(condname, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 5)
+			return MAKE_SQLSTATE(condname[0],
+								 condname[1],
+								 condname[2],
+								 condname[3],
+								 condname[4]);
+	}
+
+	for (i = 0; exception_label_map[i].label != NULL; i++)
+	{
+		if (strcmp(condname, exception_label_map[i].label) == 0)
+			return exception_label_map[i].sqlerrstate;
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_UNDEFINED_OBJECT),
+			 errmsg("unrecognized exception condition \"%s\"",
+					condname)));
+	return 0;					/* keep compiler quiet */
+}
+
 /*
  * plpgsql_parse_err_condition
  *		Generate PLpgSQL_condition entry(s) for an exception condition name
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index b248384af41..2ba45befb71 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_exec.c,v 1.213 2008/05/12 20:02:02 alvherre Exp $
+ *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_exec.c,v 1.214 2008/05/13 22:10:30 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -316,13 +316,17 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo)
 		estate.err_text = NULL;
 
 		/*
-		 * Provide a more helpful message if a CONTINUE has been used outside
-		 * a loop.
+		 * Provide a more helpful message if a CONTINUE or RAISE has been used
+		 * outside the context it can work in.
 		 */
 		if (rc == PLPGSQL_RC_CONTINUE)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("CONTINUE cannot be used outside a loop")));
+		else if (rc == PLPGSQL_RC_RERAISE)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("RAISE without parameters cannot be used outside an exception handler")));
 		else
 			ereport(ERROR,
 			   (errcode(ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT),
@@ -662,13 +666,17 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
 		estate.err_text = NULL;
 
 		/*
-		 * Provide a more helpful message if a CONTINUE has been used outside
-		 * a loop.
+		 * Provide a more helpful message if a CONTINUE or RAISE has been used
+		 * outside the context it can work in.
 		 */
 		if (rc == PLPGSQL_RC_CONTINUE)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
 					 errmsg("CONTINUE cannot be used outside a loop")));
+		else if (rc == PLPGSQL_RC_RERAISE)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("RAISE without parameters cannot be used outside an exception handler")));
 		else
 			ereport(ERROR,
 			   (errcode(ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT),
@@ -1109,6 +1117,11 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block)
 					free_var(errm_var);
 					errm_var->value = (Datum) 0;
 					errm_var->isnull = true;
+
+					/* re-throw error if requested by handler */
+					if (rc == PLPGSQL_RC_RERAISE)
+						ReThrowError(edata);
+
 					break;
 				}
 			}
@@ -1139,8 +1152,9 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block)
 	switch (rc)
 	{
 		case PLPGSQL_RC_OK:
-		case PLPGSQL_RC_CONTINUE:
 		case PLPGSQL_RC_RETURN:
+		case PLPGSQL_RC_CONTINUE:
+		case PLPGSQL_RC_RERAISE:
 			return rc;
 
 		case PLPGSQL_RC_EXIT:
@@ -1469,7 +1483,8 @@ exec_stmt_loop(PLpgSQL_execstate *estate, PLpgSQL_stmt_loop *stmt)
 				break;
 
 			case PLPGSQL_RC_RETURN:
-				return PLPGSQL_RC_RETURN;
+			case PLPGSQL_RC_RERAISE:
+				return rc;
 
 			default:
 				elog(ERROR, "unrecognized rc: %d", rc);
@@ -1532,7 +1547,8 @@ exec_stmt_while(PLpgSQL_execstate *estate, PLpgSQL_stmt_while *stmt)
 				break;
 
 			case PLPGSQL_RC_RETURN:
-				return PLPGSQL_RC_RETURN;
+			case PLPGSQL_RC_RERAISE:
+				return rc;
 
 			default:
 				elog(ERROR, "unrecognized rc: %d", rc);
@@ -1650,8 +1666,9 @@ exec_stmt_fori(PLpgSQL_execstate *estate, PLpgSQL_stmt_fori *stmt)
 		 */
 		rc = exec_stmts(estate, stmt->body);
 
-		if (rc == PLPGSQL_RC_RETURN)
-			break;				/* return from function */
+		if (rc == PLPGSQL_RC_RETURN ||
+			rc == PLPGSQL_RC_RERAISE)
+			break;				/* break out of the loop */
 		else if (rc == PLPGSQL_RC_EXIT)
 		{
 			if (estate->exitlabel == NULL)
@@ -2267,64 +2284,163 @@ exec_init_tuple_store(PLpgSQL_execstate *estate)
 static int
 exec_stmt_raise(PLpgSQL_execstate *estate, PLpgSQL_stmt_raise *stmt)
 {
-	char	   *cp;
-	PLpgSQL_dstring ds;
-	ListCell   *current_param;
+	int			err_code = 0;
+	char	   *condname = NULL;
+	char	   *err_message = NULL;
+	char	   *err_detail = NULL;
+	char	   *err_hint = NULL;
+	ListCell   *lc;
 
-	plpgsql_dstring_init(&ds);
-	current_param = list_head(stmt->params);
+	/* RAISE with no parameters: re-throw current exception */
+	if (stmt->condname == NULL && stmt->message == NULL &&
+		stmt->options == NIL)
+		return PLPGSQL_RC_RERAISE;
 
-	for (cp = stmt->message; *cp; cp++)
+	if (stmt->condname)
 	{
-		/*
-		 * Occurrences of a single % are replaced by the next parameter's
-		 * external representation. Double %'s are converted to one %.
-		 */
-		if (cp[0] == '%')
-		{
-			Oid			paramtypeid;
-			Datum		paramvalue;
-			bool		paramisnull;
-			char	   *extval;
+		err_code = plpgsql_recognize_err_condition(stmt->condname, true);
+		condname = pstrdup(stmt->condname);
+	}
+
+	if (stmt->message)
+	{
+		PLpgSQL_dstring ds;
+		ListCell   *current_param;
+		char	   *cp;
+
+		plpgsql_dstring_init(&ds);
+		current_param = list_head(stmt->params);
 
-			if (cp[1] == '%')
+		for (cp = stmt->message; *cp; cp++)
+		{
+			/*
+			 * Occurrences of a single % are replaced by the next parameter's
+			 * external representation. Double %'s are converted to one %.
+			 */
+			if (cp[0] == '%')
 			{
-				plpgsql_dstring_append_char(&ds, cp[1]);
-				cp++;
-				continue;
-			}
+				Oid			paramtypeid;
+				Datum		paramvalue;
+				bool		paramisnull;
+				char	   *extval;
 
-			if (current_param == NULL)
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("too few parameters specified for RAISE")));
+				if (cp[1] == '%')
+				{
+					plpgsql_dstring_append_char(&ds, cp[1]);
+					cp++;
+					continue;
+				}
 
-			paramvalue = exec_eval_expr(estate,
-									  (PLpgSQL_expr *) lfirst(current_param),
-										&paramisnull,
-										&paramtypeid);
+				if (current_param == NULL)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("too few parameters specified for RAISE")));
+
+				paramvalue = exec_eval_expr(estate,
+											(PLpgSQL_expr *) lfirst(current_param),
+											&paramisnull,
+											&paramtypeid);
 
-			if (paramisnull)
-				extval = "<NULL>";
+				if (paramisnull)
+					extval = "<NULL>";
+				else
+					extval = convert_value_to_string(paramvalue, paramtypeid);
+				plpgsql_dstring_append(&ds, extval);
+				current_param = lnext(current_param);
+				exec_eval_cleanup(estate);
+			}
 			else
-				extval = convert_value_to_string(paramvalue, paramtypeid);
-			plpgsql_dstring_append(&ds, extval);
-			current_param = lnext(current_param);
-			exec_eval_cleanup(estate);
-			continue;
+				plpgsql_dstring_append_char(&ds, cp[0]);
 		}
 
-		plpgsql_dstring_append_char(&ds, cp[0]);
+		/*
+		 * If more parameters were specified than were required to process the
+		 * format string, throw an error
+		 */
+		if (current_param != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("too many parameters specified for RAISE")));
+
+		err_message = plpgsql_dstring_get(&ds);
+		/* No dstring_free here, the pfree(err_message) does it */
 	}
 
-	/*
-	 * If more parameters were specified than were required to process the
-	 * format string, throw an error
-	 */
-	if (current_param != NULL)
-		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("too many parameters specified for RAISE")));
+	foreach(lc, stmt->options)
+	{
+		PLpgSQL_raise_option *opt = (PLpgSQL_raise_option *) lfirst(lc);
+		Datum		optionvalue;
+		bool		optionisnull;
+		Oid			optiontypeid;
+		char	   *extval;
+
+		optionvalue = exec_eval_expr(estate, opt->expr,
+									 &optionisnull,
+									 &optiontypeid);
+		if (optionisnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 errmsg("RAISE statement option cannot be NULL")));
+
+		extval = convert_value_to_string(optionvalue, optiontypeid);
+
+		switch (opt->opt_type)
+		{
+			case PLPGSQL_RAISEOPTION_ERRCODE:
+				if (err_code)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("RAISE option already specified: %s",
+									"ERRCODE")));
+				err_code = plpgsql_recognize_err_condition(extval, true);
+				condname = pstrdup(extval);
+				break;
+			case PLPGSQL_RAISEOPTION_MESSAGE:
+				if (err_message)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("RAISE option already specified: %s",
+									"MESSAGE")));
+				err_message = pstrdup(extval);
+				break;
+			case PLPGSQL_RAISEOPTION_DETAIL:
+				if (err_detail)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("RAISE option already specified: %s",
+									"DETAIL")));
+				err_detail = pstrdup(extval);
+				break;
+			case PLPGSQL_RAISEOPTION_HINT:
+				if (err_hint)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("RAISE option already specified: %s",
+									"HINT")));
+				err_hint = pstrdup(extval);
+				break;
+			default:
+				elog(ERROR, "unrecognized raise option: %d", opt->opt_type);
+		}
+
+		exec_eval_cleanup(estate);
+	}
+
+	/* Default code if nothing specified */
+	if (err_code == 0 && stmt->elog_level >= ERROR)
+		err_code = ERRCODE_RAISE_EXCEPTION;
+
+	/* Default error message if nothing specified */
+	if (err_message == NULL)
+	{
+		if (condname)
+		{
+			err_message = condname;
+			condname = NULL;
+		}
+		else
+			err_message = pstrdup(unpack_sql_state(err_code));
+	}
 
 	/*
 	 * Throw the error (may or may not come back)
@@ -2332,12 +2448,21 @@ exec_stmt_raise(PLpgSQL_execstate *estate, PLpgSQL_stmt_raise *stmt)
 	estate->err_text = raise_skip_msg;	/* suppress traceback of raise */
 
 	ereport(stmt->elog_level,
-		 ((stmt->elog_level >= ERROR) ? errcode(ERRCODE_RAISE_EXCEPTION) : 0,
-		  errmsg_internal("%s", plpgsql_dstring_get(&ds))));
+			(err_code ? errcode(err_code) : 0,
+			 errmsg_internal("%s", err_message),
+			 (err_detail != NULL) ? errdetail(err_detail) : 0,
+			 (err_hint != NULL) ? errhint(err_hint) : 0));
 
 	estate->err_text = NULL;	/* un-suppress... */
 
-	plpgsql_dstring_free(&ds);
+	if (condname != NULL)
+		pfree(condname);
+	if (err_message != NULL)
+		pfree(err_message);
+	if (err_detail != NULL)
+		pfree(err_detail);
+	if (err_hint != NULL)
+		pfree(err_hint);
 
 	return PLPGSQL_RC_OK;
 }
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index f7624a6c33d..cb9e9c99ec3 100644
--- a/src/pl/plpgsql/src/pl_funcs.c
+++ b/src/pl/plpgsql/src/pl_funcs.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_funcs.c,v 1.70 2008/05/03 00:11:36 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/pl_funcs.c,v 1.71 2008/05/13 22:10:30 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -1003,7 +1003,12 @@ dump_raise(PLpgSQL_stmt_raise *stmt)
 	int			i = 0;
 
 	dump_ind();
-	printf("RAISE '%s'\n", stmt->message);
+	printf("RAISE level=%d", stmt->elog_level);
+	if (stmt->condname)
+		printf(" condname='%s'", stmt->condname);
+	if (stmt->message)
+		printf(" message='%s'", stmt->message);
+	printf("\n");
 	dump_indent += 2;
 	foreach(lc, stmt->params)
 	{
@@ -1012,6 +1017,36 @@ dump_raise(PLpgSQL_stmt_raise *stmt)
 		dump_expr((PLpgSQL_expr *) lfirst(lc));
 		printf("\n");
 	}
+	if (stmt->options)
+	{
+		dump_ind();
+		printf("    USING\n");
+		dump_indent += 2;
+		foreach(lc, stmt->options)
+		{
+			PLpgSQL_raise_option *opt = (PLpgSQL_raise_option *) lfirst(lc);
+		
+			dump_ind();
+			switch (opt->opt_type)
+			{
+				case PLPGSQL_RAISEOPTION_ERRCODE:
+					printf("    ERRCODE = ");
+					break;
+				case PLPGSQL_RAISEOPTION_MESSAGE:
+					printf("    MESSAGE = ");
+					break;	
+				case PLPGSQL_RAISEOPTION_DETAIL:
+					printf("    DETAIL = ");
+					break;
+				case PLPGSQL_RAISEOPTION_HINT:
+					printf("    HINT = ");
+					break;
+			}
+			dump_expr(opt->expr);
+			printf("\n");
+		}		
+		dump_indent -= 2;
+	}
 	dump_indent -= 2;
 }
 
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index f1206932bb5..53d691a596a 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/plpgsql.h,v 1.98 2008/05/03 00:11:36 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/plpgsql.h,v 1.99 2008/05/13 22:10:30 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -106,7 +106,8 @@ enum
 	PLPGSQL_RC_OK,
 	PLPGSQL_RC_EXIT,
 	PLPGSQL_RC_RETURN,
-	PLPGSQL_RC_CONTINUE
+	PLPGSQL_RC_CONTINUE,
+	PLPGSQL_RC_RERAISE
 };
 
 /* ----------
@@ -119,6 +120,18 @@ enum
 	PLPGSQL_GETDIAG_RESULT_OID
 };
 
+/* --------
+ * RAISE statement options
+ * --------
+ */
+enum
+{
+	PLPGSQL_RAISEOPTION_ERRCODE,
+	PLPGSQL_RAISEOPTION_MESSAGE,
+	PLPGSQL_RAISEOPTION_DETAIL,
+	PLPGSQL_RAISEOPTION_HINT
+};
+
 
 /**********************************************************************
  * Node and structure definitions
@@ -539,10 +552,18 @@ typedef struct
 	int			cmd_type;
 	int			lineno;
 	int			elog_level;
-	char	   *message;
-	List	   *params;			/* list of expressions */
+	char	   *condname;		/* condition name, SQLSTATE, or NULL */
+	char	   *message;		/* old-style message format literal, or NULL */
+	List	   *params;			/* list of expressions for old-style message */
+	List	   *options;		/* list of PLpgSQL_raise_option */
 } PLpgSQL_stmt_raise;
 
+typedef struct
+{								/* RAISE statement option */
+	int 		opt_type;
+	PLpgSQL_expr *expr;
+} PLpgSQL_raise_option;
+
 
 typedef struct
 {								/* Generic SQL statement to execute */
@@ -772,6 +793,8 @@ extern PLpgSQL_variable *plpgsql_build_variable(const char *refname, int lineno,
 					   bool add2namespace);
 extern PLpgSQL_rec *plpgsql_build_record(const char *refname, int lineno,
 										 bool add2namespace);
+extern int	plpgsql_recognize_err_condition(const char *condname,
+											bool allow_sqlstate);
 extern PLpgSQL_condition *plpgsql_parse_err_condition(char *condname);
 extern void plpgsql_adddatum(PLpgSQL_datum *new);
 extern int	plpgsql_add_initdatums(int **varnos);
diff --git a/src/pl/plpgsql/src/scan.l b/src/pl/plpgsql/src/scan.l
index 38bc8a9f98d..73258ec3648 100644
--- a/src/pl/plpgsql/src/scan.l
+++ b/src/pl/plpgsql/src/scan.l
@@ -9,7 +9,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/scan.l,v 1.62 2008/05/09 15:36:31 petere Exp $
+ *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/scan.l,v 1.63 2008/05/13 22:10:30 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -120,7 +120,6 @@ close			{ return K_CLOSE;			}
 constant		{ return K_CONSTANT;		}
 continue		{ return K_CONTINUE;		}
 cursor			{ return K_CURSOR;			}
-debug			{ return K_DEBUG;			}
 declare			{ return K_DECLARE;			}
 default			{ return K_DEFAULT;			}
 diagnostics		{ return K_DIAGNOSTICS;		}
@@ -137,16 +136,13 @@ from			{ return K_FROM;			}
 get				{ return K_GET;				}
 if				{ return K_IF;				}
 in				{ return K_IN;				}
-info			{ return K_INFO;			}
 insert			{ return K_INSERT;			}
 into			{ return K_INTO;			}
 is				{ return K_IS;				}
-log				{ return K_LOG;				}
 loop			{ return K_LOOP;			}
 move			{ return K_MOVE;			}
 no{space}+scroll { return K_NOSCROLL;		}
 not				{ return K_NOT;				}
-notice			{ return K_NOTICE;			}
 null			{ return K_NULL;			}
 open			{ return K_OPEN;			}
 or				{ return K_OR;				}
@@ -163,7 +159,6 @@ then			{ return K_THEN;			}
 to				{ return K_TO;				}
 type			{ return K_TYPE;			}
 using			{ return K_USING;			}
-warning			{ return K_WARNING;			}
 when			{ return K_WHEN;			}
 while			{ return K_WHILE;			}
 
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index da987b22e46..9f18f45126a 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -3267,7 +3267,7 @@ end;
 $$ language plpgsql;
 ERROR:  cursor FOR loop must use a bound cursor variable
 CONTEXT:  compile of PL/pgSQL function "forc_bad" near line 4
--- return query execute
+-- test RETURN QUERY EXECUTE
 create or replace function return_dquery()
 returns setof int as $$
 begin
@@ -3285,3 +3285,132 @@ select * from return_dquery();
 (4 rows)
 
 drop function return_dquery();
+-- Tests for 8.4's new RAISE features
+create or replace function raise_test() returns void as $$
+begin
+  raise notice '% % %', 1, 2, 3 
+     using errcode = '55001', detail = 'some detail info', hint = 'some hint';
+  raise '% % %', 1, 2, 3 
+     using errcode = 'division_by_zero', detail = 'some detail info';
+end;
+$$ language plpgsql;
+select raise_test();
+NOTICE:  1 2 3
+DETAIL:  some detail info
+HINT:  some hint
+ERROR:  1 2 3
+DETAIL:  some detail info
+-- Since we can't actually see the thrown SQLSTATE in default psql output,
+-- test it like this; this also tests re-RAISE
+create or replace function raise_test() returns void as $$
+begin
+  raise 'check me'
+     using errcode = 'division_by_zero', detail = 'some detail info';
+  exception
+    when others then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+select raise_test();
+NOTICE:  SQLSTATE: 22012 SQLERRM: check me
+ERROR:  check me
+DETAIL:  some detail info
+create or replace function raise_test() returns void as $$
+begin
+  raise 'check me'
+     using errcode = '1234F', detail = 'some detail info';
+  exception
+    when others then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+select raise_test();
+NOTICE:  SQLSTATE: 1234F SQLERRM: check me
+ERROR:  check me
+DETAIL:  some detail info
+-- SQLSTATE specification in WHEN
+create or replace function raise_test() returns void as $$
+begin
+  raise 'check me'
+     using errcode = '1234F', detail = 'some detail info';
+  exception
+    when sqlstate '1234F' then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+select raise_test();
+NOTICE:  SQLSTATE: 1234F SQLERRM: check me
+ERROR:  check me
+DETAIL:  some detail info
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero using detail = 'some detail info';
+  exception
+    when others then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+select raise_test();
+NOTICE:  SQLSTATE: 22012 SQLERRM: division_by_zero
+ERROR:  division_by_zero
+DETAIL:  some detail info
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero;
+end;
+$$ language plpgsql;
+select raise_test();
+ERROR:  division_by_zero
+create or replace function raise_test() returns void as $$
+begin
+  raise sqlstate '1234F';
+end;
+$$ language plpgsql;
+select raise_test();
+ERROR:  1234F
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero using message = 'custom' || ' message';
+end;
+$$ language plpgsql;
+select raise_test();
+ERROR:  custom message
+create or replace function raise_test() returns void as $$
+begin
+  raise using message = 'custom' || ' message', errcode = '22012';
+end;
+$$ language plpgsql;
+select raise_test();
+ERROR:  custom message
+-- conflict on message
+create or replace function raise_test() returns void as $$
+begin
+  raise notice 'some message' using message = 'custom' || ' message', errcode = '22012';
+end;
+$$ language plpgsql;
+select raise_test();
+ERROR:  RAISE option already specified: MESSAGE
+CONTEXT:  PL/pgSQL function "raise_test" line 2 at RAISE
+-- conflict on errcode
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero using message = 'custom' || ' message', errcode = '22012';
+end;
+$$ language plpgsql;
+select raise_test();
+ERROR:  RAISE option already specified: ERRCODE
+CONTEXT:  PL/pgSQL function "raise_test" line 2 at RAISE
+-- nothing to re-RAISE
+create or replace function raise_test() returns void as $$
+begin
+  raise;
+end;
+$$ language plpgsql;
+select raise_test();
+ERROR:  RAISE without parameters cannot be used outside an exception handler
+CONTEXT:  PL/pgSQL function "raise_test"
+drop function raise_test();
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index b0799dcdc70..f64bfabc1e5 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -2670,7 +2670,7 @@ begin
 end;
 $$ language plpgsql;
 
--- return query execute
+-- test RETURN QUERY EXECUTE
 
 create or replace function return_dquery()
 returns setof int as $$
@@ -2683,3 +2683,132 @@ $$ language plpgsql;
 select * from return_dquery();
 
 drop function return_dquery();
+
+-- Tests for 8.4's new RAISE features
+
+create or replace function raise_test() returns void as $$
+begin
+  raise notice '% % %', 1, 2, 3 
+     using errcode = '55001', detail = 'some detail info', hint = 'some hint';
+  raise '% % %', 1, 2, 3 
+     using errcode = 'division_by_zero', detail = 'some detail info';
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+-- Since we can't actually see the thrown SQLSTATE in default psql output,
+-- test it like this; this also tests re-RAISE
+
+create or replace function raise_test() returns void as $$
+begin
+  raise 'check me'
+     using errcode = 'division_by_zero', detail = 'some detail info';
+  exception
+    when others then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+create or replace function raise_test() returns void as $$
+begin
+  raise 'check me'
+     using errcode = '1234F', detail = 'some detail info';
+  exception
+    when others then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+-- SQLSTATE specification in WHEN
+create or replace function raise_test() returns void as $$
+begin
+  raise 'check me'
+     using errcode = '1234F', detail = 'some detail info';
+  exception
+    when sqlstate '1234F' then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero using detail = 'some detail info';
+  exception
+    when others then
+      raise notice 'SQLSTATE: % SQLERRM: %', sqlstate, sqlerrm;
+      raise;
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero;
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+create or replace function raise_test() returns void as $$
+begin
+  raise sqlstate '1234F';
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero using message = 'custom' || ' message';
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+create or replace function raise_test() returns void as $$
+begin
+  raise using message = 'custom' || ' message', errcode = '22012';
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+-- conflict on message
+create or replace function raise_test() returns void as $$
+begin
+  raise notice 'some message' using message = 'custom' || ' message', errcode = '22012';
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+-- conflict on errcode
+create or replace function raise_test() returns void as $$
+begin
+  raise division_by_zero using message = 'custom' || ' message', errcode = '22012';
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+-- nothing to re-RAISE
+create or replace function raise_test() returns void as $$
+begin
+  raise;
+end;
+$$ language plpgsql;
+
+select raise_test();
+
+drop function raise_test();
-- 
GitLab