[HACKERS] Sync BEFORE STATEMENT trigger behavior with AFTER STATEMENT - Mailing list pgsql-hackers

From Tom Lane
Subject [HACKERS] Sync BEFORE STATEMENT trigger behavior with AFTER STATEMENT
Date
Msg-id 22315.1505584992@sss.pgh.pa.us
Whole thread Raw
List pgsql-hackers
The just-committed patch 0f79440fb arranges to suppress extra firings
of AFTER STATEMENT triggers that historically have occurred when
several FK enforcement trigger firings affect the same target table.
This leaves us in a situation where you may get more BEFORE STATEMENT
trigger firings than AFTER STATEMENT firings, which seems weird.

I did the attached simple patch to change this, using state very
similar to that now used for AFTER STATEMENT triggers.  It works, in
terms of syncing the number of trigger calls.  The timing of the
calls might seem a bit weird, as shown in the modified regression
test case.  In the corner cases where you can get multiple statement
trigger firings out of one original SQL statement, the ordering
tends to look like
    BEFORE STATEMENT
    BEFORE STATEMENT
    AFTER STATEMENT
    AFTER STATEMENT
not what you might expect,
    BEFORE STATEMENT
    AFTER STATEMENT
    BEFORE STATEMENT
    AFTER STATEMENT
I'm not sure there's anything we can or should do about that, though.
Certainly we don't want to delay the execution of BEFORE calls.

I think we should push this into v10 so that we maintain consistency
of BEFORE vs. AFTER STATEMENT behavior.  Objections?

            regards, tom lane

diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7e391a1..78e6ce7 100644
*** a/src/backend/commands/trigger.c
--- b/src/backend/commands/trigger.c
*************** static void AfterTriggerSaveEvent(EState
*** 100,105 ****
--- 100,106 ----
                        List *recheckIndexes, Bitmapset *modifiedCols,
                        TransitionCaptureState *transition_capture);
  static void AfterTriggerEnlargeQueryState(void);
+ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);


  /*
*************** ExecBSInsertTriggers(EState *estate, Res
*** 2229,2234 ****
--- 2230,2240 ----
      if (!trigdesc->trig_insert_before_statement)
          return;

+     /* no-op if we already fired BS triggers in this context */
+     if (before_stmt_triggers_fired(RelationGetRelid(relinfo->ri_RelationDesc),
+                                    CMD_INSERT))
+         return;
+
      LocTriggerData.type = T_TriggerData;
      LocTriggerData.tg_event = TRIGGER_EVENT_INSERT |
          TRIGGER_EVENT_BEFORE;
*************** ExecBSDeleteTriggers(EState *estate, Res
*** 2439,2444 ****
--- 2445,2455 ----
      if (!trigdesc->trig_delete_before_statement)
          return;

+     /* no-op if we already fired BS triggers in this context */
+     if (before_stmt_triggers_fired(RelationGetRelid(relinfo->ri_RelationDesc),
+                                    CMD_DELETE))
+         return;
+
      LocTriggerData.type = T_TriggerData;
      LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
          TRIGGER_EVENT_BEFORE;
*************** ExecBSUpdateTriggers(EState *estate, Res
*** 2651,2656 ****
--- 2662,2672 ----
      if (!trigdesc->trig_update_before_statement)
          return;

+     /* no-op if we already fired BS triggers in this context */
+     if (before_stmt_triggers_fired(RelationGetRelid(relinfo->ri_RelationDesc),
+                                    CMD_UPDATE))
+         return;
+
      updatedCols = GetUpdatedColumns(relinfo, estate);

      LocTriggerData.type = T_TriggerData;
*************** struct AfterTriggersTableData
*** 3576,3583 ****
      Oid            relid;            /* target table's OID */
      CmdType        cmdType;        /* event type, CMD_INSERT/UPDATE/DELETE */
      bool        closed;            /* true when no longer OK to add tuples */
!     bool        stmt_trig_done; /* did we already queue stmt-level triggers? */
!     AfterTriggerEventList stmt_trig_events; /* if so, saved list pointer */
      Tuplestorestate *old_tuplestore;    /* "old" transition table, if any */
      Tuplestorestate *new_tuplestore;    /* "new" transition table, if any */
  };
--- 3592,3600 ----
      Oid            relid;            /* target table's OID */
      CmdType        cmdType;        /* event type, CMD_INSERT/UPDATE/DELETE */
      bool        closed;            /* true when no longer OK to add tuples */
!     bool        before_trig_done;    /* did we already queue BS triggers? */
!     bool        after_trig_done;    /* did we already queue AS triggers? */
!     AfterTriggerEventList after_trig_events;    /* if so, saved list pointer */
      Tuplestorestate *old_tuplestore;    /* "old" transition table, if any */
      Tuplestorestate *new_tuplestore;    /* "new" transition table, if any */
  };
*************** AfterTriggerSaveEvent(EState *estate, Re
*** 5651,5656 ****
--- 5668,5704 ----
  }

  /*
+  * Detect whether we already queued BEFORE STATEMENT triggers for the given
+  * relation + operation, and set the flag so the next call will report "true".
+  */
+ static bool
+ before_stmt_triggers_fired(Oid relid, CmdType cmdType)
+ {
+     bool        result;
+     AfterTriggersTableData *table;
+
+     /* Check state, like AfterTriggerSaveEvent. */
+     if (afterTriggers.query_depth < 0)
+         elog(ERROR, "before_stmt_triggers_fired() called outside of query");
+
+     /* Be sure we have enough space to record events at this query depth. */
+     if (afterTriggers.query_depth >= afterTriggers.maxquerydepth)
+         AfterTriggerEnlargeQueryState();
+
+     /*
+      * We keep this state in the AfterTriggersTableData that also holds
+      * transition tables for the relation + operation.  In this way, if we are
+      * forced to make a new set of transition tables because more tuples get
+      * entered after we've already fired triggers, we will allow a new set of
+      * statement triggers to get queued.
+      */
+     table = GetAfterTriggersTableData(relid, cmdType);
+     result = table->before_trig_done;
+     table->before_trig_done = true;
+     return result;
+ }
+
+ /*
   * If we previously queued a set of AFTER STATEMENT triggers for the given
   * relation + operation, and they've not been fired yet, cancel them.  The
   * caller will queue a fresh set that's after any row-level triggers that may
*************** cancel_prior_stmt_triggers(Oid relid, Cm
*** 5684,5690 ****
       */
      table = GetAfterTriggersTableData(relid, cmdType);

!     if (table->stmt_trig_done)
      {
          /*
           * We want to start scanning from the tail location that existed just
--- 5732,5738 ----
       */
      table = GetAfterTriggersTableData(relid, cmdType);

!     if (table->after_trig_done)
      {
          /*
           * We want to start scanning from the tail location that existed just
*************** cancel_prior_stmt_triggers(Oid relid, Cm
*** 5695,5704 ****
          AfterTriggerEvent event;
          AfterTriggerEventChunk *chunk;

!         if (table->stmt_trig_events.tail)
          {
!             chunk = table->stmt_trig_events.tail;
!             event = (AfterTriggerEvent) table->stmt_trig_events.tailfree;
          }
          else
          {
--- 5743,5752 ----
          AfterTriggerEvent event;
          AfterTriggerEventChunk *chunk;

!         if (table->after_trig_events.tail)
          {
!             chunk = table->after_trig_events.tail;
!             event = (AfterTriggerEvent) table->after_trig_events.tailfree;
          }
          else
          {
*************** cancel_prior_stmt_triggers(Oid relid, Cm
*** 5737,5744 ****
  done:

      /* In any case, save current insertion point for next time */
!     table->stmt_trig_done = true;
!     table->stmt_trig_events = qs->events;
  }

  /*
--- 5785,5792 ----
  done:

      /* In any case, save current insertion point for next time */
!     table->after_trig_done = true;
!     table->after_trig_events = qs->events;
  }

  /*
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 3ab6be3..0a04325 100644
*** a/src/test/regress/expected/triggers.out
--- b/src/test/regress/expected/triggers.out
*************** drop table refd_table, trig_table;
*** 2338,2343 ****
--- 2338,2346 ----
  --
  create table self_ref (a int primary key,
                         b int references self_ref(a) on delete cascade);
+ create trigger self_ref_before_del_trig
+   before delete on self_ref
+   for each statement execute procedure trigger_func('self_ref');
  create trigger self_ref_r_trig
    after delete on self_ref referencing old table as old_table
    for each row execute procedure dump_delete();
*************** create trigger self_ref_s_trig
*** 2346,2352 ****
--- 2349,2357 ----
    for each statement execute procedure dump_delete();
  insert into self_ref values (1, null), (2, 1), (3, 2);
  delete from self_ref where a = 1;
+ NOTICE:  trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
  NOTICE:  trigger = self_ref_r_trig, old table = (1,), (2,1)
+ NOTICE:  trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
  NOTICE:  trigger = self_ref_r_trig, old table = (1,), (2,1)
  NOTICE:  trigger = self_ref_s_trig, old table = (1,), (2,1)
  NOTICE:  trigger = self_ref_r_trig, old table = (3,2)
*************** NOTICE:  trigger = self_ref_s_trig, old
*** 2355,2360 ****
--- 2360,2366 ----
  drop trigger self_ref_r_trig on self_ref;
  insert into self_ref values (1, null), (2, 1), (3, 2), (4, 3);
  delete from self_ref where a = 1;
+ NOTICE:  trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
  NOTICE:  trigger = self_ref_s_trig, old table = (1,), (2,1), (3,2), (4,3)
  drop table self_ref;
  -- cleanup
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 30bb7d1..2aa3120 100644
*** a/src/test/regress/sql/triggers.sql
--- b/src/test/regress/sql/triggers.sql
*************** drop table refd_table, trig_table;
*** 1834,1839 ****
--- 1834,1842 ----
  create table self_ref (a int primary key,
                         b int references self_ref(a) on delete cascade);

+ create trigger self_ref_before_del_trig
+   before delete on self_ref
+   for each statement execute procedure trigger_func('self_ref');
  create trigger self_ref_r_trig
    after delete on self_ref referencing old table as old_table
    for each row execute procedure dump_delete();

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

pgsql-hackers by date:

Previous
From: Peter Geoghegan
Date:
Subject: Re: [HACKERS] A design for amcheck heapam verification
Next
From: Alexander Korotkov
Date:
Subject: Re: [HACKERS] [PATCH]make pg_rewind to not copy useless WAL files