PostgreSQL check constraint for foreign key condition PostgreSQL check constraint for foreign key condition postgresql postgresql

PostgreSQL check constraint for foreign key condition


This would work for INSERTS:

create or replace function is_superuser(int) returns boolean as $$select exists (    select 1    from "user"    where id   = $1      and superuser = true);$$ language sql;

And then a check contraint on the user_has_job table:

create table user_has_job (    user_id integer references "user"(id),    job_id integer references job(id),    constraint user_has_job_pk PRIMARY KEY (user_id, job_id),    constraint chk_is_superuser check (is_superuser(user_id)));

Works for inserts:

postgres=# insert into "user" (name,superuser) values ('name1',false);INSERT 0 1postgres=# insert into "user" (name,superuser) values ('name2',true);INSERT 0 1postgres=# insert into job (description) values ('test');INSERT 0 1postgres=# insert into user_has_job (user_id,job_id) values (1,1);ERROR:  new row for relation "user_has_job" violates check constraint "chk_is_superuser"DETAIL:  Failing row contains (1, 1).postgres=# insert into user_has_job (user_id,job_id) values (2,1);INSERT 0 1

However this is possible:

postgres=# update "user" set superuser=false;UPDATE 2

So if you allow updating users you need to create an update trigger on the users table to prevent that if the user has jobs.


The only way I can think of is to add a unique constraint on (id, superuser) to the users table and reference that from the user_has_job table by "duplicating" the superuser flag there:

create table users (    id serial primary key,    name text not null,    superuser boolean not null default false);-- as id is already unique there is no harm adding this additional -- unique constraint (from a business perspective)alter table users add constraint uc_users unique (id, superuser);create table job (    id serial primary key,    description text);create table user_has_job (    user_id integer references users (id),    -- we need a column in order to be able to reference the unique constraint in users    -- the check constraint ensures we only reference superuser    superuser boolean not null default true check (superuser),     job_id integer references job(id),    constraint user_has_job_pk PRIMARY KEY (user_id, job_id),    foreign key (user_id, superuser) references users (id, superuser));insert into users  (id, name, superuser)values   (1, 'arthur', false),  (2, 'ford', true);insert into job   (id, description)values     (1, 'foo'),  (2, 'bar');

Due to the default value, you don't have to specify the superuser column when inserting into the user_has_job table. So the following insert works:

insert into user_has_job   (user_id, job_id)values  (2, 1);

But trying to insert arthur into the table fails:

insert into user_has_job   (user_id, job_id)values  (1, 1);

This also prevents turning ford into a non-superuser. The following update:

update users   set superuser = false where id = 2;

fails with the error

ERROR: update or delete on table "users" violates foreign key constraint "user_has_job_user_id_fkey1" on table "user_has_job"
Detail: Key (id, superuser)=(2, t) is still referenced from table "user_has_job".


Create a separate superuser table that inherits from the user table:

CREATE TABLE "user" (    id serial PRIMARY KEY,    name text NOT NULL,);CREATE TABLE superuser () INHERITS ("user");

The user_has_job table can then reference the superuser table:

CREATE TABLE user_has_job (    user_id integer REFERENCES superuser (id),    job_id integer REFERENCES job(id),    PRIMARY KEY (user_id, job_id));

Move users around between the tables as needed by inserting and deleting:

WITH promoted_user AS (    DELETE FROM "user" WHERE id = 1 RETURNING *) INSERT INTO superuser (id, name) SELECT id, name FROM promoted_user;