Question

I have a 'Users' table with columns user_email, user_company_id and user_status. The user_status column is an enum with values '1' or '0' which represents the users being either active or inactive. Is there a way to apply a unique constraint to these 3 columns such that it only allows one unique, active user email for a specific company but any number of duplicate entires for inactive emails?

E.g.: Consider a 'Users' table with the following entries

CREATE TABLE users(
  user_id BIGINT(10) PRIMARY KEY AUTO_INCREMENT, 
  user_email VARCHAR(255) NOT NULL, 
  user_companyid BIGINT(10) NOT NULL, 
  user_status enum('1', '0'))

INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (1,'test1@gmail.com','555','1');
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (2,'test2@gmail.com','555','1');
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (3,'test1@gmail.com','777','1');

SELECT * FROM users;


user_id | user_email      | user_companyid | user_status
------: | :-------------- | -------------: | :----------
      1 | test1@gmail.com |            555 | 1          
      2 | test2@gmail.com |            555 | 1          
      3 | test1@gmail.com |            777 | 1           

I shouldn't be able to add an existing, active email for a specfic company twice; the following should fail:

INSERT INTO users(user_id, user_email, user_companyid, user_status) 
 VALUES (4,'test1@gmail.com','555','1'); 

If I update the status of one of the active users to '0' (inactive), I should be able to insert the same email again since the previous email status is inactive. The following should succeed:

UPDATE users SET user_status = '0' WHERE user_id = 1;

INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (4,'test1@gmail.com','555','1');

user_id | user_email      | user_companyid | user_status
------: | :-------------- | -------------: | :----------
      1 | test1@gmail.com |            555 | 0          
      2 | test2@gmail.com |            555 | 1          
      3 | test1@gmail.com |            777 | 1          
      4 | test1@gmail.com |            555 | 1          

Also, the constraint should allow duplicate entries for inactive user emails. This should also succeed:

UPDATE users SET user_status = '0' WHERE user_id = 4;

SELECT * FROM users;

user_id | user_email      | user_companyid | user_status
------: | :-------------- | -------------: | :----------
      1 | test1@gmail.com |            555 | 0          
      2 | test2@gmail.com |            555 | 1          
      3 | test1@gmail.com |            777 | 1          
      4 | test1@gmail.com |            555 | 0
Was it helpful?

Solution

As i saig in the comment yoi have t make a BEFORE INSERT trigger

CREATE TABLE users(
  user_id BIGINT(10) PRIMARY KEY AUTO_INCREMENT, 
  user_email VARCHAR(255) NOT NULL, 
  user_companyid BIGINT(10) NOT NULL, 
  user_status enum('1', '0'))
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (1,'test1@gmail.com','555','1');
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (2,'test2@gmail.com','555','1');
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (3,'test1@gmail.com','777','1');
✓

✓

✓
SELECT * FROM users;
user_id | user_email      | user_companyid | user_status
------: | :-------------- | -------------: | :----------
      1 | test1@gmail.com |            555 | 1          
      2 | test2@gmail.com |            555 | 1          
      3 | test1@gmail.com |            777 | 1          
CREATE TRIGGER users_before_insert
BEFORE INSERT
   ON users FOR EACH ROW

BEGIN

   DECLARE vUser varchar(50);

   -- Find username of person performing INSERT into table
   IF EXISTS(SELECT 1 
            FROM users 
            WHERE 
             user_email = NEW.user_email
             AND user_companyid = NEW.user_companyid
             AND user_status = 1) THEN
     signal sqlstate '45000' 
     SET MESSAGE_TEXT = 'User already activated';

  END IF;

END; 
INSERT INTO users( user_email, user_companyid, user_status) 
  VALUES ('test1@gmail.com','555','1');
User already activated
SELECT * FROM users;
user_id | user_email      | user_companyid | user_status
------: | :-------------- | -------------: | :----------
      1 | test1@gmail.com |            555 | 1          
      2 | test2@gmail.com |            555 | 1          
      3 | test1@gmail.com |            777 | 1          

db<>fiddle here

OTHER TIPS

This did not work because auto_increment columns cannot be referenced by a generated column, but I will add it anyhow since it demonstrates a technique that can be useful. The idea is to use a generated column, that when user_status = 0 maps to something guaranteed unique (primary key) and otherwise maps to a constant. Then this column can be included in a UNIQUE constraint together with the columns that should be unique under condition:

CREATE TABLE users
( user_id BIGINT PRIMARY KEY  -- auto_increment had to be removed
, user_email VARCHAR(255) NOT NULL
, user_companyid BIGINT NOT NULL
, user_status enum('1', '0')
, gencol BIGINT GENERATED ALWAYS as 
    ( CASE WHEN user_status = 1
           THEN -1
           ELSE user_id
      END
    ) NOT NULL
);

ALTER TABLE users ADD CONSTRAINT ak1
    UNIQUE (user_email, user_companyid, gencol);

INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (1,'test1@gmail.com','555','1');
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (2,'test2@gmail.com','555','1');
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
  VALUES (3,'test1@gmail.com','777','1');

-- Fails    
-- INSERT INTO users(user_id, user_email, user_companyid, user_status) 
-- VALUES (4,'test1@gmail.com','555','1'); 

UPDATE users SET user_status = '0' WHERE user_id = 1;

-- Succeeds
INSERT INTO users(user_id, user_email, user_companyid, user_status) 
VALUES (4,'test1@gmail.com','555','1'); 
Licensed under: CC-BY-SA with attribution
Not affiliated with dba.stackexchange
scroll top