Skip to Main Content

SQL & PL/SQL

Announcement

For appeals, questions and feedback about Oracle Forums, please email oracle-forums-moderators_us@oracle.com. Technical questions should be asked in the appropriate category. Thank you!

How do I delete cascade with a PL/SQL procedure?

rodolfo4Mar 21 2011 — edited Mar 23 2011
This script will create a PL/SQL procedure that deletes cascade. This is a post to contribute to the Oracle community. Take the code as is and test it before you use it in production. Make sure this is what you want.

Procedure Delete Cascade (prc_delete_cascade)

Description
=============
The principle is very simple. The procedure uses a table called TO_BE_DELETED to keep a list of records to be deleted. This
table keeps the table name and the rowid of those records that need to be deleted. The procedure also uses a function called
DELETE_BOTT_ROW which takes one record of the table and tries to delete it. If the deletion fails with a foreign key constraint
violation, the function parses the SQL error message (SQLERRM) to get the name of the constraint. With the name of the constraint,
the function finds the name of the child table, all the child records that have references to the parent table primary or unique key,
and the parent key primary or unique key column name. Once the child records of the failed delete are identified, the function takes their table name and rowids
and records them into the TO_BE_DELETED table by inserting records of their table name and their rowids. Al the records inserted also contain the level (which
is 1 for the original records, 2 for child records, 3 for granchild records, etc.) and the sequence number of the order in wich they
are recorded. This way, when the function picks up a record to be deleted, it takes the one with the highest level and the highest
inserted sequence, or the "bottom" record. Once all the child records of the failed delete are appended to the TO_BE_DELETED table, it calls itself
recursevely, and the function takes the record at the "bottom" of the table and tries to delete it. If it succeeds, it calls
itself recursevely to delete the next record. If it fails, it goes and finds the child records as described before and once they are
inserted into the TO_BE_DELETED table, it calls itself again recursevely to try to delete again the "bottom" record. All records
that are successfully deleted are flagged as deleted usig the flag_del column so they are not pickt up again. Once all the (parent,
child, grandchild, etc.) records are deleted, the procedure ends without commiting, giving the option to the user to commit or
rollback deletions. The table TO_BE_DELETED is, at the end of the procedure, a list of all the records that were deleted, including their table names
and the order in with they were deleted. The user then can review its content and decide to commit or rollback.

Restrictions
============
1. Single tables only. The procedure only takes one table name and a WHERE clause to identified the records to be deleted.
2. Single columns only. Ther procedure only works with single-column primary, unique and foreign key constraints.
3. Single schema only.
4. Unpredictable results with circular references.
 
drop table to_be_deleted purge;
create table to_be_deleted
(tname varchar2(30)       -- table name
,rid rowid                -- rowid
,lvl number               -- level: 1=parent, 2=child, 3=grandchild, etc.
,seq_ins number           -- sequence order of record inserted
,flg_del char             -- flag deleted: Y=record deleted
,seq_del number           -- global order of record deletion
);

set serveroutput on size 1000000
create or replace procedure prc_delete_cascade
(p_tname varchar2  -- table name
,p_where varchar2  -- where clause identifying records to be cascade deleted
)
is
  dummy         char;
  v_sqlcode     number;
  v_sqlerrm     varchar2(32767);
  v_param_val   integer := 0;
  v_sql         varchar2(4000);
  v_ret_cde     number;
  e_bad_params  exception;
  v_iter        number;
  v_plvl        number;
  v_seq_del     number;
  v_max_iter    number := 1000000000;
  
  function delete_bott_row
  return number
  is
    v_sql        varchar2(4000);
    
    v_ptname     varchar2(30);  -- parent table name
    v_ppkname    varchar2(30);  -- parent primary key constraint name
    v_ppkcname   varchar2(30);  -- parnet primary key column name
 	  
    v_prowid      rowid;
    v_crowid      rowid;
 	  
    v_ctname     varchar2(30);  -- child table name
    v_cfkname    varchar2(30);  -- child foreign key constraint name
    v_cfkcname   varchar2(30);  -- child foreign key column name
 	  
    v_ins        number;
    v_seq_ins    number;
 	    
    v_sqlerrm    varchar2(4000);
    v_sqlcode    number;
 	  
    e_const_viol exception;
    pragma exception_init(e_const_viol, -2292);
 	  
    e_max_iter_reached exception;
 	  
  begin
    v_iter := v_iter + 1;
  	  
    if v_iter >= v_max_iter then
      raise e_max_iter_reached;
    end if;
  	  
    dbms_output.put_line('- Iter '||to_char(v_iter));
    dbms_output.put_line('----------');
    dbms_output.put_line('- Starting function delete_bott_row');
    v_sql := 'select tname, rid, lvl, seq_ins from (select * from to_be_deleted where flg_del = ''N'' order by lvl desc, seq_ins desc) where rownum=1';
    --  dbms_output.put_line('- SQL: '||v_sql);
    execute immediate v_sql into v_ptname, v_prowid, v_plvl, v_seq_ins;
  	  
    dbms_output.put_line('- Selected row: table name: '||v_ptname||', level: '||v_plvl||', seq: '||v_seq_ins);
  	  
    v_sql := 'delete from '||v_ptname||' where rowid='''||v_prowid||'''';
    dbms_output.put_line('- SQL: '||v_sql);
    execute immediate v_sql;
    dbms_output.put_line('- Row deleted !!!');
    v_ret_cde := 1;
    v_seq_del := v_seq_del + 1;
  	  
    dbms_output.put_line('- Mark the row deleted');
    v_sql := 'update to_be_deleted set flg_del = ''Y'', seq_del = '||to_char(v_seq_del)||' where tname='''||v_ptname||''' and rid='''||v_prowid||'''';
    -- dbms_output.put_line('- SQL: '||v_sql);
    execute immediate v_sql;
    -- dbms_output.put_line('- Updated table to_be_deleted, row marked deleted');
    -- dbms_output.put_line('- End of iter '||to_char(v_iter));
    dbms_output.put_line('----------');
      
    -- call function delete_bott_row recursively
    v_ret_cde := delete_bott_row;

    return 0;
      
  exception
    when no_data_found then
      dbms_output.put_line('- Table to_be_deleted is empty, delete cascade has completed successfully.');
      v_ret_cde := 0;
      return 0;
  	    
    when e_const_viol then
      v_sqlcode := SQLCODE;
      v_sqlerrm := SQLERRM;

      v_ret_cde := v_sqlcode;
        
      dbms_output.put_line('>Constraint Violation. Record has children');
      -- dbms_output.put_line('Error code: '||to_char(v_sqlcode));
      v_cfkname := substr(v_sqlerrm,instr(v_sqlerrm,'.')+1,instr(v_sqlerrm,')') - instr(v_sqlerrm,'.')-1);
  	    
      dbms_output.put_line('>Child FK name: '||v_cfkname);
  	    
      select table_name, column_name
        into v_ctname, v_cfkcname
        from user_cons_columns
       where constraint_name=v_cfkname;
      dbms_output.put_line('>Child table name: '||v_ctname||'. FK column name: '|| v_cfkcname);
  	    
      select constraint_name, column_name
        into v_ppkname, v_ppkcname 
        from user_cons_columns
       where constraint_name = (select r_constraint_name 
                                  from user_constraints 
                                  where constraint_name=v_cfkname);
      dbms_output.put_line('>Parent PK/UK name: '||v_ppkname||'. Parent PK/UK column: '||v_ppkcname);
  	                             
      v_sql := 'insert into to_be_deleted(tname, rid, lvl, seq_ins, flg_del) '||
               'select '''||v_ctname||''', rowid, '||to_char(v_plvl+1)||', rownum, ''N'' '||
               'from '||v_ctname||' '||
               'where '||v_cfkcname||' =any (select '||v_ppkcname||' from '||v_ptname||' where rowid =any (select rid from to_be_deleted where tname = '''||v_ptname||'''))';
      -- dbms_output.put_line('- SQL: '||v_sql);
      execute immediate v_sql;

      select count(*) 
        into v_ins
        from to_be_deleted 
       where lvl = v_plvl+1 
         and tname = v_ctname
         and flg_del = 'N';
      dbms_output.put_line('>Found '||to_char(v_ins)||' child records which were added to table to_be_deleted');   
         
      v_ret_cde := delete_bott_row;
  	    
      return  v_ret_cde;
      
    when e_max_iter_reached then
      dbms_output.put_line('Maximum iterations reached.  Terminating procedure.');
      raise;
        
    when others then
      raise;
           
  end delete_bott_row;
   	      
begin
  dbms_output.put_line('Beginning');
  dbms_output.put_line('================================');
	
  -- validate p_table
  begin
    select 'Y'
      into dummy
      from user_tables
     where table_name=upper(p_tname);
  exception
    when no_data_found then
    v_param_val := 1;
    dbms_output.put_line('Table '||p_tname||' does not exist.');
    raise e_bad_params;
  end;
  dbms_output.put_line('- Parameter p_tname validated');
	
  -- validate p_where 
  begin
    execute immediate 'select ''Y'' from '||p_tname||' where '||p_where INTO dummy;
  exception
    when no_data_found then  -- where clause returns no records
      dbms_output.put_line('Record(s) not found.  Check your where clause parameter');
      v_param_val := 2;
      raise e_bad_params;
    when too_many_rows then  -- found multiple records means it is ok
      null;  
    when others then  --  any other records means where clause has something wrong.
      dbms_output.put_line('Where clause is malformed');      
      v_param_val := 2;
      raise e_bad_params;
  end;    
  dbms_output.put_line('- Parameter p_where validated');
	
  if v_param_val > 0 then raise e_bad_params; end if;
  
  v_iter := 0;
  v_plvl := 1;
  v_seq_del := 0;
  v_sql := 'insert into to_be_deleted(tname, rid, lvl, seq_ins, flg_del) select '''||upper(p_tname)||''', rowid, '||to_char(v_plvl)||', rownum, ''N'' from '||p_tname||' where '||p_where;

  dbms_output.put_line('- Inserting initial record');
  dbms_output.put_line('- SQL: '||v_sql);
  execute immediate v_sql;
  dbms_output.put_line('- Record(s) inserted'); 
  
  
  dbms_output.put_line('- Calling function delete_bott_row to delete last row of table to_be_deleted');               
  dbms_output.put_line('-----------------------------------');               
  v_ret_cde :=  delete_bott_row; 
  -- dbms_output.put_line('- Back from function delete_bott_row');               
  -- dbms_output.put_line('Return code: '||to_char(v_ret_cde));               
  dbms_output.put_line('- End of procedure');               

exception
  when e_bad_params then
    dbms_output.put_line('Bad parameters, exiting.');
end;
/

show errors

spool prc_delete_cascade.log
--  Call to the procedure
exec prc_delete_cascade('xent','xent_id between 1669 and 1670')

select tname "Table Name", count(*) "Rows deleted" 
  from to_be_deleted
 group by tname;

spool off

set lines 120
select * 
  from to_be_deleted 
 order by seq_del;

prompt  Now commit or rollaback deletions.
-- commit;
-- rollback;
Edited by: Rodolfo4 on Mar 23, 2011 10:45 AM
Comments
Locked Post
New comments cannot be posted to this locked post.
Post Details
Locked on Apr 20 2011
Added on Mar 21 2011
4 comments
9,349 views