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

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+

Related

Save and return multiple rows within function pl/sql oracle

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.

Cursor with sql subquery present as a parameter in BigQuery

I found a previous post (Cursors in BigQuery) where there exists a way to declare cursors on bigquery. This works well and good when the cursor subquery is not present as a parameter.
Currently I was going through FOR..IN EXECUTE construct in Netezza. It behaves extactly like a cursor construct except here, the sql present is a dynamic sql. This dynamic sql is first executed, after that the construct boils down to a simple cursor statement.
Consider the following use case, where the sub-query is present as a parameter.
CREATE or replace PROCEDURE myproc(varchar(256))
RETURNS INT4
LANGUAGE NZPLSQL
AS
BEGIN_PROC
declare
sqlstr alias for $1; ---- sqlStr is a parameter
r1 record;
begin
FOR r1 IN EXECUTE sqlstr ---- sqlStr is evaluated after that it boils down to cursor statement.
loop
insert into t1 values r1.c1;
end loop;
end;
END_PROC#
Is there a similiar way to declare cursors with subquery as a parameter on BigQuery ?
Formally, both LOOPs and EXECUTE IMMEDIATE are available in BigQuery and well documented.
Practically - using LOOP to mimic cursor for real table is extremely ineffective and quite a no-no in BigQuery unless you have use-case where this is the only way to go
But it is almost in 100% cases you can express your logic to be done in batch way (as opposed to using cursor)
At the same time if Table is not that big (like lookup tab le for example) - you can select table rows into array and than loop through arrays elements running execute immediate getting the result and inserting it into target table

Using RedShift CURSOR to insert and iterate

I recently found that RedShift supports CURSOR, and more specifically it supports the commands: DECLARE & FETCH.
I managed to create a cursor and fetch some of its rows but didn't find a way to:
Insert the fetched results into neither table or variable.
Iterate over the rows of the declared cursor in a dynamic fashion (based on while or any logical test)
I didn't find any documentation on Amazon on how to do that, does someone know if that is even possible? Thanks.
You can easily achieve this by creating a stored procedure which supports variables. You can read a dataset iterate through it and perform your logic.
The following example shows a procedure with output arguments. Arguments are input (IN), input and output (INOUT), and output (OUT).
CREATE OR REPLACE PROCEDURE test_sp2(f1 IN int,
f2 INOUT varchar(256), out_var OUT varchar(256))
AS $$
DECLARE
loop_var int;
BEGIN
IF f1 is null OR f2 is null THEN
RAISE EXCEPTION 'input cannot be null';
END IF;
DROP TABLE if exists my_etl;
CREATE TEMP TABLE my_etl(a int, b varchar);
FOR loop_var IN 1..f1 LOOP
insert into my_etl values (loop_var, f2);
f2 := f2 || '+' || f2;
END LOOP;
SELECT INTO out_var count(*) from my_etl;
END;
$$ LANGUAGE plpgsql;
call test_sp2(2,'2019');
f2 | column2
---------------------+---------
2019+2019+2019+2019 | 2
(1 row)
source - https://docs.aws.amazon.com/redshift/latest/dg/stored-procedure-create.html
Redshift doesn't have variables. Inserting into another table easier and much faster with INSERT INTO ... SELECT.
If I understand your second use case, I don't know of any relational database which supports that behaviour. You can filter the cursor when you create it, but once it is created your options are getting the next row or closing the cursor. If you need to filter then you can DECLARE a new cursor.

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;
/

PL/pgSQL Return SETOF Records Error

I am relatively new to postgresql and battling my way to get familiarized with it. I had run in to an error while writing a new pl/sql function. ERROR: type "ordered_parts" does not exist
CREATE OR REPLACE FUNCTION get_ordered_parts(var_bill_to integer)
RETURNS SETOF ordered_parts AS
$BODY$
declare
var_ordered_id record;
var_part ordered_parts;
begin
for var_ordered in select order_id from view_orders where bill_to = var_bill_to
loop
for var_part select os.po_num,os.received,os.customer_note,orders.part_num,orders.description,orders.order_id,orders.remaining_quantity from (select vli.part_num,vli.description,vli.order_id,vli.quantity - vli.quantity_shipped as remaining_quantity from view_line_items as vli where vli.order_id in (select order_id from view_orders where bill_to = var_bill_to and order_id = var_ordered.order_id) and vli.quantity - vli.quantity_shipped > 0)as orders left join order_sales as os on orders.order_id = os.order_id
then
-- Then we've found a leaf part
return next var_part;
end if;
end loop;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
COST 100
ROWS 1000;
ALTER FUNCTION get_ordered_parts(integer) OWNER TO postgres;
just note - your code is perfect example how don't write stored procedure ever. For some longer results it can be extremely slow. Minimally two cycles can be joined to one, or better, you can use just only one RETURN QUERY statement. Next issue is zero formatting of embedded SQL - good length of line is between 70 and 100 chars - writing long SQL statement to one line going to zero readability and maintainability code.
Relation database is not array, and any query has some cost, so don't use nested FOR if you really don't need it. I am sorry for offtopic.
The error message is telling you that you have declared the return type of your function to be SETOF ordered_parts, but it doesn't know what kind of thing ordered_parts is. Within your Declare block you also have a variable declared as this same type (var_part ordered_parts).
If you had a table or view called ordered_parts, then its "row type" would be automatically created as a type, but this is not the case here. if you just want to use an arbitrary row from a result set, you can just use the generic type record.
So in this case your function should say RETURNS SETOF record, and your Declare block var_part record.
Bonus tip: rather than looping over the result of your query and running RETURN NEXT on each row, you can use RETURN QUERY to throw the whole result set into the returned set in one go. See this Postgres manual page.