Oracle PL equivalent of foreach-loop - oracle

I need to loop over about 10 strings, which are all known to me in advance -- I don't need to SELECT them from anywhere.
I attempted:
BEGIN
FOR tab IN ('one', 'two', 'three')
LOOP
EXECUTE IMMEDIATE 'DROP TABLE ' || tab;
END LOOP;
END;
But that does not work (LOOP is unexpected)...
Is there a way to do this?

declare
type t_strings is table of varchar2(100);
strings t_strings:=t_strings('one','two','three');
begin
for i in 1..strings.count loop
dbms_output.put_line(strings(i));
end loop;
end;
/
Results:
one
two
three
Or you can use your own short-cut functions:
http://orasql.org/2017/10/02/plsql-functions-iterate-and-keys-for-associative-arrays/
Btw, Oracle 21 has some new features for FOR-LOOP:
1.1.1 PL/SQL Extended Iterators
FOR LOOP Iteration Enhancements in Oracle Database 21c
So it can be rewritten as:
declare
type t_strings is table of varchar2(100);
begin
for str in values of t_strings('one','two','three') loop
dbms_output.put_line(str);
end loop;
end;
/

"almost real" foreach loop:
declare
type tabList is table of varchar2(32);
tabs tabList := tabList ('one', 'two', 'three');
curr varchar2(32) := tabs.first;
begin
<<foreach>> loop exit foreach when curr is null;
execute immediate 'create table '||tabs(curr)||' (col char(1))';
execute immediate 'drop table '||tabs(curr)||' purge';
curr := tabs.next (curr);
end loop;
end;
/
PL/SQL procedure successfully completed.

begin
for tab_rec in ( select 'abc' val from dual
union
select '123' val from dual )
loop
EXECUTE IMMEDIATE 'DROP TABLE ' || tab_rec.val;
end loop;
end;

Related

How can I make an entire PL/SQL code block dynamic with bind variables?

Background
I'm trying to make a re-usable PL/SQL procedure to move data from one
database to another.
For this purpose, I'm using dynamic SQL.
The procedure executes perfectly if I use a REPLACE with placeholders.
However, for security reasons, I want to use bind variables.
Question
How can I make an entire PL/SQL code block dynamic (with bind
variables)? If I use a REPLACE instead of the bind variables, it works
fine.
How to replicate
To replicate this in your database, create the following procedure as it is:
create or replace procedure move_data(i_schema_name in varchar2, i_table_name in varchar2, i_destination in varchar2) as
l_sql varchar2(32767);
l_cursor_limit pls_integer := 500;
l_values_list varchar2(32767);
begin
select listagg('l_to_be_moved(i).' || column_name, ', ') within group (order by column_id)
into l_values_list
from all_tab_cols
where owner = i_schema_name and
table_name = i_table_name and
virtual_column = 'NO';
l_sql := q'[
declare
l_cur_limit pls_integer := :l_cursor_limit;
cursor c_get_to_be_moved is
select :i_table_name.*, :i_table_name.rowid
from :i_table_name;
type tab_to_be_moved is table of c_get_to_be_moved%rowtype;
l_to_be_moved tab_to_be_moved;
begin
open c_get_to_be_moved;
loop
fetch c_get_to_be_moved
bulk collect into l_to_be_moved limit l_cur_limit;
exit when l_to_be_moved.count = 0;
for i in 1.. l_to_be_moved.count loop
begin
insert into :i_table_name#:i_destination values (:l_values_list);
exception
when others then
dbms_output.put_line(sqlerrm);
l_to_be_moved.delete(i);
end;
end loop;
forall i in 1.. l_to_be_moved.count
delete
from :i_table_name
where rowid = l_to_be_moved(i).rowid;
for i in 1..l_to_be_moved.count loop
if (sql%bulk_rowcount(i) = 0) then
raise_application_error(-20001, 'Could not find ROWID to delete. Rolling back...');
end if;
end loop;
commit;
end loop;
close c_get_to_be_moved;
exception
when others then
rollback;
dbms_output.put_line(sqlerrm);
end;]';
execute immediate l_sql using l_cursor_limit, i_table_name, i_destination, l_values_list;
exception
when others then
rollback;
dbms_output.put_line(sqlerrm);
end;
/
And then you can execute the procedure with the following:
begin
move_data('MySchemaName', 'MyTableName', 'MyDatabaseLinkName');
end;
/
Due to many reasons(inability to generate an appropriate execution plan, security checking, etc.) Oracle does not allow identifiers binding (table names, schema names, column names and so on). So if it's really necessary, the only way is to hard code those identifiers after some sort of validation (to prevent SQL injection).
If I understand well, you could try a trick, by using a dynamic SQL inside a dynamic SQL.
setup:
create table tab100 as select level l from dual connect by level <= 100;
create table tab200 as select level l from dual connect by level <= 200;
create table tabDest as select * from tab100 where 1 = 2;
This will not work:
create or replace procedure testBind (pTableName in varchar2) is
vSQL varchar2(32000);
begin
vSQL := 'insert into tabDest select * from :tableName';
execute immediate vSQL using pTableName;
end;
But this will do the trick:
create or replace procedure testBind2 (pTableName in varchar2) is
vSQL varchar2(32000);
begin
vSQL := q'[declare
vTab varchar2(30) := :tableName;
vSQL2 varchar2(32000) := 'insert into tabDest select * from ' || vTab;
begin
execute immediate vSQL2;
end;
]';
execute immediate vSQL using pTableName;
end;
I think you can do it simpler.
create or replace procedure move_data(i_schema_name in varchar2, i_table_name in varchar2, i_destination in varchar2) as
l_sql varchar2(32767);
begin
select listagg('l_to_be_moved(i).' || column_name, ', ') within group (order by column_id)
into l_values_list
from all_tab_cols
where owner = i_schema_name and
table_name = i_table_name and
virtual_column = 'NO';
l_sql := 'insert into '||i_destination||'.'||i_table_name||' select * from '||i_schema_name||'.'||i_table_name;
execute immediate l_sql;
end;
If you are concerned about SQL-Injection, have a look at package DBMS_ASSERT. This PL/SQL package provides function to validate properties of input values.

How to store a column of result of select query in an array?

If we have a column in a table of type number, how can we store the result of select query on that column in an array ?
This sample uses a list (table of numbers) to achieve this, because i find
those lists much more handy:
CREATE OR REPLACE TYPE numberlist AS TABLE OF NUMBER;
DECLARE
v_numberlist numberlist;
BEGIN
SELECT intval numbercolumn
BULK COLLECT INTO v_numberlist
FROM lookup;
FOR i IN 1..v_numberlist.count
LOOP
dbms_output.put_line( v_numberlist(i) );
END LOOP;
END;
Create a type which store number:-
CREATE OR REPLACE TYPE varray is table of number;
--write your select query inside for loop () where i am extracting through level
declare
p varray := varray();
BEGIN
for i in (select level from dual connect by level <= 10) loop
p.extend;
p(p.count) := i.level;
end loop;
for xarr in (select column_value from table(cast(p as varray))) loop
dbms_output.put_line(xarr.column_value);
end loop;
END;
output:-
1
2
3
4
5
6
7
8
9
10
Just an option to use some native SQL datatype. Hope it helps.
SET SERVEROUTPUT ON;
DECLARE
lv_num_tab DBMS_SQL.NUMBER_TABLE;
BEGIN
SELECT LEVEL BULK COLLECT INTO lv_num_tab FROM DUAL CONNECT BY LEVEL < 10;
FOR I IN lv_num_tab.FIRST..lv_num_tab.LAST
LOOP
dbms_output.put_line(lv_num_tab(i));
END LOOP;
END;
You may also want to put the whole select in a table. You can use a BULK COLLECT to an array:
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
CREATE OR REPLACE
PROCEDURE get_tables(p_owner in varchar2)
as
v_res t_my_list;
v_qry varchar2(4000) := '';
begin
v_qry := ' SELECT table_name from all_tables where owner='''||p_owner||'''';
dbms_output.put_line(v_qry);
-- all at once in the table
execute immediate v_qry bulk collect into v_res;
FOR I in 1..v_res.count
loop
dbms_output.put_line(v_res(i));
end loop;
exception
when others then
raise;
end get_tables;
/
begin
get_tables('E') ;
end;
/

PLSQL dynamic query

I have a table A which has column A which holds table names as values.
All these tables have a common column C. I need maximum value of this column for each table.
I tried this using dynamic SQL but I'm getting errors. Please suggest.
DECLARE
query1 VARCHAR2(100);
c_table VARCHAR2(40);
c_obj VARCHAR2(20);
Cursor cursor_a IS
SELECT a FROM A;
BEGIN
Open cursor_a;
LOOP
Fetch cursor_a INTO c_table2;
EXIT WHEN cursor_a%notfound;
query1 := 'SELECT max(object_ref) AS "c_obj" FROM c_table' ;
EXECUTE IMMEDIATE query1;
dbms_output.put_line('Maximum value: '|| c_table || c_obj);
END LOOP;
Close cursor_a;
END;
Dynamic SQL can't see your PL/SQL variable: you need to pass it a string which can be executed in the scope of the SQL engine. So you need to concatenate the table name with the statement's boilerplate text:
query1 := 'SELECT max(c) FROM ' || variable_name;
You also need to return the result of the query into a variable.
Here is how it works (I've stripped out some of the unnecessary code from your example):
DECLARE
c_table VARCHAR2(40);
c_obj VARCHAR2(20);
BEGIN
for lrec in ( select a as tab_name from A )
LOOP
EXECUTE IMMEDIATE 'SELECT max(object_ref) FROM ' || lrec.tab_name
into c_obj ;
dbms_output.put_line('Maximum value: '|| lrec.tab_name
|| '='|| c_obj);
END LOOP;
END;
There is some miss match in veriables that you had used i.e.
declared as "c_table" but accessing as "c_table2"
Each table common column name is "C" but accessing as "object_ref"
In dynamic query use INTO keyword to store the value to your varibale
Suggestions
Use concat() function to prepare the query dynamically i.e. something like:
SET #SQL := CONCAT('SELECT max(c) INTO ', c_obj, ' FROM ',c_table);
Steps of implementing dynamic query is:
SET #SQL = <your dynamic query>
PREPARE stmt FROM #SQL;
EXECUTE stmt;
Sample code:
DECLARE
query1 VARCHAR2(100);
c_table VARCHAR2(40);
c_obj VARCHAR2(20);
CURSOR cursor_a IS
SELECT a FROM A;
BEGIN
OPEN cursor_a;
LOOP
FETCH cursor_a INTO c_table;
EXIT WHEN cursor_a%notfound;
SET #SQL := CONCAT('SELECT max(object_ref) AS c_obj INTO ', c_obj, ' FROM ',c_table);
PREPARE stmt FROM #SQL;
EXECUTE stmt;
dbms_output.put_line('Maximum value: '|| c_table || c_obj);
END LOOP;
CLOSE cursor_a;
END;

How to output result of SELECT statement which is executed using native dynamic SQL?

I have a string which contains SQL SELECT statement.
I wonder how can I output result of the execution of that statement on the screen, execution will be done using native dynamic SQL (EXECUTE IMMEDIATE).
example:
DECLARE
v_stmt VARCHAR2 := 'SELECT * FROM employees';
BEGIN
EXECUTE IMMEDIATE v_stmt; -- ??? how to output result of that select on the screen.
END;
Important remark: structure of table can be any. I have to write a procedure which accepts name of the table as parameter, so I can't hardcode a table structure and don't want to do it.
Thanks for responses. Any ideas very appreciated/
If you are on Oracle 12c with a 12c client, this should work:
declare
rc sys_refcursor;
begin
open rc for 'select * from dual';
dbms_sql.return_result(rc);
end;
Yes we can execute select statement dynamically.
Let say we have a table test. It has four column Row_id,Name,Rank etc
When we do select * from test;
Result will be
Row_id Name Rank
1 R1 5
2 R2 1
3 R3 2
4 R4 4
Now we can use DBMS_SQL package to execute dynamically SELECT Sql Statament.
Code is below:
DECLARE
v_CursorID NUMBER;
v_table VARCHAR2(50):='test';
v_SelectRecords VARCHAR2(500);
v_NUMRows INTEGER;
v_MyNum INTEGER;
v_Myname VARCHAR2(50);
v_Rank INTEGER;
BEGIN
v_CursorID := DBMS_SQL.OPEN_CURSOR;
v_SelectRecords := 'SELECT * from ' || v_table ;
DBMS_SQL.PARSE(v_CursorID,v_SelectRecords,DBMS_SQL.V7);
DBMS_SQL.DEFINE_COLUMN(v_CursorID,1,v_MyNum);
DBMS_SQL.DEFINE_COLUMN(v_CursorID,2,v_Myname,50);
DBMS_SQL.DEFINE_COLUMN(v_CursorID,3,v_Rank);
v_NumRows := DBMS_SQL.EXECUTE(v_CursorID);
LOOP
IF DBMS_SQL.FETCH_ROWS(v_CursorID) = 0 THEN
EXIT;
END IF;
DBMS_SQL.COLUMN_VALUE(v_CursorId,1,v_MyNum);
DBMS_SQL.COLUMN_VALUE(v_CursorId,2,v_Myname);
DBMS_SQL.COLUMN_VALUE(v_CursorId,3,v_Rank);
DBMS_OUTPUT.PUT_LINE(v_MyNum || ' ' || v_Myname || ' ' || v_Rank );
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE;
DBMS_SQL.CLOSE_CURSOR(v_CursorID);
end;

how to make a loop for all columns of a table?

I have a table EMPLOYEES, i need to make a loop, on every step of the loop I use dbms_output.put_line(column_name)
How can I make this?
Just found this solution
begin
for rec in (SELECT column_name
FROM all_tab_columns
WHERE owner = 'HR'
AND table_name = 'EMPLOYEES')
loop
dbms_output.put_line(rec.column_name);
end loop;
end;
Try to use cursor as below:
set serveroutput on;
declare
var_col1 varchar2(30) ;
num_col2 number ;
CURSOR cur
is
select var_col ,num_col
from table
where <conditions>;
begin
open cur;
fetch cur into var_col1,num_col2;
LOOP
fetch cur into var_col1,num_col2;
EXIT WHEN cur%NOTFOUND;
dbms_output.put_line( var_col1||' '||num_col2);
END LOOP;
close cur;
end;
here is another solution:
set serveroutput on;
begin
FOR cur in ( select var_col ,num_col from table where <conditions>)
LOOP
dbms_output.put_line(cur.var_col||' = '||cur.num_col);
END LOOP;
end;
Of course You can rewrite this SELECT example with WHERE after table_name. I dont compiled this code but I thinks its OK. Change ID_Employee and Name for your atributes
CREATE OR REPLACE PROCEDURE AAA_TEST
IS
BEGIN
DECLARE
v_id NUMBER :=0;
v_name VARCHAR2(25);
CURSOR cursorMails IS
SELECT Id_Employee,Name FROM EMPLOYEES;
BEGIN
OPEN cursorMails;
LOOP
FETCH cursorMails INTO v_id, v_name;
DBMS_OUTPUT.PUT_LINE('Number of Employee: ' || v_id || ' Name Employee: ' || v_name);
EXIT WHEN cursorMails%NOTFOUND;
END LOOP;
CLOSE cursorMails;
END;
END;
/

Resources