Reputation: 18803
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?
Upvotes: 4
Views: 1809
Reputation: 1157
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.
Upvotes: 3
Reputation: 16001
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.
Upvotes: 0
Reputation: 31
spool "D:\test\test.txt"
-- YOUR ANONYMOUS PL/SQL Block here
spool off
Upvotes: -2
Reputation: 36862
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.
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);
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;
/
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;
/
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>
Upvotes: 4