Suppose I have a system that stores a company's departments and its locations. Each location has its own specific departments, but there are also global departments that are at every location.
In order to store a department, I use a table that stores a name and the location ID. If the location ID is null
, then the department is global.
My issue is with the uniqueness of the name. This uniqueness is dependent on the location ID. The name generally needs to be unique within its own location, i.e. a simple UNIQUE ("name", "locationId")
would suffice. However, the name needs to be globally unique should the location be null
. This is not covered by the constraint above. So, my next attempt was using two partial constraints:
CREATE UNIQUE INDEX "locationId_and_name" ON "activities" ("locationId", "name") WHERE "locationId" IS NOT NULL
CREATE UNIQUE INDEX "global_name" ON "activities" ("name") WHERE "locationId" IS NULL
This works, except for the cases where there is already a global department and one tries to create a new location bound department with the same name. For example, the following rows would be allowed to exist at the same time, even though they should not be: ('Name', 'location-1'), ('Name', NULL)
.
Is there any way that I could set up the constraints that it works properly?
For clarification, allowed are:
('Name', 'location-1'), ('Name', 'location-2')
('Name 1', 'location-1'), ('Name 2', 'location-1')
('Name 1', 'location-1'), ('Name 2', null)
And not allowed are:
('Name', 'location-1'), ('Name', 'location-1')
('Name 1', 'location-1'), ('Name 1', NULL)
('Name 1', NULL), ('Name 1', null)
CodePudding user response:
The logic is too complex to be implemented with indexes alone. Leave the first index with the columns reordered and use a trigger instead of the second index.
create unique index "name_and_locationId"
on "activities" ("name", "locationId")
where "locationId" is not null;
create or replace function activities_trigger()
returns trigger language plpgsql as $$
begin
if
new."locationId" is not null and exists(
select from activities
where "name" = new."name" and "locationId" is null)
or
new."locationId" is null and exists(
select from activities
where "name" = new."name")
then raise exception '"%" is a global department.', new."name";
end if;
return new;
end $$;
create trigger activities_trigger
before insert or update on activities
for each row execute procedure activities_trigger();
Note that reordering the columns in the index will cause it to support queries in the trigger.
Test it in db<>fiddle.