简体   繁体   中英

Postgres conditional unique constraint

Pretend I have a users table where users are members of a specific tenant, and their e-mails are uniquely indexed to their tenant, like this:

User
id  |  tenant_id  |  email
1      1             person1@example.com
2      1             person2@example.com

This user is allowed because despite a duplicate e-mail, they are at a different tenant:

3      2             person1@example.com

This user is prevented because the e-mail is a duplicate at the same tenant:

4      2             person1@example.com <--- will throw an error

We have this much covered with a unique index -- that part is easy.

Now pretend that I want to be able to add a global user that can access all tenants, but only if the e-mail doesn't already exist in the table at all. Additionally, once the record exists, nobody else -- whether tenanted or not -- will be able to use the same e-mail.

For clarity, the global users could simply have a null tenant ID but we would likely also add a global boolean.

Is there a way to write constraints for this logic? You can't simply make e-mails globally uniquely constrained because they won't be able to be repeated across tenants, and if you index with a null tenant ID, postgres will allow an untenanted user if there are tenanted users with the same e-mail.

I've looked at exclusion constraints and checks but couldn't figure out how to combine them (uniquely constrain e-mail globally if tenant_id is null, and check for records with null tenant ID and matching e-mail when inserting any record).

Please don't ask why I'm doing things this way -- my table isn't actually users and we've considered and dismissed other architectures :)

Thanks in advance!

According to PostgreSQL Documentation you can create unique partial index which will be effectively the same as creating unique partial constraint on your table:

CREATE UNIQUE INDEX some_index ON some_table (col_a) WHERE (col_b is null);

Using this technique you can create 2 separate unique indexes for admin and non-admin users.

You can use a UNIQUE constraint for both fields:

create table myUsers
(
    id int not null,
    tenant int not null,
    email varchar(200) not null,
    UNIQUE(email, tenant)
);

insert into myUsers values
(1, 1, 'person1@example.com'),
(2, 1, 'person2@example.com');

insert into myUsers values
(3, 2, 'person1@example.com');

Next insert will throw an error:

insert into myUsers values
(4, 2, 'person1@example.com');

Error(s), warning(s):

23505: duplicate key value violates unique constraint "myusers_email_tenant_key"

Check it here: http://rextester.com/AJZVI34616

For the second part of the question:

Now pretend that I want to be able to add a global user that can access all tenants, but only if the e-mail doesn't already exist in the table at all.

One solution could be to reserve a tenant for admin users:

tenant = 0  <-- admin users

But the UNIQUE constraint allow duplicated emails, I recommend you to add a rol field to this table, or have another table of admin users for this purpose.

In my case, we use two tables, and both have a rol field.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM