PSQL Constraint based on column value - sql

Is it possible to have a Constraint but only when one column is set to a particular value. For example take this pseudo-code example of a President which checks to make sure there is never more than 1 President at any time (note, this is NOT valid psql syntax)
CREATE TABLE president (
id BIGSERIAL PRIMARY KEY,
current BOOLEAN NOT NULL,
CONSTRAINT there_can_be_only_one CHECK(COUNT(current=true)<=1)
);

You can use the so called partial index to enforce this specific constraint. In SQL Server they are called filtered indexes.
CREATE UNIQUE INDEX IX ON president (current)
WHERE current = true;
This index should prevent having more than one row in a table with current value set to true, because it is defined as unique.

Unfortunately NO as far as I know and anyway it already tells us,
ERROR: aggregate functions are not allowed in check constraints.
But we can use BEFORE trigger to check that the data you are trying to insert should meets the criteria COUNT(current=true)<=1
CREATE TABLE president (
id BIGSERIAL PRIMARY KEY,
current BOOLEAN NOT NULL
);
---------------------------------------------------------------------
CREATE FUNCTION check_current_flag()
RETURNS trigger
AS $current_president$
DECLARE
current_flag_count integer;
BEGIN
SELECT COUNT(*) FILTER (WHERE current = true )
INTO current_flag_count
FROM president;
IF new.current = true
and current_flag_count >= 1 THEN
RAISE EXCEPTION 'There can be only one current president';
-- RETURN NULL;
ELSE
RETURN NEW;
END IF;
END;
$current_president$ LANGUAGE plpgsql;
---------------------------------------------------------------------
CREATE TRIGGER current_president BEFORE INSERT OR UPDATE ON president
FOR EACH ROW EXECUTE PROCEDURE check_current_flag();
Db<>Fiddle for reference
Note:
You can either throw exception in case of preconditions doesn't match ore simply returning NULL will skip the insert and do nothing. as official document says also here

Related

Is it possible to create a cross relationship constraint in postgresql? [duplicate]

I would like to add a constraint that will check values from related table.
I have 3 tables:
CREATE TABLE somethink_usr_rel (
user_id BIGINT NOT NULL,
stomethink_id BIGINT NOT NULL
);
CREATE TABLE usr (
id BIGINT NOT NULL,
role_id BIGINT NOT NULL
);
CREATE TABLE role (
id BIGINT NOT NULL,
type BIGINT NOT NULL
);
(If you want me to put constraint with FK let me know.)
I want to add a constraint to somethink_usr_rel that checks type in role ("two tables away"), e.g.:
ALTER TABLE somethink_usr_rel
ADD CONSTRAINT CH_sm_usr_type_check
CHECK (usr.role.type = 'SOME_ENUM');
I tried to do this with JOINs but didn't succeed. Any idea how to achieve it?
CHECK constraints cannot currently reference other tables. The manual:
Currently, CHECK expressions cannot contain subqueries nor refer to
variables other than columns of the current row.
One way is to use a trigger like demonstrated by #Wolph.
A clean solution without triggers: add redundant columns and include them in FOREIGN KEY constraints, which are the first choice to enforce referential integrity. Related answer on dba.SE with detailed instructions:
Enforcing constraints “two tables away”
Another option would be to "fake" an IMMUTABLE function doing the check and use that in a CHECK constraint. Postgres will allow this, but be aware of possible caveats. Best make that a NOT VALID constraint. See:
Disable all constraints and table checks while restoring a dump
A CHECK constraint is not an option if you need joins. You can create a trigger which raises an error instead.
Have a look at this example: http://www.postgresql.org/docs/9.1/static/plpgsql-trigger.html#PLPGSQL-TRIGGER-EXAMPLE
CREATE TABLE emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Check that empname and salary are given
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
END IF;
-- Who works for us when she must pay for it?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- Remember who changed the payroll when
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
...i did it so (nazwa=user name, firma = company name) :
CREATE TABLE users
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
nazwa character varying(20),
firma character varying(50)
);
CREATE TABLE test
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
firma character varying(50),
towar character varying(20),
nazwisko character varying(20)
);
ALTER TABLE public.test ENABLE ROW LEVEL SECURITY;
CREATE OR REPLACE FUNCTION whoIAM3() RETURNS varchar(50) as $$
declare
result varchar(50);
BEGIN
select into result users.firma from users where users.nazwa = current_user;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE POLICY user_policy ON public.test
USING (firma = whoIAM3());
CREATE FUNCTION test_trigger_function()
RETURNS trigger AS $$
BEGIN
NEW.firma:=whoIam3();
return NEW;
END
$$ LANGUAGE 'plpgsql'
CREATE TRIGGER test_trigger_insert BEFORE INSERT ON test FOR EACH ROW EXECUTE PROCEDURE test_trigger_function();

How to create a conditional trigger

I have a table with an id as auto incremented primary key and another id.
CREATE TABLE tester (
"id" integer PRIMARY KEY AUTOINCREMENT,
"refId" integer DEFAULT 0
);
refId should be able to either be 0 (the default) or reference id if refId > 0 (i.e. act as foreign key).
Now I need two constraints:
A row should only be deletable if its id is not used (referenced?) by any other row's refId
A row should only be deletable if its refId is 0.
From what I have understood, I need to create a trigger that checks for these constraints before a DELETE event happens. And depending on refId's value either abort the delete action or allow it.
However, I have a hard time understanding the syntax for this and how to do a conditional check. But what I have so far (in mind!) is concerning 1.):
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE "refId" = OLD."id") IS NOT NULL;
END;
And concerning 2.)
CREATE TRIGGER no_delete_if_ref
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
IF OLD."refId" > 0 THEN RAISE(ABORT, "cannot delete tester because it refers to an existing tester");
END;
Does this make sense and is valid?
I am totally not sure, to me it does but well, I am all noob.
Also as a last question, can I alternatively combine this into a single trigger? For example would this be a valid query:
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE ("refId" = OLD."id" OR "refId" > 0) ) IS NOT NULL;
END;
You can define a foreign key referring to the same table. Use null instead of 0 for rows without a reference:
create table tester(
id int primary key,
refid int references tester,
check (id <> refid)
);
insert into tester values
(1, null),
(2, null),
(3, 1),
(4, 3);
You need a trigger to ensure that a row which references another one cannot be deleted.
create or replace function before_delete_on_tester()
returns trigger language plpgsql as $$
begin
if old.refid is not null then
raise exception
'Cannot delete: (id)=(%) references (id)=(%)', old.id, old.refid;
end if;
return old;
end $$;
create trigger before_delete_on_tester
before delete on tester
for row execute procedure before_delete_on_tester();
Test:
delete from tester where id = 1;
ERROR: update or delete on table "tester" violates foreign key constraint "tester_refid_fkey" on table "tester"
DETAIL: Key (id)=(1) is still referenced from table "tester".
delete from tester where id = 4;
ERROR: Cannot delete from tester. (id)=(4) references (id)=(3)
CONTEXT: PL/pgSQL function before_delete_on_tester() line 4 at RAISE
In Postgres you have to define a trigger function. Read more:
Overview of Trigger Behavior
Trigger Procedures
Create Trigger

How to avoid "blank" insert when inserting data into a view with a trigger & insert procedure?

I'm trying to update tables from insert or update call on a PostgreSQL view. Here's a simplified example of what I do:
[Person] table:
id | lastname | firstname | city | age
[Person_View] table:
id | lastname | firstname | city
Here is the trigger and the related procedure :
CREATE TRIGGER tg_update_person_view
INSTEAD OF INSERT OR UPDATE OR DELETE ON
Person_View FOR EACH ROW EXECUTE PROCEDURE update_person_view_table();
CREATE OR REPLACE FUNCTION update_person_view_table()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $function$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO Person (id, lastname, firstname)
VALUES(NEW.id, NEW.lastname, NEW.firstname);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
UPDATE Person
SET id=NEW.id, lastname=NEW.lastname, firstname=NEW.firstname
WHERE id=OLD.id;
RETURN NEW;
END IF;
RETURN NEW;
END;
$function$;
If I do:
INSERT INTO Person_View (id, city) VALUES ('3', 'Berlin')
A row with only the ID is added to the view and the parent table.
How can I check in the procedure that columns in which values are being inserted have a "mapping" defined in the procedure and if there ain't any mapped columns, it does not proceed ?
You can define a check constraint on the table, e.g.:
create table person(
id int primary key,
lastname text,
firstname text,
city text,
age int,
check(coalesce(lastname, firstname, city, age::text) is not null)
);
insert into person (id)
values (1);
ERROR: new row for relation "person" violates check constraint "person_check"
DETAIL: Failing row contains (1, null, null, null, null).
The solution works regardless whether any views based on the table were created or not.
Have a separate trigger & trigger function for ON DELETE to simplify. (You are not doing anything ON DELETE anyway?)
A CHECK constraint like klin suggested seems like a good idea. You don't need COALESCE and casting, though. Check a row value for NULL.
CHECK (NOT ROW(lastname, firstname) IS NULL) -- ROW keyword is noise
This enforces at least one notnull value in the row. Works for any number of columns and any data type.
Note in particular that ROW(lastname, firstname) IS NOT NULL is not the same and would not work. Detailed explanation:
NOT NULL constraint over a set of columns
If the CHECK constraint is not an option, you can use the same expression in a trigger - which should be faster than adding it to the trigger function. The manual on CREATE TRIGGER:
Also, a trigger definition can specify a Boolean WHEN condition, which
will be tested to see whether the trigger should be fired. In
row-level triggers the WHEN condition can examine the old and/or new
values of columns of the row.
CREATE TRIGGER tg_update_person_view
INSTEAD OF INSERT OR UPDATE ON Person_View
FOR EACH ROW
WHEN (NOT (NEW.lastname, NEW.firstname) IS NULL) -- more columns?
EXECUTE PROCEDURE update_person_view_table();
If the WHEN expression does not evaluate to TRUE, the trigger function is not even called - so it does not proceed like requested.
However, I missed your trigger INSTEAD OF. The manual:
INSTEAD OF triggers do not support WHEN conditions.
In this case you have to move the check into the function body:
IF NOT (NEW.lastname, NEW.firstname) IS NULL THEN
-- do stuff
END IF;

How to delete/update records inside trigger based on the updated/deleted row?

I have a table with the following format
id | name | supervisor_id
I made a "BEFORE INSERT" trigger that checks if the supervisor_id exists in the id column and if not, then assign a null value to the supervisor_id.
I am trying to write two more triggers. One that checks if the supervisor_id exists in the id column before each update of the supervisor_id, and one that sets the supervisor_id to NULL for each employee if his supervisor is deleted.
This is my code, of course it's not working, help please.
CREATE OR REPLACE TRIGGER EAP_users_TRG3
AFTER DELETE
ON EAP_users
FOR EACH ROW
DECLARE
d NUMBER;
BEGIN
SELECT id INTO d FROM EAP_users WHERE id = :OLD.id;
UPDATE EAP_users SET supervisor = NULL WHERE supervisor = d;
END;
/
This is the "working" trigger:
CREATE OR REPLACE TRIGGER EAP_users_TRG1
BEFORE INSERT
ON EAP_users
FOR EACH ROW
DECLARE
supervisor EAP_users.supervisor%TYPE;
CURSOR supervisor_CUR IS SELECT idFROM EAP_users;
b BOOLEAN := FALSE;
BEGIN
IF ( :NEW.supervisor IS NOT NULL ) THEN
FOR s IN supervisor_CUR LOOP
IF ( :NEW.supervisor = s.id ) THEN
b := TRUE;
END IF;
END LOOP;
IF (b = FALSE) THEN
:NEW.supervisor := NULL;
END IF;
END IF;
END;
/
According to the definition of your problem, you are trying to enforce referential integrity of your data. In that case, a trigger is probably not the right tool. To quote Oracle's documentation:
You can use both triggers and integrity constraints to define and enforce any type of integrity rule. However, Oracle strongly recommends that you use triggers to constrain data input only in the following situations:
[...]
When a required referential integrity rule cannot be enforced using the following integrity constraints:
NOT NULL, UNIQUE
PRIMARY KEY
FOREIGN KEY
CHECK
DELETE CASCADE
DELETE SET NULL
In that particular case you should use FOREIGN KEY constraint using the DELETE SET NULL modifier. Assuming you have an index on id, all you need is:
ALTER TABLE EAP_users
ADD CONSTRAINT EAP_users_supervisor_cst
FOREIGN KEY (supervisor_id)
REFERENCES EAP_users(id)
ON DELETE SET NULL;
This simple referential integrity constraint will perform probably better the same things as your 3 triggers -- namely:
prevent insert/update with a non existing (non-NULL) supervisor_id
set all supervisor_id to NULL when you delete the supervisor
See http://sqlfiddle.com/#!4/1f8fb/1 for a live example.

How do i specify an if statement within a trigger?

i need to insert an "exam entry" row into a table.
But the exam entry cannot be inserted if a student (recognised by the student number sno) is already entered into that exam (recognised by the exam code excode), and the entry also cannot be inserted if the student has more than one exam on the same day (i have an exam table holding the information abotu the exam dates).
I am fairly certain that i should be using an insert trigger function for this and have been looking at:
Example 39-3 from http://www.postgresql.org/docs/9.2/static/plpgsql-trigger.html
so far i have:
INSERT INTO
entry(excode, sno, egrade) VALUES (2, 1, 98.56)
CREATE FUNCTION entry_insert() RETURNS trigger AS $entry_insert$
BEGIN
--Check student is not entered into same exam twice
IF BLA BLA
RAISE EXCEPTION 'A student cannot be be entered into the same exam more than once';
END IF;
--Check student not taking more than one exam on same day
IF BLA BLA
RAISE EXCEPTION 'A student cannot take more than one exam on the same day';
END IF;
END;
$entry_insert$ LANGUAGE PLPGSQL;
CREATE TRIGGER entry_insert BEFORE INSERT ON entry
FOR EACH ROW EXECUTE PROCEDURE entry_insert();
the places where I've put bla bla is where i need the conditions that i cant quite figure out how to meet my conditions.
would love some help?
edit: my exam table
CREATE TABLE exam (
excode CHAR(4) NOT NULL PRIMARY KEY,
extitle VARCHAR(20) NOT NULL,
exlocation VARCHAR(20) NOT NULL, --I'm assuming that an exam will have a location confirmed prior to insertion into the table--
exdate DATE NOT NULL
CONSTRAINT incorrectDate
CHECK (exdate >='01/06/2015' AND exdate <= '30/06/2015'), /* I'm assuming that all exams must have a date confirmed or otherwise the exam wouldn't be inserted into the table*/
extime TIME NOT NULL, -- I'm assuming that an exam will have a time confirmed prior to insertion into the table--
CONSTRAINT incorrect_time
CHECK (extime BETWEEN '09:00:00' AND '18:00:00')
);
You don't need to use triggers for this, you can use normal table constraints, although you will need to define a function.
Your first requirement - that the same student cannot enter the same exam twice - can be checked using a UNIQUE constraint on (excode,sno). In theory this check is redundant because the second check (that a student cannot enter more than one exam per day) would also be violated by that. However, to cater for the possibility of subsequent record updates, this UNIQUE constraint is still needed.
The second requirement can be met using a CHECK constraint. However you have to create a function, because it is not possible to use a subquery inside a CHECK constraint.
Here is an example:
-- Assume we have an exams table. This table specifies the time of each exam
CREATE TABLE exams(excode SERIAL PRIMARY KEY, extime timestamp);
-- Create the entry table. We cannot add the CHECK constraint right away
-- because we have to define the function first, and the table must exist
-- before we can do that.
CREATE TABLE entry(excode int, sno int, egrade FLOAT, UNIQUE(excode,sno));
-- Create a function, which performs a query to return TRUE if there is
-- another exam already existing which this student is enrolled in that
-- is on the same day as the exame identified with p_excode
CREATE FUNCTION exam_on_day(p_excode int, p_sno int) RETURNS bool as $$
SELECT TRUE
FROM entry
LEFT JOIN exams ON entry.excode=exams.excode
WHERE sno=p_sno AND entry.excode != p_excode
AND date_trunc('day',extime)=(
SELECT date_trunc('day', extime) FROM exams WHERE excode=p_excode
);
$$ LANGUAGE SQL;
-- Add check constraint
ALTER TABLE entry ADD CONSTRAINT exam_on_same_day
CHECK(not exam_on_day(excode, sno));
-- Populate some exames.
-- excode 1
INSERT INTO exams(extime) VALUES('2014-12-06 10:00');
-- excode 2
INSERT INTO exams(extime) VALUES('2014-12-06 15:00');
-- excode 3
INSERT INTO exams(extime) VALUES('2014-12-05 15:00');
Now we can try it out:
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(1,1,98.5);
INSERT 0 1
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(1,1,50);
ERROR: duplicate key value violates unique constraint "entry_excode_sno_key"
DETAIL: Key (excode, sno)=(1, 1) already exists.
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(2,1,99);
ERROR: new row for relation "entry" violates check constraint "exam_on_same_day"
DETAIL: Failing row contains (2, 1, 99).
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(3,1,75);
INSERT 0 1
harmic=> UPDATE entry SET egrade=98 WHERE excode=1 AND sno=1;
UPDATE 1
test=> UPDATE entry SET excode=2 WHERE excode=3 AND sno=1;
ERROR: new row for relation "entry" violates check constraint "exam_on_same_day"
DETAIL: Failing row contains (2, 1, 75).
Note that I named the constraint meaningfully, so that when you get an error you can see why (useful if you have more than one constraint).
Also note that the function used for the SELECT constraint excludes the record being updated from the check (entry.excode != p_excode) otherwise you could not update any records.
You could still do this with triggers, of course, although it would be unnecessarily complicated to set up. The IF condition would be similar to the function created above.