Skip to Main Content
  • Questions
  • Raise error while open distributed transaction

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Andres.

Asked: January 07, 2014 - 9:00 am UTC

Last updated: January 07, 2014 - 5:36 pm UTC

Version: 10.2.0.5

Viewed 10K+ times! This question is

You Asked

create table foo (c1 varchar2(10));
Now, if i try to use the table over db link like
begin
   insert into foo@dblink(c1) values (null);
end;
/

and then
begin
   raise_application_error(-20422, 'Test!');
end;
/

I get the
ORA-20422: Test!
ORA-06512: at line 3

as expected.

But when i run
begin
   insert into foo@dblink(c1) values (null);
   raise_application_error(-20422, 'Test!');
end;
/

i get
ORA-02055: distributed update operation failed; rollback required
ORA-20422: Test!
ORA-06512: at line 3


why the ORA-02055?
can't i raise an error while i have an open distributed transaction?

and Tom said...

there is a huge difference between:

statement 1
statement 2 (which causes an error)

and

begin
statement 1
statement 2 (which causes an error)
end


even in a non-distributed transaction. In the first case, statement 1's work will persist, will still be present - even after statement 2 fails. So, for example:


ops$tkyte%ORA11GR2> begin
  2     insert into foo@ora11gr2@loopback(c1) values ('abc');
  3  end;
  4  /

PL/SQL procedure successfully completed.

ops$tkyte%ORA11GR2> select * from foo@ora11gr2@loopback;

C1
----------
abc

ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> begin
  2     raise_application_error(-20422, 'Test!');
  3  end;
  4  /
begin
*
ERROR at line 1:
ORA-20422: Test!
ORA-06512: at line 2


ops$tkyte%ORA11GR2> select * from foo@ora11gr2@loopback;

C1
----------
abc

ops$tkyte%ORA11GR2> rollback;

Rollback complete.

ops$tkyte%ORA11GR2> select * from foo@ora11gr2@loopback;

no rows selected



Note how the work performed over the dblink is not affected *at all*. The first statement entirely entirely succeeds or entirely fails. in this case, statement 1 entirely succeeded. It will persist until you either commit or rollback. Regardless of the success or failure of statement 2 - the two are entirely unrelated (except for the fact they are in the same transaction).


However, when you put statement 1 and statement 2 together into a single statement - they must either both entirely succeed or entirely fail.

in this case, they must entirely fail - and since this is a distributed transaction - the transaction itself will have to fail.


If I knew more about your actual requirement, I could tell you how to code your block of code..

It could be that a simple savepoint/rollback to savepoint can be useful for you. If you wrap your existing logic in a block that simply does:

begin
   savepoint X; 
   <your existing code>
exception
   when others
   then
       rollback to X;
       RAISE;
end;

you'd get what you desire, for example:


ops$tkyte%ORA11GR2> begin
  2      savepoint foo;
  3      begin
  4         insert into foo@ora11gr2@loopback(c1) values ('abc');
  5         raise_application_error(-20422, 'Test!');
  6      exception
  7          when others then
  8              rollback to foo;
  9              RAISE;
 10      end;
 11  end;
 12  /
begin
*
ERROR at line 1:
ORA-20422: Test!
ORA-06512: at line 9


ops$tkyte%ORA11GR2> select * from foo@ora11gr2@loopback;

no rows selected



begin
statement1
end;
begin
statement2
end;


is very very very VERY different transactionally from

begin
statement1
statement2
end;


Here is some more background on this from Expert Oracle Database Architecture:

<quote>

Atomicity
Now we’re ready to see what’s meant by statement, procedure, and transaction atomicity.
Statement-Level Atomicity
Consider the following statement:

Insert into t values ( 1 );

It seems fairly clear that if the statement were to fail due to a constraint violation, the row would not be inserted. However, consider the following example, where an INSERT or DELETE on table T fires a trigger that adjusts the CNT column in table T2 appropriately: 

ops$tkyte%ORA11GR2> create table t2 ( cnt int );
Table created.

ops$tkyte%ORA11GR2> insert into t2 values ( 0 );
1 row created.

ops$tkyte%ORA11GR2> commit;
Commit complete.

ops$tkyte%ORA11GR2> create table t ( x int check ( x>0 ) );
Table created.

ops$tkyte%ORA11GR2> create trigger t_trigger
  2  before insert or delete on t for each row
  3  begin
  4     if ( inserting ) then
  5          update t2 set cnt = cnt +1;
  6     else
  7          update t2 set cnt = cnt -1;
  8     end if;
  9     dbms_output.put_line( 'I fired and updated '  ||
 10                                     sql%rowcount || ' rows' );
 11  end;
 12  /
Trigger created.

In this situation, it is less clear what should happen. If the error occurs after the trigger has fired, should the effects of the trigger persist, or not? That is, if the trigger fired and updated T2, but the row was not inserted into T, what should the outcome be? Clearly the answer is that we don’t want the CNT column in T2 to be incremented if a row is not actually inserted into T. Fortunately in Oracle, the original statement from the client—INSERT INTO T, in this case—either entirely succeeds or entirely fails. This statement is atomic. We can confirm this, as follows:

ops$tkyte%ORA11GR2> set serveroutput on
ops$tkyte%ORA11GR2> insert into t values (1);
I fired and updated 1 rows

1 row created.

ops$tkyte%ORA11GR2> insert into t values(-1);
I fired and updated 1 rows
insert into t values(-1)
*
ERROR at line 1:
ORA-02290: check constraint (OPS$TKYTE.SYS_C0018095) violated


ops$tkyte%ORA11GR2> select * from t2;

       CNT
----------
n Note When using SQL*Plus from Oracle9i Release 2 and before, in order to see that the trigger fired, you need to add a line of code, EXEC NULL, after the second INSERT. This is because SQL*Plus does not retrieve and display the DBMS_OUTPUT information after a failed DML statement in those releases. In Oracle 10g and above it does.
So, one row was successfully inserted into T and we duly received the message I fired and updated 1 rows. The next INSERT statement violates the integrity constraint we have on T. The DBMS_OUTPUT message appeared—the trigger on T in fact did fire and we have evidence of that. The trigger performed its updates of T2 successfully. We might expect T2 to have a value of 2 now, but we see it has a value of 1. Oracle made the original INSERT atomic—the original INSERT INTO T is the statement, and any side effects of that original INSERT INTO T are considered part of that statement.
Oracle achieves this statement-level atomicity by silently wrapping a SAVEPOINT around each of our calls to the database. The preceding two INSERTs were really treated like this:

Savepoint statement1;
   Insert into t values ( 1 );
If error then rollback to statement1;
Savepoint statement2;
   Insert into t values ( -1 );
If error then rollback to statement2;

For programmers used to Sybase or SQL Server, this may be confusing at first. In those databases exactly the opposite is true. The triggers in those systems execute independently of the firing statement. If they encounter an error, the triggers must explicitly roll back their own work and then raise another error to roll back the triggering statement. Otherwise, the work done by a trigger could persist even if the triggering statement, or some other part of the statement, ultimately fails.
In Oracle, this statement-level atomicity extends as deep as it needs to. In the preceding example, if the INSERT INTO T fires a trigger that updates another table, and that table has a trigger that deletes from another table (and so on, and so on), either all of the work succeeds or none of it does. You don’t need to code anything special to ensure this; it’s just the way it works.
Procedure-Level Atomicity
It is interesting to note that Oracle considers PL/SQL anonymous blocks to be statements as well. Consider the stored procedure and reset example tables from above:

ops$tkyte%ORA11GR2> create or replace procedure p
  2  as
  3  begin
  4          insert into t values ( 1 );
  5          insert into t values (-1 );
  6  end;
  7  /
Procedure created.

ops$tkyte%ORA11GR2> delete from t;
0 rows deleted.

ops$tkyte%ORA11GR2> update t2 set cnt = 0;
1 row updated.

ops$tkyte%ORA11GR2> commit;
Commit complete.

ops$tkyte%ORA11GR2> select * from t;
no rows selected

ops$tkyte%ORA11GR2> select * from t2;

       CNT
----------
         0

So, we have a procedure we know will fail, and the second INSERT will always fail in this case. Let’s see what happens if we run that stored procedure:

ops$tkyte%ORA11GR2> begin
  2      p;
  3  end;
  4  /
I fired and updated 1 rows
I fired and updated 1 rows
begin
*
ERROR at line 1:
ORA-02290: check constraint (OPS$TKYTE.SYS_C0018095) violated
ORA-06512: at "OPS$TKYTE.P", line 5
ORA-06512: at line 2


ops$tkyte%ORA11GR2> select * from t;
no rows selected

ops$tkyte%ORA11GR2> select * from t2;

       CNT
----------
         0

As you can see, Oracle treated the stored procedure call as an atomic statement. The client submitted a block of code—BEGIN P; END;—and Oracle wrapped a SAVEPOINT around it. Since P failed, Oracle restored the database back to the point right before it was called. 
Note The preceding behavior—statement-level atomicity—relies on the PL/SQL routine not performing any commits or rollbacks itself.  It is my opinion that COMMIT and ROLLBACK should not be used in general in PL/SQL; the invoker of the PL/SQL stored procedure is the only one that knows when a transaction is complete.  It is a bad programming practice to issue a COMMIT or ROLLBACK in your developed PL/SQL routines.
Now, if we submit a slightly different block, we will get entirely different results:

ops$tkyte%ORA11GR2> begin
  2      p;
  3  exception
  4      when others then
  5          dbms_output.put_line( 'Error!!!! ' || sqlerrm );
  6  end;
  7  /
I fired and updated 1 rows
I fired and updated 1 rows
Error!!!! ORA-02290: check constraint (OPS$TKYTE.SYS_C0018095) violated
PL/SQL procedure successfully completed.

ops$tkyte%ORA11GR2> select * from t;

         X
----------
         1

ops$tkyte%ORA11GR2> select * from t2;

       CNT
----------
         1

ops$tkyte%ORA11GR2> rollback;
Rollback complete.
Here, we ran a block of code that ignored any and all errors, and the difference in outcome is huge. Whereas the first call to P effected no changes, this time the first INSERT succeeds and the CNT column in T2 is incremented accordingly. 
Oracle considered the “statement” to be the block that the client submitted. This statement succeeded by catching and ignoring the error itself, so the If error then rollback... didn’t come into effect and Oracle didn’t roll back to the SAVEPOINT after execution. Hence, the partial work performed by P was preserved. The reason this partial work is preserved in the first place is that we have statement-level atomicity within P: each statement in P is atomic. P becomes the client of Oracle when it submits its two INSERT statements. Each INSERT either succeeds or fails entirely. This is evidenced by the fact that we can see that the trigger on T fired twice and updated T2 twice, yet the count in T2 reflects only one UPDATE. The second INSERT executed in P had an implicit SAVEPOINT wrapped around it.

</quote>


Rating

  (1 rating)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Andres, January 08, 2014 - 8:27 am UTC

It was very refreshing to reread about atomicity again, thanks.
And the savepoint helped, yes, I changed
<existing code with possible distributed transaction>
<more existing code>
if <error condition> then
    raise_application_error;
end if;
into
savepoint x;
<existing code with possible distributed transaction>
<more existing code>
if <error condition> then
    rollback to savepoint x;
    raise_application_error;
end if;

It just seemed weird that when I raise an exception, Oracle wraps another exception around that. But i see now it only happens when i do not handle the rollback myself.

More to Explore

Backup/Recovery

Check out the complete guide to all of the Backup & Recovery techniques in the Oracle Database.