Oracle pipelined function with generic record type? - oracle

I have a pipelined function which returns results of a SQL query:
function my_pipelined_function(...)
return rec_t
PIPELINED is
begin
for l in (select * from ... join ... where ...) loop
PIPE ROW(l);
end loop;
return;
end;
This works, but I have to define the record and it's type in the package header:
TYPE rec IS RECORD(
some_date_param date,
some_number_param number,
...
);
TYPE rec_t IS TABLE OF rec;
Ugly. Isn't there a way to avoid having to declare the "hard-coded" record with all types defined, one by one?
I saw the ANYDATA type, but I can't make it work. Should I use this approach, if yes, how to do it? If not, should I use some kind of ref cursor, or is this simply not possible at all in Oracle?
Thanks

Related

Invalid Datatype when passing collection into function

CREATE OR REPLACE PACKAGE test_package
IS
TYPE IDTable IS TABLE OF test_table.id%TYPE;
TYPE RowIDTable IS TABLE OF VARCHAR2(500);
FUNCTION delete_rows (row_ids IN RowIDTable) RETURN IDTable;
END test_package;
/
CREATE OR REPLACE PACKAGE BODY test_package
IS
FUNCTION delete_rows (row_ids IN RowIDTable) RETURN IDTable
IS
ids IDTable;
BEGIN
DELETE FROM test_table
WHERE ROWIDTONCHAR(rowid) IN (SELECT * FROM TABLE(row_ids))
RETURNING id BULK COLLECT INTO ids;
COMMIT;
RETURN ids;
END;
END test_package;
/
I keep getting ORA-00902: invalid datatype when using the above function. What am I doing wrong?
The error is pointing to the DELETE statement.
It looks like the problem is the type of collection you’re using. in that it’s a package-level PL/SQL collection type.
The ruby-plsql documentation says it supports PL/SQL records, but the table and varray types don’t refer to PL/SQL - suggesting they have to be SQL (i.e. schema-level) user-defined types.
Even without Ruby in the picture, the invalid-datatype error is thrown at runtime, when the same package-defined types are used consistently. Prior to 12c you couldn’t use a PL/SQL collection in a SQL statement at all, and it still doesn’t seem to work in this scenario.
Your code works if you change from the the PL/SQL type to a schema-level type, in this example a built-in varray type:
CREATE OR REPLACE PACKAGE test_package
IS
TYPE IDTable IS TABLE OF test_table.id%TYPE;
--TYPE RowIDTable IS TABLE OF VARCHAR2(500); —- not used now
FUNCTION delete_rows (row_ids IN sys.odcivarchar2list) RETURN IDTable;
END test_package;
/
CREATE OR REPLACE PACKAGE BODY test_package
IS
FUNCTION delete_rows (row_ids IN sys.odcivarchar2list) RETURN IDTable
IS
ids IDTable;
BEGIN
DELETE FROM test_table
WHERE ROWIDTONCHAR(rowid) IN (SELECT * FROM TABLE(row_ids))
RETURNING id BULK COLLECT INTO ids;
COMMIT;
RETURN ids;
END;
END test_package;
/
which you can test with anonymous block:
declare
ids test_package.idtable;
begin
ids := test_package.delete_rows(sys.odcivarchar2list('ROWID1', 'ROWID2'));
end;
/
It would be more efficient to not convert every rowid in the table, so you can do this instead:
WHERE rowid IN (SELECT CHARTOROWID(column_value) FROM TABLE(row_ids))
but bear in mind that rowids are not always immutable - hopefully the references you are passing in are recent enough to always still be valid. Otherwise, work with primary keys rather than rowids.
I don’t know Ruby so not entirely sure how that translates; again from the docs it looks like:
plsql.test_package.delete_rows(['ROWID1','ROWID2'])
... but not sure how it’ll handle the returned (PL/SQL) table type. That may need to be a schema-level table type too. (And you can probably create types as either varrays or nested tables if you want to make your own, which is probably a good idea.)
Why don't you use TABLE OF ROWID instead of TABLE OF VARCHAR2(500) on RowIDTable?
Like so:
TYPE RowIDTable IS TABLE OF ROWID;
Then you wouldn't have to cast ROWID:
DELETE FROM test_table tbl
WHERE tbl.rowid IN (SELECT column_value FROM TABLE(row_ids))
RETURNING id BULK COLLECT INTO ids;
Hope this helps.

How to transfer cursor to the procedure dynamically and to set rowtype variable dynamically?

I have written the procedure with dynamically set cursor and %rowtype variable:
create or replace procedure process(source_table IN varchar2, my_cursor IN sys_refcursor)
is
c sys_refCURSOR;
rec my_cursor%rowtype;
begin
Dbms_Output.put_line('process starts');
open c for 'select * from '||source_table;
loop
fetch c into rec;
exit when c%notfound;
end loop;
close c;
Dbms_Output.put_line('process is over');
end process;
I am going to transfer cursor to the procedure with the function as follows:
CREATE OR REPLACE FUNCTION ddp_get_allitems (source_table IN Varchar2)
RETURN SYS_REFCURSOR
AS
my_cursor SYS_REFCURSOR;
BEGIN
OPEN my_cursor FOR 'SELECT * FROM '|| source_table;
RETURN my_cursor;
END ddp_get_allitems;
While compiling the procedure "process" I have the error:
PLS-00320 the declaration of the type of the expression is incomplete or malformed.
The compiler has hilighted the row with "rec my_cursor%rowtype;" as the error source. The varibale "source_table" and "my_cursor" are based upon the same table (select * from my_table).
So Why the error has arisen and how to remove it?
Since PL/SQL is statically typed the compiler needs to know the types of all the variables at compile time.
So there is no room for advanced metaprogramming. I'm afraid you can't do that.
There are, however, generic types found at SYS.STANDARD and a few internal functions accepting them.
-- The following data types are generics, used specially within package
-- STANDARD and some other Oracle packages. They are protected against
-- other use; sorry. True generic types are not yet part of the language.
type "<ADT_1>" as object (dummy char(1));
type "<RECORD_1>" is record (dummy char(1));
type "<TUPLE_1>" as object (dummy char(1));
type "<VARRAY_1>" is varray (1) of char(1);
type "<V2_TABLE_1>" is table of char(1) index by binary_integer;
type "<TABLE_1>" is table of char(1);
type "<COLLECTION_1>" is table of char(1);
type "<REF_CURSOR_1>" is ref cursor;
Take "<ADT_1>" for example. There is XMLTYPE constructor or DBMS_AQ ENQUEUE and DEQUEUE functions. You can pass any kind of object there.
For now you cannot use this datatype in custom functions since they are "not yet part of the language", but maybe some day there will be some support for this.
Just a thought to modify soe params which can basically same output
you want to achieve. Basically here for Function i have replaced
RETURN type as TABLE TYPE which can be easilt called in Procedure abd
rest manipulations can be done.Let me know if this helps
--SQL Object creation
CREATE TYPE source_table_obj IS OBJECT
(<TABLE_ATTRIBITES DECLARATION>);
--SQL TABLE type creation
CREATE TYPE source_table_tab IS TABLE OF source_table_obj;
--Function creation with nested table type as RETURN type
CREATE OR REPLACE FUNCTION ddp_get_allitems(
source_table IN VARCHAR2)
RETURN source_table_tab
AS
src_tab source_table_tab;
BEGIN
SELECT * BULK COLLECT INTO src_tab FROM source_table;
RETURN src_tab;
END ddp_get_allitems;
-- Using Function's OUT param as an IN Param for Procedure an do all the requird processing
CREATE OR REPLACE PROCEDURE process(
source_table IN VARCHAR2,
src_tab_in IN source_table_tab)
IS
BEGIN
FOR i IN src_tab_in.FIRST..src_tab_in.LAST
LOOP
dbms_output.put_line('job processing');
END LOOP;
END process;

Tail call for pipelined functions

I have some pipelined function:
create type my_tab_type as table of ...
create function my_func (X in number) return my_tab_type pipelined as
begin
loop
...
pipe row (...);
end loop;
return;
end;
Now I want to create another pipelined function my_func_zero which does the same as my_func but for fixed value of parameter: my_func_zero must be equivalent to my_func(0).
Can I implement my_func_zero without senseless and boring loop for processing every row returned by select * from table(my_func(0))?
P.S. That thread is a bit similar, but it does not contain answer to my question.
It's possible, but only if you don't declare your second function as pipelined because all functions of this type return results on row-by-row basis.
If you omit this requirement you can reach your target with bulk collect if you need typed cursor:
create function my_zero_func return my_tab_type
as
res_table my_tab_type;
begin
select my_type(field1, field2)
bulk collect into res_table
from table(my_func(0));
return res_table;
end;
Alternatively you can use untyped cursor:
create function my_ref_zero_func return sys_refcursor
as
vRes sys_refcursor;
begin
open vRes for select * from table(my_func(0));
return vRes;
end;
SQLFiddle
In a client application my_ref_zero_func results may be used without changes, but in the SQLFiddle it converted to XML representation because there are no way to demonstrate ref cursor with this tool.

How to wrap an Oracle stored procedure in a function that gets executed by a standard SELECT query?

I am following these steps, but I continue to get an error and don't see the issue:
1) Create a custom Oracle datatype that represents the database columns that you want to retrieve:
CREATE TYPE my_object AS OBJECT
(COL1 VARCHAR2(50),
COL2 VARCHAR2(50),
COL3 VARCHAR2(50));
2) Create another datatype that is a table of the object you just created:
TYPE MY_OBJ_TABLE AS TABLE OF my_object;
3) Create a function that returns this table. Also use a pipeline clause so that results are pipelined back to the calling SQL, for example:
CREATE OR REPLACE
FUNCTION MY_FUNC (PXOBJCLASS varchar2)
RETURN MY_OBJ_TABLE pipelined IS
TYPE ref1 IS REF CURSOR
Cur1 ref1,
out_rec_my_object := my_object(null,null,null);
myObjClass VARCHAR2(50);
BEGIN
myObjClass := PXOBJCLASS
OPEN Cur1 For ‘select PYID, PXINSNAME, PZINSKEY from PC_WORK where PXOBJCLass = ;1’USING myObjClass,
LOOP
FETCH cur1 INTO out_rec.COL1, out_rec.COL2, out_rec.COL3;
EXIT WHEN Cur1%NOTFOUND;
PIPE ROW (out_rec);
END LOOP;
CLOSE Cur1;
RETURN;
END MY_FUNC;
NOTE: In the example above, you can easily replace the select statement with a call to another stored procedure that returns a cursor variable.
4) In your application, call this function as a table function using the following SQL statement:
select COL1, COL2, COL3 from TABLE(MY_FUNC('SomeSampletask'));
There is no need to use dynamic sql (dynamic sql is always a little bit slower) and there are too many variables declared. Also the for loop is much easier. I renamed the argument of the function from pxobjclass to p_pxobjclass.
Try this:
create or replace function my_func (p_pxobjclass in varchar2)
return my_obj_table pipelined
is
begin
for r_curl in (select pyid,pxinsname,pzinskey
from pc_work
where pxobjclass = p_pxobjclass) loop
pipe row (my_object(r_curl.pyid,r_curl.pxinsname,r_curl.pzinskey));
end loop;
return;
end;
EDIT1:
It is by the way faster to return a ref cursor instead of a pipelined function that returns a nested table:
create or replace function my_func2 (p_pxobjclass in varchar2)
return sys_refcursor
is
l_sys_refcursor sys_refcursor;
begin
open l_sys_refcursor for
select pyid,pxinsname,pzinskey
from pc_work
where pxobjclass = p_pxobjclass;
return l_sys_refcursor;
end;
This is faster because creating objects (my_object) takes some time.
I see two problems:
The dynamic query does not work that way, try this:
'select PYID, PXINSNAME, PZINSKEY from PC_WORK where PXOBJCLass ='''||PXOBJCLASS||''''
You don't need myObjClass, and it seems all your quotes are wrong.
The quoting on 'SomeSampletask'...
select COL1, COL2, COL3 from TABLE(MY_FUNC('SomeSampletask'));
Maybe I'm misunderstanding something here, but it seems like you want to be using a VIEW.

Returning Oracle ref cursor and appending multiple results

I have this problem I'm hoping someone knows the answer to. I have an oracle stored procedure that takes a customer id and returns all the customer's orders in a ref_cursor. Oversimplifying it, this is what I have:
Orders
- orderId
- siteID
Customers
- siteID
- Name
GetOrder(siteID, outCursor) /* returns all orders for a customer */
Now, I need to write another procedure that takes a customer name and does a LIKE query to get all custIds, then I need to reuse the GetOrder method to return all the orders for the custIds found, something like this:
PROCEDURE GetOrderbyCustName(
p_name IN VARCHAR2,
curReturn OUT sys_refcursor
)
IS
siteid number;
BEGIN
FOR rec in SELECT site_id FROM customers WHERE name LIKE p_name
LOOP
-- This will replace curReturn in each iteration
-- how do I append instead?
GetOrder(rec.site_id,
curReturn
);
END LOOP;
END GetOrderbyCustName;
My question is, how do I append the return of GetOrder to curReturn in each iteration? As it's written right now it overwrites it in each cycle of the loop.
Thanks!!
You can't do it like that - cursors cannot be appended or merged. Just do this instead:
PROCEDURE GetOrderbyCustName(
p_name IN VARCHAR2,
curReturn OUT sys_refcursor
)
IS
BEGIN
OPEN curReturn FOR
SELECT o.orderID, o.siteID
FROM Orders o
JOIN Customers c ON c.siteID = o.siteID
WHERE c.name LIKE p_name;
END GetOrderbyCustName;
If the query is simple, I would say go with Tony's answer. This is not only simple but likely to perform better than executing one query for each siteID.
If it is fairly complex then it might be worth some extra effort to reuse the GetOrder procedure so you only have to maintain one query.
To do this, you would need to actually fetch the data from the refcursor on each iteration of the loop, and put it into some other data structure.
One option, if it makes sense for the interface, is to change GetOrderbyCustName to have a PL/SQL index-by table as its output parameter instead of a refcursor. Append to that table on each iteration through the loop.
If you really need to return a refcursor, you can use a nested table type instead and then return a cursor querying that nested table. Something like this (not tested code):
CREATE TYPE number_table_type AS TABLE OF NUMBER;
PROCEDURE GetOrderbyCustName(
p_name IN VARCHAR2,
curReturn OUT sys_refcursor
)
IS
cursor_source_table number_table_type := number_table_type();
single_site_cursor sys_refcursor;
orderID NUMBER;
BEGIN
FOR rec in SELECT site_id FROM customers WHERE name LIKE p_name
LOOP
-- This will replace curReturn in each iteration
-- how do I append instead?
GetOrder(rec.site_id,
single_site_cursor
);
-- Fetch all rows from the refcursor and append them to the nested table in memory
LOOP
FETCH single_site_cursor INTO orderID;
EXIT WHEN single_site_cursor%NOTFOUND;
cursor_source_table.extend();
cursor_source_table( cursor_source_table.COUNT+1) := orderID;
END LOOP;
END LOOP;
OPEN curReturn FOR
SELECT * FROM TABLE( cursor_source_table );
END GetOrderbyCustName;

Resources