Using a Stored Procedure in a View with Postgresql - sql

I have been trying to create a View where one of the column pending_amount gets its value as a result of a stored procedure execution.
The stored procedure is pending_stock(int,int) and returns an integer. The view is created successfully but when i try to perform any query like select on the this view it takes ever to return a value.
CREATE OR REPLACE VIEW view_production_parts AS
SELECT p.part_id, p.min_amount, gp.part_num, gp.description
, p.quantity_available
, p.quantity_total - p.quantity_available AS quantity_alloc
, p.quantity_total
, (SELECT pending_stock(p.part_id, 0) AS pending_stock) AS pending_amount
, p.production_run
, CASE
WHEN ppur.purchased_part_id IS NOT NULL THEN true
ELSE false
END AS is_purchased_part, ppur.purchased_part_id, p.store_move_type_id
, gp.part_status_id, p.default_location
, COALESCE(pwoh.part_work_order_hold_id, 0) AS part_work_order_hold_id
FROM general_part gp
JOIN part p ON gp.part_id = p.part_id
LEFT JOIN purchased_part ppur ON ppur.part_id = p.part_id
LEFT JOIN part_work_order_hold pwoh ON pwoh.part_id = p.part_id
ORDER BY gp.part_num;
Can a stored procedure be used in a view? If used, Is my declaration correct?
Find the result from of this query at explain.depesz.com:
EXPLAIN ANALYZE SELECT count(*) FROM view_production_parts
I am using Postgres 8.4.
Function definition for pending_stock(int,int):
CREATE OR REPLACE FUNCTION pending_stock(var_part_id integer
, var_pattern_id integer)
RETURNS integer AS
$BODY$
declare
r record;
var_qty_expected integer;
var_qty_moved_to_stock integer;
var_total_stock_moved_out integer;
var_actual_qty integer;
begin
var_total_stock_moved_out := 0;
var_qty_expected := 0;
for r in
select work_order_id,quantity_expected
from view_work_orders
where part_id = var_part_id and open = 'TRUE'
and quantity_allocated is null and quantity_expected >= quantity_actual
loop
var_qty_expected = var_qty_expected + r.quantity_expected;
select sum(quantity) from view_work_order_move_parts_details
where source_work_order_id = r.work_order_id
and part_id = var_part_id into var_qty_moved_to_stock;
if var_qty_moved_to_stock is null then
var_qty_moved_to_stock = 0;
end if;
var_total_stock_moved_out = var_total_stock_moved_out
+ var_qty_moved_to_stock;
end loop;
var_actual_qty := var_qty_expected - var_total_stock_moved_out;
if var_actual_qty > 0 then
return var_actual_qty;
else
return 0;
end if;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE STRICT
COST 100;
ALTER FUNCTION pending_stock(integer, integer) OWNER TO postgres;

View
You don't need a subquery for the function call. And you can simplify some other minor details:
CREATE OR REPLACE VIEW view_production_parts AS
SELECT p.part_id, p.min_amount
, gp.part_num, gp.description, p.quantity_available
, p.quantity_total - p.quantity_available AS quantity_alloc
, p.quantity_total
, pending_stock(gp.part_id, 0) AS pending_amount
, p.production_run
,(ppur.purchased_part_id IS NOT NULL) AS is_purchased_part
, ppur.purchased_part_id, p.store_move_type_id, gp.part_status_id
, p.default_location
, COALESCE(pwoh.part_work_order_hold_id, 0) AS part_work_order_hold_id
FROM general_part gp
JOIN part p USING (part_id)
LEFT JOIN purchased_part ppur USING (part_id)
LEFT JOIN part_work_order_hold pwoh USING (part_id)
ORDER BY gp.part_num;
Other than that the VIEW definition looks fine.
Function
Can be largely simplified:
CREATE OR REPLACE FUNCTION pending_stock(var_part_id integer
, var_pattern_id integer)
RETURNS integer AS
$func$
DECLARE
r record;
var_qty_expected integer := 0;
var_total_stock_moved_out integer := 0;
BEGIN
FOR r IN
SELECT work_order_id, quantity_expected
FROM view_work_orders
WHERE part_id = var_part_id
AND open = 'TRUE' -- A string instead of a boolean?
AND quantity_allocated IS NULL
AND quantity_expected >= quantity_actual
LOOP
var_qty_expected := var_qty_expected + r.quantity_expected;
SELECT var_total_stock_moved_out + COALESCE(sum(quantity), 0)
FROM view_work_order_move_parts_details
WHERE source_work_order_id = r.work_order_id
AND part_id = var_part_id
INTO var_total_stock_moved_out;
END LOOP;
RETURN GREATEST(var_qty_expected - var_total_stock_moved_out, 0);
END
$func$ LANGUAGE plpgsql
Major points
Generally, assignments are comparatively expensive in plpgsql. Every assignment is executed with a (very simple and fast) SELECT statement internally. Try to use fewer of them.
You can init variables at declaration time. No need for another statement.
The assignment operator in plpgsql is :=. = works, but is undocumented.
Use COALESCE() to catch NULL values.
The function parameter var_pattern_id is never used. This is probably not the full function definition.
The whole final part can be replaced with a single statement using GREATEST
Superior query
Now, this cleaned up function will be a bit faster, but not much. Your whole design of looping repeatedly is extremely inefficient. It results in correlated subqueries that loop through correlated subqueries yet again. Performance nightmare.
Recast the problem as set-based operation to make it faster. Well, a lot faster.
SELECT e.part_id
,GREATEST(COALESCE(sum(e.quantity_expected), 0)
- COALESCE(sum(m.total_stock_moved_out), 0), 0)
FROM view_work_orders e
LEFT JOIN (
SELECT source_work_order_id AS work_order_id
,COALESCE(sum(quantity), 0) AS total_stock_moved_out
FROM view_work_order_move_parts_details
WHERE part_id = var_part_id
GROUP BY 1
) m USING (work_order_id)
WHERE e.part_id = var_part_id
AND e.open = 'TRUE'
AND e.quantity_allocated IS NULL
AND e.quantity_expected >= e.quantity_actual
GROUP BY 1;
Superior view
Integrate this into the original query / view:
CREATE OR REPLACE VIEW view_production_parts AS
SELECT p.part_id, p.min_amount
,gp.part_num, gp.description, p.quantity_available
,p.quantity_total - p.quantity_available AS quantity_alloc
,p.quantity_total
,x.pending_amount
,p.production_run
,(ppur.purchased_part_id IS NOT NULL) AS is_purchased_part
,ppur.purchased_part_id, p.store_move_type_id, gp.part_status_id
,p.default_location
,COALESCE(pwoh.part_work_order_hold_id, 0) AS part_work_order_hold_id
FROM general_part gp
JOIN part p USING (part_id)
LEFT JOIN purchased_part ppur USING (part_id)
LEFT JOIN part_work_order_hold pwoh USING (part_id)
LEFT JOIN (
SELECT e.part_id
,GREATEST(COALESCE(sum(e.quantity_expected), 0)
- COALESCE(sum(m.total_stock_moved_out), 0)
, 0) AS pending_amount
FROM view_work_orders e
LEFT JOIN (
SELECT source_work_order_id AS work_order_id
,sum(quantity) AS total_stock_moved_out
FROM view_work_order_move_parts_details
WHERE part_id = var_part_id
GROUP BY 1
) m USING (work_order_id)
WHERE e.part_id = var_part_id
AND e.open = 'TRUE'
AND e.quantity_allocated IS NULL
AND e.quantity_expected >= e.quantity_actual
GROUP BY 1
) x USING (part_id)
ORDER BY gp.part_num;
Untested, obviously.

Related

Fill empty space on recursive SQL with last value

This is the SQL query i used to get data from my table which contains parent-child values:
WITH RECURSIVE cte AS
(
SELECT array[r.term1_id, r.term2_id] AS path
FROM temp_table r
LEFT JOIN temp_table r0 ON r0.term1_id = r.term2_id
WHERE r0.term1_id IS NULL
UNION ALL
SELECT r.term1_id || c.path
FROM cte c
JOIN temp_table r ON r.term2_id = c.path[1]
)
SELECT path
FROM cte
ORDER BY path;
The result is something like this:
"{1,5,6,1452}"
"{1,5,6,1470,1475}"
How can i fill the last space of the first array obtaining something like this?
"{1,5,6,1452,1452}"
"{1,5,6,1470,1475}"
So i want to repeat the last non null value until i cover every level.
ANSWER:
Here's a functioning statement
CREATE OR REPLACE FUNCTION fill_with_last_element(arr anyarray, n INTEGER)
RETURNS anyarray LANGUAGE plpgsql as $$
DECLARE
l int = array_length(arr,1);
BEGIN
RETURN CASE
WHEN l > n THEN arr
ELSE arr || array_fill(arr[l], array[n- l])
END;
END $$;
WITH RECURSIVE cte(path) AS (
SELECT array[r.term1_id, r.term2_id] AS path
FROM temp_table r
LEFT JOIN temp_table r0 ON r0.term1_id = r.term2_id
WHERE r0.term1_id IS NULL
UNION ALL
SELECT r.term1_id || c.path
FROM cte c
JOIN temp_table r ON r.term2_id = c.path[1]
),
max_len AS (
SELECT max(array_length(path, 1)) max_len
FROM cte
)
SELECT fill_with_last_element(path, max_len)
FROM cte
CROSS JOIN max_len
ORDER BY path;
You cannot generate arrays with equal lengths because you do not know the length before the query terminates. You should modify the results. This function will be helpful:
create or replace function fill_with_last_element(arr anyarray, n integer)
returns anyarray language plpgsql as $$
declare
l int = array_length(arr,1);
begin
return case
when l > n then arr
else arr || array_fill(arr[l], array[n- l])
end;
end $$;
Example:
with cte(path) as (
values
('{1,5,6,1452}'::int[]),
('{1,5,6,1470,1475}')
),
max_len as (
select max(array_length(path, 1)) max_len
from cte
)
select fill_with_last_element(path, max_len)
from cte
cross join max_len
fill_with_last_element
------------------------
{1,5,6,1452,1452}
{1,5,6,1470,1475}
(2 rows)

Compare two columns and make the insert

I want to compare two columns which come from two different tables.
One of the columns, I need to make SUM for all rows with identity let's say 3 and store to a variable.
After that, compare with one row from other table for same identity 3 and to INSERT something ELSE to BREAK if first_column <= second_column.
Can someone suggest some query for this? For Postgresql...
CREATE OR REPLACE FUNCTION "SA_PRJ".usp_add_timesheet_test(p_uid integer, p_project_id integer, p_allocated_time numeric, p_achieved_time numeric, p_task_desc character varying, p_obs character varying, p_date timestamp without time zone)
RETURNS character varying AS
$BODY$
DECLARE sum_alloc_time numeric;
DECLARE alloc_hours integer;
DECLARE fld_id integer;
DECLARE alloc_id integer;
BEGIN
if not "SA_ADM".usp_check_permission(p_uid, 'SA_PRJ', 'usp_add_timesheet_record') then
raise exception 'User ID % dont have permission!', p_uid;
end if;
select a.fld_id into alloc_id from "SD_PRJ".tbl_project_allocation a where a.fld_emp_id = p_uid and a.fld_project_id = p_project_id;
SELECT SUM(fld_allocated_time)
INTO sum_alloc_time
FROM "SD_PRJ".tbl_project_timesheet
WHERE fld_project_id = p_project_id;
SELECT p.fld_allocated_days, p.fld_id
INTO alloc_hours, fld_id
FROM "SD_PRJ".tbl_project p
JOIN "SD_PRJ".tbl_project_timesheet t USING (fld_id)
WHERE t.fld_project_id = p_project_id;
IF #sum_alloc_time <= #alloc_hours THEN
INSERT INTO "SD_PRJ".tbl_project_timesheet
(fld_emp_id, fld_project_id, fld_is_allocated, fld_allocated_time
, fld_achieved_time, fld_task_desc, fld_obs, fld_date)
VALUES (p_uid, p_project_id, coalesce(alloc_id,0), p_allocated_time
, p_achieved_time, p_task_desc, p_obs, p_date);
RAISE NOTICE 'INSERT OK!';
ELSE
RAISE NOTICE 'NOT OK';
END IF;
END
1.tbl_project (fld_id, fld_allocated_days,fld_project_id)
2.tbl_project_timesheet(fld_id,fld_allocated_time,fld_project_id), all INTEGER
I have this , but dosen't work as I wish.Thanks
I think one problem is here:
SELECT p.fld_allocated_days, p.fld_id
INTO alloc_hours, fld_id
FROM "SD_PRJ".tbl_project p
JOIN "SD_PRJ".tbl_project_timesheet t USING (fld_id)
WHERE t.fld_project_id = p_project_id;
That will cough (I think) whenever the select query returns more than one row i.e. whenever tbl_project_timesheet has more than one record for a fld_id,project_id combination.
Anyway. Here's a partial, simplified answer, but hopefully you get the idea...
I wouldn't use local variables. Do the insert in one step:
INSERT INTO timesheet(emp_id,project_id) -- other columns
SELECT
p_uid,p.fld_project_id -- other columns
FROM
projects p
INNER JOIN
(SELECT SUM(fld_allocated_time) as sumtime
FROM timesheet t WHERE fld_project_id = p_project_id) as sumtime_subquery
ON p.fld_allocated_days < sumtime -- just join on the allocated time
WHERE p.fld_project_id = p_project_id;
Now, you need to know if anything was actually inserted. I think you can use the RETURNING option of the INSERT statement, e.g. from here (caveat - I have never used RETURNING, nor set a local variable from a with statement):
WITH ROWS AS (
INSERT INTO timesheet(emp_id,project_id) -- other columns
SELECT
p_uid,p.fld_project_id -- other columns
FROM
projects p
INNER JOIN
(SELECT SUM(fld_allocated_time) as sumtime
FROM timesheet t WHERE fld_project_id = p_project_id) as sumtime_subquery
ON p.fld_allocated_days < sumtime -- just join on the allocated time
WHERE p.fld_project_id = p_project_id
RETURNING 1
)
SELECT COUNT(*) into l_updatedCount FROM rows; -- you have to declare l_updatedCount
-- Now an if statement to handle l_updatedCount

PL/SQL Procedure - ignore argument if null

I have a procedure that takes three arguments and then queries a table based on those arguments. Now one of them could be null and if it is, I'd like it to be ignored in the WHERE clause of the selection.
create or replace PROCEDURE Procedure1
(
COUNTRY IN VARCHAR2, MAKE IN VARCHAR2, SERIAL IN number
) AS
BEGIN
DECLARE
CURSOR c1 IS select v.ID from vehicle v
where v.country = COUNTRY AND
v.make = MAKE AND
((SERIAL IS NOT NULL AND v.serial = SERIAL) OR 1);
BEGIN
FOR e_rec IN c1 LOOP
DBMS_OUTPUT.PUT_LINE(e_rec.id);
END LOOP;
END;
END Procedure1;
I tried something like this but it doesn't work.
you can use the condition like
WHERE v.country = COUNTRY AND
v.make = MAKE AND
(SERIAL IS NULL or v.serial = SERIAL)
This will also work
WHERE v.country = COUNTRY AND
v.make = MAKE AND
((SERIAL IS NOT NULL AND v.Serial = SERIAL) OR SERIAL IS NULL)
try this - this is same as #Miller suggested just using NVL function
WHERE v.country = nvl(COUNTRY,v.country)
AND v.make = nvl(MAKE,v.make)
AND v.serial = nvl(SERIAL,v.serial)

Converting a script from MSSQL to PL/pgSQL

I just started working with the EVE static dump, which is just a lot of tables with data about the game, such as a list of what solar systems connect, which is what I'm dealing with.
I want to make a webpage that lets you filter out systems, and the first step is getting a list of systems nearby, with the distance to them.
I found a script that does it for MSSQL
--By Joanna Davaham http://forum.eveuniversity.org/viewtopic.php?t=44601&p=396107#p424943
--set values
DECLARE #jumpsAway INT =10
DECLARE #MiddleSystemName VARCHAR(50) = 'Aldrat'
DECLARE #Level INT =1
IF OBJECT_ID('tempdb..#map') IS NOT NULL
DROP TABLE #map
CREATE TABLE #map
(fromSolarSystemID INT, toSolarSystemID INT, Level INT)
INSERT INTO #map
SELECT -1, mSS.solarSystemID, 0 FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= #MiddleSystemName
WHILE #Level <= #jumpsAway
BEGIN
INSERT INTO #map
SELECT mSSJ.fromSolarSystemID, mSSJ.toSolarSystemID, #Level FROM mapSolarSystemJumps mSSJ
WHERE mSSJ.fromSolarSystemID IN (SELECT toSolarSystemID FROM #map WHERE Level = #Level-1)
AND mSSJ.fromSolarSystemID NOT IN (SELECT fromSolarSystemID FROM #map)
SET #Level=#Level+1
END
SELECT m.*, mSS.solarSystemName, mSS.security FROM #map m
JOIN mapSolarSystems mSS ON m.toSolarSystemID=mSS.solarSystemID
--WHERE mSS.security<0.45 --uncomment to check all nearby lowsec system
I know that I could probably just use the MSSQL version of the dump, but I also want to be learning more about how to use PostgreSQL better.
I understand what it's doing and everything, but I just don't understand PL/pgSQL well enough to make it work.
My attempt is
CREATE FUNCTION near(VARCHAR, INTEGER) RETURNS TABLE(fromID INT,toID INT,jumps INT,name VARCHAR,security VARCHAR) AS $$
DECLARE --Declaration from here http://www.postgresql.org/docs/9.1/static/plpgsql-declarations.html
MiddleSystemName ALIAS FOR $1;
jumpsAway ALIAS FOR $2;
jumps INTEGER :=1;
BEGIN
--http://stackoverflow.com/questions/11979154/select-into-to-create-a-table-in-pl-pgsql
CREATE TEMP TABLE map AS
SELECT -1, mSS.solarSystemID, 0
FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= MiddleSystemName;
LOOP
--http://www.postgresql.org/docs/9.1/static/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN
--If you don't do it with execute, you can only do one row, I guess?
EXECUTE 'SELECT
|| mSSJ.fromSolarSystemID,
|| mSSJ.toSolarSystemID,
|| $1
|| FROM
|| mapSolarSystemJumps mSSJ
|| WHERE
|| mSSJ.fromSolarSystemID EXISTS (SELECT toSolarSystemID FROM map WHERE jumps = $1 - 1)
|| AND mSSJ.fromSolarSystemID NOT EXISTS (SELECT fromSolarSystemID FROM map)'
INTO map
USING jumps;
jumps := jumps + 1
EXIT WHEN jumps > jumpsAway;
END LOOP;
RETURN QUERY SELECT m.*,mSS.solarSystemName, mSS.security FROM JOIN mapSolarSystems mSS ON m.toSolarSystemID = mSS.solarSystemID;
END;
$$ LANGUAGE plpgsql;
And the error that produces is
Error is
ERROR: "map" is not a known variable
LINE 27: INTO map
^
Thanks for all the help.
PL/pgSQL
This should be a valid translation to plpgsql:
CREATE OR REPLACE FUNCTION f_near(_middlesystemname text, _jumpsaway int)
RETURNS TABLE(fromid int, toid int, jumps int, name text, security text) AS
$func$
DECLARE
_jumps integer;
BEGIN
CREATE TEMP TABLE map AS
SELECT -1 AS "fromSolarSystemID"
,m."solarSystemID" AS "toSolarSystemID"
,0 AS level
FROM "mapSolarSystems" m
WHERE "solarSystemName" = _middlesystemname;
-- potentially add indexes on the temp table and ANALYZE if it gets big
FOR _jumps IN 1 .. _jumpsaway LOOP
INSERT INTO map ("fromSolarSystemID", "toSolarSystemID", level)
SELECT sj."fromSolarSystemID", sj."toSolarSystemID", _jumps AS level
FROM "mapSolarSystemJumps" sj
JOIN map m ON m."toSolarSystemID" = sj."fromSolarSystemID"
AND m."level" = _jumps - 1
LEFT JOIN map mx ON mx."fromSolarSystemID" = sj."fromSolarSystemID"
WHERE mx."fromSolarSystemID" IS NULL;
END LOOP;
RETURN QUERY
SELECT m.*, s."solarSystemName", s."security"
FROM map m
JOIN "mapSolarSystems" s ON m."toSolarSystemID" = s."solarSystemID";
END
$func$ LANGUAGE plpgsql;
RECURSIVE CTE - doesn't seem to work
This short SQL query with a recursive CTE should have done it:
WITH RECURSIVE map AS (
SELECT -1 AS fromsolarsystemid, m.solarsystemid, 0 AS level
FROM mapsolarsystems m
WHERE m.solarsystemname = from_id
UNION ALL
SELECT sj.fromsolarsystemid, sj.tosolarsystemid, level + 1
FROM mapsolarsystemjumps sj
JOIN map m USING (level)
LEFT JOIN map mx USING (fromsolarsystemid)
WHERE sj.fromsolarsystemid = m.tosolarsystemid
AND mx.fromsolarsystemid IS NULL
AND m.level < 10 -- jumpsAway
)
SELECT m.*, s.solarsystemname, s.security
FROM map m
JOIN mapsolarsystems s ON m.tosolarsystemid = s.solarsystemid
-- WHERE s.security < 0.45 -- uncomment to check all nearby lowsec system
However:
ERROR: recursive reference to query "map" must not appear within an outer join
LINE 9: LEFT JOIN map mx USING (fromsolarsystemid)

how to make an outer join with the calling of other procedure

I have a stored procedure :
procedure qr_get_dep_boss(...,...)
takes two params(year, main_code) and returning one record the boss_num and his name .
I want to create another procedure and make a loop on the previous procedure calling , making an outer join with the result of another query
SELECT
year,
main_code,
name,
CASE
WHEN father_code in(-1) THEN NULL
ELSE father_code
END AS father_code,main_code || '_' || year AS main_id,
(SELECT COUNT(*)
FROM sm_r_build
WHERE father_code = sc.main_code
AND year = (SELECT MAX(year)
FROM st_quit_info)) childcount,
main_code||'_'|| father_code AS serial
FROM sm_r_build sc
WHERE year=(SELECT MAX(year)FROM st_quit_info)
So I want the result:
year,main_code,name,father_code,main_id,childcount,serial,boss_num,boss_name
I try this :
create procedure new_get_alldepwithboss()
returning int as year , int as main_code ,nvarchar(100) as name,int as father_code,nvarchar(255) as main_id,int as childcount,int as ll_boss_num,nvarchar(100) as ll_boss_name
define ll_year int;
define ll_main_code int;
define ll_name nvarchar(100);
define ll_father_code int;
define ll_main_id nvarchar(255) ;
define ll_childcount int;
define ll_boss_num int;
define ll_boss_name nvarchar(100);
foreach
SELECT year,main_code,name,CASE WHEN father_code in(-1) THEN NULL ELSE father_code END AS father_code,main_code || '_' || year AS main_id, (SELECT COUNT(*)
FROM sm_r_build
WHERE father_code=sc.main_code AND year= (SELECT MAX(year)
FROM st_quit_info)) childcount ,a.emp_num,a.emp_name
INTO ll_year ,ll_main_code,ll_name ,ll_father_code ,ll_main_id ,ll_childcount ,ll_boss_num,ll_boss_name
FROM sm_r_build sc , TABLE(FUNCTION qr_get_dep_boss(ll_main_code, ll_year))AS a(emp_num,emp_name)
WHERE year=(SELECT MAX(year)FROM st_quit_info)
return ll_year , ll_main_code, ll_name,ll_father_code,ll_main_id, ll_childcount , ll_boss_num,ll_boss_name with resume;
end foreach
end procedure
but in vain !
In case of SQL server I would recommend using rather function than store procedure. Header of function should look:
CREATE FUNCTION qr_get_dep_boss( #year DATE, #main_code INT)
RETURN #t TABLE ( boss_num INT, boss_name VARCHAR(50))
AS
... ( here boss_num and boss_name should be set )
In your main query you can use this function by:
SELECT year, main_code, ... , t.bos_num , t.boss_name
FROM ...
CROSS APPLY
qr_get_dep_boss(year, main_code) t
But I am not sure if it is possible in Informix, in case not you can always define two functions that returns single value. GL!
If I understand right, you don't need the second procedure.
What you need to do is use the RETURN xxx WITH RESUME into the procedure qr_get_dep_boss()
A example copied from the manual .
CREATE FUNCTION series (limit INT, backwards INT) RETURNING INT;
DEFINE i INT;
FOR i IN (1 TO limit)
RETURN i WITH RESUME;
END FOR;
IF backwards = 0 THEN
RETURN;
END IF;
FOR i IN (limit TO 1 STEP -1)
RETURN i WITH RESUME;
END FOR;
END FUNCTION; -- series