Postgres - Create trigger that can exist in multiple schemas - postgresql

I have a postgres trigger that I have written. It works 100%, I've tested it thoroughly. However, when my application writes to that database table, it fails. I'll put the code below, then I'll explain the failure.
CREATE OR REPLACE FUNCTION validate_client_user_role()
RETURNS trigger AS
$BODY$
DECLARE role_has_client INT;
DECLARE user_has_client INT;
BEGIN
IF NEW.client_id IS NULL THEN
RAISE EXCEPTION 'client_id cannot be null';
END IF;
IF NEW.user_id IS NULL THEN
RAISE EXCEPTION 'user_id cannot be null';
END IF;
IF NEW.role_id IS NULL THEN
RAISE EXCEPTION 'role_id cannot be null';
END IF;
SELECT COUNT(*)
INTO role_has_client
FROM roles
WHERE id = NEW.role_id
AND client_id = NEW.client_id;
SELECT COUNT(*)
INTO user_has_client
FROM client_users
WHERE user_id = NEW.user_id
AND client_id = NEW.client_id;
IF role_has_client = 0 THEN
RAISE EXCEPTION 'Role is not allowed by client';
END IF;
IF user_has_client = 0 THEN
RAISE EXCEPTION 'User is not allowed by client';
END IF;
RETURN NEW;
END
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER client_user_role_validation
BEFORE INSERT OR UPDATE
ON client_user_roles
FOR EACH ROW
EXECUTE PROCEDURE validate_client_user_role();
So, my application database has multiple schemas, one for dev, qa, prod, etc. When I first write this DDL statement, I run this first:
set search_path to dev;
This then adds it properly to the "dev" schema. As long as I'm in my DB query console, I can validate this trigger is working perfectly.
However, when my application tries to write to the tables, the trigger fails, saying the relations (roles, client_users) that it tries to do selects from don't exist. I can fix this by modifying the function code to explicitly reference the "dev" schema for each table. I don't want to do this, though. I would prefer code that I can execute as a DDL statement for dev/qa/prod without needing to replace all the schema references or keep multiple copies of it.
I'm hoping someone can explain if there is some way to do this. Obviously the trigger is not inheriting from the schema it is assigned to when I execute it. However, if there is some postgres trick I'm not aware of to make this work, I would appreciate the guidance. Thank you.

You can use the TG_TABLE_SCHEMA variable and set_config() with IS_LOCAL = true to accomplish this:
CREATE OR REPLACE FUNCTION validate_client_user_role()
RETURNS trigger AS
$BODY$
DECLARE role_has_client INT;
DECLARE user_has_client INT;
BEGIN
IF NEW.client_id IS NULL THEN
RAISE EXCEPTION 'client_id cannot be null';
END IF;
IF NEW.user_id IS NULL THEN
RAISE EXCEPTION 'user_id cannot be null';
END IF;
IF NEW.role_id IS NULL THEN
RAISE EXCEPTION 'role_id cannot be null';
END IF;
PERFORM set_config('search_path', TG_TABLE_SCHEMA, true); -- <-- This line
SELECT COUNT(*)
INTO role_has_client
FROM roles
WHERE id = NEW.role_id
AND client_id = NEW.client_id;
SELECT COUNT(*)
INTO user_has_client
FROM client_users
WHERE user_id = NEW.user_id
AND client_id = NEW.client_id;
IF role_has_client = 0 THEN
RAISE EXCEPTION 'Role is not allowed by client';
END IF;
IF user_has_client = 0 THEN
RAISE EXCEPTION 'User is not allowed by client';
END IF;
RETURN NEW;
END
$BODY$ LANGUAGE plpgsql;

Related

Trigger function can't update table

Here is my trigger function
CREATE OR REPLACE FUNCTION test_table_insert()
RETURNS TRIGGER
AS $$
BEGIN
IF NEW.id IS NULL THEN
RAISE EXCEPTION 'id is null';
END IF;
UPDATE e_sub_agreement SET ro_id = NEW.id WHERE id = NEW.id;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER test_table_insert AFTER INSERT ON e_sub_agreement FOR EACH ROW EXECUTE PROCEDURE test_table_insert();
The problem is that it doesn't update table e_sub_agreement. I checked NEW value and everything is good. It returns with the new id. If I change where statement id = "some existing id in table", then it works. It changes ro_id to the new id. How is it possible? My guess is that data has not been inserted into table and trigger function can't find row with the given id. But it's not how trigger's after insert function works. What's the magic?
An AFTER trigger can not change anything. Running an additional UPDATE is also quite inefficient. Change this to a BEFORE trigger and assign the value you want:
CREATE OR REPLACE FUNCTION test_table_insert()
RETURNS TRIGGER
AS $$
BEGIN
IF NEW.id IS NULL THEN
RAISE EXCEPTION 'id is null';
END IF;
NEW.ro_id := NEW.id;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER test_table_insert
BEFORE INSERT ON e_sub_agreement
FOR EACH ROW EXECUTE PROCEDURE test_table_insert();
Note that the NOT NULL check is better done by defining the column as NOT NULL.

PostgreSQL 10 error relation new doesn't exist on before update trigger

My before update trigger throws an error on updating single row in table company.
ERROR: relation "new" does not exist, row 7.
Trigger and trigger function code:
CREATE OR REPLACE FUNCTION public.check_company_transitivity()
RETURNS trigger
AS $$
DECLARE
cmp uuid;
head_company uuid;
audited_company uuid;
BEGIN
audited_company := (select NEW.id from NEW);
cmp:=audited_company;
head_company:='*';
while head_company is not null loop
head_company:=(select head_company from company where id=cmp);
if audited_company=head_company then
raise exception 'transitive closure';
else
cmp=head_company;
end if;
end loop;
return new;
END;
$$ LANGUAGE plpgsql;
create trigger check_company_transitivity
before update
on
public.company for each row execute procedure check_company_transitivity();
I can’t understand what the problem is, I would be appreciate for any help.
You can use the values from NEW directly
audited_company := NEW.id;

Trigger in postgres to lock table with a list

I have an issue regarding a Triggers with PostgreSQL 10. Here is the situation.
I have a table called index_name which contain à column named index_ref. Into this field I have created a list of value as example: PBM PI, PBM PO, etc.
I would like to use this table to store valid name as reference for another table called gis_osm_places.
So, whenever someone try to insert a value not into the list then an exception message will pop-up to say: NOT ALLOWED COMMIT. PLEASE USE: (reference list)
Here are my tables:
Here is where I am with the trigger:
CREATE FUNCTION public.check_column_value()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$
DECLARE
ref_allowed character varying;
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE'
THEN
IF NEW.index_a is not null OR NEW.index_a NOT IN (SELECT index_ref from public.index_name)
THEN
ref_allowed := (SELECT string_agg(index_ref,',') from public.index_name);
RAISE EXCEPTION 'NOT ALLOWED COMMIT. PLEASE USE : %',ref_allowed;
END IF;
RETURN NEW;
END IF;
END;
$BODY$;
ALTER FUNCTION public.check_column_value()
OWNER TO "postgres";
CREATE TRIGGER check_column_value
BEFORE INSERT
ON public.gis_osm_places
FOR EACH ROW
EXECUTE PROCEDURE public.check_column_value();
Actually nothing is happening, I mean I can add whatever I want without an error.
Any idea or upgrade of the code would be greatly appreciate.
Thanks in advance !
You do not need not and do not want a trigger for this. This functionality is build in. Just create a Foreign Key constraint on gis_osm_places referencing index_name.
alter table gis_osm_places
add constraint osm_places2index_fk
foreign key (index_a)
references index_name(index_ref);
Now drop your trigger and the trigger function. The downside being you do not get the message you have created. But you handle that in your exception processing in the your app.
I finally found what went wrong !
CREATE FUNCTION public.check_column_value()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$
DECLARE
ref_allowed character varying;
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE'
THEN
IF NEW.index_a is not null AND NEW.index_a NOT IN (SELECT index_ref from public.index_name)
THEN
ref_allowed := (SELECT string_agg(index_ref,',') from public.index_name);
RAISE EXCEPTION 'NOT ALLOWED COMMIT. PLEASE USE : %',ref_allowed;
END IF;
RETURN NEW;
END IF;
END;
$BODY$;
ALTER FUNCTION public.check_column_value()
OWNER TO "postgres";
CREATE TRIGGER check_column_value
BEFORE INSERT OR UPDATE
ON public.gis_osm_places
FOR EACH ROW
EXECUTE PROCEDURE public.check_column_value();

how to create triggers function before insert show test message

i have a table
CREATE TABLE test.emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
and function
CREATE FUNCTION test.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;
how to show message no need to update the values how can i modify my function
trigger is
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON test.emp
FOR EACH ROW EXECUTE PROCEDURE test.emp_stamp();
I think all you need to do is 1) make sure it's an update and not an insert (otherwise the reference to old won't make any sense and 2) compare to the existing record. Since Employee name seems to be the PK and the last two fields are only to capture changes, my guess is you only want to test the salary:
-- Who works for us when she must pay for it?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
elsif TG_OP = 'UPDATE' and new.salary = old.salary then
RAISE EXCEPTION 'salary is already %', NEW.salary;
END IF;
But of course you could also use this for any field. If you wanted to make sure at least one changed, it would be something like this:
elsif TG_OP = 'UPDATE' and
new.salary = old.salary and
new.last_date = old.last_date and
new.last_user = old.last_user then
RAISE EXCEPTION 'Update does not alter any data';
END IF;

Postgres trigger function: substitute value before insert or update

I've looked up pretty much everything I could find regarding this issue, but I still don't understand what is wrong with this trigger:
CREATE OR REPLACE FUNCTION func_SubstitutePostLatLng_Upt()
RETURNS trigger AS
$BODY$
BEGIN
IF OLD.post_latlng IS NULL AND NEW.post_latlng IS NULL AND NEW.place_guid IS NOT NULL THEN
raise notice 'SELECT';
SELECT place.geom_center, place.city_guid
INTO NEW.post_latlng, NEW.city_guid
FROM public.place
WHERE (place.origin_id, place.place_guid) IN (VALUES (NEW.origin_id,NEW.place_guid));
raise notice 'Value db_geom: %', NEW.post_latlng;
raise notice 'Value db_city_guid: %', NEW.city_guid;
IF NEW.post_latlng IS NOT NULL THEN
NEW.post_geoaccuracy = 'place';
IF NEW.city_guid IS NOT NULL THEN
SELECT country_guid INTO NEW.country_guid
FROM public.city WHERE (origin_id, city_guid) IN (VALUES (NEW.origin_id,NEW.city_guid));
END IF;
END IF;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
DROP TRIGGER IF EXISTS trig_SubstitutePostLatLng_Upd on public.post;
CREATE TRIGGER trig_SubstitutePostLatLng_Upd
BEFORE UPDATE
ON public.post
FOR EACH ROW
WHEN (pg_trigger_depth() < 1)
EXECUTE PROCEDURE func_SubstitutePostLatLng_Upt()
(I have a second similar trigger for insert)
The code is supposed to do the following:
On Update on table "post", check if no post_latlng is submitted (=NULL), and if yes, substitute post_latlng from table place (geom_center), if available.
However, no matter what I do, I get the following when updating an entry in table "post" (=triggering the above trigger):
NOTICE: SELECT
NOTICE: Value db_geom: <NULL>
NOTICE: Value db_city_guid: <NULL>
INSERT 0 1
Query returned successfully in 47 msec.
The test-data for place_guid, geom_center etc. is definitely available and both
raise notice 'Value db_geom: %', NEW.post_latlng;
raise notice 'Value db_city_guid: %', NEW.city_guid;
should not output NULL.
There were several smaller issues, it now works. Here is a more cleaner code that uses variables in between:
CREATE OR REPLACE FUNCTION func_SubstitutePostLatLng_Upt()
RETURNS trigger AS
$BODY$
DECLARE
db_geom_center text;
db_city_guid text;
db_country_guid text;
BEGIN
IF OLD.post_latlng IS NULL AND NEW.post_latlng IS NULL AND NEW.place_guid IS NOT NULL THEN
SELECT place.geom_center, place.city_guid
INTO db_geom_center, db_city_guid
FROM public.place
WHERE (place.origin_id, place.place_guid) IN (VALUES (NEW.origin_id,NEW.place_guid));
IF db_geom_center IS NOT NULL THEN
NEW.post_latlng = db_geom_center;
NEW.post_geoaccuracy = 'place';
END IF;
IF db_city_guid IS NOT NULL THEN
NEW.city_guid = db_city_guid;
SELECT city.country_guid
INTO db_country_guid
FROM public.city
WHERE (city.origin_id, city.city_guid) IN (VALUES (NEW.origin_id,db_city_guid));
NEW.country_guid = db_country_guid;
END IF;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;