PostgreSQL Trigger Function get stack and does not update the table - sql

I have a trigger that calls a function. It basically must update the same table after update. But it get stacks and does not update anything.
This is my trigger:
CREATE OR REPLACE FUNCTION invtransferences_products_after()
RETURNS TRIGGER AS
$BODY$
DECLARE
TR invtransferences_products%ROWTYPE;
v_transfer_cost NUMERIC;
BEGIN
IF(TG_OP='INSERT') THEN
TR := NEW;
RAISE NOTICE 'INVTRANSFERENCE PRODUCT ADDED %',TR.id;
UPDATE invtransferences_products
SET product_cost = (get_product_composition_cost(product_id, 0)*quantity )
WHERE invtransferences_products.id=TR.id;
ELSE
IF (TG_OP='UPDATE') THEN
TR := NEW;
RAISE NOTICE 'INVTRANSFERENCE PRODUCTS UPDATED %',TR.id;
UPDATE invtransferences_products
SET product_cost = (get_product_composition_cost(product_id, 0)*quantity )
WHERE invtransferences_products.id=TR.id;
END IF;
END IF;
RETURN TR;
END
$BODY$
LANGUAGE plpgsql;
This is my table invtransferences_products:
CREATE TABLE invtransferences_products
(
id serial NOT NULL,
invtransference_id bigint NOT NULL,
product_id bigint NOT NULL,
quantity numeric DEFAULT 1 NOT NULL,
created timestamp DEFAULT now() NOT NULL,
modified timestamp,
rcv_quantity numeric DEFAULT 0 NOT NULL,
pnd_quantity numeric DEFAULT 0 NOT NULL,
product_cost numeric
);
ALTER TABLE invtransferences_products
ADD CONSTRAINT invtransferences_products_pkey
PRIMARY KEY (id);
ALTER TABLE invtransferences_products
ADD CONSTRAINT invtransferences_products_invtransference_id_fkey FOREIGN KEY (invtransference_id)
REFERENCES invtransferences (id)
ON UPDATE CASCADE
ON DELETE CASCADE;
COMMIT;
What's wrong?? Help please.

The problem is that the UPDATE statement in the trigger function causes the trigger to fire again.
Instead of issuing a separate update, you should manipulate the data in NEW.
Something like:
CREATE OR REPLACE FUNCTION invtransferences_products_after()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF(TG_OP='INSERT') THEN
RAISE NOTICE 'INVTRANSFERENCE PRODUCT ADDED %',NEW.id;
ELSE
IF (TG_OP='UPDATE') THEN
RAISE NOTICE 'INVTRANSFERENCE PRODUCTS UPDATED %',NEW.id;
END IF;
END IF;
NEW.product_cost := get_product_composition_cost(NEW.product_id,0)*NEW.quantity ;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
Fiddle at: SQLFiddle

This trigger will cause infinite recursion ending up with a stack depth limit exceeded error, because it issues another UPDATE of the table each time an INSERT/UPDATE occurs on the same table.
The solution is, instead of this:
UPDATE invtransferences_products
SET product_cost = (get_product_composition_cost(product_id, 0)*quantity )
WHERE invtransferences_products.id=TR.id;
It should do that:
NEW.product_cost := get_product_composition_cost(NEW.product_id, 0)*NEW.quantity;
and declare the trigger as running BEFORE UPDATE or INSERT (not AFTER).
That's the more logical approach.
As a workaround, recursion can also be blocked outside of the trigger. This is answered in Prevent recursive trigger in PostgreSQL.

Related

PostgreSQL-Update last_update_date to current if modif at table

I'm trying to create a function with a trigger which when modifying a field from table tb_customer, the attribute last_update_date should be updated to the current_date of the modification.
If an user tries to enter a value in attribute last_update_date, an error should be raised with a message, and not allow this insert.
The table code for creation is:
CREATE TABLE erp.tb_customer (
cust_no CHARACTER(5) NOT NULL,
cust_name CHARACTER VARYING(50) NOT NULL,
cust_cif CHARACTER VARYING(150) NOT NULL,
last_updated_by CHARACTER VARYING(20) DEFAULT 'SYSTEM',
last_update_date DATE NOT NULL,
CONSTRAINT pk_customer PRIMARY KEY (cust_no)
);
So far, I have this code:
CREATE OR REPLACE FUNCTION modif_update_date_tracker()
RETURNS trigger AS $$
BEGIN
IF INSERT AT last_update_date THEN
RAISE EXCEPTION 'Not possible to update this field'
END IF;
NEW.last_update_date := current_time;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
------------------------------------------------------------------------------------------------
-- Create trigger 1
------------------------------------------------------------------------------------------------
CREATE TRIGGER trigger_modif_update_date_tracker
BEFORE UPDATE
ON tb_customer
FOR EACH ROW
EXECUTE PROCEDURE modif_update_date_tracker();
BEGIN
IF OLD.last_update_date!=NEW.last_update_date THEN
RAISE EXCEPTION 'Not possible to update this field';
END IF;
NEW.last_update_date := CURRENT_TIMESTAMP;
RETURN NEW;
END;
When a user is not allowed to update a column, you should REVOKE him from doing so. You don't need a trigger for that.
testing code is something you can do yourself. But I can already tell you that UDAPTE is not a valid value for TG_OP.
A complete trigger example can be found in the manual.
It needs to be created as a FUNCTION, not as a PROCEDURE. There are other problems as well, but I don't know which ones are real and which ones you introduced while retyping it.

TRIGGER BEFORE DELETE, doesn't delete data in table

I'm working on history of my database when a row is modify/delete.
My main table is "bati" and history table "bati_history", when a row is delete or modify, the trigger is suppose to insert into bati_history all the old data, then delete in the main table (bati). But with my code, the row is insert into the history but not delete in the main table and I don't know why.
I'm on PostgreSQL 11.2 64-bit
The code :
Main table:
CREATE TABLE IF NOT EXISTS bati(
id_bati BIGSERIAL NOT NULL UNIQUE,
code_bati VARCHAR(25) NOT NULL,
code_parcelle CHAR(50) NOT NULL,
...);
History table:
CREATE TABLE IF NOT EXISTS bati_history(
id_history BIGSERIAL NOT NULL PRIMARY KEY,
event CHAR(10) NOT NULL,
date_save_history TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
id_bati BIGINT NOT NULL,
code_bati VARCHAR(25) NOT NULL,
code_parcelle CHAR(50) NOT NULL,
...);
The function
CREATE FUNCTION archive_bati() RETURNS TRIGGER AS $BODY$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO bati_history (event,id_bati,code_bati,code_parcelle,...)
VALUES ('DELETE',OLD.id_bati,OLD.code_bati,OLD.code_parcelle,OLD....);
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO bati_history (event,id_bati,code_bati,code_parcelle,...)
VALUES ('UPDATE',OLD.id_bati,OLD.code_bati,OLD.code_parcelle,OLD....);
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
TRIGGERS:
CREATE TRIGGER bati_trigger_before_delete BEFORE DELETE
ON bati FOR EACH ROW
EXECUTE PROCEDURE archive_bati();
CREATE TRIGGER bati_trigger_before_update BEFORE UPDATE
ON bati FOR EACH ROW
EXECUTE PROCEDURE archive_bati();
When I try DELETE FROM bati;, I expect to copy every row in bati_history, then delete them from bati, but they are not delete from bati, and I have this output without error :
test=# INSERT INTO bati (id_bati,code_bati,code_parcelle,id_interne) VALUES (5,'CODEBATI001','CODEPARC001',02);
INSERT 0 1
test=# INSERT INTO bati (id_bati,code_bati,code_parcelle,id_interne) VALUES (6,'CODEBATI002','CODEPARC002',02);
INSERT 0 1
test=# DELETE FROM bati;
DELETE 0
(sorry for my english I'm french)
You should use the if-else branching to either return NEW OR OLD depending on the trigger operation. The variable TG_OP has a text type & could be used in the insert query directly.
So, the function definition becomes:
CREATE FUNCTION archive_bati()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO bati_history (event,id_bati,code_bati,code_parcelle)
VALUES (TG_OP, OLD.id_bati, OLD.code_bati, OLD.code_parcelle);
IF TG_OP = 'DELETE'
THEN RETURN OLD;
ELSE RETURN NEW;
END IF;
END;
$$ LANGUAGE PLPGSQL;
Also, it seems unnecessary to me to define two triggers when 1 will suffice:
CREATE TRIGGER bati_trigger_before_update BEFORE UPDATE OR DELETE
ON bati FOR EACH ROW
EXECUTE PROCEDURE archive_bati();
When you delete a row, NEW is null. If the before trigger returns a null, it means the operation should be cancelled. You should then return OLD for deletions, and NEW for updates.
From the doc:
In the case of a before-trigger on DELETE, the returned value has no
direct effect, but it has to be nonnull to allow the trigger action to
proceed. Note that NEW is null in DELETE triggers, so returning that
is usually not sensible. The usual idiom in DELETE triggers is to
return OLD.

Is it possible to create a cross relationship constraint in postgresql? [duplicate]

I would like to add a constraint that will check values from related table.
I have 3 tables:
CREATE TABLE somethink_usr_rel (
user_id BIGINT NOT NULL,
stomethink_id BIGINT NOT NULL
);
CREATE TABLE usr (
id BIGINT NOT NULL,
role_id BIGINT NOT NULL
);
CREATE TABLE role (
id BIGINT NOT NULL,
type BIGINT NOT NULL
);
(If you want me to put constraint with FK let me know.)
I want to add a constraint to somethink_usr_rel that checks type in role ("two tables away"), e.g.:
ALTER TABLE somethink_usr_rel
ADD CONSTRAINT CH_sm_usr_type_check
CHECK (usr.role.type = 'SOME_ENUM');
I tried to do this with JOINs but didn't succeed. Any idea how to achieve it?
CHECK constraints cannot currently reference other tables. The manual:
Currently, CHECK expressions cannot contain subqueries nor refer to
variables other than columns of the current row.
One way is to use a trigger like demonstrated by #Wolph.
A clean solution without triggers: add redundant columns and include them in FOREIGN KEY constraints, which are the first choice to enforce referential integrity. Related answer on dba.SE with detailed instructions:
Enforcing constraints “two tables away”
Another option would be to "fake" an IMMUTABLE function doing the check and use that in a CHECK constraint. Postgres will allow this, but be aware of possible caveats. Best make that a NOT VALID constraint. See:
Disable all constraints and table checks while restoring a dump
A CHECK constraint is not an option if you need joins. You can create a trigger which raises an error instead.
Have a look at this example: http://www.postgresql.org/docs/9.1/static/plpgsql-trigger.html#PLPGSQL-TRIGGER-EXAMPLE
CREATE TABLE emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
CREATE FUNCTION 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;
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
...i did it so (nazwa=user name, firma = company name) :
CREATE TABLE users
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
nazwa character varying(20),
firma character varying(50)
);
CREATE TABLE test
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
firma character varying(50),
towar character varying(20),
nazwisko character varying(20)
);
ALTER TABLE public.test ENABLE ROW LEVEL SECURITY;
CREATE OR REPLACE FUNCTION whoIAM3() RETURNS varchar(50) as $$
declare
result varchar(50);
BEGIN
select into result users.firma from users where users.nazwa = current_user;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE POLICY user_policy ON public.test
USING (firma = whoIAM3());
CREATE FUNCTION test_trigger_function()
RETURNS trigger AS $$
BEGIN
NEW.firma:=whoIam3();
return NEW;
END
$$ LANGUAGE 'plpgsql'
CREATE TRIGGER test_trigger_insert BEFORE INSERT ON test FOR EACH ROW EXECUTE PROCEDURE test_trigger_function();

Postgres trigger-based insert redirection without breaking RETURNING

I'm using table inheritance in postgres, but the trigger I'm using to partition data into the child tables isn't quite behaving right. For example, this query returns nil, but I would like it to return the id of the new record.
INSERT INTO flags (flaggable_id, flaggable_type)
VALUES (233, 'Thank')
RETURNING id;
If I change the return value of the trigger function from NULL to NEW, I get the desired RETURNING behavior, but then two identical rows are inserted in the database. This makes sense, since a non-null return value from the trigger function causes the original INSERT statement execute, whereas returning NULL causes the statement to halt execution. A unique index might halt the second insertion, but would probably raise an error.
Any ideas how to make the INSERT with RETURNING work properly with a trigger like this?
CREATE TABLE flags (
id integer NOT NULL,
flaggable_type character varying(255) NOT NULL,
flaggable_id integer NOT NULL,
body text
);
ALTER TABLE ONLY flags
ADD CONSTRAINT flags_pkey PRIMARY KEY (id);
CREATE TABLE "comment_flags" (
CHECK ("flaggable_type" = 'Comment'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "comments"("id")
) INHERITS ("flags");
CREATE TABLE "profile_flags" (
CHECK ("flaggable_type" = 'Profile'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "profiles"("id")
) INHERITS ("flags");
CREATE OR REPLACE FUNCTION flag_insert_trigger_fun() RETURNS TRIGGER AS $BODY$
BEGIN
IF (NEW."flaggable_type" = 'Comment') THEN
INSERT INTO comment_flags VALUES (NEW.*);
ELSIF (NEW."flaggable_type" = 'Profile') THEN
INSERT INTO profile_flags VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Wrong "flaggable_type"="%", fix flag_insert_trigger_fun() function', NEW."flaggable_type";
END IF;
RETURN NULL;
END; $BODY$ LANGUAGE plpgsql;
CREATE TRIGGER flag_insert_trigger
BEFORE INSERT ON flags
FOR EACH ROW EXECUTE PROCEDURE flag_insert_trigger_fun();
The only workaround I found, is to create a view for the base table & use INSTEAD OF triggers on that view:
CREATE TABLE flags_base (
id integer NOT NULL,
flaggable_type character varying(255) NOT NULL,
flaggable_id integer NOT NULL,
body text
);
ALTER TABLE ONLY flags_base
ADD CONSTRAINT flags_base_pkey PRIMARY KEY (id);
CREATE TABLE "comment_flags" (
CHECK ("flaggable_type" = 'Comment'),
PRIMARY KEY ("id")
) INHERITS ("flags_base");
CREATE TABLE "profile_flags" (
CHECK ("flaggable_type" = 'Profile'),
PRIMARY KEY ("id")
) INHERITS ("flags_base");
CREATE OR REPLACE VIEW flags AS SELECT * FROM flags_base;
CREATE OR REPLACE FUNCTION flag_insert_trigger_fun() RETURNS TRIGGER AS $BODY$
BEGIN
IF (NEW."flaggable_type" = 'Comment') THEN
INSERT INTO comment_flags VALUES (NEW.*);
ELSIF (NEW."flaggable_type" = 'Profile') THEN
INSERT INTO profile_flags VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Wrong "flaggable_type"="%", fix flag_insert_trigger_fun() function', NEW."flaggable_type";
END IF;
RETURN NEW;
END; $BODY$ LANGUAGE plpgsql;
CREATE TRIGGER flag_insert_trigger
INSTEAD OF INSERT ON flags
FOR EACH ROW EXECUTE PROCEDURE flag_insert_trigger_fun();
But this way you must supply the id field on each insertion (even if flags_base's primary key has a default value / is a serial), so you must prepare your insert trigger to fix NEW.id if it is a NULL.
UPDATE: It seems views' columns can have a default values too, set with
ALTER VIEW [ IF EXISTS ] name ALTER [ COLUMN ] column_name SET DEFAULT expression
which is only used in views have an insert/update rule/trigger.
http://www.postgresql.org/docs/9.3/static/sql-alterview.html
#pozs provided a correct answer but didn't quite provide the code for a full working implementation. I tried to include the code in an edit on his question, but it was not accepted. He instead suggested yet another approach, which looks cleaner, but may have some drawbacks (in the case where you re-use your trigger function elsewhere).
Including my solution here for reference:
CREATE TABLE base_flags (
id integer NOT NULL,
flaggable_type character varying(255) NOT NULL,
flaggable_id integer NOT NULL,
body text
);
ALTER TABLE ONLY base_flags
ADD CONSTRAINT base_flags_pkey PRIMARY KEY (id);
CREATE SEQUENCE base_flags_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE base_flags_id_seq OWNED BY base_flags.id;
CREATE OR REPLACE VIEW flags AS SELECT * FROM base_flags;
CREATE TABLE "comment_flags" (
CHECK ("flaggable_type" = 'Comment'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "comments"("id")
) INHERITS ("flags");
CREATE TABLE "profile_flags" (
CHECK ("flaggable_type" = 'Profile'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "profiles"("id")
) INHERITS ("flags");
CREATE OR REPLACE FUNCTION flag_insert_trigger_fun() RETURNS TRIGGER AS $BODY$
BEGIN
IF NEW.id IS NULL THEN
NEW.id := nextval('base_flags_id_seq');
END IF;
IF (NEW."flaggable_type" = 'Comment') THEN
INSERT INTO comment_flags VALUES (NEW.*);
ELSIF (NEW."flaggable_type" = 'Profile') THEN
INSERT INTO profile_flags VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Wrong "flaggable_type"="%", fix flag_insert_trigger_fun() function', NEW."flaggable_type";
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER flag_insert_trigger
INSTEAD OF INSERT ON base_flags
FOR EACH ROW EXECUTE PROCEDURE flag_insert_trigger_fun();

Trigger to delete rows from related tables before deleting rows from actual table

I have the following tables:
CREATE TABLE QUESTION(
id varchar(10) NOT NULL PRIMARY KEY,
que_type numeric(1));
CREATE TABLE ESSAY(
que_id varchar(10) NOT NULL PRIMARY KEY,
ans varchar(2000),
FOREIGN KEY (que_id) REFERENCES QUESTION (id));
CREATE TABLE TFFB(
que_id varchar(10) NOT NULL PRIMARY KEY,
ans varchar(50),
FOREIGN KEY (que_id) REFERENCES QUESTION (id));
CREATE TABLE MCQ(
que_id varchar(10) NOT NULL PRIMARY KEY,
ans varchar(200),
FOREIGN KEY (que_id) REFERENCES QUESTION (id));
and try to create trigger so that when I delete from the main table, it will delete related rows from other tables:
CREATE OR REPLACE FUNCTION delete_question()
RETURNS trigger AS $delete_question$
DECLARE
BEGIN
IF ( (OLD).que_type = '1' ) THEN
IF EXISTS (SELECT 1 FROM mcq WHERE person_id = (OLD).id) THEN
DELETE FROM mcq WHERE que_id='(OLD).id';
END IF;
ELSIF ( (OLD).que_type = '2' OR OLD.que_type = '3' ) THEN
IF EXISTS (SELECT 1 FROM tffb WHERE person_id = (OLD).id) THEN
DELETE FROM tffb WHERE que_id='(OLD).id';
END IF;
ELSIF ( (OLD).que_type = '4' ) THEN
IF EXISTS (SELECT 1 FROM essay WHERE person_id = (OLD).id) THEN
DELETE FROM essay WHERE que_id='(OLD).id';
END IF;
END IF;
RETURN NULL;
END;
$delete_question$ LANGUAGE plpgsql;
CREATE TRIGGER delete_question
BEFORE DELETE ON question
FOR EACH ROW EXECUTE PROCEDURE delete_question();
When I delete data from question, the row disappear for a while. But when I refresh, it still there.
I tried to put RETURN OLD; but it failed because of the constrain relation.
What is wrong with this?
Some more advice on your trigger function:
CREATE OR REPLACE FUNCTION delete_question()
RETURNS trigger AS
$func$
BEGIN
CASE OLD.que_type
WHEN 1 THEN
DELETE FROM mcq WHERE que_id=OLD.id;
WHEN 2, 3 THEN
DELETE FROM tffb WHERE que_id=OLD.id;
WHEN 4 THEN
DELETE FROM essay WHERE que_id=OLD.id;
-- ELSE
-- Do something?
END CASE;
RETURN OLD;
END
$func$ LANGUAGE plpgsql;
Major points
Your check for existence with a SELECT statement doubles the cost. Just run the DELETE, if no matching row is found, nothing is deleted.
Use a CASE statement here. Shorter, faster. Note that plpgsql CASE is slightly different from SQL CASE statement. For instance, you can list several cases at once.
You don't need the DECLARE keyword, unless you actually declare variables.
Alternative design
You could avoid the problem altogether by cascading deletes via foreign key, as #a_horse mentioned in the comment. My schema layout would look like this:
CREATE TABLE question (
question_id serial NOT NULL PRIMARY KEY
,que_type int -- this may be redundant as well
);
CREATE TABLE essay (
que_id int NOT NULL PRIMARY KEY
REFERNECES question(question_id) ON UPDATE CASCADE
ON DELETE CASCADE
,ans text
);
...
About serial:
Auto increment SQL function
i then try solve it again.... IT WORKS PERFECTLY. YEAY.
CREATE OR REPLACE FUNCTION delete_question()
RETURNS trigger AS $delete_question$
DECLARE
BEGIN
IF ( OLD.que_type=1 ) THEN
IF EXISTS (SELECT 1 FROM mcq WHERE que_id=OLD.id) THEN
DELETE FROM mcq WHERE que_id=OLD.id;
END IF;
ELSIF ( OLD.que_type=2 OR OLD.que_type=3) THEN
IF EXISTS (SELECT 1 FROM tffb WHERE que_id=OLD.id) THEN
DELETE FROM tffb WHERE que_id=OLD.id;
END IF;
ELSIF ( OLD.que_type=4 ) THEN
IF EXISTS (SELECT 1 FROM essay WHERE que_id=OLD.id) THEN
DELETE FROM essay WHERE que_id=OLD.id;
END IF;
END IF;
RETURN OLD;
END;
$delete_question$ LANGUAGE plpgsql;
CREATE TRIGGER delete_question
BEFORE DELETE ON question
FOR EACH ROW EXECUTE PROCEDURE delete_question();
there is no ' ' on OLD.id or even (OLD).id and use RETURN OLD;