Oracle PL/SQL How to rollback the newly inserted row - sql

create or replace trigger trig_redeem_coffee
before insert
on buycoffee
for each row
declare
CID int;
customerPoint float;
pointNeeded float;
begin
select customer_id into CID
from purchase
where purchase_id = :new.purchase_id;
select total_points into customerPoint
from customer
where customer_id = CID;
pro_get_redeem_point (:new.coffee_ID, :new.redeem_quantity, pointNeeded);
if pointNeeded>customerPoint
then
rollback;
else
pointNeeded := -1*pointNeeded;
pro_update_point(CID, pointNeeded);
end if;
commit;
end;
/
The trigger can be successfully created, but when I insert into buycoffee table(it will meet the condition that pointNeeded>customerPoint), it returns an error that it cannot rollback in a trigger. Is this a proper way to rollback a newly inserted row? Or is there any better way to do it. (all procedures are built properly)

You cannot COMMIT or ROLLBACK inside of a TRIGGER, unless it's an autonomous transaction.
Inside your TRIGGER, you should do whatever logic you wish to apply, but if you reach an error condition, you should raise an application error, rather than ROLLBACK. That should cause the INSERT statement that fired the TRIGGER to error, doing a statement level rollback, and return your transaction to the state it was just before you executed the INSERT. At that point, you can evaluate the error, decide whether to rollback the entire transaction, or re-try the INSERT, or something else.
More on autonomous transactions:
https://docs.oracle.com/database/121/CNCPT/transact.htm#GUID-C0C61571-5175-400D-AEFC-FDBFE4F87188
More on statement-level rollback:
https://docs.oracle.com/cd/B19306_01/server.102/b14220/transact.htm#i8072

Related

When does an insert commit with a trigger on the table?

I have a very simple trigger I wrote for a table at work. The problem is when I use 'AFTER INSERT', the trigger locks the table and nothing else can write to it. When I use 'BEFORE INSERT', the trigger works just fine. Both versions of the trigger compile just fine, but only the 'AFTER INSERT' gives me the error when I use a simple insert statement into the 'EVENT_MESSAGE' table. Below is the trigger and error message:
create or replace TRIGGER TWCC_MHE_RERELEASE
AFTER INSERT
ON EVENT_MESSAGE
FOR EACH ROW
DECLARE
m_count number(3);
BEGIN
select count(*) into m_count from event_message where event_id = 7010 and ek_wave_nbr = :new.ek_wave_nbr;
IF :new.event_id = 7010
THEN
insert into TEST_COUNT
(count, wave_nbr, create_date_time)
values
(m_count,:new.ek_wave_nbr, sysdate);
END IF;
END;
A mutating table occurs when a statement causes a trigger to fire and that trigger references the table that caused the trigger. The best way to avoid such problems is to not use triggers :P
Changed the trigger to an after trigger.
Changed it from a row level trigger to a statement level trigger.
Convert to a Compound Trigger.
Modified the structure of the triggers to use a combination of row and statement level triggers.
Made the trigger autonomous with a commit in it.

Trigger with Autonomous Transaction

I want to know if declaring AUTONOMOUS_TRANSACTION means that the rule action starts after event transaction INSERT on tab has ended (AKA triggering transaction had a successful COMMIT).
I'm using Oracle DB 11.2
Here I have added the trigger:
CREATE OR REPLACE TRIGGER rule1
AFTER INSERT ON tab
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
EXECUTE IMMEDIATE ('
ALTER TABLE vert_strukt
ADD '||:NEW.name||' VARCHAR2(40) ');
INSERT INTO resulted (name_unique)
VALUES (:NEW.name);
COMMIT;
END;
I recommend you to use a Stored procedure to do so. Also, no transaction control statements are allowed in a trigger. ROLLBACK, COMMIT, and SAVEPOINT cannot be used.

Count(*) not working properly

I create the trigger A1 so that an article with a certain type, that is 'Bert' cannot be added more than once and it can have only 1 in the stock.
However, although i create the trigger, i can still add an article with the type 'Bert'. Somehow, the count returns '0' but when i run the same sql statement, it returns the correct number. It also starts counting properly if I drop the trigger and re-add it. Any ideas what might be going wrong?
TRIGGER A1 BEFORE INSERT ON mytable
FOR EACH ROW
DECLARE
l_count NUMBER;
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
SELECT COUNT(*) INTO l_count FROM mytable WHERE article = :new.article;
dbms_output.put_line('Count: ' || l_count);
IF l_count >0 THEN
IF(:new.TYPEB = 'Bert') THEN
dbms_output.put_line('article already exists!');
ROLLBACK;
END IF;
ELSIF (:new.TYPEB = 'Bert' AND :new.stock_count>1) THEN
dbms_output.put_line('stock cannot have more than 1 of this article with type Bert');
ROLLBACK;
END IF;
END;
This is the insert statement I use:
INSERT INTO mytable VALUES('Chip',1,9,1,'Bert');
A couple of points. First, you are misusing the autonomous transaction pragma. It is meant for separate transactions you need to commit or rollback independently of the main transaction. You are using it to rollback the main transaction -- and you never commit if there is no error.
And those "unforeseen consequences" someone mentioned? One of them is that your count always returns 0. So remove the pragma both because it is being misused and so the count will return a proper value.
Another thing is don't have commits or rollbacks within triggers. Raise an error and let the controlling code do what it needs to do. I know the rollbacks were because of the pragma. Just don't forget to remove them when you remove the pragma.
The following trigger works for me:
CREATE OR REPLACE TRIGGER trg_mytable_biu
BEFORE INSERT OR UPDATE ON mytable
FOR EACH ROW
WHEN (NEW.TYPEB = 'Bert') -- Don't even execute unless this is Bert
DECLARE
L_COUNT NUMBER;
BEGIN
SELECT COUNT(*) INTO L_COUNT
FROM MYTABLE
WHERE ARTICLE = :NEW.ARTICLE
AND TYPEB = :NEW.TYPEB;
IF L_COUNT > 0 THEN
RAISE_APPLICATION_ERROR( -20001, 'Bert already exists!' );
ELSIF :NEW.STOCK_COUNT > 1 THEN
RAISE_APPLICATION_ERROR( -20001, 'Can''t insert more than one Bert!' );
END IF;
END;
However, it's not a good idea for a trigger on a table to separately access that table. Usually the system won't even allow it -- this trigger won't execute at all if changed to "after". If it is allowed to execute, one can never be sure of the results obtained -- as you already found out. Actually, I'm a little surprised the trigger above works. I would feel uneasy using it in a real database.
The best option when a trigger must access the target table is to hide the table behind a view and write an "instead of" trigger on the view. That trigger can access the table all it wants.
You need to do an AFTER trigger, not a BEFORE trigger. Doing a count(*) "BEFORE" the insert occurs results in zero rows because the data hasn't been inserted yet.

Why should we use rollback in sql explicitly?

I'm using PostgreSQL 9.3
I have one misunderstanding about transactions and how they work. Suppose we wrapped some SQL operator within a transaction like the following:
BEGIN;
insert into tbl (name, val) VALUES('John', 'Doe');
insert into tbl (name, val) VALUES('John', 'Doee');
COMMIT;
If something goes wrong the transaction will automatically be rolled back. Taking that into account I can't get when should we use ROLLBACK explicitly? Could you get an example when it's necessary?
In PostgreSQL the transaction is not automatically rolled back on error.
It is set to the aborted state, where further commands will fail with an error until you roll the transaction back.
Observe:
regress=> BEGIN;
BEGIN
regress=> LOCK TABLE nosuchtable;
ERROR: relation "nosuchtable" does not exist
regress=> SELECT 1;
ERROR: current transaction is aborted, commands ignored until end of transaction block
regress=> ROLLBACK;
ROLLBACK
This is important, because it prevents you from accidentally executing half a transaction. Imagine if PostgreSQL automatically rolled back, allowing new implicit transactions to occur, and you tried to run the following sequence of statements:
BEGIN;
INSERT INTO archive_table SELECT * FROM current_tabble;
DELETE FROM current_table;
COMMIT;
PostgreSQL will abort the transaction when it sees the typo current_tabble. So the DELETE will never happen - all statements get ignored after the error, and the COMMIT is treated as a ROLLBACK for an aborted transaction:
regress=> BEGIN;
BEGIN
regress=> SELECT typo;
ERROR: column "typo" does not exist
regress=> COMMIT;
ROLLBACK
If it instead automatically rolled the transaction back, it'd be like you ran:
BEGIN;
INSERT INTO archive_table SELECT * FROM current_tabble;
ROLLBACK; -- automatic
BEGIN; -- automatic
DELETE FROM current_table;
COMMIT; -- automatic
... which, needless to say, would probably make you quite upset.
Other uses for explicit ROLLBACK are manual modification and test cases:
Do some changes to the data (UPDATE, DELETE ...).
Run SELECT statements to check results of data modification.
Do ROLLBACK if results are not as expected.
In Postgres DB you can do this even with DDL statements (CREATE TABLE, ...)

T-SQL could not rollback

I have some code that has a purely sequential flow, without transaction.
I sandwich them with a begin transaction and commit transaction
begin transaction
......--My code here......
......
......--code to create Table1
......
ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT [FK_constraint] FOREIGN KEY([field1], [field2])
REFERENCES [dbo].[Table2] ([field3], [field4])
GO
....
......--End of My code here......
rollback transaction
commit transaction
when i run the script until just above "rollback transaction" in management studio, if a simple error occurs such as division by zero, I run "rollback transaction", all changes are rolledback without problem.
But if the alter table statement fails because Table2 doesn't exist, it then triggers further errors.
Msg 1767, Level 16, State 0, Line 2
Foreign key 'FK_Constraint references invalid table 'dbo.Table2'.
Msg 1750, Level 16, State 0, Line 2
Could not create constraint. See previous errors.
Msg 1767, Level 16, State 0, Line 2
Foreign key 'FK_xxxxxx' references invalid table 'Table1'.
When I run "rollback transaction", I got this error message "The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION." which is silly, because I DO HAVE a begin transaction on top!
Please tell me what went wrong. Any help would be much appreciated. Using SQL-Server 2008.
EDIT:
I added
SELECT ##TRANCOUNT;
before and after "ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT"
....
SELECT ##TRANCOUNT;
ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT [FK_constraint] FOREIGN KEY([field1], [field2]) REFERENCES [dbo].[Table2] ([field3], [field4])
GO
SELECT ##TRANCOUNT;
....
The results are 1 and 0 respectively. The alter table automatically rollbacks my transaction on error!? I can't understand this.
I think there's nothing you can do about Sql Server treatment with DDL error severity handling, some of it are handled automatically (forcibly rolling back transaction for example) by Sql Server itself.
What you can just do is make your script code cope around it and provide script users with descriptive error.
An example:
-- drop table thetransformersmorethanmeetstheeye
-- select * from thetransformersmorethanmeetstheeye
-- first batch begins here
begin tran
create table thetransformersmorethanmeetstheeye(i int); -- non-erring if not yet existing
-- even there's an error here, ##ERROR will be 0 on next batch
ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT [FK_constraint] FOREIGN KEY([field1], [field2])
REFERENCES [dbo].[Table2] ([field3], [field4]);
go -- first batch ends here
-- second batch begins here
if ##TRANCOUNT > 0 begin
PRINT 'I have a control here if things needed be committed or rolled back';
-- ##ERROR is always zero here, even there's an error before the GO batch.
-- ##ERROR cannot span two batches, it's always gets reset to zero on next batch
PRINT ##ERROR;
-- But you can choose whether to COMMIT or ROLLBACK non-erring things here
-- COMMIT TRAN;
-- ROLLBACK TRAN;
end
else if ##TRANCOUNT = 0 begin
PRINT 'Sql Server automatically rollback the transaction. Nothing can do about it';
end
else begin
PRINT 'Anomaly occured, ##TRANCOUNT cannot be -1, report this to Microsoft!';
end
-- second batch implicitly ends here
The only way this happens is if there is no open transaction in that SPID.
That's it. And the only way there's no open transaction is that either:
You never started a new transaction after the old one committed or rolled back
You have another commit or rollback somewhere you didn't notice
Something killed your connection or forced a rollback from outside your spid (like a kill command from another session)
You don't provide much code. Is there any error trapping or any other conditional logic in your query that's not shown?
As far as I know, the ALTER TABLE command will create its own new transaction, and when it fails, will rollback that transaction. A single rollback within a proc will cause all the open transactions within that proc to be rolled back. So you're seeing the error because the failure of the ALTER TABLE statement is implicitly rolling back your transaction before you try to do it..
You can confirm this easily enough by checking the #TRANCOUNT within your code, and only calling rollback when it is not zero
The error from the ALTER TABLE statement is a compile error rather than a runtime error - and so the whole batch in which that statement occurs is never executed. I'm guessing that there's no GO between BEGIN TRANSACTION and ALTER TABLE - hence the BEGIN TRANSACTION never executed, and what SQL Server is telling you is perfectly true.
Try adding a GO immediately after the BEGIN TRANSACTION.
Given this:
create table z
(
i int identity(1,1) not null,
zzz int not null
);
When you try the following..
begin try
begin transaction
alter table z drop column aaa;
commit tran;
end try
begin catch
print 'hello';
SELECT
ERROR_NUMBER() as ErrorNumber,
ERROR_MESSAGE() as ErrorMessage;
IF (XACT_STATE()) = -1
BEGIN
PRINT
N'The transaction is in an uncommittable state. ' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
end catch
print 'reached';
..the error can be caught:
ErrorNumber ErrorMessage
4924 ALTER TABLE DROP COLUMN failed because column 'aaa' does not exist in table 'z'.
But try changing alter table z drop column aaa; to alter table z add zzz int;, Sql Server can catch the error..
Column names in each table must be unique. Column name 'zzz' in table
'z' is specified more than once.
..but won't yield back the control to you, CATCH block will not be triggered. Seems there's no hard and fast rules what errors are catch-able and which are not.
To illustrate the difference, here's the error catch-able by your code
Here's an error un-catch-able by your code, which is similar to your problem.
Notice that there's no grid there(via SELECT ERROR_NUMBER() as ErrorNumber, ERROR_MESSAGE() as ErrorMessage;). That means, Sql Server did not yield back the control to you after it detected an exception.
Maybe you can see other details here that might help: http://msdn.microsoft.com/en-us/library/ms179296.aspx
See this guideline for error handling ##ERROR and/or TRY - CATCH
By the way, on Postgresql all kind of DDL errors are catch-able by your code.
do $$
begin
-- alter table z drop column aaa;
alter table z add zzz int;
exception when others then
raise notice 'The transaction is in an uncommittable state. '
'Transaction was rolled back';
raise notice 'Yo this is good! --> % %', SQLERRM, SQLSTATE;
end;
$$ language 'plpgsql';
Here's the dev-rendered error message for alter table z drop column aaa; on Postgresql:
Here's the dev-rendered error message for alter table z add zzz int; on Postgresql; which by the way in Sql Server, when it has an error on this type of statement, it won't yield back the control to you, hence your CATCH sections are sometimes useful, sometimes useless.