Adding UNIQUE constraints to tables in Postgres is very easy!

Imagine we have the following table:

CREATE TABLE users (  
  id     uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
  email  text
);

If we want to ensure that each user has a unique email we simply add:

ALTER TABLE users ADD CONSTRAINT email_unique UNIQUE (email);  

Let's try it out by inserting some data:

INSERT INTO users (email) VALUES ('test@example.com');  
INSERT INTO users (email) VALUES ('test@example.com');

ERROR:  duplicate key value violates unique constraint "email_unique"  
DETAIL:  Key (email)=(test@example.com) already exists.  

But there's a problem, the UNIQUE constraint is case sensitive!

INSERT INTO users (email) VALUES ('test@example.com');  
INSERT INTO users (email) VALUES ('TEST@example.com');

SELECT * from users;

                  id                  |      email       
--------------------------------------+------------------
 ccfcddd2-bdc5-4cf4-9475-4171960e6262 | test@example.com
 431308b4-8df8-44c9-bed4-7c44cf4e1ec1 | TEST@example.com
(2 rows)

Unfortunately, Postgres does now allow us to define a unique constraint using LOWER like:

ALTER TABLE users ADD CONSTRAINT email_unique UNIQUE (LOWER(email));

ERROR:  syntax error at or near "("  

But don't worry there's a workaround! Instead of using the text data type, we can use the citext (case insensitive text) type! First we need to enable the citext extension:

CREATE EXTENSION IF NOT EXISTS citext;  

Then we'll need to change the email data type in the users table:

ALTER TABLE users ALTER COLUMN email TYPE citext;  

Now our existing UNIQUE constraint should be case insensitive!

INSERT INTO users (email) VALUES ('test@example.com');  
INSERT INTO users (email) VALUES ('TEST@example.com');

ERROR:  duplicate key value violates unique constraint "email_unique"  
DETAIL:  Key (email)=(TEST@example.com) already exists.  

Another solution is to add a UNIQUE INDEX instead of a CONSTRAINT like:

CREATE UNIQUE INDEX email_unique_idx on users (LOWER(email));