Hello,
Weeks ago we faced an issue related to triggers firing order and some weird data loss that I am going to explain here.
So here is the situation:
- Two databases created as copies of Production, with same objects (tables, PL/SQL, etc), just some difference in data after the copies.
- The application has a similar structure of triggers on several tables:
- One BEFORE INSERT trigger that sets the primary key to the next sequence value if it was not specified.
- Another BEFORE INSERT trigger that sets other values if they are not specified, like CREATED_ON and CREATED_BY.
- One AFTER INSERT/UPDATE/DELETE trigger that stores the previous values in a log table.
- The triggers have no FOLLOWS or PRECEDES clauses to determine the order of firing.
So, the problem that we were called to analyze was that all of the sudden, with no apparent reason, the app started to receive an Oracle error that it could not insert a NULL value in a NOT NULL column:
ORA-01400: cannot insert NULL into ("OWNER"."TABLE"."CREATED_ON")
The interesting fact: in one of the DB copies the error was showing up, but not in the other.
First, I checked to see if the table structure was changed, or constraints, or whatever else I could think of that could have introduced the error. None!
I also compared all these objects between the two databases: table structures and timestamps, constraints, triggers code, etc. No difference!
So, as far as I could check, the databases were the same but the error was showing up only in one of them.
Analyzing the triggers code, it was clear that the value for the column CREATED_ON was being set to the current timestamp in one of the BEFORE INSERT triggers, but for some reason this value was not being carried on until the end of the process.
Then at some point I thought this could be an issue with the order of the triggers firing. Since no particular order was specified (which could be achieved by using the FOLLOWS or PRECEDES clauses), the Oracle documentation that it’s impossible to determine the exact order of firing.
But, since the two triggers we had with concurrent firing did not overlap each other in any way (I mean, one did not undo anything the other was doing), the order should not change the behavior, right? Wrong!!!
So, after lots of tests, I was able to determine that the error was showing up when a specific order of firing was occurring, but not with the opposite order. I was able to proof that by including a FOLLOWS clause in one of the triggers to force a particular order.
Why the order of firing differed from one DB to the other, all of the sudden and without any noticeable change? Well, I still have no idea…
So, this is the first “issue”: the triggers with the same trigger conditions could be triggered (funny uh?) in any order. It is not a de facto issue, since the documentation clearly states it is expected behavior.
Why the order of firing made a difference if one doesn’t undo anything the other does? Wait and you will figure out.
I had a hard time until I was able to create a stand-alone reproducible test case that I could provide to Oracle Support to have them see the issue. I had to had working sessions and record what was happening in our environment, showing all the details, for them to be able to understand exactly what was the issue.
While they continued (and still continue) researching the issue on their side, I also continued to work on that and finally was able to create the reproducible test case that I will present here, so you can also see the problem by yourself.
Note: the issue is reproducible both in Oracle 19.19 as well as Oracle 23 FREE.
First, I will create the tables:
create table maintab (
id number,
ts6 timestamp (6),
ts3 timestamp (3),
dt date,
nu number,
ch char(1),
vc varchar2(10));
Table created.
create table histtab (
id number references maintab,
ts6 timestamp (6) not null,
ts3 timestamp (3) not null,
dt date not null,
nu number not null,
ch char(1) not null,
vc varchar2(10) not null);
Table created.
Please note that I am creating a main table and a history table, to reproduce exactly the scenario I had in our real environment. Also note I am creating several columns, one for each data type. You will understand why ahead.
Now I will create the triggers:
create or replace trigger triggerAfter after insert on maintab for each ROW
BEGIN
dbms_output.put_line('TRIGGER triggerAfter');
dbms_output.put_line(':new.ts6: ' || :new.ts6);
dbms_output.put_line(':new.ts3: ' || :new.ts3);
dbms_output.put_line(':new.dt: ' || :new.dt);
dbms_output.put_line(':new.nu: ' || :new.nu);
dbms_output.put_line(':new.ch: ' || :new.ch);
dbms_output.put_line(':new.vc: ' || :new.vc);
dbms_output.put_line('----');
INSERT
INTO histtab
(
id, ts6, ts3, dt, nu, ch, vc
)
VALUES
(
:new.id, :new.ts6, :new.ts3, :new.dt, :new.nu, :new.ch, :new.vc
);
END;
/
Trigger created.
create or replace trigger before_dont_set_values
before insert on maintab for each row
begin
dbms_output.put_line('TRIGGER before_dont_set_values');
dbms_output.put_line(':new.ts6: ' || :new.ts6);
dbms_output.put_line(':new.ts3: ' || :new.ts3);
dbms_output.put_line(':new.dt: ' || :new.dt);
dbms_output.put_line(':new.nu: ' || :new.nu);
dbms_output.put_line(':new.ch: ' || :new.ch);
dbms_output.put_line(':new.vc: ' || :new.vc);
dbms_output.put_line('----');
end;
/
Trigger created.
create or replace trigger before_set_values
before insert on maintab for each row follows before_dont_set_values
begin
:new.ts6 := systimestamp;
:new.ts3 := systimestamp;
:new.dt := sysdate;
:new.nu := 1;
:new.ch := '1';
:new.vc := '1';
dbms_output.put_line('TRIGGER before_set_values');
dbms_output.put_line(':new.ts6: ' || :new.ts6);
dbms_output.put_line(':new.ts3: ' || :new.ts3);
dbms_output.put_line(':new.dt: ' || :new.dt);
dbms_output.put_line(':new.nu: ' || :new.nu);
dbms_output.put_line(':new.ch: ' || :new.ch);
dbms_output.put_line(':new.vc: ' || :new.vc);
dbms_output.put_line('----');
end;
/
Trigger created.
Please note that I am forcing a particular order of the BEFORE INSERT triggers firing, first the BEFORE_DONT_SET_VALUES, then the BEFORE_SET_VALUES, by including a FOLLOWS in the second one.
Now, I will issue a simple INSERT that should work, but it doesn’t:
set serverout on
INSERT INTO maintab (id) values (1);
TRIGGER before_dont_set_values
:new.ts6:
:new.ts3:
:new.dt:
:new.nu:
:new.ch:
:new.vc:
----
TRIGGER before_set_values
:new.ts6: 2023-06-27 09:43:17.374968
:new.ts3: 2023-06-27 09:43:17.375
:new.dt: 2023-06-27 09:43:17
:new.nu: 1
:new.ch: 1
:new.vc: 1
----
TRIGGER triggerAfter
:new.ts6:
:new.ts3:
:new.dt: 2023-06-27 09:43:17
:new.nu: 1
:new.ch: 1
:new.vc: 1
----
INSERT INTO maintab (id) values (1)
*
ERROR at line 1:
ORA-01400: cannot insert NULL into ("CLAROE"."HISTTAB"."TS6")
ORA-06512: at "CLAROE.TRIGGERAFTER", line 10
ORA-04088: error during execution of trigger 'CLAROE.TRIGGERAFTER'
Note that my outputs show that when the first trigger (BEFORE_DONT_SET_VALUES), all the columns have values. I did not provide those values directly in my INSERT statement, they were filled by the trigger code.
But, when the second trigger (BEFORE_SET_VALUES) starts, we can see that the values for the TIMESTAMP data types (columns TS6 and TS3) were simply lost! The other columns kept their values as expected. Since all the columns have a NOT NULL constraint, the error shows up.
If I simply force the opposite order of firing, by putting the FOLLOWS clause in BEFORE_DONT_SET_VALUES instead, then the error does not show up:
create or replace trigger before_set_values
before insert on maintab for each row
begin
:new.ts6 := systimestamp;
:new.ts3 := systimestamp;
:new.dt := sysdate;
:new.nu := 1;
:new.ch := '1';
:new.vc := '1';
dbms_output.put_line('TRIGGER before_set_values');
dbms_output.put_line(':new.ts6: ' || :new.ts6);
dbms_output.put_line(':new.ts3: ' || :new.ts3);
dbms_output.put_line(':new.dt: ' || :new.dt);
dbms_output.put_line(':new.nu: ' || :new.nu);
dbms_output.put_line(':new.ch: ' || :new.ch);
dbms_output.put_line(':new.vc: ' || :new.vc);
dbms_output.put_line('----');
end;
/
Trigger created.
create or replace trigger before_dont_set_values
before insert on maintab for each row follows before_set_values
begin
dbms_output.put_line('TRIGGER before_dont_set_values');
dbms_output.put_line(':new.ts6: ' || :new.ts6);
dbms_output.put_line(':new.ts3: ' || :new.ts3);
dbms_output.put_line(':new.dt: ' || :new.dt);
dbms_output.put_line(':new.nu: ' || :new.nu);
dbms_output.put_line(':new.ch: ' || :new.ch);
dbms_output.put_line(':new.vc: ' || :new.vc);
dbms_output.put_line('----');
end;
/
Trigger created.
INSERT INTO maintab (id) values (1);
TRIGGER before_set_values
:new.ts6: 2023-06-27 09:54:24.268542
:new.ts3: 2023-06-27 09:54:24.269
:new.dt: 2023-06-27 09:54:24
:new.nu: 1
:new.ch: 1
:new.vc: 1
----
TRIGGER before_dont_set_values
:new.ts6: 2023-06-27 09:54:24.268542
:new.ts3: 2023-06-27 09:54:24.269
:new.dt: 2023-06-27 09:54:24
:new.nu: 1
:new.ch: 1
:new.vc: 1
----
TRIGGER triggerAfter
:new.ts6: 2023-06-27 09:54:24.268542
:new.ts3: 2023-06-27 09:54:24.269
:new.dt: 2023-06-27 09:54:24
:new.nu: 1
:new.ch: 1
:new.vc: 1
----
1 row created.
You see? So, this is inconsistent as I said in the beginning. The triggers firing order should not cause any difference in this particular case.
Please be aware that this issue is being investigated as a bug, so Oracle will likely come up with a patch for it soon (I hope 🙂 ).
The most frightening thing for me is that in this case the error is a good thing, since we were able to see the issue and troubleshoot it right away. If we did not have the NOT NULL constraint, we would realize at some point in the future that a lot of rows did not have the data we would expect, and along with the data loss, this would be much more difficult to troubleshoot.
That’s it. I will post an update when the patch is available or I have any update on this case.
EDIT 2023/NOV/06: Oracle have just assigned the Bug 35680150Â – TRIGGER LOSING :NEW VALUE FOR TIMESTAMP WHEN ‘FOLLOWS’ IS USED for this issue. The link is not public yet, but they said you can request a one-off patch if you need the issue fixed now.
Until there, if you have weird behaviors of column values being lost along the chain of triggers, you now know what may be causing the issue 😉 .
See you next time.