From 8c48375e5f43ebd832f93c9166d1fe0e639ff806 Mon Sep 17 00:00:00 2001 From: Kevin Grittner Date: Fri, 4 Nov 2016 10:49:50 -0500 Subject: [PATCH] Implement syntax for transition tables in AFTER triggers. This is infrastructure for the complete SQL standard feature. No support is included at this point for execution nodes or PLs. The intent is to add that soon. As this patch leaves things, standard syntax can create tuplestores to contain old and/or new versions of rows affected by a statement. References to these tuplestores are in the TriggerData structure. C triggers can access the tuplestores directly, so they are usable, but they cannot yet be referenced within a SQL statement. --- doc/src/sgml/catalogs.sgml | 16 ++ doc/src/sgml/ref/create_trigger.sgml | 94 ++++++-- src/backend/commands/tablecmds.c | 5 +- src/backend/commands/trigger.c | 327 +++++++++++++++++++++++++-- src/backend/nodes/copyfuncs.c | 16 ++ src/backend/nodes/equalfuncs.c | 14 ++ src/backend/nodes/outfuncs.c | 13 ++ src/backend/parser/gram.y | 70 +++++- src/backend/utils/adt/ruleutils.c | 23 ++ src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_trigger.h | 13 +- src/include/commands/trigger.h | 2 + src/include/nodes/nodes.h | 1 + src/include/nodes/parsenodes.h | 17 ++ src/include/parser/kwlist.h | 3 + src/include/utils/reltrigger.h | 7 + 16 files changed, 580 insertions(+), 43 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 29738b07cb..bac169a19e 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -6231,6 +6231,22 @@ representation) for the trigger's WHEN condition, or null if none + + + tgoldtable + name + + REFERENCING clause name for OLD TABLE, + or null if none + + + + tgnewtable + name + + REFERENCING clause name for NEW TABLE, + or null if none + diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml index 4bde815012..8590e226e3 100644 --- a/doc/src/sgml/ref/create_trigger.sgml +++ b/doc/src/sgml/ref/create_trigger.sgml @@ -25,6 +25,7 @@ CREATE [ CONSTRAINT ] TRIGGER name ON table_name [ FROM referenced_table_name ] [ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ] + [ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ] [ FOR [ EACH ] { ROW | STATEMENT } ] [ WHEN ( condition ) ] EXECUTE PROCEDURE function_name ( arguments ) @@ -177,6 +178,15 @@ CREATE [ CONSTRAINT ] TRIGGER name when the constraints they implement are violated. + + The REFERENCING option is only allowed for an AFTER + trigger which is not a constraint trigger. OLD TABLE may only + be specified once, and only on a trigger which can fire on + UPDATE or DELETE. NEW TABLE may only + be specified once, and only on a trigger which can fire on + UPDATE or INSERT. + + SELECT does not modify any rows so you cannot create SELECT triggers. Rules and views are more @@ -281,6 +291,40 @@ UPDATE OF column_name1 [, column_name2 + + REFERENCING + + + This immediately preceeds the declaration of one or two relations which + can be used to read the before and/or after images of all rows directly + affected by the triggering statement. An AFTER EACH ROW + trigger is allowed to use both these transition relation names and the + row names (OLD and NEW) which reference each + individual row for which the trigger fires. + + + + + + OLD TABLE + NEW TABLE + + + This specifies whether the named relation contains the before or after + images for rows affected by the statement which fired the trigger. + + + + + + transition_relation_name + + + The (unqualified) name to be used within the trigger for this relation. + + + + FOR EACH ROW FOR EACH STATEMENT @@ -474,6 +518,30 @@ CREATE TRIGGER view_insert FOR EACH ROW EXECUTE PROCEDURE view_insert_row(); + + Execute the function check_transfer_balances_to_zero for each + statement to confirm that the transfer rows offset to a net of + zero: + + +CREATE TRIGGER transfer_insert + AFTER INSERT ON transfer + FOR EACH STATEMENT + REFERENCING NEW TABLE AS inserted + EXECUTE PROCEDURE check_transfer_balances_to_zero(); + + + Execute the function check_matching_pairs for each row to + confirm that changes are made to matching pairs at the same time (by the + same statement): + + +CREATE TRIGGER paired_items_update + AFTER UPDATE ON paired_items + FOR EACH ROW + REFERENCING NEW TABLE AS newtab OLD TABLE AS oldtab + EXECUTE PROCEDURE check_matching_pairs(); + @@ -502,24 +570,14 @@ CREATE TRIGGER view_insert - SQL allows you to define aliases for the old - and new rows or tables for use in the definition - of the triggered action (e.g., CREATE TRIGGER ... ON - tablename REFERENCING OLD ROW AS somename NEW ROW AS othername - ...). Since PostgreSQL - allows trigger procedures to be written in any number of - user-defined languages, access to the data is handled in a - language-specific way. - - - - - - PostgreSQL does not allow the old and new - tables to be referenced in statement-level triggers, i.e., the tables - that contain all the old and/or new rows, which are referred to by the - OLD TABLE and NEW TABLE clauses in - the SQL standard. + While transition tables for AFTER triggers are specified + using the REFERENCING clause in the standard way, the row + variables used in FOR EACH ROW triggers may not be + specified in REFERENCING clause. They are available in a + manner which is dependent on the language in which the trigger function + is written. Some languages effectively behave as though there is a + REFERENCING clause containing OLD ROW AS OLD NEW + ROW AS NEW. diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2137372c23..f97bee5b0e 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -7430,7 +7430,7 @@ validateForeignKeyConstraint(char *conname, trig.tgconstraint = constraintOid; trig.tgdeferrable = FALSE; trig.tginitdeferred = FALSE; - /* we needn't fill in tgargs or tgqual */ + /* we needn't fill in remaining fields */ /* * See if we can do it with a single LEFT JOIN query. A FALSE result @@ -7514,6 +7514,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, } fk_trigger->columns = NIL; + fk_trigger->transitionRels = NIL; fk_trigger->whenClause = NULL; fk_trigger->isconstraint = true; fk_trigger->deferrable = fkconstraint->deferrable; @@ -7557,6 +7558,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint, fk_trigger->timing = TRIGGER_TYPE_AFTER; fk_trigger->events = TRIGGER_TYPE_DELETE; fk_trigger->columns = NIL; + fk_trigger->transitionRels = NIL; fk_trigger->whenClause = NULL; fk_trigger->isconstraint = true; fk_trigger->constrrel = NULL; @@ -7611,6 +7613,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint, fk_trigger->timing = TRIGGER_TYPE_AFTER; fk_trigger->events = TRIGGER_TYPE_UPDATE; fk_trigger->columns = NIL; + fk_trigger->transitionRels = NIL; fk_trigger->whenClause = NULL; fk_trigger->isconstraint = true; fk_trigger->constrrel = NULL; diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 9de22a13d7..1c264b7736 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -164,6 +164,8 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, Oid constrrelid = InvalidOid; ObjectAddress myself, referenced; + char *oldtablename = NULL; + char *newtablename = NULL; if (OidIsValid(relOid)) rel = heap_open(relOid, ShareRowExclusiveLock); @@ -309,6 +311,87 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, errmsg("INSTEAD OF triggers cannot have column lists"))); } + /* + * We don't yet support naming ROW transition variables, but the parser + * recognizes the syntax so we can give a nicer message here. + * + * Per standard, REFERENCING TABLE names are only allowed on AFTER + * triggers. Per standard, REFERENCING ROW names are not allowed with FOR + * EACH STATEMENT. Per standard, each OLD/NEW, ROW/TABLE permutation is + * only allowed once. Per standard, OLD may not be specified when + * creating a trigger only for INSERT, and NEW may not be specified when + * creating a trigger only for DELETE. + * + * Notice that the standard allows an AFTER ... FOR EACH ROW trigger to + * reference both ROW and TABLE transition data. + */ + if (stmt->transitionRels != NIL) + { + List *varList = stmt->transitionRels; + ListCell *lc; + + foreach(lc, varList) + { + TriggerTransition *tt = (TriggerTransition *) lfirst(lc); + + Assert(IsA(tt, TriggerTransition)); + + if (!(tt->isTable)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ROW variable naming in the REFERENCING clause is not supported"), + errhint("Use OLD TABLE or NEW TABLE for naming transition tables."))); + + /* + * Because of the above test, we omit further ROW-related testing + * below. If we later allow naming OLD and NEW ROW variables, + * adjustments will be needed below. + */ + + if (stmt->timing != TRIGGER_TYPE_AFTER) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("transition table name can only be specified for an AFTER trigger"))); + + if (tt->isNew) + { + if (!(TRIGGER_FOR_INSERT(tgtype) || + TRIGGER_FOR_UPDATE(tgtype))) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("NEW TABLE can only be specified for an INSERT or UPDATE trigger"))); + + if (newtablename != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("NEW TABLE cannot be specified multiple times"))); + + newtablename = tt->name; + } + else + { + if (!(TRIGGER_FOR_DELETE(tgtype) || + TRIGGER_FOR_UPDATE(tgtype))) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("OLD TABLE can only be specified for a DELETE or UPDATE trigger"))); + + if (oldtablename != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("OLD TABLE cannot be specified multiple times"))); + + oldtablename = tt->name; + } + } + + if (newtablename != NULL && oldtablename != NULL && + strcmp(newtablename, oldtablename) == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("OLD TABLE name and NEW TABLE name cannot be the same"))); + } + /* * Parse the WHEN clause, if any */ @@ -664,6 +747,17 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, else nulls[Anum_pg_trigger_tgqual - 1] = true; + if (oldtablename) + values[Anum_pg_trigger_tgoldtable - 1] = DirectFunctionCall1(namein, + CStringGetDatum(oldtablename)); + else + nulls[Anum_pg_trigger_tgoldtable - 1] = true; + if (newtablename) + values[Anum_pg_trigger_tgnewtable - 1] = DirectFunctionCall1(namein, + CStringGetDatum(newtablename)); + else + nulls[Anum_pg_trigger_tgnewtable - 1] = true; + tuple = heap_form_tuple(tgrel->rd_att, values, nulls); /* force tuple to have the desired OID */ @@ -682,6 +776,10 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, pfree(DatumGetPointer(values[Anum_pg_trigger_tgname - 1])); pfree(DatumGetPointer(values[Anum_pg_trigger_tgargs - 1])); pfree(DatumGetPointer(values[Anum_pg_trigger_tgattr - 1])); + if (oldtablename) + pfree(DatumGetPointer(values[Anum_pg_trigger_tgoldtable - 1])); + if (newtablename) + pfree(DatumGetPointer(values[Anum_pg_trigger_tgnewtable - 1])); /* * Update relation's pg_class entry. Crucial side-effect: other backends @@ -1584,6 +1682,23 @@ RelationBuildTriggers(Relation relation) } else build->tgargs = NULL; + + datum = fastgetattr(htup, Anum_pg_trigger_tgoldtable, + tgrel->rd_att, &isnull); + if (!isnull) + build->tgoldtable = + DatumGetCString(DirectFunctionCall1(nameout, datum)); + else + build->tgoldtable = NULL; + + datum = fastgetattr(htup, Anum_pg_trigger_tgnewtable, + tgrel->rd_att, &isnull); + if (!isnull) + build->tgnewtable = + DatumGetCString(DirectFunctionCall1(nameout, datum)); + else + build->tgnewtable = NULL; + datum = fastgetattr(htup, Anum_pg_trigger_tgqual, tgrel->rd_att, &isnull); if (!isnull) @@ -1680,6 +1795,19 @@ SetTriggerFlags(TriggerDesc *trigdesc, Trigger *trigger) trigdesc->trig_truncate_after_statement |= TRIGGER_TYPE_MATCHES(tgtype, TRIGGER_TYPE_STATEMENT, TRIGGER_TYPE_AFTER, TRIGGER_TYPE_TRUNCATE); + + trigdesc->trig_insert_new_table |= + (TRIGGER_FOR_INSERT(tgtype) && + TRIGGER_USES_TRANSITION_TABLE(trigger->tgnewtable)); + trigdesc->trig_update_old_table |= + (TRIGGER_FOR_UPDATE(tgtype) && + TRIGGER_USES_TRANSITION_TABLE(trigger->tgoldtable)); + trigdesc->trig_update_new_table |= + (TRIGGER_FOR_UPDATE(tgtype) && + TRIGGER_USES_TRANSITION_TABLE(trigger->tgnewtable)); + trigdesc->trig_delete_old_table |= + (TRIGGER_FOR_DELETE(tgtype) && + TRIGGER_USES_TRANSITION_TABLE(trigger->tgoldtable)); } /* @@ -1729,6 +1857,10 @@ CopyTriggerDesc(TriggerDesc *trigdesc) } if (trigger->tgqual) trigger->tgqual = pstrdup(trigger->tgqual); + if (trigger->tgoldtable) + trigger->tgoldtable = pstrdup(trigger->tgoldtable); + if (trigger->tgnewtable) + trigger->tgnewtable = pstrdup(trigger->tgnewtable); trigger++; } @@ -1761,6 +1893,10 @@ FreeTriggerDesc(TriggerDesc *trigdesc) } if (trigger->tgqual) pfree(trigger->tgqual); + if (trigger->tgoldtable) + pfree(trigger->tgoldtable); + if (trigger->tgnewtable) + pfree(trigger->tgnewtable); trigger++; } pfree(trigdesc->triggers); @@ -1839,6 +1975,18 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2) return false; else if (strcmp(trig1->tgqual, trig2->tgqual) != 0) return false; + if (trig1->tgoldtable == NULL && trig2->tgoldtable == NULL) + /* ok */ ; + else if (trig1->tgoldtable == NULL || trig2->tgoldtable == NULL) + return false; + else if (strcmp(trig1->tgoldtable, trig2->tgoldtable) != 0) + return false; + if (trig1->tgnewtable == NULL && trig2->tgnewtable == NULL) + /* ok */ ; + else if (trig1->tgnewtable == NULL || trig2->tgnewtable == NULL) + return false; + else if (strcmp(trig1->tgnewtable, trig2->tgnewtable) != 0) + return false; } } else if (trigdesc2 != NULL) @@ -1870,6 +2018,18 @@ ExecCallTriggerFunc(TriggerData *trigdata, Datum result; MemoryContext oldContext; + /* + * Protect against code paths that may fail to initialize transition table + * info. + */ + Assert(((TRIGGER_FIRED_BY_INSERT(trigdata->tg_event) || + TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event) || + TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) && + TRIGGER_FIRED_AFTER(trigdata->tg_event) && + !(trigdata->tg_event & AFTER_TRIGGER_DEFERRABLE) && + !(trigdata->tg_event & AFTER_TRIGGER_INITDEFERRED)) || + (trigdata->tg_oldtable == NULL && trigdata->tg_newtable == NULL)); + finfo += tgindx; /* @@ -1960,6 +2120,8 @@ ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo) LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_trigtuple = NULL; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_trigtuplebuf = InvalidBuffer; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) @@ -2017,6 +2179,8 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) { @@ -2070,7 +2234,8 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - if (trigdesc && trigdesc->trig_insert_after_row) + if (trigdesc && + (trigdesc->trig_insert_after_row || trigdesc->trig_insert_new_table)) AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT, true, NULL, trigtuple, recheckIndexes, NULL); } @@ -2092,6 +2257,8 @@ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) { @@ -2159,6 +2326,8 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo) LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_trigtuple = NULL; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_trigtuplebuf = InvalidBuffer; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) @@ -2230,6 +2399,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) { @@ -2273,7 +2444,8 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - if (trigdesc && trigdesc->trig_delete_after_row) + if (trigdesc && + (trigdesc->trig_delete_after_row || trigdesc->trig_delete_old_table)) { HeapTuple trigtuple; @@ -2310,6 +2482,8 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) { @@ -2363,6 +2537,8 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_trigtuple = NULL; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_trigtuplebuf = InvalidBuffer; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) @@ -2464,6 +2640,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, TRIGGER_EVENT_ROW | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; updatedCols = GetUpdatedColumns(relinfo, estate); for (i = 0; i < trigdesc->numtriggers; i++) { @@ -2528,7 +2706,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - if (trigdesc && trigdesc->trig_update_after_row) + if (trigdesc && (trigdesc->trig_update_after_row || + trigdesc->trig_update_old_table || trigdesc->trig_update_new_table)) { HeapTuple trigtuple; @@ -2567,6 +2746,8 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_ROW | TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -2635,6 +2816,8 @@ ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo) LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_trigtuple = NULL; LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_oldtable = NULL; + LocTriggerData.tg_newtable = NULL; LocTriggerData.tg_trigtuplebuf = InvalidBuffer; LocTriggerData.tg_newtuplebuf = InvalidBuffer; for (i = 0; i < trigdesc->numtriggers; i++) @@ -3163,8 +3346,11 @@ typedef struct AfterTriggerEventList * fdw_tuplestores[query_depth] is a tuplestore containing the foreign tuples * needed for the current query. * - * maxquerydepth is just the allocated length of query_stack and - * fdw_tuplestores. + * old_tuplestores[query_depth] and new_tuplestores[query_depth] hold the + * transition relations for the current query. + * + * maxquerydepth is just the allocated length of query_stack and the + * tuplestores. * * state_stack is a stack of pointers to saved copies of the SET CONSTRAINTS * state data; each subtransaction level that modifies that state first @@ -3193,7 +3379,9 @@ typedef struct AfterTriggersData AfterTriggerEventList events; /* deferred-event list */ int query_depth; /* current query list index */ AfterTriggerEventList *query_stack; /* events pending from each query */ - Tuplestorestate **fdw_tuplestores; /* foreign tuples from each query */ + Tuplestorestate **fdw_tuplestores; /* foreign tuples for one row from each query */ + Tuplestorestate **old_tuplestores; /* all old tuples from each query */ + Tuplestorestate **new_tuplestores; /* all new tuples from each query */ int maxquerydepth; /* allocated len of above array */ MemoryContext event_cxt; /* memory context for events, if any */ @@ -3222,14 +3410,16 @@ static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, /* - * Gets the current query fdw tuplestore and initializes it if necessary + * Gets a current query transition tuplestore and initializes it if necessary. + * This can be holding a single transition row tuple (in the case of an FDW) + * or a transition table (for an AFTER trigger). */ static Tuplestorestate * -GetCurrentFDWTuplestore(void) +GetTriggerTransitionTuplestore(Tuplestorestate **tss) { Tuplestorestate *ret; - ret = afterTriggers.fdw_tuplestores[afterTriggers.query_depth]; + ret = tss[afterTriggers.query_depth]; if (ret == NULL) { MemoryContext oldcxt; @@ -3256,7 +3446,7 @@ GetCurrentFDWTuplestore(void) CurrentResourceOwner = saveResourceOwner; MemoryContextSwitchTo(oldcxt); - afterTriggers.fdw_tuplestores[afterTriggers.query_depth] = ret; + tss[afterTriggers.query_depth] = ret; } return ret; @@ -3554,7 +3744,9 @@ AfterTriggerExecute(AfterTriggerEvent event, { case AFTER_TRIGGER_FDW_FETCH: { - Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore(); + Tuplestorestate *fdw_tuplestore = + GetTriggerTransitionTuplestore + (afterTriggers.fdw_tuplestores); if (!tuplestore_gettupleslot(fdw_tuplestore, true, false, trig_tuple_slot1)) @@ -3623,6 +3815,20 @@ AfterTriggerExecute(AfterTriggerEvent event, } } + /* + * Set up the tuplestore information. + */ + if (LocTriggerData.tg_trigger->tgoldtable) + LocTriggerData.tg_oldtable = + GetTriggerTransitionTuplestore(afterTriggers.old_tuplestores); + else + LocTriggerData.tg_oldtable = NULL; + if (LocTriggerData.tg_trigger->tgnewtable) + LocTriggerData.tg_newtable = + GetTriggerTransitionTuplestore(afterTriggers.new_tuplestores); + else + LocTriggerData.tg_newtable = NULL; + /* * Setup the remaining trigger information */ @@ -3912,6 +4118,8 @@ AfterTriggerBeginXact(void) Assert(afterTriggers.state == NULL); Assert(afterTriggers.query_stack == NULL); Assert(afterTriggers.fdw_tuplestores == NULL); + Assert(afterTriggers.old_tuplestores == NULL); + Assert(afterTriggers.new_tuplestores == NULL); Assert(afterTriggers.maxquerydepth == 0); Assert(afterTriggers.event_cxt == NULL); Assert(afterTriggers.events.head == NULL); @@ -3956,6 +4164,8 @@ AfterTriggerEndQuery(EState *estate) { AfterTriggerEventList *events; Tuplestorestate *fdw_tuplestore; + Tuplestorestate *old_tuplestore; + Tuplestorestate *new_tuplestore; /* Must be inside a query, too */ Assert(afterTriggers.query_depth >= 0); @@ -4014,6 +4224,18 @@ AfterTriggerEndQuery(EState *estate) tuplestore_end(fdw_tuplestore); afterTriggers.fdw_tuplestores[afterTriggers.query_depth] = NULL; } + old_tuplestore = afterTriggers.old_tuplestores[afterTriggers.query_depth]; + if (old_tuplestore) + { + tuplestore_end(old_tuplestore); + afterTriggers.old_tuplestores[afterTriggers.query_depth] = NULL; + } + new_tuplestore = afterTriggers.new_tuplestores[afterTriggers.query_depth]; + if (new_tuplestore) + { + tuplestore_end(new_tuplestore); + afterTriggers.new_tuplestores[afterTriggers.query_depth] = NULL; + } afterTriggerFreeEventList(&afterTriggers.query_stack[afterTriggers.query_depth]); afterTriggers.query_depth--; @@ -4127,6 +4349,8 @@ AfterTriggerEndXact(bool isCommit) */ afterTriggers.query_stack = NULL; afterTriggers.fdw_tuplestores = NULL; + afterTriggers.old_tuplestores = NULL; + afterTriggers.new_tuplestores = NULL; afterTriggers.maxquerydepth = 0; afterTriggers.state = NULL; @@ -4259,6 +4483,18 @@ AfterTriggerEndSubXact(bool isCommit) tuplestore_end(ts); afterTriggers.fdw_tuplestores[afterTriggers.query_depth] = NULL; } + ts = afterTriggers.old_tuplestores[afterTriggers.query_depth]; + if (ts) + { + tuplestore_end(ts); + afterTriggers.old_tuplestores[afterTriggers.query_depth] = NULL; + } + ts = afterTriggers.new_tuplestores[afterTriggers.query_depth]; + if (ts) + { + tuplestore_end(ts); + afterTriggers.new_tuplestores[afterTriggers.query_depth] = NULL; + } afterTriggerFreeEventList(&afterTriggers.query_stack[afterTriggers.query_depth]); } @@ -4338,6 +4574,12 @@ AfterTriggerEnlargeQueryState(void) afterTriggers.fdw_tuplestores = (Tuplestorestate **) MemoryContextAllocZero(TopTransactionContext, new_alloc * sizeof(Tuplestorestate *)); + afterTriggers.old_tuplestores = (Tuplestorestate **) + MemoryContextAllocZero(TopTransactionContext, + new_alloc * sizeof(Tuplestorestate *)); + afterTriggers.new_tuplestores = (Tuplestorestate **) + MemoryContextAllocZero(TopTransactionContext, + new_alloc * sizeof(Tuplestorestate *)); afterTriggers.maxquerydepth = new_alloc; } else @@ -4353,9 +4595,19 @@ AfterTriggerEnlargeQueryState(void) afterTriggers.fdw_tuplestores = (Tuplestorestate **) repalloc(afterTriggers.fdw_tuplestores, new_alloc * sizeof(Tuplestorestate *)); + afterTriggers.old_tuplestores = (Tuplestorestate **) + repalloc(afterTriggers.old_tuplestores, + new_alloc * sizeof(Tuplestorestate *)); + afterTriggers.new_tuplestores = (Tuplestorestate **) + repalloc(afterTriggers.new_tuplestores, + new_alloc * sizeof(Tuplestorestate *)); /* Clear newly-allocated slots for subsequent lazy initialization. */ memset(afterTriggers.fdw_tuplestores + old_alloc, 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *)); + memset(afterTriggers.old_tuplestores + old_alloc, + 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *)); + memset(afterTriggers.new_tuplestores + old_alloc, + 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *)); afterTriggers.maxquerydepth = new_alloc; } @@ -4800,7 +5052,14 @@ AfterTriggerPendingOnRel(Oid relid) * * NOTE: this is called whenever there are any triggers associated with * the event (even if they are disabled). This function decides which - * triggers actually need to be queued. + * triggers actually need to be queued. It is also called after each row, + * even if there are no triggers for that event, if there are any AFTER + * STATEMENT triggers for the statement which use transition tables, so that + * the transition tuplestores can be built. + * + * Transition tuplestores are built now, rather than when events are pulled + * off of the queue because AFTER ROW triggers are allowed to select from the + * transition tables for the statement. * ---------- */ static void @@ -4831,6 +5090,46 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, if (afterTriggers.query_depth >= afterTriggers.maxquerydepth) AfterTriggerEnlargeQueryState(); + /* + * If the relation has AFTER ... FOR EACH ROW triggers, capture rows into + * transition tuplestores for this depth. + */ + if (row_trigger) + { + if ((event == TRIGGER_EVENT_DELETE && + trigdesc->trig_delete_old_table) || + (event == TRIGGER_EVENT_UPDATE && + trigdesc->trig_update_old_table)) + { + Tuplestorestate *old_tuplestore; + + Assert(oldtup != NULL); + old_tuplestore = + GetTriggerTransitionTuplestore + (afterTriggers.old_tuplestores); + tuplestore_puttuple(old_tuplestore, oldtup); + } + if ((event == TRIGGER_EVENT_INSERT && + trigdesc->trig_insert_new_table) || + (event == TRIGGER_EVENT_UPDATE && + trigdesc->trig_update_new_table)) + { + Tuplestorestate *new_tuplestore; + + Assert(newtup != NULL); + new_tuplestore = + GetTriggerTransitionTuplestore + (afterTriggers.new_tuplestores); + tuplestore_puttuple(new_tuplestore, newtup); + } + + /* If transition tables are the only reason we're here, return. */ + if ((event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) || + (event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) || + (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_after_row)) + return; + } + /* * Validate the event code and collect the associated tuple CTIDs. * @@ -4928,7 +5227,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, { if (fdw_tuplestore == NULL) { - fdw_tuplestore = GetCurrentFDWTuplestore(); + fdw_tuplestore = + GetTriggerTransitionTuplestore + (afterTriggers.fdw_tuplestores); new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH; } else diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 71714bc1d6..04e49b7795 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2718,6 +2718,18 @@ _copyRoleSpec(const RoleSpec *from) return newnode; } +static TriggerTransition * +_copyTriggerTransition(const TriggerTransition *from) +{ + TriggerTransition *newnode = makeNode(TriggerTransition); + + COPY_STRING_FIELD(name); + COPY_SCALAR_FIELD(isNew); + COPY_SCALAR_FIELD(isTable); + + return newnode; +} + static Query * _copyQuery(const Query *from) { @@ -3893,6 +3905,7 @@ _copyCreateTrigStmt(const CreateTrigStmt *from) COPY_NODE_FIELD(columns); COPY_NODE_FIELD(whenClause); COPY_SCALAR_FIELD(isconstraint); + COPY_NODE_FIELD(transitionRels); COPY_SCALAR_FIELD(deferrable); COPY_SCALAR_FIELD(initdeferred); COPY_NODE_FIELD(constrrel); @@ -5088,6 +5101,9 @@ copyObject(const void *from) case T_RoleSpec: retval = _copyRoleSpec(from); break; + case T_TriggerTransition: + retval = _copyTriggerTransition(from); + break; /* * MISCELLANEOUS NODES diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 29a090fc48..2eaf41c37f 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1905,6 +1905,7 @@ _equalCreateTrigStmt(const CreateTrigStmt *a, const CreateTrigStmt *b) COMPARE_NODE_FIELD(columns); COMPARE_NODE_FIELD(whenClause); COMPARE_SCALAR_FIELD(isconstraint); + COMPARE_NODE_FIELD(transitionRels); COMPARE_SCALAR_FIELD(deferrable); COMPARE_SCALAR_FIELD(initdeferred); COMPARE_NODE_FIELD(constrrel); @@ -2634,6 +2635,16 @@ _equalRoleSpec(const RoleSpec *a, const RoleSpec *b) return true; } +static bool +_equalTriggerTransition(const TriggerTransition *a, const TriggerTransition *b) +{ + COMPARE_STRING_FIELD(name); + COMPARE_SCALAR_FIELD(isNew); + COMPARE_SCALAR_FIELD(isTable); + + return true; +} + /* * Stuff from pg_list.h */ @@ -3387,6 +3398,9 @@ equal(const void *a, const void *b) case T_RoleSpec: retval = _equalRoleSpec(a, b); break; + case T_TriggerTransition: + retval = _equalTriggerTransition(a, b); + break; default: elog(ERROR, "unrecognized node type: %d", diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index ae869547f3..748b687929 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2561,6 +2561,16 @@ _outXmlSerialize(StringInfo str, const XmlSerialize *node) WRITE_LOCATION_FIELD(location); } +static void +_outTriggerTransition(StringInfo str, const TriggerTransition *node) +{ + WRITE_NODE_TYPE("TRIGGERTRANSITION"); + + WRITE_STRING_FIELD(name); + WRITE_BOOL_FIELD(isNew); + WRITE_BOOL_FIELD(isTable); +} + static void _outColumnDef(StringInfo str, const ColumnDef *node) { @@ -3852,6 +3862,9 @@ outNode(StringInfo str, const void *obj) case T_ForeignKeyCacheInfo: _outForeignKeyCacheInfo(str, obj); break; + case T_TriggerTransition: + _outTriggerTransition(str, obj); + break; default: diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5547fc8658..0ec1cd345b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -310,6 +310,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type TriggerEvents TriggerOneEvent %type TriggerFuncArg %type TriggerWhen +%type TransitionRelName +%type TransitionRowOrTable TransitionOldOrNew +%type TriggerTransition %type event_trigger_when_list event_trigger_value_list %type event_trigger_when_item @@ -374,6 +377,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); create_generic_options alter_generic_options relation_expr_list dostmt_opt_list transform_element_list transform_type_list + TriggerTransitions TriggerReferencing %type group_by_list %type group_by_item empty_grouping_set rollup_clause cube_clause @@ -610,11 +614,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEXT NO NONE + NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NO NONE NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OUT_P OUTER_P OVER OVERLAPS OVERLAY OWNED OWNER PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY @@ -623,8 +627,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); QUOTE - RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFRESH REINDEX - RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA + RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFERENCING + REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP ROW ROWS RULE @@ -4748,19 +4752,20 @@ CreateAmStmt: CREATE ACCESS METHOD name TYPE_P INDEX HANDLER handler_name CreateTrigStmt: CREATE TRIGGER name TriggerActionTime TriggerEvents ON - qualified_name TriggerForSpec TriggerWhen + qualified_name TriggerReferencing TriggerForSpec TriggerWhen EXECUTE PROCEDURE func_name '(' TriggerFuncArgs ')' { CreateTrigStmt *n = makeNode(CreateTrigStmt); n->trigname = $3; n->relation = $7; - n->funcname = $12; - n->args = $14; - n->row = $8; + n->funcname = $13; + n->args = $15; + n->row = $9; n->timing = $4; n->events = intVal(linitial($5)); n->columns = (List *) lsecond($5); - n->whenClause = $9; + n->whenClause = $10; + n->transitionRels = $8; n->isconstraint = FALSE; n->deferrable = FALSE; n->initdeferred = FALSE; @@ -4782,6 +4787,7 @@ CreateTrigStmt: n->events = intVal(linitial($6)); n->columns = (List *) lsecond($6); n->whenClause = $14; + n->transitionRels = NIL; n->isconstraint = TRUE; processCASbits($10, @10, "TRIGGER", &n->deferrable, &n->initdeferred, NULL, @@ -4834,6 +4840,49 @@ TriggerOneEvent: { $$ = list_make2(makeInteger(TRIGGER_TYPE_TRUNCATE), NIL); } ; +TriggerReferencing: + REFERENCING TriggerTransitions { $$ = $2; } + | /*EMPTY*/ { $$ = NIL; } + ; + +TriggerTransitions: + TriggerTransition { $$ = list_make1($1); } + | TriggerTransitions TriggerTransition { $$ = lappend($1, $2); } + ; + +TriggerTransition: + TransitionOldOrNew TransitionRowOrTable opt_as TransitionRelName + { + TriggerTransition *n = makeNode(TriggerTransition); + n->name = $4; + n->isNew = $1; + n->isTable = $2; + $$ = (Node *)n; + } + ; + +TransitionOldOrNew: + NEW { $$ = TRUE; } + | OLD { $$ = FALSE; } + ; + +TransitionRowOrTable: + TABLE { $$ = TRUE; } + /* + * According to the standard, lack of a keyword here implies ROW. + * Support for that would require prohibiting ROW entirely here, + * reserving the keyword ROW, and/or requiring AS (instead of + * allowing it to be optional, as the standard specifies) as the + * next token. Requiring ROW seems cleanest and easiest to + * explain. + */ + | ROW { $$ = FALSE; } + ; + +TransitionRelName: + ColId { $$ = $1; } + ; + TriggerForSpec: FOR TriggerForOptEach TriggerForType { @@ -13810,6 +13859,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NEW | NEXT | NO | NOTHING @@ -13820,6 +13870,7 @@ unreserved_keyword: | OF | OFF | OIDS + | OLD | OPERATOR | OPTION | OPTIONS @@ -13851,6 +13902,7 @@ unreserved_keyword: | RECHECK | RECURSIVE | REF + | REFERENCING | REFRESH | REINDEX | RELATIVE_P diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 8a81d7a078..a3a4174abf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -813,6 +813,8 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) SysScanDesc tgscan; int findx = 0; char *tgname; + char *tgoldtable; + char *tgnewtable; Oid argtypes[1]; /* dummy */ Datum value; bool isnull; @@ -924,6 +926,27 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) appendStringInfoString(&buf, "IMMEDIATE "); } + value = fastgetattr(ht_trig, Anum_pg_trigger_tgoldtable, + tgrel->rd_att, &isnull); + if (!isnull) + tgoldtable = NameStr(*((NameData *) DatumGetPointer(value))); + else + tgoldtable = NULL; + value = fastgetattr(ht_trig, Anum_pg_trigger_tgnewtable, + tgrel->rd_att, &isnull); + if (!isnull) + tgnewtable = NameStr(*((NameData *) DatumGetPointer(value))); + else + tgnewtable = NULL; + if (tgoldtable != NULL || tgnewtable != NULL) + { + appendStringInfoString(&buf, "REFERENCING "); + if (tgoldtable != NULL) + appendStringInfo(&buf, "OLD TABLE AS %s ", tgoldtable); + if (tgnewtable != NULL) + appendStringInfo(&buf, "NEW TABLE AS %s ", tgnewtable); + } + if (TRIGGER_FOR_ROW(trigrec->tgtype)) appendStringInfoString(&buf, "FOR EACH ROW "); else diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index cd3048db86..880559650a 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201610201 +#define CATALOG_VERSION_NO 201611041 #endif diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h index eb39c50e63..da6a7f3a2e 100644 --- a/src/include/catalog/pg_trigger.h +++ b/src/include/catalog/pg_trigger.h @@ -59,6 +59,8 @@ CATALOG(pg_trigger,2620) #ifdef CATALOG_VARLEN bytea tgargs BKI_FORCE_NOT_NULL; /* first\000second\000tgnargs\000 */ pg_node_tree tgqual; /* WHEN expression, or NULL if none */ + NameData tgoldtable; /* old transition table, or NULL if none */ + NameData tgnewtable; /* new transition table, or NULL if none */ #endif } FormData_pg_trigger; @@ -73,7 +75,7 @@ typedef FormData_pg_trigger *Form_pg_trigger; * compiler constants for pg_trigger * ---------------- */ -#define Natts_pg_trigger 15 +#define Natts_pg_trigger 17 #define Anum_pg_trigger_tgrelid 1 #define Anum_pg_trigger_tgname 2 #define Anum_pg_trigger_tgfoid 3 @@ -89,6 +91,8 @@ typedef FormData_pg_trigger *Form_pg_trigger; #define Anum_pg_trigger_tgattr 13 #define Anum_pg_trigger_tgargs 14 #define Anum_pg_trigger_tgqual 15 +#define Anum_pg_trigger_tgoldtable 16 +#define Anum_pg_trigger_tgnewtable 17 /* Bits within tgtype */ #define TRIGGER_TYPE_ROW (1 << 0) @@ -142,4 +146,11 @@ typedef FormData_pg_trigger *Form_pg_trigger; #define TRIGGER_TYPE_MATCHES(type, level, timing, event) \ (((type) & (TRIGGER_TYPE_LEVEL_MASK | TRIGGER_TYPE_TIMING_MASK | (event))) == ((level) | (timing) | (event))) +/* + * Macro to determine whether tgnewtable or tgoldtable has been specified for + * a trigger. + */ +#define TRIGGER_USES_TRANSITION_TABLE(namepointer) \ + ((namepointer) != (char *) NULL) + #endif /* PG_TRIGGER_H */ diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 0ed7c86eb2..c6e3e2c234 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -37,6 +37,8 @@ typedef struct TriggerData Trigger *tg_trigger; Buffer tg_trigtuplebuf; Buffer tg_newtuplebuf; + Tuplestorestate *tg_oldtable; + Tuplestorestate *tg_newtable; } TriggerData; /* diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 88297bbe80..cb9307cd00 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -453,6 +453,7 @@ typedef enum NodeTag T_OnConflictClause, T_CommonTableExpr, T_RoleSpec, + T_TriggerTransition, /* * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 6de2cab6b2..9b600a5f76 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1204,6 +1204,21 @@ typedef struct CommonTableExpr ((Query *) (cte)->ctequery)->targetList : \ ((Query *) (cte)->ctequery)->returningList) +/* + * TriggerTransition - + * representation of transition row or table naming clause + * + * Only transition tables are initially supported in the syntax, and only for + * AFTER triggers, but other permutations are accepted by the parser so we can + * give a meaningful message from C code. + */ +typedef struct TriggerTransition +{ + NodeTag type; + char *name; + bool isNew; + bool isTable; +} TriggerTransition; /***************************************************************************** * Optimizable Statements @@ -2105,6 +2120,8 @@ typedef struct CreateTrigStmt List *columns; /* column names, or NIL for all columns */ Node *whenClause; /* qual expression, or NULL if none */ bool isconstraint; /* This is a constraint trigger */ + /* explicitly named transition data */ + List *transitionRels; /* TriggerTransition nodes, or NIL if none */ /* The remaining fields are only used for constraint triggers */ bool deferrable; /* [NOT] DEFERRABLE */ bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 17ffef53a7..77d873beca 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -251,6 +251,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD) +PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD) PG_KEYWORD("no", NO, UNRESERVED_KEYWORD) PG_KEYWORD("none", NONE, COL_NAME_KEYWORD) @@ -268,6 +269,7 @@ PG_KEYWORD("of", OF, UNRESERVED_KEYWORD) PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD) +PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD) PG_KEYWORD("on", ON, RESERVED_KEYWORD) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD) @@ -313,6 +315,7 @@ PG_KEYWORD("recheck", RECHECK, UNRESERVED_KEYWORD) PG_KEYWORD("recursive", RECURSIVE, UNRESERVED_KEYWORD) PG_KEYWORD("ref", REF, UNRESERVED_KEYWORD) PG_KEYWORD("references", REFERENCES, RESERVED_KEYWORD) +PG_KEYWORD("referencing", REFERENCING, UNRESERVED_KEYWORD) PG_KEYWORD("refresh", REFRESH, UNRESERVED_KEYWORD) PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD) PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD) diff --git a/src/include/utils/reltrigger.h b/src/include/utils/reltrigger.h index e87f2283ec..756b417128 100644 --- a/src/include/utils/reltrigger.h +++ b/src/include/utils/reltrigger.h @@ -39,6 +39,8 @@ typedef struct Trigger int16 *tgattr; char **tgargs; char *tgqual; + char *tgoldtable; + char *tgnewtable; } Trigger; typedef struct TriggerDesc @@ -68,6 +70,11 @@ typedef struct TriggerDesc /* there are no row-level truncate triggers */ bool trig_truncate_before_statement; bool trig_truncate_after_statement; + /* Is there at least one trigger specifying each transition relation? */ + bool trig_insert_new_table; + bool trig_update_old_table; + bool trig_update_new_table; + bool trig_delete_old_table; } TriggerDesc; #endif /* RELTRIGGER_H */