How to Log Alter Column DDL Operations - oracle

I need to create a database trigger which will record each alter (Add Column,Modify Column,Drop Column) statements in a specific table using Oracle's schema trigger. How to get it ?
I tried the code below so far :
TRIGGER after_ddl_creation
after CREATE ON SCHEMA
DECLARE
V CLOB;
BEGIN
FOR REC IN(SELECT TEXT FROM user_source WHERE NAME=SYS.DICTIONARY_OBJ_NAME)
LOOP
V:=V||REC.TEXT;
END LOOP ;
INSERT INTO myAudit VALUES
(SYS.DICTIONARY_OBJ_NAME,SYS.DICTIONARY_OBJ_TYPE,SYSDATE,USER,NULL,NULL,V);
END;

You can use such a database trigger :
create or replace trigger after_ddl_creation after ddl on schema
declare
v_oty varchar2(75) := ora_dict_obj_type;
v_don varchar2(75) := ora_dict_obj_name;
v_evt varchar2(75) := ora_sysevent;
v_olu varchar2(75) := nvl(ora_login_user,'Undefined Schema');
v_sql ora_name_list_t;
v_stm clob;
v_sct owa.vc_arr;
n pls_integer;
n_max pls_integer := 10000;
--> can log upto ten-thousand rows of "text" value, within "stmt" column,
--> which can be accessed by using (`[user|all|dba]_source`) views.
begin
v_sct(1) := 'SESSIONID';
v_sct(2) := 'IP_ADDRESS';
v_sct(3) := 'TERMINAL';
v_sct(4) := 'OS_USER';
v_sct(5) := 'AUTHENTICATION_TYPE';
v_sct(6) := 'CLIENT_INFO';
v_sct(7) := 'MODULE';
for i in 1..7
loop
v_sct(i) := sys_context('USERENV',v_sct(i));
end loop;
select decode(v_sct(1),0,null,v_sct(1)),
decode(upper(v_sct(3)),'UNKNOWN',null,v_sct(3))
into v_sct(1),v_sct(3) from dual;
n := ora_sql_txt( v_sql );
if n > n_max then
n := n_max;
end if;
for i in 1..n
loop
v_stm := v_stm || v_sql(i);
end loop;
if ( evt = 'ALTER' and oty = 'TABLE'
and regexp_like(v_stm,'Add|Modify|Drop','i') ) then
insert into myAudit(ts,usr,evnt,stmt,sessionid,ip,terminal,os_user,auth_type,
object_type,object_name,client_info,module_info)
values(sysdate,v_olu,v_evt,v_stm,v_sct(1),v_sct(2),v_sct(3),v_sct(4),v_sct(5),
v_oty,v_don,v_sct(6),v_sct(7));
end if;
end;
by recreating myAudit table due to the above INSERT statement.

Related

PLSQL looping through JSON object

Basically I have a json that looks like this [{"group":"groupa","status":"active"},{"group":"groupb","status":"inactive"}] and I want to loop through and extract the group only and save them in a variable in order to loop and compare the groups to a certain variable.
for example
group := 'groupc'
while counter < jsonGroup.count
loop
if jsonGroup(counter) := group then ....
is there any way to save the group into jsonGroup as an array or table?
thank you
From Oracle 12, you can use JSON PL/SQL object types to iterate over the JSON array and to extract the value of the group attribute of the objects:
DECLARE
value VARCHAR2(4000) := '[{"group":"groupa","status":"active"},{"group":"groupb","status":"inactive"}]';
ja JSON_ARRAY_T := JSON_ARRAY_T.PARSE(value);
je JSON_ELEMENT_T;
grp VARCHAR2(20);
i PLS_INTEGER := 0;
BEGIN
LOOP
je := ja.GET(i);
EXIT WHEN je IS NULL;
grp := TREAT(je AS JSON_OBJECT_T).get_string('group');
DBMS_OUTPUT.PUT_LINE(grp);
i := i + 1;
END LOOP;
END;
/
Which outputs:
groupa
groupb
The values can be stored into an array using the JSON_ARRAY_T functionality as MT0 described or using JSON_TABLE which might be better if your JSON is already stored in a table.
Below is an example of how to use both methods to store the groups into an array.
DECLARE
l_json_text VARCHAR2 (100)
:= '[{"group":"groupa","status":"active"},{"group":"groupb","status":"inactive"}]';
TYPE tab_t IS TABLE OF VARCHAR2 (100);
l_table tab_t := tab_t ();
l_array json_array_t;
PROCEDURE print_tab
IS
BEGIN
FOR i IN 1 .. l_table.COUNT
LOOP
DBMS_OUTPUT.put_line (l_table (i));
END LOOP;
END;
BEGIN
l_array := json_array_t (l_json_text);
l_table.EXTEND (l_array.get_size);
FOR i IN 1 .. l_array.get_size
LOOP
l_table (i) := TREAT (l_array.get (i - 1) AS json_object_t).get_string ('group');
END LOOP;
DBMS_OUTPUT.put_line ('After JSON_ARRAY_T method');
print_tab;
l_table.delete;
DBMS_OUTPUT.put_line ('After delete');
print_tab;
SELECT grp
BULK COLLECT INTO l_table
FROM JSON_TABLE (l_json_text, '$[*]' COLUMNS grp PATH '$.group');
DBMS_OUTPUT.put_line ('After JSON_TABLE method');
print_tab;
END;
/

how to convert DBMS_METADATA Oracle to Postgres?

How to convert DBMS_METADATA Procedures and Functions for Submitting XML Data in oracle to postgresql
For example :
SET serveroutput ON;
DECLARE
v_handle NUMBER;
v_transform_handle NUMBER;
v_ddls sys.ku$_ddls;
v_ddl sys.ku$_ddl;
BEGIN
v_handle := DBMS_METADATA.OPEN ('OBJECT_GRANT');
v_transform_handle := DBMS_METADATA.ADD_TRANSFORM(v_handle, 'DDL');
DBMS_METADATA.SET_TRANSFORM_PARAM(v_transform_handle,'PRETTY', TRUE);
DBMS_METADATA.SET_TRANSFORM_PARAM(v_transform_handle,'SQLTERMINATOR',TRUE);
DBMS_METADATA.SET_FILTER(v_handle, 'BASE_OBJECT_NAME', 'TABLE_NAME' );
DBMS_METADATA.SET_FILTER(v_handle, 'BASE_OBJECT_SCHEMA', 'OWNER_NAME' );
DBMS_METADATA.SET_COUNT (v_handle, 10); -- 10 object by call to fetch
v_ddls := DBMS_METADATA.FETCH_DDL(v_handle);
WHILE (v_ddls IS NOT NULL)
LOOP
FOR indx IN 1 .. v_ddls.COUNT
LOOP
v_ddl := v_ddls(indx);
DBMS_OUTPUT.put_line ('Output: ' || v_ddl.ddlText);
END LOOP;
v_ddls := DBMS_METADATA.FETCH_DDL(v_handle);
END LOOP;
DBMS_METADATA.CLOSE (v_handle);
END;
/

How can I use MOD function within WHILE LOOP with PLS_INTEGER data type?

I'm trying to print on screen odd values of an associative array using a simple "WHILE LOOP" with MOD condition. Is it possible? I know that PLS_INTEGER only accept not decimal values (like int datatype on Java). So... I tried with a NUMBER counter but I receive the same results. How Can I resolve it? . Thanks
SET SERVEROUTPUT ON
DECLARE
TYPE type_test IS TABLE OF VARCHAR2(45)
INDEX BY PLS_INTEGER;
t_test_5 type_test;
v_counter_1 PLS_INTEGER;
v_counter_2 NUMBER;
BEGIN
t_test_5(1) := 'Test1';
t_test_5(2) := 'Test2';
t_test_5(3) := 'Test3';
t_test_5(4) := 'Test4';
t_test_5(5) := 'Test5';
t_test_5(6) := 'Test6';
t_test_5(7) := 'Test7';
t_test_5(8) := 'Test8';
t_test_5(9) := 'Test9';
t_test_5(10) := 'Test10';
DBMS_OUTPUT.PUT_LINE('PLS_INTEGER COUNTER TEST');
v_counter_1 := t_test_5.FIRST;
WHILE MOD(v_counter_1, 2) <> 0
LOOP
DBMS_OUTPUT.PUT_LINE(t_test_5(v_counter_1));
v_counter_1 := t_test_5.NEXT(v_counter_1);
END LOOP;
DBMS_OUTPUT.PUT_LINE(' ');
DBMS_OUTPUT.PUT_LINE('NUMBER COUNTER TEST');
v_counter_2 := t_test_5.FIRST;
WHILE MOD(v_counter_2, 2) <> 0
LOOP
DBMS_OUTPUT.PUT_LINE(t_test_5(v_counter_2));
v_counter_2 := t_test_5.NEXT(v_counter_2);
END LOOP;
END;
I want to retrieve on screen the values 1, 3, 5, 7, 9 but in both situations I only retrieve value 1:
Procedimiento PL/SQL terminado correctamente.
PLS_INTEGER COUNTER TEST
Test1
NUMBER COUNTER TEST
Test1
The issue is not in the type of your variables, but in the fact that your loops end at the first row that does not match MOD(v_counter_1, 2) <> 0, thus not scanning all the rows.
What you need is not a loop ending when MOD(v_counter_1, 2) = 0, but a loop that scans all the rows, simply printing the values for the only rows that match your criteria:
DECLARE
TYPE type_test IS TABLE OF VARCHAR2(45)
INDEX BY PLS_INTEGER;
t_test_5 type_test;
v_counter_1 PLS_INTEGER;
v_counter_2 NUMBER;
BEGIN
t_test_5(1) := 'Test1';
t_test_5(2) := 'Test2';
t_test_5(3) := 'Test3';
t_test_5(4) := 'Test4';
t_test_5(5) := 'Test5';
t_test_5(6) := 'Test6';
t_test_5(7) := 'Test7';
t_test_5(8) := 'Test8';
t_test_5(9) := 'Test9';
t_test_5(10) := 'Test10';
DBMS_OUTPUT.PUT_LINE('PLS_INTEGER COUNTER TEST');
v_counter_1 := t_test_5.FIRST;
WHILE v_counter_1 is not null
LOOP
if mod(v_counter_1, 2) != 0 then
DBMS_OUTPUT.PUT_LINE(t_test_5(v_counter_1));
end if;
v_counter_1 := t_test_5.NEXT(v_counter_1);
END LOOP;
END;
the result:
PLS_INTEGER COUNTER TEST
Test1
Test3
Test5
Test7
Test9
Your while loop ends as soon as the counter's value is an even number - so as soon as it reaches 2 then loop will end. What you want is to loop through all the values but skip the even values:
WHILE ( v_counter_1 IS NOT NULL )
LOOP
IF MOD( v_counter_1, 2 ) = 0 THEN
v_counter_1 := t_test_5.NEXT(v_counter_1);
CONTINUE;
END IF;
DBMS_OUTPUT.PUT_LINE(t_test_5(v_counter_1));
v_counter_1 := t_test_5.NEXT(v_counter_1);
END LOOP;
If you are not going to have a sparse array then you do not need to use an associative array:
DECLARE
TYPE type_test IS TABLE OF VARCHAR2(45);
t type_test := type_test( 'Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6' );
BEGIN
FOR i = 1 .. t.COUNT / 2 LOOP
DBMS_OUTPUT.PUT_LINE(t(2*i-1));
END LOOP;
END;
/

PL/SQL file writing with generic input

I recently created a PL/SQL program that creates five different pipe delimited files from related data in a database.
I could not find a way to dynamically pull different tabular data in this case cursors, into a generic procedure that would create the files.
Instead I had to create five separate procedures, one for each file, that took in five different cursors, one for each file requirement record selection.
I can't help but think that there has to be a better way. I was looking into reference cursors but I don't think they are exactly what I am looking for.
How can I achieve this in PL/SQL?
I think what I am looking for is some generic type that can take any data from a cursor given any amount of records and record columns and have the ability to query itself to find what data is in it.
Pass the cursor into your procedure as a SYS_REFCURSOR. Then, use DBMS_SQL.TO_CURSOR_NUMBER(); to convert the ref cursor to a DBMS_SQL cursor.
Then, use DBMS_SQL.DESCRIBE_COLUMNS to figure out the columns in the cursor and DBMS_SQL.DEFINE_COLUMN, DBMS_SQL.FETCH_ROWS and DBMS_SQL.VALUE to get the data from the cursor into PL/SQL variables. Then, write your PL/SQL variables to your output file.
Here's some code that puts all that together for you.
DECLARE
l_rc SYS_REFCURSOR;
PROCEDURE dump_cursor (p_rc IN OUT SYS_REFCURSOR) IS
-- Dump the results of p_rc to log
l_cursor INTEGER;
l_column_count INTEGER;
l_column_descriptions SYS.DBMS_SQL.desc_tab;
l_status INTEGER;
l_column_value VARCHAR2 (4000);
l_column_width NUMBER;
l_rec_count NUMBER := 0;
l_line VARCHAR2 (4000);
FUNCTION get_length (l_column_def IN SYS.DBMS_SQL.desc_rec)
RETURN NUMBER IS
l_width NUMBER;
BEGIN
l_width := l_column_def.col_max_len;
l_width := CASE l_column_def.col_type WHEN 12 THEN /* DATE */
20 WHEN 2 THEN /* NUMBER */
10 ELSE l_width END;
-- Don't display more than 256 characters of any one column (this was my requirement -- your file writer probably doesn't need to do this
l_width := LEAST (256, GREATEST (l_width, l_column_def.col_name_len));
RETURN l_width;
END get_length;
BEGIN
-- This is the date format that I want to use for dates in my output
EXECUTE IMMEDIATE 'alter session set nls_date_format=''DD-MON-YYYY HH24:MI:SS''';
l_cursor := sys.DBMS_SQL.to_cursor_number (p_rc);
-- Describe columns
sys.DBMS_SQL.describe_columns (c => l_cursor, col_cnt => l_column_count, desc_t => l_column_descriptions);
l_line := '';
FOR i IN 1 .. l_column_count LOOP
l_column_width := get_length (l_column_descriptions (i));
l_line := l_line || RPAD (l_column_descriptions (i).col_name, l_column_width);
l_line := l_line || ' ';
DBMS_SQL.define_column (l_cursor,
i,
l_column_value,
4000);
END LOOP;
DBMS_OUTPUT.put_line (l_line);
l_line := '';
FOR i IN 1 .. l_column_count LOOP
l_column_width := get_length (l_column_descriptions (i));
l_line := l_line || RPAD ('-', l_column_width, '-');
l_line := l_line || ' ';
DBMS_SQL.define_column (l_cursor,
i,
l_column_value,
4000);
END LOOP;
DBMS_OUTPUT.put_line (l_line);
-- l_status := sys.DBMS_SQL.execute (l_cursor);
WHILE (sys.DBMS_SQL.fetch_rows (l_cursor) > 0) LOOP
l_rec_count := l_rec_count + 1;
l_line := '';
FOR i IN 1 .. l_column_count LOOP
DBMS_SQL.COLUMN_VALUE (l_cursor, i, l_column_value);
l_column_value := TRANSLATE (l_column_value, CHR (10), CHR (200));
l_column_width := get_length (l_column_descriptions (i));
IF l_column_value IS NULL THEN
l_line := l_line || RPAD (' ', l_column_width);
ELSE
l_line := l_line || RPAD (l_column_value, l_column_width);
END IF;
l_line := l_line || ' ';
END LOOP;
DBMS_OUTPUT.put_line (l_line);
END LOOP;
IF l_rec_count = 0 THEN
DBMS_OUTPUT.put_line ('No data found.');
ELSE
DBMS_OUTPUT.put_line (l_rec_count || ' rows returned.');
END IF;
sys.DBMS_SQL.close_cursor (l_cursor);
-- It would be better to store the current NLS_DATE_FORMAT on entry and restore it here, instead of assuming that it was
-- set to DD-MON-YYYY.
EXECUTE IMMEDIATE 'alter session set nls_date_format=''DD-MON-YYYY''';
EXCEPTION
WHEN OTHERS THEN
EXECUTE IMMEDIATE 'alter session set nls_date_format=''DD-MON-YYYY''';
-- Add your own handling here.
END dump_cursor;
-- Tester code, make sure server output is on
BEGIN
OPEN l_rc FOR 'SELECT object_id, object_name, object_type FROM dba_objects WHERE rownum <= 15';
dump_cursor(l_rc);
END;

Oracle PL/SQL - How to create a simple array variable?

I'd like to create an in-memory array variable that can be used in my PL/SQL code. I can't find any collections in Oracle PL/SQL that uses pure memory, they all seem to be associated with tables. I'm looking to do something like this in my PL/SQL (C# syntax):
string[] arrayvalues = new string[3] {"Matt", "Joanne", "Robert"};
Edit:
Oracle: 9i
You can use VARRAY for a fixed-size array:
declare
type array_t is varray(3) of varchar2(10);
array array_t := array_t('Matt', 'Joanne', 'Robert');
begin
for i in 1..array.count loop
dbms_output.put_line(array(i));
end loop;
end;
Or TABLE for an unbounded array:
...
type array_t is table of varchar2(10);
...
The word "table" here has nothing to do with database tables, confusingly. Both methods create in-memory arrays.
With either of these you need to both initialise and extend the collection before adding elements:
declare
type array_t is varray(3) of varchar2(10);
array array_t := array_t(); -- Initialise it
begin
for i in 1..3 loop
array.extend(); -- Extend it
array(i) := 'x';
end loop;
end;
The first index is 1 not 0.
You could just declare a DBMS_SQL.VARCHAR2_TABLE to hold an in-memory variable length array indexed by a BINARY_INTEGER:
DECLARE
name_array dbms_sql.varchar2_table;
BEGIN
name_array(1) := 'Tim';
name_array(2) := 'Daisy';
name_array(3) := 'Mike';
name_array(4) := 'Marsha';
--
FOR i IN name_array.FIRST .. name_array.LAST
LOOP
-- Do something
END LOOP;
END;
You could use an associative array (used to be called PL/SQL tables) as they are an in-memory array.
DECLARE
TYPE employee_arraytype IS TABLE OF employee%ROWTYPE
INDEX BY PLS_INTEGER;
employee_array employee_arraytype;
BEGIN
SELECT *
BULK COLLECT INTO employee_array
FROM employee
WHERE department = 10;
--
FOR i IN employee_array.FIRST .. employee_array.LAST
LOOP
-- Do something
END LOOP;
END;
The associative array can hold any make up of record types.
Hope it helps,
Ollie.
You can also use an oracle defined collection
DECLARE
arrayvalues sys.odcivarchar2list;
BEGIN
arrayvalues := sys.odcivarchar2list('Matt','Joanne','Robert');
FOR x IN ( SELECT m.column_value m_value
FROM table(arrayvalues) m )
LOOP
dbms_output.put_line (x.m_value||' is a good pal');
END LOOP;
END;
I would use in-memory array. But with the .COUNT improvement suggested by uziberia:
DECLARE
TYPE t_people IS TABLE OF varchar2(10) INDEX BY PLS_INTEGER;
arrayvalues t_people;
BEGIN
SELECT *
BULK COLLECT INTO arrayvalues
FROM (select 'Matt' m_value from dual union all
select 'Joanne' from dual union all
select 'Robert' from dual
)
;
--
FOR i IN 1 .. arrayvalues.COUNT
LOOP
dbms_output.put_line(arrayvalues(i)||' is my friend');
END LOOP;
END;
Another solution would be to use a Hashmap like #Jchomel did here.
NB:
With Oracle 12c you can even query arrays directly now!
Another solution is to use an Oracle Collection as a Hashmap:
declare
-- create a type for your "Array" - it can be of any kind, record might be useful
type hash_map is table of varchar2(1000) index by varchar2(30);
my_hmap hash_map ;
-- i will be your iterator: it must be of the index's type
i varchar2(30);
begin
my_hmap('a') := 'apple';
my_hmap('b') := 'box';
my_hmap('c') := 'crow';
-- then how you use it:
dbms_output.put_line (my_hmap('c')) ;
-- or to loop on every element - it's a "collection"
i := my_hmap.FIRST;
while (i is not null) loop
dbms_output.put_line(my_hmap(i));
i := my_hmap.NEXT(i);
end loop;
end;
Sample programs as follows and provided on link also https://oracle-concepts-learning.blogspot.com/
plsql table or associated array.
DECLARE
TYPE salary IS TABLE OF NUMBER INDEX BY VARCHAR2(20);
salary_list salary;
name VARCHAR2(20);
BEGIN
-- adding elements to the table
salary_list('Rajnish') := 62000; salary_list('Minakshi') := 75000;
salary_list('Martin') := 100000; salary_list('James') := 78000;
-- printing the table name := salary_list.FIRST; WHILE name IS NOT null
LOOP
dbms_output.put_line ('Salary of ' || name || ' is ' ||
TO_CHAR(salary_list(name)));
name := salary_list.NEXT(name);
END LOOP;
END;
/
Using varray is about the quickest way to duplicate the C# code that I have found without using a table.
Declare your public array type to be use in script
type t_array is varray(10) of varchar2(60);
This is the function you need to call - simply finds the values in the string passed in using a comma delimiter
function ConvertToArray(p_list IN VARCHAR2)
RETURN t_array
AS
myEmailArray t_array := t_array(); --init empty array
l_string varchar2(1000) := p_list || ','; - (list coming into function adding final comma)
l_comma_idx integer;
l_index integer := 1;
l_arr_idx integer := 1;
l_email varchar2(60);
BEGIN
LOOP
l_comma_idx := INSTR(l_string, ',', l_index);
EXIT WHEN l_comma_idx = 0;
l_email:= SUBSTR(l_string, l_index, l_comma_idx - l_index);
dbms_output.put_line(l_arr_idx || ' - ' || l_email);
myEmailArray.extend;
myEmailArray(l_arr_idx) := l_email;
l_index := l_comma_idx + 1;
l_arr_idx := l_arr_idx + 1;
END LOOP;
for i in 1..myEmailArray.count loop
dbms_output.put_line(myEmailArray(i));
end loop;
dbms_output.put_line('return count ' || myEmailArray.count);
RETURN myEmailArray;
--exception
--when others then
--do something
end ConvertToArray;
Finally Declare a local variable, call the function and loop through what is returned
l_array t_array;
l_Array := ConvertToArray('email1#gmail.com,email2#gmail.com,email3#gmail.com');
for idx in 1 .. l_array.count
loop
l_EmailTo := Trim(replace(l_arrayXX(idx),'"',''));
if nvl(l_EmailTo,'#') = '#' then
dbms_output.put_line('Empty: l_EmailTo:' || to_char(idx) || l_EmailTo);
else
dbms_output.put_line
( 'Email ' || to_char(idx) ||
' of array contains: ' ||
l_EmailTo
);
end if;
end loop;

Resources