Thread: surprising trigger/foreign key interaction

surprising trigger/foreign key interaction

From
Stefan Kaltenbrunner
Date:
While working on some code I ran into a problem where some DELETE 
requests would get seamingly ignored after a while I managed to boil it 
down to:

CREATE TABLE foo (a INT PRIMARY KEY, b int);
CREATE TABLE bar (x int PRIMARY KEY, a int references foo(a) ON DELETE 
SET NULL);

INSERT INTO foo VALUES (1,10);
INSERT INTO bar VALUES (99,1);

CREATE OR REPLACE FUNCTION bar_proc() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN        RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN        RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN        DELETE FROM foo WHERE a=1;        RETURN OLD;
END IF;
RETURN OLD;
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER bar_tr BEFORE INSERT OR DELETE ON bar FOR EACH ROW 
EXECUTE PROCEDURE bar_proc();

DELETE FROM bar where x=99;

which results in:

CREATE TABLE
INSERT 0 1
INSERT 0 1
CREATE FUNCTION
CREATE TRIGGER
DELETE 0


the "surprise" here was that the delete is getting silently surpressed 
even though the original Qual still holds and afaik should result in the 
row deleted.
Is that somehow expected behaviour or a bug(or at least something that 
should get documented somehow)?


Stefan


Re: surprising trigger/foreign key interaction

From
Tom Lane
Date:
Stefan Kaltenbrunner <stefan@kaltenbrunner.cc> writes:
> the "surprise" here was that the delete is getting silently surpressed 
> even though the original Qual still holds and afaik should result in the 
> row deleted.

The delete from foo acts first (since you put it in a BEFORE trigger).
After the trigger comes back, the outer delete finds itself trying
to delete a row that was already updated by a later-starting command in
the same transaction.  So it doesn't do anything to that row (cf the
HeapTupleSelfUpdated cases in execMain.c).  I think this is documented
someplace but don't remember where.
        regards, tom lane


Re: surprising trigger/foreign key interaction

From
Alvaro Herrera
Date:
Tom Lane wrote:
> Stefan Kaltenbrunner <stefan@kaltenbrunner.cc> writes:
> > the "surprise" here was that the delete is getting silently surpressed 
> > even though the original Qual still holds and afaik should result in the 
> > row deleted.
> 
> The delete from foo acts first (since you put it in a BEFORE trigger).
> After the trigger comes back, the outer delete finds itself trying
> to delete a row that was already updated by a later-starting command in
> the same transaction.

Hmm, I misread the code when talking to Stefan on IM.  I followed the
path that heap_update returns HeapTupleUpdated, which proceeds to do
EvalPlanQual and attempt to update the updated version of the row.
However I'm guessing that what actually happens is that heap_update is
returning HeapTupleSelfUpdated instead, which the code states as
/* nothing to do */.

I imagine this is so because of some old fiddling to get semantics just
right for obscure corner cases, but it feels wrong nevertheless.

-- 
Alvaro Herrera                                http://www.CommandPrompt.com/
PostgreSQL Replication, Consulting, Custom Development, 24x7 support


Re: surprising trigger/foreign key interaction

From
Tom Lane
Date:
Alvaro Herrera <alvherre@commandprompt.com> writes:
> However I'm guessing that what actually happens is that heap_update is
> returning HeapTupleSelfUpdated instead, which the code states as
> /* nothing to do */.

Yeah.

> I imagine this is so because of some old fiddling to get semantics just
> right for obscure corner cases, but it feels wrong nevertheless.

I suspect it was reluctance to use the EvalPlanQual semantics (which
are pretty bogus in their own way) for perfectly deterministic
single-transaction cases.
        regards, tom lane


Re: surprising trigger/foreign key interaction

From
Stefan Kaltenbrunner
Date:
Tom Lane wrote:
> Alvaro Herrera <alvherre@commandprompt.com> writes:
>> However I'm guessing that what actually happens is that heap_update is
>> returning HeapTupleSelfUpdated instead, which the code states as
>> /* nothing to do */.
> 
> Yeah.
> 
>> I imagine this is so because of some old fiddling to get semantics just
>> right for obscure corner cases, but it feels wrong nevertheless.
> 
> I suspect it was reluctance to use the EvalPlanQual semantics (which
> are pretty bogus in their own way) for perfectly deterministic
> single-transaction cases.

still the current behaviour feels quite wrong because even after the 
update the modified tuple still satisfies the WHERE clause of the DELETE 
but still it won't actually get deleted.


Stefan


Re: surprising trigger/foreign key interaction

From
Alvaro Herrera
Date:
Tom Lane wrote:
> Alvaro Herrera <alvherre@commandprompt.com> writes:

> > I imagine this is so because of some old fiddling to get semantics just
> > right for obscure corner cases, but it feels wrong nevertheless.
> 
> I suspect it was reluctance to use the EvalPlanQual semantics (which
> are pretty bogus in their own way) for perfectly deterministic
> single-transaction cases.

I suspect the FK trigger messing up the visibility is an obscure corner
case too :-(

-- 
Alvaro Herrera                                http://www.CommandPrompt.com/
PostgreSQL Replication, Consulting, Custom Development, 24x7 support


Re: surprising trigger/foreign key interaction

From
Josh Berkus
Date:
On 8/13/09 7:03 AM, Alvaro Herrera wrote:
> Tom Lane wrote:
>> Alvaro Herrera <alvherre@commandprompt.com> writes:
> 
>>> I imagine this is so because of some old fiddling to get semantics just
>>> right for obscure corner cases, but it feels wrong nevertheless.
>> I suspect it was reluctance to use the EvalPlanQual semantics (which
>> are pretty bogus in their own way) for perfectly deterministic
>> single-transaction cases.
> 
> I suspect the FK trigger messing up the visibility is an obscure corner
> case too :-(

Yes, but it's one which happens frequently.

I've already had to debug a client case where a client had a before
trigger, and after trigger, and a self-join FK.  That seems like a
bizarre arrangement, but for a proximity tree (which we're going to see
a lot more of thanks to WITH RECURSIVE) it actually makes a lot of sense.

The result is that you can get a *successful* transaction, with no
error, that nevertheless results in rows which are inconsistent with the
FK -- silent data corruption.  I had to tell the user to disable the FK
and maintain consistency by trigger as well, which doesn't reflect well
on our devotion to avoiding data corruption.

This is 100% reproduceable; test case below my sig.

-- 
Josh Berkus
PostgreSQL Experts Inc.
www.pgexperts.com


-- create two tables, one of which is the master table (reftable) the
other of which is a child which contains a tree structure (treetab):

create table reftable(   refid int primary key,   refname text
);


create table treetab (   id int primary key,   parent int,   refid int not null references reftable(refid) on delete
cascade,  name text
 
);

-- now create a trigger function to maintain the integrity of the trees
in treetab by "pulling up"
-- each node to its parent if intermediate nodes get deleted
-- this trigger is inherently flawed and won't work with the FK below

create function treemaint () returns trigger as $t$
begin   update treetab set parent = OLD.parent   where parent = OLD.id;   return OLD;
end; $t$ language plpgsql;

create trigger treemaint_trg before delete on treetab
for each row execute procedure treemaint();

-- populate reftable

insert into reftable
select i, ( 'Ref' || i::TEXT ) from generate_series(1,100) as g(i);

-- populate treetab with 10 rows each pointing to reftable

insert into treetab (id, refid)
select i, (( i / 10::INT ) + 1 )
from generate_series (1,900) as g(i);

-- create trees in treetab.  for this simple example each treeset is
just a chain with each child node
-- pointing to one higher node

update treetab set parent = ( id - 1 )
where id >  (   select min(id) from treetab tt2   where tt2.refid = treetab.refid);

update treetab set "name" = ('tree' || parent::TEXT || '-' || id::TEXT);

-- now create a self-referential FK to enforce tree integrity.  This
logically breaks the trigger

alter table treetab add constraint selfref foreign key (parent)
references treetab (id);

-- show tree for id 45

select * from treetab where refid = 45;
id  | parent | refid |    name
-----+--------+-------+-------------440 |        |    45 |441 |    440 |    45 | tree440-441442 |    441 |    45 |
tree441-442443|    442 |    45 | tree442-443444 |    443 |    45 | tree443-444445 |    444 |    45 | tree444-445446 |
445 |    45 | tree445-446447 |    446 |    45 | tree446-447448 |    447 |    45 | tree447-448449 |    448 |    45 |
tree448-449


-- now, we're going to delete the tree.  This delete should fail with an
error because the
-- trigger will violate "selfref"

delete from reftable where refid = 45;

-- however, it doesn't fail.  it reports success, and some but not all
rows from treetab
-- are deleted, leaving the database in an inconsistent state.

select * from treetab where refid = 45;
id  | parent | refid |    name
-----+--------+-------+-------------441 |        |    45 | tree440-441443 |    441 |    45 | tree442-443445 |    443 |
 45 | tree444-445447 |    445 |    45 | tree446-447449 |    447 |    45 | tree448-449
 

-- this means we now have rows in the table which
-- violate the FK to reftable.

postgres=# select * from reftable where refid = 45;refid | refname
-------+---------
(0 rows)