PostgreSQL how to write trigger - postgresql

Say I have a following table like this:
http://i.stack.imgur.com/qU3gh.png
where sid stands for sailor ID, and bid stands for boat ID.
I want to write a trigger and a function to detect whether the new input is invalid. Look at the table, the 3rd record shouldn't be there because the renting time has overlap with the former one, one boat can't be rented before it is returned. That is to say, I want a trigger and a function to stop input like the 3rd one, when someone tries to give a 3rd input, it will just stop you from doing so.
Currently I wrote the following code, but as I am new to this, I am really not sure whether it is correct:
CREATE FUNCTION update() RETURNS TRIGGER AS $logupdate$
DECLARE
judge boolean;
BEGIN
judge := EXECUTE ( 'SELECT starttime,endtime,NEW.starttime,NEW.endtime FROM reserves WHERE bid = NEW.bid AND startdate = NEW.startdate AND (starttime,endtime) overlaps(NEW.starttime,NEW.endtime) IS NULL');
IF judge = f THEN RAISE EXCEPTION 'failed due to some reasons';
END IF;
RETURN NEW;
END;
$logupdate$ LANGUAGE plpgsql;
CREATE TRIGGER logupdate
before UPDATE ON reserves
FOR EACH ROW
EXECUTE PROCEDURE update();
How can I correct this?

consider using a timestamp field instead of seperate date and time fields for simplicity. you can easily format a timestamp into either a date or time.
update and insert triggers must return NEW on success, or FALSE/NULL
in order to enforce a constraint with a trigger you must run a BEFORE UPDATE trigger, otherwise the row's already added.
CREATE TRIGGER logupdate
BEFORE UPDATE ON reserves
FOR EACH ROW EXECUTE PROCEDURE update();
CREATE FUNCTION update() RETURNS TRIGGER AS $$
BEGIN
minstart := EXECUTE ( 'SELECT MAX(endtime) FROM reserves WHERE bid = NEW.bid AND startdate = NEW.startdate' ) ;
IF minstart < NEW.starttime THEN RETURN FALSE;
RETURN NEW;
END
$$

Related

How to use the same trigger function for insert/update/delete triggers avoiding the problem with new and old objects

I am looking for an elegant solution to this situation:
I have created a trigger function that updates the table supply with the sum of some detail rows, whenever a row is inserted or updated on warehouse_supplies.
PostgreSQL insert or update syntax allowed me to share the same function sync_supply_stock() for the insert and update conditions.
However, when I try to wire the after delete condition to the function it cannot be reused (although it is logically valid), for the returning object must be old instead of new.
-- The function I want to use for the 3 conditions (insert, update, delete)
create or replace function sync_supply_stock ()
returns trigger
as $$
begin
-- update the supply whose stock just changed in warehouse_supply with
-- the sum its stocks on all the warehouses.
update supply
set stock = (select sum(stock) from warehouse_supplies where supply_id = new.supply_id)
where supply_id = new.supply_id;
return new;
end;
$$ language plpgsql;
-- The (probably) unnecessary copy of the previous function, this time returning old.
create or replace function sync_supply_stock2 ()
returns trigger
as $$
begin
-- update the supply whose stock just changed in warehouse_supply with
-- the sum its stocks on all the warehouses.
update supply
set stock = (select sum(stock) from warehouse_supplies where supply_id = old.supply_id)
where supply_id = old.supply_id;
return old;
end;
$$ language plpgsql;
-- The after insert/update trigger
create trigger on_warehouse_supplies__after_upsert after insert or update
on warehouse_supplies for each row
execute procedure sync_supply_stock ();
-- The after delete trigger
create trigger on_warehouse_supplies__after_delete after delete
on warehouse_supplies for each row
execute procedure sync_supply_stock2 ();
Am I missing something or is there any fixing to duplicating sync_supply_stock2() as sync_supply_stock2()?
EDIT
For the benefit of future readers, following #bergi answer and discusion, this is a possible factorized solution
create or replace function sync_supply_stock ()
returns trigger
as $$
declare
_supply_id int;
begin
-- read the supply_id column from `new` on insert/update conditions and from `old` on delete conditions
_supply_id = coalesce(new.supply_id, old.supply_id);
-- update the supply whose stock just changed in of_warehouse_supply with
-- the sum its stocks on all the warehouses.
update of_supply
set stock = (select sum(stock) from of_warehouse_supplies where supply_id = _supply_id)
where supply_id = _supply_id;
-- returns `new` on insert/update conditions and `old` on delete conditions
return coalesce(new, old);
end;
$$ language plpgsql;
create trigger on_warehouse_supplies__after_upsert after insert or update
on of_warehouse_supplies for each row
execute procedure sync_supply_stock ();
create trigger on_warehouse_supplies__after_delete after delete
on of_warehouse_supplies for each row
execute procedure sync_supply_stock ();
for the returning object must be old instead of new.
No. The return value is only relevant for BEFORE ROW or INSTEAD OF triggers. From the docs: "The return value of a row-level trigger fired AFTER or a statement-level trigger fired BEFORE or AFTER is always ignored; it might as well be null".
So you can just make your sync_supply_stock trigger function RETURN NULL and it can be used on all operations.

How to update the tables NEW values after INSERT Trigger in PostgreSQL/PostGIS?

I try to automatize some calculations on tables in my database. I try to perform some UPDATE on rows that are newly inserted in a table, but I newer used NEW or OLD statements before. I tried writing the code that updates happen on new values by assigning NEW.[tablename], but it wont work. Isn't there any statement in the beginning of the trigger function to specify running the function only on new values, I cannot find useful information about this.
CREATE OR REPLACE FUNCTION cost_estimation()
RETURNS TRIGGER AS
$func$
DECLARE
a INTEGER := 3;
BEGIN
UPDATE NEW.cost_table
SET column4 = a;
UPDATE NEW.cost_table
SET column 5 = column4 - column2;
[...]
RETURN NEW;
END
$func$ language plpgsql
UPDATE:
Thank you for the answers so far.
My original code is written based on the update structure, and needs to be rewritten when omitting UPDATE. I should give a better example of my situation. Easy spoken: I have a table (T1) which will be filled with data from another table (T2).
After data is inserted in T1 from T2 I want to run calculations on the new values inside of T1.(The code includes PostGIS functionalities):
CREATE OR REPLACE FUNCTION cost_estimation()
RETURNS TRIGGER AS
$func$
BEGIN
NEW.column6 = column2 FROM external_table WHERE
St_Intersects(NEW.geom, external_table.geom) LIMIT1;
NEW.column8 = CASE
WHEN st_intersects(NEW.geom, external_table2.geom) then 'intersects'
WHEN (NEW.column9 = 'K' and NEW.column10 <= 6) then 'somethingelse'
ELSE 'nothing'
END
FROM external_table2;
[...]
RETURN NEW;
END
$func$ language plpgsql
CREATE TRIGGER table_calculation_on_new
BEFORE INSERT OR UPDATE ON cost_estimation
FOR EACH ROW EXECUTE PROCEDURE road_coast_estimation();
After inserting values in my table no calculations will be performed.
UPDATE2: I checked my tables again and detected that another trigger was blocking the table operation. The code in the lower half is working fine now, thanks to #a_horse_with_no_name.
NEW and OLD aren't "statements", those are records that represent the modified rows from the DML statement that fired the trigger.
Assuming the trigger is defined on cost_table you can simply change the fields in the NEW record. No need to UPDATE anything:
CREATE OR REPLACE FUNCTION cost_estimation()
RETURNS TRIGGER AS
$func$
DECLARE
a INTEGER := 3;
BEGIN
new.column4 := a;
new.column5 := new.column4 - new.column2;
return new;
END;
$func$ language plpgsql
For this to work the trigger needs to be defined as a BEFORE trigger:
create trigger cost_table_trigger
BEFORE insert or update on cost_table
for each row execute procedure cost_estimation();

how to have a trigger work only for certain columns (they are chosen by their id) after updating

I have two tables.Tickets which has the columns: ticketid,startdate , enddate
and transactions which has the columns: transactionid, ticketid (fk to tickets), ticketcost.I want to create a trigger on tickets that makes a discount to ticketcost of transactions(of the table transactions) whenever the enddate of a ticket is updated.Multiple transactions might have the same ticket.
I was able to make a trigger that did what i described however not only at one ticket, the one that the date was changed, but every ticket of the tickets table.
first attempt:
create or replace function changeDate() returns trigger as $changeDate$
BEGIN
IF new.enddate != old.enddate THEN
update transactions
set ticketcost = ticketcost - ticketcost*0.1
from tickets
where tickets.ticketid= transactions.ticketid;
END IF;
return new;
END
$changeDate$ LANGUAGE plpgsql;
CREATE TRIGGER changeDate after UPDATE ON tickets
FOR EACH ROW EXECUTE FUNCTION changeDate();
This obviously failed because it is done for every row so every ticket is updated.
What i have now is this:
create or replace function changeDate() returns trigger as $changeDate$
Declare
arg1 integer;
BEGIN
IF new.enddate != old.enddate THEN
update transactions
set ticketcost = ticketcost - ticketcost*0.1
from tickets
where arg1 = transactions.ticketid;
END IF;
return new;
END
$changeDate$ LANGUAGE plpgsql;
CREATE TRIGGER changeDate after UPDATE ON tickets
FOR EACH ROW
WHEN (new.enddate != old.enddate)
EXECUTE FUNCTION changeDate(tickets.ticketid);
I have been trying to pass only the id of a ticket that has different new and old dates.The query works but nothing is changed.Basically i m trying to find a way to pass the id of the ticket that has had its enddate field changed.In the above example i m trying to pass it as a variable when the condition i described occurs.Any help would be appreciated as i cant really find a solution.
You can restrict the trigger to fire only when the date has been updated by specifying the column name and by ensuring the updated value is not the same as the old one:
CREATE TRIGGER changeDate after UPDATE OF enddate ON tickets
FOR EACH ROW
WHEN (OLD.enddateIS DISTINCT FROM NEW.enddate )
EXECUTE FUNCTION changeDate();
Inside the trigger function, you can refer to the NEW.ticketID directly
create or replace function changeDate() returns trigger as $changeDate$
BEGIN
update transactions
set ticketcost = ticketcost - ticketcost*0.1
where transactions.ticketid = NEW.ticketid;
return new;
END
$changeDate$ LANGUAGE plpgsql;
PS: since the function is not changing the date, a better name would be setDiscount

How to modify Trigger to update a single attribute in PostgreSQL

Here is my sample table.
CREATE TABLE employee_test(
idTst SERIAL PRIMARY KEY,
monthDownload VARCHAR(6),
changeDate DATE);
I am trying to create a function and trigger that would update changeDate attribute with a current date when monthDownload attribute is updated.
The function I have it works with one problem. It updates all records instead of the one that was updated.
CREATE OR REPLACE FUNCTION downloadMonthChange()
RETURNS TRIGGER AS
$$
BEGIN
IF NEW.monthDownload <> OLD.monthDownload THEN
UPDATE employee_test
SET changeDate = current_date
where OLD.idTst = NEW.idTst;
END IF;
RETURN NEW;
END;
$$
Language plpgsql;
Trigger
Create TRIGGER dataTest
AFTER UPDATE
ON employee_test
FOR EACH ROW
EXECUTE PROCEDURE downloadMonthChange();
When I execute the following Update statement:
UPDATE employee_test SET monthDownload = 'oct12'
WHERE idTst = 1;
All changeDate rows get update with a current date.
Is there a way to have only a row with changed record to have a current date updated.
If you use a before trigger you can write directly to NEW
CREATE OR REPLACE FUNCTION downloadMonthChange()
RETURNS TRIGGER AS
$$
BEGIN
IF NEW.monthDownload <> OLD.monthDownload THEN
NEW.changeDate = current_date;
END IF;
RETURN NEW;
END;
$$
Language plpgsql;
the other option when you must use an after trigger is to include the primary key in the where clause. It appears that you were trying to do this, but you had a spurious OLD in the query. beause of that the where clause was only looking at the record responsible for the trigger call, and not limiting which records were to be updated.
IF NEW.monthDownload <> OLD.monthDownload THEN
UPDATE employee_test
SET changeDate = current_date
where idTst = NEW.idTst;

Postgresql Trigger Not Firing

I have a function and a trigger as below:
CREATE FUNCTION update() RETURNS TRIGGER AS $logupdate$
DECLARE
judge boolean;
BEGIN
judge := EXECUTE ('SELECT starttime,endtime,NEW.starttime,NEW.endtime FROM reserves WHERE bid = NEW.bid AND startdate = NEW.startdate AND (starttime,endtime) overlaps(NEW.starttime,NEW.endtime) IS NULL');
IF judge = f THEN RETURN false;
END IF;
RETURN NEW;
END;
$logupdate$ LANGUAGE plpgsql;
CREATE TRIGGER logupdate
before UPDATE ON reserves
FOR EACH ROW
EXECUTE PROCEDURE update();
The trigger should detect if the new input has overlapping with previous time, for example if there is an old time in my table [11:00 - 13:00], and a new input is [12:00 - 14:00] then the trigger should fire at it and stop insertion.
However, it doesn't work in my computer, what's wrong here?
Your trigger gets executed for every row. You don't need to query the tables. You already have access to the old and new rows.
CREATE FUNCTION update() RETURNS TRIGGER AS $logupdate$
BEGIN
IF (NEW.starttime, NEW.endtime) OVERLAPS (OLD.starttime, OLD.endtime) THEN
RETURN false;
END IF;
RETURN NEW;
END;
$logupdate$ LANGUAGE plpgsql;
However, this is super inefficient. You should consider using an exclusion constraint http://www.postgresql.org/docs/9.0/static/ddl-constraints.html
I'm pretty sure there's an exclusion constraint that will work with OVERLAPS