Thread: BUG #6123: DELETE fails if before trigger causes another trigger to UPDATE

BUG #6123: DELETE fails if before trigger causes another trigger to UPDATE

From
"Kevin Grittner"
Date:
The following bug has been logged online:

Bug reference:      6123
Logged by:          Kevin Grittner
Email address:      Kevin.Grittner@wicourts.gov
PostgreSQL version: HEAD and 9.0.4
Operating system:   Linux (probably doesn't matter)
Description:        DELETE fails if before trigger causes another trigger to
UPDATE
Details:

create table a (aid int not null primary key,
                bcnt int not null default 0);
create table b (bid int not null primary key,
                aid int not null);

create function a_del_func()
  returns trigger language plpgsql as
  $$begin delete from b where aid = old.aid;
          return old; end;$$;
create trigger a_del_trig before delete On a
  for each row execute procedure a_del_func();

create function b_ins_func()
  returns trigger language plpgsql as
  $$begin update a set bcnt = bcnt + 1
            where aid = new.aid; return new; end;$$;
create trigger b_ins_trig after insert on b
  for each row execute procedure b_ins_func();

create function b_del_func()
  returns trigger language plpgsql as
  $$begin update a set bcnt = bcnt - 1
            where aid = old.aid; return old; end;$$;
create trigger b_del_trig after delete on b
  for each row execute procedure b_del_func();

insert into a values (1, 0);
insert into b values (10, 1);
select * from a; select * from b;
delete from a where aid = 1;
select * from a; select * from b;

I expect the DELETE from a to delete related b record(s) and then succeed.
Instead the UPDATE in b_del_trig causes delete of a to silently fail.

-Kevin
"Kevin Grittner" <Kevin.Grittner@wicourts.gov> writes:
> [ circular trigger relationships ]

> I expect the DELETE from a to delete related b record(s) and then succeed.
> Instead the UPDATE in b_del_trig causes delete of a to silently fail.

The update causes the already-selected target row version of the DELETE
to be obsoleted, so heap_delete finds it has nothing to do.  I'm
disinclined to mess with that logic.

            regards, tom lane

Re: BUG #6123: DELETE fails if before trigger causes another trigger to UPDATE

From
"Kevin Grittner"
Date:
Tom Lane <tgl@sss.pgh.pa.us> wrote:
> "Kevin Grittner" <Kevin.Grittner@wicourts.gov> writes:
>> [ circular trigger relationships ]
>
>> I expect the DELETE from a to delete related b record(s) and then
>> succeed.  Instead the UPDATE in b_del_trig causes delete of a to
>> silently fail.
>
> The update causes the already-selected target row version of the
> DELETE to be obsoleted, so heap_delete finds it has nothing to do.
> I'm disinclined to mess with that logic.

It's pretty astonishing behavior for application programmers.  It's
not unusual for triggers on detail from one table to maintain a
status, count, or sum in a higher level table.  When a DELETE from
the higher level table causes deletes at the lower level, the lower
level trigger really doesn't have any way to know that.  I think
this should be considered a bug.

At the point where heap_delete finds that the tuple has expired, I
think it should follow the ctid chain as long as xmax is from the
same top-level transaction, and then decide if it has something to
do.  Normally, that would be zero iterations, so I doubt the
performance impact is huge.

I'm willing to mess with that logic if there's a chance that it will
be accepted.  I may *need* to even if it won't, to allow our
migration to native PostgreSQL triggers to succeed -- we've been
trying to work around it for the last few days, but the places where
it's popping up are becoming increasingly hard to accommodate that
way.

-Kevin
"Kevin Grittner" <Kevin.Grittner@wicourts.gov> writes:
> Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> The update causes the already-selected target row version of the
>> DELETE to be obsoleted, so heap_delete finds it has nothing to do.
>> I'm disinclined to mess with that logic.

> It's pretty astonishing behavior for application programmers.  It's
> not unusual for triggers on detail from one table to maintain a
> status, count, or sum in a higher level table.  When a DELETE from
> the higher level table causes deletes at the lower level, the lower
> level trigger really doesn't have any way to know that.  I think
> this should be considered a bug.

I think you have no idea how large a can of worms you're opening.

To point out just one problem, if heap_delete restarts itself and tries
to delete some other row version than it started with, should the ON
DELETE triggers be fired again?  If not, why not?  If they are, what
prevents an infinite loop?

IMO, application code that causes this to happen is impossibly fragile
and needs to be rewritten to not do it.

            regards, tom lane

Re: BUG #6123: DELETE fails if before trigger causes another trigger to UPDATE

From
"Kevin Grittner"
Date:
Tom Lane <tgl@sss.pgh.pa.us> wrote:
> "Kevin Grittner" <Kevin.Grittner@wicourts.gov> writes:
>> Tom Lane <tgl@sss.pgh.pa.us> wrote:
>>> The update causes the already-selected target row version of the
>>> DELETE to be obsoleted, so heap_delete finds it has nothing to
>>> do. I'm disinclined to mess with that logic.
>
>> It's pretty astonishing behavior for application programmers.
>> It's not unusual for triggers on detail from one table to
>> maintain a status, count, or sum in a higher level table.  When a
>> DELETE from the higher level table causes deletes at the lower
>> level, the lower level trigger really doesn't have any way to
>> know that.  I think this should be considered a bug.
>
> I think you have no idea how large a can of worms you're opening.
>
> To point out just one problem, if heap_delete restarts itself and
> tries to delete some other row version than it started with,
> should the ON DELETE triggers be fired again?

No.

> If not, why not?

It's the same row.  The UPDATE trigger fires when it's UPDATEd; but
it isn't being DELETEd a second time.  Imposing this level of
awareness of MVCC mechanics on application programmers isn't a good
idea, in my view.

I wouldn't dive into it lightly.  As I said, we've been trying to
work around it, but it is neither unreasonable to maintain summary
data in high level tables, nor to cascade deletes, and with hundreds
of tables, most of which have many triggers, it's not trivial to
catch all of these or to figure out how to avoid the issue in each
case.

For those who haven't heard about our environment or have forgotten,
these triggers were written in a vendor-neutral SQL dialect based on
the standard, and we parsed the SQL to generate Java classes which
contained the procedural logic and the less portable behaviors,
calling out to JDBC through a thin DBMS-specific portability layer.
Since we've decided to commit to PostgreSQL we're converting all
triggers to native PostgreSQL, with a mechanical first pass based on
a new tree-walker (for the parser which emitted the Java classes)
which is emitting plpgsql functions and trigger creation statements.

While the database is pretty well normalized, there is some
summarization of data from detail tables to higher levels for
performance, and some cases where a delete at a higher level should
cascade to lower levels, at least in some circumstances.  Both
behaviors are implemented in triggers in the old code and have been
working fine in the old framework.

> IMO, application code that causes this to happen is impossibly
> fragile

They've been working fine for over ten years in our old framework,
so "impossibly fragile" seems an overstatement.

> and needs to be rewritten to not do it.

Would you say it is more sane to sprinkle the logic to maintain
redundant summary data across the application instead of putting it
in triggers on the detail, or more sane to move logic to cascade
deletes outside the database?  When the triggers were firing from
our middle tier just above the database we didn't have to choose;
you seem to be saying we should, but neither alternative looks
pretty to me.

-Kevin