Return updated rows from a stored function - oracle

Im trying to select some rows from a Table in ORACLE and at the same time update the selected rows state. I found a way to do so with a stored function and Cursors but I cant manage to return the rows after using the cursor to update. This is my code:
CREATE OR REPLACE FUNCTION FUNCTION_NAME
RETURN SYS_REFCURSOR
IS
l_return SYS_REFCURSOR;
CURSOR c_operations IS
SELECT * FROM TABLE1
WHERE STATUS != 'OK'
FOR UPDATE OF TABLE1.STATUS;
BEGIN
FOR r_operation IN c_operations
LOOP
UPDATE
TABLE1
SET
TABLE1.STATUS = 'OK'
WHERE
TABLE1.ID_TABLE1 = r_operation.ID_TABLE1;
END LOOP;
COMMIT;
-- Missing conversion from cursor to sys_refcursor
RETURN l_return;
END;
The update is working but Im still missing how to return the updated rows that are in the cursor (c_operations ).
Thank you.

I'm going to make some assumptions:
id_table1 is the primary key of the table, so your RBAR (*) update affects only one row
id_table1 is numeric
If these assumptions are wrong you will need to tweak the following code.
CREATE OR REPLACE FUNCTION FUNCTION_NAME
RETURN SYS_REFCURSOR
IS
l_return SYS_REFCURSOR;
l_id table1.id_table1%type;
l_upd_ids sys.odcinumberlist := new sys.odcinumberlist();
CURSOR c_operations IS
SELECT * FROM TABLE1
WHERE STATUS != 'OK'
FOR UPDATE OF TABLE1.STATUS;
BEGIN
FOR r_operation IN c_operations LOOP
UPDATE TABLE1
SET TABLE1.STATUS = 'OK'
WHERE TABLE1.ID_TABLE1 = r_operation.ID_TABLE1
returning TABLE1.ID_TABLE1 into l_id;
l_upd_ids.extend();
l_upd_ids(l_upd_ids.count()) := l_id;
END LOOP;
COMMIT;
open l_return for
select * from table(l_upd_ids);
RETURN l_return;
END;
The key points of the solution.
uses Oracle maintained collection (of number) sys.odcinumberlist to store the updated IDs;
uses RETURNING clause to capture the id_table1 value for the updated row;
stores the returned key in the collection;
uses a table() function to casrt the collection into a table which can be queried in the ref cursor.
This last point is why I chose to use sys.odcinumberlist rather than defining a collection in the procedure. It's a SQL type, so we can use it in SELECT statements.
(*) Row-by-agonizing-row. Updating single records in a PL/SQL loop is the slowest way of executing bulk updates, and normally constitutes an anti-pattern. A straightforward set-based UPDATE should suffice. However, you know your own situation so I'm going to leave that as it is.

It looks to me like you don't need the initial cursor, since you're changing the STATUS of every row which is not 'OK' to 'OK', so you can do this is a simple UPDATE statement. Then use an OPEN...FOR statement to return a cursor of all rows where STATUS is not 'OK', which shouldn't return anything because you've already changed all the status values to 'OK'. I suggest that you rewrite your procedure as:
CREATE OR REPLACE FUNCTION FUNCTION_NAME
RETURN SYS_REFCURSOR
IS
l_return SYS_REFCURSOR;
BEGIN
UPDATE TABLE1
SET STATUS = 'OK'
WHERE STATUS != 'OK';
COMMIT;
OPEN l_return FOR SELECT *
FROM TABLE1
WHERE STATUS != 'OK'
FOR UPDATE OF TABLE1.STATUS;
RETURN l_return;
END;

Instead of a loop to update how about a bulk update collecting the updated ids. Then a table function from those returned ids.
create type t_table1_id is
table of integer;
create or replace function set_table1_status_ok
return sys_refcursor
is
l_results_cursor sys_refcursor;
l_updated_ids t_table1_id;
begin
update table1
set status = 'Ok'
where status != 'Ok'
returning table1.id
bulk collect
into l_updated_ids;
open l_results_cursor for
select *
from table1
where id in (select * from table(l_updated_ids));
return l_results_cursor;
end set_table1_status_ok;
-- test
declare
updated_ids sys_refcursor;
l_this_rec table1%rowtype;
begin
updated_ids := set_table1_status_ok();
loop
fetch updated_ids into l_this_rec;
exit when updated_ids%notfound;
dbms_output.put_line ( l_this_rec.id || ' updated.');
end loop;
close updated_ids;
end ;

Related

Validation of result before executing the query

When running a stored procedure to fetch some rows, First I want to validate if the query will return a row before sending the result, and second if it is possible to validate without running the same query twice.
I am using a cursor to store the yielded result, So I tried the cursor attribute %ROWCOUNT & %NOTFOUND. But the doesnt quite work. Plus I want to do this without running a loop on the cursor.
procedure MODULE_LIST_GK(p_module_Id IN MODULE_LIST.MODULE_ID% TYPE,
p_Error_Code out nvarchar2,
p_Error_Msg out nvarchar2,
p_Cursor out sys_refcursor) IS
BEGIN
OPEN p_Cursor FOR
SELECT A.MODULE_ID,
A.MODULE_NM,
A.AUTH_STATUS_ID
FROM MODULE_LIST A
WHERE A.MODULE_ID=p_module_Id;
SELECT COUNT(MODULE_ID)
INTO v_row_num
FROM MODULE_LIST A
WHERE A.MODULE_ID=p_module_Id;
IF v_row_num=0 THEN
p_Error_Code := SQLCODE;
p_Error_Msg := 'Does not Exists';
Return;
end IF;
EXCEPTION
WHEN OTHERS THEN
p_error_code:= SQLCODE;
p_error_msg := SQLERRM;
END MODULE_LIST_GK;
Your implementation have several points that could be improved.
First if you expect that for lot of parameters the returned cursor will be empty,
than first check the empty cursor and only after this check open the cursor. You do it vice versa.
How to check if the cursor is empty? Unfortunatelly you must fetch the first row to be able to verify it.
open l_cur for
select id, status from tab where id = p_id;
fetch l_cur into l_id, l_status;
if l_cur%NOTFOUND then
p_Error_Msg := 'Does not Exists';
Return;
end if;
This check is far more effective that the often used count(*) as it is considering only the first (few) rows and not counting all rows in the cursor.
If the check fails you are ready, othervise simple open the cursor and return it.
open l_cur for
select id, status from tab where id = p_id;
p_Cursor := l_cur;
Two additional thinks come to mind.
You should rething the generall approach if the database is very dynamic. How would you handle the case when other session deletes some row between the check and the second open of the cursor?
Finally consider returning an exception instead of the return code.
In order to know whether a cursor contains rows, you must open it and fetch the first row. Once you've done this, it makes no sense anymore to return that cursor, for the recipient will not be able to fetch that first row, because the cursor already points beyond it.
So, you must select twice. What you'd do is to use ROWNUM or an EXISTS clause here to show the DBMS that you are not interested in any more rows. This can speed up the query extremely.
PROCEDURE module_list_gk(p_module_id IN MODULE_LIST.MODULE_ID%TYPE,
p_error_code OUT NVARCHAR2,
p_error_msg OUT NVARCHAR2,
p_cursor OUT SYS_REFCURSOR) IS
v_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM module_list
WHERE module_id = p_module_Id
AND ROWNUM = 1;
IF v_count = 0 THEN
p_error_code := 0; -- Or -1403 for NO DATA FOUND if you like
p_error_msg := 'Does not Exists';
RETURN;
END IF;
OPEN p_Cursor FOR
SELECT module_id, module_nm, auth_status_id
FROM module_list
WHERE module_id = p_module_id;
EXCEPTION WHEN OTHERS THEN
p_error_code:= SQLCODE;
p_error_msg := SQLERRM;
END module_list_gk;
SQLCODE for the first query will be 0 by the way (SELECT COUNT(*) returns one row with the number of records found - no error hence). This is why you should decide to either return zero explicitly or some error code, such as -1403.
Here is the same with EXISTS:
BEGIN
SELECT CASE WHEN EXISTS
(
SELECT NULL
FROM module_list
WHERE module_id = p_module_Id
) THEN 1 ELSE 0 END
INTO v_count
FROM DUAL;
IF v_count = 0 THEN

Oracle: How to populate/insert row to a Ref Cursor?

Really need help regarding Ref Cursor. I have a Stored Procedure GET_PERSONROLES that have parameter type ref cursor. I just wanted to pupulate this ref cursor manually like inserting a row to the refcursor.
Can I insert a row into a refcursor though a loop?
Thank you in advance.
The procedure depends on this publicly declared type:
create or replace package types
as
type cursorTypePersonRole is ref cursor;
end;
Here is my pseudo-codeL
create or replace PROCEDURE GET_PERSONROLES
(
P_CURSOR IN OUT types.cursorTypePersonRole
) AS
REFCUR SYS_REFCURSOR;
TYPE REFTABLETYPE IS RECORD (
IS_MANAGER_LEVEL1 VARCHAR2(1),
IS_MANAGER_LEVEL2 VARCHAR2(1)
);
TYPE TABLETYPE IS TABLE OF REFTABLETYPE;
PERSONROLES_TABLETYPE TABLETYPE;
BEGIN
--calls another stored proc to populate REFCUR with data without problem
MY_STOREDPROC('12345', REFCUR);
LOOP
FETCH REFCUR BULK COLLECT INTO PERSONROLES_TABLETYPE;
EXIT WHEN PERSONROLES_TABLETYPE.COUNT = 0;
FOR indx IN 1 .. PERSONROLES_TABLETYPE.COUNT
LOOP
-- I'm able to query perfectly the values of IS_MANAGER_LEVEL1 and IS_MANAGER_LEVEL 2
-- I'm aware that the below codes are wrong
-- However this means I wanted to insert these values to a row of the cursor if possible
-- Do some logic to know what data will be assigned in the row.
if PERSONROLES_TABLETYPE(indx).IS_MANAGER_LEVEL1 = 'Y' then
P_CURSOR := <DO SOME LOGIC AND ASSIGN THE VALUE TO THE ROW>
end if;
if PERSONROLES_TABLETYPE(indx).IS_MANAGER_LEVEL2 = 'Y' then
P_CURSOR := <DO SOME LOGIC AND ASSIGN THE VALUE TO THE ROW>
end if;
END LOOP;
END LOOP;
CLOSE REFCUR;
END GET_PERSONROLES;
A ref cursor is not a variable: it is a pointer to a result set which is consumed by the act of reading it. The result set itself is immutable.
Immutability makes sense, because it reflects Oracle's emphasis on read consistency.
The simplest way to produce the output you appear to want is to create a SQL Type
open P_CURSOR for
select IS_MANAGER_LEVEL1,
IS_MANAGER_LEVEL2
from table ( PERSONROLES_TABLETYPE );
This will work in 12c; in earlier versions to use the table() call like this you may need to declare REFTABLETYPE and TABLETYPE as SQL types( rather than in PL/SQL).
"Ok edited it now"
Alas your requirements are still not clear. You haven't given us the structure of the output ref cursor or shown what other processing you want to undertake.
However, given the title of your question, let's have a guess. So:
create or replace PROCEDURE GET_PERSONROLES ( P_CURSOR IN OUT types.cursorTypePersonRole) AS
REFCUR SYS_REFCURSOR;
TYPE REFTABLETYPE IS RECORD (IS_MANAGER_LEVEL1 VARCHAR2(1),
IS_MANAGER_LEVEL2 VARCHAR2(1));
TYPE TABLETYPE IS TABLE OF REFTABLETYPE;
PERSONROLES_TABLETYPE TABLETYPE;
personrole_rec PersonRole%rowtype;
type personrole_nt is table of PersonRole%rowtype;
personroles_recs personrole_nt := new personrole_nt() ;
BEGIN
MY_STOREDPROC('12345', REFCUR);
FETCH REFCUR BULK COLLECT INTO PERSONROLES_TABLETYPE;
FOR indx IN 1 .. PERSONROLES_TABLETYPE.COUNT LOOP
/* in the absence of requirements I'm just making some stuff up */
if PERSONROLES_TABLETYPE(indx).IS_MANAGER_LEVEL1 = 'Y' then
personrole_rec.whatever1 := 'something';
else
personrole_recc.whatever1 := null;
end if;
if PERSONROLES_TABLETYPE(indx).IS_MANAGER_LEVEL2 = 'Y' then
personrole_rec.whatever2 := 'something else';
else
personrole_recc.whatever2 := null;
end if;
if personrole_rec.whatever1 is not null
or personrole_rec.whatever2 is mot null then
personroles_recs.exend();
personroles_recs(personroles_recs.count()) := personroles_rec;
end if;
END LOOP;
CLOSE REFCUR;
open p_cursor for
select * from table ( personroles_recs );
END GET_PERSONROLES;
This code uses a second collection to store the desired output. Like your code it reads the populated collection and evaluates the attributes of each row. If a value which means the criteria it sets an attribute in a rowtype variable. If one or both attributes are set it populates a new row in a second collection. At the end of the procedure it opens the ref cursor using a table() function call on the second collection.
Note that you do not need the nested loop: you're not using the LIMIT clause so your coder reads the entire cursor into the collection in one swoop.
The implemented rules may not be exactly what you want (because you haven't explained exactly what you want) but this should give you the general idea.
Note that, depending on exactly what processing is masked by <DO SOME LOGIC AND ASSIGN THE VALUE TO THE ROW>, the simpler approach could still be feasible:
open P_CURSOR for
select case when IS_MANAGER_LEVEL1 = 'Y' then 'YES' else 'NO' end,
case when IS_MANAGER_LEVEL2 = 'Y' then 'YES' else 'NO' end
from table ( PERSONROLES_TABLETYPE );

Return table from function in Oracle

Following function works with SQL Server.
CREATE FUNCTION GET_EMP_PROB_START_ATT_CODE(#EVAL_TYPE VARCHAR(6))
RETURNS #EMP_PROB_START_ATT_CODE TABLE (
PROBATION_START_ATT_CODE varchar(6) NULL
)
AS
BEGIN
IF #EVAL_TYPE='New'
INSERT INTO #EMP_PROB_START_ATT_CODE (PROBATION_START_ATT_CODE)
SELECT NEW_EMP_PROB_START_ATT_CODE FROM HS_HR_PEA_DYNAMIC_ATTRIBUTES;
ELSE
INSERT INTO #EMP_PROB_START_ATT_CODE (PROBATION_START_ATT_CODE)
SELECT EXIS_EMP_PROB_START_ATT_CODE FROM HS_HR_PEA_DYNAMIC_ATTRIBUTES;
RETURN;
END;
I just want to convert this to Oracle equivalent query . Tried but not succeeded.
CREATE FUNCTION GET_EMP_PROB_START_ATT_CODE(EVAL_TYPE VARCHAR(6))
RETURN EMP_PROB_START_ATT_CODE IS TABLE
PROBATION_START_ATT_CODE VARCHAR(6) NULL;
AS
BEGIN
IF EVAL_TYPE='New'
INSERT INTO EMP_PROB_START_ATT_CODE (PROBATION_START_ATT_CODE)
SELECT NEW_EMP_PROB_START_ATT_CODE FROM HS_HR_PEA_DYNAMIC_ATTRIBUTES;
ELSE
INSERT INTO EMP_PROB_START_ATT_CODE (PROBATION_START_ATT_CODE)
SELECT EXIS_EMP_PROB_START_ATT_CODE FROM HS_HR_PEA_DYNAMIC_ATTRIBUTES;
RETURN;
END;
Warning: compiled but with compilation errors
Any help guys..
First of all, create a database-wide type:
create type string_tab6 is table of varchar2(6);
Then you would create your function like so:
create or replace function get_emp_prob_start_att_code(eval_type varchar2)
return string_tab6
is
v_array string_tab6;
begin
if eval_type = 'New' then
select new_emp_prob_start_att_code
bulk collect into v_array
from hs_hr_pea_dynamic_attributes;
else
select exis_emp_prob_start_att_code
bulk collect into v_array
from hs_hr_pea_dynamic_attributes;
end if;
return v_array;
end get_emp_prob_start_att_code;
/
However, I would question why you need such a function. What are you going to use it for?
In Oracle, I'd expect to see that logic embedded directly into whichever SQL statement needs that info, which you could easily do via a case statement.
First define row type. For existing database table you can use %ROWTYPE.
Then define table type.
Finally use the table type as return type of the function.
TYPE tr_row IS my_table%ROWTYPE;
TYPE tab_row IS TABLE OF tr_row;
FUNCTION get_row RETURN tab_row IS
mytab tab_row;
BEGIN
SELECT * FROM my_table
BULK COLLECT INTO mytab;
RETURN mytab;
END;

Sync between two tables and show the result in an oracle function

I am making an oracle function that sync values between two tables and My intention is to show a string that show how many rows affected and displays it when user execute the function.
my function creation is like this
CREATE OR REPLACE FUNCTION WELTESADMIN.DUM_MST_SYNC(PROJNAME IN VARCHAR2)
RETURN NUMBER IS NUM_ROWS NUMBER;
BEGIN
MERGE INTO MASTER_DRAWING DST
USING (SELECT WEIGHT_DUM, HEAD_MARK_DUM FROM DUMMY_MASTER_DRAWING) SRC
ON (DST.HEAD_MARK = SRC.HEAD_MARK_DUM)
WHEN MATCHED THEN UPDATE SET DST.WEIGHT = SRC.WEIGHT_DUM
WHERE DST.PROJECT_NAME = SRC.PROJECT_NAME_DUM
AND DST.PROJECT_NAME = PROJNAME;
dbms_output.put_line( sql%rowcount || ' rows merged' );
END;
if i execute the begin part in the TOAD or SQL Developer i can see how many rows affected. My target is to collect this function into a procedure and when user wants to sync tables they just need to run the procedure with PROJNAME value supplied for specific project.
Please help me on how to improve this code,
Best regards
You can use SQL%ROWCOUNT to get the number of rows affected by the MERGE. Add the following statement in your code immediately after the MERGE :
dbms_output.put_line( sql%rowcount || ' rows merged' );
To return this value, declare a NUMBER variable and assign the sql%rowcount value to it. And then return that value. Something like :
Function
.......
Return NUMBER
.......
num_rows NUMBER;
......
Begin
Merge....
num_rows := SQL%ROWCOUNT;
RETURN num_rows;
END;
And, you don't need a procedure to execute the function. You can execute in SQL :
select function(project_name)
from dual
/
Update Since OP is trying to use DML inside a function, need to make it autonomous transaction to be able to perform DML without raising the ORA-14551.
You could use the directive pragma autonomous_transaction. This will run the function into an independent transaction that will be able to perform DML without raising the ORA-14551. However, remember, the results of the DML will be committed outside of the scope of the parent transaction. If you have only single transaction, you could use the workaround that I suggested. Add, PRAGMA AUTONOMOUS_TRANSACTION; immediately after the RETURN statement before BEGIN.
CREATE OR REPLACE
FUNCTION WELTESADMIN.DUM_MST_SYNC(
PROJNAME IN VARCHAR2)
RETURN NUMBER
IS
num_rows NUMBER;
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
MERGE INTO MASTER_DRAWING DST USING
(SELECT WEIGHT_DUM, HEAD_MARK_DUM FROM DUMMY_MASTER_DRAWING
) SRC ON (DST.HEAD_MARK = SRC.HEAD_MARK_DUM)
WHEN MATCHED THEN
UPDATE
SET DST.WEIGHT = SRC.WEIGHT_DUM
WHERE DST.PROJECT_NAME = SRC.PROJECT_NAME_DUM
AND DST.PROJECT_NAME = PROJNAME;
num_rows := SQL%ROWCOUNT;
COMMIT;
RETURN num_rows;
END;
/

oracle stored procedure - select, update and return a random set of rows

oracle i wish to select few rows at random from a table, update a column in those rows and return them using stored procedure
PROCEDURE getrows(box IN VARCHAR2, row_no IN NUMBER, work_dtls_out OUT dtls_cursor) AS
v_id VARCHAR2(20);
v_workname VARCHAR2(20);
v_status VARCHAR2(20);
v_work_dtls_cursor dtls_cursor;
BEGIN
OPEN v_work_dtls_cursor FOR
SELECT id, workname, status
FROM item
WHERE status IS NULL
AND rownum <= row_no
FOR UPDATE;
LOOP
FETCH v_work_dtls_cursor
INTO v_id ,v_workname,v_status;
UPDATE item
SET status = 'started'
WHERE id=v_id;
EXIT
WHEN v_work_dtls_cursor % NOTFOUND;
END LOOP;
close v_work_dtls_cursor ;
/* I HAVE TO RETURN THE SAME ROWS WHICH I UPDATED NOW.
SINCE CURSOR IS LOOPED THRU, I CANT DO IT. */
END getrows;
PLEASE HELP
Following up on Sjuul Janssen's excellent recommendation:
create type get_rows_row_type as object
(id [item.id%type],
workname [item.workname%type],
status [item.status%type]
)
/
create type get_rows_tab_type as table of get_rows_row_type
/
create function get_rows (box in varchar2, row_no in number)
return get_rows_tab_type pipelined
as
v_work_dtls_cursor dtls_cursor;
l_out_rec get_rows_row_type;
BEGIN
OPEN v_work_dtls_cursor FOR
SELECT id, workname, status
FROM item sample ([ROW SAMPLE PERCENTAGE])
WHERE status IS NULL
AND rownum <= row_no
FOR UPDATE;
LOOP
FETCH v_work_dtls_cursor
INTO l_out_rec.id, l_out_rec.workname, l_outrec.status;
EXIT WHEN v_work_dtls_cursor%NOTFOUND;
UPDATE item
SET status = 'started'
WHERE id=l_out_rec.id;
l_out_rec.id.status := 'started';
PIPE ROW (l_out_rec);
END LOOP;
close v_work_dtls_cursor ;
END;
/
A few notes:
This is untested.
You'll need to replace the bracketed section in the type declarations with appropriate types for your schema.
You'll need to come up with an appropriate value in the SAMPLE clause of the SELECT statement; it might be possible to pass that in as an argument, but that may require using dynamic SQL. However, if your requirement is to get random rows from the table -- which just filtering by ROWNUM will not accomplish -- you'll want to do something like this.
Because you're SELECTing FOR UPDATE, one session can block another. If you're in 11g, you may wish to examine the SKIP LOCKED clause of the SELECT statement, which will enable multiple concurrent sessions to run code like this.
Not sure where you are doing your committing, but based on the code as it stands all you should need to do is SELECT ... FROM ITEM WHERE STATUS='started'
If it is small numbers, you could keep a collection of ROWIDs.
if it is larger, then I'd do an
INSERT into a global temporary table SELECT id FROM item .. AND ROWNUM < n;
UPDATE item SET status = .. WHERE id in (SELECT id FROM global_temp_table);
Then return a cursor of
SELECT ... FROM item WHERE id in (SELECT id FROM global_temp_table);
Maybe this can help you to do what you want?
http://it.toolbox.com/blogs/database-solutions/returning-rows-through-a-table-function-in-oracle-7802
A possible solution:
create type nt_number as table of number;
PROCEDURE getrows(box IN VARCHAR2,
row_no IN NUMBER,
work_dtls_out OUT dtls_cursor) AS
v_item_rows nt_number;
indx number;
cursor cur_work_dtls_cursor is
SELECT id
FROM item
WHERE status IS NULL
AND rownum <= row_no
FOR UPDATE;
BEGIN
open cur_work_dtls_cursor;
fetch cur_work_dtls_cursor bulk collect into nt_number;
for indx in 1 .. item_rows.count loop
UPDATE item
SET status = 'started'
WHERE id=v_item_rows(indx);
END LOOP;
close cur_work_dtls_cursor;
open work_dtls_out for select id, workname, status
from item i, table(v_item_rows) t
where i.id = t.column_value;
END getrows;
If the number of rows is particularly large, the global temporary solution may be better.

Resources