ORACLE PL/SQL for each passing tablename to procedure - oracle

I need to do a FOR EACH loop in a procedure, but I need to pass the table name dynamically.
This is the declaration
CREATE OR REPLACE PROCEDURE MIGRATE_PRIMITIVES_PROPS
(
FromTable IN VARCHAR2,
ToTable IN VARCHAR2
)
When I try and do this
FOR EachRow IN (SELECT * FROM FromTable) It says the table isn't valid
The table coming into the procedure is dynamic, columns are added and deleted all the time so I can't spell out the columns and use a cursor to populate them.

You have to use dynamic SQL to query a table whose name you don't know at compile time. You can do that with a dynamic cursor:
as
l_cursor sys_refcursor;
begin
open l_cursor for 'select * from ' || fromtable;
loop
fetch l_cursor into ...
... but then it breaks down because you can't define a record type to fetch into based on a weak ref cursor; and you don't know the column names or types you're actually interested in - you're using select * and have specific names to exclude, not include. You mentioned an inner loop that works and gets the column names, but there is no way to refer to a field in that cursor variable dynamically either.
So you have to work a bit harder and use the dbms_sql package instead of native dynamic SQL.
Here's a basic version:
create or replace procedure migrate_primitives_props
(
fromtable in varchar2,
totable in varchar2
)
as
l_cursor pls_integer;
l_desc_tab dbms_sql.desc_tab;
l_columns pls_integer;
l_value varchar2(4000);
l_status pls_integer;
begin
l_cursor := dbms_sql.open_cursor;
-- parse the query using the parameter table name
dbms_sql.parse(l_cursor, 'select * from ' || fromtable, dbms_sql.native);
dbms_sql.describe_columns(l_cursor, l_columns, l_desc_tab);
-- define all of the columns
for i in 1..l_columns loop
dbms_sql.define_column(l_cursor, i, l_value, 4000);
end loop;
-- execute the cursor query
l_status := dbms_sql.execute(l_cursor);
-- loop over the rows in the result set
while (dbms_sql.fetch_rows(l_cursor) > 0) loop
-- loop over the columns in each row
for i in 1..l_columns loop
-- skip the columns you aren't interested in
if l_desc_tab(i).col_name in ('COL_NAME', 'LIB_NAME', 'PARTNAME',
'PRIMITIVE', 'PART_ROW')
then
continue;
end if;
-- get the column value for this row
dbms_sql.column_value(l_cursor, i, l_value);
-- insert the key-value pair for this row
execute immediate 'insert into ' || totable
|| '(key, value) values (:key, :value)'
using l_desc_tab(i).col_name, l_value;
end loop;
end loop;
end;
/
I've assumed you know the column names in your ToTable but still used a dynamic insert statement since that table name is unknown. (Which seems strange, but...)
Creating and populating sample tables, and then calling the procedure with their names:
create table source_table (col_name varchar2(30), lib_name varchar2(30),
partname varchar2(30), primitive number, part_row number,
col1 varchar2(10), col2 number, col3 date);
create table target_table (key varchar2(30), value varchar2(30));
insert into source_table (col_name, lib_name, partname, primitive, part_row,
col1, col2, col3)
values ('A', 'B', 'C', 0, 1, 'Test', 42, sysdate);
exec migrate_primitives_props('source_table', 'target_table');
End up with the target table containing:
select * from target_table;
KEY VALUE
------------------------------ ------------------------------
COL1 Test
COL2 42
COL3 2015-05-22 15:29:31
It's basic because it isn't sanitising the inputs (look up the dbms_assert package), and isn't doing any special handling for different data types. In my example my source table had a date column; the target table gets a string representation of that date value based on the calling session's NLS_DATE_FORMAT setting, which isn't ideal. There's a simple but slightly hacky way to get a consistent date format, and a better but more complicated way; but you may not have date values so this might be good enough as it is.

Related

Loop trough a fixed list of values

I have a list of tables that needs to be renamed before each data migration.
I came up with the procedure below to check if the table exists and eventually rename it, appending its creation date:
DECLARE
v_cnt PLS_INTEGER;
v_date varchar2(50);
v_table varchar2(50);
v_table_short varchar2(50);
BEGIN
v_table:='MT_TABLE_1';
v_table_short:=SUBSTR(v_table,8);
SELECT COUNT(*)
INTO v_cnt
FROM dba_tables
WHERE owner = 'OWNER'
AND table_name = v_table;
IF(v_cnt > 0) THEN
select CREATED into v_date FROM DBA_OBJECTS WHERE OBJECT_TYPE = 'TABLE' AND OBJECT_NAME=v_table;
execute immediate 'rename "'||v_table||'" to "'||v_table_short||'_'||v_date||'"';
END IF;
v_table:='MY_TABLE_2';
v_table_short:=SUBSTR(v_table,8);
SELECT COUNT(*)
INTO v_cnt
FROM dba_tables
WHERE owner = 'OWNER'
AND table_name = v_table;
IF(v_cnt > 0) THEN
select CREATED into v_date FROM DBA_OBJECTS WHERE OBJECT_TYPE = 'TABLE' AND OBJECT_NAME=v_table;
execute immediate 'rename "'||v_table||'" to "'||v_table_short||'_'||v_date||'"';
END IF;
END;
/
Now I want to avoid defining the v_table variable manually and repeating the code for each of the objects that I need to check for: is there a way to insert them as a list in a table variable or so, and have the code loop for each of the values there?
Thank you.
Yes, I like #Srinika's idea. Something like that:
CREATE TABLE table_list (
sort_order NUMBER UNIQUE,
long_name VARCHAR2(128 BYTE) PRIMARY KEY,
short_name VARCHAR2(128 BYTE) UNIQUE
);
INSERT INTO table_list VALUES (1,'MY_TABLE_1','MYTAB1');
INSERT INTO table_list VALUES (2,'MY_TABLE_2','MYTAB2');
I've added a column for the short name instead of computing it on the fly, as two tables might have the same first 8 characters in the name. The short_name needs to be unique else two tables will end up with the same short name. The sort order is only to do the renaming in the same order every time.
CREATE OR REPLACE PROCEDURE rename_tables AS
v_date VARCHAR2(8);
v_short_name VARCHAR2(128);
v_stmt VARCHAR2(200);
BEGIN
v_date := TO_CHAR(SYSDATE, 'YYYYMMDD');
FOR r IN (SELECT * FROM table_list ORDER BY sort_order) LOOP
v_short_name := r.short_name || '_' || v_date;
v_stmt := 'rename '||r.long_name||' to '||v_short_name;
DBMS_OUTPUT.PUT_LINE(v_stmt);
EXECUTE IMMEDIATE v_stmt;
END LOOP;
END rename_tables;
/
Calling this procedure would produce the output:
rename MY_TABLE_1 to MYTAB1_20200330
rename MY_TABLE_2 to MYTAB2_20200330
You'll need to add checks if the long table exists, and whether a renamed table is already there. I'd log the statements to a log table, too.
Regarding ": I would use them only if you have mixed case table names in the database.

How it insert result of select statement into table of records (associative array)

I am working on HR scheme of oracle. I have table of records
type emp_record is RECORD(emp_first_name employees.first_name%type,
emp_last_name employees.last_name%type,
);
type emp_record_table is table of emp_record
index by pls_integer;
I want to insert into emp_record_table results of the next select statement
select first_name, last_name
from employees
where department_id=30;
can you explain me how to solve this problem? thank you.
Simplest approach is to use bulk collect:
declare
type emp_record is RECORD(emp_first_name employees.first_name%type,
emp_last_name employees.last_name%type
);
type emp_record_table is table of emp_record
index by pls_integer;
l_recs emp_record_table;
begin
select first_name, last_name
bulk collect into l_recs
from employees
where department_id=30;
for idx in l_recs.first()..l_recs.last() loop
dbms_output.put_line(l_recs(idx).emp_first_name ||' '|| l_recs(idx).emp_last_name);
end loop;
end;
/
Note that you don't really need an associative array to process records like this. You can ditch the index by pls_integer and things will still work just fine. The value of associative arrays is when we need to maintain an access path to specific rows. For instance, we might want to use the primary key of the employees table to index the array. This would create a sparse array, because the selected employee IDs are not guaranteed to form a contiguous sequence. Consequently the logic for wrangling the array is more verbose:
declare
type emp_record is RECORD(emp_first_name employees.first_name%type,
emp_last_name employees.last_name%type
);
type emp_record_table is table of emp_record
index by pls_integer;
l_recs emp_record_table;
idx pls_integer;
begin
for r in (select emp_id, first_name, last_name
from employees
where department_id=30 )
loop
l_recs(r.emp_id).emp_first_name := r.first_name;
l_recs(r.emp_id).emp_last_name := r.last_name;
end loop;
idx := l_recs.first();
while idx is not null loop
dbms_output.put_line(l_recs(idx).emp_first_name ||' '|| l_recs(idx).emp_last_name);
idx := l_recs.next(idx);
end loop;
end;
/
Here is a demo on db<>fiddle.
Oracle Setup:
CREATE TABLE employees (
id NUMBER(8,0) PRIMARY KEY,
first_name VARCHAR2(50),
last_name VARCHAR2(80)
);
CREATE PACKAGE test_pkg IS
TYPE emp_record IS RECORD(
emp_first_name employees.first_name%type,
emp_last_name employees.last_name%type
);
TYPE emp_record_table IS TABLE OF emp_record INDEX BY pls_integer;
END;
/
INSERT INTO employees( id, first_name, last_name )
SELECT -1, 'a', 'aaa' FROM DUAL UNION ALL
SELECT +3, 'b', 'bbb' FROM DUAL;
PL/SQL Block:
DECLARE
x PLS_INTEGER;
emps test_pkg.emp_record_table;
BEGIN
-- Populate the associative array
FOR row IN ( SELECT * FROM employees ) LOOP
emps(row.id).emp_first_name := row.first_name;
emps(row.id).emp_last_name := row.last_name;
END LOOP;
-- Read the associative array
x := emps.FIRST;
WHILE x IS NOT NULL LOOP
DBMS_OUTPUT.PUT_LINE( x || ': ' || emps(x).emp_first_name || ' ' || emps(x).emp_last_name );
x := emps.NEXT(x);
END LOOP;
END;
/
Output:
-1: a aaa
3: b bbb
db<>fiddle here
You don't need associative array for that operation. Nested table will be perfect for that. Just declare your table type as following:
type emp_record_table is table of emp_record;
Firstly you have to declare table of your created type. Now you have only declared type or record and type of table of these records. You don't have yet your table. You can daclare it following way in the DECLARE section:
l_emp_table emp_record_table;
Then initialize it:
l_emp_table := emp_record_table();
Then you create cursor that will get data from your SELECT query. If you don't know how to do it, please read about cursor declaration, and fetching.
Next step will be: for each cursor row insert its data into table. You write simple loop that will do following steps:
Extend your declared table l_emp_table.extend()
Save data into table l_emp_table(i).emp_first_name := FETCHED_ROW.first_name and so on...

How to Insert multiple columns data into table containing single column using cursor in pl/sql?

I'm trying to insert data from table 1 to another table using cursor and purpose is to understand objects in Oracle.
However, I'm getting error like "Invalid no. of arguments" while inserting data using cursor loop.
create type airport_t as object
(
Rank number,
Airport varchar2(80),
Location varchar2(60),
Country varchar2(50),
Code_iata varchar2(3),
Code_icao varchar2(4),
Total_Passenger number,
Rank_change number,
Percent_change number
);
create table AIRPORTS2017OO
(
AIRPORT airport_t // Look above code..
);
declare
cursor insert_cr is select * from AIRPORTS2017;
begin
open insert_cr;
for i in insert_cr
loop
insert into Airports2017oo values( airport_t(i.Rank || '
'||i.airport ||' '||
i.Location ||' '|| i.Country ||' '|| i.code_iata ||' '||
i.code_icao ||' '||
i.Total_Passenger ||' '|| i.Rank_change ||' '||
i.Percent_change));
end loop;
end;
/
Table 1 contains 50 rows and 9 columns. However, table 2 has only 1 column and I want all data from table 1 into table 2 in single column only.
Error image:[1]
Your column in the AIRPORTS2017OO table is of type AIRPORT_T. The AIRPORT_T type, meanwhile, has a number of different parameters of various types. However, when inserting the row you are concatenating all fields into a single string to pass as a single parameter. You need to either pass them as separate parameters, or redefine your object type as having a single VARCHAR2 attribute. If the former is correct, the following should work.
DECLARE
CURSOR insert_cr IS
SELECT * FROM airports2017;
BEGIN
FOR i IN insert_cr LOOP
INSERT INTO airports2017oo
VALUES
(airport_t(i.rank
,i.airport
,i.location
,i.country
,i.code_iata
,i.code_icao
,i.total_passenger
,i.rank_change
,i.percent_change));
END LOOP;
END;
/
Also, see Oracle Object-Relational Developer's Guide for details on the system-defined constructor.

Put Column Name in Variable and use it in output statement

Here is What i actually wanted to do, Fetch Data From a Table without knowing any columns but i.j.Column_Name gives an error, so i wanted to put it in a variable. After reading the comments i think it's not possible
DECLARE
CURSOR C1 IS SELECT * FROM Table_Name;
CURSOR C2 IS SELECT Table_Name,Column_Name FROM user_tab_columns
WHERE data_type='VARCHAR2';
v_table Varchar2(256);
v_Col varchar2(200);
BEGIN
FOR i in C1 LOOP
FOR j in (SELECT Column_Name FROM user_tab_columns WHERE
Table_Name='Table_Name') LOOP
dbms_output.put_line(i.j.Column_Name);
END LOOP;
END LOOP;
END;
/
No, There is no Column Named v_Col
You can't refer to a field in a record (which is what the cursor loop is giving you) dynamically. If you need to do flexibly then you can use dbms_sql (possibly adapting this approach), but in the scenario you've shown you could use dynamic SQl to only get the column you want in the cursor:
-- dummy data
create table table_name (id number, column_name varchar2(10), other_col date);
insert into table_name values (1, 'Test 1', sysdate);
insert into table_name values (2, 'Test 2', sysdate);
DECLARE
CURSOR C1 IS SELECT * FROM Table_Name;
v_Cur sys_refcursor;
v_Col varchar2(200);
v_Val varchar2(4000);
BEGIN
v_Col:= 'Column_Name';
OPEN v_Cur for 'SELECT ' || v_Col || ' FROM Table_Name';
LOOP
FETCH v_Cur INTO v_Val;
EXIT WHEN v_Cur%notfound;
dbms_output.put_line(v_val);
END LOOP;
END;
/
Test 1
Test 2
PL/SQL procedure successfully completed.
The downside of this is that whatever the data type of the target column is, you have to implicitly convert it to a string; but you would be doing that in the dbms_output call anyway. So if you change the column you want to print:
v_Col:= 'Other_Col';
then the output from my dummy data would be:
2018-08-23
2018-08-23
PL/SQL procedure successfully completed.
where the date value is being implicitly formatted as a string using my current NLS session settings.
You could get more advanced by checking the data type in user_tab_columns and changing the dynamic query and/or the fetch and handling, but it isn't clear what you really need to do.

Oracle - In CLAUSE question when using with multiple values, making it dynamic

I just spent an hour on google and here trying to get a straight answer for how to do this in Oracle. What I need is the ability to use select in clause that constructed automatically such as
select col1 from table1 where id.table IN ('1','2','3');
where the id values are passed to the stored procedure inside the array. The associative array has been defined as such:
TYPE varchar_array_type IS TABLE OF VARCHAR2 (40) INDEX BY BINARY_INTEGER;
Is there a simple, concrete way to do that? thanks
Unfortunately, if your collection type is defined in PL/SQL (rather than SQL), you cannot use it in SQL because the SQL engine doesn't know how to handle it.
If instead you defined the collection type in SQL, i.e.
CREATE TYPE varchar_tbl
IS TABLE OF varchar2(40);
Then you can do something like
SELECT col1
FROM table1 t1
WHERE t1.id IN (SELECT column_value
FROM TABLE( <<variable of type varchar2_tbl>> ) )
depending on the Oracle version-- the syntax for using collections in SQL has evolved over time-- older versions of Oracle had more complex syntax.
You can convert a PL/SQL associative array (your VARCHAR_ARRAY_TYPE) to a SQL nested table collection in PL/SQL, but that requires iterating through the associative array and filling the nested table, which is a bit of a pain. Assuming that the VARCHAR_TBL nested table collection has been created already
SQL> CREATE OR REPLACE TYPE varchar_tbl
IS TABLE OF varchar2(40);
you can convert from the associative array to the nested table and use the nested table in a SQL statement like this (using the SCOTT.EMP table)
declare
type varchar_array_type
is table of varchar2(40)
index by binary_integer;
l_associative_array varchar_array_type;
l_index binary_integer;
l_nested_table varchar_tbl := new varchar_tbl();
l_cnt pls_integer;
begin
l_associative_array( 1 ) := 'FORD';
l_associative_array( 10 ) := 'JONES';
l_associative_array( 100 ) := 'NOT A NAME';
l_associative_array( 75 ) := 'SCOTT';
l_index := l_associative_array.FIRST;
while( l_index IS NOT NULL )
loop
l_nested_table.EXTEND;
l_nested_table( l_nested_table.LAST ) :=
l_associative_array( l_index );
l_index := l_associative_array.NEXT( l_index );
end loop;
SELECT COUNT(*)
INTO l_cnt
FROM emp
WHERE ename IN (SELECT column_value
FROM TABLE( l_nested_table ) );
dbms_output.put_line( 'There are ' || l_cnt || ' employees with a matching name' );
end;
Because converting between collection types is a bit of a pain, however, you would generally be better off just using the nested table collection (and passing that to the stored procedure) unless there is a particular reason that the associative array is needed.

Resources