PL/SQL Table Function - How to return an empty result set? - oracle

I'm trying to create a PL/SQL table function. I don't mind if it is PIPELINED or not. I just want it to return a query-able result set.
And I want to start with an empty result set. (Because it is possible that the result set I intend to construct will be empty.)
Is it possible to create a PL/SQL table function that returns zero rows?
In the books I have, and tutorials I can find, I only see examples that must return at least one record.
Example of the problem:
CREATE OR REPLACE PACKAGE z_util AS
TYPE t_row
IS RECORD (
CATEGORY VARCHAR2( 128 CHAR )
, MEASURE NUMBER
);
TYPE t_tab
IS TABLE OF t_row;
FUNCTION f_test
RETURN t_tab;
END z_util;
/
CREATE OR REPLACE PACKAGE BODY z_util AS
FUNCTION f_test
RETURN t_tab IS
retval t_tab;
BEGIN
RETURN retval;
END;
END z_util;
/
SELECT test.*
FROM TABLE ( z_util.f_test ) test;
Output:
Error starting at line : 24 in command -
SELECT test.*
FROM TABLE ( z_util.f_test ) test
Error at Command Line : 25 Column : 14
Error report -
SQL Error: ORA-00902: invalid datatype
00902. 00000 - "invalid datatype"
*Cause:
*Action:

Something like this?
SQL> CREATE TYPE t_row AS OBJECT (id NUMBER, name VARCHAR2 (50));
2 /
Type created.
SQL> CREATE TYPE t_tab IS TABLE OF t_row;
2 /
Type created.
SQL> CREATE OR REPLACE FUNCTION f_test
2 RETURN t_tab
3 AS
4 retval t_tab;
5 BEGIN
6 RETURN retval;
7 END;
8 /
Function created.
SQL> SELECT f_test FROM DUAL;
F_TEST(ID, NAME)
--------------------------------------------------------------------
SQL>
Saying that this is too simple and that it doesn't work while in package:
SQL> CREATE OR REPLACE PACKAGE pkg_test
2 AS
3 FUNCTION f_test
4 RETURN t_tab;
5 END;
6 /
Package created.
SQL> CREATE OR REPLACE PACKAGE BODY pkg_test
2 AS
3 FUNCTION f_test
4 RETURN t_tab
5 AS
6 retval t_tab;
7 BEGIN
8 RETURN retval;
9 END f_test;
10 END pkg_test;
11 /
Package body created.
SQL> select pkg_test.f_test from dual;
F_TEST(ID, NAME)
-------------------------------------------------------
SQL>
Works OK as well. Did you, by any chance, declare type within the package? If so, try to create it at SQL level.

See 6.4.6 Querying a Collection:
Note: In SQL contexts, you cannot use a function whose return type was declared in a package specification.
You cannot unnest the result of function, but the variable with the same data type:
create or replace package utils as
type t_row is record (category varchar2 (8), measure number);
type t_tab is table of t_row;
function passon (t t_tab:=null) return t_tab;
end utils;
/
create or replace package body utils as
function passon (t t_tab:=null) return t_tab is
begin
return t;
end;
end utils;
/
Usage and outcomes:
var rc refcursor
declare
tab1 utils.t_tab := utils.passon (); -- empty
tab2 utils.t_tab := utils.passon (utils.t_tab (utils.t_row ('category', 50)));
begin
open :rc for
select * from table (tab1) union all
select * from table (tab2);
end;
/
CATEGORY MEASURE
-------- ----------
category 50

jsut use a return statment without any parameters.
I have already one table function that splits a string based in token.
I modified the code for you as an example answet for your quetion.
If you pass the first string null, the function will return no rows, otherwise will return each token in separate row based on the second parameter offcurse.
// create needed types
// row object
CREATE OR REPLACE TYPE T_TOKEN_ROW AS OBJECT (
id NUMBER,
token_text VARCHAR2(50)
);
// table object
CREATE OR REPLACE TYPE T_TOKEN_TAB IS TABLE OF t_token_row;
// create table function to toknize a string
// input : P_string : the string to be toknized
// P_separator : a character to separate tokens
// Outputs : each token in separate record with id field
CREATE OR REPLACE FUNCTION PIPE_tokens (P_string varchar2,P_separator char) RETURN t_token_tab PIPELINED
AS
sLine VARCHAR2(2000);
nPos INTEGER;
nPosOld INTEGER;
nIndex INTEGER;
nLength INTEGER;
nCnt INTEGER;
sToken VARCHAR2(200);
BEGIN
if (P_string is null ) then
return ;
else
sLine := P_string;
IF (SUBSTR(sLine, LENGTH(sLine), 1) <> '|') THEN
sLine := sLine || '|';
END IF;
nPos := 0;
sToken := '';
nLength := LENGTH(sLine);
nCnt := 0;
FOR nIndex IN 1..nLength LOOP
IF ((SUBSTR(sLine, nIndex, 1) = P_separator) OR (nIndex = nLength)) THEN
nPosOld := nPos;
nPos := nIndex;
nCnt := nCnt + 1;
sToken := SUBSTR(sLine, nPosOld + 1, nPos - nPosOld - 1);
PIPE ROW(t_token_row(nCnt,sToken));
--tTokenTab(nCnt) := sToken;
END IF;
END LOOP;
RETURN;
end if;
END;
// 2 Test query
select * from table(PIPE_tokens(null,';')) ;
select * from table(PIPE_tokens('5;2;3',';')) ;

Related

Unable to create table function in oracle, type mismatch found between FETCH cursor and INTO variable

I am trying to create a table function to use in tableau's custom SQL, but I am getting an error, type mismatch found between FETCH cursor and INTO variable. Below is the code I am trying, I have created a type object and table of that type object. Function my_fct should return the table with a select statement output.
CREATE
OR replace type DATA_OBJ AS OBJECT (
id varchar2(10)
);
CREATE
OR replace type
DATA_OBJ_TAB AS TABLE OF DATA_OBJ;
CREATE OR REPLACE FUNCTION my_fct()
RETURN DATA_OBJ_TAB PIPELINED
AS
TYPE CurTyp IS REF CURSOR RETURN DATA_OBJ_TAB%ROWTYPE;
rc CurTyp;
CURSOR data IS SELECT ID from alumni_data;
BEGIN
FOR rc IN data LOOP
PIPE ROW (rc);
END LOOP;
END;
This can be implemented with a packaged PTF without using the SQL data types at all.
Something like this:
create table alumni_data (id, memo) as
select rownum id, 'memo '||rownum from dual connect by level<=3
/
create or replace package pack as
type arrT is table of alumni_data%rowtype;
function get (c varchar2) return arrT pipelined;
end;
/
create or replace package body pack as
function get (c varchar2) return arrT pipelined is
arr arrT;
begin
select * bulk collect into arr
from alumni_data
where memo like c||'%';
for i in 1..arr.count loop
pipe row (arr(i));
end loop;
return;
end;
end;
/
Result:
select * from pack.get ('mem');
ID MEMO
---------- ---------------------------------------------
1 memo 1
2 memo 2
3 memo 3
Have a look at the following example:
SQL> create or replace type data_obj as object
2 (id varchar2(10));
3 /
Type created.
SQL> create or replace type
2 data_obj_tab as table of data_obj;
3 /
Type created.
SQL> create or replace function my_fct
2 return data_obj_tab pipelined
3 as
4 l_vc data_obj := data_obj(null);
5 begin
6 for cur_r in (select id from alumni_data) loop
7 l_vc.id := cur_r.id;
8 pipe row (l_vc);
9 end loop;
10 return;
11 end;
12 /
Function created.
SQL> select * from table(my_fct);
ID
----------
CLARK
KING
MILLER
SQL>

How to give the result of a pipelined function as a default parameter?

I have created these 4 types :
record type myrecord1 is (...)
record type myrecord2 is (...)
table tableofmyrecord1 is table of myrecord1;
table tableofmyrecord2 is table of myrecord2;
and 2 functions :
function a(k in tableofmyrecord2) return packageName.tableofmyrecord1 PIPELINED;
function b return packageName.tableofmyrecord2 PIPELINED;
I can effectively give a default parameter ; null, the table empty, a table that I have create, but I can give directly the result of a of pipelined function.
function a return packageName.tOfmyrecord(k in tableofmyrecord2 :=package.b) PIPELINED
This does'nt work.
This solution does'nt work too.
declare
defaultArgOFA :=package.b;
function a return packageName.tOfmyrecord(k in tableofmyrecord2 :=defaultArgOFA) PIPELINED
same error
PLS-00653: aggregate/table functions are not allowed
This error says that you cannot call pipelined functions using PLSQL, so you can't directly write v:=a(). But you can use SQL. Below is an example where pipelined b gets input from pipelined a and multiplies salaries.
Package:
create or replace package pkg is
type tr is record (id int, name varchar2(10), sal int);
type tt is table of tr;
function a return tt pipelined;
function b(par in tt) return tt pipelined;
end pkg;
Body:
create or replace package body pkg is
function a return tt pipelined is
v_tr tr;
begin
v_tr.id := 1; v_tr.name := 'Mark'; v_tr.sal := 100;
pipe row (v_tr);
v_tr.id := 2; v_tr.name := 'Pete'; v_tr.sal := 120;
pipe row (v_tr);
return;
end;
function b(par in tt) return tt pipelined is
v_tr tr;
begin
for i in 1..par.last loop
v_tr := par(i);
v_tr.sal := v_tr.sal * 10;
pipe row (v_tr);
end loop;
return;
end;
end pkg;
And this test worked for me:
select * from table(pkg.b(pkg.a));
Result:
ID NAME SAL
------ ---------- ----------
1 Mark 1000
2 Pete 1200

What is wrong with this PL/SQL function?

I have a function declared in as follows:
FUNCTION NewLogEntry(
is_warning IN NUMBER,
log_msg IN VARCHAR2) RETURN log_entry
IS
e log_entry;
BEGIN
e.is_warning := is_warning;
e.log_msg := log_msg;
return(e);
END NewLogEntry;
TYPE log_array IS VARRAY(5000) OF log_entry;
Of course TYPE log_array is not part of the function, but it is on a line that gives the compile error:
PLS-00103: Encountered the symbol "TYPE" when expecting one of the following: begin function pragma procedure
BTW, log_entry is declared as:
TYPE log_entry IS RECORD
(
is_warning BOOLEAN,
log_msg VARCHAR2(2000)
);
What is wrong with my function syntax?
If you want a type declared (as a RECORD) that your function can see, you'll need it in a package definition, eg
create or replace
package MY_TYPES is
TYPE log_entry IS RECORD
(
is_warning BOOLEAN,
log_msg VARCHAR2(2000)
);
end;
and then you can do
create or replace
FUNCTION NewLogEntry(
is_warning IN NUMBER,
log_msg IN VARCHAR2) RETURN MY_TYPES.log_entry
IS
e MY_TYPES.log_entry;
BEGIN
e.is_warning := is_warning;
e.log_msg := log_msg;
return(e);
END NewLogEntry;
There are other ways to do this, but this should get you moving. Here's some output
SQL> create or replace
2 package MY_TYPES is
3
4 TYPE log_entry IS RECORD
5 (
6 is_warning BOOLEAN,
7 log_msg VARCHAR2(2000)
8 );
9
10 end;
11 /
Package created.
SQL>
SQL> create or replace
2 FUNCTION NewLogEntry(
3 is_warning IN boolean,
4 log_msg IN VARCHAR2) RETURN MY_TYPES.log_entry
5 IS
6 e MY_TYPES.log_entry;
7 BEGIN
8 e.is_warning := is_warning;
9 e.log_msg := log_msg;
10 return(e);
11 END NewLogEntry;
12 /
Function created.

Return collection from packaged function for use in select

I'm currently using this block of code to return a collection of rows from my function.
--Source: http://www.adp-gmbh.ch/ora/plsql/coll/return_table.html
create or replace type t_col as object (
i number,
n varchar2(30)
);
/
create or replace type t_nested_table as table of t_col;
/
create or replace function return_table return t_nested_table as
v_ret t_nested_table;
begin
v_ret := t_nested_table();
v_ret.extend;
v_ret(v_ret.count) := t_col(1, 'one');
v_ret.extend;
v_ret(v_ret.count) := t_col(2, 'two');
v_ret.extend;
v_ret(v_ret.count) := t_col(3, 'three');
return v_ret;
end return_table;
/
Which I call by issuing SQL
select * from table(return_table);
Object types can not be defined in a package, I tried using the record type which worked (in PL/SQL) but I couldn't select from it in the same way as I can here.
How do I achieve this result using a function inside a package?
You could either use SQL objects inside your package or use pipelined functions (tested with 10gr2). Using SQL objects is straightforward, your actual function could be used as is inside a package.
Here's how you could use a pipelined function with a RECORD type:
SQL> CREATE OR REPLACE PACKAGE my_pkg IS
2 TYPE t_col IS RECORD(
3 i NUMBER,
4 n VARCHAR2(30));
5 TYPE t_nested_table IS TABLE OF t_col;
6 FUNCTION return_table RETURN t_nested_table PIPELINED;
7 END my_pkg;
8 /
Package created
SQL> CREATE OR REPLACE PACKAGE BODY my_pkg IS
2 FUNCTION return_table RETURN t_nested_table PIPELINED IS
3 l_row t_col;
4 BEGIN
5 l_row.i := 1;
6 l_row.n := 'one';
7 PIPE ROW(l_row);
8 l_row.i := 2;
9 l_row.n := 'two';
10 PIPE ROW(l_row);
11 RETURN;
12 END;
13 END my_pkg;
14 /
Package body created
SQL> select * from table(my_pkg.return_table);
I N
---------- ------------------------------
1 one
2 two
What happens behind the scene is that Oracle understands that since you want to use your function in a query (because of the PIPELINED keyword), you will need SQL objects, so those objects are created behind the scene for you:
SQL> select object_name
2 from user_objects o
3 where o.created > sysdate - 1
4 and object_type = 'TYPE';
OBJECT_NAME
--------------------------------------------------------------------------------
SYS_PLSQL_798806_24_1
SYS_PLSQL_798806_DUMMY_1
SYS_PLSQL_798806_9_1
SQL> select text from user_source where name='SYS_PLSQL_798806_9_1';
TEXT
--------------------------------------------------------------------------------
type SYS_PLSQL_798806_9_1 as object (I NUMBER,
N VARCHAR2(30));
create or replace type t_col as object (
i number,
n varchar2(30)
);
/
create or replace package foo as
type t_nested_table is table of t_col;
function return_table return t_nested_table pipelined;
end;
/
show errors
create or replace package body foo as
data t_nested_table := t_nested_table(t_col(1, 'one'),
t_col(2, 'two'),
t_col(3, 'three'));
function return_table return t_nested_table pipelined as
begin
for i in data.first .. data.last loop
pipe row(data(i));
end loop;
return;
end;
end;
/
show errors
column n format a10
select * from table(foo.return_table);
I N
---------- ----------
1 one
2 two
3 three

Checking if a collection element exists in Oracle

I create a simple type:
create or replace TYPE SIMPLE_TYPE AS OBJECT (ID NUMBER(38), NAME VARCHAR2(20));
Simple test:
DECLARE
TYPE ObjectList IS TABLE OF SIMPLE_TYPE;
tmp SIMPLE_TYPE := SIMPLE_TYPE(1, 'a');
o ObjectList := new ObjectList(SIMPLE_TYPE(2, 'a'), SIMPLE_TYPE(3, 'a'));
BEGIN
IF tmp.EXISTS(tmp) THEN
dbms_output.put_line('OK, exists.');
END IF;
END;
I get an exception: PLS-00302: component 'EXISTS' must be declared
But this example work:
DECLARE
TYPE NumList IS TABLE OF INTEGER;
n NumList := NumList(1,3,5,7);
BEGIN
n.DELETE(2);
IF n.EXISTS(1) THEN
dbms_output.put_line('OK, element #1 exists.');
END IF;
IF n.EXISTS(3) = FALSE THEN
dbms_output.put_line('OK, element #2 has been deleted.');
END IF;
IF n.EXISTS(99) = FALSE THEN
dbms_output.put_line('OK, element #99 does not exist at all.');
END IF;
END;
Is it possible to implement EXISTS method in SIMPLE_TYPE type?
As the documentation states, EXISTS() tests for the existence of a numbered entry in a collection. That is, array.exists(3) asserts that the third element of array is populated.
What you are trying to do in your first example is test whether the instance tmp matches an element in ObjectList. From 10g onwards we can do this using the MEMBER OF syntax. Unfortunately, in order to make that work we have to declare a MAP method, which is rather clunky and would get rather annoying if the object has a lot of attributes.
SQL> create or replace type simple_type as object
2 ( id number
3 , name varchar2(30)
4 , map member function compare return varchar2);
5 /
Type created.
SQL>
SQL> create or replace type body simple_type as
2 map member function compare return varchar2
3 is
4 return_value integer;
5 begin
6 return to_char(id, '0000000')||name;
7 end compare;
8 end;
9 /
Type body created.
SQL>
Running the example...
SQL> set serveroutput on size unlimited
SQL>
SQL> declare
2 type objectlist is table of simple_type;
3 tmp simple_type := simple_type(1, 'a');
4 o objectlist := new objectlist(simple_type(2, 'a'), simple_type(3, 'a'));
5 begin
6 if tmp MEMBER OF o then
7 dbms_output.put_line('ok, exists.');
8 else
9 dbms_output.put_line('search me');
10 end if;
11 end;
12 /
search me
PL/SQL procedure successfully completed.
SQL>
tmp SIMPLE_TYPEE := SIMPLE_TYPE(1, 'a');
…
IF tmp.EXISTS(tmp) THEN
You declare tmp as SIMPLE_TYPE, not ObjectList.
SIMPLE_TYPE is scalar type, not a collection.
Probably you wanted to check o.EXISTS instead (which is an ObjectList)?
Update:
EXISTS when applied to a collection takes an integer index as an argument and checks if the element with this index exists (not its value).
To check that SIMPLE_TYPE(1, 'a') exists in your table, you should so the following:
Create ObjectList in a dictionary:
CREATE TYPE ObjectList IS TABLE OF SIMPLE_TYPE;
Issue the SELECT query:
DECLARE
tmp SIMPLE_TYPE := SIMPLE_TYPE(1, 'a');
o ObjectList := new ObjectList(SIMPLE_TYPE(2, 'a'), SIMPLE_TYPE(3, 'a'));
myid INT;
BEGIN
SELECT 1
INTO myid
FROM TABLE(o) q
WHERE SIMPLE_TYPE(q.id, q.name) = tmp
AND rownum = 1;
IF (myid = 1) THEN
dbms_output.put_line('OK, exists.');
END IF;
END;

Resources