Postgresql: create trigger to increment count_column between two tables? - postgresql

I have a table called car and another table called inventory.
car has a column called needs_oil_change (bool)
inventory has a column called oil_change_due_count INTEGER
I want to create a trigger that will incremented or decrement the inventory.oil_change_due_count whenever a car.oil_change_due_count is changed (or whenever a new car record is inserted, or deleted).
So, in short, I want a trigger to keep the inventory summary count column oil_change_due_count synchronized as car records are created/deleted/updated.
I've tried to follow some docs online like these:
https://w3resource.com/PostgreSQL/postgresql-triggers.php
https://dataegret.com/2017/10/postgresqls-transition-relations/
and others.
I haven't been able to create one that works yet.
How would I write a trigger that could handle that logic?
I just found this:
PostgreSQL: Checking for NEW and OLD in a function for a trigger
not sure if it will answer my question, but I will try to learn from that and see if I can apply to my question.

First of all you need to create a trigger function to handle the logic, in postgresql you can do something like this
CREATE OR REPLACE FUNCTION fn_oil_change() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
UPDATE inventory SET oil_change_due_count = oil_change_due_count - OLD.oil_change_due_count;
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
... some logic to hangle update
RETURN NEW
ELSIF (TG_OP = 'INSERT') THEN
... some logic to hangle update
RETURN NEW;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$$ LANGUAGE plpgsql;
and then create the trigger itself
CREATE TRIGGER tg_oil_change
AFTER INSERT OR UPDATE OR DELETE ON car
FOR EACH ROW EXECUTE PROCEDURE fn_oil_change();
Something more or less like this, right now I can't test the code, and you'll need to think how to handle the update.

Related

SELECT in cascaded AFTER DELETE trigger returning stale data in Postgres 11

I have an AFTER INSERT/UPDATE/DELETE trigger function which runs after any change to table campaigns and triggers an update on table contracts:
CREATE OR REPLACE FUNCTION update_campaign_target() RETURNS trigger AS $update_campaign_target$
BEGIN
UPDATE contracts SET updated_at = now() WHERE contracts.contract_id = NEW.contract_id;
END;
$update_campaign_target$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_campaign_target ON campaigns;
CREATE TRIGGER update_campaign_target AFTER INSERT OR UPDATE OR DELETE ON campaigns
FOR EACH ROW EXECUTE PROCEDURE update_campaign_target();
I have another trigger on table contracts that runs BEFORE UPDATE. The goal is to generate a computed column target which displays either contracts.manual_target (if set) or SUM(campaigns.target) WHERE campaign.contract_id = NEW.contract_id.
CREATE OR REPLACE FUNCTION update_contract_manual_target() RETURNS trigger AS $update_contract_manual_target$
DECLARE
campaign_target_count int;
BEGIN
IF NEW.manual_target IS NOT NULL
THEN
NEW.target := NEW.manual_target;
RETURN NEW;
ELSE
SELECT SUM(campaigns.target) INTO campaign_target_count
FROM campaigns
WHERE campaigns.contract_id = NEW.contract_id;
NEW.target := campaign_target_count;
RETURN NEW;
END IF;
END;
$update_contract_manual_target$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_contract_manual_target ON contracts;
CREATE TRIGGER update_contract_manual_target BEFORE INSERT OR UPDATE ON contracts
FOR EACH ROW EXECUTE PROCEDURE update_contract_manual_target();
This works as expected on INSERT and UPDATE on campaigns, but does not work on DELETE. When a campaign is deleted, the result of SUM(campaigns.target) in the second trigger includes the deleted campaign's target, and thus does not update the contracts.target column to the expected value. A second update of contracts will correctly set the value.
Three questions:
Why doesn't this work?
Is there a way to achieve the behavior I'm looking for using triggers?
For this type of data synchronization, is it better to achieve this using triggers or views? Triggers make sense to me because this is a table that we will read many magnitudes of times more than we'll write to it, but I'm not sure what the best practices are.
The reason this doesn't work is the usage of NEW.contract_id in the AFTER DELETE trigger:
UPDATE contracts SET updated_at = now() WHERE contracts.contract_id = NEW.contract_id;
Per the Triggers on Data Changes documentation, NEW is NULL for DELETE triggers.
Updating the code to use OLD instead of NEW fixes the issue:
CREATE OR REPLACE FUNCTION update_campaign_target() RETURNS trigger AS $update_campaign_target$
BEGIN
IF TG_OP = 'DELETE'
THEN
UPDATE contracts SET updated_at = now() WHERE contracts.contract_id = OLD.contract_id;
ELSE
UPDATE contracts SET updated_at = now() WHERE contracts.contract_id = NEW.contract_id;
END IF;
RETURN NULL;
END;
$update_campaign_target$ LANGUAGE plpgsql;
Thanks to Anthony Sotolongo and Belayer for your help!

Track Changes in Source table and load to History Table

I have created a view which has columns that i need to track for any change and load it into a history table. I created a Trigger as below and when i execute change, it only updates the History but does not add a new updated record. Any idea what im doing wrong?
create or replace function asset_h_fn() returns trigger
LANGUAGE plpgsql
as $$
begin
if (asset = 'Insert') then
insert into asset_history (sys_period,col1,col2,col3,col4,col5,col6)
values (tstzrange(lower(OLD.sys_period), current_timestamp), OLD.col1, OLD.col2, OLD.col3, OLD.col4, OLD.col5);
NEW.sys_period = tstzrange(current_timestamp,null);
return new;
return old;
end if;
end $$ ;
The RETURN OLD; in your code is fortunately unreachable; remove it.
You don't show us the CREATE TRIGGER statement, but it must be an INSTEAD OF trigger.
A view does not hold any data, it is an SQL statement with a name. So if you want a new row to appear in the view, you have to add a second INSERT to the trigger function that inserts a row into the table(s) on which the view is defined.
Well, It depends on how do you define your trigger...
Looking at your code I suppose you should use CREATE TRIGGER <trigger_name> BEFORE INSERT OR UPDATE ON <table_name> FOR EACH ROW EXECUTE PROCEDURE asset_h_fn();
The key here is BEFORE INSERT - if you wish to alter somehow inserted into original table data... If you don't wish to alter it, you should probably use AFTER INSERT.
You may read more about defining triggers and see some examples in official docs

Having multiple trigger events when redirecting insertions to partition tables

I am trying to set up triggers for insert and update events for the master table of some partition tables in PostgreSQL. Each time an insertion is made into the master table, the insert trigger event will redirect it into the correct partition table. Consequently, I will need to return NULL from this function call, since I don't want the master table to be populated as well. If the master table receives an update event, it will update a timestamp before making the change in the table. The problem is that the update trigger is never fired. I am using PostgreSQL version 9.6.
I have tried to combine the trigger functions into one, and merged the called trigger procedures into one as well, but the results are the same. The update trigger is only triggered if I return NEW from the insertion trigger function (which populates the master table), or if I comment out the insertion trigger function altogether.
DROP SCHEMA IF EXISTS test CASCADE;
CREATE SCHEMA test;
SET SCHEMA 'test';
CREATE TYPE test_type AS ENUM ('unit', 'performance');
CREATE TABLE test (
type test_type NOT NULL,
score INTEGER NOT NULL CHECK (score > 0),
id SERIAL PRIMARY KEY,
updated_at TIMESTAMP DEFAULT current_timestamp
);
CREATE TABLE performance_test (
CHECK (type = 'performance')
) INHERITS (test);
CREATE FUNCTION insert_test()
RETURNS trigger AS
$$
BEGIN
INSERT INTO performance_test VALUES (NEW.*);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION update_timestamp()
RETURNS trigger AS
$$
BEGIN
RAISE NOTICE 'This is never reached.';
UPDATE performance_test
SET updated_at = current_timestamp
WHERE id = NEW.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_insertion BEFORE INSERT ON test
FOR EACH ROW EXECUTE PROCEDURE insert_test();
CREATE TRIGGER test_update BEFORE UPDATE ON test
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();
---------------------------------------------------------------------------
INSERT INTO test VALUES ('performance', 10);
SELECT * FROM performance_test;
UPDATE test SET score = 20 WHERE id = 1;
SELECT * FROM performance_test;
I am not sure if it is possible to achieve what I want with this method, so I'm reaching out here for any advice. Thanks in advance!
/ Hampus
Row triggers must be defined on individual partitions, not the partitioned table. See https://www.postgresql.org/docs/10/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-LIMITATIONS
I don't know why the documentation for 9.6 doesn't mention this
working update trigger:
CREATE FUNCTION update_timestamp()
RETURNS trigger AS
$$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_update BEFORE UPDATE ON performance_test
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();
if you do UPDATE test SET score = 30, updated_at=DEFAULT; or UPDATE test SET score = 30, updated_at=current_timestamp; you might not need the update trigger.
Partitioning is not a free lunch because it has non-obvious effects on both behavior and performance, as you noticed by the trigger not behaving as you expected. If you make a mistake it can easily lead to failing queries and even bad data.
If you are really sure you need it you should make sure you understand it in detail and otherwise I'd recommend you to avoid it, most issues with slow queries can be solved by making sure the table statistics is up to date, using the right indexes, optimizing queries, changing Postgres configuration or adding more hardware.

Trigger update another table

I have been trying to write a trigger function that updates the rows in the child table when the parent is changed for a while now.
I have read Trigger procedure documentation but i have not really grasped how to build the functions.
This is what I have tried that does not work...
CREATE FUNCTION myschema.update_child() RETURNS trigger AS
$BODY$
BEGIN
UPDATE myschema.child
set new.number = parent.number
FROM myschema.parent
WHERE id = "id";
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql
Then the trigger
CREATE TRIGGER update_child_after_update
AFTER INSERT OR UPDATE OR DELETE
ON myschema.child
FOR EACH ROW
EXECUTE PROCEDURE myschema.update_child();
Does anyone have some tips to give?
Best regards
You don't need to use the parent table in the body of the trigger function, because the values from the parent table are available in the function in the special variable OLD and NEW. In this case you only need NEW.
If you need the trigger only on update, than define a update-only trigger:
CREATE or replace FUNCTION update_child() RETURNS trigger AS
$BODY$
BEGIN
UPDATE child
set number = NEW.number
WHERE id = NEW.id;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_child_after_update
AFTER UPDATE
ON parent
FOR EACH ROW
EXECUTE PROCEDURE update_child();

Postgres - update statement as a trigger

I've been playing around for the last hour or more trying to put an update statement into a trigger. I understand the concept of an UPDATE statement and the below works just fine
UPDATE cars SET country = 'France';
What I want is to put this into a trigger so that when the cars table is updated, the column country will automatically be updated with France.
I've played around with adapting Functions and Triggers that I've found out on the interweb but I'm obviously making the statement wrong as either they don't execute or they execute but don't update the country field when a new record is added.
CREATE FUNCTION update_country () RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'UPDATE') THEN
UPDATE cars SET country = 'France' WHERE id = New.id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql; --The trigger used to update a table.
CREATE TRIGGER update_country_col BEFORE UPDATE ON cars FOR EACH ROW EXECUTE PROCEDURE update_country();
The above scripts executes but does not add France to the country column.
The function was adapted from a statement that I found out on the web.
Postgres 9.1.
I know that the answer is going to be so simple!
In update triggers you should modify NEW record.
Also, you may need to return NEW record from procedure.
So, you should use following procedure instead of yours:
CREATE FUNCTION update_country () RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'UPDATE') THEN
NEW.country = 'France';
END IF;
RETURN NEW;
END; $$ LANGUAGE plpgsql;