Thread: Deferred foreign key and two updates block ALTER TABLE

Deferred foreign key and two updates block ALTER TABLE

From
André Hänsel
Date:
Hi list,

this looks like a bug to me, forgive me if it's expected behavior:

BEGIN;

DROP TABLE IF EXISTS test;
DROP TABLE IF EXISTS test_other;

CREATE TABLE test(
    some_column integer NOT NULL,
    other_id integer NOT NULL
);
CREATE TABLE test_other(
    other_id integer NOT NULL
);
ALTER TABLE test_other ADD PRIMARY KEY (other_id);
ALTER TABLE test ADD FOREIGN KEY (other_id) REFERENCES test_other
DEFERRABLE;

INSERT INTO test_other (other_id) VALUES (1);
INSERT INTO test (some_column, other_id) VALUES (0, 1);

COMMIT;

BEGIN;

SET CONSTRAINTS ALL DEFERRED;

UPDATE test SET some_column = 1 WHERE some_column = 0;
UPDATE test SET some_column = 2 WHERE some_column = 1;
ALTER TABLE test ALTER COLUMN some_column TYPE smallint;

COMMIT;

Expected result: Both transactions can be committed without error

Actual result: ERROR:  cannot ALTER TABLE "test" because it has pending
trigger events SQL state: 55006

Notes:
- It doesn't matter if the UPDATEs are on the same or different colums that
are altered, there is still an error
- There is no error if there is just one UPDATE instead of two (this makes
me think this might be a bug)
- There is no error if both UPDATEs lead to the same result (when the second
UPDATE is a no-op, like setting some_column = 1 again)
- There is no error if the foreign key is non-deferrable or currently
immediate

Regards,
André





Re: Deferred foreign key and two updates block ALTER TABLE

From
Andrew Gierth
Date:
>>>>> "André" == André Hänsel <andre@webkr.de> writes:

 André> Hi list,
 André> this looks like a bug to me, forgive me if it's expected
 André> behavior:

It is expected behavior.

 André> Actual result: ERROR: cannot ALTER TABLE "test" because it has
 André> pending trigger events SQL state: 55006

What this means is that there are pending trigger events on the table,
and calling those triggers after altering the column type would not be
possible, for example because rewriting the table would change the TIDs
of rows making it impossible for the queued event to locate them.

 André> - There is no error if there is just one UPDATE instead of two
 André> (this makes me think this might be a bug)

That's actually an optimization; there are cases where a foreign-key
check can be optimized away, which is happening on the first update, but
one of the current conditions for that optimization is that the row or
row version being updated is not new in the current transaction, so a
second or subsequent update, or an update after an insert, will require
the check to actually be queued.

 André> - There is no error if both UPDATEs lead to the same result
 André> (when the second UPDATE is a no-op, like setting some_column = 1
 André> again)

This one I can't currently explain without digging into the code.

 André> - There is no error if the foreign key is non-deferrable or
 André> currently immediate

This is because these cases don't leave trigger events on the queue
between statements - they fire at the end of the statement.

BTW, this kind of restriction is anticipated in the SQL spec (in SQL
2016, see 4.41.1 "General description of SQL-transactions"):

  It is implementation-defined whether or not the execution of an
  SQL-data statement is permitted to occur within the same
  SQL-transaction as the execution of an SQL-schema statement. If it
  does occur, then the effect on any open cursor or deferred constraint
  is implementation-defined. There may be additional implementation-
  defined restrictions, requirements, and conditions. If any such
  restrictions, requirements, or conditions are violated, then an
  implementation-defined exception condition or a completion condition
  warning with an implementation-defined subclass code is raised.

PG tries hard to allow mixed DDL and DML/queries in transactions, but
where one would invalidate information needed by the other, it has to
throw an error.

--
Andrew (irc:RhodiumToad)


Re: Deferred foreign key and two updates block ALTER TABLE

From
David Rowley
Date:
On Mon, 21 Jan 2019 at 14:31, André Hänsel <andre@webkr.de> wrote:
> BEGIN;
>
> SET CONSTRAINTS ALL DEFERRED;
>
> UPDATE test SET some_column = 1 WHERE some_column = 0;
> UPDATE test SET some_column = 2 WHERE some_column = 1;
> ALTER TABLE test ALTER COLUMN some_column TYPE smallint;
>
> COMMIT;
>
> Expected result: Both transactions can be committed without error
>
> Actual result: ERROR:  cannot ALTER TABLE "test" because it has pending
> trigger events SQL state: 55006
>
> Notes:
> - It doesn't matter if the UPDATEs are on the same or different colums that
> are altered, there is still an error
> - There is no error if there is just one UPDATE instead of two (this makes
> me think this might be a bug)
> - There is no error if both UPDATEs lead to the same result (when the second
> UPDATE is a no-op, like setting some_column = 1 again)

I don't think this is a bug.  Your 2nd UPDATE updates the row that was
updated by the previous statement, which hits the following case
inside RI_FKey_fk_upd_check_required():

/*
* If the original row was inserted by our own transaction, we
* must fire the trigger whether or not the keys are equal.  This
* is because our UPDATE will invalidate the INSERT so that the
* INSERT RI trigger will not do anything; so we had better do the
* UPDATE check.  (We could skip this if we knew the INSERT
* trigger already fired, but there is no easy way to know that.)
*/
if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(old_row->t_data)))
return true;

The first UPDATE did not require a check since you didn't update any
of the referencing columns.





--
 David Rowley                   http://www.2ndQuadrant.com/
 PostgreSQL Development, 24x7 Support, Training & Services