PostgreSQL-Update last_update_date to current if modif at table - sql

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.

Related

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();

How to call a trigger when constraint violation occurs?

I have a situation where users are not allowed to enter a duplicate value. If user tries to add duplicate value, the system saves a details of user in a audit table. Trigger is used for that. My code is below
create or replace trigger tr_add_on_audit_table
before insert on lds_consultant
for each row
declare
uname varchar2(30);
begin
select username into uname from lds_consultant where username = :NEW.USERNAME;
if uname <> '' or uname <> null then
insert into audit_table values(null, null, 'nishan', 'insert', null, null, 'cmd', null, 'LDS_CONSULTANT', 'CONSULTANT_ID',null, null, null);
end if;
end;
but this code doesn't insert data into audit table.
How can I achieve that?
NULL isn't equal to nor different from anything. You should use IS NULL or IS NOT NULL, not <> nor =.
Something like this:
create or replace trigger tr_add_on_audit_table
before insert on lds_consultant
for each row
declare
uname varchar2(30);
begin
select username
into uname
from lds_consultant
where username = :NEW.USERNAME;
if uname is not null then --> this!
insert into audit_table
values(null, null, 'nishan', 'insert', null, null, 'cmd', null, 'LDS_CONSULTANT', 'CONSULTANT_ID',null, null, null);
end if;
exception
when no_data_found then
null;
end;
I included exception handler section in case that SELECT doesn't return anything; if it isn't probable, remove it (or handle it properly; I'm doing nothing (NULL;). Also, handle other exceptions, if necessary.
Also, I'd suggest you to name all columns you're inserting into. Today, you know what value goes where, but in a matter of a month or two you'll forget what is the third NULL value supposed to mean.
Furthermore, you said that user isn't allowed to enter a duplicate value - well, this code won't make it happen.
The simplest option is to create a unique key constraint on the USERNAME column and let Oracle handle duplicates.
If you want to do that yourself, you should e.g.
raise_application_error(-20000, 'Duplicate username is not allowed);
However, that won't save your INSERT into the table as everything will be rolled back. In order to fix that, create a procedure that uses pragma autonomous_transaction and commits insert into the audit table.
Everything would look like this:
create or replace procedure p_audit as
pragma autonomous_transaction;
begin
insert into audit_table
values(null, null, 'nishan', 'insert', null, null, 'cmd', null, 'LDS_CONSULTANT', 'CONSULTANT_ID',null, null, null);
commit;
end;
/
create or replace trigger tr_add_on_audit_table
before insert on lds_consultant
for each row
declare
uname varchar2(30);
begin
select username
into uname
from lds_consultant
where username = :NEW.USERNAME;
if uname is not null then
p_audit;
raise_application_error(-20000, 'Duplicates are not allowed')
end if;
exception
when no_data_found then
null;
end;
/
But, once again, why bother? Uniqueness is the keyword here.

PostgreSQL Trigger Function get stack and does not update the table

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.

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();