On Sat, 2009-10-10 at 00:04 +0300, Peter Eisentraut wrote:
> On Mon, 2009-09-14 at 18:58 +0900, Itagaki Takahiro wrote:
> > Itagaki Takahiro <itagaki.takah...@oss.ntt.co.jp> wrote:
> > 
> > > Ok, the attached patch implements standard-compliant version of
> > > column trigger.
> > 
> > Here is an updated version of column-level trigger patch.
> > I forgot to adjust pg_get_triggerdef() in the previous version.
> > pg_dump also uses pg_get_triggerdef() instead of building
> > CREATE TRIGGER statements to avoid duplicated codes if the
> > server version is 8.5 or later.
> 
> I have committed the parts involving pg_get_triggerdef and pg_dump.  I
> will get to the actual column trigger functionality next.

Attached is a merged up patch with some slightly improved documentation.

I think the patch is almost ready now.  One remaining issue is, in
TriggerEnabled() you apparently check the column list only if it is a
row trigger.  But columns are supported for statement triggers as well
per SQL standard.  Check please.

Btw., I might not get a chance to commit this within the next 48 hours.
If someone else wants to, go ahead.
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 6185a7d..f656cbf 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -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>colname1</replaceable> [, <replaceable>colname2</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>
 
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 36bf050..ed956fe 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -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 only fire 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/commands/trigger.c b/src/backend/commands/trigger.c
index 790cbdc..1af0c61 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -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"
@@ -66,7 +69,8 @@ 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);
+static bool TriggerEnabled(Trigger *trigger, Bitmapset *modifiedCols);
 
 
 /*
@@ -98,6 +102,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 +343,39 @@ CreateTrigger(CreateTrigStmt *stmt,
 														CStringGetDatum(""));
 	}
 
-	/* tgattr is currently always a zero-length array */
-	tgattr = buildint2vector(NULL, 0);
+	/* build column references for UPDATE OF */
+	ncolumns = list_length(stmt->columns);
+	if (ncolumns == 0)
+		columns = NULL;
+	else
+	{
+		ListCell   *cell;
+		int			x = 0;
+
+		columns = (int2 *) palloc(ncolumns * sizeof(int2));
+
+		foreach (cell, stmt->columns)
+		{
+			char   *name = strVal(lfirst(cell));
+			int		attnum;
+			int		y;
+
+			/* Lookup column name. System columns are not allowed. */
+			attnum = attnameAttNum(rel, name, false);
+
+			/* Check for duplicates */
+			for (y = x - 1; y >= 0; y--)
+			{
+				if (columns[y] == attnum)
+					ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_COLUMN),
+						 errmsg("column \"%s\" specified more than once", name)));
+			}
+
+			columns[x++] = attnum;
+		}
+	}
+	tgattr = buildint2vector(columns, ncolumns);
 	values[Anum_pg_trigger_tgattr - 1] = PointerGetDatum(tgattr);
 
 	tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
@@ -434,6 +471,23 @@ CreateTrigger(CreateTrigStmt *stmt,
 		Assert(!OidIsValid(indexOid));
 	}
 
+	/* Add dependency on columns */
+	if (columns != NULL)
+	{
+		int					i;
+		Form_pg_attribute  *attrs;
+
+		attrs = RelationGetDescr(rel)->attrs;
+
+		referenced.classId = RelationRelationId;
+		referenced.objectId = RelationGetRelid(rel);
+		for (i = 0; i < ncolumns; i++)
+		{
+			referenced.objectSubId = attrs[columns[i] - 1]->attnum;
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+	}
+
 	/* Keep lock on target rel until end of xact */
 	heap_close(rel, NoLock);
 
@@ -1626,18 +1680,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, NULL))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -1659,7 +1704,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 +1730,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, NULL))
+			continue;
+
 		LocTriggerData.tg_trigtuple = oldtuple = newtuple;
 		LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
 		LocTriggerData.tg_trigger = trigger;
@@ -1721,7 +1757,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 +1793,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, NULL))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -1790,7 +1817,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 +1851,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, NULL))
+			continue;
+
 		LocTriggerData.tg_trigtuple = trigtuple;
 		LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
 		LocTriggerData.tg_trigger = trigger;
@@ -1869,7 +1887,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);
 	}
 }
@@ -1907,18 +1925,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, NULL))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -1940,7 +1949,7 @@ 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, NULL);
 }
 
 HeapTuple
@@ -1957,12 +1966,15 @@ ExecBRUpdateTriggers(EState *estate, PlanState *subplanstate,
 	HeapTuple	intuple = newtuple;
 	TupleTableSlot *newSlot;
 	int			i;
+	Bitmapset   *modifiedCols;
 
 	trigtuple = GetTupleForTrigger(estate, subplanstate, relinfo, tupleid,
 								   &newSlot);
 	if (trigtuple == NULL)
 		return NULL;
 
+	modifiedCols = rt_fetch(relinfo->ri_RangeTableIndex, estate->es_range_table)->modifiedCols;
+
 	/*
 	 * In READ COMMITTED isolation level it's possible that newtuple was
 	 * changed due to concurrent update.  In that case we have a raw subplan
@@ -1980,18 +1992,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, modifiedCols))
+			continue;
+
 		LocTriggerData.tg_trigtuple = trigtuple;
 		LocTriggerData.tg_newtuple = oldtuple = newtuple;
 		LocTriggerData.tg_trigtuplebuf = InvalidBuffer;
@@ -2024,7 +2027,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 												   tupleid, NULL);
 
 		AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE,
-							  true, trigtuple, newtuple, recheckIndexes);
+							  true, trigtuple, newtuple, recheckIndexes,
+							  rt_fetch(relinfo->ri_RangeTableIndex, estate->es_range_table)->modifiedCols);
 		heap_freetuple(trigtuple);
 	}
 }
@@ -2062,18 +2066,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, NULL))
+			continue;
+
 		LocTriggerData.tg_trigger = trigger;
 		newtuple = ExecCallTriggerFunc(&LocTriggerData,
 									   tgindx[i],
@@ -2095,7 +2090,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);
 }
 
 
@@ -3825,7 +3820,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,19 +3922,8 @@ 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, modifiedCols))
+			continue;
 
 		/*
 		 * If this is an UPDATE of a PK table or FK table that does not change
@@ -4012,3 +3996,43 @@ AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger,
 							 &new_event, &new_shared);
 	}
 }
+
+static bool
+TriggerEnabled(Trigger *trigger, Bitmapset *modifiedCols)
+{
+	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-level triggers */
+	if (trigger->tgnattr > 0 && modifiedCols != NULL &&
+		(trigger->tgtype & (TRIGGER_TYPE_UPDATE | TRIGGER_TYPE_ROW)) ==
+		(TRIGGER_TYPE_UPDATE | TRIGGER_TYPE_ROW))
+	{
+		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;
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d4acf60..9a2da19 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -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 = llast($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 = llast($6);
 					n->isconstraint  = TRUE;
 					n->deferrable = ($10 & 1) != 0;
 					n->initdeferred = ($10 & 2) != 0;
@@ -3279,17 +3281,22 @@ TriggerEvents:
 				{ $$ = $1; }
 			| TriggerEvents OR TriggerOneEvent
 				{
-					if ($1 & $3)
+					int		events1 = intVal(linitial($1));
+					int		events2 = intVal(linitial($3));
+
+					if (events1 & events2)
 						parser_yyerror("duplicate trigger events specified");
-					$$ = $1 | $3;
+					$$ = list_make2(makeInteger(events1 | events2),
+										list_concat(llast($1), llast($3)));
 				}
 		;
 
 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 d20c893..529056d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -543,6 +543,20 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty)
 			appendStringInfo(&buf, " OR UPDATE");
 		else
 			appendStringInfo(&buf, " UPDATE");
+		if (trigrec->tgattr.dim1 > 0)
+		{
+			int		i;
+
+			appendStringInfoString(&buf, " OF ");
+			for (i = 0; i < trigrec->tgattr.dim1; i++)
+			{
+				if (i > 0)
+					appendStringInfoString(&buf, ", ");
+				appendStringInfoString(&buf,
+					quote_identifier(get_relid_attribute_name(
+					trigrec->tgrelid, trigrec->tgattr.values[i])));
+			}
+		}
 	}
 	if (TRIGGER_FOR_TRUNCATE(trigrec->tgtype))
 	{
diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h
index 64ca26f..17198a9 100644
--- a/src/include/catalog/pg_trigger.h
+++ b/src/include/catalog/pg_trigger.h
@@ -53,7 +53,7 @@ CATALOG(pg_trigger,2620)
 	int2		tgnargs;		/* # of extra arguments in tgargs */
 
 	/* VARIABLE LENGTH FIELDS: */
-	int2vector	tgattr;			/* reserved for column-specific triggers */
+	int2vector	tgattr;			/* column-specific triggers */
 	bytea		tgargs;			/* first\000second\000tgnargs\000 */
 } FormData_pg_trigger;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index c629aca..bbd903f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1575,6 +1575,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 */
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index c0b7645..3e8c599 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,73 @@ SELECT * FROM main_table ORDER BY a, b;
     |   
 (8 rows)
 
+-- Column-level triggers should only fire on after row-level updates
+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');
+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_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_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+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
+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
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP TRIGGER after_upd_a_b_row_trig ON main_table;
+DROP TRIGGER after_upd_b_row_trig ON main_table;
+ALTER TABLE main_table DROP COLUMN b;
 -- 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 878adbb..ebef805 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,32 @@ COPY main_table (a, b) FROM stdin;
 
 SELECT * FROM main_table ORDER BY a, b;
 
+-- Column-level triggers should only fire on after row-level updates
+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');
+
+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;
+
+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');
+
+ALTER TABLE main_table DROP COLUMN b;
+DROP TRIGGER after_upd_a_b_row_trig ON main_table;
+DROP TRIGGER after_upd_b_row_trig ON main_table;
+ALTER TABLE main_table DROP COLUMN b;
+
 -- Test enable/disable triggers
 
 create table trigtest (i serial primary key);
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to