It has been explained already that a constraint or unique index only cannot enforce the logic that you want.
An alternative approach would be to use a materialized view. The logic is to use window functions to create an additional column in the view that resets every two rows having the same (user_id, enabled)
. You can then put a unique partial index on that column. Finally, you can create a trigger that refreshes the view everytime a record is inserted or updated, which effectively enforces the unique constraint.
-- table set-up
create table mytable(user_id int, enabled boolean);
-- materialized view set-up
create materialized view myview as
select
user_id,
enabled,
(row_number() over(partition by user_id, enabled) - 1) % 2 rn
from mytable;
-- unique partial index that enforces integrity
create unique index on myview(user_id, rn) where(enabled);
-- trigger code
create or replace function refresh_myview()
returns trigger language plpgsql
as $$
begin
refresh materialized view myview;
return null;
end$$;
create trigger refresh_myview
after insert or update
on mytable for each row
execute procedure refresh_myview();
With this set-up in place, let's insert the initial content:
insert into mytable values
(123, true),
(123, true),
(234, false),
(234, true);
This works, and the content of the view is now:
user_id | enabled | rn
------: | :------ | -:
123 | t | 0
123 | t | 1
234 | f | 0
234 | t | 0
Now if we try to insert a row that violates the constraint, an error is raised, and the insert
is rejected.
insert into mytable values(123, true);
-- ERROR: could not create unique index "myview_user_id_rn_idx"
-- DETAIL: Key (user_id, rn)=(123, 0) is duplicated.
-- CONTEXT: SQL statement "refresh materialized view myview"
-- PL/pgSQL function refresh_myview() line 3 at SQL statement
Demo on DB Fiddle