I need to delete one or more row from list of tables stored in a table, and commit only if all deletion succeed.
So I wrote something like this (as part of a bigger procedure):
BEGIN
SAVEPOINT sp;
FOR cur_table IN (SELECT * FROM TABLE_OF_TABLES)
LOOP
EXECUTE IMMEDIATE 'DELETE FROM ' || cur_table.TABNAME || ' WHERE ID = :id_bind'
USING id;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO SAVEPOINT sp;
END;
I know this couldn't work, because of the "execute immediate".
So, what is the correct way to do that?
Dynamic SQL (Execute Immediate) doesn't commit the transaction. You have to commit explicitly. Your code is fine but it doesn't record/log the errors in case if they occur.
Log the errors in the exception handler section.
Honestly, this sounds like a bad idea. Since you have the tables stored in the database, why not just list them out in your procedure? Surely, you're not adding tables that often that this procedure would need to be updated often?
As pointed out in the comments, this will work fine, since EXECUTE IMMEDIATE does not automatically commit.
Don't forget to add a RAISE at the end of your exception block, or you'll never know that an error happened.
Related
I want to write a stored proc that delete data from two tables.
Should either of the deletes fail I want to make sure that no data was deleted.
This should be a simple task, but I never worked in Oracle before.
I'm not if I should be using TRY/CATCH, TRANSACTIONS or SAVEPOINTS.
Any guidance would be appreciated.
Currently I have:
CREATE OR REPLACE PROCEDURE SP_DELETE_STUFF
(
GROUPNAME IN VARCHAR2
) AS
BEGIN
SAVEPOINT Original_Start;
-- First delete all permissions for a given group
DELETE FROM my_table_1
WHERE group_name = GROUPNAME;
-- Second delete the group
DELETE FROM my_table_2
WHERE group_name = GROUPNAME;
EXCEPTION
WHEN OTHERS THEN
BEGIN
ROLLBACK TO SAVEPOINT Original_Start;
COMMIT;
END;
END
If your goal is just to rollback the changes that a particular call of the stored procedure has made if there is an error, you'd use a savepoint and a rollback to savepoint like you are doing here.
I would question your use of a commit after your rollback to savepoint. That will commit the transaction that the caller of your stored procedure had started. It seems unlikely that you want to commit the caller's changes when your procedure encounters an error. So I would expect that you want to remove the commit.
Not related to transaction scoping, I would also expect that you would want to at least re-raise the exception that you caught so that the caller is aware that there was a failure. I would expect that you would want something like
EXCEPTION
WHEN OTHERS THEN
BEGIN
ROLLBACK TO SAVEPOINT Original_Start;
RAISE;
END;
We have a stored procedure that inserts data into a table from some temporary tables in oracle database. There is an update statement after the insert that updates a flag in the same table based on certain checks. At the end of the stored procedure commit happens.
The problem is that the update works on 95% cases but in some cases it fails to update. When we try to run it again without changing anything, it works. Even trying to execute the same stored procedure on the same data at some other time, works perfectly. I haven't found any issues in the logic in the stored procedure. I feel there is some database level issue which we are not able to find (maybe related to concurrency). Any ideas on this would be very helpful.
Without seeing the source code we will just be guessing. The most obvious suggestion that I can think of is that it hits an exception somewhere along the way in some cases and never gets as far as the commit. Another possibility is that there is a lock on the table during execution when it fails.
Probably the best thing to investigate further would be to add an exception handler that writes the exceptions to some table or file and see what error is raised e.g.
-- create a logging table
create table tmp_error_log (timestamp timestamp(0), Error_test varchar2(1000));
-- add a variable to your procedure declaration
v_sql varchar2(1000);
-- add an exception handler just before the final end; statement on your procedure
exception
when others then
begin
v_sql := 'insert into tmp_error_log values(''' ||
to_char(sysdate, 'DD-MON-YYYY HH24:MI:SS') || ''', ''' || SQLERRM || ''')';
dbms_output.put_line(v_sql);
execute immediate v_sql;
commit;
end;
-- see what you get in the table
select * from tmp_error_log;
Right now I'm importing and transforming data into an Oracle database as follows:
A program regularly polls specific folders, once a file is found it executes a batch file which does some light transformation in Python & bash and then calls SQL*Loader to load the CSV file into a staging table.
Then, the batch script calls an SQL script (via SQLPlus) to do the final transformation and insert the transformed data into master tables for their respective staging table.
The problem with this method is there's no error-handling on the SQLPlus side, eg. if an 'insert into' statement fails because of a violated constraint (or any other reason), it will still continue to execute the rest of the statements contained in the SQL script.
Ideally, if any exception occurs, I'd prefer all changes to be rolled back and details of the exception inserted into an etl log table.
Stored procedures seem to be a good fit as exception handling is built-in. However, I'm struggling with the syntax - specifically how I can take my big SQL scripts (which are just a combination of INSERT INTO, UPDATE, CREATE, DROP, DELETE, etc statements) and throw them into a stored procedure with some very basic error handling.
What I'm hoping for is either:
a quick & dirty dummy's guide to taking my depressing blob of PL/SQL and get it to execute within a stored procedure OR
Any alternative (if a stored proc isn't appropriate) which offers the same functionality, ie. a way to execute a bunch of SQL statements and rollback if any of these statements throw an exception.
About my attempts - I've tried copying portions of my SQL scripts into a stored procedure but they always fail to compile with the error 'PLS-00103 Encountered the symbol when expecting one of the following'. eg.
CREATE OR REPLACE PROCEDURE ETL_2618A AS
BEGIN
DROP SEQUENCE "METER_REPORTING"."SEQ_2618";
CREATE SEQUENCE SEQ_2618;
END ETL_2618A;
Oracle documentation isn't terribly accessible and I've not had much luck with googling/searching StackOverflow, but I apologise if I've missed something obvious.
To do DDL in PL/SQL you need to use dynamic sql.
CREATE OR REPLACE PROCEDURE testProc IS
s_sql VARCHAR2(500);
BEGIN
s_sql := 'DROP SEQUENCE "METER_REPORTING"."SEQ_2618"';
EXECUTE IMMEDIATE s_sql;
s_sql := 'CREATE SEQUENCE "METER_REPORTING"."SEQ_2618"';
EXECUTE IMMEDIATE s_sql;
EXCEPTION
WHEN OTHERS THEN
NULL;
end testProc;
/
If you run the script in sqlplus you can use:
whenever sqlerror
to control what should happen when an error occurs.
http://docs.oracle.com/cd/B19306_01/server.102/b14357/ch12052.htm
Adding exception handling to a PL/SQL proc or script isn't difficult, but of course some coding is required. Here's your procedure dressed up slightly with some very basic error reporting added:
CREATE OR REPLACE PROCEDURE ETL_2618A AS
nCheckpoint NUMBER;
BEGIN
nCheckpoint := 1;
EXECUTE IMMEDIATE 'DROP SEQUENCE "METER_REPORTING"."SEQ_2618"';
nCheckpoint := 2;
EXECUTE IMMEDIATE 'CREATE SEQUENCE SEQ_2618';
RETURN;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('ETL_2618A failed at checkpoint ' || nCheckpoint ||
' with error ' || SQLCODE || ' : ' || SQLERRM);
RAISE;
END ETL_2618A;
Not tested on animals - you'll be first! :-)
The following link on the PostgreSQL documentation manual http://www.postgresql.org/docs/8.3/interactive/populate.html says that to disable autocommit in postgreSQL you can simply place all insert statements within BEGIN; and COMMIT;
However I have difficulty in capturing any exceptions that may happen between the BEGIN; COMMIT; and if an error occurs (like trying to insert a duplicate PK) I have no way to explicitly call the ROLLBACK or COMMIT commands. Although all insert statements are automatically rolled back, PostgreSQL still expects an explicit call to either the COMMIT or ROLLBACK commands before it can consider the transaction to be terminated. Otherwise, the script has to wait for the transaction to time out and any statements executed thereafter will raise an error.
In a stored procedure you can use the EXCEPTION clause to do this but the same does not apply in my circumstance of performing bulk inserts. I have tried it and the exception block did not work for me because the next statement/s executed after the error takes place fails to execute with the error:
ERROR: current transaction is aborted, commands ignored until end of transaction block
The transaction remains open as it has not been explicitly finalised with a call to COMMIT or ROLLBACK;
Here is a sample of the code I used to test this:
BEGIN;
SET search_path TO testing;
INSERT INTO friends (id, name) VALUES (1, 'asd');
INSERT INTO friends (id, name) VALUES (2, 'abcd');
INSERT INTO friends (id, nsame) VALUES (2, 'abcd'); /*note the deliberate mistake in attribute name and also the deliberately repeated pk value number 2*/
EXCEPTION /* this part does not work for me */
WHEN OTHERS THEN
ROLLBACK;
COMMIT;
When using such technique do I really have to guarantee that all statements will succeed? Why is this so? Isn't there a way to trap errors and explicitly call a rollback?
Thank you
if you do it between begin and commit then everything is automatically rolled back in case of an exception.
Excerpt from the url you posted:
"An additional benefit of doing all insertions in one transaction is that if the insertion of one row were to fail then the insertion of all rows inserted up to that point would be rolled back, so you won't be stuck with partially loaded data."
When I initialize databases, i.e. create a series of tables/views/functions/triggers/etc. and/or loading in the initial data, I always use psql and it's Variables to control the flow. I always add:
\set ON_ERROR_STOP
to the top of my scripts, so whenever I hit any exception, psql will abort. It looks like this might help in your case too.
And in cases when I need to do some exception handling, I use anonymous code blocks like this:
DO $$DECLARE _rec record;
BEGIN
FOR _rec IN SELECT * FROM schema WHERE schema_name != 'master' LOOP
EXECUTE 'DROP SCHEMA '||_rec.schema_name||' CASCADE';
END LOOP;
EXCEPTION WHEN others THEN
NULL;
END;$$;
DROP SCHEMA master CASCADE;
I have a PL/SQL script where it runs a bunch of commands, and calls commit at the very end.
select count(*) into countCol from USER_TAB_COLUMNS where TABLE_NAME = 'EVAPP_CARLETON_RESULT' and COLUMN_NAME = 'AMTFIN' and DATA_SCALE is null;
IF (countCol <> 0) then
execute immediate 'alter table EVAPP_CARLETON_RESULT add AMTFIN_TMP NUMBER(10,2)' ;
execute immediate 'update EVAPP_CARLETON_RESULT set AMTFIN_TMP = AMTFIN' ;
execute immediate 'alter table EVAPP_CARLETON_RESULT drop column AMTFIN' ;
execute immediate 'alter table EVAPP_CARLETON_RESULT rename column AMTFIN_TMP to AMTFIN' ;
DBMS_OUTPUT.put_line('This column EVAPP_CARLETON_RESULT.AMTFIN has been modified to the required precision');
END IF;
logger('68');
evaluate.commitScript;
There are 68 such blocks prior to this but evaluate.commitScript is called at the very end.
But, logger is called after every such block, and it has a commit statement inside of it.
create or replace
PROCEDURE LOGGER
(MESSAGE1 IN VARCHAR2 ) AS
pragma autonomous_transaction;
BEGIN
insert into message_log (datetime, message) values (sysdate, message1);
commit;
END LOGGER;
The commit commits all changes simultaneously. Not just the changes made by the procedure. Is there anyway, we can call commit selectively?
It looks like you already have the solution with this line in the LOGGER proc:
pragma autonomous_transaction
That statement sets a separate transaction within the procedure. The COMMIT issued there will not commit any transactions outside of that procedure.
Also, the DDL in the EXECUTE IMMEDIATE statements are implicitly committed.
The commit in the LOGGER procedure will only commit the changes made by that call of the LOGGER procedure.
If the snippet you posted is representative of the 68 blocks, the problem is that DDL statements (like ALTER TABLE) implicitly issue two commits-- one before the statement is executed and one after the execution if the execution was successful. That means that DDL cannot be done transactionally and the evaluate.commitScript call is neither committing nor rolling back any changes.
Depending on the Oracle version, edition, and configuration, you may be able to use something like Oracle flashback database to create a restore point before running your script and the flashing back to that restore point if your script fails (though that would also roll back the changes made by your LOGGER procedure).