Choose schema of PostgreSQL trigger - postgresql

I have created simple trigger :
CREATE OR REPLACE FUNCTION emplacement_libre() RETURNS TRIGGER AS $BODY$
DECLARE
i int;
curseur CURSOR FOR
SELECT COUNT(*) FROM beta2.astres a
INNER JOIN beta2.planetes p ON a.id = p.astre_id
WHERE a.galaxie = NEW.galaxie AND
a.ss = NEW.ss AND
a.position = NEW.position;
BEGIN
OPEN curseur;
LOOP
FETCH curseur INTO i;
IF i>0 THEN
RAISE EXCEPTION 'la planète est déjà occupée';
END IF;
EXIT WHEN NOT FOUND;
END LOOP;
CLOSE curseur;
RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER emplacement_libre
BEFORE INSERT OR UPDATE ON beta2.astres
FOR EACH ROW EXECUTE PROCEDURE emplacement_libre();
In PostgreSQL doc, it is written about the name of a trigger :
The name to give the new trigger. This must be distinct from the name of any other trigger for the same table. The name cannot be schema-qualified — the trigger inherits the schema of its table. For a constraint trigger, this is also the name to use when modifying the trigger's behavior using SET CONSTRAINTS.
So I expected my trigger to get schema beta2 because I use it in my CREATE TRIGGER request but it's not the case, my trigger is created inside default public schema...why ?

I think the confusion is over the schema the trigger exists in vs the search_path of the trigger, i.e. the schema the function you call is.
The trigger is stored in the same schema as the table. However the search path for the procedure is handled at creation time based on your current search_path settings. So if you want to call a function in a specific schema, you probably want to call by the schema-qualified name.

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!

Syntax error while creating a trigger in PostgreSQL

I created a table name Student in PostgreSQL and then I tried defining a trigger on that table but it's showing an error message in doing so.
Trigger Syntax:
CREATE TRIGGER bi_Student BEFORE INSERT ON Student as $$
FOR EACH ROW
BEGIN
raise notice 'Successfully inserted into table(%)', user;
end $$;
Table Creation Command:
create table Student(Stu_id int, Stu_Name text, Stu_Age int, Stu_address char(30));
Actually I tried to declare the execution statements directly inside the trigger only rather than calling any procedure/ function from the trigger which is working fine but I want to do in this way in PostgreSQL.
PostgreSQL doesn't support it. You need trigger function always.
As documented in the manual you need a trigger function
create function my_trigger_function()
returns trigger
as
$$
begin
raise notice 'Successfully inserted into table(%)', user;
return new; --<< important (see the manual for details)
end
$$
language plpgsql;
Not sure what you intend with the user parameter there, as that is not the table name, but the current database user. If you want to display the actual table name, you need to use TG_RELNAME instead - which is an implicit variable available in the trigger function.
And a trigger definition
CREATE TRIGGER bi_Student
BEFORE INSERT ON Student
FOR EACH ROW
execute function my_trigger_function();

Postgres triggers and producers (column "new" of relation does not exist)

I am trying to create a trigger and procedure to update a last_changed_timestamp column upon UPDATE and INSERT.
I can register the function and trigger just fine, but when I try to update a record I receive the error.
CREATE OR REPLACE FUNCTION update_my_table_last_changed_timestamp()
RETURNS trigger AS
$BODY$
BEGIN
UPDATE my_table SET NEW.last_changed_timestamp = NOW();
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_my_table_last_changed_timestamp
BEFORE UPDATE
ON my_table
FOR EACH ROW
EXECUTE PROCEDURE update_my_table_last_changed_timestamp();
column "new" of relation "my_table" does not exist
I also do not fully understand how update_my_table_last_changed_timestamp knows which row it's suppose to update, nor if there were parameters passed to it, how the I would get those variables from the trigger to the procedure.
Modify the NEW record, there is no need to update.
BEGIN
NEW.last_changed_timestamp = NOW();
RETURN NEW;
END;
Read in the documentation: Overview of Trigger Behavior
If you still want to access a (other )table in the update trigger.
You can add to beginning of your trigger body the following:
EXECUTE format('SET search_path TO %I', TG_TABLE_SCHEMA);
For some reason with the update trigger it can happen that you're not on the correct search_path (i believe some old psql version have this)

PostgreSQL: Checking for NEW and OLD in a function for a trigger

I want to create a trigger which counts rows and updates a field in an other table. My current solution works for INSERT statements but failes when I DELETE a row.
My current function:
CREATE OR REPLACE FUNCTION update_table_count()
RETURNS trigger AS
$$
DECLARE updatecount INT;
BEGIN
Select count(*) into updatecount
From source_table
Where id = new.id;
Update dest_table set count=updatecount
Where id = new.id;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
The trigger is a pretty basic one, looking like.
CREATE TRIGGER count_trigger
AFTER INSERT OR DELETE
ON source_table
FOR EACH ROW
EXECUTE PROCEDURE update_table_count();
When I excute a DELETE statement the following error occurs:
ERROR: record "new" is not assigned yet
DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
I know one solution could be to create just one set of trigger and function for the DELETE and one for the INSERT statement. But I want to do it a bit more elegant and want to know, if there is a solution to check if NEW or OLD is present in the current context and just implement an IF ELSE block. But I dont know how to check for this context sensitive items.
Thanks for your help
The usual approach to make a trigger function do different things depending on how the trigger was fired is to check the trigger operation through TG_OP
CREATE OR REPLACE FUNCTION update_table_count()
RETURNS trigger AS
$$
DECLARE
updatecount INT;
BEGIN
if tg_op = 'UPDATE' then
select count(*) into updatecount from source_table where id = new.id;
update dest_table set count=updatecount where id = new.id;
elsif tg_op = 'DELETE' then
... do something else
end if;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
Unrelated, but: the language name is an identifier. Do not quote it using single quotes.
From PostgreSQL's documentation:
NEW
Data type RECORD; variable holding the new database row for INSERT/UPDATE operations in row-level triggers. This variable is null in statement-level triggers and for DELETE operations.
OLD
Data type RECORD; variable holding the old database row for UPDATE/DELETE operations in row-level triggers. This variable is null in statement-level triggers and for INSERT operations.
So, for example, if NEW is NULL, then the trigger was invoked on DELETE.

PostgreSQL triggers and temporary table

I have created an before update and after update trigger on a postgresql db table.
There is a requirement to preserve historical record and at the same time create a new record for the said data. Old record is to be marked as archived.
I was planning on using temporary table to keep track of the NEW values and reset the NEW values such that it is marked as archived.
In my after update trigger I would read the data from the temporary table, and create a brand new active record.
My problem is temporary table created in before update trigger is not visible to after update trigger. Moreover I cannot even pass on any argument (of type record) to the after update trigger as it is not allowed.
I have already achieved the desired result in Oracle db, using Global Temporary table, but having a tough time in PostgreSQL.
Here is the sample code for before update trigger function:
CREATE OR REPLACE FUNCTION trigger_fct_trig_trk_beforeupdate()
RETURNS trigger AS
$BODY$
DECLARE
some variable declarations;
BEGIN
Drop table IF EXISTS track_tmp_test;
CREATE TEMPORARY TABLE track_tmp_test(
...
);
Insert into track_tmp_test (........)
values(NEW., NEW..., NEW.., NEW...);
NEW... := OLD...;
NEW... := OLD.... ;
NEW... := OLD...;
Mark the NEW.status : = 'archived';
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
CREATE TRIGGER trig_trk_test_beforeupdate
BEFORE UPDATE ON test
FOR EACH ROW EXECUTE PROCEDURE trigger_fct_trig_trk_beforeupdate() ;
NOW the after UPDATE trigger function:
CREATE OR REPLACE FUNCTION trigger_fct_trg_trk_afterupdate()
RETURNS trigger AS
$BODY$
DECLARE
some variables;
-- insert into original table the data from temporary that was inserted in before update trigger
INSERT into TEST (....)
select ....
from track_tmp_test ;
-- delete data from temporary table after insert
delete from track_tmp_test ;
EXCEPTION
WHEN OTHERS THEN
-- Consider logging the error and then re-raise
RAISE;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Is there a way that after update trigger can access the temporary table created in before update trigger function?
I cannot have a permanent table hold he values, as trigger can be fired by many users updating the data in the table.
There is no problem with access to temporary table from triggers, and following code working without issue (on PostgreSQL 9.4):
CREATE OR REPLACE FUNCTION public.f1()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
begin
drop table if exists bubu;
create temp table bubu(a int);
insert into bubu values(10);
return new;
end
$function$
CREATE OR REPLACE FUNCTION public.f2()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
declare r record;
begin
for r in select * from bubu
loop
raise notice '%', r;
end loop;
return null;
end
$function$
create trigger xx
before insert on omega
for each row execute procedure f1();
create trigger yy
after insert on omega
for each row execute procedure f2();
postgres=# insert into omega values(333);
NOTICE: (10)
INSERT 0 1
So I am sure, so your problem will not be in access to temporary tables. It works well. There can be a issue on some 8.2, 8.3 and older with invalid plans due reference on dropped objects. Isn't it your problem?
I can say, so your design is wrong - there is not any reason, why you have to use a temp table. Same job you can do in after trigger. Any operations inside triggers should be fast, pretty fast. Dropping or creating temporary table is not fast operation.
If you have a older PostgreSQL release, you have not to drop temp table every. You should to delete content only. See a article http://postgres.cz/wiki/Automatic_execution_plan_caching_in_PL/pgSQL
The temporary table should be visible as #Pavel explains, but that's not the main issue here.
Your approach might make sense in Oracle with a global temporary table. But the posted Postgres code does not.
The trigger is fired for each row. You would (drop and) create a temp table for every row, and call another trigger, just to do what you could easily do in one trigger directly.
Instead, to keep the old row and set it to archived, plus INSERT a copy of the NEW row:
Demo table:
CREATE TEMP TABLE test (id int, txt text, archived bool DEFAULT FALSE);
Trigger func:
CREATE OR REPLACE FUNCTION trg_test_beforeupdate()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO test SELECT (NEW).*; -- insert a copy of the NEW row
SELECT (OLD).* INTO NEW; -- revert row to previous state
NEW.archived = TRUE; -- just set it to "archived"
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Trigger:
CREATE TRIGGER beforeupdate
BEFORE UPDATE ON test
FOR EACH ROW EXECUTE PROCEDURE trg_test_beforeupdate();
Test:
INSERT INTO test VALUES (1, 'foo'), (2, 'bar');
UPDATE test SET txt = 'baz' WHERE id = 1;
SELECT * FROM test;
Works.