When does user_tab_columns get updated? - oracle

In another question I tried to create a hist table, which keeps a log from the given table. With the answers in that question, I tried to create something new.
Since it is not possible to create a system trigger on tables or views, I created a DDL trigger like this:
create or replace trigger ident_hist_trig after alter on schema
declare
v_table varchar2(30);
begin
select upper(ora_dict_obj_name) into v_table from dual;
if (v_table = 'Z_IDENT') then
prc_create_hist_tabel('z_ident_hist', 'z_ident');
elsif (v_table = 'D_IDENT') then
prc_create_hist_tabel('d_ident_hist', 'd_ident');
elsif (v_table = 'X_IDENT') then
prc_create_hist_tabel('x_ident_hist', 'x_ident');
else
null;
end if;
end;
/
The procedure prc_create_hist_tabel looks like this:
create or replace procedure prc_create_hist_tabel(p_naam_hist_tabel in varchar2, p_naam_tabel in varchar2) is
cursor c is
select 'alter table ' || p_naam_hist_tabel || ' add ' || column_name || ' ' || data_type || case when data_type = 'DATE' then null else '(' || data_length || ')' end lijn
from user_tab_columns
where TABLE_NAME = upper(p_naam_tabel)
and column_name not in (select column_name from user_tab_columns where table_name = upper(p_naam_hist_tabel));
v_dummy number(1);
cursor trig is
select column_name || ',' kolom, ':old.' || column_name || ',' old
from user_tab_columns
where table_name = upper(p_naam_tabel);
v_trigger_sql varchar2(32767);
begin
begin
select 1 into v_dummy
from user_tab_columns
where TABLE_NAME = upper(p_naam_hist_tabel)
group by 1;
exception when no_data_found then
execute immediate 'create table ' || p_naam_hist_tabel || ' (wijziger varchar2(60) default user, wijzigdatum date default sysdate, constraint pk_' || p_naam_hist_tabel || ' primary key (wijziger, wijzigdatum))';
end;
dbms_output.put_line('BBB');
for i in c
loop
begin
dbms_output.put_line(i.lijn);
execute immediate i.lijn;
exception when others then
dbms_output.put_line(i.lijn);
end;
end loop;
v_trigger_sql := 'create or replace trigger ' || p_naam_tabel || '_hist_trig after update on ' || p_naam_tabel || ' for each row begin insert into ' || p_naam_hist_tabel || ' (';
for v_lijn in trig
loop
v_trigger_sql := v_trigger_sql || v_lijn.kolom;
end loop;
v_trigger_sql := substr(v_trigger_sql, 1, length(v_trigger_sql) - 1);
v_trigger_sql := v_trigger_sql || ') values (';
for v_lijn in trig
loop
v_trigger_sql := v_trigger_sql || v_lijn.old;
end loop;
v_trigger_sql := substr(v_trigger_sql, 1, length(v_trigger_sql) - 1);
v_trigger_sql := v_trigger_sql || '); end;';
execute immediate v_trigger_sql;
end;
/
In short what that function does, is maintain the history table. If it doesn't exist, it will create one, and if it exists, it will add the new columns to it. The procedure also creates a new trigger which will write the old values into the history table after update.
But when I alter one of the tables x_ident, z_ident or d_ident, the cursor c will return nothing (I can check that with the print when I loop through it). Although when execute the select after I altered my table, then I do get results.
The results I get from altering the table d_ident are these:
BBB
d_ident: Table altered.
But I guess it should be the other way around, I think that the procedure prc_create_hist_tabel is executed before the alter table actually goes off, and I guess I should get something like this:
d_ident: Table altered.
BBB
Any help would be apreciated. I tried to create a trigger on insert on user_tab_columns, but that gave me ORA-25001: cannot create this trigger type on views.
I tried with a sleep command as well, but that didn't work either.

This won't work. Even if you were able to get the column that is being added to the table in your trigger, if you tried to actually do DDL in a trigger, you'd get an error that DDL isn't allowed in a trigger.
I'd expect that the right way to approach this would be to make the call to prc_create_hist_tabel as part of your promotion scripts. Reasonable systems don't add columns to tables willy-nilly. The DDL is part of a promotion that exists in source control and gets deployed after testing. If your promotion scripts failed to modify the history table, you'd find out during testing that you missed a step and the change would never go to production. Having changes happen automatically means that they're not in change control which makes it more difficult to do a build from change control.
If you are determined to do this automatically, your trigger would need to submit a job, realistically using dbms_job not the newer dbms_scheduler, that calls the procedure. That job would run after the transaction the DDL trigger is a part of committed. At that point, the column would be visible in dba_tab_columns. And your job is free to do DDL.

Related

how to provide multiple bind variable for execute immediate inside pl/sql block

I have a table which contains the metadata of the table. The task is to periodically delete a specific set of tables, provided the information for where condition and how many days the data is retained are present.If a user needs to delete a data on daily basis, he simply enter his table name in audit log. The procedure will do the rest. The example is shown below.
Table structure:
CREATE TABLE delete_tbl_list (
id NUMBER NOT NULL,
table_name VARCHAR2(100) NOT NULL,
column_name VARCHAR2(100) NOT NULL,
day_retented NUMBER NOT NULL,
where_clause VARCHAR2(2000)
);
the day_retended is the number which will tell on how many days the data can hold.
select * from delete_tbl_list
ID TABLE_NAME COLUMN_NAME DAY_RETENTED WHERE_CLAUSE
---------- -----------------------------------------------
1 audit_log log_TS 60
So if i need to delete a table taking log_ts(timestamp) as column with 60days period as retention. The table in query needs to do
delete * from audit_log where log_ts<systimestamp -60
Now i need to do it using bulk delete and more dynamic and hence i wrote the procedure below,
create procedure dynamic_mass_delete as
TYPE tbl_rec_rowid IS TABLE OF ROWID INDEX BY PLS_INTEGER;
lv_del_exec_rec tbl_rec_rowid;
v_limit PLS_INTEGER := 10000;
m_date date:=sysdate;
total_records_deleted number:=0;
l_where delete_tbl_list.where_clause%type;
l_sql varchar2(2000);
TYPE ref_cur_type IS REF CURSOR;
delete_content ref_cur_type;
BEGIN
for i in (select table_name,COLUMN_NAME,DAY_RETENTED,WHERE_CLAUSE from delete_tbl_list) loop
DBMS_OUTPUT.PUT_LINE('tablename..'||i.table_name);
l_where:='';
m_date:=m_date-i.day_retented;
if i.where_clause is not null then
l_where:=' and '||i.where_clause;
end if;
OPEN delete_content FOR 'SELECT rowid from ' || i.table_name ||' where '|| i.COLUMN_NAME || ' <= to_timestamp('''||m_date||''')'||l_where;
LOOP
total_records_deleted := 0;
FETCH delete_content BULK COLLECT INTO lv_del_exec_rec LIMIT v_limit;
FORALL j IN lv_del_exec_rec.first..lv_del_exec_rec.last
execute immediate 'DELETE FROM :1 where rowid=:2 'using i.table_name,lv_del_exec_rec(j);
total_records_deleted := total_records_deleted + SQL%rowcount;
DBMS_OUTPUT.PUT_LINE('Delete count..'||total_records_deleted);
EXIT WHEN delete_content%notfound;
END LOOP;
CLOSE delete_content;
end loop;
EXCEPTION
when others then
DBMS_OUTPUT.PUT_LINE('Error-->'||SQLERRM);
END;
/
Now i getting error in the delete query stating invalid table name, i was not able to write dbms_output inside for all statment. Is it possible to use multiple bind variable inside a pl/sql procedure.
The error which i get is ,
Error-->ORA-00942: table or view does not exist
PL/SQL procedure successfully completed.
The table very much exists, but it is throwing error, i was not able to print inside the forall block too.
Switch to
execute immediate 'DELETE FROM ' || i.table_name ||' where rowid = ' || lv_del_exec_rec(j);
You can simplify your code as follows:
BEGIN
FOR TBL_DETAILS IN (
SELECT
TABLE_NAME,
COLUMN_NAME,
DAY_RETENTED,
WHERE_CLAUSE
FROM
DELETE_TBL_LIST
) LOOP
DBMS_OUTPUT.PUT_LINE('tablename..' || TBL_DETAILS.TABLE_NAME);
EXECUTE IMMEDIATE 'DELETE FROM '
|| TBL_DETAILS.TABLE_NAME
|| ' WHERE '
|| TBL_DETAILS.COLUMN_NAME
|| ' < SYSTIMESTAMP - '
|| TBL_DETAILS.DAY_RETENTED
|| CASE
WHEN TBL_DETAILS.WHERE_CLAUSE IS NOT NULL THEN ' AND ' || TBL_DETAILS.WHERE_CLAUSE
END;
DBMS_OUTPUT.PUT_LINE('Delete count..' || SQL%ROWCOUNT);
END LOOP;
END;
/
Hope, This will help you in creating simpler code.
Cheers!!

plsql get table in 'before alter' trigger

I have a table ident, and I also have a table ident_hist, which just keeps a log from the table ident. The table ident gets altered a lot, so I want to add the new columns to ident_hist dynamically as well. I have created a procedure which does that:
create or replace procedure prc_create_hist_tabel(p_naam_hist_tabel in varchar2, p_naam_tabel in varchar2) is
cursor c is
select 'alter table ' || p_naam_hist_tabel || ' add ' || column_name || ' ' || data_type || case when data_type = 'DATE' then null else '(' || data_length || ')' end lijn
from user_tab_columns
where TABLE_NAME = upper(p_naam_tabel)
and column_name not in (select column_name from user_tab_columns where table_name = upper(p_naam_hist_tabel));
v_dummy number(1);
begin
begin
select 1 into v_dummy
from user_tab_columns
where TABLE_NAME = upper(p_naam_hist_tabel)
group by 1;
exception when no_data_found then
execute immediate 'create table ' || p_naam_hist_tabel || ' (wijziger varchar2(60) default user, wijzigdatum date default sysdate, constraint pk_' || p_naam_hist_tabel || ' primary key (wijziger, wijzigdatum))';
end;
for i in c
loop
execute immediate i.lijn;
end loop;
end;
My question is, how can I check in my DDL trigger if I'm altering the tabel ident?
I want to do something like this:
create or replace trigger ident_hist_trig before alter on ident
begin
prc_create_hist_tabel('ident_hist', 'ident');
end;
When I try to compile the trigger, I get this error message:
ORA-30506: system triggers cannot be based on tables or views
How can I make my DDL trigger check if I'm altering my table ident? I only want to fire the trigger it when I'm altering the table ident, not any other table.
30506, 00000, "system triggers cannot be based on tables or views"
Cause: An attempt was made to base a system trigger on a table or a
view.
Action: Make sure the type of the trigger is compatible with the base
object.
System triggers are not associated with individual objects.
You can create DDL trigger before before create or alter or drop on SCHEMA(User/Owner). Then you can filter the object names and the DDL types(DROP, ALTER).
Here Tom has explained about this in detail.
Writting DDL_EVENT Triggers

Find a Value in Table Using SQL Developer

I inherited a large database and nobody seems to know which table/column a particular data set is coming from. I've spent a lot of time going through table by table in Oracle's SQL Developer, but I can't find it. Is there a way in SQLDeveloper to search the entire table for a single value. Something like:
select table_name from all_tab_columns where column_value='desired value';
The db has around 1K+ tables each with lots of columns so manually combing through this isn't working.
You can use the following script to search for a value in all columns of your schema. The execution time for the script will depend on the number of tables in your schema and the number of rows in each of your table.
Replace 'abc' with the value which you intend to search. Also, right now the script will search all VARCHAR2 columns. You can also insert the table names and counts into a table instead of doing a DBMS_OUTPUT.PUT_LINE.
DECLARE
CURSOR cur_tables
IS
SELECT table_name,
column_name
FROM user_tab_columns
WHERE data_type = 'VARCHAR2';
v_sql VARCHAR2(4000);
v_value VARCHAR2(50);
v_count NUMBER;
BEGIN
v_value := 'abc';
FOR c_tables IN cur_tables LOOP
v_sql := 'SELECT count(1) FROM ' || c_tables.table_name || ' WHERE ' || c_tables.column_name || ' = :val' ;
EXECUTE IMMEDIATE v_sql INTO v_count USING v_value;
IF v_count > 0 THEN
DBMS_OUTPUT.PUT_LINE('Table Name ' || c_tables.table_name || ' Column Name ' || c_tables.column_name || ' Row Count ' || v_count);
END IF;
END LOOP;
END;

Oracle dynamic sql with Trigger using :new and :old variables

I am trying to simplify large trigger code into concise code using all_tab_columns since table contains 200 columns. Some reason when tried with dynamic sql, it does not let me update declared variables.
DECLARE
v_new_rec SOME_TABLE%ROWTYPE;
v_sql VARCHAR(4000);
BEGIN
SELECT 'v_new_act.' || lower(column_name) || ' := :new.' || lower(column_name)
INTO v_sql
FROM all_tab_columns
WHERE table_name = 'SOME_TABLE'
ORDER BY column_id;
EXECUTE IMMEDIATE v_sql USING v_new_rec;
EXEC my_stored_proc(v_new_rec);
END;
/
Any suggestions???
you cannot do that: the variables :new and :old are out of scope in dynamic SQL.
You cannot do it also as a rowtype/Oracle type - I tried many times with different things.
All you can do - generate a full trigger code dynamically. I do it for triggers which reload all the things to history table on the table data change.
And as far as your trigger is very big in size (if it exceeds 32767 bytes in length) you should use dbms_sql package instead of execute immediate
This approach does not and can not work. You can't dynamically refer to the :new or :old pseudo-record.
If you want to go down this path, you'd realistically want to write dynamic SQL that generated the trigger, not dynamic SQL within the trigger. Something along the lines of (obviously untested)
l_sql := 'CREATE OR REPLACE TRIGGER trg_' || l_table_name ||
' BEFORE INSERT ON ' || l_table_name ||
' FOR EACH ROW ' ||
'DECLARE ' ||
' l_rec ' || l_table_name || '%ROWTYPE' ||
'BEGIN ';
for cols in (select * from all_tab_cols where table_name = l_table_name)
loop
l_sql := l_sql || ' l_rec.' || cols.column_name ||
' = :new.' || cols.column_name || ';';
end loop;
...
Alternately, if you want to declare your table based on object types, :new and :old would be actual instances of the object type that could then be passed to your stored procedure.

can I update all the columns in one set

I am just asking if there is an alternative way for the below update.
update test_table set
col 1 = trim(col1),
col 2 = trim(col2),
col 3 = trim(col3),
col 4 = trim(col4),
col 5 = trim(col5),
In my real table there are about 20 columns, maybe more. Is there a fast way where I can TRIM all the columns in one shot ?
seeking to a similar query if exists:
update test_table set all columns = trim(columns)
Edit:
I can do like this :
UPDATE test_table
SET (col1,col2,col3,col4,col5) = (
SELECT col1,col2,col3,col4,col5
FROM test_table)
Is there other way ?
There is no SQL syntax to give you a quick and easy way of doing this that I know of.
One way you could do it is to write a PL/SQL block to select the column names of a given table from a system view e.g. user_tab_cols and write them to a variable to build up the SQL code to run dynamically. To be honest, unless you have a number of tables to do this on or loads of columns it would probably be easier to write the query manually.
EDIT - Here is the code incase you want it
declare
v_table varchar2(50) := 'test_table';
v_sql varchar2(2000) := null;
cursor c_cols(p_table varchar2) is
select c.column_name as col
from user_tab_cols c
where c.table_name = upper(p_table)
order by c.column_id;
begin
-- write first line of sql...
v_sql := 'update ' || v_table || ' set' || chr(10);
-- loop through col names to add each col into update statement...
for l_c_cols in c_cols(v_table) loop
v_sql := v_sql || l_c_cols.col || ' = trim(' || l_c_cols.col || '),' || chr(10);
end loop;
-- remove last comma...
v_sql := substr(v_sql,1,length(v_sql)-2);
-- run dynamic sql...
dbms_output.put_line(v_sql);
begin
execute immediate v_sql;
commit;
end;
end;
Let me know if you have any questions on this.

Resources