Triggering an update on a second table after insert - postgresql

I have two tables, A and P.
A
________________
id | num_cars
----------
1 | 2
2 | 0
3 0
P
__________________________
id_driver | id_car
--------------------------
1 | Porsche
1 | BMW
A.id and P.id_driver referes to the same person. I created the below trigger. The idea is, every time I add a new row in P for an existing driver its correspondent row in A must be updated with the number of total cars owned by the person with that id.
CREATE OR REPLACE FUNCTION update_a() RETURNS trigger AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE A a
SET num_cars = (SELECT COUNT(NEW.id_driver)
FROM P p
WHERE (a.id = p.id_driver AND a.id=NEW.id_driver));
ELSIF TG_OP = 'DELETE' THEN
UPDATE A a
SET num_cars = num_cars - 1
WHERE a.id = OLD.id_driver AND a.num_cars<>0;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER add_car
AFTER INSERT OR DELETE ON PARTICIPATION
FOR EACH ROW EXECUTE PROCEDURE update_a();
The trigger works fine when I add a row in B for a driver. However, if I then add a row for a different driver in B the rest of the rows in A are set back to 0. I would like the procedure to run only when A.id = P.id_driver. How can I do this?

The update query makes a cross product between A and P, and therefore updates the entire table, counting 0 cars most of the time.
You would need to restrict the update to the proper driver only, and also to compute the number of cars only for this driver:
UPDATE A a
SET num_cars = (SELECT COUNT(*)
FROM P p
WHERE p.id_driver = NEW.id_driver)
WHERE a.id = NEW.id_driver;

Related

Are "updateable views" the correct approach for manually editing multiple tables

I have a PostgreSQL database with the following tables
person
city
country
Each person is linked to a city through a city_id foreign key, and each city is linked to a country through country_id in a similar way.
To easily view the names of each person's city and country, I create a view:
CREATE VIEW person_view AS
SELECT
person.id,
person.name,
city.name as city,
country.name as country
FROM person
LEFT JOIN city ON person.city_id = city.id
LEFT JOIN country ON city.country_id = country.id
Which gives something easy to read.
| id | name | city | country |
------------------------------------------
| 1 | Steve | New York | United States |
| 2 | Rachel | Paris | France |
Now, using a program like dbeaver, I was hoping to manage these entries using this view. Instead of looking up IDs whenever a person's city/country needs to change, it'd be much easier just to type in changes in the view and have those changes carry over to the original tables.
I thought that this was what an updatable view was meant for, but dbeaver will not allow this view to be updated directly, and suggests implementing INSTEAD OF UPDATE triggers or ON UPDATE DO INSTEAD rules.
Am I approaching this correctly? Is the operation I've described here what updatable views are meant to do?
demo
Only show update trigger. You can do similar thing for insert and delete.
CREATE OR REPLACE FUNCTION person_view_upd_trig_fn ()
RETURNS TRIGGER
AS $$
BEGIN
IF tg_op = 'UPDATE' THEN
IF NEW.name <> OLD.name THEN
RAISE NOTICE 'update person name';
UPDATE
person
SET
name = NEW.name
WHERE
id = OLD.id;
RETURN new;
END IF;
IF NEW.city <> OLD.city AND NEW.country = OLD.country THEN
RAISE NOTICE 'update city name';
IF (
SELECT
count(DISTINCT country_id)
FROM
city
WHERE
name = OLD.city OR name = NEW.city) = 2 THEN
RAISE EXCEPTION 'not good';
END IF;
IF (
SELECT
city_id
FROM
city
WHERE
name = NEW.city) IS NULL THEN
RAISE EXCEPTION 'city not in the list';
END IF;
UPDATE
person
SET
city_id = (
SELECT
city_id
FROM
city
WHERE
name = NEW.city)
WHERE
id = OLD.id;
RETURN new;
END IF;
IF NEW.country <> OLD.country AND NEW.city <> OLD.city THEN
RAISE NOTICE 'updating person country & city';
IF NOT EXISTS (
SELECT
FROM
country
WHERE
name = NEW.country) THEN
RAISE EXCEPTION 'not good';
END IF;
UPDATE
person
SET
city_id = (
SELECT
city_id
FROM
city
WHERE
name = NEW.city)
WHERE
id = OLD.id;
RETURN new;
END IF;
RAISE NOTICE 'new.person_view:%', new;
RAISE NOTICE 'old.person_view:%', old;
RETURN NULL;
END IF;
END
$$
LANGUAGE plpgsql;
create trigger:
CREATE TRIGGER person_view_upd_trig
INSTEAD OF UPDATE ON person_view
FOR EACH ROW EXECUTE PROCEDURE person_view_upd_trig_fn();
person_view id column cannot update, update will have no effect. all other 3 column can update.
IF tg_op = 'UPDATE' THEN is not that redundant. You can add more control block, like IF tg_op = 'DELETE THEN in this function. Then one function, you can control 3 actions/trigger(delete, update, insert), instead of 3 function and 3 triggers.
https://www.postgresql.org/docs/current/plpgsql-trigger.html
A trigger function must return either NULL or a record/row value
having exactly the structure of the table the trigger was fired for.
and
INSTEAD OF triggers (which are always row-level triggers, and may only
be used on views) can return null to signal that they did not perform
any updates, and that the rest of the operation for this row should be
skipped (i.e., subsequent triggers are not fired, and the row is not
counted in the rows-affected status for the surrounding
INSERT/UPDATE/DELETE). Otherwise a nonnull value should be returned,
to signal that the trigger performed the requested operation. For
INSERT and UPDATE operations, the return value should be NEW, which
the trigger function may modify to support INSERT RETURNING and UPDATE
RETURNING (this will also affect the row value passed to any
subsequent triggers, or passed to a special EXCLUDED alias reference
within an INSERT statement with an ON CONFLICT DO UPDATE clause). For
DELETE operations, the return value should be OLD.

How do I cap number of rows in based on category using postgres policies?

I have a database of orders and each order has a time_slot (of type TIME).
select id, time_slot from orders limit 5;
10 | 13:00:00
11 | 12:00:00
13 | 11:00:00
14 | 12:30:00
15 | 11:30:00
I want to make sure that only a certain number of orders can be placed in a time slot; for example, let's say I only want 8 orders per time slot.
I am using Supabase, so I would like to implement using RLS policies.
I've been through several iterations, but none of them have worked. Some complain of infinite recursion. My current approach is the following: I have created a view of time slot load.
create or replace view time_slot_load as
select time_slot, count(*)
from orders
group by time_slot;
select * from time_slot_load limit 5;
11:00:00 | 1
12:30:00 | 1
11:30:00 | 1
13:00:00 | 1
12:00:00 | 7
I can then create a policy that checks against this view.
ALTER POLICY "Only 8 orders per time slot"
ON public.orders
WITH CHECK (
(SELECT (load.count <= 8)
FROM time_slot_load load
WHERE (load.time_slot = orders.time_slot))
);
But this is not working.
Is there some way I can do this using constraints or RLS policies?
Any help is appreciated.
demo
table structure:
begin;
create table orders_count(
time_slot tstzrange ,
order_count integer default 0,
constraint order_ct_max1000 check(order_count <=4));
create table
orders(orderid int primary key, realtimestamp timestamptz , order_info text);
with a as(
SELECT x as begin_,x + interval '1 hour' as end_
FROM generate_series(
timestamp '2022-01-01',
timestamp '2022-01-02',
interval '60 min') t(x))
insert into orders_count(time_slot)
select tstzrange(a.begin_, a.end_,'[]') from a;
commit;
two table, one table for all the orders info, another table is track the timeslot's count, also make one constraint, within one timeslot no more than 4 orderid.
Then create trigger for delete/update/insert action on table orders.
for table orders_count, 20 years only 175200 rows (one hour one row).
main trigger function:
begin;
create or replace function f_trigger_orders_in()
returns trigger as
$$
begin
update orders_count set order_count = order_count + 1
where time_slot #> NEW.realtimestamp;
return null;
end;
$$ language plpgsql;
create or replace function f_trigger_orders_up()
returns trigger as
$$
begin
if OLD.realtimestamp <> NEW.realtimestamp THEN
update orders_count
set order_count = order_count -1
where time_slot #> OLD.realtimestamp;
update orders_count
set order_count = order_count + 1
where time_slot #> NEW.realtimestamp;
end if;
return null;
end
$$ language plpgsql;
create or replace function f_trigger_order_delaft()
returns trigger as
$$
BEGIN
update orders_count set order_count = order_count -1 where time_slot #> OLD.realtimestamp;
return null;
end;
$$
language plpgsql;
triggers action:
create trigger trigger_in_orders
AFTER INSERT ON public.orders FOR EACH ROW execute procedure f_trigger_orders_in();
create trigger trigger_up_orders
after update on public.orders for each row execute procedure f_trigger_orders_up();
create trigger trigger_del_orders
AFTER DELETE ON public.orders FOR EACH ROW execute procedure f_trigger_order_delaft();
I want to make sure that only a certain number of orders can be placed
in a time slot; for example, let's say I only want 8 orders per time
slot.
You cannot do that in PostgreSQL.
https://www.postgresql.org/docs/current/sql-createpolicy.html
check_expression:
Any SQL conditional expression (returning boolean). The conditional
expression cannot contain any aggregate or window functions. This
expression will be used in INSERT and UPDATE queries against the table
if row-level security is enabled. Only rows for which the expression
evaluates to true will be allowed. An error will be thrown if the
expression evaluates to false or null for any of the records inserted
or any of the records that result from the update. Note that the
check_expression is evaluated against the proposed new contents of the
row, not the original contents.
Why view won't work:
https://www.postgresql.org/docs/current/sql-createview.html
see Updatable Views section:
A view is automatically updatable if it satisfies all of the following
conditions:
The view's select list must not contain any aggregates, window functions or set-returning functions.

PostgreSQL Trigger on last deleted line

I created a trigger that updates the number of species counted from the "effectif" table to the "citation" table.
It works well except when I delete the last row, the calculation does not perform and remains at the last state. For example, if I delete the last row on the "effectif" table that represents 6 species, I would still have 6 on my "citation" table where I'm suppose to find 0 or null.
Here is my trigger :
CREATE OR REPLACE FUNCTION data.del_eff_tot()
RETURNS trigger AS
$BODY$
BEGIN
IF OLD.effectif IS NOT NULL THEN
UPDATE data.citation
SET effectif =
(SELECT sum(a.effectif) FROM data.effectif a, data.citation b WHERE OLD.id_cit = a.id_cit AND OLD.id_cit = b.id)+COALESCE((SELECT a.effectif FROM data.effectif a, data.citation b WHERE OLD.id_cit = a.id_cit AND OLD.id_cit = b.id AND sexe = 'sexe_4_1'),0)
WHERE id IN (SELECT id_cit FROM data.effectif a, data.citation b WHERE OLD.id_cit = b.id AND b.id = a.id_cit);
END IF;
RETURN OLD;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION data.del_eff_tot()
OWNER TO postgres;
------------------------------
CREATE TRIGGER del_eff_tot
AFTER DELETE
ON data.effectif
FOR EACH ROW
EXECUTE PROCEDURE data.del_eff_tot();

if inside for Loop in postgresql

I have two tables as below.
TABLE 1 TABLE 2
-------- --------
id id
date table1_id
total subtotal
balance
table 1 table 2
-------- ---------
id total balance id table1_id subtotal paid
1 20 10 1 1 5 5
2 30 30 2 1 15 5
3 2 10 0
4 2 10 0
5 2 10 0
I have to add paid column in table2. so can anyone help me to add values to newly added column for existing data. I tried to wrote procedure as below but as postgres will not allow if in for loop so am unable to do it.
CREATE OR REPLACE FUNCTION public.add_amountreceived_inbillitem() RETURNS void AS
$BODY$
DECLARE
rec RECORD;
inner_rec RECORD;
distributebalance numeric;
tempvar numeric;
BEGIN
FOR rec IN select * from table1
LOOP
distributebalance = rec.balance;
FOR inner_rec IN(select * from table2 where table1_id = rec.id order by id limit 1)
tempvar = distributebalance - inner_rec.subtotal;
if (distributebalance >0 and tempvar>=0) THEN
update table2 set paid = inner_rec.subtotal where id = inner_rec.id ;
distributebalance =distributebalance-inner_rec.subtotal;
else if( distributebalance >0 and tempvar<0 )THEN
update table2 set paid = distributebalance where id = inner_rec.id;
END IF;
END LOOP;
END LOOP;
END; $BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Thanks In advance :)
Postgres does allow for IF statements in loops.
The issue is that by writing ELSE IF you've started a new IF statement, so you now have 2 opening IFs but only one closing END IF. A "proper" elseif in plpgsql is ELSIF or ELSEIF. So just delete the space between those words and it should work.

Postgres trigger function to update aggregated result in another table

I have two tables. Table x and table y. Table x gets updated everyday. I wish to update the table y as soon as new data is inserted in Table x. Table y contains the aggregated value of all the update in Table x each day.
Date is in Date type and the rest of the two column are of real type.
Table_x can be updated daily and table_y should be updated automatically.
Table x:
Date Sales product
12/12/2017 4000 2
12/12/2017 3000 1
12/12/2017 2000 1
12/12/2017 5000 3
11/12/2017 1000 3
11/12/2017 2000 4
Table y (to be as updated as shown below):
Date Sales product
12/12/2017 14000 7
11/12/2017 3000 7
I wrote the trigger function as shown below but it updates each item rather than aggregated value.
CREATE OR REPLACE FUNCTION public.rec_insert_table_y()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO table_y ("Date","Sales","product")
SELECT NEW."Date",(sum(NEW."Sales")),(sum(NEW."product"))
GROUP BY NEW."Date";
RETURN NEW;
Trigger Function :
CREATE TRIGGER insert_into_table_y
AFTER INSERT
ON public.table_x
FOR EACH ROW
EXECUTE PROCEDURE public.rec_insert_table_y();
You can write a trigger that updates the aggregate value if exists or inserts if not.
Also you must be aware updates and deletes in the 'x' table:
create function y_x_trg() returns trigger
language plpgsql
as
$body$
declare
row_exists boolean;
begin
if tg_op<>'INSERT' then
update y
set sales = y.sales - old.sales,
product = y.product - old.product
where y.date = old.date
returning true into row_exists;
end if;
if tg_op<>'DELETE' then
update y
set sales = y.sales + new.sales,
product = y.product + new.product
where y.date = new.date
returning true into row_exists;
if row_exists is not true then
insert into y values (new.date, new.sales, new.product);
end if;
return new;
else
return null;
end if;
end;
$body$;
create trigger y_x_trg AFTER INSERT OR UPDATE OR DELETE ON x
FOR EACH ROW EXECUTE PROCEDURE y_x_trg();
You can see a running example at http://rextester.com/FVR79644