Making two columns in a table unique - sql

I have a table that looks like this:
ID
email
alt_email
1
abc#gmail.com
123#gmail.com
2
def#gmail.com
ghi#gmail.com
Each user has an email and alternative email.
I want to make it so that when a user is added (or modified), the PSQL table does not allow duplicates, even between the two columns.
For example, this would not be allowed:
ID
email
alt_email
1
abc#gmail.com
def#gmail.com
2
def#gmail.com
ghi#gmail.com
Because def#gmail.com is duplicated. How can I accomplish this?

The easiest alternative - without changing the data model - would be to control it within a trigger function:
Declare the columns independently as UNIQUE, so that there are no duplicated e-mails in the same column:
CREATE TABLE t (
id int,
email text UNIQUE,
alt_email text UNIQUE
);
Create a function to check if the e-mails exists in another column:
CREATE OR REPLACE FUNCTION check_email()
RETURNS TRIGGER AS $$
BEGIN
IF (SELECT EXISTS
(SELECT 1 FROM t WHERE alt_email = NEW.email OR email = NEW.alt_email)) THEN
RAISE EXCEPTION 'E-mail already exists!';
ELSE
RETURN NEW;
END IF;
END; $$ LANGUAGE plpgsql;
And finally attach the trigger function to the table
CREATE TRIGGER tgr_check_email
BEFORE INSERT OR UPDATE ON t
FOR EACH ROW EXECUTE PROCEDURE check_email();
Demo: db<>fiddle

Neither unique index nor constraint will help you there.
You can:
Create a INSERT/UPDATE trigger that will check the emails. A locking mechanism must be implemented to disallow concurrent inserts/updates as checks are performed in different transactions - that may harm your performance.
Move your emails to separate table as this is a one-to-many relationship. On this new table (let's call it email) you can add a unique index or even have an email as a primary key. This is the right and most clean solution to your problem. You can create a view with a name and the columns the same as your current table to move with the change quicker.
Do both of 1 and 2: create the email table and add the trigger maintaining the records of it based on the email and alt_email values. You will have the email duplicates but you will not have to change anything in your application - all the changes go into DB schema.

Related

How to create a trigger that automatically generates a primary key from multiple fields

I have a geospatial db with (a.o.) a table with locations, and a table with features. The primary key for the locations table is location_id. Location_id is also a foreign key in the features table. The features table also includes the fields "type" (in which a two-letter code is entered to denote particular types of features), and N (which differentiates the different features that may be linked to one location). I figured a combination of location_id, type, and N would make a decent primary key for the features table. Previously, I entered these ids manually. However, I would like for this to be automatically done when a "user" enters a location ID, N, and type. (Ideally I want to find a way to automatically generate the correct N, so that "users" need only enter location_id and type, but I think this should be posted as a separate question?).
I have been trying to achieve this via triggers (see code below), but when I test it by trying to add a new data row to my features table, I get the error message "duplicate key value violates unique constraint features_pkey". Could someone point me in the direction of help for this issue?
CREATE OR REPLACE FUNCTION set_features_id()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
DECLARE
compos_id text;
BEGIN
SELECT loc_id || type || N FROM features INTO compos_id;
NEW.id := compos_id;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS set_lf_id_trigger on public.landscape_features_point;
CREATE TRIGGER set_features_id_trigger
BEFORE INSERT
ON "features"
FOR EACH ROW
EXECUTE PROCEDURE set_features_id();

DB2 locking when no record yet exists

I have a table, something like:
create table state {foo int not null, bar int not null, baz varchar(32)};
create unique index on state(foo,bar);
I'd like to lock for a unique record in this table. However, if there's no existing record I'd like to prevent anyone else from inserting a record, but without inserting myself.
I'd use "FOR UPDATE WITH RS USE AND KEEP EXCLUSIVE LOCKS" but that only seems to work if the record exists.
A) You can let DB2 create every ID number. Let's say you have defined your Customer table
CREATE TABLE Customers
( CustomerID Int NOT NULL
GENERATED ALWAYS AS IDENTITY
PRIMARY KEY
, Name Varchar(50)
, Billing_Type Char(1)
, Balance Dec(9,2) NOT NULL DEFAULT
);
Insert rows without specifying the CustomerID, since DB2 will always produce the value for you.
INSERT INTO Customers
(Name, Billing_Type)
VALUES
(:cname, :billtype);
If you need to know what the last value assigned in your session was, you can then use the IDENTITY_VAL_LOCAL() function.
B) In my environment, I generally specify GENERATED BY DEFAULT. This is in part due to the nature of our principle programming language, ILE RPG-IV, where developers have traditionally to allowed the compiler to use the entire record definition. This leads me to I can tell everyone to use a sequence to generate ID values for a given table or set of tables.
You can grant select to only you, but if there are others with secadm or other privileges, they could insert.
You can do something with a trigger, something like check the current session, and if the user is your user, then it inserts the row.
if (SESSION_USER <> 'Alex) then
rollback -- or generate an exception
end if;
It seems that you also want to keep just one row, then, you can control that also in a trigger:
select count(0) into value from state
if (value > 1) then
rollback -- or generate an exception
end if;

Unique combination of composite key

I want to create table with composite key, combinations of which must be unique.
For example,
CREATE TABLE [dbo].[TEST3](
[field1][int] NOT NULL,
[field2][int] NOT NULL
PRIMARY KEY (field1,field2)
)
GO
rows:
field1 field2
----------------
1 2
2 1
How to prevent such behavior? I need to have an error when user inserts row (2,1) to the table which already has row (1,2)
You may be able to do that using a user-defined type, but IMHO this sounds like a bad design and possibly an XY problem.
If you are trying to create a many-to-many table (e.g. a table for mutual friends) I would recommend instead using a check constraint to always make sure field1 is strictly less than field2.
You can even take it a step further and require the use of a stored procedure to insert rows, or create an INSTEAD OF INSERT trigger. This would put the row in the right order for you, this way you don't have to rely on the front end knowing which order the columns should be in.
Some insert ( and update ) trigger would be possible solution, i.e.
create trigger prevent
on TEST3
for insert
as
if (select count(1)
from TEST3, inserted
where TEST3.field1=inserted.field2 and TEST3.field2=inserted.field1) > 0
/* Cancel the insert and print a message.*/
begin
rollback transaction
print "Failed."
end
/* Otherwise, allow it. */
else
print "Added!"

Including multiple columns in a single index in Postgres

I have a 'users' table with two columns, 'email' and 'new_email'. I need:
A case-insensitive uniqueness constraint covering both columns - i.e., if "Bob#Example.com" appears in one row's 'email' column, then inserting "bob#example.com" into another row's (or even the same row's) 'new_email' column should fail.
Fast case-insensitive searching for a given email address in either the 'email' or 'new_email' fields - i.e. find the row where the new_email OR email is "Bob#example.com", case-insensitive.
I know that I could do this more easily by creating a related 'emails' table, but I'm expecting to be looking up users in this table (by primary key) from several applications, and I'd like to avoid duplicating the join logic in various places to also retrieve their emails. So I think some kind of expression index would be best, if that's possible.
If this isn't possible, I suppose my next best option would be to create a view that the other applications could use to easily fetch a user's emails along with their other information, but I'm not sure how to do that either.
I'm using Postgres 8.4. Thank you!
I think you'll have to use a trigger to enforce your cross-column uniqueness constraint. If you add unique indexes on each column and then a trigger something like this (untested off the top of my head code):
CREATE FUNCTION no_dups_allowed() RETURNS trigger AS $$
DECLARE
r ROW;
BEGIN
SELECT 1 INTO r
FROM users
WHERE LOWER(email) = LOWER(NEW.email_new)
OR LOWER(email_new) = LOWER(NEW.email);
IF FOUND THEN
-- Found a duplicate so it is time for a hissy fit!
RAISE 'Duplicate email address found' USING ERRCODE = 'unique_violation';
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
You'd want something like that as a BEFORE INSERT and BEFORE UPDATE trigger. That trigger would take care of catching cross-column duplicates and the unique indexes would take care of in-column duplicates.
Some useful references:
FOUND
RAISE
Triggers
Trigger Procedures
You'll want the individual indexes for your queries anyway and using the uniqueness half of the indexes simplifies your trigger by leaving it to only deal with the cross-column part; if you try to do it all in the trigger, then you'll have to watch out for updating a row without really changing the email or email_new columns.
For the querying half, you could create a view that used a UNION to combine the two columns. You could also create a function to merge the user's email addresses into one list. Hard to say which would be best without know more details of these other queries but I suspect that fixing all the other queries to know about email and email_new would be the best approach; you'll have to update all the other queries to use the view or function anyway so why build a view or function at all?
No need for triggers. Try this:
create table et (email text, email2 text);
create unique index et_u on et (coalesce(lower(email),lower(email2)));
insert into et (email,email2) values ('scott#gmail.com',NULL);
insert into et (email,email2) values ('scott#gmail.com',NULL);
ERROR: duplicate key value violates unique constraint "et_u"
insert into et (email,email2) values (NULL,'scott#gmail.com');
ERROR: duplicate key value violates unique constraint "et_u"
insert into et (email,email2) values (NULL,'Scott#gmail.com');
ERROR: duplicate key value violates unique constraint "et_u"

SQL: dealing with unique values in the table when UNIQUE key constraint isn't applicable

What you do when you need to maintain a table with unique values when you can't use UNIQUE constraint?
For example, I use MySQL and want to map my urls to ids. So I create a table:
CREATE TABLE url (id INTEGER PRIMARY KEY AUTO_INCREMENT, url VARCHAR(2048));
The problem is that mysql doesn't allow unique field bigger than 1000 bytes.
How in general do insert only if not exist in sql atomically?
You could create an extra field which would be the hash of a url e.g. md5, and make that hash field unique. You can certainly be sure that the URL is unique then, and with almost 100% certainty you can insert a new URL if it isn't already there.
It is tempting to create a table lock, however creating a table lock will implicitly commit the transaction you are working on: http://www.databasesandlife.com/mysql-lock-tables-does-an-implicit-commit/
You could create a single-row table e.g. name mutex, type=InnoDB, insert a row into it, and do a select for update on that row to create a lock which is compatible with transactions. It's nasty but that's the way I do table locks in MySQL in my applications :(
You could use a not exist condition:
insert YourTable
(url)
values ('blah blah blah')
where not exists
(
select *
from YourTable
where url = 'blah blah blah'
)
In my opinion the best way to handle it is to write a trigger. The trigger is going to check each value in the table to see whether they are equal and if yes, to raise an error. However, I don't think an URL will go beyond 1000 characters but if it does in your case, you should write a trigger to handle the uniqueness.