PostgreSQL - Constraint Based on Column in Another Table - sql

I have two tables, one called ballots and one called votes. Ballots stores a list of strings representing options that people can vote for:
CREATE TABLE IF NOT EXISTS Polls (
id SERIAL PRIMARY KEY,
options text[]
);
Votes stores votes that users have made (where a vote is an integer representing the index of the option they voted for):
CREATE TABLE IF NOT EXISTS Votes (
id SERIAL PRIMARY KEY,
poll_id integer references Polls(id),
value integer NOT NULL ,
cast_by integer NOT NULL
);
I want to ensure that whenever a row is created in the Votes table, the value of 'value' is in the range [0,length(options)) for the corresponding row in Polls (by corresponding, I mean the row where the poll_id's match).
Is there any kind of check or foreign key constraint I can implement to accomplish this? Or do I need some kind of trigger? If so, what would that trigger look like and would there be performance concerns? Would it be just as performant to just manually query for the corresponding poll using a SELECT statement and then assert that 'value' is valid before inserting into Votes table?

In your requirement you can not use Check Constraint because it can refer the column of the same table.
You can refer the Official Manual for the same.
So, here you should use Trigger on BEFORE INSERT event of your Votes Table or you can use function/procedure(depend upon your version of PostgreSQL) for your insert operation where you can check the value before insert and raise exception if the condition not satisfied.
USING Trigger:
create or replace function id_exist() returns trigger as
$$
begin
if new.value<0 or new.value>=(select array_length(options,1) from polls where id=new.poll_id) then
raise exception 'value is not in range';
end if;
return new;
end;
$$
language plpgsql
CREATE TRIGGER check_value BEFORE INSERT ON votes
FOR EACH ROW EXECUTE PROCEDURE id_exist();
DEMO

I would suggest that you modify your data model to have a table, PollOptions:
CREATE TABLE IF NOT EXISTS PollOptions (
PollOptionsId SERIAL PRIMARY KEY, -- should use generated always as identity
PollId INT NOT NULL, REFERENCES Polls(id),
OptionNumber int,
Option text,
UNIQUE (PollId, Option)
);
Then your Votes table should have a foreign key reference to PollOptions. You can use either PollOptionId or (PollId, Option).
No triggers or special functions are needed if you set up the data correctly.

Related

how to set a constraint that sets null on only one of the fields in the composite foreign key when the parent record is deleted?

I have 2 postgres tables.
table one:
|id|user_id|master_team_id|role_id
table two:
|uuid|user_id|master_team_id|number
master_team_id in table two can be null.
user_id and master_id foreign key references the user_id and
master_team_id in table one.
in order for master_team_id in table two to not be null, the user_id
and master_team_id combo must exist in table one.
how do i add a constraint that sets null on only master_team_id in the composite key(user_id, master_team_id) in table two when the referenced row in table one is deleted?
in the FK constraint specify ON DELETE SET NULL
https://www.techonthenet.com/sql_server/foreign_keys/foreign_delete_null.php
Side Note: I would suggest using adding a new column to table two called "TableOneID" that way you can know if the matching record exists or not.
You can't do that yet.
What a coincidence. The ability to do that was committed yesterday, and (if all goes according to plan) will be included in v15, which is not due out for nearly a year.
You can use a trigger, which would look like this:
create table p (c1 int, c2 int, unique (c1,c2));
create table c (c1 int, c2 int, r double precision);
alter table c add constraint fk99999 foreign key (c1,c2) references p (c1,c2);
create function update_c() returns trigger language plpgsql as $$
BEGIN
update c set c2=null where c1=old.c1 and c2=old.c2;
return old;
END; $$;
create trigger sdjflsfd before delete ON p for each row execute function update_c();

How to create and normalise this SQL relational database?

I am trying to design an SQL lite database. I have made a quick design on paper of what I want.
So far, I have one table with the following columns:
user_id (which is unique and the primary key of the table)
number_of_items_allowed (can be any number and does not have to be
unique)
list_of_items (a list of any size as long as it is less than or equal
to number_of_items_allowed and this list stores item IDs)
The column I am struggling the most with is list_of_items. I know that a relational database does not have a column which allows lists and you must create a second table with that information to normalise the database. I have looked at a few stack overflow answers including this one, which says that you can't have lists stored in a column, but I was not able to apply the accepted answer to my case.
I have thought about having a secondary table which would have a row for each item ID belonging to a user_id and the primary key in that case would have been the combination of the item ID and the user_id, however, I was not sure if that would be the ideal way of going about it.
Consider the following schema with 3 tables:
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
user TEXT NOT NULL,
number_of_items_allowed INTEGER NOT NULL CHECK(number_of_items_allowed >= 0)
);
CREATE TABLE items (
item_id INTEGER PRIMARY KEY,
item TEXT NOT NULL
);
CREATE TABLE users_items (
user_id INTEGER NOT NULL REFERENCES users(user_id) ON UPDATE CASCADE ON DELETE CASCADE,
item_id INTEGER NOT NULL REFERENCES items (item_id) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY(user_id, item_id)
);
For this schema, you need a BEFORE INSERT trigger on users_items which checks if a new item can be inserted for a user by comparing the user's number_of_items_allowed to the current number of items that the user has:
CREATE TRIGGER check_number_before_insert_users_items
BEFORE INSERT ON users_items
BEGIN
SELECT
CASE
WHEN (SELECT COUNT(*) FROM users_items WHERE user_id = NEW.user_id) >=
(SELECT number_of_items_allowed FROM users WHERE user_id = NEW.user_id)
THEN RAISE (ABORT, 'No more items allowed')
END;
END;
You will need another trigger that will check when number_of_items_allowed is updated if the new value is less than the current number of the items of this user:
CREATE TRIGGER check_number_before_update_users
BEFORE UPDATE ON users
BEGIN
SELECT
CASE
WHEN (SELECT COUNT(*) FROM users_items WHERE user_id = NEW.user_id) > NEW.number_of_items_allowed
THEN RAISE (ABORT, 'There are already more items for this user than the value inserted')
END;
END;
See the demo.

Constraint on a group of rows

For a simple example, let's say I have a list table and a list_entry table:
CREATE TABLE list
(
id SERIAL PRIMARY KEY,
);
CREATE TABLE list_entry
(
id SERIAL PRIMARY KEY,
list_id INTEGER NOT NULL
REFERENCES list(id)
ON DELETE CASCADE,
position INTEGER NOT NULL,
value TEXT NOT NULL,
CONSTRAINT list_entry__position_in_list_unique
UNIQUE(list_id, position)
);
I now want to add the following constraint: all list entries with the same list_id have position entries that form a contiguous sequence starting at 1.
And I have no idea how.
I first thought about EXCLUDE constraints, but that seems to lead nowhere.
Could of course create a trigger, but I'd prefer not to, if at all possible.
You can't do that with a constraint - you would need to implement the logic in code (e.g. using triggers, stored procedures, application code, etc.)
I'm not aware of such way to use constraints. Normally a trigger would be the most straightforward choice, but in case you want to avoid using them, try to get the current position number for the list_entry with the list_id you're about to insert, e.g. inserting a list_entry with list_id = 1:
INSERT INTO list_entry (list_id,position,value) VALUES
(1,(SELECT coalesce(max(position),0)+1 FROM list_entry WHERE list_id = 1),42);
Demo: db<>fiddle
You can use a generated column to reference the previous number in the list, essentially building a linked list. This works in Postgres:
create table list_entry
(
pos integer not null primary key,
val text not null,
prev_pos integer not null
references list_entry (pos)
generated always as (greatest(0, pos-1)) stored
);
In this implementation, the first item (pos=0) points to itself.

Unique index by another column for postgresql

For example, I have this table (postgresql):
CREATE TABLE t(
a TEXT,
b TEXT
);
CREATE UNIQUE INDEX t_a_uniq_idx ON t(a);
I want to create unique constraint/index for b and a columns. But not simple ADD CONSTRAINT t_ab UNIQUE (a, b). I want unique b by a:
INSERT INTO t(a,b) VALUES('123', null); -- this is ok
INSERT INTO t(a,b) VALUES('456', '123'); -- this is not ok, because duplicate '123'
How can I do this?
Edit:
Why do I need this? For example, If I have users table and I want to create email changing feature, I need structure like this:
CREATE TABLE users(
email TEXT,
unconfirmed_email TEXT
-- some other data
);
CREATE UNIQUE INDEX unq_users_email_idx ON users(email);
User can set value into unconfirmed_email column, but only if this value don't used in email column.
If uniqueness is needed across both columns, I think you have the wrong data model. Instead of storing pairs on a single row, you should have two tables:
create table pairs (
pairid int generated always as identity,
. . . -- more information about the pair, if needed
);
create table pairElements (
pairElementId int generated always as identity,
pairId int references pairs(pairid),
which int check (which in (1, 2)),
value text,
unique (pairid, which)
);
Then the condition is simple:
create constraint unq_pairelements_value unique pairelements(value);
While this leads to an interesting problem, I agree that the data could be modelled better - specifically, the column unconfirmed_email can be seen as combining two attributes: an association between an address and a user, which it shares with the email column; and a status of whether that address is confirmed, which is dependant on the combination of user and address, not on one or the other.
This implies that a new table should be extracted of user_email_addresses:
user_id - foreign key to users
email - non-nullable
is_confirmed boolean
Interestingly, as often turns out to be the case, this extracted table has natural data that could be added:
When was the address added?
When was it confirmed?
What is the verification code sent to the user?
If the user is allowed multiple addresses, which is the primary, or which is to be used for a particular purpose?
We can now model various constraints on this table (using unique indexes in some cases because you can't specify Where on a unique constraint):
Each user can only have one association (whether confirmed or not) with a particular e-mail address: Constraint Unique ( user_id, email )
An e-mail address can only be confirmed for one user: Unique Index On user_emails ( email ) Where is_confirmed Is True;
Each user can have only one confirmed address: Unique Index On user_emails ( user_id ) Where is_confirmed Is True;. You might want to adjust this to allow users to confirm multiple addresses, but have a single "primary" address.
Each user can have only one unconfirmed address: Unique Index On user_emails ( user_id ) Where is_confirmed Is False;. This is implied in your current design, but may not actually be necessary.
This leaves us with a re-worded version of your original problem: how do we forbid unconfirmed rows with the same email as a confirmed row, but allow multiple identical unconfirmed rows.
One approach would be to use an Exclude constraint for rows where email matches, but is_confirmed doesn't match. The cast to int is necessary because creating the gist index on boolean fails.
Alter Table user_emails
Add Constraint unconfirmed_must_not_match_confirmed
Exclude Using gist (
email With =,
Cast(is_confirmed as Int) With <>
);
On its own, this would allow multiple copies of email, as long as they all had the same value of is_confirmed. But since we've already constrained against multiple rows where is_confirmed Is True, the only duplicates remaining will be where is_confirmed Is False on all matching rows.
Here is a db<>fiddle demonstrating the above design: https://dbfiddle.uk/?rdbms=postgres_12&fiddle=fd8e4e6a4cce79d9bc6bf07111e68df9
As I understand it, you want a UNIQUE index over a and b combined.
You update narrowed it down (b shall not exist in a). This solution is stricter.
Solution
After trying and investigating quite a bit (see below!) I came up with this:
ALTER TABLE tbl ADD CONSTRAINT a_not_equal_b CHECK (a <> b);
ALTER TABLE tbl ADD CONSTRAINT ab_unique
EXCLUDE USING gist ((ARRAY[hashtext(COALESCE(a, ''))
, hashtext(COALESCE(b, ''))]) gist__int_ops WITH &&);
db<>fiddle here
Since the exclusion constraint won't currently (pg 12) work with text[], I work with int4[] of hash values. hashtext() is the built-in hash function that's also used for hash-partitioning (among other uses). Seems perfect for the job.
The operator class gist__int_ops is provided by the additional module intarray, which has to be installed once per database. Its optional, the solution works with the default array operator class as well. Just drop gist__int_ops to fall back. But intarray is faster. Related:
How to create an index for elements of an array in PostgreSQL?
Caveats
int4 may not be big enough to rule out hash collisions sufficiently. You might want to go with bigint instead. But that's more expensive and can't use the gist__int_ops operator class to improve performance. Your call.
Unicode has the dismal property that equal strings can be encoded in different ways. If you work with Unicode (typical encoding UTF8) and use non-ASCII characters (and this matters to you), compare normalized forms to rule out such duplicates. The upcoming Postgres 13 adds the function normalize() for that purpose. This is a general caveat of character type duplicates, though, not specific to my solution.
NULL value is allowed, but collides with empty string (''). I would rather go with NOT NULL columns and drop COALESCE() from the expressions.
Obstacle course to an exclusion constraint
My first thought was: exclusion constraint. But it falls through:
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gist ((ARRAY[a,b]) WITH &&);
ERROR: data type text[] has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
There is an open TODO item for this. Related:
ERROR: data type text[] has no default operator class for access method “gist”
But can't we use a GIN index for text[]? Alas, no:
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gin ((ARRAY[a,b]) WITH &&);
ERROR: access method "gin" does not support exclusion constraints
Why? The manual:
The access method must support amgettuple (see Chapter 61); at present this means GIN cannot be used.
It seems hard to implement, so don't hold your breath.
If a and b were integer columns, we could make it work with an integer array:
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gist ((ARRAY[a,b]) WITH &&);
Or with the gist__int_ops operator class from the additional module intarray (typically faster):
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gist ((ARRAY[a,b]) gist__int_ops WITH &&);
To also forbid duplicates within the same row, add a CHECK constraint:
ALTER TABLE tbl ADD CONSTRAINT a_not_equal_b CHECK (a <> b);
Remaining issue: Does work with NULL values.
Workaround
Add a helper table to store values from a and b in one column:
CREATE TABLE tbl_ab(ab text PRIMARY KEY);
Main table, like you had it, plus FK constraints.
CREATE TABLE tbl (
a text REFERENCES tbl_ab ON UPDATE CASCADE ON DELETE CASCADE
, b text REFERENCES tbl_ab ON UPDATE CASCADE ON DELETE CASCADE
);
Use a function like this to INSERT:
CREATE OR REPLACE FUNCTION f_tbl_insert(_a text, _b text)
RETURNS void
LANGUAGE sql AS
$func$
WITH ins_ab AS (
INSERT INTO tbl_ab(ab)
SELECT _a WHERE _a IS NOT NULL -- NULL is allowed (?)
UNION ALL
SELECT _b WHERE _b IS NOT NULL
)
INSERT INTO tbl(a,b)
VALUES (_a, _b);
$func$;
db<>fiddle here
Or implement a trigger to take care of it in the background.
CREATE OR REPLACE FUNCTION trg_tbl_insbef()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO tbl_ab(ab)
SELECT NEW.a WHERE NEW.a IS NOT NULL -- NULL is allowed (?)
UNION ALL
SELECT NEW.b WHERE NEW.b IS NOT NULL;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER tbl_insbef
BEFORE INSERT ON tbl
FOR EACH ROW EXECUTE PROCEDURE trg_tbl_insbef();
db<>fiddle here
NULL handling can be changed as desired.
Either way, while the added (optional) FK constraints enforce that we can't sidestep the helper table tbl_ab, and allow to UPDATE and DELETE in tbl_ab to cascade, you still need to project UPDATE and DELETE into the helper table as well (or implement more triggers). Tricky corner cases, but there are solutions. Not going into this, after I found the solution above with an exclusion constraint using hashtext() ...
Related:
Can PostgreSQL have a uniqueness constraint on array elements?

a trigger to check a value

I have two tables :
create table building(
id integer,
name varchar(15),
rooms_num integer,
primary key(id)
);
create table room(
id integer,
building_id integer,
primary key(id),
foreign key(building_id) references building(id)
);
as you see, there is a rooms_num field in the building table which shows the number of rooms in that building and a building_id in the room table which shows that room's building.
All I want is that when I insert a value into the room table , the database check itself and see if the number of room is not out of bound.
is it not better to code it with a trigger?
i have tried this but i dont know what to put in the condition part :
CREATE TRIGGER onRoom
ON room
BEFORE INSERT
????
First you should tighten up your data model. Everything that should be present should be marked NOT NULL such as the building's name. I tend to like making sure required text fields have actual values in them, so I put a check constraint in that matches at least one "word" character (alphanumeric).
You should not ever be allowing negative room numbers, right? In addition, you should avoid using private database information like a room number or building number as a primary key. It should be considered a candidate key only. Best practice in my opinion would be to use UUIDs for primary keys, but some people love their auto-incrementing integers, so I won't push here. The point is that rooms tend to (for example) get drywall put up separating them or taken down to make bigger spaces. Switching around primary key IDs can have unexpected results. Better to separate how its identified within the database from the data itself so you can add "Room 6A".
It should also be a safe bet that you won't have more than 32,767 rooms per building, so int2 (16-bit, 2-byte integer) would work here.
CREATE TABLE building (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name varchar(15) NOT NULL UNIQUE CHECK (name ~ '\w'),
rooms_num int2 NOT NULL CHECK (rooms_num >= 0)
);
CREATE TABLE room (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- Should there be a room name too?
room_id int2 NOT NULL CHECK (room_id > 0),
building_id integer NOT NULL REFERENCES building (id),
UNIQUE (room_id, building_id)
);
Okay, now you have a more solid foundation to work from. Let's make the trigger.
CREATE FUNCTION room_in_building ()
RETURNS TRIGGER LANGUAGE plpgsql STRICT STABLE AS $$
BEGIN
IF (
-- We can safely just check upper bounds because the check constraint on
-- the table prevents zero or negative values.
SELECT b.rooms_num >= NEW.room_id
FROM building AS b
WHERE b.id = NEW.building_id
) THEN
RETURN NEW; -- Everything looks good
ELSE
RAISE EXCEPTION 'Room number (%) out of bounds', NEW.id;
END IF;
END;
$$;
CREATE TRIGGER valid_room_number
BEFORE INSERT OR UPDATE -- Updates shouldn't break the data model either
ON room
FOR EACH ROW EXECUTE PROCEDURE room_in_building();
You should also add an update trigger for the building table. If someone were to update the rooms_num column to a smaller number, you could end up with an inconsistent data model.