Can I create a "table-valued function" in an Oracle package? - sql

I'm relatively new to the Oracle world. Most of my experience is with SQL Server.
I am writing code that would benefit from a "parameterized view", aka a "table-valued function" (tvf) in SQL Server.
I found a good example here that I'm trying to follow: Oracle: Return a «table» from a function
But I need mine to be inside a package, and I'm having a devil of a time with it.
Here's an example of what I'm trying:
CREATE OR REPLACE PACKAGE pkg_test_oracle_tvfs IS
TYPE t_tvf_row IS RECORD(
i NUMBER,
n VARCHAR2(30));
TYPE t_tvf_tbl IS TABLE OF t_tvf_row INDEX BY BINARY_INTEGER;
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl;
END pkg_test_oracle_tvfs;
CREATE OR REPLACE PACKAGE BODY pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl IS
v_tvf_tbl t_tvf_tbl;
BEGIN
SELECT pkg_test_oracle_tvfs.t_tvf_row(rownum,
uo.object_name)
BULK COLLECT
INTO v_tvf_tbl
FROM user_objects uo
WHERE rownum <= p_max_num_rows;
RETURN v_tvf_tbl;
END;
END pkg_test_oracle_tvfs;
With the intent that I can do something like:
SELECT * FROM pkg_test_oracle_tvfs.fn_get_tvf(5);
Or
SELECT * FROM TABLE(pkg_test_oracle_tvfs.fn_get_tvf(5));
(I'm unclear if the TABLE() is required.)
But when I compile the package I get:
Compilation errors for PACKAGE BODY XXX.PKG_TEST_ORACLE_TVFS
Error: PL/SQL: ORA-00913: too many values
Line: 11
Text: FROM user_objects uo
Error: PL/SQL: SQL Statement ignored
Line: 7
Text: SELECT pkg_test_oracle_tvfs.t_tvf_row(rownum,
What am I doing wrong here? Why does this syntax seem to work fine outside of a package but not inside one?
Do I need to use the "pipeline" style of constructing the table as described in Oracle: Pipelined PL/SQL functions If so, why is this example different than the one I've been trying to follow?
Thanks!

Your initial error is because you're selecting into a record type, not an object type, so you don't need the constructor:
SELECT rownum, uo.object_name
BULK COLLECT
INTO v_tvf_tbl
fiddle, which shows it now compiles, but you can't call it from SQL for the reason's MTO already explained.
As an alternative to creating an object type, you can as you suggested use a pipelined function, if you modify the collection type:
CREATE OR REPLACE PACKAGE pkg_test_oracle_tvfs IS
TYPE t_tvf_row IS RECORD(
i NUMBER,
n VARCHAR2(30));
TYPE t_tvf_tbl IS TABLE OF t_tvf_row;
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl PIPELINED;
END pkg_test_oracle_tvfs;
/
CREATE OR REPLACE PACKAGE BODY pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl PIPELINED IS
v_tvf_tbl t_tvf_tbl;
BEGIN
SELECT rownum, uo.object_name
BULK COLLECT
INTO v_tvf_tbl
FROM user_objects uo
WHERE rownum <= p_max_num_rows;
FOR i IN 1..v_tvf_tbl.COUNT LOOP
PIPE ROW (v_tvf_tbl(i));
END LOOP;
RETURN;
END;
END pkg_test_oracle_tvfs;
/
SELECT * FROM pkg_test_oracle_tvfs.fn_get_tvf(5);
I
N
1
PKG_TEST_ORACLE_TVFS
2
PKG_TEST_ORACLE_TVFS
SELECT * FROM TABLE(pkg_test_oracle_tvfs.fn_get_tvf(5));
I
N
1
PKG_TEST_ORACLE_TVFS
2
PKG_TEST_ORACLE_TVFS
fiddle

There is a fundamental flaw; both RECORDs and associative arrays (TABLE OF ... INDEX BY ...) are PL/SQL only data types and cannot be used in SQL statements.
If you want to use a record-like and array-like data structure in an SQL statement then you will need to define it in the SQL scope which means that you cannot define it in a package and would need to use an OBJECT type and a nested-table collection type:
CREATE TYPE t_tvf_row IS OBJECT(
i NUMBER,
n VARCHAR2(30)
);
CREATE TYPE t_tvf_tbl IS TABLE OF t_tvf_row;
Then:
CREATE OR REPLACE PACKAGE pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(
p_max_num_rows INTEGER
) RETURN t_tvf_tbl;
END pkg_test_oracle_tvfs;
/
CREATE OR REPLACE PACKAGE BODY pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(
p_max_num_rows INTEGER
) RETURN t_tvf_tbl
IS
v_tvf_tbl t_tvf_tbl;
BEGIN
SELECT t_tvf_row(
rownum,
object_name
)
BULK COLLECT INTO v_tvf_tbl
FROM (
SELECT object_name
FROM user_objects
ORDER BY object_name
)
WHERE rownum <= p_max_num_rows;
RETURN v_tvf_tbl;
END;
END pkg_test_oracle_tvfs;
/
fiddle
why is this example different than the one I've been trying to follow?
Because you are defining data-types in a PL/SQL scope (a package) that can only be used in PL/SQL (because records and associative arrays are PL/SQL-only data types) and then trying to use them in an SQL scope (a SELECT statement). The example you are following defines the data types as an OBJECT and a non-associative array and defines them in the SQL scope (outside of a package) and then using them in an SQL statement is allowable.

Related

How to define a type within a with block?

This code works:
WITH
FUNCTION a (a IN INTEGER)
RETURN INTEGER
IS
BEGIN
RETURN a + 1;
END;
b(v) AS (SELECT column_value FROM sys.ODCINUMBERLIST(1,2,3))
SELECT a (v) FROM b;
But I would like to define a type in this with statement. Later I want to reuse this type in order to use a pipelined function. Therefore I will need a type which is a table of a record. And the type must be defined outside of the function (not inside the function ) because the type will be returned by the function
I tried with this simple type w.
WITH
type w is record(w1 integer);
FUNCTION f (a in integer)
RETURN INTEGER
IS
ret integer;
BEGIN
return 2;
END;
B(b1) as (select 1 from dual)
select f(3) from dual;
It doesn't work:
[Error] Compilation (3: 5): ORA-00900: invalid SQL statement
Is it possible to define a type within a with statement and how can I do that?
The return data type of a function used in a SQL context (as opposed to PL/SQL) must be either a native SQL type or a schema-level user-defined type (a type defined on its own, with the create type statement, rather than defined in a package).
This restriction was not lifted when support for functions defined in the with clause was added in Oracle 12.1 - not even for the data type of a function so defined. In particular, the return data type can't be defined in the with clause of a SQL statement.
Then, records are supported only in PL/SQL; at the schema level, you will need to create an object type, rather than a record type.
NOTE: A pipelined function returning a collection of records (with the record type defined in a package) can be used in a SQL context; the reason is that Oracle defines a corresponding object type, at the schema level, behind the scenes, and takes care of the conversion. Why Oracle doesn't do the same for all functions is something only Oracle can explain. Of course, as you are finding out in another thread you started, pipelined table functions in the with clause are not supported (even though non-pipelined table functions are!)
We don't know the actual problem you are trying to solve, but it seems unlikely that you will be able to do everything you are trying to do in the with clause.
You cannot.
If you look at the SELECT documentation, particularly the syntax diagrams:
with_clause::=
plsql_declarations::=
You can see that you can only declare a function or a procedure.
If you try:
WITH FUNCTION f (a in integer)
RETURN w
IS
type w is record(w1 integer);
BEGIN
return w(2);
END;
SELECT f(3)
FROM DUAL;
You get the error:
ORA-06553: PLS-313: 'F' not declared in this scope
ORA-06552: PL/SQL: Item ignored
ORA-06553: PLS-498: illegal use of a type before its declaration
You cannot fix that error but, if you magically could (which you cannot using only local declarations), you would get a second error as a RECORD is a PL/SQL only data type and you are then trying to use it in an SQL scope.
For example, if you declared the type globally in a PL/SQL package:
CREATE PACKAGE pkg AS
type w is record(w1 integer);
END;
/
WITH FUNCTION f (a in integer)
RETURN pkg.w
IS
BEGIN
return pkg.w(2);
END;
SELECT f(3).w1
FROM DUAL;
The query gives the error:
ORA-00902: invalid datatype
You would need to use an SQL OBJECT type and declare it in the global SQL scope before running your query.
For example:
CREATE TYPE w IS OBJECT(w1 INTEGER);
WITH FUNCTION f (a in integer)
RETURN w
IS
BEGIN
return w(2);
END;
SELECT f(3).w1
FROM DUAL;
Outputs:
F(3).W1
2
To use a RECORD, you need to declare the type in a PL/SQL package or in a PL/SQL anonymous block and then use it only in PL/SQL.
For example, if you just want to run your function using a locally declared PL/SQL type then you can do it entirely in a PL/SQL anonymous block:
DECLARE
TYPE w IS record(w1 integer);
v_w w;
FUNCTION f (a in integer)
RETURN w
IS
BEGIN
return w(2);
END f;
BEGIN
v_w := f(3);
DBMS_OUTPUT.PUT_LINE(v_w.w1);
END;
/
Outputs:
2
db<>fiddle here
Your code will work if you define the type within the function like so:
WITH
FUNCTION f (a in integer)
RETURN INTEGER
IS
ret integer;
type w is record(w1 integer);
BEGIN
return 2;
END;
B(b1) as (select 1 from dual)
select f(3) from dual;

How to use one sql parameter to represent input array

Is there a way to write sql for Oracle, MS SQL:
Select * from table where id in(:arr)
Select * from table where id in(#arr)
With one param in sql 'arr' to represent an array of items?
I found examples that explode arr to #arr0,.., #arrn and feed array as n+1 separate parameters, not array, like this
Select * from table where id in(:arr0, :arr1, :arr2)
Select * from table where id in(#arr0, #arr1, #arr2)
Not what i want.
These will cause change in sql query and this creates new execution plans based on number of parameter.
I ask for .net, c# and Oracle and MS SQL.
Thanks for constructive ideas!
/ip/
I believe Table Value Parameter is good option for this case. Have a look at a sample code below in SQL Server.
-- Your table
CREATE TABLE SampleTable
(
ID INT
)
INSERT INTO SampleTable VALUES
(1010),
(2010),
(3010),
(4010),
(5010),
(6010),
(7010),
(8030)
GO
-- Create a TABLE type in SQL database which you can fill from front-end code
CREATE TYPE ParameterTableType AS TABLE
(
ParameterID INT
--, some other columns
)
GO
-- Create a stored proc using table type defined above
CREATE PROCEDURE ParameterArrayProcedure
(
#ParameterTable AS ParameterTableType READONLY
)
AS
BEGIN
SELECT
S.*
FROM SampleTable S
INNER JOIN #ParameterTable P ON S.ID = P.ParameterID
END
GO
-- Populated table type variable
DECLARE #ParameterTable AS ParameterTableType
INSERT INTO #ParameterTable (ParameterID) VALUES (1010), (4010), (7010)
EXECUTE ParameterArrayProcedure #ParameterTable
DROP PROCEDURE ParameterArrayProcedure
DROP TYPE ParameterTableType
DROP TABLE SampleTable
GO
Apart from Table Value Parameter, you can also use Json or XML values as SQL parameter but yes, it will definitely change your execution plan accordingly.
In addition to a Table Valued Parameter as Steve mentioned, there are a couple of other techniques available. For example you can parse a delimited string
Example
Declare #arr varchar(50) = '10,20,35'
Select A.*
From YourTable A
Join string_split(#arr,',') B on A.ID=value
Or even
Select A.*
From YourTable A
Where ID in ( select value from string_split(#arr,',') )
Oracle
In other languages (i.e. Java) you can pass an SQL collection as a bind parameter and directly use it in an SQL statement.
However, C# does not support passing SQL collections and only supports passing OracleCollectionType.PLSQLAssociativeArray (documentation link) which is a PL/SQL only data-type and cannot be used (directly) in SQL statements.
To pass an array, you would need to pass a PLSQLAssociativeArray to a PLSQL stored procedure and use that to convert it to an SQL collection that you can use in an SQL statement. An example of a procedure to convert from a PL/SQL associative array to an SQL collection is:
CREATE TYPE IntList AS TABLE OF INTEGER
/
CREATE PACKAGE tools IS
TYPE IntMap IS TABLE OF INTEGER INDEX BY BINARY_INTEGER;
FUNCTION IntMapToList(
i_map IntMap
) RETURN IntList;
END;
/
CREATE PACKAGE BODY tools IS
FUNCTION IntMapToList(
i_map IntMap
) RETURN IntList
IS
o_list IntList := IntList();
i BINARY_INTEGER;
BEGIN
IF i_map IS NOT NULL THEN
i := o_list.FIRST;
WHILE i IS NOT NULL LOOP
o_list.EXTEND;
o_list( o_list.COUNT ) := i_map( i );
i := i_map.NEXT( i );
END LOOP;
END IF;
RETURN o_list;
END;
END;
/

How return a list in PL/SQL

I'm trying to return a list of char in a function PL/SQL with Oracle 11, but without succes.
I've got a few difficulties for understand their running...
For example, i have this code created for test:
create type test is table of varchar(500);
/
CREATE OR REPLACE FUNCTION test2 (id INT)
RETURN test
is
tt_t test;
BEGIN
SELECT descriptifSpecifique INTO tt_t(1)
FROM DECOMPOSE
where idRecette=id
AND idEtape=2;
SELECT descriptifSpecifique INTO tt_t(2)
FROM DECOMPOSE
where idRecette=id
AND idEtape=3;
RETURN tt_t;
END;
/
show errors function test;
The fonction is created without compilation's problem, but at the execution, I have this message: ORA-06531: Reference to uninitialized collection.
Also, how can I return a type (with a varchar and a int generated by a select for example) IN PL/SQL. Because when I try to make a declaration of type with RECORD, and RETURN this type, I have compilation's problem because type is not declarated...
Thank you
You are basically doing it correctly. But you need to EXTEND your collection before you put new elements into it.
Personally, I prefer to BULK COLLECT into the collection to avoid having to EXTEND and manage the entries at each index. Like this (code untested):
CREATE OR REPLACE FUNCTION test2 (id INT)
RETURN test
is
tt_t test;
BEGIN
SELECT descriptifSpecifique
BULK COLLECT INTO tt_t
FROM DECOMPOSE
where idRecette=id
AND idEtape IN (2,3)
ORDER BY idEtape;
RETURN tt_t;
END;
/
To return a TYPE having multiple columns, you need to create two types: an OBJECT type and a TABLE OF that object type.
Like so,
CREATE TYPE test_rec IS OBJECT ( a_varchar VARCHAR2(500), a_number NUMBER);
CREATE TYPE test_tbl IS TABLE OF test_rec;
Then, you can modify your function accordingly:
CREATE OR REPLACE FUNCTION test2 (id INT)
RETURN test_tbl
is
tt_t test_tbl;
BEGIN
SELECT test_rec(idEtape, descriptifSpecifique)
BULK COLLECT INTO tt_t
FROM DECOMPOSE
where idRecette=id
AND idEtape IN (2,3)
ORDER BY idEtape;
RETURN tt_t;
END;
/

Oracle SQL Create function or procedure returning a table

In SQL Server, I can just use 'RETURNS TABLE' and it will do the job. But I can't find how to do the same in Oracle SQL
I have the following SELECT statement that needs to be put in a function or procedure:
SELECT a.CodAcord, a.Descr
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.codSoce = codSoce;
codSoce is an INTEGER IN parameter, and I need to return a.CodAcord and a.Descr as result from my function/procedure.
Is there a simple way to do this? Without having to deal with temp variables and/or advanced content...
EDIT: Aditional info:
- I need to return a.CodAcord and a.Descr, but when I did some research to know how to return more than one variable using SQL Functions or Procedures, all I could find was that this was only possible by returning a TABLE. If there's a way to return more than one item from a Function or Procedure, please let me know.
- The use of Functions or Procedures is strictly required.
- I'm using SQL Developer.
You can achieve a table as a return value from function by using a pipelined table function. Please see the example below:
-- Create synthetic case
CREATE TABLE Acord AS
SELECT rownum CodAcord, 'Description ' || rownum Descr
FROM dual CONNECT BY LEVEL <= 5;
CREATE TABLE FreqSoce AS
SELECT rownum CodSoce, rownum CodAcord
FROM dual CONNECT BY LEVEL <= 10;
-- Test dataset
SELECT a.CodAcord, a.Descr
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.CodSoce = 10;
-- Here begins actual code
-- Create an object type to hold each table row
CREATE OR REPLACE TYPE typ_acord AS OBJECT(
CodAcord NUMBER,
Descr VARCHAR2(40)
);
/
-- Create a collection type to hold all result set rows
CREATE OR REPLACE TYPE tab_acord AS TABLE OF typ_acord;
/
-- Our function that returns a table
CREATE OR REPLACE FUNCTION getAcord(pCodSoce IN NUMBER)
RETURN tab_acord PIPELINED
AS
BEGIN
FOR x IN (SELECT a.CodAcord, a.Descr
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.CodSoce = pCodSoce)
LOOP
PIPE ROW (typ_acord(x.CodAcord, x.Descr));
END LOOP;
END;
/
-- Testing the function (please note the TABLE operator)
SELECT * FROM TABLE(getAcord(5));
Take the following as a code template:
CREATE OR REPLACE PACKAGE tacord AS
TYPE ttabAcord IS TABLE OF ACord%ROWTYPE;
END tacord;
/
show err
CREATE OR REPLACE PACKAGE BODY tacord AS
BEGIN
NULL;
END tacord;
/
show err
CREATE OR REPLACE FUNCTION demo RETURN tacord.ttabAcord AS
to_return tacord.ttabAcord;
BEGIN
SELECT a.*
BULK COLLECT INTO to_return
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.codSoce = codSoce
;
RETURN to_return;
END demo;
/
show err
Key points:
%ROWTYPE represents the datatype of a database table's record
BULK COLLECT INTO inserts a complete result set into a plsql data structure
Iteration over the table contents is availabel by the plsql collection methods, namely .FIRST, .LAST, .NEXT.

Where column in <function returns a collection > in oracle

I want to achieve something like this:
Example:
Table Customers has columns customer_no, name , age.
some_package package has the following types defined in its spec:
type cust_type is record (custs Customers.customer_no);
type rec_type is table of cust_type index by binary_integer;
function some_function return rec_type;
I am trying to create a view the goes like this:
select ....
from customers c, tablex, tabley
where c.customer_no in some_function() and
... <<other clauses>>
I cannot avoid using some_function() as the logic uses dynamic SQL statements.
I get invalid data type error when i try to compile the view
Is it possible to achieve this in Oracle sql? I don't want to use another function and loops to do this.
Thanks.
No, type rec_type is declared in a package and can be used in PL/SQL blocks but cannot be used in SQL statements.
If you want to use a type in SQL statements then you will need to declare it using a CREATE TYPE statement like this:
CREATE TYPE customers_tab IS TABLE OF NUMBER;
or you can use an existing type like SYS.ODCINUMBERLIST.
Then change the package to:
CREATE OR REPLACE PACKAGE some_package
AS
FUNCTION some_function RETURN SYS.ODCINUMBERLIST;
END;
/
CREATE OR REPLACE PACKAGE BODY some_package
AS
FUNCTION some_function RETURN SYS.ODCINUMBERLIST
AS
t_customers SYS.ODCINUMBERLIST;
BEGIN
SELECT customer_no
BULK COLLECT INTO t_customers
FROM customers
WHERE MOD( customer_no, 3 ) = 0; -- or whatever your query is.
RETURN t_customers;
END;
END;
/
Then you can do:
SELECT *
FROM Customers c
INNER JOIN
TABLE( some_package.some_function() ) t
ON ( c.customer_no = t.COLUMN_VALUE );