a trigger to check a value - sql

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.

Related

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.

PostgreSQL - Constraint Based on Column in Another Table

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.

SQL : how to check value used in other table?

CREATE TABLE sectors
(
name varchar,
id integer,
capacity integer
)
CREATE TABLE employees
(
id integer,
for_sector_id integer,
name varchar
)
Here I need to check if entered for_sector_id is whether null or already using in data of sectors. Thought about adding check clause down by create script:
CREATE TABLE employees
(
...
CHECK(for_sector_id is null or for_sector_id in sectors.id)
)
May this solve the problem?
If you just need to check that the reference is valid then you can use a foreign key reference:
CREATE TABLE sectors (
id integer primary key,
name varchar,
capacity integer
);
CREATE TABLE employees (
id integer,
for_sector_id integer,
name varchar(255), -- you want a length in most databases
foreign key for_sector_id references sector(id)
);
The referenced id needs to be a primary or unique key.
EDIT:
I originally misunderstood the question. I thought the sector needed to be "marked as used" in the sector table to be valid (say by having capacity > 0). The OP appears to merely want its existence in the table.
For this situation, I can think of three options if you want a "conditional" foreign key reference (i.e. there is a used flag or say, capacity > 0):
A trigger
A user-defined function for the check constraint
A funky foreign key relationship that uses a computed column
Not all databases support all of these. (As I write this, you haven't specified the database, so I'm only answering the question that you have explicitly asked and not elaborating.)

Correct way to create a table that references variables from another table

I have these relationships:
User(uid:integer,uname:varchar), key is uid
Recipe(rid:integer,content:text), key is rid
Rating(rid:integer, uid:integer, rating:integer) , key is (uid,rid).
I built the table in the following way:
CREATE TABLE User(
uid INTEGER PRIMARY KEY ,
uname VARCHAR NOT NULL
);
CREATE TABLE Recipes(
rid INTEGER PRIMARY KEY,
content VARCHAR NOT NULL
);
Now for the Rating table: I want it to be impossible to insert a uid\rid that does not exist in User\Recipe.
My question is: which of the following is the correct way to do it? Or please suggest the correct way if none of them are correct. Moreover, I would really appreciate if someone could explain to me what is the difference between the two.
First:
CREATE TABLE Rating(
rid INTEGER,
uid INTEGER,
rating INTEGER CHECK (0<=rating and rating<=5) NOT NULL,
PRIMARY KEY(rid,uid),
FOREIGN KEY (rid) REFERENCES Recipes,
FOREIGN KEY (uid) REFERENCES User
);
Second:
CREATE TABLE Rating(
rid INTEGER REFERENCES Recipes,
uid INTEGER REFERENCES User,
rating INTEGER CHECK (0<=rating and rating<=5) NOT NULL,
PRIMARY KEY(rid,uid)
);
EDIT:
I think User is problematic as a name for a table so ignore the name.
Technically both versions are the same in Postgres. The docs for CREATE TABLE say so quite clearly:
There are two ways to define constraints: table constraints and column constraints. A column constraint is defined as part of a column definition. A table constraint definition is not tied to a particular column, and it can encompass more than one column. Every column constraint can also be written as a table constraint; a column constraint is only a notational convenience for use when the constraint only affects one column.
So when you have to reference a compound key a table constraint is the only way to go.
But for every other case I prefer the shortest and most concise form where I don't need to give names to stuff I'm not really interested in. So my version would be like this:
CREATE TABLE usr(
uid SERIAL PRIMARY KEY ,
uname TEXT NOT NULL
);
CREATE TABLE recipes(
rid SERIAL PRIMARY KEY,
content TEXT NOT NULL
);
CREATE TABLE rating(
rid INTEGER REFERENCES recipes,
uid INTEGER REFERENCES usr,
rating INTEGER NOT NULL CHECK (rating between 0 and 5),
PRIMARY KEY(rid,uid)
);
This is a SQL Server based solution, but the concept applies to most any RDBMS.
Like so:
CREATE TABLE Rating (
rid int NOT NULL,
uid int NOT NULL,
CONSTRAINT PK_Rating PRIMARY KEY (rid, uid)
);
ALTER TABLE Rating ADD CONSTRAINT FK_Rating_Recipies FOREIGN KEY(rid)
REFERENCES Recipies (rid);
ALTER TABLE Rating ADD CONSTRAINT FK_Rating_User FOREIGN KEY(uid)
REFERENCES User (uid);
This ensures that the values inside of Rating are only valid values inside of both the Users table and the Recipes table. Please note, in the Rating table I didn't include the other fields you had, just add those.
Assume in the users table you have 3 users: Joe, Bob and Bill respective ID's 1,2,3. And in the recipes table you had cookies, chicken pot pie, and pumpkin pie respective ID's are 1,2,3. Then inserting into Rating table will only allow for these values, the minute you enter 4 for a RID or a UID SQL throws an error and does not commit the transaction.
Try it yourself, its a good learning experience.
In Postgresql a correct way to implement these tables are:
CREATE SEQUENCE uid_seq;
CREATE SEQUENCE rid_seq;
CREATE TABLE User(
uid INTEGER PRIMARY KEY DEFAULT nextval('uid_seq'),
uname VARCHAR NOT NULL
);
CREATE TABLE Recipes(
rid INTEGER PRIMARY KEY DEFAULT nextval('rid_seq'),
content VARCHAR NOT NULL
);
CREATE TABLE Rating(
rid INTEGER NOT NULL REFERENCES Recipes(rid),
uid INTEGER NOT NULL REFERENCES User(uid),
rating INTEGER CHECK (0<=rating and rating<=5) NOT NULL,
PRIMARY KEY(rid,uid)
);
There is no real difference between the two options that you have written.
A simple (i.e. single-column) foreign key may be declared in-line with the column declaration or not. It's merely a question of style. A third way should be to omit foreign key declarations from the CREATE TABLE entirely and later add them using ALTER TABLE statements; done in a transaction (presumable along with all the other tables, constraints, etc) the table would never exist without its required constraints. Choose whichever you think is easiest fora human coder to read and understand i.e. is easiest to maintain.
EDIT: I overlooked the REFERENCES clause in the second version when I wrote my original answer. The two versions are identical in terms of referential integrity, there are just two ways of syntax to do this.

Constraint To Prevent Adding Value Which Exists In Another Table

I would like to add a constraint which prevents adding a value to a column if the value exists in the primary key column of another table. Is this possible?
EDIT:
Table: MasterParts
MasterPartNumber (Primary Key)
Description
....
Table: AlternateParts
MasterPartNumber (Composite Primary Key, Foreign Key to MasterParts.MasterPartNumber)
AlternatePartNumber (Composite Primary Key)
Problem - Alternate part numbers for each master part number must not themselves exist in the master parts table.
EDIT 2:
Here is an example:
MasterParts
MasterPartNumber Decription MinLevel MaxLevel ReOderLevel
010-00820-50 Garmin GTN™ 750 1 5 2
AlternateParts
MasterPartNumber AlternatePartNumber
010-00820-50 0100082050
010-00820-50 GTN750
only way I could think of solving this would be writing a checking function(not sure what language you are working with), or trying to play around with table relationships to ensure that it's unique
Why not have a single "part" table with an "is master part" flag and then have an "alternate parts" table that maps a "master" part to one or more "alternate" parts?
Here's one way to do it without procedural code. I've deliberately left out ON UPDATE CASCADE and ON DELETE CASCADE, but in production I'd might use both. (But I'd severely limit who's allowed to update and delete part numbers.)
-- New tables
create table part_numbers (
pn varchar(50) primary key,
pn_type char(1) not null check (pn_type in ('m', 'a')),
unique (pn, pn_type)
);
create table part_numbers_master (
pn varchar(50) primary key,
pn_type char(1) not null default 'm' check (pn_type = 'm'),
description varchar(100) not null,
foreign key (pn, pn_type) references part_numbers (pn, pn_type)
);
create table part_numbers_alternate (
pn varchar(50) primary key,
pn_type char(1) not null default 'a' check (pn_type = 'a'),
foreign key (pn, pn_type) references part_numbers (pn, pn_type)
);
-- Now, your tables.
create table masterparts (
master_part_number varchar(50) primary key references part_numbers_master,
min_level integer not null default 0 check (min_level >= 0),
max_level integer not null default 0 check (max_level >= min_level),
reorder_level integer not null default 0
check ((reorder_level < max_level) and (reorder_level >= min_level))
);
create table alternateparts (
master_part_number varchar(50) not null references part_numbers_master (pn),
alternate_part_number varchar(50) not null references part_numbers_alternate (pn),
primary key (master_part_number, alternate_part_number)
);
-- Some test data
insert into part_numbers values
('010-00820-50', 'm'),
('0100082050', 'a'),
('GTN750', 'a');
insert into part_numbers_master values
('010-00820-50', 'm', 'Garmin GTN™ 750');
insert into part_numbers_alternate (pn) values
('0100082050'),
('GTN750');
insert into masterparts values
('010-00820-50', 1, 5, 2);
insert into alternateparts values
('010-00820-50', '0100082050'),
('010-00820-50', 'GTN750');
In practice, I'd build updatable views for master parts and for alternate parts, and I'd limit client access to the views. The updatable views would be responsible for managing inserts, updates, and deletes. (Depending on your company's policies, you might use stored procedures instead of updatable views.)
Your design is perfect.
But SQL isn't very helpful when you try to implement such a design. There is no declarative way in SQL to enforce your business rule. You'll have to write two triggers, one for inserts into masterparts, checking the new masterpart identifier doesn't yet exist as an alias, and the other one for inserts of aliases checking that the new alias identifier doesn't yet identiy a masterpart.
Or you can do this in the application, which is worse than triggers, from the data integrity point of view.
(If you want to read up on how to enforce constraints of arbitrary complexity within an SQL engine, best coverage I have seen of the topic is in the book "Applied Mathematics for Database Professionals")
Apart that it sounds like a possibly poor design,
You in essence want values spanning two columns in different tables, to be unique.
In order to utilize DBs native capability to check for uniqueness, you can create a 3rd, helper column, which will contain a copy of all the values inside the wanted two columns. And that column will have uniqueness constraint. So for each new value added to one of your target columns, you need to add the same value to the helper column. In order for this to be an inner DB constraint, you can add this by a trigger.
And again, needing to do the above, sounds like an evidence for a poor design.
--
Edit:
Regarding your edit:
You say " Alternate part numbers for each master part number must not themselves exist in the master parts table."
This itself is a design decision, which you don't explain.
I don't know enough about the domain of your problem, but:
If you think of master and alternate parts, as totally different things, there is no reason why you may want "Alternate part numbers for each master part number must not themselves exist in the master parts table". Otherwise, you have a common notion of "parts" be it master or alternate. This means they need to be in the same table, and column.
If the second is true, you need something like this:
table "parts"
columns:
id - pk
is_master - boolean (assuming a part can not be master and alternate at the same time)
description - text
This tables role is to list and describe the parts.
Then you have several ways to denote which part is alternate to which. It depends on whether a part can be alternate to more than one part. And it sounds that anyway one master part can have several alternates.
You can do it in the same table, or create another one.
If same: add column: alternate_to, which will be null for master parts, and will have a foreign key into the id column of the same table.
Otherwise create a table, say "alternatives" with: master_id, alternate_id both referencing with a foreign key to the parts table.
(The first above assumes that a part cannot be alternate to more than one other part. If this is not true, the second will work anyway)