How to define a pipelined table function within a with statement? - oracle

It's possible to defined a function within a with statement (How to define a function and a query in the same with block?). But I fail to define a pipelined function inside a with statement.
WITH
FUNCTION f (a IN INTEGER)
RETURN SYS.odcinumberlist
PIPELINED
IS
ret INTEGER;
BEGIN
FOR z IN 1 .. a
LOOP
PIPE ROW (z);
END LOOP;
RETURN;
END;
SELECT * FROM f(3); --I tried with table(f(3)) too
[Error] Execution (2: 1): ORA-06553: PLS-653: aggregate/table functions are not allowed in PL/SQL scope
Is it possible to define a pipelined table function within a with statement and how?
If it is possible, I would like to do that with a table of record. Therefore I need to know how defined type within a with statement too.

Within a subquery-factoring clause, you can make a non-pipelined function that returns a collection:
WITH FUNCTION f (a IN INTEGER)
RETURN SYS.ODCINUMBERLIST
IS
v_list SYS.ODCINUMBERLIST := SYS.ODCINUMBERLIST();
BEGIN
FOR z IN 1 .. a LOOP
v_list.EXTEND;
v_list(v_list.COUNT) := z;
END LOOP;
RETURN v_list;
END f;
SELECT *
FROM TABLE(f(3));
Which outputs:
COLUMN_VALUE
1
2
3
db<>fiddle here
If it is possible, I would like to do that with a table of record.
A record is a PL/SQL data type; you cannot use it in an SQL statement. Instead use an OBJECT data type and declare it in the global SQL scope before you run your query (no, you cannot declare it locally inside your query).
Therefore I need to know how defined type within a with statement too.
Again, you cannot as the SQL syntax does not allow it. See the answer to your previous question.
You can declare it globally:
CREATE TYPE test_obj IS OBJECT (
w1 INTEGER
);
CREATE TYPE test_obj_table IS TABLE OF test_obj;
WITH FUNCTION f (a IN INTEGER)
RETURN test_obj_table
IS
v_list test_obj_table := test_obj_table();
BEGIN
FOR z IN 1 .. a LOOP
v_list.EXTEND;
v_list(v_list.COUNT) := test_obj(z);
END LOOP;
RETURN v_list;
END f;
SELECT *
FROM TABLE(f(3));
Outputs:
W1
1
2
3
db<>fiddle here

Related

How to select a record defined in a package in sql query

to test the code:
https://dbfiddle.uk/?rdbms=oracle_21&fiddle=b02671a25f9d7949e0b55ca59084ecd1
If I define a record as an object, I can call it in a sql statement this way objectname(field1, field2)
But If I define a record inside package as a record. I can't do that anymore.
create TYPE arguments_r IS object
(
q integer,
b INTEGER
);
CREATE FUNCTION f (p IN arguments_r) RETURN INTEGER
IS
BEGIN
RETURN 1;
END;
/
select arguments_r(1,1) from dual -- not printed but exists nevertheless. THe following statement prove it.
select f(arguments_r(1,1)) from dual --print the expected result
CREATE PACKAGE pck
as
TYPE arguments_r IS record
(
q integer,
b INTEGER
);
FUNCTION f (p IN pck.arguments_r) RETURN INTEGER;
end;
CREATE PACKAGE body pck
as
FUNCTION f (p IN pck.arguments_r) RETURN INTEGER
is
begin
return 1;
end;
END;
select pck.arguments_r(1,1) from dual -- ORA-06553: PLS-306: wrong number or types of arguments in call to 'ARGUMENTS_R'
[TL;DR] In general, you cannot.
A record is a PL/SQL ONLY data type and it CANNOT be used in SQL. If you want to use the data in SQL then you will need to put it into an SQL data type such as an Object.
There are ways of achieving what you want via PIPELINED functions (which implicitly convert a RECORD to an OBJECT so, technically you never use a PL/SQL data type in the SQL scope but it certainly looks like you do) but the implementation is convoluted and if I ever did a code review on code that tried to use records the way I show below then I would fail it in the review and tell the author to just use an SQL OBJECT.
Given the setup:
CREATE PACKAGE pck as
TYPE args_r IS record (
q integer,
b INTEGER
);
TYPE args_t IS TABLE OF args_r;
FUNCTION f RETURN args_t PIPELINED;
FUNCTION f2 RETURN args_t;
FUNCTION create_arg(q INTEGER, b INTEGER) RETURN args_t PIPELINED;
FUNCTION create_arg2(q INTEGER, b INTEGER) RETURN args_t;
FUNCTION g(args IN args_r) RETURN INTEGER;
END;
/
CREATE PACKAGE BODY pck as
FUNCTION f RETURN args_t PIPELINED
IS
BEGIN
PIPE ROW (args_r(1,1));
PIPE ROW (args_r(2,2));
END;
FUNCTION f2 RETURN args_t
IS
BEGIN
RETURN args_t(args_r(1,1), args_r(2,2));
END;
FUNCTION create_arg(q INTEGER, b INTEGER) RETURN args_t PIPELINED
IS
BEGIN
PIPE ROW (args_r(q, b));
END;
FUNCTION create_arg2(q INTEGER, b INTEGER) RETURN args_t
IS
BEGIN
RETURN args_t(args_r(q, b));
END;
FUNCTION g(args IN args_r) RETURN INTEGER
IS
BEGIN
RETURN args.q;
END;
END;
/
From PL/SQL to SQL via a PIPELINED function:
If you want to pass PL/SQL records from a PL/SQL function into an SQL query then it will "work" (see the comment below for clarification of why) if you use a PIPELINED function then:
SELECT *
FROM TABLE(pck.f());
Outputs:
Q
B
1
1
2
2
BUT that is not because you can use PL/SQL records in SQL it is because a PIPELINED function is intended to be used in the SQL scope so Oracle has implicitly created a duplicate OBJECT data type (and TABLE OF ... data type) and mapped the PL/SQL record to the SQL object and despite what the signature of the function says, it is not returning records and returning Objects instead.
From PL/SQL to SQL via a function returning a collection:
If you try to do exactly the same thing with a non-PIPELINED function:
SELECT *
FROM TABLE(pck.f2());
Then you get the error:
ORA-00902: invalid datatype
Because no such implicit conversion has been applied and you CANNOT use PL/SQL record types in SQL.
From SQL to PL/SQL:
Going the other way, as per the question, and trying to create records in the SQL scope and pass them to a PL/SQL function then it CANNOT work because it is impossible to create PL/SQL records in the SQL scope.
The query:
SELECT pck.g(pck.args_r(1,1))
FROM DUAL;
Fails with the error:
ORA-06553: PLS-306: wrong number or types of arguments in call to 'ARGS_R'
From PL/SQL to SQL back to PL/SQL via a PIPELINED function:
If you create the record in PL/SQL and return it via a PIPELINED function then it "works":
SELECT pck.g((SELECT VALUE(t) FROM TABLE(pck.create_arg(1,1)) t))
FROM DUAL;
and outputs:
PCK.G((SELECTVALUE(T)FROMTABLE(PCK.CREATE_ARG(1,1))T))
1
From PL/SQL to SQL back to PL/SQL via a function returning a collection:
However, if you use a non-PIPELINED function:
SELECT pck.g((SELECT VALUE(t) FROM TABLE(pck.create_arg2(1,1)) t))
FROM DUAL;
Then it raises the exception:
ORA-00902: invalid datatype
db<>fiddle here
Conclusion:
Just define an OBJECT data type; do not try to use convoluted methods to ram PL/SQL only data types into an SQL scope where they are not meant to be used.

inner use of cursor outside declare block

I have to use a cursor inside a particular block which contains query with WHERE clause getting parameters dynamically...
Therefore how to use a cursor without declaring in DECLARE block.
(I can't use nested cursors - am looking for examples).
You could use an Implicit Cursor that uses a local parameter to your procedure. Here is how it looks like:
declare
v_parm varchar2(3) := 'Z';
begin
-- FIRST LOOP will display all objects owned by Z
for x in ( select * from all_objects where owner=v_parm) loop
dbms_output.put_line(x.object_name);
end loop;
-- SECOND LOOP will display all objects owned by SYS until 100 is reached
-- (that would be a huge bunch otherwise!)
v_parm := 'SYS';
for x in ( select * from all_objects where owner=v_parm and rowid < 100) loop
dbms_output.put_line(x.object_name);
end loop;
end;
/

Oracle PL/SQL array input into parameter of pipelined function

I am new to PL/SQL. I have created a pipelined function inside a package which takes as its parameter input an array of numbers (nested table).
But I am having trouble trying to run it via an sql query. Please see below
my input array
CREATE OR REPLACE TYPE num_array is TABLE of number;
my function declaration
CREATE OR REPLACE PACKAGE "my_pack" as
TYPE myRecord is RECORD(column_a NUMBER);
TYPE myTable IS TABLE of myRecord;
FUNCTION My_Function(inp_param num_array) return myTable PIPELINED;
end my_pack;
my function definition
CREATE OR REPLACE PACKAGE BODY "my_pack" as
FUNCTION My_Function(inp_param num_array) return myTable PIPELINED as
rec myRecord;
BEGIN
FOR i in 1..inp_param.count LOOP
FOR e IN
(
SELECT column_a FROM table_a where id=inp_param(i)
)
LOOP
rec.column_a := e.column_a;
PIPE ROW (rec);
END LOOP;
END LOOP;
RETURN;
END;
end my_pack;
Here is the latest code I've tried running from toad. But it doesn't work
declare
myarray num_array;
qrySQL varchar2(4000);
begin
myarray := num_array(6341,6468);
qrySQL := 'select * from TABLE(my_pack.My_Function(:myarray))';
execute immediate qrySQL;
end;
So my question is how can I feed an array into this pipelined function from either TOAD or SQL Developer. An example would be really handy.
Thanks
The error is fairly clear, you have a bind variable that you haven't assigned anything to. You need to pass your actual array with:
qrySQL := 'select * from TABLE(my_pack.My_Function(:myarray))';
execute immediate qrySQL using myarray;
It's maybe more useful, if you want to call it from PL/SQL, to use static SQL as a cursor:
set serveroutput on
declare
myarray num_array;
begin
myarray := num_array(6341,6468);
for r in (select * from TABLE(my_pack.My_Function(myarray))) loop
dbms_output.put_line(r.column_a);
end loop;
end;
/
Or just query it statically as a test, for fixed values:
select * from TABLE(my_pack.My_Function(num_array(6341,6468)));
SQL Fiddle with some minor tweaks to the function to remove errors I think came from editing to post.

Selecting Values from Oracle Table Variable / Array?

Following on from my last question (Table Variables in Oracle PL/SQL?)...
Once you have values in an array/table, how do you get them back out again? Preferably using a select statement or something of the like?
Here's what I've got so far:
declare
type array is table of number index by binary_integer;
pidms array;
begin
for i in (
select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
)
loop
pidms(pidms.count+1) := i.sgbstdn_pidm;
end loop;
select *
from pidms; --ORACLE DOESN'T LIKE THIS BIT!!!
end;
I know I can output them using dbms_output.putline(), but I'm hoping to get a result set like I would from selecting from any other table.
Thanks in advance,
Matt
You might need a GLOBAL TEMPORARY TABLE.
In Oracle these are created once and then when invoked the data is private to your session.
Oracle Documentation Link
Try something like this...
CREATE GLOBAL TEMPORARY TABLE temp_number
( number_column NUMBER( 10, 0 )
)
ON COMMIT DELETE ROWS;
BEGIN
INSERT INTO temp_number
( number_column )
( select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
);
FOR pidms_rec IN ( SELECT number_column FROM temp_number )
LOOP
-- Do something here
NULL;
END LOOP;
END;
/
In Oracle, the PL/SQL and SQL engines maintain some separation. When you execute a SQL statement within PL/SQL, it is handed off to the SQL engine, which has no knowledge of PL/SQL-specific structures like INDEX BY tables.
So, instead of declaring the type in the PL/SQL block, you need to create an equivalent collection type within the database schema:
CREATE OR REPLACE TYPE array is table of number;
/
Then you can use it as in these two examples within PL/SQL:
SQL> l
1 declare
2 p array := array();
3 begin
4 for i in (select level from dual connect by level < 10) loop
5 p.extend;
6 p(p.count) := i.level;
7 end loop;
8 for x in (select column_value from table(cast(p as array))) loop
9 dbms_output.put_line(x.column_value);
10 end loop;
11* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
SQL> l
1 declare
2 p array := array();
3 begin
4 select level bulk collect into p from dual connect by level < 10;
5 for x in (select column_value from table(cast(p as array))) loop
6 dbms_output.put_line(x.column_value);
7 end loop;
8* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
Additional example based on comments
Based on your comment on my answer and on the question itself, I think this is how I would implement it. Use a package so the records can be fetched from the actual table once and stored in a private package global; and have a function that returns an open ref cursor.
CREATE OR REPLACE PACKAGE p_cache AS
FUNCTION get_p_cursor RETURN sys_refcursor;
END p_cache;
/
CREATE OR REPLACE PACKAGE BODY p_cache AS
cache_array array;
FUNCTION get_p_cursor RETURN sys_refcursor IS
pCursor sys_refcursor;
BEGIN
OPEN pCursor FOR SELECT * from TABLE(CAST(cache_array AS array));
RETURN pCursor;
END get_p_cursor;
-- Package initialization runs once in each session that references the package
BEGIN
SELECT level BULK COLLECT INTO cache_array FROM dual CONNECT BY LEVEL < 10;
END p_cache;
/
The sql array type is not neccessary. Not if the element type is a primitive one. (Varchar, number, date,...)
Very basic sample:
declare
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
pidms TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into pidms
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH';
-- do something with pidms
open :someCursor for
select value(t) pidm
from table(pidms) t;
end;
When you want to reuse it, then it might be interesting to know how that would look like.
If you issue several commands than those could be grouped in a package.
The private package variable trick from above has its downsides.
When you add variables to a package, you give it state and now it doesn't act as a stateless bunch of functions but as some weird sort of singleton object instance instead.
e.g. When you recompile the body, it will raise exceptions in sessions that already used it before. (because the variable values got invalided)
However, you could declare the type in a package (or globally in sql), and use it as a paramter in methods that should use it.
create package Abc as
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList;
function Test1(list in TPidmList) return PLS_Integer;
-- "in" to make it immutable so that PL/SQL can pass a pointer instead of a copy
procedure Test2(list in TPidmList);
end;
create package body Abc as
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList is
result TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into result
from sgbstdn
where sgbstdn_majr_code_1 = majorCode
and sgbstdn_program_1 = program;
return result;
end;
function Test1(list in TPidmList) return PLS_Integer is
result PLS_Integer := 0;
begin
if list is null or list.Count = 0 then
return result;
end if;
for i in list.First .. list.Last loop
if ... then
result := result + list(i);
end if;
end loop;
end;
procedure Test2(list in TPidmList) as
begin
...
end;
return result;
end;
How to call it:
declare
pidms constant Abc.TPidmList := Abc.CreateList('HS04', 'HSCOMPH');
xyz PLS_Integer;
begin
Abc.Test2(pidms);
xyz := Abc.Test1(pidms);
...
open :someCursor for
select value(t) as Pidm,
xyz as SomeValue
from table(pidms) t;
end;

Oracle, calling PL/SQL issues from within SQL-Plus file x.sql says my_function "may not be a function"

so simple, if I create my function as CREATE OR REPLACE FUNCTION MD5_ENCODE it will run smoothly, but if it stays anonymously within the SQL-Plus block as PL/SQL --> "may not be a function" error.
What is this Oracle "feature" again?
DECLARE
FUNCTION MD5_ENCODE(CLEARTEXT IN VARCHAR2) RETURN VARCHAR2 IS
CHK VARCHAR2(16);
HEX VARCHAR2(32);
I INTEGER;
C INTEGER;
H INTEGER;
BEGIN
IF CLEARTEXT IS NULL THEN
RETURN '';
ELSE
CHK := DBMS_OBFUSCATION_TOOLKIT.MD5(INPUT_STRING
=> CLEARTEXT);
FOR I IN 1 .. 16 LOOP
C := ASCII(SUBSTR(CHK, I, 1));
H := TRUNC(C / 16);
IF H >= 10 THEN
HEX := HEX || CHR(H + 55);
ELSE
HEX := HEX || CHR(H + 48);
END IF;
H := MOD(C, 16);
IF H >= 10 THEN
HEX := HEX || CHR(H + 55);
ELSE
HEX := HEX || CHR(H + 48);
END IF;
END LOOP;
RETURN HEX;
END IF;
END;
BEGIN
UPDATE ADDRESSES_T SET STREET = MD5ENCODE(STREET) ;
-- etc...
END
http://forums.oracle.com/forums/thread.jspa?threadID=245112
There are a number of things it could be.
My #1 candidate is, we can only use functions in SQL statements that are public i.e. declared in a package spec. This is the case even for SQL statements executed within the same Package Body. The reson is that SQL statements are executed by a different engine which can only see publicly declared functions.
Long story short, function must be defined in a package, like CREATE OR REPLACE FUNCTION MD5ENCODE(IN_TEXT IN VARCHAR2) RETURN VARCHAR2 IS ...
In order to call a function within a SQL statement, it needs to exist as a function object within the database. The SQL statement is parsed and executed separately from your PL/SQL code, so it doesn't have access to the locally defined function.
If you really don't want to create the function object, you could use a cursor to loop over the rows in the table, execute the function in a PL/SQL statement, and update each row as you go.
Your first problem is you have a typo.
FUNCTION MD5_ENCODE(CLEARTEXT IN
VARCHAR2) RETURN VARCHAR2 IS
versus
UPDATE ADDRESSES_T SET STREET =
MD5ENCODE(STREET) ;
You are missing the underscore in the function call in the update statement.
The next error you will encounter is :
PLS-00231: function 'MD5_ENCODE' may not be used in SQL
So you can simply assign the function results to a variable and use it in the Update statement.
As it mentioned above comments, this could be useful.
DECLARE
V NUMBER := 0;
FUNCTION GET_SQ(A NUMBER) RETURN NUMBER AS
BEGIN
RETURN A * A;
END;
BEGIN
V := GET_SQ(5);
--DBMS_OUTPUT.PUT_LINE(V);
UPDATE MYTABLE A SET A.XCOL = V;
END;

Resources