PL SQL : Create oracle record dynamically - oracle

I dont know if it is possible but I would like to do this in PL/SQL
Let's say I have a parameter in my procedure, a number : numberColumns.
Inside the procedure I would like to create a record :
TYPE arrayColumn IS RECORD (
column1 VARCHAR2(200),
column2 VARCHAR2(200)...
... as much à numberColumns value
....
);
ty

This can be achieved easily with with OBJECT type in Oracle. But this kind of architecture is not at all suggested. Hope this below solution helps.
CREATE OR REPLACE
PROCEDURE test_obj_form(
a NUMBER )
AS
lv_sql VARCHAR2(32676);
BEGIN
SELECT '('
||listagg(str,',') WITHIN GROUP (
ORDER BY lvl)
||')'
INTO lv_sql
FROM
(SELECT 1 dum,
'column'
||LEVEL
||' '
||'varchar2(200)' str,
level lvl
FROM dual
CONNECT BY LEVEL < A
ORDER BY LEVEL
)
GROUP BY dum;
EXECUTE IMMEDIATE 'CREATE OR REPLACE TYPE name_rec IS OBJECT '||lv_sql;
dbms_output.put_line(lv_sql);
END;
/

Rather than using a record, you could use a collection:
CREATE OR REPLACE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/
Then do:
CREATE OR REPLACE PROCEDURE your_procedure(
number_columns IN INTEGER,
values OUT stringlist
)
IS
BEGIN
values := stringlist();
IF number_columns < 1 THEN
RETURN;
END IF;
values.EXTEND( number_columns );
FOR i IN 1 .. number_columns LOOP
values(i) := DBMS_RANDOM.STRING( 'X', 100 ); -- Assign some value
END LOOP;
END;
/
Otherwise, if you really want a record, then you will have to result to dynamic SQL.

Related

PL/SQL - pass query in for loop stored in variable

i have a scenario where i need to create a dynamic query with some if/else statements, i have prepared the query but unable to use this inside loop below is the snippet of what i am trying to acheive.
Query :
select user_name into username from table1 where user ='user1';
for(query)
Loop
Dbms_output.put_line('user name ' || user_name);
END Loop;
is this possible to use the vaiable in the for loop ?
Not really. Notice in the 19c PL/SQL Reference it says:
cursor
Name of an explicit cursor (not a cursor variable) that is not open when the cursor FOR LOOP is entered.
You have to code this the long way, by explicitly fetching the cursor into a record until you hit cursorname%notfound, e.g.
create or replace procedure cheese_report
( cheese_cur in sys_refcursor )
as
type cheese_detail is record
( name varchar2(15)
, region varchar2(30) );
cheese cheese_detail;
begin
loop
fetch cheese_cur into cheese;
dbms_output.put_line(cheese.name || ', ' || cheese.region);
exit when cheese_cur%notfound;
end loop;
close cheese_cur;
end;
Test:
declare
cheese sys_refcursor;
begin
open cheese for
select 'Cheddar', 'UK' from dual union all
select 'Gruyere', 'France' from dual union all
select 'Ossau Iraty', 'Spain' from dual union all
select 'Yarg', 'UK' from dual;
cheese_report(cheese);
end;
/
Cheddar, UK
Gruyere, France
Ossau Iraty, Spain
Yarg, UK
In Oracle 21c you can simplify this somewhat, though you still have to know the structure of the result set:
create or replace procedure cheese_report
( cheese_cur in sys_refcursor )
as
type cheese_detail is record
( cheese varchar2(15)
, region varchar2(30) );
begin
for r cheese_detail in values of cheese_cur
loop
dbms_output.put_line(r.cheese || ', ' || r.region);
end loop;
end;
You could parse an unknown ref cursor using dbms_sql to find the column names and types, but it's not straightforward as you have to do every processing step yourself. For an example of something similar, see www.williamrobertson.net/documents/refcursor-to-csv.shtml.

looping through an array for the where condition pl/sql

Is it possible in pl/sql to loop through a number of id's that need to go in the WHERE clause of the pl/sql statement. The sql statement itself is pretty simple, but I need to iterate over a number of id's:
SELECT x_name
FROM table_x
WHERE x_id = {array of 90 id's};
How can I insert the 90 id's here so that sql iterates over them? I tried using the cursor For Loop, but I'm stuck. The code below is erroneous, but it might give an indication what Im trying to achieve here
DECLARE
TYPE x_id_array IS VARRAY(3) OF NUMBER;
CURSOR cur_x_id (x_ondz_id NUMBER) IS
SELECT x_name
FROM table_x
WHERE x_id = var_ondz_id;
loop_total integer;
x_id x_id_array;
name VARCHAR;
BEGIN
x_id_new := x_id_array(8779254, 8819930, 8819931); --3 for testing
loop_total := x_id_new.count;
FOR i in 1 .. loop_total LOOP
dbms_output.put_line('x_id: ' || x_id_new(i) || '= Name: ' || x_name );
END LOOP;
END;
/
The expected out put would be
x_id: 8779254= Name: Name_1
x_id: 8819930= Name: Name_2
x_id: 8819931= Name: Name_3
...
... etc for all 90 id's in the array
Any help is appreciated
We can use TABLE function on a collection to get a list of numbers / character.
SELECT *
FROM TABLE ( sys.odcinumberlist(8779254,8819930,8819931) );
8779254
8819930
8819931
Here I'm using Oracle's internal VARRAY with a limit of 32767. You may use your own NESTED TABLE type.
create OR REPLACE TYPE yourtype AS TABLE OF NUMBER;
and then select it.
SELECT *
FROM TABLE ( yourtype(8779254,8819930,8819931) );
So, your query can simply be written as
SELECT x_name
FROM table_x
WHERE x_id IN ( SELECT * FROM
TABLE ( yourtype(8779254,8819930,8819931) ) );
12.2 and above, you won't even need to specify TABLE.
SELECT * FROM yourtype(8779254,8819930,8819931) works.

How to access and query objects passed as parameter to a procedure while converting from Oracle to postgresql

I have a procedure in Oracle that I need to convert to Postgresql and need help on it. It paases a collection of objects in a procedure.The procedure then checks if each object is present in a database table or not and if present it gives a message that , that specific element is found/present. if some element that is paassed to the procedure is not present in the table, the procedure just doesnt do anything. I have to write equivalent of that in postgresql. I think the heart of the issue is this statement:
SELECT COUNT (*)
INTO v_cnt
FROM **TABLE (p_cust_tab_type_i)** pt
WHERE pt.ssn = cc.ssn;
In Oracle a collection can be treated as a table and one can query it but I dont know how to do that in postgresql. The code to create the table, add data, create the procedure, call the procedure by passing the collection (3 objects) and output of that is posted below. Can someone suggest how this can be done in postgresql?
Following the oracle related code and details:
--create table
create table temp_n_tab1
(ssn number,
fname varchar2(20),
lname varchar2(20),
items varchar2(100));
/
--add data
insert into temp_n_tab1 values (1,'f1','l1','i1');
--SKIP no. ssn no. 2 intentionally..
insert into temp_n_tab1 values (3,'f3','l3','i3');
insert into temp_n_tab1 values (4,'f4','l4','i4');
insert into temp_n_tab1 values (5,'f5','l5','i5');
insert into temp_n_tab1 values (6,'f6','l6','i6');
commit;
--create procedure
SET SERVEROUTPUT ON
CREATE OR REPLACE PROCEDURE temp_n_proc (
p_cust_tab_type_i IN temp_n_customer_tab_type)
IS
t_cust_tab_type_i temp_n_customer_tab_type;
v_cnt NUMBER;
v_ssn temp_n_tab1.ssn%TYPE;
CURSOR c
IS
SELECT ssn
FROM temp_n_tab1
ORDER BY 1;
BEGIN
--t_cust_tab_type_i := p_cust_tab_type_i();
FOR cc IN c
LOOP
SELECT COUNT (*)
INTO v_cnt
FROM TABLE (p_cust_tab_type_i) pt
WHERE pt.ssn = cc.ssn;
IF (v_cnt > 0)
THEN
DBMS_OUTPUT.put_line (
'The array element '
|| TO_CHAR (cc.ssn)
|| ' exists in the table.');
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE (SQLERRM);
END;
/
--caller proc
SET SERVEROUTPUT ON
declare
array temp_n_customer_tab_type := temp_n_customer_tab_type();
begin
for i in 1 .. 3
loop
array.extend;
array(i) := temp_n_cust_header_type( i, 'name ' || i, 'lname ' || i,i*i*i*i );
end loop;
temp_n_proc( array );
end;
/
caller proc output:
The array element 1 exists in the table.
The array element 3 exists in the table.
When you create a table in Postgres, a type with the same name is also created. So you can simply pass an array of the table's type as a parameter to the function.
Inside the function you can then use unnest() to treat the array like a table.
The following is the closest match to your original Oracle code:
create function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns void
as
$$
declare
l_rec record;
l_msg text;
l_count integer;
BEGIN
for l_rec in select t1.ssn
from temp_n_tab1 t1
loop
select count(*)
into l_count
from unnest(p_cust_tab_type_i) as t
where t.ssn = l_rec.ssn;
if l_count > 0 then
raise notice 'The array element % exist in the table', l_rec.ssn;
end if;
end loop;
END;
$$
language plpgsql;
The row-by-row processing is not a good idea to begin with (neither in Postgres, nor in Oracle). It would be a lot more efficient to get the existing elements in a single query:
create function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns void
as
$$
declare
l_rec record;
l_msg text;
BEGIN
for l_rec in select t1.ssn
from temp_n_tab1 t1
where t1.ssn in (select t.ssn
from unnest(p_cust_tab_type_i) as t)
loop
raise notice 'The array element % exist in the table', l_rec.ssn;
end loop;
return;
END;
$$
language plpgsql;
You can call the function like this:
select temp_n_proc(array[row(1,'f1','l1','i1'),
row(2,'f2','l2','i2'),
row(3,'f3','l3','i3')
]::temp_n_tab1[]);
However a more "Postgres" like and much more efficient way would be to not use PL/pgSQL for this, but create a simple SQL function that returns the messages as a result:
create or replace function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns table(message text)
as
$$
select format('The array element %s exist in the table', t1.ssn)
from temp_n_tab1 t1
where t1.ssn in (select t.ssn
from unnest(p_cust_tab_type_i) as t)
$$
language sql;
This returns the output of the function as a result rather than using the clumsy raise notice.
You can use it like this:
select *
from temp_n_proc(array[row(1,'f1','l1','i1'),
row(2,'f2','l2','i2'),
row(3,'f3','l3','i3')
]::temp_n_tab1[]);

PLS-00497: cannot mix between single row and multi-row (BULK) in INTO list

I have created a procure to display the data in two table using BULK COLLECT, but i keep getting this error.
PLS-00497: cannot mix between single row and multi-row (BULK) in INTO list
However it works if i remove the BULK COLLECT and include a where clause in the statement.
create or replace PROCEDURE sktReport IS
TYPE inventory_table_type is RECORD (
v_WH_ID INVENTORY.WH_ID%TYPE,
v_wa_Product_quantity_id Product_quantity.ST_ID%TYPE);
v_inventory_table inventory_table_type;
BEGIN
SELECT INVENTORY.WH_ID, Product_quantity.ST_ID,
BULK COLLECT INTO
v_inventory_table.v_WH_ID,
v_inventory_table.v_wa_Product_quantity_id,
FROM INVENTORY
INNER JOIN Product_quantity
ON Product_quantity.ST_ID = INVENTORY.ST_ID;
FOR i IN v_inventory_table.v_WH_ID..v_inventory_table.v_WH_ID
LOOP
DBMS_OUTPUT.PUT_LINE ('ID : ' || v_inventory_table.v_WH_ID
|| ' quantity ID : ' || v_inventory_table.v_in_Product_quantity_id);
END LOOP;
END;
Here's a simple showcase, I've just written that might help you :)
SET SERVEROUTPUT ON;
DECLARE
TYPE t_some_type IS RECORD (
the_id NUMBER
,the_name VARCHAR2(1)
);
TYPE t_some_type_tab IS TABLE OF t_some_type;
lt_some_record t_some_type;
lt_some_array t_some_type_tab := NEW t_some_type_tab();
BEGIN
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
BULK COLLECT INTO -- use this to select into an array/collection type variable
lt_some_array
FROM
some_values sv;
DBMS_OUTPUT.PUT_LINE(lt_some_array.COUNT);
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
INTO -- use this to select into a regular variables
lt_some_record.the_id
,lt_some_record.the_name
FROM
some_values sv
WHERE
sv.the_id = 1;
DBMS_OUTPUT.PUT_LINE(lt_some_record.the_id||': '||lt_some_record.the_name);
-- you can also insert such record into your array type variable
lt_some_array := NEW t_some_type_tab();
lt_some_array.EXTEND; -- extend the array type variable (so it could store one more element, than now - which was 0)
lt_some_array(lt_some_array.LAST) := lt_some_record; -- assign the first element of array type variable
DBMS_OUTPUT.PUT_LINE(lt_some_array.COUNT||' '||lt_some_array(lt_some_array.LAST).the_id||': '||lt_some_array(lt_some_array.LAST).the_name);
END;
/
Also, since you want to iterate through your results, you can just use cursor (implicit or explicit) e.g.
DECLARE
-- cursor declaration
CURSOR c_some_cursor IS
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
FROM
some_values sv;
BEGIN
-- using explicit, earlier declared cursor
FOR c_val IN c_some_cursor
LOOP
DBMS_OUTPUT.PUT_LINE(c_val.the_id||': '||c_val.the_name);
END LOOP;
-- using implicit, not declared cursor
FOR c_val IN (
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
FROM
some_values sv
)
LOOP
DBMS_OUTPUT.PUT_LINE(c_val.the_id||': '||c_val.the_name);
END LOOP;
END;
/
Here i have demonstrated a simple example to replicate your scenario. Please see below code. This may help you out.
SET serveroutput ON;
DECLARE
TYPE AV_TEST
IS
RECORD
(
lv_att1 PLS_INTEGER,
lv_att2 PLS_INTEGER );
type av_test_tab
IS
TABLE OF av_test;
av_test_tab_av av_test_tab;
BEGIN
NULL;
SELECT LEVEL,
LEVEL+1 BULK COLLECT
INTO av_test_tab_av
FROM DUAL
CONNECT BY LEVEL < 10;
dbms_output.put_line(av_test_tab_av.count);
FOR I IN av_test_tab_av.FIRST..av_test_tab_av.LAST
LOOP
dbms_output.put_line('working fine '||av_test_tab_av(i).lv_att1||' '||av_test_tab_av(i).lv_att2);
END LOOP;
END;

How to use function returning Oracle REF_CURSOR in a procedure

I have to write an Oracle procedure which should invoke an Oracle function returning REF_CURSOR. The function is declared like that
FUNCTION "IMPACTNET"."TF_CONVERTPARA" (PARASTRING IN NVARCHAR2) RETURN SYS_REFCURSOR
AS
c SYS_REFCURSOR;
BEGIN
OPEN c FOR
SELECT SUBSTR(element, 1, INSTR(element, '|') - 1) as key,
SUBSTR(element, INSTR(element, '|') + 1, 99999) as val
FROM (
SELECT REGEXP_SUBSTR(PARASTRING, '[^;]+', 1, LEVEL) element
FROM dual
CONNECT BY LEVEL < LENGTH(REGEXP_REPLACE(PARASTRING, '[^;]+')) + 1
);
RETURN c;
END;
Can you tell me what I need to write in order to invoke the function from within my procedure? I'd like to insert all the returned values (shaped a table with two columns) into a rational table.
Thank you in advance!
Something along the lines of this should work (obviously, I'm guessing about table names and column names and the exact logic that you're trying to implement)
CREATE PROCEDURE some_procedure_name
AS
l_rc SYS_REFCURSOR := impactnet.tf_convertpara( <<some string>> );
l_key VARCHAR2(100);
l_val VARCHAR2(100);
BEGIN
LOOP
FETCH l_rc
INTO l_key, l_val;
EXIT WHEN l_rc%notfound;
INSERT INTO some_table( key_column, val_column )
VALUES( l_key, l_val );
END LOOP;
END;
As Ollie points out, it would be more efficient to do a BULK COLLECT and a FORALL. If you're just dealing with a few thousand rows (since your function is just parsing the data in a delimited string, I'm assuming you expect relatively few rows to be returned), the performance difference is probably minimal. But if you're processing more data, the difference can be quite noticeable. Depending on the Oracle version and your specific requirements, you may be able to simplify the INSERT statement in the FORALL to insert a record rather than listing each column from the record individually.
CREATE PROCEDURE some_procedure_name
AS
TYPE key_val_rec
IS RECORD(
key VARCHAR2(100),
val VARCHAR2(100)
);
TYPE key_val_coll
IS TABLE OF key_val_rec;
l_rc SYS_REFCURSOR := impactnet.tf_convertpara( <<some string>> );
l_coll key_val_coll;
BEGIN
LOOP
FETCH l_rc
BULK COLLECT INTO l_coll
LIMIT 100;
EXIT WHEN l_coll.count = 0;
FORALL i IN l_coll.FIRST .. l_coll.LAST
INSERT INTO some_table( key_column, val_column )
VALUES( l_coll(i).key, l_coll(i).val );
END LOOP;
END;

Resources