Returned my Cursor in my oracle PL/SLQ function but not all rows are being returned. Can you only return 1 row in a Oracle pl/sql function? - sql

Here is my code I have 2 rows that share the same name, reservation date, and hotel Id. I don't understand why when I execute this function it gives me the error "exact fetch returns more than requested number of rows" instead of returning my both rows in my Reservation Table.
I have returned the cursor correctly I assume, so it should work?
CREATE OR REPLACE FUNCTION findres(cname IN reservation.cust_name%type,
hotelID IN reservation.hotel_id%type,
resdate IN reservation.reserve_date%type)
RETURN reservation.reserve_id%type is
resid reservation.reserve_id%type;
BEGIN
SELECT reserve_id
INTO resid
FROM reservation
WHERE Cust_name = cname
AND Hotel_id = hotelID
AND reserve_date = resdate;
RETURN resid;
EXCEPTION
WHEN no_data_found THEN
dbms_output.put_line('No reservation found');
END;
/

From the documentation for definition of into_clause : the SELECT INTO statement retrieves one or more columns from a single row and stores them in either one or more scalar variables or one record variable
Then the current SELECT statement should be replaced against the cases of returning more than one row. The following queries might be alternatives for your current SQL Select statement
SELECT reserve_id
INTO resid
FROM
( SELECT r.*,
ROW_NUMBER() OVER (ORDER BY 0) AS rn
FROM reservation
WHERE Cust_name = cname
AND Hotel_id = hotelID
AND reserve_date = resdate
)
WHERE rn = 1;
If DB version is 12+, then use
SELECT reserve_id
INTO resid
FROM reservation
WHERE Cust_name = cname
AND Hotel_id = hotelID
AND reserve_date = resdate
FETCH NEXT 1 ROW ONLY;
without a subquery in order to return one row only, considering you only get duplicates for those columns with no ordering rules for the data. Through use of these queries, no need to handle no_data_found or too_many_rows exceptions.
Update : If your aim is to return all the rows even there are more than one row at once, then you can use SYS_REFCURSOR such as
CREATE OR REPLACE FUNCTION findres(cname reservation.cust_name%type,
hotelID reservation.hotel_id%type,
resdate reservation.reserve_date%type)
RETURN SYS_REFCURSOR IS
recordset SYS_REFCURSOR;
BEGIN
OPEN recordset FOR
SELECT reserve_id
FROM reservation
WHERE Cust_name = cname
AND Hotel_id = hotelID
AND reserve_date = resdate;
RETURN recordset;
END;
/
and call in such a way that
VAR v_rc REFCURSOR
EXEC :v_rc := findres('Avoras',111,date'2020-12-06');
PRINT v_rc
from the SQL Developer's console.

Related

Select statement where one of the column values change dynamically

I am trying to write a query as follows
select trading_day,trading_time,quantity,price from trade where trade_id=A903
Now for this trading_time the logic of extraction is
If the price above matches table trade_sb price and trade_id then that becomes trading time
if there is no match I go to another table trade_mb and search for match of price and trade_id
If there is no match in both above cases the original query holds good.
I tried with rank but couldn't get the result
Please help
it's not clear to me but i think you can create a function for it and use it in the query, see sample code below,
CREATE FUNCTION get_trading_time (p_trade_id VARCHAR2, p_price NUMBER, p_trading_time DATE) RETURN DATE
IS
v_trading_time DATE;
BEGIN
BEGIN
SELECT trading_time
INTO v_trading_time
FROM trade_sb
WHERE trade_id = p_trade_id
AND price = p_price;
RETURN v_trading_time;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
END;
BEGIN
SELECT trading_time
INTO v_trading_time
FROM trade_mb
WHERE trade_id = p_trade_id
AND prioe = p_price;
RETURN v_trading_time;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
END;
IF v_trading_time IS NULL THEN
RETURN p_trading_time;
END IF;
END;
/
select trading_day,trading_time,quantity,price ,
get_trading_time(trade_id, price, trading_time) trading_time2
from trade
where trade_id='A903';

Display Number of Rows based on input parameter

CREATE OR REPLACE PROCEDURE test_max_rows (
max_rows IN NUMBER DEFAULT 1000
)
IS
CURSOR cur_test ( max_rows IN number ) IS
SELECT id FROM test_table
WHERE user_id = 'ABC'
AND ROWNUM <= max_rows;
id test_table.id%TYPE;
BEGIN
OPEN cur_test(max_rows) ;
LOOP
FETCH cur_test INTO id;
EXIT WHEN cur_test%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ID:' || id);
END LOOP;
END;
My requirement is to modify the above code so that when I pass -1 for max_rows, the proc should return all the rows returned by the query. Otherwise, it should limit the rows as per max_rows.
For example:
EXECUTE test_max_rows(-1);
This command should return all the rows returned by the SELECT statement above.
EXECUTE test_max_rows(10);
This command should return only 10 rows.
You can do this with a OR clause; change:
AND ROWNUM <= max_rows;
to:
AND (max_rows < 1 OR ROWNUM <= max_rows);
Then passing zero, -1, or any negative number will fetch all rows, and any positive number will return a restricted list. You could also replace the default 1000 clause with default null, and then test for null instead, which might be a bit more obvious:
AND (max_rows is null OR ROWNUM <= max_rows);
Note that which rows you get with a passed value will be indeterminate because you don't have an order by clause at the moment.
Doing this in a procedure also seems a bit odd, and you're assuming whoever calls it will be able to see the output - i.e. will have done set serveroutput on or the equivalent for their client - which is not a very safe assumption. An alternative, if you can't specify the row limit in a simple query, might be to use a pipelined function instead - you could at least then call that from plain SQL.
CREATE OR REPLACE FUNCTION test_max_rows (max_rows IN NUMBER DEFAULT NULL)
RETURN sys.odcinumberlist PIPELINED
AS
BEGIN
FOR r IN (
SELECT id FROM test_table
WHERE user_id = 'ABC'
AND (max_rows IS NULL OR ROWNUM <= max_rows)
) LOOP
PIPE ROW (r.id);
END LOOP;
END;
/
And then call it as:
SELECT * FROM TABLE(test_max_rows);
or
SELECT * FROM TABLE(test_max_rows(10));
Here's a quick SQL Fiddle demo. But you should still consider if you can do the whole thing in plain SQL and PL/SQL altogether.

PL/SQL stored function return with row type

CREATE OR REPLACE FUNCTION lab( f_name IN VARCHAR2 )
RETURN test%ROWTYPE
IS
total NUMBER := 0;
CURSOR c_app IS
SELECT count(*),LISTAGG(s.sname,',') WITHIN GROUP (ORDER BY s.sname)
FROM APPLICANT a INNER JOIN SPOSSESSED s ON a.A# = s.A#
WHERE a.fname = f_name;
rec_app c_app%ROWTYPE;
BEGIN
OPEN c_app;
LOOP
FETCH c_app into rec_app;
EXIT WHEN c_app%NOTFOUND;
END LOOP;
CLOSE c_app;
RETURN rec_app;
END lab;
/
Fail to compile with errors that expression wrong type?
Isn't it possible to return with rowtype result?
for example i run this function
select lab(fname) from position where fname='PETER';
so the result will be display like
PETER : aaaa,bbbb,cccc
You're declaring the return as test%rowtype, then trying to return rec_app, which is declared as c_app%rowtype - so the types don't match. You can't do that.
c_app is only in scope within this function so it would not have any meaning for any callers, and you can't use it as the return type. You can return something that is actually test%rowtype, assuming test is a table, but not an arbitrary different type. It isn't clear that there is any relationship at all between your cursor and its row type, and the test table.
You're also looping round to potentially fetch multiple rows, but only returning the last one (or trying to, anyway), which probably isn't what you mean to do.
The simplest way to get all the cursor rows back to the caller is with a ref cursor:
CREATE OR REPLACE FUNCTION lab( f_name IN VARCHAR2 )
RETURN SYS_REFCURSOR
IS
ref_cur SYS_REFCURSOR;
BEGIN
OPEN ref_cur FOR
SELECT count(*),LISTAGG(s.sname,',') WITHIN GROUP (ORDER BY s.sname)
FROM APPLICANT a INNER JOIN SPOSSESSED s ON a.A# = s.A#
WHERE a.fname = f_name;
RETURN ref_cur;
END lab;
/
If you create an external type you could use PIPELINED but that doesn't appear necessary here. neither is quite using a %rowtype though. You can only return a %rowtype if you have a table that has the columns you want to return.

Select multiple rows as array

I have a table where two people may have the same name, but separate IDs. I am given the name, and need to request their IDs.
When I use a command like:
SELECT id_num INTO cust_id FROM Customers WHERE name=CName;
If I use this command on the command line (psql), it returns 2 results (for example).
But when I use it as part of an SQL script (PL/pgSQL), it always just grabs the first instance.
I tried selecting into cust_id[], but that produced an error. So what is the proper way to select ALL results, and pump them into an array or another easy way to use them?
In declare
DECLARE id_nums bigint[];
in select
id_nums := ARRAY(select cust_id from Customers WHERE name = CName);
If you prefer loop use
DECLARE id_num bigint;
FOR id_num in select cust_id from Customers WHERE name = CName LOOP
your code here
END LOOP;
Read plpgsql control structures in postgresql docs 9.1.
To put data from individual rows into an array, use an array constructor:
DECLARE id_nums int[]; -- assuming cust_id is of type int
id_nums := ARRAY (SELECT cust_id FROM customers WHERE name = cname);
Or the aggregate function array_agg()
id_nums := (SELECT array_agg(cust_id) FROM customers WHERE name = cname);
Or use SELECT INTO for the assignment::
SELECT INTO id_nums
ARRAY (SELECT cust_id FROM customers WHERE name = cname);

How to manipulate data according to cursor values which are also the return value of a stored procedure in Oracle sql?

I have an oracle sql stored procedure that returns a cursor.
This cursor gets in the stored procedure body the value of a complex select statement (in the example below I made the select statement simple).
Then, I want to use the cursor for two things:
1. Use it as return value of the stored procedure
2. Use its data to update some values in another table within the stored procedure body
I couldn't find how to do it, so in the meanwhile I had to replicate the (complex) select statement and let another cursor have its value for updating the other table.
create or replace procedure sp_GetBuildings(returned_cursor OUT SYS_REFCURSOR,
timeFrameHrsParam number) is
v_buildingID Buildings.buildingId%type;
cursor t_result is
select customerId
from (select buildingId from Buildings) b
inner join Customers c on c.building_id = b.building_id;
begin
open returned_cursor for
select customerId
from (select buildingId from Buildings) b
inner join Customers c on c.building_id = b.building_id;
for t in t_result
loop
v_buildingID := t.building_id;
update Buildings set already = 1 where building_id = v_buildingID ;
end loop;
commit;
end sp_GetBuildings;
Can you help me with a solution that will save me the select statement replication?
You can't copy or clone a cursor in Oracle. The cursor is just a pointer to the result set and reading the cursor moves along the result list in a single direction. However, you can achieve something very similar using arrays:
CREATE TYPE nt_number AS TABLE OF NUMBER;
CREATE OR REPLACE PROCEDURE sp_getbuildings(returned_table OUT nt_number,
timeframehrsparam NUMBER) IS
CURSOR t_result IS
SELECT customerid
FROM buildings b
JOIN customers c
ON c.building_id = b.building_id;
i NUMBER;
BEGIN
OPEN t_result;
FETCH t_result
BULK COLLECT INTO returned_table;
CLOSE t_result;
FORALL i IN returned_table.FIRST .. returned_table.LAST
UPDATE buildings
SET already = 1
WHERE building_id = v_buildingid;
COMMIT;
END sp_getbuildings;