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.
Related
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.
I have 2 tables ("reports" and "RA_log"), one for the user to record points with agglomeration and another that records the logs. I created a function to audit every time any change is made to the "reports" table.
CREATE TABLE Reports
(
report_id SERIAL PRIMARY KEY,
report_user_name VARCHAR(20) NOT NULL,
report_location_name VARCHAR(50) NOT NULL,
report_number_people INT NOT NULL,
report_mask VARCHAR(20) NOT NULL,
report_distance BOOLEAN NOT NULL,
report_observations VARCHAR(255),
report_date_time timestamp NOT NULL,
report_latitude VARCHAR(100) NOT NULL,
report_longitude VARCHAR(100) NOT NULL
);
CREATE TABLE RA_log
(
log_change_type CHAR(1) NOT NULL,
log_user VARCHAR NOT NULL,
log_date_occurrence TIMESTAMP NOT NULL,
report_username VARCHAR NOT NULL,
report_quantity_people INTEGER
);
CREATE OR REPLACE FUNCTION audita_RA_log() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO RA_log VALUES ('D', USER, now(), OLD.*);
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO RA_log VALUES ('U', USER, now(), NEW.*);
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO RA_log VALUES ('I', USER, now(), NEW.*);
RETURN NEW;
END IF;
RETURN NULL; --the result is ignored because the trigger is AFTER
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trig_log_RA
AFTER INSERT OR UPDATE OR DELETE ON reports
FOR EACH ROW EXECUTE PROCEDURE audita_RA_log();
INSERT INTO reports (report_username, report_location_name, report_quantity_people, report_mask, report_distance, report_observacoes, report_date_time, report_latitude, report_longitude)
VALUES ('Scooby', 'Lucky Lottery', 20, 'No', '1', 'Lots of people', '2021-06-22 4:10:25-07','-6.47926', '-35.4348' );
but when I'm going to enter some data. is returning the following error:
ERROR: INSERT has more expressions than target columns
LINE 1: INSERT INTO RA_log VALUES ('D', USER, now(), OLD.)
^
QUERY: INSERT INTO RA_log VALUES ('D', USER, now(), OLD.)
CONTEXT: função PL/pgSQL audita_ra_log() linha 4 em comando SQL
SQL state: 42601
Your table ra_log has 5 columns, but your inserts statements are attempting to insert 13 columns: Type, User, date and the 10 columns from Reports. That is because old.* and new.* actually mean each column from the table the trigger is fired on. In this case all 10 columns from Reports. You have 2 options.
Refer directly to the columns defined in reports. So
old.report_user_name, old.report_number_people (or new. as
appropriate) that you want in ra_log.
create or replace function audita_ra_log()
returns trigger
language plpgsql
as $$
begin
if (tg_op = 'delete') then
insert into ra_log values ('d', user, now(), old.report_id, old.report_user_name);
else
insert into ra_log values (lower(substr(tg_op,1,1)), user, now(), new.report_id, new.report_user_name);
end if;
end;
$$;
If you want all columns from Reports then discontinue the columns
report_username, report_quantity_people and replace them by a single column of hstore or json. Change the trigger accordingly.
CREATE TABLE ra_log(
log_change_type CHAR(1) NOT NULL,
, log_user VARCHAR NOT NULL,
, log_date_occurrence TIMESTAMP NOT NULL,
, reports_value json
);
create or replace function audita_ra_log()
returns trigger
language plpgsql
as $$
begin
if (tg_op = 'delete') then
insert into ra_log values ('d', user, now(), row_to_json(old.*));
else
insert into ra_log values (lower(substr(tg_op,1,1)), user, now(), row_to_json(new.*));
end if;
end;
$$;
If keeping the complete row I prefer [hstore][1] as it keeps actual column names, but json is probably the more common.
it was like this, I modified some columns in the table RA_log.
CREATE TABLE RA_log
(
change VARCHAR NOT NULL,
changed_table VARCHAR NOT NULL,
date_time_change TIMESTAMP,
user_change VARCHAR NOT NULL
);
CREATE FUNCTION log_reports() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO sys_log(change, table_changed, date_time_change, user_change)
VALUES (TG_OP, TG_TABLE_NAME, now(), CURRENT_USER );
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER change_reports
AFTER INSERT OR DELETE OR UPDATE ON reports
FOR EACH ROW
EXECUTE PROCEDURE log_reports();
I got a table Location
CREATE TABLE Location (
idL INTEGER,
City VARCHAR2(15) NOT NULL,
Street VARCHAR2(35) NOT NULL,
Nation CHAR(6) NOT NULL,
CONSTRAINT PK_idL PRIMARY KEY(idL)
);
And a table Person
CREATE TABLE Person(
p_Name VARCHAR2(20) NOT NULL,
p_Surname VARCHAR2(20) NOT NULL,
idP INTEGER,
b_Date DATE NOT NULL,
id_PL INTEGER,
CONSTRAINT PK_idP PRIMARY KEY(idP),
CONSTRAINT FK_idPL FOREIGN KEY(id_PL) REFERENCES Location(idL)
);
I calculate the primary key "automatically" as it follows:
CREATE SEQUENCE seq_loc_pk
start with 1
increment by 1;
CREATE OR REPLACE TRIGGER auto_pk_loc
BEFORE INSERT ON Location
FOR EACH ROW
BEGIN
:new.idL := seq_loc_pk.nextval;
END;
/
Now I want to insert the residence for a new person (after I've created the right view of course) with an instead of trigger like this:
CREATE OR REPLACE TRIGGER newperson
INSTEAD OF INSERT ON Residence
FOR EACH ROW
DECLARE
nl Loc.idL%TYPE;
BEGIN
ALTER TRIGGER auto_pk_loc DISABLE; -- Error
nl := seq_loc_pk.nextval;
:NEW.idL := nl;
INSERT INTO Location VALUES(:NEW.City,:NEW.Street,:NEW.Nation);
INSERT INTO Patient VALUES(:NEW.P_Name,:NEW.P_Surname,:NEW.B_Date,,nl);
ALTER TRIGGER auto_pk_loc ENABLE;
END;
/
I thought about disabling and enabling the trigger auto_pk_loc so that it doesn't create extra values for no reason, but I think this is not the right way to do it? What is it though? Thanks for whoever answers.
You can do this by placing it in execute immedaite:
BEGIN
execute immedidate 'ALTER TRIGGER auto_pk_loc DISABLE';
nl := seq_loc_pk.nextval;
:NEW.idL := nl;
INSERT INTO Location VALUES(:NEW.City,:NEW.Street,:NEW.Nation);
INSERT INTO Patient VALUES(:NEW.P_Name,:NEW.P_Surname,:NEW.B_Date,,nl);
execute immedidate 'ALTER TRIGGER auto_pk_loc ENABLE';
END;
/
But this will cause you all sorts of issues; DDL commits so you'll have to make this an autonomous transaction and you'll hit concurrency problems. This is best avoided.
A better method is to use the returning clause to fetch the value you just inserted:
BEGIN
INSERT INTO Location VALUES(:NEW.City,:NEW.Street,:NEW.Nation)
returning idl into nl;
INSERT INTO Patient VALUES(:NEW.P_Name,:NEW.P_Surname,:NEW.B_Date,nl);
END;
/
Though as #astentx noted, you probably want to use merge to avoid having duplicate locations. This doesn't support returing, so you'll have to use some combination of insert+update instead.
Finally - assuming you're on 12c or higher - it's better to use an identity column or sequence default to auto-generate the location IDs over a trigger.
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.
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();