Unique constraint on 3 different columns but a specific value in 3rd column allows duplicate row entries
-
27-02-2021 - |
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
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');