From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Wed, 14 Oct 2009 22:14:25 +0000 (+0000)
Subject: Support SQL-compliant triggers on columns, ie fire only if certain columns
X-Git-Tag: REL8_5_ALPHA2~12
X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=b2734a0d792df710aeeab21242cfa21ab470c773;p=postgresql

Support SQL-compliant triggers on columns, ie fire only if certain columns
are named in the UPDATE's SET list.

Note: the schema of pg_trigger has not actually changed; we've just started
to use a column that was there all along.  catversion bumped anyway so that
this commit is included in the history of potentially interesting changes
to system catalog contents.

Itagaki Takahiro
---

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 487dd7e169..174dd0ee81 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/catalogs.sgml,v 2.209 2009/10/07 22:14:14 alvherre Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/catalogs.sgml,v 2.210 2009/10/14 22:14:21 tgl Exp $ -->
 <!--
  Documentation of the system catalogs, directed toward PostgreSQL developers
  -->
@@ -4745,8 +4745,9 @@
      <row>
       <entry><structfield>tgattr</structfield></entry>
       <entry><type>int2vector</type></entry>
-      <entry></entry>
-      <entry>Currently unused</entry>
+      <entry><literal><link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.attnum</literal></entry>
+      <entry>column numbers, if trigger is column-specific; otherwise an
+       empty array</entry>
      </row>
 
      <row>
@@ -4759,6 +4760,14 @@
    </tgroup>
   </table>
 
+  <para>
+   Currently, column-specific triggering is supported only for
+   <literal>UPDATE</> events, and so <structfield>tgattr</> is relevant
+   only for that event type.  <structfield>tgtype</structfield> might
+   contain bits for other event types as well, but those are presumed
+   to be table-wide regardless of what is in <structfield>tgattr</>.
+  </para>
+
   <note>
    <para>
     When <structfield>tgconstraint</> is nonzero,
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 38036ee168..04f681f030 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -1,5 +1,5 @@
 <!--
-$PostgreSQL: pgsql/doc/src/sgml/ref/create_trigger.sgml,v 1.50 2009/09/19 10:23:27 petere Exp $
+$PostgreSQL: pgsql/doc/src/sgml/ref/create_trigger.sgml,v 1.51 2009/10/14 22:14:21 tgl Exp $
 PostgreSQL documentation
 -->
 
@@ -49,7 +49,7 @@ CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTE
    current row, or change the row being inserted (for
    <command>INSERT</command> and <command>UPDATE</command> operations
    only). If the trigger fires after the event, all changes, including
-   the last insertion, update, or deletion, are <quote>visible</quote>
+   the effects of other triggers, are <quote>visible</quote>
    to the trigger.
   </para>
 
@@ -122,6 +122,16 @@ CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTE
       this specifies the event that will fire the trigger. Multiple
       events can be specified using <literal>OR</literal>.
      </para>
+
+     <para>
+      For <command>UPDATE</command> triggers, it is possible to
+      specify a list of columns using this syntax:
+<synopsis>
+UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</replaceable> ... ]
+</synopsis>
+      The trigger will only fire if at least one of the listed columns
+      is mentioned as a target of the update.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -169,8 +179,8 @@ CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTE
       literal string constants.  Simple names and numeric constants
       can be written here, too, but they will all be converted to
       strings.  Please check the description of the implementation
-      language of the trigger function about how the trigger arguments
-      are accessible within the function; it might be different from
+      language of the trigger function to find out how these arguments
+      can be accessed within the function; it might be different from
       normal function arguments.
      </para>
     </listitem>
@@ -191,6 +201,18 @@ CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTE
    endterm="sql-droptrigger-title"> to remove a trigger.
   </para>
 
+  <para>
+   A column-specific trigger (<literal>FOR UPDATE OF
+   <replaceable>column_name</replaceable></literal>) will fire when any
+   of its columns are listed as targets in the <command>UPDATE</>
+   command's <literal>SET</> list.  It is possible for a column's value
+   to change even when the trigger is not fired, because changes made to the
+   row's contents by <literal>BEFORE UPDATE</> triggers are not considered.
+   Conversely, a command such as <literal>UPDATE ... SET x = x ...</>
+   will fire a trigger on column <literal>x</>, even though the column's
+   value did not change.
+  </para>
+
   <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
@@ -218,13 +240,6 @@ CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTE
    <acronym>SQL</> standard. The following functionality is currently missing:
 
    <itemizedlist>
-    <listitem>
-     <para>
-      SQL allows triggers to fire on updates to specific columns
-      (e.g., <literal>AFTER UPDATE OF col1, col2</literal>).
-     </para>
-    </listitem>
-
     <listitem>
      <para>
       SQL allows you to define aliases for the <quote>old</quote>
@@ -264,9 +279,10 @@ CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTE
    The <productname>PostgreSQL</productname> behavior is for <literal>BEFORE
    DELETE</literal> to always fire before the delete action, even a cascading
    one.  This is considered more consistent.  There is also unpredictable
-   behavior when <literal>BEFORE</literal> triggers modify rows that are later
-   to be modified by referential actions.  This can lead to constraint violations
-   or stored data that does not honor the referential constraint.
+   behavior when <literal>BEFORE</literal> triggers modify rows or prevent
+   updates during an update that is caused by a referential action.  This can
+   lead to constraint violations or stored data that does not honor the
+   referential constraint.
   </para>
 
   <para>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 50e5943401..60977cbd56 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/trigger.sgml,v 1.58 2009/08/04 22:04:37 petere Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/trigger.sgml,v 1.59 2009/10/14 22:14:21 tgl Exp $ -->
 
  <chapter id="triggers">
   <title>Triggers</title>
@@ -36,10 +36,13 @@
     performed.  Triggers can be defined to execute either before or after any
     <command>INSERT</command>, <command>UPDATE</command>, or
     <command>DELETE</command> operation, either once per modified row,
-    or once per <acronym>SQL</acronym> statement.  Triggers can also fire
-    for <command>TRUNCATE</command> statements.  If a trigger event occurs,
-    the trigger's function is called at the appropriate time to handle the
-    event.
+    or once per <acronym>SQL</acronym> statement.
+    <command>UPDATE</command> triggers can moreover be set to fire only if
+    certain columns are mentioned in the <literal>SET</literal> clause of the
+    <command>UPDATE</command> statement.
+    Triggers can also fire for <command>TRUNCATE</command> statements.
+    If a trigger event occurs, the trigger's function is called at the
+    appropriate time to handle the event.
    </para>
 
    <para>
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index b43b2910e7..573b00216d 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/catalog/index.c,v 1.322 2009/10/05 19:24:35 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/catalog/index.c,v 1.323 2009/10/14 22:14:21 tgl Exp $
  *
  *
  * INTERFACE ROUTINES
@@ -792,6 +792,7 @@ index_create(Oid heapRelationId,
 				trigger->before = false;
 				trigger->row = true;
 				trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE;
+				trigger->columns = NIL;
 				trigger->isconstraint = true;
 				trigger->deferrable = true;
 				trigger->initdeferred = initdeferred;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ce7d14cee6..24cbe1dd94 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/commands/tablecmds.c,v 1.303 2009/10/13 00:53:07 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/commands/tablecmds.c,v 1.304 2009/10/14 22:14:21 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -5441,6 +5441,7 @@ CreateFKCheckTrigger(RangeVar *myRel, Constraint *fkconstraint,
 		fk_trigger->events = TRIGGER_TYPE_UPDATE;
 	}
 
+	fk_trigger->columns = NIL;
 	fk_trigger->isconstraint = true;
 	fk_trigger->deferrable = fkconstraint->deferrable;
 	fk_trigger->initdeferred = fkconstraint->initdeferred;
@@ -5491,6 +5492,7 @@ createForeignKeyTriggers(Relation rel, Constraint *fkconstraint,
 	fk_trigger->before = false;
 	fk_trigger->row = true;
 	fk_trigger->events = TRIGGER_TYPE_DELETE;
+	fk_trigger->columns = NIL;
 	fk_trigger->isconstraint = true;
 	fk_trigger->constrrel = myRel;
 	switch (fkconstraint->fk_del_action)
@@ -5543,6 +5545,7 @@ createForeignKeyTriggers(Relation rel, Constraint *fkconstraint,
 	fk_trigger->before = false;
 	fk_trigger->row = true;
 	fk_trigger->events = TRIGGER_TYPE_UPDATE;
+	fk_trigger->columns = NIL;
 	fk_trigger->isconstraint = true;
 	fk_trigger->constrrel = myRel;
 	switch (fkconstraint->fk_upd_action)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 47cf0c470d..dd526f6db1 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/commands/trigger.c,v 1.253 2009/10/10 01:43:45 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/commands/trigger.c,v 1.254 2009/10/14 22:14:21 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -30,8 +30,11 @@
 #include "executor/executor.h"
 #include "executor/instrument.h"
 #include "miscadmin.h"
+#include "nodes/bitmapset.h"
 #include "nodes/makefuncs.h"
 #include "parser/parse_func.h"
+#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
 #include "pgstat.h"
 #include "storage/bufmgr.h"
 #include "tcop/utility.h"
@@ -51,6 +54,9 @@
 int			SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
 
 
+#define GetModifiedColumns(relinfo, estate) \
+	(rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->modifiedCols)
+
 /* Local function prototypes */
 static void ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid);
 static void InsertTrigger(TriggerDesc *trigdesc, Trigger *trigger, int indx);
@@ -59,6 +65,8 @@ static HeapTuple GetTupleForTrigger(EState *estate,
 				   ResultRelInfo *relinfo,
 				   ItemPointer tid,
 				   TupleTableSlot **newSlot);
+static bool TriggerEnabled(Trigger *trigger, TriggerEvent event,
+						   Bitmapset *modifiedCols);
 static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
 					int tgindx,
 					FmgrInfo *finfo,
@@ -66,7 +74,7 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
 					MemoryContext per_tuple_context);
 static void AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event,
 					  bool row_trigger, HeapTuple oldtup, HeapTuple newtup,
-					  List *recheckIndexes);
+					  List *recheckIndexes, Bitmapset *modifiedCols);
 
 
 /*
@@ -98,6 +106,8 @@ CreateTrigger(CreateTrigStmt *stmt,
 			  bool checkPermissions)
 {
 	int16		tgtype;
+	int			ncolumns;
+	int2       *columns;
 	int2vector *tgattr;
 	Datum		values[Natts_pg_trigger];
 	bool		nulls[Natts_pg_trigger];
@@ -337,8 +347,44 @@ CreateTrigger(CreateTrigStmt *stmt,
 														CStringGetDatum(""));
 	}
 
-	/* tgattr is currently always a zero-length array */
-	tgattr = buildint2vector(NULL, 0);
+	/* build column number array if it's a column-specific trigger */
+	ncolumns = list_length(stmt->columns);
+	if (ncolumns == 0)
+		columns = NULL;
+	else
+	{
+		ListCell   *cell;
+		int			i = 0;
+
+		columns = (int2 *) palloc(ncolumns * sizeof(int2));
+		foreach(cell, stmt->columns)
+		{
+			char   *name = strVal(lfirst(cell));
+			int2	attnum;
+			int		j;
+
+			/* Lookup column name.  System columns are not allowed */
+			attnum = attnameAttNum(rel, name, false);
+			if (attnum == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_COLUMN),
+						 errmsg("column \"%s\" of relation \"%s\" does not exist",
+								name, RelationGetRelationName(rel))));
+
+			/* Check for duplicates */
+			for (j = i - 1; j >= 0; j--)
+			{
+				if (columns[j] == attnum)
+					ereport(ERROR,
+							(errcode(ERRCODE_DUPLICATE_COLUMN),
+							 errmsg("column \"%s\" specified more than once",
+									name)));
+			}
+
+			columns[i++] = attnum;
+		}
+	}
+	tgattr = buildint2vector(columns, ncolumns);
 	values[Anum_pg_trigger_tgattr - 1] = PointerGetDatum(tgattr);
 
 	tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
@@ -358,6 +404,7 @@ CreateTrigger(CreateTrigStmt *stmt,
 
 	pfree(DatumGetPointer(values[Anum_pg_trigger_tgname - 1]));
 	pfree(DatumGetPointer(values[Anum_pg_trigger_tgargs - 1]));
+	pfree(DatumGetPointer(values[Anum_pg_trigger_tgattr - 1]));
 
 	/*
 	 * Update relation's pg_class entry.  Crucial side-effect: other backends
@@ -434,6 +481,20 @@ CreateTrigger(CreateTrigStmt *stmt,
 		Assert(!OidIsValid(indexOid));
 	}
 
+	/* If column-specific trigger, add normal dependencies on columns */
+	if (columns != NULL)
+	{
+		int		i;
+
+		referenced.classId = RelationRelationId;
+		referenced.objectId = RelationGetRelid(rel);
+		for (i = 0; i < ncolumns; i++)
+		{
+			referenced.objectSubId = columns[i];
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+	}
+
 	/* Keep lock on target rel until end of xact */
 	heap_close(rel, NoLock);
 
@@ -1626,18 +1687,9 @@ ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo)
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 		HeapTuple	newtuple;
 
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -1659,7 +1711,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo)
 
 	if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_INSERT] > 0)
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT,
-							  false, NULL, NULL, NIL);
+							  false, NULL, NULL, NIL, NULL);
 }
 
 HeapTuple
@@ -1685,18 +1737,9 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 	{
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL))
+			continue;
+
 		LocTriggerData.tg_trigtuple = oldtuple = newtuple;
 		LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
 		LocTriggerData.tg_trigger = trigger;
@@ -1721,7 +1764,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 
 	if (trigdesc && trigdesc->n_after_row[TRIGGER_EVENT_INSERT] > 0)
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT,
-							  true, NULL, trigtuple, recheckIndexes);
+							  true, NULL, trigtuple, recheckIndexes, NULL);
 }
 
 void
@@ -1757,18 +1800,9 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 		HeapTuple	newtuple;
 
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -1790,7 +1824,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
 
 	if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_DELETE] > 0)
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE,
-							  false, NULL, NULL, NIL);
+							  false, NULL, NULL, NIL, NULL);
 }
 
 bool
@@ -1824,18 +1858,9 @@ ExecBRDeleteTriggers(EState *estate, PlanState *subplanstate,
 	{
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL))
+			continue;
+
 		LocTriggerData.tg_trigtuple = trigtuple;
 		LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
 		LocTriggerData.tg_trigger = trigger;
@@ -1869,7 +1894,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
 												   tupleid, NULL);
 
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE,
-							  true, trigtuple, NULL, NIL);
+							  true, trigtuple, NULL, NIL, NULL);
 		heap_freetuple(trigtuple);
 	}
 }
@@ -1882,6 +1907,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
 	int		   *tgindx;
 	int			i;
 	TriggerData LocTriggerData;
+	Bitmapset   *modifiedCols;
 
 	trigdesc = relinfo->ri_TrigDesc;
 
@@ -1894,6 +1920,8 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
 	if (ntrigs == 0)
 		return;
 
+	modifiedCols = GetModifiedColumns(relinfo, estate);
+
 	LocTriggerData.type = T_TriggerData;
 	LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
 		TRIGGER_EVENT_BEFORE;
@@ -1907,18 +1935,9 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 		HeapTuple	newtuple;
 
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, LocTriggerData.tg_event, modifiedCols))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -1940,7 +1959,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
 
 	if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_UPDATE] > 0)
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE,
-							  false, NULL, NULL, NIL);
+							  false, NULL, NULL, NIL,
+							  GetModifiedColumns(relinfo, estate));
 }
 
 HeapTuple
@@ -1957,6 +1977,7 @@ ExecBRUpdateTriggers(EState *estate, PlanState *subplanstate,
 	HeapTuple	intuple = newtuple;
 	TupleTableSlot *newSlot;
 	int			i;
+	Bitmapset   *modifiedCols;
 
 	trigtuple = GetTupleForTrigger(estate, subplanstate, relinfo, tupleid,
 								   &newSlot);
@@ -1971,6 +1992,8 @@ ExecBRUpdateTriggers(EState *estate, PlanState *subplanstate,
 	if (newSlot != NULL)
 		intuple = newtuple = ExecRemoveJunk(relinfo->ri_junkFilter, newSlot);
 
+	modifiedCols = GetModifiedColumns(relinfo, estate);
+
 	LocTriggerData.type = T_TriggerData;
 	LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
 		TRIGGER_EVENT_ROW |
@@ -1980,18 +2003,9 @@ ExecBRUpdateTriggers(EState *estate, PlanState *subplanstate,
 	{
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, LocTriggerData.tg_event, modifiedCols))
+			continue;
+
 		LocTriggerData.tg_trigtuple = trigtuple;
 		LocTriggerData.tg_newtuple = oldtuple = newtuple;
 		LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
@@ -2024,7 +2038,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 												   tupleid, NULL);
 
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE,
-							  true, trigtuple, newtuple, recheckIndexes);
+							  true, trigtuple, newtuple, recheckIndexes,
+							  GetModifiedColumns(relinfo, estate));
 		heap_freetuple(trigtuple);
 	}
 }
@@ -2062,18 +2077,9 @@ ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 		HeapTuple	newtuple;
 
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -2095,7 +2101,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
 
 	if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_TRUNCATE] > 0)
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_TRUNCATE,
-							  false, NULL, NULL, NIL);
+							  false, NULL, NULL, NIL, NULL);
 }
 
 
@@ -2201,6 +2207,52 @@ ltrmark:;
 	return result;
 }
 
+/*
+ * Is trigger enabled to fire?
+ */
+static bool
+TriggerEnabled(Trigger *trigger, TriggerEvent event, Bitmapset *modifiedCols)
+{
+	/* Check replication-role-dependent enable state */
+	if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
+	{
+		if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
+			trigger->tgenabled == TRIGGER_DISABLED)
+			return false;
+	}
+	else	/* ORIGIN or LOCAL role */
+	{
+		if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
+			trigger->tgenabled == TRIGGER_DISABLED)
+			return false;
+	}
+
+	/*
+	 * Check for column-specific trigger (only possible for UPDATE, and in
+	 * fact we *must* ignore tgattr for other event types)
+	 */
+	if (trigger->tgnattr > 0 && TRIGGER_FIRED_BY_UPDATE(event))
+	{
+		int			i;
+		bool		modified;
+
+		modified = false;
+		for (i = 0; i < trigger->tgnattr; i++)
+		{
+			if (bms_is_member(trigger->tgattr[i] - FirstLowInvalidHeapAttributeNumber,
+							  modifiedCols))
+			{
+				modified = true;
+				break;
+			}
+		}
+		if (!modified)
+			return false;
+	}
+
+	return true;
+}
+
 
 /* ----------
  * After-trigger stuff
@@ -3825,7 +3877,7 @@ AfterTriggerPendingOnRel(Oid relid)
 static void
 AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger,
 					  HeapTuple oldtup, HeapTuple newtup,
-					  List *recheckIndexes)
+					  List *recheckIndexes, Bitmapset *modifiedCols)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -3927,26 +3979,15 @@ AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger,
 	{
 		Trigger    *trigger = &trigdesc->triggers[tgindx[i]];
 
-		/* Ignore disabled triggers */
-		if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
-		else	/* ORIGIN or LOCAL role */
-		{
-			if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA ||
-				trigger->tgenabled == TRIGGER_DISABLED)
-				continue;
-		}
+		if (!TriggerEnabled(trigger, event, modifiedCols))
+			continue;
 
 		/*
 		 * If this is an UPDATE of a PK table or FK table that does not change
 		 * the PK or FK respectively, we can skip queuing the event: there is
 		 * no need to fire the trigger.
 		 */
-		if ((event & TRIGGER_EVENT_OPMASK) == TRIGGER_EVENT_UPDATE)
+		if (TRIGGER_FIRED_BY_UPDATE(event))
 		{
 			switch (RI_FKey_trigger_type(trigger->tgfoid))
 			{
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 49360e506a..92da5324c2 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -15,7 +15,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.447 2009/10/13 00:53:08 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.448 2009/10/14 22:14:21 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -3160,6 +3160,7 @@ _copyCreateTrigStmt(CreateTrigStmt *from)
 	COPY_SCALAR_FIELD(before);
 	COPY_SCALAR_FIELD(row);
 	COPY_SCALAR_FIELD(events);
+	COPY_NODE_FIELD(columns);
 	COPY_SCALAR_FIELD(isconstraint);
 	COPY_SCALAR_FIELD(deferrable);
 	COPY_SCALAR_FIELD(initdeferred);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index cfa0a40de7..0c30d6aa9d 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -22,7 +22,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.369 2009/10/13 00:53:08 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.370 2009/10/14 22:14:21 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -1671,6 +1671,7 @@ _equalCreateTrigStmt(CreateTrigStmt *a, CreateTrigStmt *b)
 	COMPARE_SCALAR_FIELD(before);
 	COMPARE_SCALAR_FIELD(row);
 	COMPARE_SCALAR_FIELD(events);
+	COMPARE_NODE_FIELD(columns);
 	COMPARE_SCALAR_FIELD(isconstraint);
 	COMPARE_SCALAR_FIELD(deferrable);
 	COMPARE_SCALAR_FIELD(initdeferred);
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 20d40841ef..4fec749eba 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -9,7 +9,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/optimizer/plan/setrefs.c,v 1.152 2009/10/12 18:10:48 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/optimizer/plan/setrefs.c,v 1.153 2009/10/14 22:14:22 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -193,7 +193,8 @@ set_plan_references(PlannerGlobal *glob, Plan *plan,
 	 * needed by the executor; this reduces the storage space and copying cost
 	 * for cached plans.  We keep only the alias and eref Alias fields, which
 	 * are needed by EXPLAIN, and the selectedCols and modifiedCols bitmaps,
-	 * which are needed for executor-startup permissions checking.
+	 * which are needed for executor-startup permissions checking and for
+	 * trigger event checking.
 	 */
 	foreach(lc, rtable)
 	{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 17463ee3b8..2fe6ab1612 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -11,7 +11,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.685 2009/10/12 23:41:43 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.686 2009/10/14 22:14:22 tgl Exp $
  *
  * HISTORY
  *	  AUTHOR			DATE			MAJOR EVENT
@@ -251,7 +251,7 @@ static TypeName *TableFuncTypeName(List *columns);
 
 %type <boolean> TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs
 
-%type <ival>	TriggerEvents TriggerOneEvent
+%type <list>	TriggerEvents TriggerOneEvent
 %type <value>	TriggerFuncArg
 
 %type <str>		relation_name copy_file_name
@@ -3240,7 +3240,8 @@ CreateTrigStmt:
 					n->args = $13;
 					n->before = $4;
 					n->row = $8;
-					n->events = $5;
+					n->events = intVal(linitial($5));
+					n->columns = (List *) lsecond($5);
 					n->isconstraint  = FALSE;
 					n->deferrable	 = FALSE;
 					n->initdeferred  = FALSE;
@@ -3260,7 +3261,8 @@ CreateTrigStmt:
 					n->args = $18;
 					n->before = FALSE;
 					n->row = TRUE;
-					n->events = $6;
+					n->events = intVal(linitial($6));
+					n->columns = (List *) lsecond($6);
 					n->isconstraint  = TRUE;
 					n->deferrable = ($10 & 1) != 0;
 					n->initdeferred = ($10 & 2) != 0;
@@ -3279,17 +3281,36 @@ TriggerEvents:
 				{ $$ = $1; }
 			| TriggerEvents OR TriggerOneEvent
 				{
-					if ($1 & $3)
+					int		events1 = intVal(linitial($1));
+					int		events2 = intVal(linitial($3));
+					List   *columns1 = (List *) lsecond($1);
+					List   *columns2 = (List *) lsecond($3);
+
+					if (events1 & events2)
 						parser_yyerror("duplicate trigger events specified");
-					$$ = $1 | $3;
+					/*
+					 * concat'ing the columns lists loses information about
+					 * which columns went with which event, but so long as
+					 * only UPDATE carries columns and we disallow multiple
+					 * UPDATE items, it doesn't matter.  Command execution
+					 * should just ignore the columns for non-UPDATE events.
+					 */
+					$$ = list_make2(makeInteger(events1 | events2),
+									list_concat(columns1, columns2));
 				}
 		;
 
 TriggerOneEvent:
-			INSERT								{ $$ = TRIGGER_TYPE_INSERT; }
-			| DELETE_P							{ $$ = TRIGGER_TYPE_DELETE; }
-			| UPDATE							{ $$ = TRIGGER_TYPE_UPDATE; }
-			| TRUNCATE							{ $$ = TRIGGER_TYPE_TRUNCATE; }
+			INSERT
+				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_INSERT), NIL); }
+			| DELETE_P
+				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_DELETE), NIL); }
+			| UPDATE
+				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_UPDATE), NIL); }
+			| UPDATE OF columnList
+				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_UPDATE), $3); }
+			| TRUNCATE
+				{ $$ = list_make2(makeInteger(TRIGGER_TYPE_TRUNCATE), NIL); }
 		;
 
 TriggerForSpec:
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 35c530bbda..8538c74d12 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9,7 +9,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.309 2009/10/10 01:43:49 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.310 2009/10/14 22:14:23 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -543,6 +543,23 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty)
 			appendStringInfo(&buf, " OR UPDATE");
 		else
 			appendStringInfo(&buf, " UPDATE");
+		/* tgattr is first var-width field, so OK to access directly */
+		if (trigrec->tgattr.dim1 > 0)
+		{
+			int		i;
+
+			appendStringInfoString(&buf, " OF ");
+			for (i = 0; i < trigrec->tgattr.dim1; i++)
+			{
+				char   *attname;
+
+				if (i > 0)
+					appendStringInfoString(&buf, ", ");
+				attname = get_relid_attribute_name(trigrec->tgrelid,
+												   trigrec->tgattr.values[i]);
+				appendStringInfoString(&buf, quote_identifier(attname));
+			}
+		}
 	}
 	if (TRIGGER_FOR_TRUNCATE(trigrec->tgtype))
 	{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b0dfba9d6d..e25bf7ab0b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -12,7 +12,7 @@
  *	by PostgreSQL
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/bin/pg_dump/pg_dump.c,v 1.551 2009/10/12 23:41:43 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/bin/pg_dump/pg_dump.c,v 1.552 2009/10/14 22:14:23 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -4419,6 +4419,18 @@ getTriggers(TableInfo tblinfo[], int numTables)
 			if (i_tgdef >= 0)
 			{
 				tginfo[j].tgdef = strdup(PQgetvalue(res, j, i_tgdef));
+
+				/* remaining fields are not valid if we have tgdef */
+				tginfo[j].tgfname = NULL;
+				tginfo[j].tgtype = 0;
+				tginfo[j].tgnargs = 0;
+				tginfo[j].tgargs = NULL;
+				tginfo[j].tgisconstraint = false;
+				tginfo[j].tgdeferrable = false;
+				tginfo[j].tginitdeferred = false;
+				tginfo[j].tgconstrname = NULL;
+				tginfo[j].tgconstrrelid = InvalidOid;
+				tginfo[j].tgconstrrelname = NULL;
 			}
 			else
 			{
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index abbcb006e1..ea053041bf 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -37,7 +37,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/catalog/catversion.h,v 1.545 2009/10/12 18:10:51 tgl Exp $
+ * $PostgreSQL: pgsql/src/include/catalog/catversion.h,v 1.546 2009/10/14 22:14:24 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	200910121
+#define CATALOG_VERSION_NO	200910141
 
 #endif
diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h
index 64730bc60f..eb6a583fd2 100644
--- a/src/include/catalog/pg_trigger.h
+++ b/src/include/catalog/pg_trigger.h
@@ -8,7 +8,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/catalog/pg_trigger.h,v 1.34 2009/07/28 02:56:31 tgl Exp $
+ * $PostgreSQL: pgsql/src/include/catalog/pg_trigger.h,v 1.35 2009/10/14 22:14:24 tgl Exp $
  *
  * NOTES
  *	  the genbki.sh script reads this file and generates .bki
@@ -52,8 +52,8 @@ CATALOG(pg_trigger,2620)
 	bool		tginitdeferred; /* constraint trigger is deferred initially */
 	int2		tgnargs;		/* # of extra arguments in tgargs */
 
-	/* VARIABLE LENGTH FIELDS: */
-	int2vector	tgattr;			/* reserved for column-specific triggers */
+	/* VARIABLE LENGTH FIELDS (note: these are not supposed to be null) */
+	int2vector	tgattr;			/* column numbers, if trigger is on columns */
 	bytea		tgargs;			/* first\000second\000tgnargs\000 */
 } FormData_pg_trigger;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ba05f157f5..adc870543b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -13,7 +13,7 @@
  * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/nodes/parsenodes.h,v 1.409 2009/10/13 00:53:08 tgl Exp $
+ * $PostgreSQL: pgsql/src/include/nodes/parsenodes.h,v 1.410 2009/10/14 22:14:24 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -1560,10 +1560,9 @@ typedef struct DropUserMappingStmt
 } DropUserMappingStmt;
 
 /* ----------------------
- *		Create/Drop TRIGGER Statements
+ *		Create TRIGGER Statement
  * ----------------------
  */
-
 typedef struct CreateTrigStmt
 {
 	NodeTag		type;
@@ -1575,6 +1574,7 @@ typedef struct CreateTrigStmt
 	bool		row;			/* ROW/STATEMENT */
 	/* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
 	int16		events;			/* INSERT/UPDATE/DELETE/TRUNCATE */
+	List	   *columns;		/* column names, or NIL for all columns */
 
 	/* The following are used for constraint triggers (RI and unique checks) */
 	bool		isconstraint;	/* This is a constraint trigger */
@@ -1584,7 +1584,7 @@ typedef struct CreateTrigStmt
 } CreateTrigStmt;
 
 /* ----------------------
- *		Create/Drop PROCEDURAL LANGUAGE Statement
+ *		Create/Drop PROCEDURAL LANGUAGE Statements
  * ----------------------
  */
 typedef struct CreatePLangStmt
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index c0b7645600..b21c93f3b5 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -278,37 +278,37 @@ CREATE TABLE main_table (a int, b int);
 COPY main_table (a,b) FROM stdin;
 CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
 BEGIN
-	RAISE NOTICE ''trigger_func() called: action = %, when = %, level = %'', TG_OP, TG_WHEN, TG_LEVEL;
+	RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
 	RETURN NULL;
 END;';
 CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table
-FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
 CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
-FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
 --
 -- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
 -- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
 --
-CREATE TRIGGER before_upd_stmt_trig AFTER UPDATE ON main_table
-EXECUTE PROCEDURE trigger_func();
-CREATE TRIGGER before_upd_row_trig AFTER UPDATE ON main_table
-FOR EACH ROW EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_table
+EXECUTE PROCEDURE trigger_func('after_upd_stmt');
+CREATE TRIGGER after_upd_row_trig AFTER UPDATE ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_row');
 INSERT INTO main_table DEFAULT VALUES;
-NOTICE:  trigger_func() called: action = INSERT, when = BEFORE, level = STATEMENT
-NOTICE:  trigger_func() called: action = INSERT, when = AFTER, level = STATEMENT
+NOTICE:  trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE:  trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
 UPDATE main_table SET a = a + 1 WHERE b < 30;
-NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = ROW
-NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = ROW
-NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = ROW
-NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = ROW
-NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT
+NOTICE:  trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
 -- UPDATE that effects zero rows should still call per-statement trigger
 UPDATE main_table SET a = a + 2 WHERE b > 100;
-NOTICE:  trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT
+NOTICE:  trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
 -- COPY should fire per-row and per-statement INSERT triggers
 COPY main_table (a, b) FROM stdin;
-NOTICE:  trigger_func() called: action = INSERT, when = BEFORE, level = STATEMENT
-NOTICE:  trigger_func() called: action = INSERT, when = AFTER, level = STATEMENT
+NOTICE:  trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT
+NOTICE:  trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT
 SELECT * FROM main_table ORDER BY a, b;
  a  | b  
 ----+----
@@ -322,6 +322,91 @@ SELECT * FROM main_table ORDER BY a, b;
     |   
 (8 rows)
 
+-- Test column-level triggers
+DROP TRIGGER after_upd_row_trig ON main_table;
+CREATE TRIGGER before_upd_a_row_trig BEFORE UPDATE OF a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_a_row');
+CREATE TRIGGER after_upd_b_row_trig AFTER UPDATE OF b ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_b_row');
+CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_a_b_row');
+CREATE TRIGGER before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_upd_a_stmt');
+CREATE TRIGGER after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_upd_b_stmt');
+SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig';
+                                                             pg_get_triggerdef                                                             
+-------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_a_b_row')
+(1 row)
+
+SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig';
+                    pg_get_triggerdef                    
+---------------------------------------------------------
+ CREATE TRIGGER after_upd_a_b_row_trig
+     AFTER UPDATE OF a, b ON main_table
+     FOR EACH ROW
+     EXECUTE PROCEDURE trigger_func('after_upd_a_b_row')
+(1 row)
+
+UPDATE main_table SET a = 50;
+NOTICE:  trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW
+NOTICE:  trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+UPDATE main_table SET b = 10;
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
+NOTICE:  trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+NOTICE:  trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+-- bogus cases
+CREATE TRIGGER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_and_col');
+ERROR:  duplicate trigger events specified at or near "ON"
+LINE 1: ...ER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_ta...
+                                                             ^
+CREATE TRIGGER error_upd_a_a BEFORE UPDATE OF a, a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_a_a');
+ERROR:  column "a" specified more than once
+CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_ins_a');
+ERROR:  syntax error at or near "OF"
+LINE 1: CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table
+                                                 ^
+-- check dependency restrictions
+ALTER TABLE main_table DROP COLUMN b;
+ERROR:  cannot drop table main_table column b because other objects depend on it
+DETAIL:  trigger after_upd_b_row_trig on table main_table depends on table main_table column b
+trigger after_upd_a_b_row_trig on table main_table depends on table main_table column b
+trigger after_upd_b_stmt_trig on table main_table depends on table main_table column b
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+-- this should succeed, but we'll roll it back to keep the triggers around
+begin;
+DROP TRIGGER after_upd_a_b_row_trig ON main_table;
+DROP TRIGGER after_upd_b_row_trig ON main_table;
+DROP TRIGGER after_upd_b_stmt_trig ON main_table;
+ALTER TABLE main_table DROP COLUMN b;
+rollback;
 -- Test enable/disable triggers
 create table trigtest (i serial primary key);
 NOTICE:  CREATE TABLE will create implicit sequence "trigtest_i_seq" for serial column "trigtest.i"
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 878adbb0d4..70c563477c 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -220,25 +220,25 @@ COPY main_table (a,b) FROM stdin;
 
 CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
 BEGIN
-	RAISE NOTICE ''trigger_func() called: action = %, when = %, level = %'', TG_OP, TG_WHEN, TG_LEVEL;
+	RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
 	RETURN NULL;
 END;';
 
 CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table
-FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
 
 CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
-FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func();
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
 
 --
 -- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
 -- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
 --
-CREATE TRIGGER before_upd_stmt_trig AFTER UPDATE ON main_table
-EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_table
+EXECUTE PROCEDURE trigger_func('after_upd_stmt');
 
-CREATE TRIGGER before_upd_row_trig AFTER UPDATE ON main_table
-FOR EACH ROW EXECUTE PROCEDURE trigger_func();
+CREATE TRIGGER after_upd_row_trig AFTER UPDATE ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_row');
 
 INSERT INTO main_table DEFAULT VALUES;
 
@@ -254,6 +254,45 @@ COPY main_table (a, b) FROM stdin;
 
 SELECT * FROM main_table ORDER BY a, b;
 
+-- Test column-level triggers
+DROP TRIGGER after_upd_row_trig ON main_table;
+
+CREATE TRIGGER before_upd_a_row_trig BEFORE UPDATE OF a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_a_row');
+CREATE TRIGGER after_upd_b_row_trig AFTER UPDATE OF b ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_b_row');
+CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_a_b_row');
+
+CREATE TRIGGER before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_upd_a_stmt');
+CREATE TRIGGER after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_upd_b_stmt');
+
+SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig';
+SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig';
+
+UPDATE main_table SET a = 50;
+UPDATE main_table SET b = 10;
+
+-- bogus cases
+CREATE TRIGGER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_and_col');
+CREATE TRIGGER error_upd_a_a BEFORE UPDATE OF a, a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_a_a');
+CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table
+FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_ins_a');
+
+-- check dependency restrictions
+ALTER TABLE main_table DROP COLUMN b;
+-- this should succeed, but we'll roll it back to keep the triggers around
+begin;
+DROP TRIGGER after_upd_a_b_row_trig ON main_table;
+DROP TRIGGER after_upd_b_row_trig ON main_table;
+DROP TRIGGER after_upd_b_stmt_trig ON main_table;
+ALTER TABLE main_table DROP COLUMN b;
+rollback;
+
 -- Test enable/disable triggers
 
 create table trigtest (i serial primary key);