diff --git a/doc/src/sgml/ref/create_rule.sgml b/doc/src/sgml/ref/create_rule.sgml
index 1ab861ddf172ff87bf2142cd6e78df941fd51c7d..9380ab5a184627c7507fcc96f5ad6662da195967 100644
--- a/doc/src/sgml/ref/create_rule.sgml
+++ b/doc/src/sgml/ref/create_rule.sgml
@@ -1,5 +1,5 @@
 <!--
-$PostgreSQL: pgsql/doc/src/sgml/ref/create_rule.sgml,v 1.45 2005/01/04 00:39:53 tgl Exp $
+$PostgreSQL: pgsql/doc/src/sgml/ref/create_rule.sgml,v 1.46 2006/09/02 17:06:52 tgl Exp $
 PostgreSQL documentation
 -->
 
@@ -70,7 +70,9 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
    <literal>ON INSERT</literal>, <literal>ON UPDATE</literal>, and
    <literal>ON DELETE</literal> rules (or any subset of those that's
    sufficient for your purposes) to replace update actions on the view
-   with appropriate updates on other tables.
+   with appropriate updates on other tables.  If you want to support
+   <command>INSERT RETURNING</> and so on, then be sure to put a suitable
+   <literal>RETURNING</> clause into each of these rules.
   </para>
 
   <para>
@@ -87,7 +89,8 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
    understands it will never be called on to update the dummy table.
    Then make the conditional rules non-<literal>INSTEAD</literal>; in
    the cases where they are applied, they add to the default
-   <literal>INSTEAD NOTHING</literal> action.
+   <literal>INSTEAD NOTHING</literal> action.  (This method does not
+   currently work to support <literal>RETURNING</> queries, however.)
   </para>
  </refsect1>
 
@@ -201,13 +204,30 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
    be allowed to define a rule on it.
   </para>
 
+  <para>
+   In a rule for <literal>INSERT</literal>, <literal>UPDATE</literal>, or
+   <literal>DELETE</literal> on a view, you can add a <literal>RETURNING</>
+   clause that emits the view's columns.  This clause will be used to compute
+   the outputs if the rule is triggered by an <command>INSERT RETURNING</>,
+   <command>UPDATE RETURNING</>, or <command>DELETE RETURNING</> command
+   respectively.  When the rule is triggered by a command without
+   <literal>RETURNING</>, the rule's <literal>RETURNING</> clause will be
+   ignored.  The current implementation allows only unconditional
+   <literal>INSTEAD</> rules to contain <literal>RETURNING</>; furthermore
+   there can be at most one <literal>RETURNING</> clause among all the rules
+   for the same event.  (This ensures that there is only one candidate
+   <literal>RETURNING</> clause to be used to compute the results.)
+   <literal>RETURNING</> queries on the view will be rejected if
+   there is no <literal>RETURNING</> clause in any available rule.
+  </para>
+
   <para>
    It is very important to take care to avoid circular rules.  For
    example, though each of the following two rule definitions are
    accepted by <productname>PostgreSQL</productname>, the
    <command>SELECT</command> command would cause
    <productname>PostgreSQL</productname> to report an error because
-   the query cycled too many times:
+   of recursive expansion of a rule:
 
 <programlisting>
 CREATE RULE "_RETURN" AS
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index ad5a2d4bfea85254cc6bcb2770db43d204352c8f..84cf3bce622d68c36ef84162adbe2b0fe5abce7f 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/rules.sgml,v 1.45 2006/04/23 03:39:52 momjian Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/rules.sgml,v 1.46 2006/09/02 17:06:52 tgl Exp $ -->
 
 <chapter id="rules">
 <title>The Rule System</title>
@@ -873,7 +873,7 @@ SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;
 
         <listitem>
         <para>
-            They can be <literal>INSTEAD</> or <literal>ALSO</> (default).
+            They can be <literal>INSTEAD</> or <literal>ALSO</> (the default).
         </para>
         </listitem>
 
@@ -920,7 +920,8 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
     Initially the query-tree list is empty.
     There can be zero (<literal>NOTHING</> key word), one, or multiple actions.
     To simplify, we will look at a rule with one action. This rule
-    can have a qualification or not and it can be <literal>INSTEAD</> or <literal>ALSO</> (default).
+    can have a qualification or not and it can be <literal>INSTEAD</> or
+    <literal>ALSO</> (the default).
 </para>
 
 <para>
@@ -932,22 +933,13 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
 </para>
 
    <para>
-    So we have four cases that produce the following query trees for
+    So we have three cases that produce the following query trees for
     a one-action rule.
 
     <variablelist>
      <varlistentry>
-      <term>No qualification and <literal>ALSO</></term>
-      <listitem>
-       <para>
-        the query tree from the rule action with the original query
-        tree's qualification added
-       </para>
-      </listitem>
-     </varlistentry>
-
-     <varlistentry>
-      <term>No qualification but <literal>INSTEAD</></term>
+      <term>No qualification, with either <literal>ALSO</> or
+      <literal>INSTEAD</></term>
       <listitem>
        <para>
         the query tree from the rule action with the original query
@@ -1283,7 +1275,7 @@ SELECT shoelace_data.sl_name, 0,
     A simple way to protect view relations from the mentioned
     possibility that someone can try to run <command>INSERT</command>,
     <command>UPDATE</command>, or <command>DELETE</command> on them is
-    to let those query trees get thrown away.  So we create the rules
+    to let those query trees get thrown away.  So we could create the rules
 
 <programlisting>
 CREATE RULE shoe_ins_protect AS ON INSERT TO shoe
@@ -1338,6 +1330,34 @@ CREATE RULE shoelace_del AS ON DELETE TO shoelace
 </programlisting>
    </para>
 
+   <para>
+    If you want to support <literal>RETURNING</> queries on the view,
+    you need to make the rules include <literal>RETURNING</> clauses that
+    compute the view rows.  This is usually pretty trivial for views on a
+    single table, but it's a bit tedious for join views such as
+    <literal>shoelace</literal>.  An example for the insert case is
+
+<programlisting>
+CREATE RULE shoelace_ins AS ON INSERT TO shoelace
+    DO INSTEAD
+    INSERT INTO shoelace_data VALUES (
+           NEW.sl_name,
+           NEW.sl_avail,
+           NEW.sl_color,
+           NEW.sl_len,
+           NEW.sl_unit
+    )
+    RETURNING
+           shoelace_data.*,
+           (SELECT shoelace_data.sl_len * u.un_fact
+            FROM unit u WHERE shoelace_data.sl_unit = u.un_name);
+</programlisting>
+
+    Note that this one rule supports both <command>INSERT</> and
+    <command>INSERT RETURNING</> queries on the view &mdash; the
+    <literal>RETURNING</> clause is simply ignored for <command>INSERT</>.
+   </para>
+
    <para>
     Now assume that once in a while, a pack of shoelaces arrives at
     the shop and a big parts list along with it.  But you don't want
diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c
index df24c6751c2c8b79095ae0df29ec84d21a4b6c2f..a961f49d9b1b16915f488c5343a37d2cfac3f79f 100644
--- a/src/backend/rewrite/rewriteDefine.c
+++ b/src/backend/rewrite/rewriteDefine.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/rewrite/rewriteDefine.c,v 1.112 2006/08/12 20:05:55 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/rewrite/rewriteDefine.c,v 1.113 2006/09/02 17:06:52 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -31,6 +31,8 @@
 #include "utils/syscache.h"
 
 
+static void checkRuleResultList(List *targetList, TupleDesc resultDesc,
+								bool isSelect);
 static void setRuleCheckAsUser_Query(Query *qry, Oid userid);
 static void setRuleCheckAsUser_Expr(Node *node, Oid userid);
 static bool setRuleCheckAsUser_walker(Node *node, Oid *context);
@@ -235,15 +237,11 @@ DefineQueryRewrite(RuleStmt *stmt)
 					 errhint("Use triggers instead.")));
 	}
 
-	/*
-	 * Rules ON SELECT are restricted to view definitions
-	 */
 	if (event_type == CMD_SELECT)
 	{
-		ListCell   *tllist;
-		int			i;
-
 		/*
+		 * Rules ON SELECT are restricted to view definitions
+		 *
 		 * So there cannot be INSTEAD NOTHING, ...
 		 */
 		if (list_length(action) == 0)
@@ -282,71 +280,17 @@ DefineQueryRewrite(RuleStmt *stmt)
 		 * ... the targetlist of the SELECT action must exactly match the
 		 * event relation, ...
 		 */
-		i = 0;
-		foreach(tllist, query->targetList)
-		{
-			TargetEntry *tle = (TargetEntry *) lfirst(tllist);
-			int32		tletypmod;
-			Form_pg_attribute attr;
-			char	   *attname;
-
-			if (tle->resjunk)
-				continue;
-			i++;
-			if (i > event_relation->rd_att->natts)
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				  errmsg("SELECT rule's target list has too many entries")));
-
-			attr = event_relation->rd_att->attrs[i - 1];
-			attname = NameStr(attr->attname);
-
-			/*
-			 * Disallow dropped columns in the relation.  This won't happen in
-			 * the cases we actually care about (namely creating a view via
-			 * CREATE TABLE then CREATE RULE).	Trying to cope with it is much
-			 * more trouble than it's worth, because we'd have to modify the
-			 * rule to insert dummy NULLs at the right positions.
-			 */
-			if (attr->attisdropped)
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("cannot convert relation containing dropped columns to view")));
-
-			if (strcmp(tle->resname, attname) != 0)
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-						 errmsg("SELECT rule's target entry %d has different column name from \"%s\"", i, attname)));
-
-			if (attr->atttypid != exprType((Node *) tle->expr))
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-						 errmsg("SELECT rule's target entry %d has different type from column \"%s\"", i, attname)));
-
-			/*
-			 * Allow typmods to be different only if one of them is -1, ie,
-			 * "unspecified".  This is necessary for cases like "numeric",
-			 * where the table will have a filled-in default length but the
-			 * select rule's expression will probably have typmod = -1.
-			 */
-			tletypmod = exprTypmod((Node *) tle->expr);
-			if (attr->atttypmod != tletypmod &&
-				attr->atttypmod != -1 && tletypmod != -1)
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-						 errmsg("SELECT rule's target entry %d has different size from column \"%s\"", i, attname)));
-		}
-
-		if (i != event_relation->rd_att->natts)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				   errmsg("SELECT rule's target list has too few entries")));
+		checkRuleResultList(query->targetList,
+							RelationGetDescr(event_relation),
+							true);
 
 		/*
 		 * ... there must not be another ON SELECT rule already ...
 		 */
 		if (!replace && event_relation->rd_rules != NULL)
 		{
+			int		i;
+
 			for (i = 0; i < event_relation->rd_rules->numLocks; i++)
 			{
 				RewriteRule *rule;
@@ -425,6 +369,42 @@ DefineQueryRewrite(RuleStmt *stmt)
 			RelisBecomingView = true;
 		}
 	}
+	else
+	{
+		/*
+		 * For non-SELECT rules, a RETURNING list can appear in at most one
+		 * of the actions ... and there can't be any RETURNING list at all
+		 * in a conditional or non-INSTEAD rule.  (Actually, there can be
+		 * at most one RETURNING list across all rules on the same event,
+		 * but it seems best to enforce that at rule expansion time.)  If
+		 * there is a RETURNING list, it must match the event relation.
+		 */
+		bool	haveReturning = false;
+
+		foreach(l, action)
+		{
+			query = (Query *) lfirst(l);
+
+			if (!query->returningList)
+				continue;
+			if (haveReturning)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot have multiple RETURNING lists in a rule")));
+			haveReturning = true;
+			if (event_qual != NULL)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("RETURNING lists are not supported in conditional rules")));
+			if (!is_instead)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("RETURNING lists are not supported in non-INSTEAD rules")));
+			checkRuleResultList(query->returningList,
+								RelationGetDescr(event_relation),
+								false);
+		}
+	}
 
 	/*
 	 * This rule is allowed - prepare to install it.
@@ -484,6 +464,95 @@ DefineQueryRewrite(RuleStmt *stmt)
 	heap_close(event_relation, NoLock);
 }
 
+/*
+ * checkRuleResultList
+ *		Verify that targetList produces output compatible with a tupledesc
+ *
+ * The targetList might be either a SELECT targetlist, or a RETURNING list;
+ * isSelect tells which.  (This is mostly used for choosing error messages,
+ * but also we don't enforce column name matching for RETURNING.)
+ */
+static void
+checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect)
+{
+	ListCell   *tllist;
+	int			i;
+
+	i = 0;
+	foreach(tllist, targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(tllist);
+		int32		tletypmod;
+		Form_pg_attribute attr;
+		char	   *attname;
+
+		/* resjunk entries may be ignored */
+		if (tle->resjunk)
+			continue;
+		i++;
+		if (i > resultDesc->natts)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 isSelect ?
+					 errmsg("SELECT rule's target list has too many entries") :
+					 errmsg("RETURNING list has too many entries")));
+
+		attr = resultDesc->attrs[i - 1];
+		attname = NameStr(attr->attname);
+
+		/*
+		 * Disallow dropped columns in the relation.  This won't happen in the
+		 * cases we actually care about (namely creating a view via CREATE
+		 * TABLE then CREATE RULE, or adding a RETURNING rule to a view).
+		 * Trying to cope with it is much more trouble than it's worth,
+		 * because we'd have to modify the rule to insert dummy NULLs at the
+		 * right positions.
+		 */
+		if (attr->attisdropped)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot convert relation containing dropped columns to view")));
+
+		if (isSelect && strcmp(tle->resname, attname) != 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("SELECT rule's target entry %d has different column name from \"%s\"", i, attname)));
+
+		if (attr->atttypid != exprType((Node *) tle->expr))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 isSelect ?
+					 errmsg("SELECT rule's target entry %d has different type from column \"%s\"",
+							i, attname) :
+					 errmsg("RETURNING list's entry %d has different type from column \"%s\"",
+							i, attname)));
+
+		/*
+		 * Allow typmods to be different only if one of them is -1, ie,
+		 * "unspecified".  This is necessary for cases like "numeric",
+		 * where the table will have a filled-in default length but the
+		 * select rule's expression will probably have typmod = -1.
+		 */
+		tletypmod = exprTypmod((Node *) tle->expr);
+		if (attr->atttypmod != tletypmod &&
+			attr->atttypmod != -1 && tletypmod != -1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 isSelect ?
+					 errmsg("SELECT rule's target entry %d has different size from column \"%s\"",
+							i, attname) :
+					 errmsg("RETURNING list's entry %d has different size from column \"%s\"",
+							i, attname)));
+	}
+
+	if (i != resultDesc->natts)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 isSelect ?
+				 errmsg("SELECT rule's target list has too few entries") :
+				 errmsg("RETURNING list has too few entries")));
+}
+
 /*
  * setRuleCheckAsUser_Query
  *		Recursively scan a query and set the checkAsUser field to the
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 286fc5b498cb50130aa1c85869b2770fdc36cfbb..1af5bd7e7f57c4fb00d0079e661cfc9f26b5f633 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/rewrite/rewriteHandler.c,v 1.165 2006/08/02 01:59:47 joe Exp $
+ *	  $PostgreSQL: pgsql/src/backend/rewrite/rewriteHandler.c,v 1.166 2006/09/02 17:06:52 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -39,7 +39,8 @@ static Query *rewriteRuleAction(Query *parsetree,
 				  Query *rule_action,
 				  Node *rule_qual,
 				  int rt_index,
-				  CmdType event);
+				  CmdType event,
+				  bool *returning_flag);
 static List *adjustJoinTreeList(Query *parsetree, bool removert, int rt_index);
 static void rewriteTargetList(Query *parsetree, Relation target_relation,
 							  List **attrno_list);
@@ -251,13 +252,26 @@ acquireLocksOnSubLinks(Node *node, void *context)
  * rewriteRuleAction -
  *	  Rewrite the rule action with appropriate qualifiers (taken from
  *	  the triggering query).
+ *
+ * Input arguments:
+ *	parsetree - original query
+ *	rule_action - one action (query) of a rule
+ *	rule_qual - WHERE condition of rule, or NULL if unconditional
+ *	rt_index - RT index of result relation in original query
+ *	event - type of rule event
+ * Output arguments:
+ *	*returning_flag - set TRUE if we rewrite RETURNING clause in rule_action
+ *					(must be initialized to FALSE)
+ * Return value:
+ *	rewritten form of rule_action
  */
 static Query *
 rewriteRuleAction(Query *parsetree,
 				  Query *rule_action,
 				  Node *rule_qual,
 				  int rt_index,
-				  CmdType event)
+				  CmdType event,
+				  bool *returning_flag)
 {
 	int			current_varno,
 				new_varno;
@@ -416,6 +430,32 @@ rewriteRuleAction(Query *parsetree,
 			rule_action = sub_action;
 	}
 
+	/*
+	 * If rule_action has a RETURNING clause, then either throw it away
+	 * if the triggering query has no RETURNING clause, or rewrite it to
+	 * emit what the triggering query's RETURNING clause asks for.  Throw
+	 * an error if more than one rule has a RETURNING clause.
+	 */
+	if (!parsetree->returningList)
+		rule_action->returningList = NIL;
+	else if (rule_action->returningList)
+	{
+		if (*returning_flag)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot have RETURNING lists in multiple rules")));
+		*returning_flag = true;
+		rule_action->returningList = (List *)
+			ResolveNew((Node *) parsetree->returningList,
+					   parsetree->resultRelation,
+					   0,
+					   rt_fetch(parsetree->resultRelation,
+								parsetree->rtable),
+					   rule_action->returningList,
+					   CMD_SELECT,
+					   0);
+	}
+
 	return rule_action;
 }
 
@@ -1357,6 +1397,8 @@ CopyAndAddInvertedQual(Query *parsetree,
  * Output arguments:
  *	*instead_flag - set TRUE if any unqualified INSTEAD rule is found
  *					(must be initialized to FALSE)
+ *	*returning_flag - set TRUE if we rewrite RETURNING clause in any rule
+ *					(must be initialized to FALSE)
  *	*qual_product - filled with modified original query if any qualified
  *					INSTEAD rule is found (must be initialized to NULL)
  * Return value:
@@ -1377,6 +1419,7 @@ fireRules(Query *parsetree,
 		  CmdType event,
 		  List *locks,
 		  bool *instead_flag,
+		  bool *returning_flag,
 		  Query **qual_product)
 {
 	List	   *results = NIL;
@@ -1438,7 +1481,8 @@ fireRules(Query *parsetree,
 				continue;
 
 			rule_action = rewriteRuleAction(parsetree, rule_action,
-											event_qual, rt_index, event);
+											event_qual, rt_index, event,
+											returning_flag);
 
 			rule_action->querySource = qsrc;
 			rule_action->canSetTag = false;		/* might change later */
@@ -1463,6 +1507,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 {
 	CmdType		event = parsetree->commandType;
 	bool		instead = false;
+	bool		returning = false;
 	Query	   *qual_product = NULL;
 	List	   *rewritten = NIL;
 
@@ -1551,6 +1596,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 										event,
 										locks,
 										&instead,
+										&returning,
 										&qual_product);
 
 			/*
@@ -1591,6 +1637,47 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 			}
 		}
 
+		/*
+		 * If there is an INSTEAD, and the original query has a RETURNING,
+		 * we have to have found a RETURNING in the rule(s), else fail.
+		 * (Because DefineQueryRewrite only allows RETURNING in unconditional
+		 * INSTEAD rules, there's no need to worry whether the substituted
+		 * RETURNING will actually be executed --- it must be.)
+		 */
+		if ((instead || qual_product != NULL) &&
+			parsetree->returningList &&
+			!returning)
+		{
+			switch (event)
+			{
+				case CMD_INSERT:
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cannot INSERT RETURNING on relation \"%s\"",
+									RelationGetRelationName(rt_entry_relation)),
+							 errhint("You need an unconditional ON INSERT DO INSTEAD rule with a RETURNING clause.")));
+					break;
+				case CMD_UPDATE:
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cannot UPDATE RETURNING on relation \"%s\"",
+									RelationGetRelationName(rt_entry_relation)),
+							 errhint("You need an unconditional ON UPDATE DO INSTEAD rule with a RETURNING clause.")));
+					break;
+				case CMD_DELETE:
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cannot DELETE RETURNING on relation \"%s\"",
+									RelationGetRelationName(rt_entry_relation)),
+							 errhint("You need an unconditional ON DELETE DO INSTEAD rule with a RETURNING clause.")));
+					break;
+				default:
+					elog(ERROR, "unrecognized commandType: %d",
+						 (int) event);
+					break;
+			}
+		}
+
 		heap_close(rt_entry_relation, NoLock);
 	}
 
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index 32568ccc2b7288cf6267d73c56e7d23975764ea7..07c51a489c79e0567e5f758b425c72cc33beeec0 100644
--- a/src/test/regress/expected/returning.out
+++ b/src/test/regress/expected/returning.out
@@ -192,4 +192,143 @@ SELECT * FROM foochild;
 ----+----+----+----+----
 (0 rows)
 
-DROP TABLE foochild, foo;
+DROP TABLE foochild;
+-- Rules and views
+CREATE TEMP VIEW voo AS SELECT f1, f2 FROM foo;
+CREATE RULE voo_i AS ON INSERT TO voo DO INSTEAD
+  INSERT INTO foo VALUES(new.*, 57);
+INSERT INTO voo VALUES(11,'zit');
+-- fails:
+INSERT INTO voo VALUES(12,'zoo') RETURNING *, f1*2;
+ERROR:  cannot INSERT RETURNING on relation "voo"
+HINT:  You need an unconditional ON INSERT DO INSTEAD rule with a RETURNING clause.
+-- fails, incompatible list:
+CREATE OR REPLACE RULE voo_i AS ON INSERT TO voo DO INSTEAD
+  INSERT INTO foo VALUES(new.*, 57) RETURNING *;
+ERROR:  RETURNING list has too many entries
+CREATE OR REPLACE RULE voo_i AS ON INSERT TO voo DO INSTEAD
+  INSERT INTO foo VALUES(new.*, 57) RETURNING f1, f2;
+-- should still work
+INSERT INTO voo VALUES(13,'zit2');
+-- works now
+INSERT INTO voo VALUES(14,'zoo2') RETURNING *;
+ f1 |  f2  
+----+------
+ 14 | zoo2
+(1 row)
+
+SELECT * FROM foo;
+ f1 |  f2  | f3 | f4  
+----+------+----+-----
+  2 | more | 42 | 141
+ 11 | zit  | 57 |  99
+ 13 | zit2 | 57 |  99
+ 14 | zoo2 | 57 |  99
+(4 rows)
+
+SELECT * FROM voo;
+ f1 |  f2  
+----+------
+  2 | more
+ 11 | zit
+ 13 | zit2
+ 14 | zoo2
+(4 rows)
+
+CREATE OR REPLACE RULE voo_u AS ON UPDATE TO voo DO INSTEAD
+  UPDATE foo SET f1 = new.f1, f2 = new.f2 WHERE f1 = old.f1
+  RETURNING f1, f2;
+update voo set f1 = f1 + 1 where f2 = 'zoo2';
+update voo set f1 = f1 + 1 where f2 = 'zoo2' RETURNING *, f1*2;
+ f1 |  f2  | ?column? 
+----+------+----------
+ 16 | zoo2 |       32
+(1 row)
+
+SELECT * FROM foo;
+ f1 |  f2  | f3 | f4  
+----+------+----+-----
+  2 | more | 42 | 141
+ 11 | zit  | 57 |  99
+ 13 | zit2 | 57 |  99
+ 16 | zoo2 | 57 |  99
+(4 rows)
+
+SELECT * FROM voo;
+ f1 |  f2  
+----+------
+  2 | more
+ 11 | zit
+ 13 | zit2
+ 16 | zoo2
+(4 rows)
+
+CREATE OR REPLACE RULE voo_d AS ON DELETE TO voo DO INSTEAD
+  DELETE FROM foo WHERE f1 = old.f1
+  RETURNING f1, f2;
+DELETE FROM foo WHERE f1 = 13;
+DELETE FROM foo WHERE f2 = 'zit' RETURNING *;
+ f1 | f2  | f3 | f4 
+----+-----+----+----
+ 11 | zit | 57 | 99
+(1 row)
+
+SELECT * FROM foo;
+ f1 |  f2  | f3 | f4  
+----+------+----+-----
+  2 | more | 42 | 141
+ 16 | zoo2 | 57 |  99
+(2 rows)
+
+SELECT * FROM voo;
+ f1 |  f2  
+----+------
+  2 | more
+ 16 | zoo2
+(2 rows)
+
+-- Try a join case
+CREATE TEMP TABLE joinme (f2j text, other int);
+INSERT INTO joinme VALUES('more', 12345);
+INSERT INTO joinme VALUES('zoo2', 54321);
+INSERT INTO joinme VALUES('other', 0);
+CREATE TEMP VIEW joinview AS
+  SELECT foo.*, other FROM foo JOIN joinme ON (f2 = f2j);
+SELECT * FROM joinview;
+ f1 |  f2  | f3 | f4  | other 
+----+------+----+-----+-------
+  2 | more | 42 | 141 | 12345
+ 16 | zoo2 | 57 |  99 | 54321
+(2 rows)
+
+CREATE RULE joinview_u AS ON UPDATE TO joinview DO INSTEAD
+  UPDATE foo SET f1 = new.f1, f3 = new.f3
+    FROM joinme WHERE f2 = f2j AND f2 = old.f2
+    RETURNING foo.*, other;
+UPDATE joinview SET f1 = f1 + 1 WHERE f3 = 57 RETURNING *, other + 1;
+ f1 |  f2  | f3 | f4 | other | ?column? 
+----+------+----+----+-------+----------
+ 17 | zoo2 | 57 | 99 | 54321 |    54322
+(1 row)
+
+SELECT * FROM joinview;
+ f1 |  f2  | f3 | f4  | other 
+----+------+----+-----+-------
+  2 | more | 42 | 141 | 12345
+ 17 | zoo2 | 57 |  99 | 54321
+(2 rows)
+
+SELECT * FROM foo;
+ f1 |  f2  | f3 | f4  
+----+------+----+-----
+  2 | more | 42 | 141
+ 17 | zoo2 | 57 |  99
+(2 rows)
+
+SELECT * FROM voo;
+ f1 |  f2  
+----+------
+  2 | more
+ 17 | zoo2
+(2 rows)
+
diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql
index 7a0dc8f84551f9383017adf031ea14301eb64672..0ed9a4895102c05a8bd01e83f6a6b375a1a6e57c 100644
--- a/src/test/regress/sql/returning.sql
+++ b/src/test/regress/sql/returning.sql
@@ -84,4 +84,73 @@ DELETE FROM foo
 SELECT * FROM foo;
 SELECT * FROM foochild;
 
-DROP TABLE foochild, foo;
+DROP TABLE foochild;
+
+-- Rules and views
+
+CREATE TEMP VIEW voo AS SELECT f1, f2 FROM foo;
+
+CREATE RULE voo_i AS ON INSERT TO voo DO INSTEAD
+  INSERT INTO foo VALUES(new.*, 57);
+
+INSERT INTO voo VALUES(11,'zit');
+-- fails:
+INSERT INTO voo VALUES(12,'zoo') RETURNING *, f1*2;
+
+-- fails, incompatible list:
+CREATE OR REPLACE RULE voo_i AS ON INSERT TO voo DO INSTEAD
+  INSERT INTO foo VALUES(new.*, 57) RETURNING *;
+
+CREATE OR REPLACE RULE voo_i AS ON INSERT TO voo DO INSTEAD
+  INSERT INTO foo VALUES(new.*, 57) RETURNING f1, f2;
+
+-- should still work
+INSERT INTO voo VALUES(13,'zit2');
+-- works now
+INSERT INTO voo VALUES(14,'zoo2') RETURNING *;
+
+SELECT * FROM foo;
+SELECT * FROM voo;
+
+CREATE OR REPLACE RULE voo_u AS ON UPDATE TO voo DO INSTEAD
+  UPDATE foo SET f1 = new.f1, f2 = new.f2 WHERE f1 = old.f1
+  RETURNING f1, f2;
+
+update voo set f1 = f1 + 1 where f2 = 'zoo2';
+update voo set f1 = f1 + 1 where f2 = 'zoo2' RETURNING *, f1*2;
+
+SELECT * FROM foo;
+SELECT * FROM voo;
+
+CREATE OR REPLACE RULE voo_d AS ON DELETE TO voo DO INSTEAD
+  DELETE FROM foo WHERE f1 = old.f1
+  RETURNING f1, f2;
+
+DELETE FROM foo WHERE f1 = 13;
+DELETE FROM foo WHERE f2 = 'zit' RETURNING *;
+
+SELECT * FROM foo;
+SELECT * FROM voo;
+
+-- Try a join case
+
+CREATE TEMP TABLE joinme (f2j text, other int);
+INSERT INTO joinme VALUES('more', 12345);
+INSERT INTO joinme VALUES('zoo2', 54321);
+INSERT INTO joinme VALUES('other', 0);
+
+CREATE TEMP VIEW joinview AS
+  SELECT foo.*, other FROM foo JOIN joinme ON (f2 = f2j);
+
+SELECT * FROM joinview;
+
+CREATE RULE joinview_u AS ON UPDATE TO joinview DO INSTEAD
+  UPDATE foo SET f1 = new.f1, f3 = new.f3
+    FROM joinme WHERE f2 = f2j AND f2 = old.f2
+    RETURNING foo.*, other;
+
+UPDATE joinview SET f1 = f1 + 1 WHERE f3 = 57 RETURNING *, other + 1;
+
+SELECT * FROM joinview;
+SELECT * FROM foo;
+SELECT * FROM voo;