oracle cursor parameter as collection - sql

it works like:
create or replace procedure back_end_proc(p_rc OUT SYS_REFCURSOR) is
v_script VARCHAR2(4000); begin
v_script := 'select sysdate data from dual where ''a'' = :a union all select sysdate-1 data from dual where ''b'' = :b';
open p_rc for v_script using 'a','b'; end;
when calling the procedure:
DECLARE
rc SYS_REFCURSOR;
BEGIN
victom.back_end_proc2(:rc );
END;
will have an output rows:
DATA
27.06.2017 11:25:02
26.06.2017 11:25:02
THE QUESTION IS:
how to modify the using 'a','b' params into one array or some collection data that will contain 'a','b' as one string or smth like one single value?

If your target query looked like this ...
select * from whatever
where id in ( .... )
it would be quite simple to pass a collection.
create or replace type tt1 as table of varchar2(1);
/
create or replace procedure back_end_proc(
p_ids in tt1
, p_rc OUT SYS_REFCURSOR) is
begin
open p_rc for
'select * from whatever
where id member of :1'
using p_ids;
end;
However, your posted case is slightly more complicated because you want to assign specific values from the collection in each sub-query. This means you need to use a VARRAY because that is the only collection type which guarantees ordering. So:
create or replace type va1 as varray(10) of varchar2(1);
/
create or replace procedure back_end_proc(
p_ids in va1
, p_rc OUT SYS_REFCURSOR) is
begin
open p_rc for
'select sysdate from dual where ''a'' = :1
union all
select sysdate-1 from dual where ''b'' = :2'
using p_ids(1), p_ids(2);
end;

Related

Oracle: Using an array datav type in package

I have a package which contains a procedure in which I need to query a list of id numbers, which are varchar2. I will have to query this same list multiple times, and I'd rather not have redo the query. Ideally I'd like to make a function in my package that would return the list of id numbers. So I could load the array of id numbers into a variable, and then use that variable as a table throughout my procedure. I've been googling like crazy and it just doesn't seem like it's possible. Is there some way to do this?
Note: I'm not able to create a new type at the schema level, and it won't let me do this with a local collection type.
Also, I would prefer not to use dynamic sql; the main query in my procedure is enormous, and I don't want to deal with a string of that size.
I want to do something like this:
id_number_list array_type := my_function();
select *
from my_table mt
left join table(id_number_list) idl on mt.id_number = idl.column_value;
EDIT: Thanks for your help so far! MTO's answer works for a select statement join, like I described above. However, I also need to delete from the table where the id_number is in the list. This gives me an "invalid data type" error. What could explain this? The data type should always be the same: varchar2(10).
Here I create the type at package level:
type string_list is table of varchar2(10);
Then I create a function that returns the list of id numbers (for our purposes, the "action" is always c_action_refresh, so the if statement is true):
function get_modified_ids(scope in smallint, action in smallint) return string_list is
modified_ids string_list := string_list();
last_refreshed date;
begin
last_refreshed := get_last_refreshed_date(scope,action);
if action = c_action_refresh then
select id_number
bulk collect into modified_ids
from(
select id_number
from adv.hr_giving cg
join adv.pbi_dates d
on d.DATE_FULL = trunc(cg.processed_date)
where d.RELATIVE_DATE >= last_refreshed
and d.RELATIVE_DATE <= trunc(CURRENT_DATE)
and cg.fiscal_year >= adv.current_fiscal_year - 6
union
select gi.gift_donor_id as id_number
from adv.gift gi
where gi.date_added >= last_refreshed
or gi.date_modified >= last_refreshed
union
select p.pledge_donor_id as id_number
from adv.pledge_rev p
where p.date_added >= last_refreshed
or p.date_modified >= last_refreshed
union
select a.id_number
from adv.affiliation a
where a.date_added >= last_refreshed
or a.date_modified >= last_refreshed
);
end if;
return(modified_ids);
end get_modified_ids;
Then, in my procedure, I initialize a variable by calling the function:
modified_ids string_list := get_modified_ids(scope,action);
Then I try to use the list in a delete statement:
delete from advrpt.pbi_gvg_profile_ag p
where p.id_number in
(select column_value from table(modified_ids));
This gives the error ORA-00902: invalid datatype. The type of id_number is varchar2(10). And again, it works fine in a join in a select statement.
So why am I getting this error?
Don't use a variable in the package (as there would only be a single variable and if your procedure is called twice in short succession then the second set of values would overwrite the first and potentially cause issues if that happened mid-way through processing the first invocation).
Instead, create a user-defined collection type:
CREATE TYPE number_list IS TABLE OF NUMBER;
And pass a collection as an argument to the procedure:
CREATE PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id MEMBER OF i_numbers;
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
/
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_procedure(number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
Or create the type as part of your package:
CREATE PACKAGE your_package AS
TYPE number_list IS TABLE OF NUMBER;
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
);
END;
/
Then create the package body:
CREATE PACKAGE BODY your_package AS
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id IN (SELECT COLUMN_VALUE FROM TABLE(i_numbers));
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
END;
/
Note: The MEMBER OF operator only works with collections defined in the SQL scope and not collections defined locally in a PL/SQL scope.
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_package.your_procedure(your_package.number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
fiddle
To me, it looks as if you have it all. If not, here's an example.
Function:
SQL> create or replace function f_test
2 return sys.odcinumberlist
3 is
4 begin
5 return sys.odcinumberlist(10, 20);
6 end;
7 /
Function created.
How to use it?
SQL> set serveroutput on
SQL> declare
2 id_number_list sys.odcinumberlist := f_test;
3 begin
4 for cur_r in (select e.deptno, e.ename
5 from emp e join table(id_number_list) idl on idl.column_value = e.deptno
6 order by 1, 2
7 )
8 loop
9 dbms_output.put_line(cur_r.deptno ||' '|| cur_r.ename);
10 end loop;
11 end;
12 /
10 CLARK
10 KING
10 MILLER
20 ADAMS
20 FORD
20 JONES
20 SCOTT
20 SMITH
PL/SQL procedure successfully completed.
SQL>

Returning values from Dynamic PL/SQL to match a basic SELECT FROM statement

This is undoubtedly a very basic Dynamic PL/SQL question, but I am stuck.
I am looking to write a dynamic PL/SQL which will function the same as the code below, returning values of field1, field2, field3 from all rows of tablename:
SELECT field1, field2, field3 FROM databasename.tablename;
If I write the following Dynamic SQL, I can get it to successfully execute. However, I can't get it to return anything:
declare
Query VARCHAR2(200) := 'SELECT field1, field2, field3 FROM databasename.tablename';
begin
EXECUTE IMMEDIATE Query;
end;
How does one return the results of the Dynamic SQL to match the one-line SELECT statement above, please?
Your first select statement is sql query and it can return the result as an tabular format.
But your second code is anonymous block and you can not execute any select query in anonumous block without INTO clause.
If you know that your select query is going to give you single record then use the INTO clause as follows:
EXECUTE IMMEDIATE Query into var1, var2, var3;
Note: declare the variables var1, var2 and var3 in declare section with proper data type.
A straightforward method to return the dataset from a Query string for your case would be using a cursor within a Stored Function or Procedure with return type SYS_REFCURSOR such as
CREATE OR REPLACE FUNCTION Get_Result_Of_TheTable RETURN SYS_REFCURSOR IS
Query VARCHAR2(200) := 'SELECT field1, field2, field3 FROM tablename';
v_recordset SYS_REFCURSOR;
BEGIN
OPEN v_recordset FOR Query;
RETURN v_recordset;
END;
/
and then call from SQL Developer's console as
DECLARE
result SYS_REFCURSOR;
BEGIN
:result := Get_Result_Of_TheTable;
END;
/
PRINT result;
There are several possibilities. If you know which fields are returned, then you can solve it this way:
DECLARE
TYPE cursor_type IS REF CURSOR;
l_cursor cursor_type;
l_record tablename%ROWTYPE;
l_query VARCHAR2(32767) := 'SELECT field1, field2, field3 FROM tablename';
BEGIN
OPEN l_cursor FOR l_query;
LOOP
FETCH l_cursor INTO l_record;
EXIT WHEN l_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE (l_record.field1 ||' '|| l_record.field2 ||' '|| l_record.field3);
END LOOP;
CLOSE l_cursor;
END;
/
Or you can write a small pipelined function:
CREATE OR REPLACE PACKAGE pkg_test
AS
TYPE test_type IS TABLE
OF tablename%ROWTYPE;
FUNCTION get_fields
RETURN test_type PIPELINED;
END pkg_test;
/
CREATE OR REPLACE PACKAGE BODY pkg_test
AS
FUNCTION get_fields
RETURN test_type PIPELINED
AS
TYPE cursor_type IS REF CURSOR;
l_cursor cursor_type;
l_record tablename%ROWTYPE;
l_query VARCHAR2(32767) := 'SELECT field1, field2, field3 FROM tablename';
BEGIN
OPEN l_cursor FOR l_query;
LOOP
FETCH l_cursor INTO l_record;
EXIT WHEN l_cursor%NOTFOUND;
PIPE ROW (l_record);
END LOOP;
CLOSE l_cursor;
END;
END pkg_test;
/
SELECT * FROM TABLE(pkg_test.get_fields);
This was for my test:
CREATE TABLE tablename
( field1 VARCHAR2(10),
field2 VARCHAR2(10),
field3 VARCHAR2(10)
);
INSERT INTO tablename VALUES ('A1', 'A2', 'A3');
INSERT INTO tablename VALUES ('B1', 'B2', 'B3');
INSERT INTO tablename VALUES ('C1', 'C2', 'C3');
COMMIT;
You can use the bulk collect into clause, like for static SQL in PL/SQL block.
Here is a simple example:
drop table users_tb;
create table users_tb (
id integer generated always as identity,
--
username varchar2(30) not null,
is_active char(1) default 1 not null,
created_date date default sysdate not null ,
edited_date date,
--
constraint user_id_pk primary key (id),
constraint user_is_active_ch check (is_active in (1,0))
);
insert into users_tb (username) values ('john.wick');
insert into users_tb (username) values ('constantine');
insert into users_tb (username) values ('neo');
commit;
declare
-- types
type tr_list is record (
username varchar2(30),
is_active integer
);
type tt_usernamelist is table of tr_list
index by pls_integer;
-- variables
lt_users tt_usernamelist;
l_stmt varchar2(2000);
begin
-- select statement
l_stmt := 'select username, is_active from users_tb';
-- execution of dynamic code
execute immediate l_stmt
bulk collect into lt_users;
-- loop over retrived data
for i in 1..lt_users.count loop
dbms_output.put_line('User: '||lt_users(i).username||' - Is active: '||lt_users(i).is_active);
end loop;
end;
The important part is the execution of dynamic code where I am using bulk collect into. Here is a link to DBFiddle to try out.

How to tokenize semicolon separated column value to pass to IF statement in a function in Oracle DB

I have a table called 'config' and when I query it in following manner:
SELECT value FROM config WHERE property = 'SPECIAL_STORE_ID'
its response will be: 59216;131205;76707;167206 //... (1)
I want to tokenize the above values using semicolon as the delimiter and then use them in a user-defined Function's IF statement to compare, something like this:
IF in_store_id exists in (<delimited response from (1) above>)//...(2)
THEN do some stuff
where in_store_id is the parameter passed-in to the function
Is this possible to do as one-liner in (2) above ?
I'm on Oracle 12c
One-liner? I don't think so, but - if you're satisfied with something like this, fine.
SQL> select * From config;
VALUE PROPERTY
-------------- ----------------
7369;7499;7521 SPECIAL_STORE_ID
SQL> declare
2 in_store_id varchar2(20) := 7369;
3 l_exists number;
4 begin
5 select instr(value, ';' || in_store_id || ';')
6 into l_exists
7 from config
8 where property = 'SPECIAL_STORE_ID';
9
10 if l_exists > 0 then
11 dbms_output.put_line('that STORE_ID exists in the value');
12 else
13 dbms_output.put_line('that STORE_ID does not exist in the value');
14 end if;
15 end;
16 /
that STORE_ID exists in the value
PL/SQL procedure successfully completed.
SQL>
If the delimited response is a collection then you can use member of to check if the collection contains the ID or not like
create or replace procedure test_procedure2(p_property in varchar2, p_id in varchar2) is
type test_t is table of varchar2(20);
l_ids test_t;
begin
select regexp_substr(value, '[^;]+', 1, level) bulk collect into l_ids
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1;
if(p_id member of (l_ids)) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
or do it without the collection with intermediate select like
create or replace procedure test_procedure1(p_property in varchar2, p_id in varchar2) is
l_flag number(3);
begin
select count(1) into l_flag from dual where p_id in (
select regexp_substr(value, '[^;]+', 1, level)
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1
);
if(l_flag > 0) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
See fiddle

Oracle PL/SQL - how to use table of varchar in select statement?

I have a table type declared as
TYPE t_table IS TABLE OF VARCHAR2(15) INDEX BY PLS_INTEGER;
I am trying to use it in a procedure with a select statement, but it does not work:
procedure get_something (
p_in_list IN t_table,
p_out_list OUT t_table
)
IS
BEGIN
SELECT item
BULK collect into p_out_list
from my_table
where myrow in (select * from table(p_in_list));
END get_something;
How can I use this in a select ... in statement?
This it tested in 19.0. I don't have earlier versions to test on right now, but I think it will require at least 12.1.
First, if you need the type to be an associative array ('index by'), it needs to be in a package specification:
create or replace package demo_pkg
as
type t_table is table of varchar2(15) index by pls_integer;
end demo_pkg;
Then SQL can see it:
declare
-- type t_table is table of varchar2(15) index by pls_integer;
subtype t_table is demo_pkg.t_table;
from_list t_table;
to_list t_table;
procedure get_something
( p_in_list in t_table
, p_out_list out t_table )
is
begin
select dummy bulk collect into p_out_list
from dual
where dummy in (select * from table(p_in_list));
end get_something;
begin
from_list(1) := 'X';
from_list(2) := 'Y';
from_list(3) := 'Z';
get_something(from_list, to_list);
end;
From 18c you can populate the array declaratively using a qualified expression, e.g:
from_list t_table := demo_pkg.t_table(1 => 'X', 2 => 'Y', 3 => 'Z');
or
get_something
( demo_pkg.t_table(1 => 'X', 2 => 'Y', 3 => 'Z')
, to_list );
Some of these restrictions are because associative arrays aren't really a natural fit for SQL queries, and support for them took a while to be added. If you declare t_table as a regular nested table, it should work in an earlier version:
create or replace package demo_pkg
as
type t_table is table of varchar2(15);
end demo_pkg;
or create it as a standalone SQL object:
create or replace type t_table as table of varchar2(15);
This also makes a member of construction possible:
declare
from_list t_table := t_table('X','Y','Z');
to_list t_table;
procedure get_something
( p_in_list in t_table
, p_out_list out t_table )
is
begin
select dummy bulk collect into p_out_list
from dual
where dummy member of p_in_list;
end get_something;
begin
get_something(from_list, to_list);
end;
member of only works with "nested table" collections, not associative arrays or varrays. I can never really see the point of varrays, unless the size limit is so useful for your business logic that you can live with all the lost functionality.
Here's one option; see if it helps.
Sample table and type:
SQL> create table my_table as
2 select dname item, loc myrow from dept;
Table created.
SQL> create or replace type t_table as varray(20) of varchar2(20);
2 /
Type created.
Procedure:
SQL> create or replace procedure
2 get_something (
3 p_in_list in t_table,
4 p_out_list out t_table
5 )
6 is
7 begin
8 select item
9 bulk collect into p_out_list
10 from my_table
11 where myrow in (select * from table(p_in_list));
12 end get_something;
13 /
Procedure created.
SQL>
Testing:
SQL> set serveroutput on
SQL> declare
2 l_in t_table;
3 l_out t_table;
4 begin
5 l_in := t_table();
6 l_in.extend(2);
7 l_in(1) := 'DALLAS';
8 l_in(2) := 'NEW YORK';
9
10 get_something(l_in, l_out);
11
12 for i in 1 .. l_out.count loop
13 dbms_output.put_line(l_out(i));
14 end loop;
15 end;
16 /
RESEARCH
ACCOUNTING
PL/SQL procedure successfully completed.
SQL>

How include array (or similar) into an in-clause SQL statement?

I am new to oracle and PL SQL. Currently I struggle with the handling of arrays and "similar things" like i.e. collection. I am trying to build a procedure like:
procedure insert_by_array( my_array some_array_type)
begin
insert into table1 (some_column)
select some_column
from table2
where column2 in my_array
;
end;
However I could not make I did try some array types but I did not find the right one. The entries of the type must be varchar2 - a part of this criterium I am open to any array type. I.e when my array_type is
type array_of_strings is varray(100) of varchar2(40);
My error would be: "local collection types are not alowed in sql statements"
I am using Oracle Database 12c Enterprise Edition Release 12.1.0.2.0.
So at the end, this worked:
create type table_of_strings IS TABLE OF VARCHAR2(64); --define it global;
declare
my_table table_of_strings;
begin
my_table := table_of_strings('aaa', 'bb','c');
insert into table1 (some_column)
select some_column
from table2
where column2 in (select * from table(my_table))
;
end;
First of all, you have to define sql level collection. (varray is not an option)
create type array_of_strings as table of varchar2(40);
Now you can use table or "member of" approach
declare
c array_of_strings := new array_of_strings();
begin
c.extend();
c(c.count) := 'A';
c.extend();
c(c.count) := 'B';
for rec in (select * from dual where 'A' member of c ) loop
dbms_output.put_line('Option with memeber of ');
end loop;
for rec in (select * from dual where 'A' in (select * from table(c))) loop
dbms_output.put_line('Option with table');
end loop;
end;
As error says, local collections, defined in procedure, function, code block, cannot be used in queries, at least in Oracle 11 which I use.
This does not work (PLS-00642):
declare
type strings is table of varchar2(5);
v_i strings := strings('A', 'X', 'Q');
v_o strings;
begin
select * bulk collect into v_o from dual where dummy in (select * from table(v_i));
end;
So... either use type defined at schema level (your own or predefined sys.odcivarchar2list):
declare
type strings is table of varchar2(5);
v_i strings := strings('A', 'X', 'Q');
v_o strings;
v_so sys.odcivarchar2list := sys.odcivarchar2list();
begin
v_so.extend(v_i.count);
for i in 1..v_i.count loop
v_so(i) := v_i(i);
end loop;
select * bulk collect into v_o from dual where dummy in (select * from table(v_so));
end;
... either use dynamic sql:
declare
type strings is table of varchar2(5);
v_i strings := strings('A', 'X', 'Q');
v_o strings;
v_str varchar2(4000);
begin
for i in 1..v_i.count loop
v_str := v_str || case when i > 1 then ', ' end || ''''||v_i(i)||'''';
end loop;
execute immediate 'select * from dual where dummy in ('||v_str||')' bulk collect into v_o ;
end;
... either use loop (probably slowest):
declare
type strings is table of varchar2(5);
v_i strings := strings('A', 'X', 'Q');
v_o strings := strings();
begin
for rec in (select * from dual) loop
if rec.dummy member of v_i then
v_o.extend();
v_o(v_o.count) := rec.dummy;
end if;
end loop;
end;