René Nyffenegger's collection of things on the web
René Nyffenegger on Oracle - Most wanted - Feedback
 

Logging a procedure's progress using autonomous transactions in PL/SQL

Each message will be assigned to a type that determines the nature of the message. For example (as shown below), there are error messages, progress messages, debug messages and simply messages.
create table msg_type (
  tp          varchar2(3),
  dsc         varchar2(40),
  constraint  pk_msg_type primary key (tp)
);

insert into msg_type (tp,dsc) values
  ('err', 'Error');

insert into msg_type (tp,dsc) values
  ('prg', 'Progress');

insert into msg_type (tp,dsc) values
  ('dbg', 'Debug');

insert into msg_type (tp,dsc) values
  ('msg', 'Message');
A message has also a source. This source is the originator of the message, in most cases the name of the programm.
create table msg_source (
  src         varchar2(3),
  dsc         varchar2(40),
  constraint  pk_msg_source primary key (src)
);

insert into msg_source (src,dsc) values
  ('pfc', 'Upload Peformance');
insert into msg_source (src,dsc) values
  ('fsr', 'Upload Fund Shr');

insert into msg_source (src,dsc) values
  ('sr', 'Upload Fund Shr SR');
Anticipating anomalies while loading the data, but also to have a means to have place to report Messages (for example Debug, but also Success and Progress) the tables msg_head and msg_tail are created.:
create table msg_head (
  dt          date         not null,
  id          number       not null,
  tp                       not null,
  src                      not null,
  cd          varchar2(30) not null,
  sql_cd      number,
  constraint pk_msg_hd     primary key (id),
  constraint fk_msg_hd_tp  foreign key (tp) 
    references msg_type,
  constraint fk_msg_hd_src foreign key (src)
    references msg_source
) storage (pctincrease 0 initial 512k next 512k);

create sequence seq_msg_head start with 1 increment by 1;

create or replace trigger ai_msg_hd 
  before insert on msg_head
  for each row
  begin
    if :new.id is null then
      select seq_msg_head.nextval into :new.id from dual;
    end if;
      
    select sysdate into :new.dt from dual;
  end;
/
The sequence seq_fund_error_head guarantees unique IDs for the table error_head.
Msg_Head stands in a 1:n relationship to Msg_Tail, that is, for each Msg In Msg_Head, zero, one, or more Msg_Tails can be appended, thus making it possible to further describe the message.
create table msg_tail (
  id                   not null,
  description          varchar2(100),
  constraint fk_er_tl foreign key (id) 
     references msg_head
     on delete cascade
) storage (pctincrease 0 initial 512k next 512k);
create or replace package msg as
  p_def_src  msg_source.src%type;
  p_hd_id    msg_head.id%type;

  procedure id(v_id in number);
  procedure tp(
    v_tp in msg_head.tp%type,
    tail in boolean default true);
  procedure clean;

  procedure head(
    c in msg_head.cd%type,
    t in msg_head.tp%type,
    s in msg_head.src%type default p_def_src);
 
  -- sql_err is similar to the procedure head 
  -- in that it writes an entry into msg_head. However,
  -- it differs in the following ways: it writes sql_code
  -- and writes an entry with the error msg into err_tail
  procedure sql_err(
    c in msg_head.cd%type,
    s in msg_head.src%type default p_def_src,
    t in msg_head.tp%type default 'err');

  procedure tail(
    d in msg_tail.description%type);

  -- This procedure shows and deletes the
  -- next v_n entries of msg_head
  procedure nxt(v_n in number default 1);
 
  -- This procedure shows the last v_n entries
  -- in msg_head
  procedure lst(v_n in number default 1);

  -- This procedure shows the first v_n entries
  -- in msg_head
  procedure fst(v_n in number default 1);

  procedure def_src(v_def_src in msg_source.src%type);

end;
/

create or replace package body msg as
  procedure tp(
      v_tp in msg_head.tp%type,
      tail in boolean default true) is
    cursor c(v_tp in msg_head.tp%type) is
      select id from msg_head where tp=v_tp
        order by id;
  begin
    for r in c(v_tp) loop
      id(r.id);
    end loop;
  end;


  procedure id(v_id in number) is
    v_cd   msg_head.cd%type;
    cursor c(v_id in number)
      is select description from msg_tail where
        id = v_id;
  begin
    select cd into v_cd from msg_head
      where id=v_id;
    dbms_output.put_line('');
    dbms_output.put_line('  [' ||
     to_char(v_id,'999999999') || '] ' || v_cd);

    for r in c(v_id) loop
      dbms_output.put_line ('               ' || r.description);
    end loop;
  exception
    when no_data_found then
      dbms_output.put_line ('  # no entry for ' || v_id);
  end;  

  procedure clean is 
  begin
    execute immediate 
      'alter table msg_tail modify constraint fk_er_tl disable';
    execute immediate 'truncate table msg_tail';
    execute immediate 'truncate table msg_head';
    execute immediate 
     'alter table msg_tail modify constraint fk_er_tl enable';
  end;

  procedure nxt(v_n in number default 1) is
    cursor c is 
     select id from msg_head order by id for update;
    a_n number:=0;
   
  begin
    for r in c loop
      exit when a_n >= v_n;
      id(r.id);
      delete msg_head where current of c;
      a_n := a_n + 1;
    end loop;
    commit;
  end;

  procedure sql_err(
    c in msg_head.cd%type,
    s in msg_head.src%type default p_def_src,
    t in msg_head.tp%type  default 'err')
  is
    PRAGMA AUTONOMOUS_TRANSACTION;
    v_sqlcode       number;
    v_sqlerrm       varchar2(200);
  begin
    v_sqlcode := sqlcode;
    v_sqlerrm := sqlerrm(v_sqlcode);

    insert into msg_head(cd,tp,src,sql_cd)
      values (c,t,s,v_sqlcode) returning id into p_hd_id; 
--    commit;
    tail(v_sqlerrm);
end;


  procedure head(
    c in msg_head.cd%type,
    t in msg_head.tp%type,
    s in msg_head.src%type default p_def_src)
  is
    PRAGMA AUTONOMOUS_TRANSACTION;
  begin
    insert into msg_head(cd,tp,src) values (c,t,s) 
      returning id into p_hd_id;
    commit;
  end;

  procedure tail(d in msg_tail.description%type) is
    PRAGMA AUTONOMOUS_TRANSACTION;
  begin
    insert into msg_tail(id,description) values (p_hd_id,d);
--    commit;
  end;

  procedure lst(v_n in number default 1) is
    stmt varchar2(200);
    type rct is ref cursor;
    c    rct;
    v_id number;

  begin
    stmt := 'select id from (
        select id, rank() over(order by id desc) rn
        from msg_head
      ) where rn <= :n order by id';

      open c for stmt using v_n;
      loop
        fetch c into v_id;
        exit when c%notfound;

            id(v_id);
    end loop;
  end;
 
  procedure fst(v_n in number default 1) is
    stmt varchar2(200);
      type rct is ref cursor;
      c    rct;
      v_id number;
  begin
    stmt := 'select id from (
        select id, rank() over(order by id) rn
        from msg_head where src = :1
      ) where rn <= :2 ';

      open c for stmt using p_def_src, v_n;
      loop
        fetch c into v_id;
        exit when c%notfound;

              id(v_id);
    end loop;

  end;

  procedure def_src(v_def_src in msg_source.src%type)
  is
  begin
    p_def_src := v_def_src;
  end;

end;
/

Writing Messages

SQL Errors

Whenever an SQL Error occurs, you can use sql_err to write an according message to msg_head. Sql_err checks the PL/SQL variable sqlcode and uses it to get an error string that then is written into msg_head. Consider the following example:
create table d (a number, b number, c number);
insert into d values(25,5,null);
insert into d values(64,4,null);
insert into d values(81,3,null);
insert into d values(17,0,null);
insert into d values(28,2,null);

commit;

declare
  cursor cr is select a, b from d;
begin
  msg.def_src('tst');
  for r in cr loop
    msg.head('updating d','prg');
    msg.tail('a: ' || r.a);
    msg.tail('b: ' || r.b);
    update d set c = r.a/r.b  where a=r.a and b=r.b;
  end loop;
exception
   when others then
      msg.sql_err('sql error');
      msg.tail('tail');  
      rollback;
	end;
     /

     select * from d;

     drop table d;

Reading from MSG

Use fst to read the first n entries of msg_head (and msg_tail)
Use lst to read the last n entries of msg_head (and msg_tail)
Use nxt to read and then delte the next n entries of msg_head (and msg_tail)