diff --git a/doc/src/sgml/syntax.sgml b/doc/src/sgml/syntax.sgml
index 97020845b50182b746b2fe43b09bc57f872d08fe..74cc813a681f2cd3a98765ae2a2e4589517090f3 100644
--- a/doc/src/sgml/syntax.sgml
+++ b/doc/src/sgml/syntax.sgml
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/syntax.sgml,v 1.106 2006/03/10 19:10:49 momjian Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/syntax.sgml,v 1.107 2006/06/26 17:24:40 tgl Exp $ -->
 
 <chapter id="sql-syntax">
  <title>SQL Syntax</title>
@@ -1570,6 +1570,31 @@ SELECT ROW(1,2.5,'this is a test');
     expression in the list.
    </para>
 
+   <para>
+    A row constructor can include the syntax
+    <replaceable>rowvalue</replaceable><literal>.*</literal>,
+    which will be expanded to a list of the elements of the row value,
+    just as occurs when the <literal>.*</> syntax is used at the top level
+    of a <command>SELECT</> list.  For example, if table <literal>t</> has
+    columns <literal>f1</> and <literal>f2</>, these are the same:
+<programlisting>
+SELECT ROW(t.*, 42) FROM t;
+SELECT ROW(t.f1, t.f2, 42) FROM t;
+</programlisting>
+   </para>
+
+   <note>
+    <para>
+     Before <productname>PostgreSQL</productname> 8.2, the
+     <literal>.*</literal> syntax was not expanded, so that writing
+     <literal>ROW(t.*, 42)</> created a two-field row whose first field
+     was another row value.  The new behavior is usually more useful.
+     If you need the old behavior of nested row values, write the inner
+     row value without <literal>.*</literal>, for instance
+     <literal>ROW(t, 42)</>.
+    </para>
+   </note>
+
    <para>
     By default, the value created by a <literal>ROW</> expression is of
     an anonymous record type.  If necessary, it can be cast to a named
@@ -1619,7 +1644,7 @@ SELECT getf1(CAST(ROW(11,'this is a test',2.5) AS myrowtype));
 <programlisting>
 SELECT ROW(1,2.5,'this is a test') = ROW(1, 3, 'not the same');
 
-SELECT ROW(a, b, c) IS NOT NULL FROM table;
+SELECT ROW(table.*) IS NULL FROM table;  -- detect all-null rows
 </programlisting>
    For more detail see <xref linkend="functions-comparisons">.
    Row constructors can also be used in connection with subqueries,
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 414afe6dfa536abcf92a97bbb1991a8b11f8da2a..17d221ac19b3d1cd56f87c88f1b99987e687836c 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/parser/parse_expr.c,v 1.192 2006/04/22 01:26:00 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/parser/parse_expr.c,v 1.193 2006/06/26 17:24:41 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -29,6 +29,7 @@
 #include "parser/parse_func.h"
 #include "parser/parse_oper.h"
 #include "parser/parse_relation.h"
+#include "parser/parse_target.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
@@ -1289,6 +1290,43 @@ transformRowExpr(ParseState *pstate, RowExpr *r)
 		Node	   *e = (Node *) lfirst(arg);
 		Node	   *newe;
 
+		/*
+		 * Check for "something.*".  Depending on the complexity of the
+		 * "something", the star could appear as the last name in ColumnRef,
+		 * or as the last indirection item in A_Indirection.
+		 */
+		if (IsA(e, ColumnRef))
+		{
+			ColumnRef  *cref = (ColumnRef *) e;
+
+			if (strcmp(strVal(llast(cref->fields)), "*") == 0)
+			{
+				/* It is something.*, expand into multiple items */
+				newargs = list_concat(newargs,
+									  ExpandColumnRefStar(pstate, cref,
+														  false));
+				continue;
+			}
+		}
+		else if (IsA(e, A_Indirection))
+		{
+			A_Indirection *ind = (A_Indirection *) e;
+			Node	   *lastitem = llast(ind->indirection);
+
+			if (IsA(lastitem, String) &&
+				strcmp(strVal(lastitem), "*") == 0)
+			{
+				/* It is something.*, expand into multiple items */
+				newargs = list_concat(newargs,
+									  ExpandIndirectionStar(pstate, ind,
+															false));
+				continue;
+			}
+		}
+
+		/*
+		 * Not "something.*", so transform as a single expression
+		 */
 		newe = transformExpr(pstate, e);
 		newargs = lappend(newargs, newe);
 	}
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 86670d2679410811a5c891d003bd664926c2c262..859de3b10c66253455a1a8446070464a5ca0ebd3 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/parser/parse_target.c,v 1.143 2006/06/16 18:42:22 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/parser/parse_target.c,v 1.144 2006/06/26 17:24:41 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -42,9 +42,7 @@ static Node *transformAssignmentIndirection(ParseState *pstate,
 							   ListCell *indirection,
 							   Node *rhs,
 							   int location);
-static List *ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref);
 static List *ExpandAllTables(ParseState *pstate);
-static List *ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind);
 static int	FigureColnameInternal(Node *node, char **name);
 
 
@@ -117,7 +115,8 @@ transformTargetList(ParseState *pstate, List *targetlist)
 			{
 				/* It is something.*, expand into multiple items */
 				p_target = list_concat(p_target,
-									   ExpandColumnRefStar(pstate, cref));
+									   ExpandColumnRefStar(pstate, cref,
+														   true));
 				continue;
 			}
 		}
@@ -131,7 +130,8 @@ transformTargetList(ParseState *pstate, List *targetlist)
 			{
 				/* It is something.*, expand into multiple items */
 				p_target = list_concat(p_target,
-									   ExpandIndirectionStar(pstate, ind));
+									   ExpandIndirectionStar(pstate, ind,
+															 true));
 				continue;
 			}
 		}
@@ -696,13 +696,16 @@ checkInsertTargets(ParseState *pstate, List *cols, List **attrnos)
 
 /*
  * ExpandColumnRefStar()
- *		Turns foo.* (in the target list) into a list of targetlist entries.
+ *		Transforms foo.* into a list of expressions or targetlist entries.
  *
  * This handles the case where '*' appears as the last or only name in a
- * ColumnRef.
+ * ColumnRef.  The code is shared between the case of foo.* at the top level
+ * in a SELECT target list (where we want TargetEntry nodes in the result)
+ * and foo.* in a ROW() construct (where we want just bare expressions).
  */
-static List *
-ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref)
+List *
+ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
+					bool targetlist)
 {
 	List	   *fields = cref->fields;
 	int			numnames = list_length(fields);
@@ -713,7 +716,12 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref)
 		 * Target item is a bare '*', expand all tables
 		 *
 		 * (e.g., SELECT * FROM emp, dept)
+		 *
+		 * Since the grammar only accepts bare '*' at top level of SELECT,
+		 * we need not handle the targetlist==false case here.
 		 */
+		Assert(targetlist);
+
 		return ExpandAllTables(pstate);
 	}
 	else
@@ -775,13 +783,22 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref)
 
 		rtindex = RTERangeTablePosn(pstate, rte, &sublevels_up);
 
-		return expandRelAttrs(pstate, rte, rtindex, sublevels_up);
+		if (targetlist)
+			return expandRelAttrs(pstate, rte, rtindex, sublevels_up);
+		else
+		{
+			List	*vars;
+
+			expandRTE(rte, rtindex, sublevels_up, false,
+					  NULL, &vars);
+			return vars;
+		}
 	}
 }
 
 /*
  * ExpandAllTables()
- *		Turns '*' (in the target list) into a list of targetlist entries.
+ *		Transforms '*' (in the target list) into a list of targetlist entries.
  *
  * tlist entries are generated for each relation appearing in the query's
  * varnamespace.  We do not consider relnamespace because that would include
@@ -814,18 +831,22 @@ ExpandAllTables(ParseState *pstate)
 
 /*
  * ExpandIndirectionStar()
- *		Turns foo.* (in the target list) into a list of targetlist entries.
+ *		Transforms foo.* into a list of expressions or targetlist entries.
  *
  * This handles the case where '*' appears as the last item in A_Indirection.
+ * The code is shared between the case of foo.* at the top level in a SELECT
+ * target list (where we want TargetEntry nodes in the result) and foo.* in
+ * a ROW() construct (where we want just bare expressions).
  */
-static List *
-ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind)
+List *
+ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind,
+					  bool targetlist)
 {
+	List	   *result = NIL;
 	Node	   *expr;
 	TupleDesc	tupleDesc;
 	int			numAttrs;
 	int			i;
-	List	   *te_list = NIL;
 
 	/* Strip off the '*' to create a reference to the rowtype object */
 	ind = copyObject(ind);
@@ -860,7 +881,6 @@ ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind)
 	{
 		Form_pg_attribute att = tupleDesc->attrs[i];
 		Node	   *fieldnode;
-		TargetEntry *te;
 
 		if (att->attisdropped)
 			continue;
@@ -893,14 +913,22 @@ ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind)
 			fieldnode = (Node *) fselect;
 		}
 
-		te = makeTargetEntry((Expr *) fieldnode,
-							 (AttrNumber) pstate->p_next_resno++,
-							 pstrdup(NameStr(att->attname)),
-							 false);
-		te_list = lappend(te_list, te);
+		if (targetlist)
+		{
+			/* add TargetEntry decoration */
+			TargetEntry *te;
+
+			te = makeTargetEntry((Expr *) fieldnode,
+								 (AttrNumber) pstate->p_next_resno++,
+								 pstrdup(NameStr(att->attname)),
+								 false);
+			result = lappend(result, te);
+		}
+		else
+			result = lappend(result, fieldnode);
 	}
 
-	return te_list;
+	return result;
 }
 
 /*
diff --git a/src/include/parser/parse_target.h b/src/include/parser/parse_target.h
index f1cb2bd8a1c65839970388bcc0af0824026a242d..6080696baa092e01262cafc0f4dd2fbff2a6d7cf 100644
--- a/src/include/parser/parse_target.h
+++ b/src/include/parser/parse_target.h
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/parser/parse_target.h,v 1.39 2006/03/23 00:19:30 tgl Exp $
+ * $PostgreSQL: pgsql/src/include/parser/parse_target.h,v 1.40 2006/06/26 17:24:41 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -22,6 +22,10 @@ extern void markTargetListOrigins(ParseState *pstate, List *targetlist);
 extern TargetEntry *transformTargetEntry(ParseState *pstate,
 					 Node *node, Node *expr,
 					 char *colname, bool resjunk);
+extern List *ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
+								 bool targetlist);
+extern List *ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind,
+								   bool targetlist);
 extern void updateTargetListEntry(ParseState *pstate, TargetEntry *tle,
 					  char *colname, int attrno,
 					  List *indirection,
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index d8b0594c2bd8be7dd9be17bba5d79ccf1c127b44..1af4d9e2890184e61433cb390217996dcfa00ccc 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -487,3 +487,53 @@ DROP TRIGGER show_trigger_data_trig on trigger_test;
       
 DROP FUNCTION trigger_data();
 DROP TABLE trigger_test;
+--
+-- Test use of row comparisons on OLD/NEW
+--
+CREATE TABLE trigger_test (f1 int, f2 text, f3 text);
+-- this is the obvious (and wrong...) way to compare rows
+CREATE FUNCTION mytrigger() RETURNS trigger LANGUAGE plpgsql as $$
+begin
+	if row(old.*) = row(new.*) then
+		raise notice 'row % not changed', new.f1;
+	else
+		raise notice 'row % changed', new.f1;
+	end if;
+	return new;
+end$$;
+CREATE TRIGGER t
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE mytrigger();
+INSERT INTO trigger_test VALUES(1, 'foo', 'bar');
+INSERT INTO trigger_test VALUES(2, 'baz', 'quux');
+UPDATE trigger_test SET f3 = 'bar';
+NOTICE:  row 1 not changed
+NOTICE:  row 2 changed
+UPDATE trigger_test SET f3 = NULL;
+NOTICE:  row 1 changed
+NOTICE:  row 2 changed
+-- this demonstrates that the above isn't really working as desired:
+UPDATE trigger_test SET f3 = NULL;
+NOTICE:  row 1 changed
+NOTICE:  row 2 changed
+-- the right way when considering nulls is
+CREATE OR REPLACE FUNCTION mytrigger() RETURNS trigger LANGUAGE plpgsql as $$
+begin
+	if row(old.*) is distinct from row(new.*) then
+		raise notice 'row % changed', new.f1;
+	else
+		raise notice 'row % not changed', new.f1;
+	end if;
+	return new;
+end$$;
+UPDATE trigger_test SET f3 = 'bar';
+NOTICE:  row 1 changed
+NOTICE:  row 2 changed
+UPDATE trigger_test SET f3 = NULL;
+NOTICE:  row 1 changed
+NOTICE:  row 2 changed
+UPDATE trigger_test SET f3 = NULL;
+NOTICE:  row 1 not changed
+NOTICE:  row 2 not changed
+DROP TABLE trigger_test;
+DROP FUNCTION mytrigger();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 909a7d68d9be28cc7791589a50f2e45d00fdd35b..647c4c448ea49e4d4637e38b2ed401bd64aa396b 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -367,3 +367,51 @@ DROP TRIGGER show_trigger_data_trig on trigger_test;
 DROP FUNCTION trigger_data();
 
 DROP TABLE trigger_test;
+
+--
+-- Test use of row comparisons on OLD/NEW
+--
+
+CREATE TABLE trigger_test (f1 int, f2 text, f3 text);
+
+-- this is the obvious (and wrong...) way to compare rows
+CREATE FUNCTION mytrigger() RETURNS trigger LANGUAGE plpgsql as $$
+begin
+	if row(old.*) = row(new.*) then
+		raise notice 'row % not changed', new.f1;
+	else
+		raise notice 'row % changed', new.f1;
+	end if;
+	return new;
+end$$;
+
+CREATE TRIGGER t
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE mytrigger();
+
+INSERT INTO trigger_test VALUES(1, 'foo', 'bar');
+INSERT INTO trigger_test VALUES(2, 'baz', 'quux');
+
+UPDATE trigger_test SET f3 = 'bar';
+UPDATE trigger_test SET f3 = NULL;
+-- this demonstrates that the above isn't really working as desired:
+UPDATE trigger_test SET f3 = NULL;
+
+-- the right way when considering nulls is
+CREATE OR REPLACE FUNCTION mytrigger() RETURNS trigger LANGUAGE plpgsql as $$
+begin
+	if row(old.*) is distinct from row(new.*) then
+		raise notice 'row % changed', new.f1;
+	else
+		raise notice 'row % not changed', new.f1;
+	end if;
+	return new;
+end$$;
+
+UPDATE trigger_test SET f3 = 'bar';
+UPDATE trigger_test SET f3 = NULL;
+UPDATE trigger_test SET f3 = NULL;
+
+DROP TABLE trigger_test;
+
+DROP FUNCTION mytrigger();