Save and return multiple rows within function pl/sql oracle - sql

declare
type t_trayIds is table of number(38,0) index by binary_integer;
v_trayIdsTable t_trayIds;
create or replace function F_getTrayIdByDiameter(v_diameterInCm tray.diameterincm%TYPE)
return t_trayIds
as
v_trayIdsTable t_trayIds := null;
begin
select t.trayid into v_trayIds from tray t
where t.diameterincm = v_diameterincm;
return v_trayIdsTable;
end;
So what I want is, to ask for all Tray IDs with a specific Diameter and store them in an Array or Table. In Java I used ArrayList. I want to return the Table in the end to pass the result onto another function. The above code doesn't seem to work. SQL Developer gives me a syntax error at the word create.
Can someone help?

Your code fails because you are mixing a declare section that must be followed by a begin section, with a "create or replace function" that is a standalone statement to create objects;
If you want to declare a PL/SQL table type and make it public,
you must put it in a package specification, so it can be visible by any function (I also declare here the function F_getTrayIdByDiameter, to make it visible):
CREATE OR REPLACE package utils is
type t_trayIds is table of number(38,0) index by binary_integer;
function F_getTrayIdByDiameter(v_diameterInCm tray.diameterincm%TYPE) return t_trayIds;
end utils;
/
besides, you can't use SELECT INTO syntax, because
select col into var
can be used only for single row, not for lists;
in PL/SQL, if you want to manage multiple rows, you have to use a cursor;
so, if you want to create your PL/SQL table, you can fetch your cursor and build your list (PL/SQL table);
so, your package body can be,
CREATE OR REPLACE package body utils is
function F_getTrayIdByDiameter(v_diameterInCm tray.diameterincm%TYPE) return t_trayIds is
v_trayIdsTable t_trayIds;
i number := 0;
cursor c is
select t.trayid from tray t
where t.diameterincm = v_diameterincm;
begin
for my_rec in c loop
v_trayIdsTable(i) := my_rec.trayid;
i := i + 1;
end loop;
return v_trayIdsTable;
end;
end utils;
/
Then, you can use your list in another function, or in an anonymous block, just for example:
declare
my_result utils.t_trayIds;
begin
my_result := utils.F_GETTRAYIDBYDIAMETER(20);
dbms_output.put_line(my_result(0));
end;

By starting with declare you are creating an anonymous block, which do not have return values. They just do stuff and quit. It sounds like you want to create a function instead.
First, to return a collection of trayids, you need to create a type to return. This has to be done at the schema level; it is an object in its own right. There are three kinds of collections in Oracle: nested tables, associative arrays ("index by" tables), and varrays. I pretty much never use varrays and I don't think you can use associative arrays like this (but I forget, this may have changed in recent versions of Oracle). So create your type:
create or replace type t_trayids as table of number;
Now create your function. The key here is you must use bulk collect to populate the array. This is vastly faster than creating a result set and looping over it.
create or replace function F_getTrayIdByDiameter(v_diameterInCm tray.diameterincm%TYPE)
return t_trayIds
as
v_trayIdsTable t_trayIds;
begin
select t.trayid bulk collect into v_trayIdsTable from tray t
where t.diameterincm = v_diameterincm;
return v_trayIdsTable;
end;
As Nicola points out, you can also create a package and declare the type inside the package specification. You can use associative arrays this way. Which approach depends on what you're trying to do.

Related

Is there a way to declare an array variable which accepts a cursor ROWTYPE as its datatype in Postgres?

I am facing a problem in PL/pgSQL while trying to convert some procedures from Oracle to Postgres RDBMS. At the original procedure in PL/SQL, one of these procedures has declared within the DECLARE clause: a bounded cursor, a TYPE which is a TABLE OF ROWTYPE of that bounded cursor and a variable which takes the datatype of this predefined TYPE.
In a few words (as far as I've understood), a bounded cursor and an ARRAY of its type - meaning that each position of that array is actually a ROWTYPE of that cursor - are declared. I have tried to research ways to get around this in Postgres but no success, yet.
This isn't the original I am trying to migrate from Oracle to Postgres, but I think it clarifies what I am trying to reach:
CREATE OR REPLACE PROCEDURE schema.procedure_name (arguments)
AS
$BODY$
DECLARE
CUR_FILMS2 CURSOR (year INTEGER) for
SELECT *
FROM film
WHERE release_year = year;
TYPE TYPE_EXAMPLE IS TABLE OF CUR_FILMS2%ROWTYPE;
V_TYPE_EXAMPLE TYPE_EXAMPLE;
...
BEGIN
OPEN CUR_FILMS2(2022);
FETCH CUR_FILMS2 BULK COLLECT INTO V_TYPE_EXAMPLE;
CLOSE CUR_FILMS2;
...
-- Later on, that variable is iterated to execute some code beneath.
FOR I IN 1 .. V_TYPE_EXAMPLE.COUNT LOOP
...
END LOOP;
END;
$BODY$
I have already thought about the RECORD datatype using, but it only accepts one record per time, and as far as I know there isn't a sort of TABLE variable in Postgres. The Postgres version I am using is PostgreSQL 14.3.
What is the easiest way to get around this problem? Is there a way?
Postgres does not have table variables - and no BULK COLLECT for cursors in PL/pgSQL. You might work with temporary tables.
Seems like you can replace all the overhead with a plain FOR loop (and its implicit cursor) using a generic record variable for the row type:
CREATE OR REPLACE PROCEDURE schema.procedure_name (_year int) AS
$func$
DECLARE
_rec record;
BEGIN
FOR _rec IN
SELECT *
FROM film
WHERE release_year = _year
LOOP
...
END LOOP;
END
$func$;
See:
Cursor based records in PostgreSQL
Loop on tables with PL/pgSQL in Postgres 9.0+

Oracle Stored Procedure into a View

I have a SP in oracle wich returns a cursor, before I had a query to create a viewn, but due to the complexity of the data I need, the better option to get it was a SP, but I strictly need the view with the information ( client's requirement), but now I have no idea how to put/convert the data (cursor) in the view, I was checking Global Temporary Tables, but that means I need to call the SP before accessing the view, and that's not possible. It's imperative that I call access the view with a select, Select * from view_data_sp, and obviously that the performance is not affected.
Any idea how can I achieve this?
Thanks
Consider using record type, table of records and pipe rows. Something like this would do the trick?
CREATE OR REPLACE package sysop as
type file_list_rec is record(filename varchar2(1024));
type file_list is table of file_list_rec;
function ls(v_directory varchar2) return file_list pipelined;
end;
/
CREATE OR REPLACE package body sysop as
function ls(v_directory varchar2) return file_list pipelined is
rec file_list_rec;
v_host_list varchar2(32000) := '';
begin
v_host_list := host_list(v_directory);
for file in (
select regexp_substr(v_host_list, '[^'||chr(10)||']+', 1, level)
from dual
connect by
regexp_substr(v_host_list, '[^'||chr(10)||']+', 1, level) is not null)
loop
pipe row (file);
end loop;
return;
end ls;
end sysop;
/

PL/SQL: Can you pass the returned data from a pipelined function on?

I have a function in a database package which returns a pipelined object collection. I wish to generalise that function so that the same functionality can be called based on a different data. however, I wish to retain the existing function for compatibility. Is there any way to pass the pipeline on without looping the rows?
An example to make it clearer. I have a function foo:
FUNCTION foo(some_id in varchar2) return mypackage.mytype pipelined is
arFoo mypackage.mytype;
BEGIN
-- do stuff to fill data in arFoo based on some_id
for i in nvl(arFoo.first,0) .. nvl(arFoo.last, -1) loop
pipe row(arFoo(i));
end loop;
return;
END;
I will create a function bar instead
FUNCTION bar(arData in myParamType) return return mypackage.mytype pipelined is
arFoo mypackage.mytype;
BEGIN
-- do stuff to fill data in arFoo based on data in arData
for i in nvl(arFoo.first,0) .. nvl(arFoo.last, -1) loop
pipe row(arFoo(i));
end loop;
return;
END;
I would like to retain foo in the following fashion:
FUNCTION foo(some_id in varchar2) return mypackage.mytype pipelined is
arData myParamType;
BEGIN
-- do stuff to fill data in arData based on some_id
return arBar(arData);
END;
However, the return arBar(arData);is not allowed - pipelined functions must have return as a staement by itself. The question is how to connect the pipe returned by bar to the pipe returned by foo The only way I can see is to loop the results and pipe them again:
for r in (select * from table(bar(arData))) loop
pipe row(r);
end loop;
That, however, strikes me as particularly inefficient. Is there any more efficient way to connect the pipeline from bar to the pipeline in foo without looping?
I am submitting this as an aswer since it is how I solved it. It is in some ways only a partial answer in that I could not find a good way to do this with pipelined functions. What I did instead was create the return objects as global user-defined objects (instead of as record types in the package spec) and then return the object directly.
In other words I defined the functions like this:
FUNCTION bar(arData in myParamType) return mytype is
arFoo mytype;
BEGIN
-- do stuff to fill data in arFoo based on data in arData
return arFoo;
END;
FUNCTION foo(some_id in varchar2) return mytype is
arData myParamType;
BEGIN
-- do stuff to fill data in arData based on some_id
return arBar(arData);
END;
and remove the following (which had been in the mypackage spec)
type mytype is record(
foo_id foo.id%type
)
and instead ran a script to create a global type
create or replace type mytype as object(
foo_id number(5,0)
)
This means that the use of the function (which is variants on select * from table(foo('x'))) remains unchanged while my new internal function exists and can be called directly in other situations.
It would be neater to have the type defined in the package spec, but we already have some global objects so we can handle it. (I think that oracle internally defines global objects to handle pipelined functions anyway). Unfortunately we cannot use %type in the global object definitions but we can live with that.

using comma separated values inside IN clause for NUMBER column

I have 2 procedures inside a package. I am calling one procedure to get a comma separated list of user ids.
I am storing the result in a VARCHAR variable. Now when I am using this comma separated list to put inside an IN clause in it is throwing "ORA-01722:INVALID NUMBER" exception.
This is how my variable looks like
l_userIds VARCHAR2(4000) := null;
This is where i am assigning the value
l_userIds := getUserIds(deptId); -- this returns a comma separated list
And my second query is like -
select * from users_Table where user_id in (l_userIds);
If I run this query I get INVALID NUMBER error.
Can someone help here.
Do you really need to return a comma-separated list? It would generally be much better to declare a collection type
CREATE TYPE num_table
AS TABLE OF NUMBER;
Declare a function that returns an instance of this collection
CREATE OR REPLACE FUNCTION get_nums
RETURN num_table
IS
l_nums num_table := num_table();
BEGIN
for i in 1 .. 10
loop
l_nums.extend;
l_nums(i) := i*2;
end loop;
END;
and then use that collection in your query
SELECT *
FROM users_table
WHERE user_id IN (SELECT * FROM TABLE( l_nums ));
It is possible to use dynamic SQL as well (which #Sebas demonstrates). The downside to that, however, is that every call to the procedure will generate a new SQL statement that needs to be parsed again before it is executed. It also puts pressure on the library cache which can cause Oracle to purge lots of other reusable SQL statements which can create lots of other performance problems.
You can search the list using like instead of in:
select *
from users_Table
where ','||l_userIds||',' like '%,'||cast(user_id as varchar2(255))||',%';
This has the virtue of simplicity (no additional functions or dynamic SQL). However, it does preclude the use of indexes on user_id. For a smallish table this shouldn't be a problem.
The problem is that oracle does not interprete the VARCHAR2 string you're passing as a sequence of numbers, it is just a string.
A solution is to make the whole query a string (VARCHAR2) and then execute it so the engine knows he has to translate the content:
DECLARE
TYPE T_UT IS TABLE OF users_Table%ROWTYPE;
aVar T_UT;
BEGIN
EXECUTE IMMEDIATE 'select * from users_Table where user_id in (' || l_userIds || ')' INTO aVar;
...
END;
A more complex but also elegant solution would be to split the string into a table TYPE and use it casted directly into the query. See what Tom thinks about it.
DO NOT USE THIS SOLUTION!
Firstly, I wanted to delete it, but I think, it might be informative for someone to see such a bad solution. Using dynamic SQL like this causes multiple execution plans creation - 1 execution plan per 1 set of data in IN clause, because there is no binding used and for the DB, every query is a different one (SGA gets filled with lots of very similar execution plans, every time the query is run with a different parameter, more memory is needlessly used in SGA).
Wanted to write another answer using Dynamic SQL more properly (with binding variables), but Justin Cave's answer is the best, anyway.
You might also wanna try REF CURSOR (haven't tried that exact code myself, might need some little tweaks):
DECLARE
deptId NUMBER := 2;
l_userIds VARCHAR2(2000) := getUserIds(deptId);
TYPE t_my_ref_cursor IS REF CURSOR;
c_cursor t_my_ref_cursor;
l_row users_Table%ROWTYPE;
l_query VARCHAR2(5000);
BEGIN
l_query := 'SELECT * FROM users_Table WHERE user_id IN ('|| l_userIds ||')';
OPEN c_cursor FOR l_query;
FETCH c_cursor INTO l_row;
WHILE c_cursor%FOUND
LOOP
-- do something with your row
FETCH c_cursor INTO l_row;
END LOOP;
END;
/

Postgresql trigger function with parameters

I want to create a trigger on a table called takes in postgresql to update a value in another table called student
I'm trying to do it in the following way. But I'm getting an error that there is syntax error near "OLD". I don't understand whats wrong with this. This is my code:
CREATE OR REPLACE FUNCTION upd8_cred_func
(id1 VARCHAR, gr1 VARCHAR,id2 VARCHAR, gr2 VARCHAR)
RETURNS void AS $$
BEGIN
IF (id1=id2 and gr1 is null and gr2 is not null) THEN
update student set tot_cred = tot_cred + 6 where id = id1;
END IF;
RETURN;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER upd8_cred
AFTER UPDATE ON takes
FOR EACH ROW
EXECUTE PROCEDURE upd8_cred_func(OLD.id,OLD.grade,NEW.id,NEW.grade);
You do not need to pass the NEW and OLD as parameters to the trigger function. They are automagically available there:
http://www.postgresql.org/docs/9.1/interactive/trigger-definition.html :
The trigger function must be declared as a function taking no arguments and returning type trigger. (The trigger function receives its input through a specially-passed TriggerData structure, not in the form of ordinary function arguments.)
About the records passed to the trigger procedure, please see http://www.postgresql.org/docs/9.1/interactive/plpgsql-trigger.html :
When a PL/pgSQL function is called as a trigger, several special variables are created automatically in the top-level block. They are: [...] NEW, [...] OLD [...]
As SeldomNeedy pointed in the comment below, you can still pass and use parameters to the trigger function. You declare the function as taking no parameters, but when defining the trigger (by CREATE TRIGGER), you may add some.
They will be available for the trigger as TG_NARG (the number of such parameters), and TG_ARGV[] (an array of text values).
As Greg stated, trigger functions can take arguments, but the functions themselves cannot have declared parameters. Here's a simple example in plpgsql:
CREATE TABLE my_table ( ID SERIAL PRIMARY KEY ); -- onelined for compactness
CREATE OR REPLACE FUNCTION raise_a_notice() RETURNS TRIGGER AS
$$
DECLARE
arg TEXT;
BEGIN
FOREACH arg IN ARRAY TG_ARGV LOOP
RAISE NOTICE 'Why would you pass in ''%''?',arg;
END LOOP;
RETURN NEW; -- in plpgsql you must return OLD, NEW, or another record of table's type
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER no_inserts_without_notices BEFORE INSERT ON my_table
FOR EACH ROW EXECUTE PROCEDURE raise_a_notice('spoiled fish','stunned parrots');
INSERT INTO my_table DEFAULT VALUES;
-- the above kicks out the following:
--
-- NOTICE: Why would you pass in 'spoiled fish'?
-- NOTICE: Why would you pass in 'stunned parrots'?
--
There are a few other goodies such as TG_NARGS (to know how many args you got without looping through them) discussed in the docs. There's also information there about how to get the name of the triggering table in case you have mostly-but-not-quite-shared logic for one trigger-function that spans a number of tables.
The trigger function can have parameters, but, you can't have those parameters passed like a normal function (e.g. arguments in the function definition). You can get the same result... In python you get access to the OLD and NEW data as the answer above describes. For example, I can use TD['new']['column_name'] in python to reference the new data for column_name. You also have access to the special variable TD['args']. So, if you like:
create function te() returns trigger language plpython2u as $function$
plpy.log("argument passed 1:%s 2:%s" %(TD['args'][0], TD['args'][1], ))
$function$
create constraint trigger ta after update of ttable
for each for execute procedure te('myarg1','myarg2');
Granted, these arguments are static, but, they are useful when calling a common trigger function from multiple trigger declarations. I am pretty sure that the same variables are available for other stored procedure languages. (sorry if the code doesn't work verbatim, but, I do practice this technique, so I know you can pass arguments!).