4

Our data migration scripts make use of anonymous PL/SQL blocks to help tidy up the code, mainly so we can set the create and updated user Id columns to a "system" user.

Our migration scripts look something like:

DECLARE
    v_user_id users.id%TYPE;
BEGIN
    SELECT id INTO v_user_id FROM users WHERE username = 'system';

    UPDATE table1
    SET col1 = value,
        updated_at = SYSDATE,
        updated_by = v_user_id
    WHERE some condition;

    INSERT INTO table2 (val1, SYSDATE, v_user_id);
END;
/

The user who updated a record is a numeric Id from our users table, not the string username. This was a requirement from our data modeling team, otherwise I would just hard-code the username of our "system" account.

And as an additional side note, our DBA runs the scripts, and he should not be the one showing up as the person/user who updated or inserted records. Another requirement from the larger enterprise environment.

The output I would like to see from the sqlplus command line is something like:

Updated X rows
Inserted Y rows

Just like if you had run the INSERT and UPDATE statements outside of the PL/SQL block.

And I'm really hoping for a solution that doesn't require explicit calls to DBMS_OUTPUT.PUT_LINE.

How can I automatically display the output of each DML statement inside an anonymous PL/SQL block without explicit calls to DBMS_OUTPUT.PUT_LINE for each statement?

Greg Burghardt
  • 17,900
  • 9
  • 49
  • 92
  • 2
    you can use sql%rowcount to see how many rows were inserted/updated (looks like in your example only one row will be inserted) . If it was me I would create a function that used the autonomous transaction feature to log the progress (that separate transaction could commit so you could see the progress in the log table while the other program was running) – Peter M Mar 28 '18 at 14:53
  • 1
    @PeterM: From the research I've done, and your comment, basically you're saying there is no `SET FOO ON` setting for Oracle that automatically dumps the output of each statement inside the PL/SQL block to the standard output ... ? – Greg Burghardt Mar 28 '18 at 14:59
  • If you want feedback like "Inserted X rows" automatically, that's a SQL\*Plus feature which only works for SQL statements run directly. If they're embedded in a PL/SQL program SQL\*Plus is not in control and doesn't get that sort of feedback. That's what `dbms_output` is for. – Jeffrey Kemp Mar 28 '18 at 15:21
  • no, any dbms_output.put_line will only print after the whole thing is done. if you need an audit kind of thing, if it was me, I would generate the update statements using sql plus or some other program and run them all as a script, then for sure you will get the output after every statement. something like select 'update tab set col=''' || col_name ||''' where id = '''||id||''';' from mytab; – Peter M Mar 28 '18 at 17:06
  • 1
    You can audit the execution of a pl/sql blok only in the another session. There's really no way to do what you're asking. – 0xdb Mar 28 '18 at 23:27
  • `dbms_output` is to display anything you want inside the `PLSQL` block. Incase you don't want to use `dbms_output` in you current block, then create an `autonomous` transaction where you simply display the result in another `PLSQL` block. Again, in the autonomous transaction you need to use `dbms_output`. – XING Mar 29 '18 at 05:02
  • @GregBurghardt Would an autonomous PL/SQL logging procedure be appropriate for you if live monitoring is your goal? Or alternatively since you seem to be using PL/SQL for its variables and not its other language constructs, instead use bind variables in SQL*Plus. – BLaZuRE Mar 30 '18 at 22:49
  • Thanks to everyone for the comments and answers. It's going to take me a few days to try things out. – Greg Burghardt Mar 31 '18 at 21:50

4 Answers4

4

Compound triggers can count and display the number of rows modified without changing the original code.

There are still a few issues and challenges here. This solution probably won't work with parallel DML - it either won't count properly or the triggers will prevent direct-path writes. It probably will work in a multi-user environment but that needs to be tested. You'll also need to build code for DELETEs, and maybe MERGEs. And this will probably slow down DML.

Sample Schema

create table users(id number, username varchar2(100));
insert into users values(1, 'system');

create table table1(col1 number, updated_at date, updated_by number);
insert into table1 values(1, null, null);
insert into table1 values(2, null, null);

create table table2(col1 number, updated_at date, updated_by number);

Package to prevent excessive DBMS_OUTPUT

Constantly printing output can cause problems. So we want to disable output by default. And you probably don't simply want to use DBMS_OUTPUT.DISABLE, that might turn off something else and it's hard to always remember to run that.

Create a simple package with a global variable.

create or replace package print_feedback is
    --Outputing large amounts of data can sometimes break things.
    --Only enable DBMS_OUTPUT when explicitly requested.
    g_print_output boolean := false;
end;
/

Set it to TRUE before running import.

--Run this block first to enable printing.
begin
    print_feedback.g_print_output := true;
end;
/

PL/SQL Block to create INSERT and UPDATE triggers

This code dynamically generates triggers to capture INSERTs and UPDATEs.

Dynamic PL/SQL is a bit tricky. Notice I used templates and the alternative quoting mechanism to avoid concatenation hell. Once you understand those tricks the code becomes relatively readable. (And hopefully your IDE understands how the q'[ works better than the StackOverflow syntax highlighter.)

--Create automatic UPDATE and INSERT feedback triggers.
declare
    c_sql_template constant varchar2(32767) :=
    q'[
create or replace trigger #TABLE_NAME#_#UPD_or_INS#_trg for #UPDATE_OR_INSERT# on #TABLE_NAME# compound trigger

--Purpose: Print a feedback message after updates and inserts.
g_count number := 0;

after each row is
begin
    g_count := g_count + 1;
end after each row;

after statement is
begin
    if print_feedback.g_print_output then
        if g_count = 1 then
            dbms_output.put_line('#Inserted_or_Updated# '||g_count||' row in #TABLE_NAME#');
        else
            dbms_output.put_line('#Inserted_or_Updated# '||g_count||' rows in #TABLE_NAME#');
        end if;
    end if;
end after statement;

end;
    ]';
    v_sql varchar2(32767);
begin
    --Loop through the relevant tables
    for tables in
    (
        select table_name
        from user_tables
        where table_name in ('TABLE1', 'TABLE2')
        order by table_name
    ) loop
        --Create and execute update trigger.
        v_sql := replace(replace(replace(replace(c_sql_template
            , '#TABLE_NAME#', tables.table_name)
            , '#UPD_or_INS#', 'upd')
            , '#UPDATE_OR_INSERT#', 'update')
            , '#Inserted_or_Updated#', 'Updated');
        execute immediate v_sql;
        --Create and execute insert trigger.
        v_sql := replace(replace(replace(replace(c_sql_template
            , '#TABLE_NAME#', tables.table_name)
            , '#UPD_or_INS#', 'ins')
            , '#UPDATE_OR_INSERT#', 'insert')
            , '#Inserted_or_Updated#', 'Inserted');
        execute immediate v_sql;
    end loop;
end;
/

Sample run

Now your unchanged script will display some output. (I did make a few trivial changes to the script but only to make it runnable.)

SQL>    --Run this block first to enable printing.
SQL>    set serveroutput on;
SQL>    begin
  2             print_feedback.g_print_output := true;
  3     end;
  4     /

PL/SQL procedure successfully completed.

SQL> DECLARE
  2      v_user_id users.id%TYPE;
  3  BEGIN
  4      SELECT id INTO v_user_id FROM users WHERE username = 'system';
  5
  6      UPDATE table1
  7      SET col1 = 1,--value,
  8          updated_at = SYSDATE,
  9          updated_by = v_user_id
 10      WHERE 1=1;--some condition;
 11
 12      INSERT INTO table2 values(2/*val1*/, SYSDATE, v_user_id);
 13  END;
 14  /
Updated 2 rows in TABLE1
Inserted 1 row in TABLE2

PL/SQL procedure successfully completed.

SQL>
Jon Heller
  • 34,999
  • 6
  • 74
  • 132
  • I just tried this out. Had a few modifications to make to get sqlplus to dump the dbms_output to the screen (`set serveroutput on`), but that was about it. Interesting. – Greg Burghardt Apr 05 '18 at 12:26
  • While the triggers might slow down the DML operations, we could create them at the beginning of the SQL scripts, and then remove them afterwards. – Greg Burghardt Apr 05 '18 at 12:47
3

I'm not sure if there is some oracle parameter or config to change so that your PL/SQL will work the way you want, but you can create a procedure which accepts DML statement and run that DML statement. See sample below,

DECLARE
    v_var VARCHAR2(10);
    PROCEDURE run_dml (p_dml VARCHAR2)
    IS
    BEGIN
        EXECUTE IMMEDIATE p_dml;
        DBMS_OUTPUT.PUT_LINE(p_dml);
        DBMS_OUTPUT.PUT_LINE(sql%rowcount||' rows '||REPLACE(LOWER(TRIM(SUBSTR(p_dml, 1, 6)))||'ed.', 'eed', 'ed'));
    END;
BEGIN
   v_var := 'hello too';
   run_dml(q'[INSERT INTO test1_log VALUES ('hello')]');
   run_dml(q'[DELETE FROM test1_log WHERE log1 = 'hello']');
   run_dml(q'[UPDATE test1_log SET log1 = 'hello1']');
   run_dml('INSERT INTO test1_log VALUES('''||v_var||''')');
END;
/

INSERT INTO test1_log VALUES ('hello')
1 rows inserted.
DELETE FROM test1_log WHERE log1 = 'hello'
1 rows deleted.
UPDATE test1_log SET log1 = 'hello1'
1 rows updated.
INSERT INTO test1_log VALUES('hello too')
1 rows inserted.
eifla001
  • 1,137
  • 8
  • 8
  • 2
    If you propose dynamic SQL you should use escaping to avoid duplicating `'` so it could be: `run_dml(q[' INSERT INTO test1 VALUES ('hello') ]')` – Lukasz Szozda Mar 31 '18 at 05:54
  • 1
    yeah, that's a nice suggestion, thanks. I modified my answer above. – eifla001 Mar 31 '18 at 14:17
  • For some reason single quotes in the INSERT or UPDATE statements are causing parse errors: `ORA-06550: line 8, column 6: PLS-00103: Encountered the symbol "[" when expecting one of the following: ...` – Greg Burghardt Apr 05 '18 at 12:35
  • Ah, never mind. I transposed the single quote at the beginning of the multi line string (`q['` instead of `q'[`). – Greg Burghardt Apr 05 '18 at 12:37
  • Shoot. Actually, this solution won't work in my case, because I need to use a variable in my SQL statements. – Greg Burghardt Apr 05 '18 at 12:41
  • You can actually pass a variable in to the SQL statements. See my updated answer above. – eifla001 Apr 06 '18 at 06:07
0

SQL*Plus gets its status information about the number of rows affected etc by examining the OCI return status. OCI is not really my area, but I am pretty sure that in the case of a PL/SQL block the only information it holds will be the fact that a block was submitted and whether it succeeded or failed, as the block was submitted to the server as a single unit and there is no structure in the call interface documenting every step within it together with status and affected rowcounts. There is simply no mechanism to capture that information. (It would also be unwise for Oracle to implement such an interface as the number of statements executed within a PL/SQL block can be arbitrarily large, for example if it performs a loop over a million row table.)

I suppose you may be able to enable auditing at a suitable level of granularity, and then query DBA_AUDIT_TRAIL after each call and filtering by timestamp, user and terminal to limit the report to the most recent call in the current session, but it sounds as though even that would be some way off what you are looking for.

William Robertson
  • 15,273
  • 4
  • 38
  • 44
-2
spool "D:\test\test.txt"

-- YOUR ANONYMOUS PL/SQL Block here

spool off  

this will give u your required out put and without using the DBMS_OUTPUT.PUT_LINE the ouput will be in the path specified

  • 1
    That simply captures the output in a local file. It won't report the number of rows updated in table1 or inserted in table2 in the OP's example. – William Robertson Apr 01 '18 at 10:58