3

I'm having trouble with ON CONFLICT not working for foreign key columns where the foreign key is compound. Here's an example.

create table foreign_table (
   id_a text    not null,
   id_b text    not null,
   id   integer primary key,
   constraint ft_a_b_key unique (id_a, id_b)
);

create table my_table (
   id          integer,
   ftable_id_a text,
   ftable_id_b text,
   constraint my_table_a_b_fk
      foreign key (ftable_id_a, ftable_id_b) references foreign_table (id_a, id_b)
);

Using this query:

insert into tcell_test.my_table (id, ftable_id_a, ftable_id_b) 
    values (3, 'a3', 'b3') on conflict do nothing ;

where, say, 'a3' isn't in foreign_table, I would expect the ON CONFLICT to handle the error.

Instead I get the error:

[23503] ERROR: insert or update on table "my_table" 
   violates foreign key constraint "my_table_a_b_fk" 
Detail: Key (ftable_id_a, ftable_id_b)=(a3, b3) 
   is not present in table "foreign_table".

Is there a way to correct this so ON CONFLICT handles the error?

  • 4
    AFAIK, what you describe is not a conflict. The construction is a way to handle UNIQUE constraint violations, not constraint violations in general. – Lennart Nov 9 '19 at 1:10
  • If I understand correctly that you want to insert the given row only if the FK constraint is satisfied and not raise an exception otherwise, then I suggest to change the title accordingly. Something like: "How to skip INSERT rows silently when FK constraint is violated?" And please remember to always disclose your version of Postgres. – Erwin Brandstetter Nov 10 '19 at 23:22
3

I think you may be misunderstanding what a CONFLICT is. A CONFLICT is a violation of uniqueness, basically the row that is being added should not be added because another row with the same values already exists.

In your example of insert into tcell_test.my_table (id, ftable_id_a, ftable_id_b) values (3, 'a3', 'b3') on conflict do nothing;, the ON CONFLICT condition will never be reached because you have no primary key or unique constraint on my_table:

edb=# alter table my_table add primary key (ftable_id_a,ftable_id_b);
ALTER TABLE
edb=# insert into my_table (id, ftable_id_a, ftable_id_b) 
    values (3, 'a3', 'b3') on conflict do nothing ;
INSERT 0 1
edb=# insert into my_table (id, ftable_id_a, ftable_id_b) 
    values (3, 'a3', 'b3') on conflict do nothing ;
INSERT 0 0
edb=# select * from my_table ;
 id | ftable_id_a | ftable_id_b 
----+-------------+-------------
  3 | a3          | b3
(1 row)

As you can see in the example above, my second insertion into my_table did nothing because there would have been a primary key violation. If I omit the ON CONFLICT clause, I get:

edb=# insert into my_table (id, ftable_id_a, ftable_id_b) 
    values (3, 'a3', 'b3');
ERROR:  duplicate key value violates unique constraint "my_table_pkey"
DETAIL:  Key (ftable_id_a, ftable_id_b)=(a3, b3) already exists.

The message that you mentioned in you original post (ERROR: insert or update on table "my_table" violates foreign key constraint "my_table_a_b_fk") pertains to a Foreign Key violation (not a primary key/uniqueness violation). This violation occurs when there should be a row in foreign_table with id_a=a3 and id_b=b3, but there isn't. The database expects this row to exist because you have defined that my_table references foreign_table (in other words, my_table depends on foreign_table). Because the row doesn't exist in foreign_table, your insertion into my_table fails. First, insert into foreign_table, and then you can insert into my_table:

edb=# insert into my_table (id, ftable_id_a, ftable_id_b) 
    values (3, 'a3', 'b3') on conflict do nothing ;
ERROR:  insert or update on table "my_table" violates foreign key constraint "my_table_a_b_fk"
DETAIL:  Key (ftable_id_a, ftable_id_b)=(a3, b3) is not present in table "foreign_table".
edb=# insert into foreign_table values ('a3','b3',1);
INSERT 0 1
edb=# select * from foreign_table ;
 id_a | id_b | id 
------+------+----
 a3   | b3   |  1
(1 row)
edb=# insert into my_table (id, ftable_id_a, ftable_id_b) 
    values (3, 'a3', 'b3') on conflict do nothing ;
INSERT 0 1
edb=# select * from my_table ;
 id | ftable_id_a | ftable_id_b 
----+-------------+-------------
  3 | a3          | b3
(1 row)
3

If I understand correctly you want to insert the given row only if the FK constraint is satisfied - and do nothing otherwise, in particular do not raise an exception.

This is no "UPSERT", i.e. not a use case for INSERT .. ON CONFLICT DO NOTHING, which only works for violations of unique indexes or exclusion constraints. The manual:

The optional ON CONFLICT clause specifies an alternative action to raising a unique violation or exclusion constraint violation error.

Solution

Use an INSERT with a conditional SELECT instead. And possibly add a locking clause.

INSERT INTO my_table (id, ftable_id_a, ftable_id_b)
SELECT i.*
FROM  (VALUES (3, 'a2', 'b3')) i(id, ftable_id_a, ftable_id_b) -- provide values once
                                                               -- works for multiple rows
WHERE  EXISTS (
   SELECT FROM foreign_table f                                 -- SELECT list can be empty
   WHERE (f.id_a, f.id_b) = (i.ftable_id_a, i.ftable_id_b) 
   FOR    SHARE                                                -- weakest lock
   );

The locking clause FOR SHARE is the weakest lock and good enough for the use case. Details in the manual. Without locking, there is a tiny time frame for a race condition between looking the row(s) in the EXISTS subquery and the actual INSERT: concurrent transactions might UPDATE / DELETE the FK columns of the row in foreign_table in the meantime, which would result in an exception after all - the thing we want to avoid.

If there cannot be such concurrent writes, you can skip the locking.

Be aware of a subtle difference in type handling introduced with the subquery. With basic data types integer and text and appropriate numeric / string literal input, everything just keeps working. But you may have to cast input values explicitly for other input/target type combinations that cannot be resolved automatically. See:

Related:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.