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

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.

Related

Oracle query logic to identify email addresses across the dwh schemas

I was performing an activity to identify eMail addresses based on certain pattern (#xyz.de). I initially tried checking the DBA_TAB_COLS [data dictionary] view but this just finds email column names and I manually need to check the big list of tables. Instead of doing that, is there is a more effective way to just fetch the the pattern value #xyz.de ?
Database - oracle 11g
Query used
SET SERVEROUTPUT ON 100000
DECLARE
lv_count number(10):=0;
l_str varchar2 (1000);
lv_col_name varchar2(255) :='EMAIL';
BEGIN
FOR V1 IN
(select distinct table_name
from dba_tab_columns
where column_name = lv_col_name
order by table_name)
LOOP
dbms_output.put_line(lv_col_name||' '||v1.table_name);
END LOOP;
END;
Please note that
I don't exactly know the table or column names.
The value #xyz.de can be in any schema and any table and any column. This has to be identified but in an effective way.
Any suggestions?
i have used the above block query to fetch the email column along with the table name , but how can i achieve by searching certain value #xyz.de using the dynamic sql ?
I don't know what you want to do with the values that you are trying to extract, so the below code just prints them. Refer to PL/SQL Dynamic SQL from the Oracle documentation.
declare
type EMAILS is ref cursor;
L_CURSOR EMAILS;
cursor EMAIL_COLS is
select OWNER
,TABLE_NAME
,COLUMN_NAME
from DBA_TAB_COLS
where COLUMN_NAME like '%EMAIL%'
and OWNER <> 'SYS';
L_SQL varchar2(200);
L_EMAIL varchar2(500);
begin
for REC in EMAIL_COLS
loop
L_SQL := 'select ' || REC.COLUMN_NAME || ' from ' || REC.OWNER || '.' || REC.TABLE_NAME || ' where ' || REC.COLUMN_NAME || ' like ''%xyz.de''';
open L_CURSOR for L_SQL;
loop
fetch L_CURSOR into L_EMAIL;
exit when L_CURSOR%notfound;
DBMS_OUTPUT.PUT_LINE(L_EMAIL);
end loop;
end loop;
end;

When does user_tab_columns get updated?

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.

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;

Cursor Operation in Netezza

CREATE OR REPLACE PROCEDURE SP_NEW_PROCEDURE1( )
RETURNS REFTABLE(employees)
LANGUAGE NZPLSQL AS
BEGIN_PROC
DECLARE
l_conditions varchar(1000);
p_rec RECORD;
BEGIN
FOR P_REC IN select empid, mgrid, empname, salary from employees where mgrid = 7
LOOP
l_conditions := 'insert into ' ||
REFTABLENAME ||
' VALUES (' ||
P_REC.EMPID ||
',' ||
P_REC.MGRID ||
',' ||
P_REC.EMPNAME ||
',' ||
P_REC.SALARY ||
' ) ; ' ;
execute immediate l_conditions;
l_conditions := ' ';
END LOOP;
RETURN REFTABLE;
END;
END_PROC;
When I run this:
select SP_NEW_PROCEDURE1()
I get the errors:
ERROR [01000] NOTICE: Error occurred while executing PL/pgSQL function SP_NEW_PROCEDURE1
ERROR [01000] NOTICE: line 24 at execute statement
ERROR [42S22] ERROR: Attribute 'DAN' not found
Can someone help whats wrong ...thanks
This has nothing do with the cursor itself, and everything to do with how you are building your dynamical SQL string.
When building dynamic SQL in a Netezza stored procedure, you can use the quote_ident and quote_literal helper functions to let the system know whether you are passing it a literal, or whether you are passing it an identifier. There is an example in the online documentation here. Essentially all they do is figure out the escaped quotation notation needed.
Since you are trying to put the values stored in the columns of your P_REC record into the VALUES part of an insert statement, you could use quote_literal like this:
CREATE OR REPLACE PROCEDURE SP_NEW_PROCEDURE1( )
RETURNS REFTABLE(employees)
LANGUAGE NZPLSQL AS
BEGIN_PROC
DECLARE
l_conditions varchar(1000);
p_rec RECORD;
BEGIN
FOR P_REC IN select empid, mgrid, empname, salary from employees where mgrid = 7
LOOP
l_conditions := 'insert into ' ||
REFTABLENAME ||
' VALUES (' ||
quote_literal(P_REC.EMPID) ||
',' ||
quote_literal(P_REC.MGRID) ||
',' ||
quote_literal(P_REC.EMPNAME) ||
',' ||
quote_literal(P_REC.SALARY ) ||
' ) ; ' ;
execute immediate l_conditions;
l_conditions := ' ';
END LOOP;
RETURN REFTABLE;
END;
END_PROC;
That being said, using a cursor to loop over records to insert a row one at a time is horribly inefficient in an MPP database like Netezza. Assuming this question is a follow-on from your question about an alternative to a recursive CTE to explore hierarchies, there's nothing wrong with looping in general, but try to avoid doing it record by record. Here is a version that will exploit the MPP nature of the system. For the record, if you are going to return your result set to a REFTABLE, then your only choice is Dynamic SQL.
CREATE OR REPLACE PROCEDURE SP_NEW_PROCEDURE1( )
RETURNS REFTABLE(employees)
LANGUAGE NZPLSQL AS
BEGIN_PROC
DECLARE
l_conditions varchar(1000);
p_rec RECORD;
BEGIN
-- FOR P_REC IN select empid, mgrid, empname, salary from employees where mgrid = 7
-- LOOP
l_conditions := 'insert into ' ||
REFTABLENAME ||
' select empid, mgrid, empname, salary from employees where mgrid = 7 ; ' ;
execute immediate l_conditions;
l_conditions := ' ';
-- END LOOP;
RETURN REFTABLE;
END;
END_PROC;
I suspect that you are building a query that is meant to insert a literal 'DAN' but which does not include the required quote marks, hence it is referencing DAN -- the optimiser is therefore trying to find an attribute of that name.
So the fix is to include the quotation marks when you build the SQL insert statement, or (preferably) to just use static SQL to insert the values instead of execute immediate.
When in doubt, always look at the data, as this would probably have been obvious to you if you inspected the value of l_conditions.

oracle pl/sql how to replicate more than one tables' data

I am having a scenario where my oracle db has some 20 tables, all have an index field called itemId.
I want to copy records with an id (called 101) from all 20 tables and insert them with a new id (called 102).
I don't want to have 20 cursors or 20 functions/procedures because the table list may grow in future. help me to achieve this in a better way.
As you have a potentially variable number of tables, you will need to use dynamic SQL to achieve your aim. Something like the following should hopefully help:
DECLARE
l_insert_column_list VARCHAR2(4000);
l_select_column_list VARCHAR2(4000);
BEGIN
FOR table_rec IN (SELECT table_name
FROM user_tab_columns
WHERE UPPER(column_name) = 'ITEMID')
LOOP
l_insert_column_list := '';
l_select_column_list := '';
FOR col_rec IN (SELECT column_name
FROM user_tab_columns
WHERE table_name = table_rec.table_name)
LOOP
l_insert_column_list := l_insert_column_list || col_rec.column_name || ',';
IF UPPER(col_rec.column_name) = 'ITEMID' THEN
l_select_column_list := l_select_column_list || '102,';
ELSE
l_select_column_list := l_select_column_list || col_rec.column_name || ',';
END IF;
END LOOP;
l_insert_column_list := RTRIM(l_insert_column_list, ',');
l_select_column_list := RTRIM(l_select_column_list, ',');
EXECUTE IMMEDIATE 'INSERT INTO ' || table_rec.table_name || ' ('
|| l_insert_column_list || ') SELECT ' || l_select_column_list ||
' FROM ' || table_rec.table_name || ' WHERE ITEMID = 101';
END LOOP;
END;
/
The idea here is to use the user_tab_columns data dictionary view to list all of the tables that have an ITEMID column. For each such table we then use user_tab_columns to list all of the columns in the table and then build up a SQL string to copy the data.
I've made a few assumptions here:
All of your tables are in the same schema. If not, you might need to use all_tab_columns instead of user_tab_columns, and include the schema owner in table_rec and in the SQL strings where table_rec.table_name is also used.
None of your table or column names have names that need to be enclosed in double quotes. (If they do, you'll have to adjust the SQL strings to include double quotes.)
4000 characters is enough for all of the column names in your tables. (As column names are limited to 30 characters, you'd need over 100 columns to trigger this problem.)
Before you let this script loose on your database, I would strongly recommend replacing EXECUTE IMMEDIATE with a call to dbms_output.put_line so that you can see the SQL strings that are being generated.

Resources