I want to create a function that returns multiple rows into a table that is of object type.
I have created an object and a nested table object and now when I run the function there is an error which says
PL/SQL: SQL Statement ignored
PL/SQL: ORA-00947: not enough values
-- Object type creation
create or replace type test_object_sn as object
(
column_1 varchar2(30),
column_2 varchar2(30),
column_3 number
);
-- Table of object
create or replace type test_otable_sn as table of test_object_sn;
-- function (where I get an error)
create or replace function load_test_object_sn
return test_otable_sn
as
details test_otable_sn;
begin
with ad as (select 'a', 'b', 4 from dual
union all
select 'r', '5', 3 from dual
union all
select 'g', 's', 3 from dual)
select * into details from ad;
return details;
end;
I want to have the test_otable_sn table object loaded with the data and then query it using the table() function via my load_test_object_sn function
e.g. select * from table(load_test_object_sn);
Update:
do you know how to modify this for scenario whereby I have an sql
statement contained in a string variable to execute?
Yes, we can use a cursor reference (SYS_REFCURSOR) and OPEN/FETCH/CLOSE instead of a CURSOR and CURSOR FOR LOOP.
The syntax is OPEN <cursor-reference> FOR <string-containing-sql-statement> . See below.
CREATE OR REPLACE FUNCTION load_test_object_sn
RETURN test_otable_sn
AS
details test_otable_sn := test_otable_sn();
-- Variable stores SQL statement for cursor
l_sql CLOB :=
q'[with ad as (
select 'a' column_1, 'b' column_2, 4 column_3 from dual union all
select 'r', '5', 3 from dual union all
select 'g', 's', 3 from dual
)
select *
from ad]';
-- Cursor reference allows us to open cursor for SQL statement above
rc SYS_REFCURSOR;
-- Define object instance to store each row fetched from the cursor
l_obj test_object_sn := test_object_sn(NULL, NULL, NULL);
i PLS_INTEGER := 1;
BEGIN
-- Explicitly open, fetch from, and close the cursor
OPEN rc FOR l_sql;
LOOP
FETCH rc INTO l_obj.column_1, l_obj.column_2, l_obj.column_3;
EXIT WHEN rc%NOTFOUND;
details.extend();
details(i) := test_object_sn(l_obj.column_1, l_obj.column_2, l_obj.column_3);
i := i + 1;
END LOOP;
CLOSE rc;
RETURN details;
END;
Original answer:
Unfortunately, one can't use SELECT * INTO with a collection in this manner, so here's an alternative way to populate the table:
create or replace function load_test_object_sn
return test_otable_sn
as
details test_otable_sn := test_otable_sn();
cursor c_ad is
with ad as (select 'a' column_1, 'b' column_2, 4 column_3 from dual
union all
select 'r', '5', 3 from dual
union all
select 'g', 's', 3 from dual)
select * from ad;
i pls_integer := 1;
begin
for ad_rec in c_ad loop
details.extend();
details(i) := test_object_sn(ad_rec.column_1, ad_rec.column_2, ad_rec.column_3);
i := i + 1;
end loop;
return details;
end;
/
Output:
SQL> SELECT * FROM TABLE(load_test_object_sn);
COLUMN_1 COLUMN_2 COLUMN_3
---------- ---------- ----------
a b 4
r 5 3
g s 3
Related
How can I use columns dynamically with bulk collect?
The code shown here is throwing errors:
SET serverout ON
DECLARE
r_emp SYS.ODCINUMBERLIST := SYS.ODCINUMBERLIST();
t_emp SYS.ODCINUMBERLIST := SYS.ODCINUMBERLIST('CUST_ID');
v_array SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'CUST_TYPE',
'CERT_TYPE_NAME',
'CERT_NBR',
'NEW_PARENT_CUST_ID');
BEGIN
DBMS_OUTPUT.ENABLE;
FOR i IN 1..v_array.COUNT LOOP
r_emp.extend;
EXECUTE IMMEDIATE
'SELECT '||t_emp||' FROM CUSTOMER_PROFILE where '||v_array(i)||' is null'
BULK COLLECT INTO r_emp(i);
for k in 1..r_emp(i).count loop
dbms_output.put_line(v_array(i) || ': ' || r_emp(k));
end loop;
END LOOP;
END;
Error report:
ORA-06550: line 15, column 7:
PLS-00306: wrong number or types of arguments in call to '||'
r_emp is an array and not an array or arrays so you cannot BULK COLLECT INTO r_emp(i), instead, you need to BULK COLLECT INTO r_emp (and need to neither initialise nor extend it as that is done automatically by BULK COLLECT).
You also cannot concatenate a string with a VARRAY so 'SELECT '||t_emp will not work (and is where your error message comes from). Instead, you can concatenate two strings so 'SELECT '||t_emp(1) would work.
And you cannot initialise a SYS.ODCINUMBERLIST with the value 'CUST_ID' as it is a string and not a number.
Instead, you can use:
DECLARE
r_emp SYS.ODCINUMBERLIST;
t_emp SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST('CUST_ID');
v_array SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'CUST_TYPE',
'CERT_TYPE_NAME',
'CERT_NBR',
'NEW_PARENT_CUST_ID'
);
BEGIN
DBMS_OUTPUT.ENABLE;
FOR i IN 1..v_array.COUNT LOOP
FOR j IN 1..t_emp.COUNT LOOP
EXECUTE IMMEDIATE
'SELECT '||t_emp(j)||' FROM CUSTOMER_PROFILE where '||v_array(i)||' is null'
BULK COLLECT INTO r_emp;
FOR k IN 1..r_emp.COUNT LOOP
dbms_output.put_line(v_array(i) || ': ' || r_emp(k));
END LOOP;
END LOOP;
END LOOP;
END;
/
Which, for the sample data:
create table customer_profile (
cust_id NUMBER,
cust_type VARCHAR2(20),
cert_type_name VARCHAR2(20),
cert_nbr VARCHAR2(20),
new_parent_cust_id VARCHAR2(20)
);
INSERT INTO customer_profile
SELECT 1, NULL, 'a', 'a', 'a' FROM DUAL UNION ALL
SELECT 2, 'b', NULL, 'b', 'b' FROM DUAL UNION ALL
SELECT 3, 'c', 'c', NULL, 'c' FROM DUAL UNION ALL
SELECT 4, 'd', 'd', 'd', NULL FROM DUAL UNION ALL
SELECT 5, NULL, NULL, NULL, NULL FROM DUAL;
Outputs:
CUST_TYPE: 1
CUST_TYPE: 5
CERT_TYPE_NAME: 2
CERT_TYPE_NAME: 5
CERT_NBR: 3
CERT_NBR: 5
NEW_PARENT_CUST_ID: 4
NEW_PARENT_CUST_ID: 5
fiddle
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
I get Error:
PLS-00231: function 'GET_NUM' may not be used in SQL
when the following code is executed;
CREATE OR REPLACE PACKAGE BODY TESTJNSABC IS
-- FUNCTION IMPLEMENTATIONS
FUNCTION get_num(num IN NUMBER)
RETURN SYS_REFCURSOR AS
my_cursor SYS_REFCURSOR;
BEGIN
--
OPEN my_cursor FOR
WITH ntable AS (
SELECT 1 ID, 111 AGT, 'ABC' DESCRIP FROM DUAL
UNION ALL
SELECT 2 ID, 222 AGT, 'ABC' DESCRIP FROM DUAL
UNION ALL
SELECT 1 ID, 333 AGT, 'ABC' DESCRIP FROM DUAL
)
SELECT AGT FROM ntable WHERE ID = num;
RETURN my_cursor;
END;
-- PROCEDURE IMPLEMENTATIONS
PROCEDURE testingabc AS
BEGIN
WITH xtable AS (
SELECT 111 AGT, 'A' DESCRIP FROM DUAL
UNION ALL
SELECT 222 AGT, 'B' DESCRIP FROM DUAL
UNION ALL
SELECT 333 AGT, 'C' DESCRIP FROM DUAL
)
SELECT DESCRIP FROM xtable WHERE COD_AGT IN get_num(1);
END testingabc;
END TESTJNSABC;
Even if I call the function as TESTJNSABC.get_num(1) I still get the same error.
--UPDATE. So in real life scenario I would like to call a Function from a WHERE CLAUSE; the function should return a set of NUMBER values (that's why I use the IN clause).
So is it possible then to create a variable on the Procedure and
assign the Function values to the variable? Let's say
It sould not be the question whether it is possible or not rather it should had been if this is the right way. Ofcourse you can do it in the way you are doing but as experts suggested, that's not the right and efficient way. See how you can do it. PS: Not tested.
CREATE OR REPLACE PACKAGE BODY TESTJNSABC IS
-- FUNCTION IMPLEMENTATIONS
FUNCTION get_num(num IN NUMBER)
RETURN SYS_REFCURSOR AS
my_cursor SYS_REFCURSOR;
BEGIN
--
OPEN my_cursor FOR
WITH ntable AS (
SELECT 1 ID, 111 AGT, 'ABC' DESCRIP FROM DUAL
UNION ALL
SELECT 2 ID, 222 AGT, 'ABC' DESCRIP FROM DUAL
UNION ALL
SELECT 1 ID, 333 AGT, 'ABC' DESCRIP FROM DUAL
)
SELECT AGT FROM ntable WHERE ID = num;
RETURN my_cursor;
END;
-- PROCEDURE IMPLEMENTATIONS
PROCEDURE testingabc AS
--Creating a collection to hold return of the function
type y is table of varchar2(1000) index by pls_integer;
var_z y;
var_1 varchar2(100);
BEGIN
Select get_num(1)
bulk collect into var_z
from dual;
For i in 1..var_z.count
loop
WITH xtable AS (
SELECT 111 AGT, 'A' DESCRIP FROM DUAL
UNION ALL
SELECT 222 AGT, 'B' DESCRIP FROM DUAL
UNION ALL
SELECT 333 AGT, 'C' DESCRIP FROM DUAL
)
SELECT DESCRIP
into var_1
FROM xtable
WHERE AGT = var_z(i) ; ---Check each record
dbms_output.put_line(var_1);
end loop;
END testingabc;
END TESTJNSABC;
in () requires either a subquery or a comma-separated list of values, so no, you can't substitute a function that returns a collection.
Assuming the function is in scope for SQL queries (it's either a standalone function or declared in a package specification), you could use it in a table() construction (this needs a table function, i.e. it needs to return a collection, not a cursor):
where somecol in (select column_value from table(get_num(1)) )
(or the equivalent inner join etc.)
Demo at livesql.oracle.com/apex/livesql/file/content_EF2M0F1LV9LTP6PEII3BDFKAI.html
Edit: I've just noticed the example in the question tried to use a ref cursor. Note that the table() operator works on collections, not ref cursors. Therefore the function has to return a collection type (nested table or varray).
As per #William Robertson answer, I tried to implement the solution but getting issue :
[Error] ORA-22905 (18: 26): PL/SQL: ORA-22905: cannot access rows from
a non-nested table item
-- FUNCTION IMPLEMENTATIONS
create or replace FUNCTION get_num(num IN NUMBER)
RETURN SYS_REFCURSOR AS
my_cursor SYS_REFCURSOR;
BEGIN
--
OPEN my_cursor FOR
SELECT AGT
FROM ntable
WHERE ID = num;
RETURN my_cursor;
END;
-- PROCEDURE IMPLEMENTATIONS
CREATE OR REPLACE PROCEDURE testingabc
AS
type var is table of xtable.DESCRIP%type;
v_var var;
BEGIN
SELECT DESCRIP
bulk collect into var
FROM xtable
WHERE AGT IN (SELECT COLUMN_VALUE
FROM TABLE (get_num (1)));
END testingabc;
I want to use this array in 'select from..where..in(YYY)' statement.
I don't want to iterate through array values, I want to use it whole in my select statement.
Unfortunately, I found only how to iterate it:
1 declare
2 type array is table of varchar2(30) index by binary_integer;
3 a array;
4 procedure p( array_in array )
5 is
6 begin
7 for i in 1..array_in.count loop
8 dbms_output.put_line( array_in(i) );
9 end loop;
10 end;
11 begin
12 a(1) := 'Apple';
13 a(2) := 'Banana';
14 a(3) := 'Pear';
15 p( a );
16 end;
17 /
You can do this by creating a function returning your array. Then you can use it into a select:
Create external types and function
create or replace type t_array is table of varchar2(30);
create or replace function list_of_fruits
return t_array
is
l_ t_array:=t_array();
begin
l_.extend(); l_(l_.COUNT) := 'Apple';
l_.extend(); l_(l_.COUNT) := 'Banana';
l_.extend(); l_(l_.COUNT) := 'Pear';
return l_;
end list_of_fruits;
/
And here is how to use it:
select * from (
select 'Peter' this_and_that from dual
union all select 'Joy' from dual
union all select 'God' from dual
union all select 'Pear' from dual
union all select 'Man' from dual
)
where this_and_that in (
select column_value from (table( list_of_fruits() ))
);
The trick here is to use the table() function to make a SQL usable list for your select; also difficult for me was to discover the name of that column_value... which is some built-in constant from Oracle: how do you guess that?
You can use oracle defined collection to achieve this as well. Please see below and example.
declare
a sys.odcivarchar2list;
begin
a := sys.odcivarchar2list('Apple','Banana','Pear');
for r in ( SELECT m.column_value m_value
FROM table(a) m )
loop
dbms_output.put_line (r.m_value);
end loop;
end;
I am trying to return a table via a function in an Oracle package:
CREATE OR REPLACE PACKAGE test AS
TYPE rec IS RECORD(
col1 VARCHAR(10));
TYPE rec_table IS TABLE OF rec;
FUNCTION get_table(input VARCHAR2)
RETURN rec_table
PIPELINED;
END;
CREATE OR REPLACE PACKAGE BODY test AS
FUNCTION get_table(input VARCHAR2)
RETURN rec_table
PIPELINED IS
rec1 rec;
BEGIN
SELECT * INTO rec1
FROM
(
SELECT '1' from dual
UNION ALL
SELECT '2' from dual
);
PIPE ROW (rec1)
RETURN;
END get_table;
END;
but when I try to run
select * from table(test.get_table('blah'))
I get an error: exact fetch returns more then requested number of rows
I've read a bit about BULK COLLECT INTO, but I am not understanding the syntax...
The following piece of code:
SELECT '1' from dual
UNION ALL
SELECT '2' from dual
Returns two, not one record, and you are trying to put those two records in one rec variable. You should instead loop through the results of the UNION:
FOR v_rec IN (
SELECT *
FROM (
SELECT '1' from dual
UNION ALL
SELECT '2' from dual
)
)
LOOP
PIPE ROW (v_rec);
END LOOP;