UPSERT inserts duplicate null entry into table (ORACLE) - sql

I am trying to make an upsert trigger on ORACLE via PL/SQL by checking some examples, i am doing fine, i think it is the last step i should only configure. My requirement is that :
A system that will insert to that field will remain one column always null, so i will read column value from another table, then upsert it with inclusion of that value.
d2c_region_locale_config holds d2c_is_active value, so i firstly read that value regarding to locale condition then trigger inserts or updates to table with addition of this value on active_for_d2c column.(for update i am using locale and country columns as it is shown on where clause, they are not PK but has not null condition)
So i've created this trigger:
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
l_active_for_d2c INTEGER;
BEGIN
if :NEW.active_for_d2c is null then
DELETE from pim_locale_country where active_for_d2c is null;
select distinct(d2c_isactive) into l_active_for_d2c from d2c_region_locale_config where d2c_locale= :NEW.locale;
UPDATE pim_locale_country
SET locale = :NEW.locale, locale_name = :NEW.locale_name,
country = :NEW.country, country_name = :NEW.country_name, isdummy = :NEW.isdummy,
active_for_d2c = l_active_for_d2c, itextpos = :NEW.itextpos, locale_charset = :NEW.locale_charset,
fallback_locale = :NEW.fallback_locale, default_for_lang = :NEW.default_for_lang, opeclang = :NEW.opeclang
where locale = :NEW.locale and country = :NEW.country;
IF ( sql%notfound ) THEN
INSERT INTO PIM_LOCALE_COUNTRY (locale,locale_name,country,country_name,isdummy,active_for_d2c,itextpos,locale_charset,fallback_locale,default_for_lang,opeclang)
VALUES (:NEW.locale, :NEW.locale_name,:NEW.country,:NEW.country_name,:NEW.isdummy,l_active_for_d2c,:NEW.itextpos,:NEW.locale_charset,:NEW.fallback_locale,:NEW.default_for_lang,:NEW.opeclang);
END IF;
end if;
END;
It currently does the job, reads value and inserts or updates the existing locale-country couple for other values. But critical thing is that, table always has one "null" value(Please check screenshot), even that i run delete statement at the beginning on my trigger. So my question would be how to delete, or how to make this approach on trigger side ?
Many thanks for answers!

Trigger before insert doesn't block insert itself, so you insert that record twice. That is, once your trigger done its work (inserted or updated record), oracle will proceed with insert (or update) using values that stand in NEW record of your trigger. If trigger modifies NEW., it will be stored as you changed it, but if trigger inserts something itself, you can get more records.
You can use instead of insert or update triggers, and then oracle will not run its own inserts/updates after trigger finishes.
But more common way for 1-record triggers is to modify fields in NEW, for this case field NEW.d2c_is_active.
It looks like this (possible typos, please check)
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
if :NEW.active_for_d2c is null then
select d2c_isactive
into :NEW.active_for_d2c
from d2c_region_locale_config
where d2c_locale= :NEW.locale and rownum<=1;
end if;
END;

Related

how to make sure that trigger generated value is being returned?

I have this INSERT query, which purpose is to insert the one row in my database.
Similarly I also have a INSERT query which insert multiple rows.
One of the columns in the table is generated after the values has been generated, since it combines a set of column values to construct a name. The name itself it generated from a Trigger, and its triggered After insert, since the column values has to exist for me to generate the name.
my problem now is when I insert one row or multiple rows, I want to know the the generated column value, but when I return it, it states its null?
#$"INSERT INTO registration_table (id, ...,)
VALUES (1,...,)
RETURNING row_id, name;";
which in return gives me an id the one I inserted, but the not actual name but instead I get null..
The trigger is pretty straight forward
CREATE TRIGGER name_insert_trigger
AFTER INSERT
ON registration_table
REFERENCING NEW TABLE AS new_inserts
FOR EACH STATEMENT
WHEN (pg_trigger_depth() = 0)
EXECUTE PROCEDURE registration_entry_name();
CREATE OR REPLACE FUNCTION registration_entry_name()
RETURNS trigger AS
$$
DECLARE
BEGIN
UPDATE registration_table
SET name = |Pattern| -- This one being the actual name generated..
FROM new_inserts
WHERE new_inserts.row_id = registration_table.row_id;
RETURN null;
END;
$$
LANGUAGE plpgsql;
but the insert query above does not return the name?
why not?
You actually need a BEFORE trigger, your data values will be there. The designation of Before and After very often causes misconceptions especially of row level triggers. The terms do not indicate their timing in relation to the DML. I have found it useful to think of them as "before final data values are set" and "after final data values are set" but both run before the invoking DML completes (for now we will bypass deferred triggers). Lets look at inserts. When the before row trigger fires the NEW row contains the values at that point for every column in the row, any value not specified in the statement will be null or contain the specified default if any. Before row triggers can can change any column. After row triggers cannot change columns, if present any change is ignored.
Your description and code imply you need to combine a couple columns to generate the content of another. Since you did not specify exactly that I will build an example and demo.
create table users ( usr_id integer generated always as identity
, lname text not null
, fname text not null
, full_name text not null
) ;
create or replace
function users_bir()
returns trigger
language plpgsql
as $$
begin
if new.full_name is null
then
new.full_name = trim(new.fname) || ' ' || trim(new.lname);
end if;
return new;
end;
$$;
create trigger users_bir_trg
before insert on users
for each row
execute procedure users_bir();
insert into users(fname, lname)
values ( 'George', 'Henery')
, ( 'Samatha', 'van Horm');
insert into users(fname, lname, full_name)
values ( 'Wacky', 'Warriors','Not so tough guys');
This setup allows the full_name to be specified or generated. If only generation is desired remove the IF leaving only the assignment statement. Even better if you have Postgres 12 or higher just define the the column as a generated column. This is also in the demo.

trigger to update specific column when insert/update happened in same table

I trying to write a trigger that will update a column when user insert or updates a row, within the same table.
Example:
insert into user(ID, F_NM, L_NM, EMAIL) values ('1', 'John','Doe','john.doe#market.org.com');
after the insert, i want to call: update user set ORG = 'market' where ID = '1'.
create or replace trigger user_change
after insert or update of EMAIL on USER
for each row
declare
NEW_ORG VARCHAR(10);
BEGIN
CASE
when :NEW.EMAIL like '$#market.org.com' then
NEW_ORG := 'market';
........
END CASE;
UPDATE USER set ORG = NEW_ORG where ID = :NEW.ID
END;
Calculating the new ORG work, but I can't get the update statement to work.
I get 'ORA-04091 table USER is mutating, trigger/funtion may not see it', figure its due to me inserting/updating the same record at same time. Tried adding 'pragma autonomous_transaction' and 'commit' to the trigger, the insert/update of fields works but the ORG does not get updated.
Also tried changing to INSTEAD OF INSERT OR UPDATE OF EMAIL but I keep getting 'ORA-04073 column list not valid for this trigger type'
create or replace trigger user_change
instead of insert or update of EMAIL on USER
while i get 'ORA-25002 cannot create instead of triggers on tables'
create or replace trigger user_change
instead of insert on USER
Why not simply turn the trigger to a before trigger, when you can set the value before it is written? This way, you don't need to run a new DML statement on the table, which avoid the "mutating" error.
create or replace trigger user_change
after insert or update of email on user
for each row
begin
if :new.email like '%#market.org.com' then
:new.org := 'market';
end if;
end;
Looks like your org column can be calculated virtual column. In this case it would be better to create user-defined deterministic pl/sql function that returns correct calculated value and add it to your table, for example:
Alter table t add org varchar2(30) generated always as (f_get_org(email))

PLS-00049: bad bind variable 'NEW.REQUEST_DATETIME' Issue

I am trying to write a Trigger which basically updates one table when an entry is made on another table.
CREATE OR REPLACE TRIGGER "DTISCDB_OWNER"."REQUEST_CONTEXT_TR"
AFTER INSERT OR UPDATE ON REQUEST_CONTEXT
FOR EACH ROW
BEGIN
:NEW.REQUEST_DATETIME := SYSDATE;
:NEW.ID := TRUNC(DBMS_RANDOM.VALUE(100000000000000000000000000000000000,999999999999999999999999999999999999));
SELECT bre_conditions_seq.NEXTVAL INTO :OLD.seq_number FROM dual;
SELECT REQUEST_CONTEXT.CURRENT_STATE INTO :NEW.STATE FROM REQUEST_CONTEXT;
SELECT REQUEST_CONTEXT.REQUEST_ID INTO :NEW.REQUEST_ID FROM REQUEST_CONTEXT;
INSERT INTO REQUEST_LIFECYCLES(ID,SEQ_NUMBER,STATE,REQUEST_ID,REQUEST_DATETIME)
VALUES(:NEW.ID,:NEW.seq_number,:NEW.STATE,:NEW.REQUEST_ID,:NEW.REQUEST_DATETIME);
END;
You seem to be confused about what the correlation pseudorows are for and what they hold and can do. It looks like you're treating :NEW as if it is related to the REQUEST_LIFECYCLES table you are inserting into inside the trigger, and :OLD as if it is related to the REQUEST_CONTEXT row that has been inserted or updated and caused the trigger to fire.
Both OLD and NEW refer to the table the trigger is against, REQUEST_CONTEXT. If the trigger was fired by an update then OLD has the pre-update values for the affected row; if it was fired by an insert then it is empty as there is no old state. Either way NEW has the current state, with the newly-inserted or post-update values. You can't change the OLD values, and it doesn't make sense to change the NEW values in an 'after' trigger. You also don't need to query the table the trigger is fired against, as the NEW pseudorow already makes that information available.
So if you are trying to use the inserted/updated values in REQUEST_CONTEXT to create a row in REQUEST_LIFECYCLES, you would do something like:
CREATE OR REPLACE TRIGGER "DTISCDB_OWNER"."REQUEST_CONTEXT_TR"
AFTER INSERT OR UPDATE ON REQUEST_CONTEXT
FOR EACH ROW
BEGIN
INSERT INTO REQUEST_LIFECYCLES(ID, SEQ_NUMBER, STATE, REQUEST_ID,
REQUEST_DATETIME)
VALUES(TRUNC(DBMS_RANDOM.VALUE(100000000000000000000000000000000000, 999999999999999999999999999999999999)),
bre_conditions_seq.NEXTVAL, :NEW.CURRENT_STATE, :NEW.REQUEST_ID,SYSDATE);
END;
/
I'm assuming you wanted to set the 'lifecycle' SEQ_NUMBER value from the trigger, despite you trying to set the :OLD value - hopefully the old reference was a mistake. If you were trying to set that value in both REQUEST_CONTEXT and REQUEST_LIFECYCLES you would need a before insert/update trigger instead, and would set :NEW.SEQ_NUMBER rather than the :OLD value, before using it in the values clause.
As Justin said using a random value for the ID is rather strange, not least because it won't be unique, and a sequence is much more common. You may actually want the ID from the inserted/updated row, in which case you can just refer to :NEW.ID in the values clause instead of generating a new value. (It's also possible you are trying to set that ID in both REQUEST_CONTEXT and REQUEST_LIFECYCLES too, but that would be even stranger, and you'd need a before-insert/update trigger to do that anyway).

Disable Trigger for a particular DELETE Query

I have a ruby app. The app is doing the insert,update and delete on a particular table.
It does 2 kinds of INSERT, one insert should insert a record in the table and also into trigger_logs table. Another insert is just to insert the record into the table and do nothing. Another way to put it is, one kind of insert should log that the 'insert' happened into another table and another kind of insert should just be a normal insert. Similarly, there are 2 kinds of UPDATE and DELETE also.
I have achieved the 2 types of INSERT and UPDATE using a trigger_disable. Please refer to the trigger code below.
So, when I do a INSERT, I will set the trigger_disable boolean to true if I don't want to log the trigger. Similarly I am doing for an UPDATE too.
But I am not able to differentiate between the 2 kinds of DELETE as I do for an INSERT or UPDATE. The DELETE action is logged for both kinds of DELETE.
NOTE: I am logging all the changes that are made under a certain condition, which will be determined by the ruby app. If the condition is not satisfied, I just need to do a normal INSERT, UPDATE or DELETE accordingly.
CREATE OR REPLACE FUNCTION notify_#{#table_name}()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
changed_row_id varchar(100);
BEGIN
IF TG_OP = 'DELETE' THEN
-- When the trigger is due to a delete
IF (OLD.trigger_disable IS NULL)
OR (OLD.trigger_disable = false) THEN
-- Prevent the trigger if trigger_disable is 'true'
-- The Problem is here: This insertion into the
-- trigger_logs table happens
-- for all the delete statements.
-- But during certain deletes I should not
-- insert into trigger_logs
INSERT INTO trigger_logs (table_name, action, row_id, dirty)
VALUES (
'#{#table_name}',
CAST(TG_OP AS Text),
OLD.id,
true
) RETURNING id into changed_row_id;
END IF;
RETURN OLD;
ELSE
-- The trigger is due to a Insert or Update
IF (NEW.trigger_disable IS NULL)
OR (NEW.trigger_disable = false) THEN
-- Prevent the trigger if trigger_disable is 'true'
INSERT INTO trigger_logs (table_name, action, row_id, dirty)
VALUES (
'#{#table_name}',
CAST(TG_OP AS Text),
NEW.id,
true
) RETURNING id into changed_row_id;
ELSE
NEW.trigger_disable := false;
END IF;
RETURN NEW;
END IF;
END
I'm going to take a stab in the dark here and guess that you're trying to contextually control whether triggers get fired.
If so, perhaps you can use a session variable?
BEGIN;
SET LOCAL myapp.fire_trigger = 'false';
DELETE FROM ...;
COMMIT;
and in your trigger, test it:
IF current_setting('myapp.fire_trigger') = 'true' THEN
Note, however, that if the setting is missing from a session you won't get NULL, you'll get an error:
regress=> SELECT current_setting('myapp.xx');
ERROR: unrecognized configuration parameter "myapp.xx"
so you'll want to:
ALTER DATABASE mydb SET myapp.fire_trigger = 'true';
Also note that the parameter is text not boolean.
Finally, there's no security on session variables. So it's not useful for security audit, since anybody can come along and just SET myapp.fire_trigger = 'false'.
(If this doesn't meet your needs, you might want to re-think whether you should be doing this with triggers at all, rather than at the application level).

Create a trigger that updates a column on one table when a column in another table is updated

i have two tables
Order(id, date, note)
and
Delivery(Id, Note, Date)
I want to create a trigger that updates the date in Delivery when the date is updated in Order.
I was thinking to do something like
CREATE OR REPLACE TRIGGER your_trigger_name
BEFORE UPDATE
ON Order
DECLARE
BEGIN
UPDATE Delivery set date = ??? where id = ???
END;
How do I get the date and row id?
thanks
How do i get the date and row id?
Assuming these are columns on your ORDER table called DELIVERY_DATE and ID your trigger should look something like this:
CREATE OR REPLACE TRIGGER your_trigger_name
BEFORE UPDATE ON Order
FOR EACH ROW
BEGIN
if :new.delivery_date != :old.delivery_date
then
UPDATE Delivery d
set d.delivery_date = :new.delivery_date
where d.order_id = :new.id;
end if;
END;
Note the FOR EACH ROW clause: that is necessary to reference values from individual rows. I have used an IF construct to test whether to execute the UPDATE on Delivery. If you have no other logic in your trigger you could write it like this...
CREATE OR REPLACE TRIGGER your_trigger_name
BEFORE UPDATE OF delivery_date ON Order
FOR EACH ROW
BEGIN
UPDATE Delivery d
set d.delivery_date = :new.delivery_date
where d.order_id = :new.id;
END;
I have answered the question you asked but, as an aside, I will point out that your data model is sub-optimal. A properly normalized design would hold DELIVERY_DATE on only one table: DELIVERY seems teh logical place for it.
Use the OLD and NEW bind variables. OLD references the row or column being updated before the change is made; NEW references it after the change.
CREATE OR REPLACE TRIGGER trig1
BEFORE UPDATE
ON order REFERENCING NEW AS new
FOR EACH ROW
BEGIN
UPDATE delivery
SET ddate = :new.ddate
WHERE id = :new.id;
END;
You can modify the REFERENCING clause to give your bind variables different names. You can include OLD as <name> too. Example:
CREATE OR REPLACE TRIGGER trig1
BEFORE UPDATE
ON order REFERENCING OLD AS old_values NEW AS new_values
...
If you don't want to change the default names of "old" and "new", you can leave out the REFERENCING clause completely.
There is an implicit new and old reference in the trigger in the form of:
REFERENCING OLD AS OLD NEW AS NEW
You can write to the :NEW value but not to the :OLD value.
UPDATE Delivery set date = :new.delivery_date where id = :new.id;
CREATE OR REPLACE TRIGGER "BUR_TABLENAME" BEFORE
UPDATE ON "TABLE" FOR EACH ROW
BEGIN
If :new.active_date is not null Then
:new.active_date := TRUNC(:new.active_date);
End If;
END;
Template:
CREATE OR REPLACE TRIGGER TRIGGER_NAME
BEFORE
UPDATE
ON TABLE_NAME
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
V_VARIABLE NUMBER (1);
BEGIN
//Do Stuff;
null;
end;
Whenever there is a need for this kind of trigger, have a good look at your design. Is there really a need for a separate delivery record? Does an order really have more than 1 delivery ?
Triggers seem nice but they do tend to mess things up pretty quickly.