Oracle: detect exception in type construtor during bulk collect - oracle

Environment: Oracle 11g
I have a type ty1 with some arguments like s1, s2.
If I use it like this:
SELECT ty1(s1,s2) BULK COLLECT INTO l_collection_of_ty1 FROM ...
I get a collection of ty1.
Now if there is an exception raised inside one of the constructor calls of ty1 the corresponding element of my collection is set to NULL, but the overall SELECT works (no exception, collection returned).
My question, can I detect this right after the SELECT without having to loop over the collection? Is there maybe even a way to access the original error message in a similar way as SQL%BULK_EXCEPTION does for DML?
One workaround I thought of is to not use the constructor during BULK COLLECT, but read collections of s1 and s2, then construct the TYPE in my own loop where I can handle the exception, but that is much more code and I would prefer it if Oracle has some build in way.

here is a testcase that demonstrates that all exceptions are thrown through a bulk-select, but NO_DATA_FOUND is dropped.
-- remember to retreive dbms_output
SET SERVEROUTPUT ON
CREATE OR REPLACE FUNCTION p1 (v1 PLS_INTEGER)
RETURN NUMBER
AS
BEGIN
CASE v1
WHEN 1
THEN
RAISE ACCESS_INTO_NULL;
WHEN 2
THEN
RAISE CASE_NOT_FOUND;
WHEN 3
THEN
RAISE COLLECTION_IS_NULL;
WHEN 4
THEN
RAISE CURSOR_ALREADY_OPEN;
WHEN 5
THEN
RAISE DUP_VAL_ON_INDEX;
WHEN 6
THEN
RAISE INVALID_CURSOR;
WHEN 7
THEN
RAISE INVALID_NUMBER;
WHEN 8
THEN
RAISE LOGIN_DENIED;
WHEN 9
THEN
RAISE NO_DATA_FOUND;
WHEN 10
THEN
RAISE NOT_LOGGED_ON;
WHEN 11
THEN
RAISE PROGRAM_ERROR;
WHEN 12
THEN
RAISE ROWTYPE_MISMATCH;
WHEN 13
THEN
RAISE SELF_IS_NULL;
WHEN 14
THEN
RAISE STORAGE_ERROR;
WHEN 15
THEN
RAISE SUBSCRIPT_BEYOND_COUNT;
WHEN 16
THEN
RAISE SUBSCRIPT_OUTSIDE_LIMIT;
WHEN 17
THEN
RAISE SYS_INVALID_ROWID;
WHEN 18
THEN
RAISE TIMEOUT_ON_RESOURCE;
WHEN 19
THEN
RAISE TOO_MANY_ROWS;
WHEN 20
THEN
RAISE VALUE_ERROR;
WHEN 21
THEN
RAISE ZERO_DIVIDE;
ELSE
RETURN v1;
END CASE;
END;
/
DECLARE
TYPE type1 IS TABLE OF NUMBER;
col1 type1;
BEGIN
FOR ii IN 1 .. 22
LOOP
BEGIN
SELECT p1 (ii)
BULK COLLECT INTO col1
FROM DUAL;
IF col1 (1) IS NULL
THEN
DBMS_OUTPUT.put_line (TO_CHAR (ii, '00') || ': NULL');
ELSE
DBMS_OUTPUT.put_line (TO_CHAR (ii, '00') || ': ' || col1 (1));
END IF;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line (
TO_CHAR (ii, '00')
|| ': exception '
|| SQLCODE);
END;
END LOOP;
END;
/

I wrote a small test case for you and the exception has to be swallowed inside ty1 and not raised, because otherwise the select would not finish successfully:
create or replace function p1 (v1 number) return number
as
begin
if v1 = 1 then
return 1;
elsif v1 = 2 then
raise_application_error(-20010,'err 2');
else
raise_application_error(-20010,'err 3');
end if;
end;
/
declare
type type1 is table of number;
col1 type1;
begin
select p1(level) bulk collect into col1 from dual connect by level <=3;
end;
/
Result:
Error report -
ORA-20010: err 2
So my suggestion to you is - if you want to stay close to your solution - that at the place where you handle the exception in ty1, you write the exceptions to a table. You can then access this table to find the exceptions and don't need to loop through the whole collection. But honestly, what's wrong with looping in PL/SQL over a collection, it's all in memory? HTH

I see only two options.
An exception is consumed inside constructor.
The another constructor is in use.
Example:
create or replace type ty1 as object (p1 number
, constructor function ty1 (p1 varchar2)
return self as result);
create or replace type body ty1
is
constructor function ty1 (p1 varchar2)
return self as result is
x number;
begin
raise_application_error(-20000,'Always Exception');
return;
end;
end;
Test 1 no exception:
declare
type l_collection_of_ty1 is table of ty1;
a varchar2(4000);
x l_collection_of_ty1;
begin
--test1 select ty1(level) bulk collect into x from dual connect by level < 10;
-- no exceptions
--test2 select ty1(level||1) bulk collect into x from dual connect by level < 10;
-- exceptions
--test3 select ty1(null) bulk collect into x from dual connect by level < 10;
-- exceptions
end;
In Test1 db is using Attribute-Value Constructor. It is generate by default.
In Test2 db is using user defined constructor.
In Test3 db is not able to find out which constructor should be used.()

Related

how to cancel Pipe row?

I have a pipeline function. If I catch an exception, I want to return no lines. Even if the exception occurs after the first utilisation of pipe row.
CREATE TYPE a_r IS OBJECT (a1 VARCHAR (1), a2 VARCHAR (1));
CREATE TYPE a_t IS TABLE OF a_r;
CREATE or replace FUNCTION get_a
RETURN a_t
PIPELINED
IS
BEGIN
FOR c IN (SELECT '1' a , '2' b FROM DUAL)
LOOP
PIPE ROW (a_r(c.a,c.b));
END LOOP;
FOR a
IN (SELECT 'a2' a,
'b' b
FROM DUAL)
LOOP
PIPE ROW (a_r(a.a,a.b));
END LOOP;
exception
WHEN VALUE_ERROR
THEN
DBMS_OUTPUT.put_line ('VALUE_ERROR');
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line ('other');
END;
select * from table(get_a())
In this example, Before the error ('ab' is not varchar2(1)) occurs, the first line is already piped. But I want nothing to be returned. Is there a way to write cancel what was piped in the exception block?
code
No because the client may already have consumed the row.
You could code your function so that it fills a local collection with data and only at the end decides whether to iterate through the collection to actually return the rows. That will require more memory since you're materializing the entire result set before returning it and defeats some of the performance benefits of using pipelined table functions.
Something like this would appear to do what you want
CREATE or replace FUNCTION get_a
RETURN a_t
PIPELINED
IS
l_a_t a_t := new a_t();
BEGIN
FOR c IN (SELECT '1' a , '2' b FROM DUAL)
LOOP
l_a_t.extend;
l_a_t( l_a_t.count ) := a_r(c.a,c.b);
END LOOP;
FOR a
IN (SELECT 'a2' a,
'b' b
FROM DUAL)
LOOP
l_a_t.extend;
l_a_t( l_a_t.count ) := a_r(a.a,a.b);
END LOOP;
for i in 1 .. l_a_t.count
loop
pipe row( l_a_t(i) );
end loop;
exception
WHEN VALUE_ERROR
THEN
DBMS_OUTPUT.put_line ('VALUE_ERROR');
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line ('other');
END;
/
See this dbfiddle

Why are none of my conditions being met in this proc?

So i have a stored procedure (that's been watered down below for demo purposes) that aren't passing any conditions and thus aren't inserting/passing any values into my table. I've tried converting the varchar/string that is being passed in by Java to a number but nothing is working. Below is my 'simplified code'
Create or Replace Procedure SAMPLE(rValue IN VARCHAR)
IS
v_Max value.value%type;
v_forecast value.value%type;
BEGIN
--
SELECT BUFFER_MAX_VALUE
INTO v_MAX
FROM look_up;
--
EXCEPTION
WHEN no_data_found
THEN SELECT 0
INTO v_forecast
FROM DUAL;
--
IF to_Number(rValue) < 0 OR to_Number(rValue) > v_MAX)
THEN
dbms_output.put_line('IF1 Works');
insert into value(value_id, value)
values(1, rValue);
ELSIF rValue is null OR to_Number(rValue) = 0
THEN
dbms_output.put_line('IF1A ONLY Works');
END IF;
ELSE
insert into value(value_id, value)
values(1, v_forecast);
dbms_output.put_line('IF1 ELSE ONLY Works');
END SAMPLE;
i tried passing the following in:
BEGIN
SAMPLE('-7');
END;
If the first SELECT BUFFER_MAX_VALUE returns anything, nothing else will be executed because you put absolutely everything into the EXCEPTION section. If you meant to handle that statement only, you should have enclosed it into its own BEGIN-END block, such as
create procedure ...
begin
-- its own begin starts now
begin
select buffer_max_value into v_max
from look_up;
exception
when no_data_found then
-- do something here
end;
-- its own end ends now
-- put the rest of your code here
end;
By the way, does LOOK_UP table contain no rows or only one row, always? Because, as SELECT you wrote contains no WHERE clause, it might raise TOO_MANY_ROWS (which you should also handle).
You declared rValue as VARCHAR2, and then apply TO_NUMBER to it. Why don't you declare it to be a NUMBER, instead? Because, nothing prevents you from passing, for example, 'XYZ' to the procedure, and then TO_NUMBER will miserably fail with the INVALID NUMBER error.
[EDIT: some more exception handling]
EXCEPTION section handles all exceptions that happen in that BEGIN-END block, no matter how many SELECT statements you have. Though, you won't know which one failed, unless you include a little bit of additional (simple) programming.
Note that this is just for showing what I meant; don't handle errors with DBMS_OUTPUT (as, most probably, nobody will see it), and rarely you'd want to handle errors with WHEN OTHERS.
create procedure ...
l_position number;
begin
l_position := 1;
select ... into ... from ...;
l_position := 2;
select ... into ...
exception
when others then
dbms_output.put_line('Error on position ' || l_position ||' '|| sqlerrm);
raise;
end;
As far as I can tell, you wanted the exception section to trap the situation where there is nothing in the lookup table. In that case, you set v_forecast and then continue. That means you need to put the select inside its own block.
I also avoiding multiple to_number calls by setting a constant.
I got rid of the unnecessary select from dual.
I also really really hope that you do not have a table named VALUE with a column named VALUE. Choose more meaningful names.
See how this works for you.
CREATE OR REPLACE PROCEDURE sample (rvalue IN VARCHAR2)
IS
c_rvalue CONSTANT NUMBER := rvalue;
v_max VALUE.VALUE%TYPE;
v_forecast VALUE.VALUE%TYPE;
BEGIN
BEGIN
SELECT buffer_max_value INTO v_max FROM look_up;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
v_forecast := 0;
END;
IF c_rvalue < 0 OR c_rvalue > v_max
THEN
DBMS_OUTPUT.put_line ('IF1 Works');
INSERT INTO VALUE (value_id, VALUE)
VALUES (1, rvalue);
ELSIF c_rvalue IS NULL OR c_rvalue = 0
THEN
DBMS_OUTPUT.put_line ('IF1A ONLY Works');
ELSE
INSERT INTO VALUE (value_id, VALUE)
VALUES (1, v_forecast);
DBMS_OUTPUT.put_line ('IF1 ELSE ONLY Works');
END IF;
END sample;

How to populate TABLE OF RECORDS with rows of 2 levels parent-child

I have a table with rows describing a tree, columns are:
(column child_num is unique and used as primary key)
TABLE items_tree (
child_num number,
parent_ref varchar2(10),
child_ref varchar2(10)
);
TYPE item_rec_type IS RECORD (
item_id NUMBER,
spaces_number NUMBER,
parent_ref VARCHAR2(10),
child_ref VARCHAR2(10)
);
TYPE tree_table_type IS TABLE OF item_rec_type%ROWTYPE
INDEX BY BINARY_INTEGER;
table_tree tree_table_type; -- table of all items
Example data for table items_tree (values of child_num are not relevant):
parent_ref child_ref
---------------------------
null Abraham
Abraham Isaac
Abraham Ishmael
Isaac Jakob
Jakob Yehuda
Jakob Josef
Jakob Benjamin
Yehuda David
Josef Efraim
Josef Menashe
David Solomon
Solomon Isaiah
Isaiah Jeremiah
I need to populate table_tree records from items_tree table. To do so, I am using a package, the item_rec_type, tree_table_type, table_tree are defined in it and two procedures: print_tree which retrieves the ROOT items of the tree, starts the process and prints the tree from table_tree. Second procedure get_items_by_parent_recursively is recursive procedure that retrieves all the items or a parent item, e.g. calling get_items_by_parent_recursively('Abraham') will add Isaac and Ishmael to table_tree.
The cursor is declared in the package body:
CURSOR get_children_cur(c_parent in varchar2(10))
IS
SELECT parent_ref, child_ref
FROM items_tree
WHERE parent_ref = c_parent
ORDER BY 1, 2;
The code in get_items_by_parent_recursively that retrieves the items for the parent:
procedure get_items_by_parent_recursively(p_parent in VARCHAR2(10), p_spaces_number in NUMBER )
AS
l_spaces_number NUMBER := 0;
l_child VHARCHAR2(10);
l_parent VHARCHAR2(10);
BEGIN
l_spaces_number := p_spaces_number + 3;
OPEN get_children_cur(p_parent);
LOOP
FETCH get_children_cur INTO l_parent, l_child;
EXIT WHEN get_children_cur%NOTFOUND;
IF (l_child is not null) THEN
v_row_number := v_row_number + 1;
tree_table(v_row_number).row_num := v_row_number;
tree_table(v_row_number).spaces_number := l_spaces_number;
tree_table(v_row_number).parent_ref := l_parent;
tree_table(v_row_number).child_ref := l_child;
-- Calling procedure recursively
get_items_by_parent_recursively( l_child, l_spaces_number );
END IF;
END LOOP;
CLOSE get_children_cur;
EXCEPTION
WHEN CURSOR_ALREADY_OPEN THEN
DBMS_OUTPUT.put_line(' Exception -- CURSOR_ALREADY_OPEN');
WHEN INVALID_CURSOR THEN
DBMS_OUTPUT.put_line(' Exception -- INVALID_CURSOR');
WHEN INVALID_NUMBER THEN
DBMS_OUTPUT.put_line(' Exception -- INVALID_NUMBER');
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.put_line(' Exception -- NO_DATA_FOUND');
WHEN PROGRAM_ERROR THEN
DBMS_OUTPUT.put_line(' Exception -- PROGRAM_ERROR');
WHEN ROWTYPE_MISMATCH THEN
DBMS_OUTPUT.put_line(' Exception -- ROWTYPE_MISMATCH');
WHEN STORAGE_ERROR THEN
DBMS_OUTPUT.put_line(' Exception -- STORAGE_ERROR');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.put_line(' Exception -- TOO_MANY_ROWS');
WHEN VALUE_ERROR THEN
DBMS_OUTPUT.put_line(' Exception -- VALUE_ERROR');
END get_items_by_parent_recursively;
Running this procedure I am getting the exception: CURSOR_ALREADY_OPEN.
I have searched for a reply, but none came close to what I need. I will appreciate any ideas.
I will try to make the cursor get_children_cur part of the recursive procedure.
As #vmachan said, you need to move the cursor definition into the procedure. While you have it in the package specification or body, but outside the procedure, there is one instance of it which is global to the session. Each call to your procedure attempts to open the same cursor; the initial call from print_tree succeeds, and your table is populated with 'Abraham'; but then the recursive call tries to re-open it and gets the CURSOR_ALREADY_OPEN exception, and stops.
Moving the cursor into the procedure means each call/iteration has its own independent copy. Cleaning up naming and various other issues, this then works:
procedure get_items_by_parent(p_parent in VARCHAR2, p_spaces_number in NUMBER)
AS
l_spaces_number NUMBER := 0;
l_child VARCHAR2(10);
l_parent VARCHAR2(10);
CURSOR get_children_cur(p_parent in varchar2) IS
SELECT parent_item, child_item
from items_tree
where parent_item = p_parent
or (p_parent is null and parent_item is null);
BEGIN
l_spaces_number := p_spaces_number + 3;
OPEN get_children_cur(p_parent);
LOOP
FETCH get_children_cur INTO l_parent, l_child;
EXIT WHEN get_children_cur%NOTFOUND;
IF (l_child is not null) THEN
v_row_number := v_row_number + 1;
table_tree(v_row_number).item_id := v_row_number;
table_tree(v_row_number).spaces_number := l_spaces_number;
table_tree(v_row_number).parent_item_ref := l_parent;
table_tree(v_row_number).item_ref := l_child;
-- Calling procedure recursively
get_items_by_parent( l_child, l_spaces_number );
END IF;
END LOOP;
CLOSE get_children_cur;
EXCEPTION
WHEN CURSOR_ALREADY_OPEN THEN
DBMS_OUTPUT.put_line(' Exception -- CURSOR_ALREADY_OPEN');
WHEN INVALID_CURSOR THEN
DBMS_OUTPUT.put_line(' Exception -- INVALID_CURSOR');
WHEN INVALID_NUMBER THEN
DBMS_OUTPUT.put_line(' Exception -- INVALID_NUMBER');
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.put_line(' Exception -- NO_DATA_FOUND');
WHEN PROGRAM_ERROR THEN
DBMS_OUTPUT.put_line(' Exception -- PROGRAM_ERROR');
WHEN ROWTYPE_MISMATCH THEN
DBMS_OUTPUT.put_line(' Exception -- ROWTYPE_MISMATCH');
WHEN STORAGE_ERROR THEN
DBMS_OUTPUT.put_line(' Exception -- STORAGE_ERROR');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.put_line(' Exception -- TOO_MANY_ROWS');
WHEN VALUE_ERROR THEN
DBMS_OUTPUT.put_line(' Exception -- VALUE_ERROR');
END get_items_by_parent;
Inventing a print_tree based on what you describe:
procedure print_tree is
begin
get_items_by_parent(null, 0);
for i in 1..table_tree.count loop
dbms_output.put_line(to_char(table_tree(i).item_id, '99999') || ' '
|| lpad(' ', table_tree(i).spaces_number, ' ')
|| table_tree(i).item_ref);
end loop;
end print_tree;
... calling that now works, and produces 13 indented records:
1 Abraham
2 Isaac
3 Jakob
4 Yehuda
5 David
6 Solomon
7 Isaiah
8 Jeremiah
9 Josef
10 Efraim
11 Menashe
12 Benjamin
13 Ishmael
As #XING said, you can get the same result more simply with a different for of cursor loop:
procedure get_items_by_parent(p_parent in VARCHAR2, p_spaces_number in NUMBER)
AS
l_spaces_number NUMBER := 0;
CURSOR get_children_cur(p_parent in varchar2) IS
SELECT parent_item, child_item
from items_tree
where child_item is not null
and (parent_item = p_parent
or (p_parent is null and parent_item is null));
BEGIN
l_spaces_number := p_spaces_number + 3;
FOR r IN get_children_cur(p_parent)
LOOP
v_row_number := v_row_number + 1;
table_tree(v_row_number).item_id := v_row_number;
table_tree(v_row_number).spaces_number := l_spaces_number;
table_tree(v_row_number).parent_item_ref := r.parent_item;
table_tree(v_row_number).item_ref := r.child_item;
-- Calling procedure recursively
get_items_by_parent( r.child_item, l_spaces_number );
END LOOP;
END get_items_by_parent;
or even:
procedure get_items_by_parent(p_parent in VARCHAR2, p_spaces_number in NUMBER)
AS
BEGIN
FOR r IN (
SELECT parent_item, child_item
from items_tree
where child_item is not null
and (parent_item = p_parent
or (p_parent is null and parent_item is null)))
LOOP
v_row_number := v_row_number + 1;
table_tree(v_row_number).item_id := v_row_number;
table_tree(v_row_number).spaces_number := p_spaces_number + 3;
table_tree(v_row_number).parent_item_ref := r.parent_item;
table_tree(v_row_number).item_ref := r.child_item;
-- Calling procedure recursively
get_items_by_parent( r.child_item, p_spaces_number + 3 );
END LOOP;
END get_items_by_parent;
Of course, you don't need to use PL/SQL or a table at all, you can use a hierarchical query:
select rownum, lpad(' ', level * 3, ' ') || child_item as item
from items_tree
start with parent_item is null
connect by parent_item = prior child_item
order siblings by child_num;
ROWNUM ITEM
---------- --------------------------------------------------
1 Abraham
2 Isaac
3 Jakob
4 Yehuda
5 David
6 Solomon
7 Isaiah
8 Jeremiah
9 Josef
10 Efraim
11 Menashe
12 Benjamin
13 Ishmael
but presumably this is a PL/SQL exercise. If you aren't required to use a recursive procedure you could still populate your table from a similar query, using bulk collect.

How to raise exception in procedures?

This is my procedure:
create or replace procedure p1(p_deptno in number)
is
cursor c is select * from emp where deptno=p_deptno;
i emp%rowtype;
begin
open c;
loop
fetch c into i;
exit when c%notfound;
dbms_output.put_line(i.ename);
end loop;
exception
when no_data_found then
dbms_output.put_line('Give proper deptno');
end p1;
/
When I run it using SQL*Plus, I get this:
SQL> exec p1(70);
PL/SQL procedure successfully completed.
But deptno 70 is not available. Exception should be raised, but it is not. What am I doing wrong?
Using RAISE_APPLICATION_ERROR Will be more appropriate here if we need to show a user defined error for this particular error message.
CREATE OR REPLACE PROCEDURE p1(
p_deptno IN NUMBER)
IS
i emp%rowtype;
BEGIN
SELECT * INTO i FROM emp WHERE deptno=p_deptno;
EXCEPTION
WHEN no_data_found THEN
RAISE_APPLICATION_ERROR(-20001,'Dept no '||p_deptno||' has no data',TRUE);
END p1;
No data found exception would be raised only if you fire a select (not a cursor). Following is a sample based on your code where this would happen:
create or replace procedure p1(p_deptno in number)
is
i emp%rowtype;
begin
select * into i
from emp where deptno=p_deptno;
exception
when no_data_found then
dbms_output.put_line('Give proper deptno');
end p1;
/
You could raise an exception based on %NOTFOUND as demonstrated by Gavin.
Also,the first fetch from an open cursor, cursor_name%NOTFOUND returns NULL.Thereafter, it returns FALSE if the last fetch returned a row, or TRUE if the last fetch failed to return a row. (https://docs.oracle.com/cd/B12037_01/appdev.101/b10807/13_elems011.htm)
You need to call RAISE (documentation link here)
IF i IS NULL THEN
RAISE no_data_found;
END IF;

Selecting Values from Oracle Table Variable / Array?

Following on from my last question (Table Variables in Oracle PL/SQL?)...
Once you have values in an array/table, how do you get them back out again? Preferably using a select statement or something of the like?
Here's what I've got so far:
declare
type array is table of number index by binary_integer;
pidms array;
begin
for i in (
select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
)
loop
pidms(pidms.count+1) := i.sgbstdn_pidm;
end loop;
select *
from pidms; --ORACLE DOESN'T LIKE THIS BIT!!!
end;
I know I can output them using dbms_output.putline(), but I'm hoping to get a result set like I would from selecting from any other table.
Thanks in advance,
Matt
You might need a GLOBAL TEMPORARY TABLE.
In Oracle these are created once and then when invoked the data is private to your session.
Oracle Documentation Link
Try something like this...
CREATE GLOBAL TEMPORARY TABLE temp_number
( number_column NUMBER( 10, 0 )
)
ON COMMIT DELETE ROWS;
BEGIN
INSERT INTO temp_number
( number_column )
( select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
);
FOR pidms_rec IN ( SELECT number_column FROM temp_number )
LOOP
-- Do something here
NULL;
END LOOP;
END;
/
In Oracle, the PL/SQL and SQL engines maintain some separation. When you execute a SQL statement within PL/SQL, it is handed off to the SQL engine, which has no knowledge of PL/SQL-specific structures like INDEX BY tables.
So, instead of declaring the type in the PL/SQL block, you need to create an equivalent collection type within the database schema:
CREATE OR REPLACE TYPE array is table of number;
/
Then you can use it as in these two examples within PL/SQL:
SQL> l
1 declare
2 p array := array();
3 begin
4 for i in (select level from dual connect by level < 10) loop
5 p.extend;
6 p(p.count) := i.level;
7 end loop;
8 for x in (select column_value from table(cast(p as array))) loop
9 dbms_output.put_line(x.column_value);
10 end loop;
11* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
SQL> l
1 declare
2 p array := array();
3 begin
4 select level bulk collect into p from dual connect by level < 10;
5 for x in (select column_value from table(cast(p as array))) loop
6 dbms_output.put_line(x.column_value);
7 end loop;
8* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
Additional example based on comments
Based on your comment on my answer and on the question itself, I think this is how I would implement it. Use a package so the records can be fetched from the actual table once and stored in a private package global; and have a function that returns an open ref cursor.
CREATE OR REPLACE PACKAGE p_cache AS
FUNCTION get_p_cursor RETURN sys_refcursor;
END p_cache;
/
CREATE OR REPLACE PACKAGE BODY p_cache AS
cache_array array;
FUNCTION get_p_cursor RETURN sys_refcursor IS
pCursor sys_refcursor;
BEGIN
OPEN pCursor FOR SELECT * from TABLE(CAST(cache_array AS array));
RETURN pCursor;
END get_p_cursor;
-- Package initialization runs once in each session that references the package
BEGIN
SELECT level BULK COLLECT INTO cache_array FROM dual CONNECT BY LEVEL < 10;
END p_cache;
/
The sql array type is not neccessary. Not if the element type is a primitive one. (Varchar, number, date,...)
Very basic sample:
declare
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
pidms TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into pidms
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH';
-- do something with pidms
open :someCursor for
select value(t) pidm
from table(pidms) t;
end;
When you want to reuse it, then it might be interesting to know how that would look like.
If you issue several commands than those could be grouped in a package.
The private package variable trick from above has its downsides.
When you add variables to a package, you give it state and now it doesn't act as a stateless bunch of functions but as some weird sort of singleton object instance instead.
e.g. When you recompile the body, it will raise exceptions in sessions that already used it before. (because the variable values got invalided)
However, you could declare the type in a package (or globally in sql), and use it as a paramter in methods that should use it.
create package Abc as
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList;
function Test1(list in TPidmList) return PLS_Integer;
-- "in" to make it immutable so that PL/SQL can pass a pointer instead of a copy
procedure Test2(list in TPidmList);
end;
create package body Abc as
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList is
result TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into result
from sgbstdn
where sgbstdn_majr_code_1 = majorCode
and sgbstdn_program_1 = program;
return result;
end;
function Test1(list in TPidmList) return PLS_Integer is
result PLS_Integer := 0;
begin
if list is null or list.Count = 0 then
return result;
end if;
for i in list.First .. list.Last loop
if ... then
result := result + list(i);
end if;
end loop;
end;
procedure Test2(list in TPidmList) as
begin
...
end;
return result;
end;
How to call it:
declare
pidms constant Abc.TPidmList := Abc.CreateList('HS04', 'HSCOMPH');
xyz PLS_Integer;
begin
Abc.Test2(pidms);
xyz := Abc.Test1(pidms);
...
open :someCursor for
select value(t) as Pidm,
xyz as SomeValue
from table(pidms) t;
end;

Resources