postgresql trigger to compare the third to last character with an attribute - sql

I have recently started studying PostgreSQL and am having trouble creating triggers.
In the specific case I should check that a male athlete cannot participate in a competition for women and vice versa; in the match_code attribute an 'M' or an 'F' is inserted as the third to last character to identify that the race is for males or females (for example: 'Q100x4M06'); only one character, 'M' or 'F', is stored in the gender attribute.
I would therefore need to understand how to compare them and activate the trigger when they are not correctly entered in the participation table.
This is what i have assumed but i know it is wrong, it is just an idea, can someone help me?
CREATE FUNCTION check()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $BODY$
DECLARE
x VARCHAR;
y VARCHAR;
BEGIN
SELECT match_code INTO x FROM race;
SELECT gender INTO y FROM athlete;
IF $x LIKE '&$y__' == athlete.gender
THEN
RETURN new;
ELSE
RAISE EXCEPTION $$It is not possible to add an athlete of a gender that is not compatible with the competition$$;
RETURN NULL;
END IF;
END;
$BODY$
CREAT TRIGGER triggerCheck
BEFORE INSERT OR UPDATE ON participation
FOR EACH ROW
EXECUTE PROCEDURE check();
below are the definitions of the tables:
CREATE TABLE race (
ID_g SERIAL NOT NULL,
code_race VARCHAR (20) PRIMARY KEY,
r_date DATE,
discipline VARCHAR (20) NOT NULL
);
CREATE TABLE athlete (
ID_a SERIAL NOT NULL,
code_athlete INT CHECK (codice_atleta >= 0 AND codice_atleta <= 15000) PRIMARY KEY,
name VARCHAR (30),
surname VARCHAR (30) NOT NULL,
nation VARCHAR (3) NOT NULL,
gender CHAR CHECK (gender = 'M' OR gender = 'F'),
b_date DATE,
sponsor VARCHAR (20)
);
CREATE TABLE participation (
ID_p SERIAL NOT NULL,
codR VARCHAR (20) REFERENCES race (code_race) ON DELETE CASCADE ON UPDATE CASCADE,
codA INT REFERENCES athlete (code_athlete) ON DELETE CASCADE ON UPDATE CASCADE,
arrival_order INT CHECK (arrival_order > 0),
r_time TIME DEFAULT '00:00:00.00',
PRIMARY KEY (codG, codA)
);

According to the tables definition, your trigger function should be someting like :
CREATE FUNCTION check()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $BODY$
BEGIN
IF EXISTS ( SELECT 1
FROM race AS r
INNER JOIN athlete AS a
ON r.code_race ~ (a.gender || '..$')
WHERE r.code_race = NEW.codR
AND a.code_athlete = NEW.codA
)
THEN
RETURN NEW ;
ELSE
RAISE EXCEPTION 'It is not possible to add an athlete of a gender that is not compatible with the competition';
RETURN NULL;
END IF;
END;
$BODY$
In a function called by a trigger, you can (have to) use the variable NEW (resp. OLD) so that to refer to the new row (resp. the old row) to be inserted or updated (resp. only to be updated) in the targeted table ("participation" in your case) see the manual.
The proposed function doesn't need to declare any variable, as the test can be performed directly through the proposed sql query.
The proposed sql query :
First search for the rows in table "race" whose code_race equals the inserted/updated value of participation.codR = NEW.codR
Then search for the rows in table "athlete" whose code_athlete equals
the inserted/updated value of participation.codA = NEW.codA
Finally compare the selected rows from both tables "race" and
"athlete" using a regular expression to compare the code_race with
the gender values, see the manual
By the way,
(a) I don't see the added value of the columns ID of type serial in the tables definition, especially as they are not used in any primary key nor foreign key.
(b) The significant codification defined for the code_race attribute (for instance : 3rd last character defining the gender, an other character defining the competition level, ...) is a quite old-fashion practice that was used 30 years ago when the computer sciences had limited capacities and performances. In a more up-to-date approach, I would suggest you to manage these meaningful information in dedicated columns of the table "race", and then, if you really need a significant composite code_race, to implement it as a generated column :
CREATE TABLE race
( ID_g SERIAL NOT NULL PRIMARY KEY
, gender_race CHAR(1) NOT NULL CHECK (gender_race IN ('M', 'F'))
, competition_level VARCHAR(12) NOT NULL CHECK (competition_level IN ('Q-Qualifiers', 'H-Heats', 'S-Semifinals', 'F=Finals'))
, code_race VARCHAR (20) GENERATED ALWAYS AS (ID_g :: text || '-' || discipline || '-' || gender_race || '-' || left(competition_level, 1)) STORED
, r_date DATE NOT NULL
, discipline VARCHAR (20) NOT NULL
);
see the result in db<>fiddle.

Related

PostgreSQL Trigger takes 5 seconds

i have a MediaStore Database on Postgres where tried to make a trigger which Updates the average Rating of a Product if a new review is inserted.
The Problem is: If I insert a review now, it takes more than 5 seconds.
Im not really into Databases so i thought of asking you people here :)
The DDL of the two relevant tables are:
create table review
(
review_id bigint generated by default as identity primary key,
rating integer not null CHECK (rating BETWEEN 1 AND 5),
helpful integer not null CHECK (helpful >= 0),
reviewDate date,
benutzer varchar(255),
summary varchar(255),
comment text,
produkt_id bigint NOT NULL references produkt ON DELETE CASCADE
);
create table produkt
(
produkt_id bigint generated by default as identity primary key,
asin varchar(255) unique NOT NULL,
titel varchar(1000) NOT NULL,
rating double precision,
bild varchar(1000),
verkaufsrang integer
);
And the Trigger:
CREATE OR REPLACE FUNCTION update_rating()
RETURNS TRIGGER AS $$
BEGIN
UPDATE produkt
SET rating =
(SELECT AVG(rating) AS rating
FROM review
GROUP BY produkt_id
Having review.produkt_id = new.produkt_id)
WHERE produkt_id = new.produkt_id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER update_rating
AFTER INSERT ON review
FOR EACH ROW
EXECUTE PROCEDURE update_rating();
Does somebody have a solution which reduces the Time of the Insert?
You don't describe your indexes. An index on review (produkt_id, rating) could help a lot if you don't have one already. If you had columns in produkt for sum and count, then you could just compute the new average without needing to traverse the entire set in review for that produkt_id. You might have a problem with concurrency, but that could be a problem with your current one too.

sql oracle - constraint on 2 columns from different tables

I have designed a ticket system booking for flights. I want to add a constraint such that the number of tickets you can insert to be less than number of seats from a flight plane.
Let's say I inserted a flight with a plane with 10 seats. I can insert only 10 tickets for that particular flight. Otherwise, an error message should appear.
I tried to make a trigger using the count function on flight number.
CREATE OR REPLACE TRIGGER trg_ticket_BRIU
BEFORE INSERT OR UPDATE ON Ticket
FOR EACH ROW
DECLARE
l_numberofseats flight.numberofseats%type;
BEGIN
select numberofseats into l_numberofseats
from flight
where flightnumber=:new.flightnumber;
IF :new.count(flightnumber) > l_numberofseats
THEN
raise_application_error(-2000, 'Not enough seats');
END IF;
END;
but I get this error
Trigger TRG_TICKET_BRIU compiled
LINE/COL ERROR
--------- -------------------------------------------------------------
8/5 PLS-00049: bad bind variable 'NEW.COUNT'
Errors: check compiler log
Personally, I would add an AIRCRAFT and a SEAT table:
CREATE TABLE aircraft (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT aircraft__id__pk PRIMARY KEY,
tail_number VARCHAR2(6)
NOT NULL
CONSTRAINT aircraft__tn__u UNIQUE
CONSTRAINT aircraft__tn_chk CHECK(
REGEXP_LIKE(
tail_number,
'[A-Z]\d{1,5}|[A-Z]\d{1,4}[A-Z]|[A-Z]\d{1,3}[A-Z]{2}'
)
),
manufacturer VARCHAR2(20)
NOT NULL,
model VARCHAR2(20)
NOT NULL,
airline_id CONSTRAINT aircraft__aid__fk REFERENCES airline(airline_id)
NOT NULL
);
CREATE TABLE seat (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT seat__id__pk PRIMARY KEY,
aircraft_id CONSTRAINT seat__aid__fk REFERENCES aircraft(id)
NOT NULL,
seat_row VARCHAR2(3)
NOT NULL,
seat_column NUMBER
NOT NULL,
CONSTRAINT seat__aid_r_c__u UNIQUE (aircraft_id, seat_row, seat_column)
);
Then your flight table would reference the aircraft:
CREATE TABLE flight (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT flight__id__pk PRIMARY KEY,
aircraft_id CONSTRAINT flight__aid__fk REFERENCES aircraft(id)
NOT NULL
-- ...
);
And the ticket would reference a flight and a seat:
CREATE TABLE ticket (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT ticket__id__pk PRIMARY KEY,
flight_id CONSTRAINT ticket__fid__fk REFERENCES flight(id)
NOT NULL,
seat_id CONSTRAINT ticket__sid__fk REFERENCES seat(id)
NOT NULL,
-- ...
CONSTRAINT ticket__fid_sid__u UNIQUE (flight_id, seat_id)
);
Then you can never sell a seat that does not exist on an aircraft and do not need to count the maximum number of tickets and compare it to seats (and the seat has added attributes like its location on the plane that can be displayed on the ticket).
All you need then is to ensure the referential consistency that, for a ticket, the flight and the seat are on the same aircraft; which can be done with a trigger:
CREATE TRIGGER ticket_check_seat_on_flight
BEFORE INSERT OR UPDATE ON ticket
FOR EACH ROW
DECLARE
is_valid NUMBER(1);
BEGIN
SELECT 1
INTO is_valid
FROM flight f
INNER JOIN seat s
ON (f.aircraft_id = s.aircraft_id)
WHERE f.id = :NEW.flight_id
AND s.id = :NEW.seat_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(
-20000,
'Flight and seat are on different aircraft.'
);
END;
/
db<>fiddle here
You can use an AFTER STATEMENT trigger:
CREATE TRIGGER ticket__check_number_of_seats
AFTER INSERT OR UPDATE OR DELETE ON ticket
DECLARE
is_invalid NUMBER(1,0);
BEGIN
SELECT 1
INTO is_invalid
FROM flight f
INNER JOIN (
SELECT flight_id,
COUNT(*) AS tickets_sold
FROM ticket
GROUP BY flight_id
) t
ON f.id = t.flight_id
WHERE t.tickets_sold > f.number_of_seats;
RAISE_APPLICATION_ERROR(
-20000,
'Too many tickets sold for flight.'
);
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END;
/
It could be made more efficient by using a compound trigger to collate, for each row, the flight_id values into a collection and then, after the statement, only checking the number of tickets for those flights; however, I'll leave that extension as an exercise for the OP.
db<>fiddle here
As others indicated there is no :new.count column. This is because :new (and :old) create a pseudo-row containing exactly the same columns as the table definition. Further you will get a Mutating exception as what you need to count in the flight_number from tickets. However, since that is the table causing he trigger to fire you cannot reference it. So what to do: create a compound trigger, and a supporting Type (nested table). Within it use the after row section to capture the flight_numbers processed. Then in the after statement section you can select count of tickets for each flight. If that count > 0 then raise your exception. ( see Demo )
create type flight_tickets_ntt
is table of integer;
create or replace trigger trg_ticket_ciu
for update or insert on tickets
compound trigger
l_flights flight_tickets_ntt := flight_tickets_ntt();
after each row is
begin
if :new.flight_number not member of l_flights then
l_flights.extend ;
l_flights(l_flights.count) := :new.flight_number;
end if;
end after each row;
after statement is
l_flight_cnt flight.flight_number%type;
begin
select count(*)
into l_flight_cnt
from flight f
where f.number_of_seats <
( select count(*)
from tickets t
where t.flight_number in
( select *
from table (l_flights)
)
);
if l_flight_cnt > 0 then
raise_application_error(-20000, 'Not enough seats');
end if;
end after statement;
end trg_ticket_ciu;
There remains a you need to handle: What happens if an update changes the flight number or perhaps (missing column) the data of the flight.

Oracle SQL Check

I'm trying to implement an Oracle SQL database, in one of my tables I must introduce a restriction which does not allow to have more than 4 people in the same group:
I've tried this:
CREATE TABLE PERSON (name VARCHAR (20) PRIMARY KEY, group VARCHAR (3), CHECK (COUNT (*) group FROM PERSON) <=4);
also this (among others):
CREATE TABLE PERSON (name VARCHAR (20) PRIMARY KEY, group VARCHAR (3), CHECK NOT EXISTS (Select COUNT(*) FROM PERSON GROUP BY group HAVING COUNT(*) > 4);
But I'm getting errors every time (ORA-00934: group function is not allowed here or ORA-02251: subquery not allowed here.
What is the correct way to do it?
You have multiple issues with this
CREATE TABLE PERSON (
name VARCHAR(20) PRIMARY KEY,
group VARCHAR(3),
CHECK (COUNT (*) group FROM PERSON) <=4);
);
Oracle explicitly prefers VARCHAR2() to VARCHAR().
GROUP is a really bad name for a column, because it is a keyword. Surely you can find something like group_name or whatever for the name.
CHECK constraints only work within a single row.
Probably the best way to handle this is:
Create a new table called groups -- or whatever. It should have a group_id as well as group_name and num_persons.
Add triggers to person to keep the counter up-to-date for inserts, deletes, and updates to person.
Add a check constraint to groups, say check (num_persons <= 4).
You need to create the table as following:
CREATE TABLE PERSON (
name VARCHAR2(20) PRIMARY KEY,
group_ VARCHAR2(3) -- added _ after column name
); -- used varchar2 as data type of column
Then create before insert trigger as following:
create trigger person_trg
before insert on person
for each row
declare
group_cnt number;
begin
select count(distinct name)
into group_cnt
from person
where group_ = :new.group_;
if group_cnt = 4 then
raise_application_error(-20001, 'more than 4 persons are not allowed in the group');
end if;
end;
/
I have used distinct person name as more than 4 distinct persons are not allowed in the group as per your requirement.
db<>fiddle demo
Cheers!!

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();

Postgresql SET DEFAULT value from another table SQL

I'm making a sql script so I have create tables, now I have a new table that have columns. One column has a FOREIGN KEY so I need this value to be SET DEFAULT at the value of the value of the original table. For example consider this two table
PERSON(Name,Surename,ID,Age);
EMPLOYER(Name,Surname,Sector,Age);
In Employer I need AGE to be setted on default on the AGE of Person, this only if PERSON have rows or just 1 row.
ID is Primary key for person and Surname,Sector for employer and AGE is FOREIGN KEY in Employer refferenced from Person
Example sql :
CREATE TABLE PERSON(
name VARCHAR(30) ,
surename VARCHAR(20),
ID VARCHAR(50) PRIMARY KEY,
Age INT NOT NULL,
);
CREATE TABLE EMPLOYER(
name VARCHAR(30) ,
Surename VARCHAR(20),
Sector VARCHAR(20),
Age INT NOT NULL,
PRIMARY KEY (Surename,Sector),
FOREIGN KEY (Age) REFERENCES Person(Age) //HERE SET DEFAULT Person(Age), how'??
);
Taking away the poor design choices of this exercise it is possible to assign the value of a column to that of another one using a trigger.
Rough working example below:
create table a (
cola int,
colb int) ;
create table b (
colc int,
cold int);
Create or replace function fn()
returns trigger
as $$ begin
if new.cold is null then
new.cold = (select colb from a where cola = new.colc);
end if;
return new;
end;
$$ language plpgsql;
CREATE TRIGGER
fn
BEFORE INSERT ON
b
FOR EACH ROW EXECUTE PROCEDURE
fn();
Use a trigger rather than a default. I have done things like this (useful occasionally for aggregated full text vectors among other things).
You cannot use a default here because you have no access to the current row data. Therefore there is nothing to look up if it is depending on your values currently being saved.
Instead you want to create a BEFORE trigger which sets the value if it is not set, and looks up data. Note that this has a different limitation because DEFAULT looks at the query (was a value specified) while a trigger looks at the value (i.e. what does your current row look like). Consequently a default can be avoided by explicitly passing in a NULL. But a trigger will populate that anyway.