i have a scenario where i need to create a dynamic query with some if/else statements, i have prepared the query but unable to use this inside loop below is the snippet of what i am trying to acheive.
Query :
select user_name into username from table1 where user ='user1';
for(query)
Loop
Dbms_output.put_line('user name ' || user_name);
END Loop;
is this possible to use the vaiable in the for loop ?
Not really. Notice in the 19c PL/SQL Reference it says:
cursor
Name of an explicit cursor (not a cursor variable) that is not open when the cursor FOR LOOP is entered.
You have to code this the long way, by explicitly fetching the cursor into a record until you hit cursorname%notfound, e.g.
create or replace procedure cheese_report
( cheese_cur in sys_refcursor )
as
type cheese_detail is record
( name varchar2(15)
, region varchar2(30) );
cheese cheese_detail;
begin
loop
fetch cheese_cur into cheese;
dbms_output.put_line(cheese.name || ', ' || cheese.region);
exit when cheese_cur%notfound;
end loop;
close cheese_cur;
end;
Test:
declare
cheese sys_refcursor;
begin
open cheese for
select 'Cheddar', 'UK' from dual union all
select 'Gruyere', 'France' from dual union all
select 'Ossau Iraty', 'Spain' from dual union all
select 'Yarg', 'UK' from dual;
cheese_report(cheese);
end;
/
Cheddar, UK
Gruyere, France
Ossau Iraty, Spain
Yarg, UK
In Oracle 21c you can simplify this somewhat, though you still have to know the structure of the result set:
create or replace procedure cheese_report
( cheese_cur in sys_refcursor )
as
type cheese_detail is record
( cheese varchar2(15)
, region varchar2(30) );
begin
for r cheese_detail in values of cheese_cur
loop
dbms_output.put_line(r.cheese || ', ' || r.region);
end loop;
end;
You could parse an unknown ref cursor using dbms_sql to find the column names and types, but it's not straightforward as you have to do every processing step yourself. For an example of something similar, see www.williamrobertson.net/documents/refcursor-to-csv.shtml.
Related
Apologies for the newbie question, I am writing an Oracle stored procedure that opens a cursor for a specific SQL, calculates some variables for each row returned by the cursor but the stored procedure should return as a result set these variables that have been calculated for each row returned by the cursor. I am a bit confused on how to do this - can anyone help?!
I did read some of it so far I have something like this (just a trimmed down example and not exact code) but just need to return v_calc and v_calc_res in a result set:-
CREATE OR REPLACE procedure sp_test
(
in_input in number,
out_return out sys_refcursor
)
as
v_calc number;
v_calc_res number;
CURSOR C_test IS
select blah from test where blah = in_input;
begin
open c_test
loop
fetch c_test into v_calc;
v_calc_res := v_calc*5;
end loop;
end;
If you want a procedure to return a reference cursor for the calling routine to consume the procedure itself cannot then consume it. Cursors, including reference cursors are 1 way, 1 time consumables. As for as the desired calculations, they can be added to select defined for the cursor. So:
-- setup
create table test (blah integer, blah_stuff varchar2(50) );
-- build sp
create or replace procedure sp_blah_text(
in_input in number
, out_cur out sys_refcursor
)
is
begin
open out_cur for
select blah, blah_stuff, blah*5 as blah_x_5
from test
where blah = in_input;
end sp_blah_text;
-- test data
insert into test(blah, blah_stuff)
select 1,'a' from dual union all
select 2,'b' from dual union all
select 2,'x' from dual union all
select 2,'z' from dual union all
select 3,'c' from dual;
-- test
declare
ref_cur sys_refcursor;
l_blah test.blah%type;
l_stuff test.blah_stuff%type;
l_blah_5 test.blah%type;
begin
dbms_output.enable(null);
sp_blah_text(2,ref_cur);
loop
fetch ref_cur
into l_blah
, l_stuff
, l_blah_5;
exit when ref_cur%notfound;
dbms_output.put_line('blah=' || l_blah || ',stuff=' || l_stuff || ',blah*5=' || l_blah_5);
end loop;
end;
This works a treat thank you very much. I now have a performance issue that maybe you could help with. When I open the cursor, I then run several other SELECT statements to retrieve values using the variables from the cursor (see below). I assume this is because the switch between PL/SQL and SQL engine. Would using table collections help? But as I see since I need different columns from different tables I would need to have several different collections, how could I output everything in one record?
CREATE OR REPLACE procedure sp_test
(
in_input in number
)
as
v_calc number;
v_calc_res number;
v_blah_blah number;
v_blah_blah_blah number;
v_blah_blah_blah number;
CURSOR C_test IS
select blah from test where blah = in_input;
begin
open c_test
loop
fetch c_test into v_calc;
select blah_blah into v_blah_blah from t_blah_blah;
select blah_blah_blah into v_blah_blah_blah from t_blah_blah_blah;
select blah_blah_blah_blah into v_blah_blah_blah_blah from t_blah_blah_blah_blah;
v_calc_res := v_calc*5*v_blah_blah*v_blah_blah_blah*v_blah_blah_blah_blah
end loop;
end;
I dont know if it is possible but I would like to do this in PL/SQL
Let's say I have a parameter in my procedure, a number : numberColumns.
Inside the procedure I would like to create a record :
TYPE arrayColumn IS RECORD (
column1 VARCHAR2(200),
column2 VARCHAR2(200)...
... as much à numberColumns value
....
);
ty
This can be achieved easily with with OBJECT type in Oracle. But this kind of architecture is not at all suggested. Hope this below solution helps.
CREATE OR REPLACE
PROCEDURE test_obj_form(
a NUMBER )
AS
lv_sql VARCHAR2(32676);
BEGIN
SELECT '('
||listagg(str,',') WITHIN GROUP (
ORDER BY lvl)
||')'
INTO lv_sql
FROM
(SELECT 1 dum,
'column'
||LEVEL
||' '
||'varchar2(200)' str,
level lvl
FROM dual
CONNECT BY LEVEL < A
ORDER BY LEVEL
)
GROUP BY dum;
EXECUTE IMMEDIATE 'CREATE OR REPLACE TYPE name_rec IS OBJECT '||lv_sql;
dbms_output.put_line(lv_sql);
END;
/
Rather than using a record, you could use a collection:
CREATE OR REPLACE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/
Then do:
CREATE OR REPLACE PROCEDURE your_procedure(
number_columns IN INTEGER,
values OUT stringlist
)
IS
BEGIN
values := stringlist();
IF number_columns < 1 THEN
RETURN;
END IF;
values.EXTEND( number_columns );
FOR i IN 1 .. number_columns LOOP
values(i) := DBMS_RANDOM.STRING( 'X', 100 ); -- Assign some value
END LOOP;
END;
/
Otherwise, if you really want a record, then you will have to result to dynamic SQL.
I am trying to create a PL/SQL script that extracts a root "object" together with all children and other relevant information from an oracle production database. The purpose is to create a set of test-data to recreate issues that are encountered in production. Due to data protection laws the data needs to be anonymized when extracted - object names, certain types of id's, and monetary amounts need to be replaced.
I was trying to create one or more temporary translation tables, which would contain both the original values and anonymized versions. Then I would join the real data with the translation tables and output the anonymized values wherever required.
DECLARE
rootId integer := 123456;
TYPE anonTableRow IS RECORD
(
id NUMBER,
fieldC NUMBER,
anonymizedFieldC NUMBER
);
TYPE anonTable IS TABLE OF anonTableRow;
anonObject anonTable;
BEGIN
FOR cursor_row IN
(
select
id,
fieldC,
1234 -- Here I would create anonymized values based on rowNum or something similar
from
prodTable
where id = rootId
)
LOOP
i := i + 1;
anonObject(i) := cursor_row;
END LOOP;
FOR cursor_row IN
(
select
prod_table.id,
prod_table.fieldB,
temp_table.anonymizedFieldC fieldC,
prod_table.fieldD
from
prod_table
inner join table(temp_table) on prod_table.id = temp_table.id
where prod_table.id = 123456789
)
LOOP
dbms_output.put_line('INSERT INTO prod_table VALUES (' || cursor_row.id || ', ' || cursor_row.fieldB || ', ' || cursor_row.fieldC || ', , ' || cursor_row.fieldD);
END LOOP;
END;
/
However I ran into several problems with this approach - it seems to be near impossible to join oracle PL/SQL tables with real database tables. My access to the production database is severely restricted, so I cannot create global temporary tables, declare types outside PL/SQL or anything of that sort.
My attempt to declare my own PL/SQL types failed with the problems mentioned in this question - the solution does not work for me because of the limited permissions.
Is there a pure PL/SQL way that does not require fancy permissions to achieve something like the above?
Please Note: The above code example is simplified a lot and would not really require a separate translation table - in reality I need access to the original and translated values in several different queries, so I would prefer not having to "recalculate" translations everywhere.
If your data is properly normalized, then I guess this should only be necessary for internal IDs (not sure why you need to translate them though).
The following code should work for you, keeping the mappings in Associative Arrays:
DECLARE
TYPE t_number_mapping IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;
mapping_field_c t_number_mapping;
BEGIN
-- Prepare mapping
FOR cur IN (
SELECT 101 AS field_c FROM dual UNION ALL SELECT 102 FROM dual -- test-data
) LOOP
mapping_field_c(cur.field_c) := mapping_field_c.COUNT; -- first entry mapped to 1
END LOOP;
-- Use mapping
FOR cur IN (
SELECT 101 AS field_c FROM dual UNION ALL SELECT 102 FROM dual -- test-data
) LOOP
-- You can use the mapping when generating the `INSERT` statement
dbms_output.put_line( cur.field_c || ' mapped to ' || mapping_field_c(cur.field_c) );
END LOOP;
END;
Output:
101 mapped to 1
102 mapped to 2
If this isn't a permanent piece of production code, how about "borrowing" an existing collection type - e.g. one define in SYS that you can access.
Using this script from your schema you can generate a SQL Plus script to describe all SYS-owned types:
select 'desc ' || type_name from all_types
where typecode = 'COLLECTION'
and owner = 'SYS';
Running the resulting script will show you the structure of all the ones you can access. This one looks potentially suitable for example:
SQL> desc KU$_PARAMVALUES1010
KU$_PARAMVALUES1010 TABLE OF SYS.KU$_PARAMVALUE1010
Name Null? Type
----------------------------------------- -------- ----------------------------
PARAM_NAME VARCHAR2(30)
PARAM_OP VARCHAR2(30)
PARAM_TYPE VARCHAR2(30)
PARAM_LENGTH NUMBER
PARAM_VALUE_N NUMBER
PARAM_VALUE_T VARCHAR2(4000)
Of course, you can't guarantee that type will still exist or be the same or be accessible to you after a database upgrade, hence my caveat at the start.
More generic way to achieve this goal.
In my example i'm using xquery flwor expressions and dbms_xmlstore. Knowledge about xquery is mandatory.
create table mask_user_objects as select * from user_objects where rownum <0;
declare
v_s_table varchar2(30) := 'USER_OBJECTS'; --uppercase!!!
v_d_table varchar2(30) := 'MASK_USER_OBJECTS'; --uppercase!!!
v_mask_columns xmltype := xmltype('<COLS><OBJECT_NAME>XXXX</OBJECT_NAME>
<DATA_OBJECT_ID>-1</DATA_OBJECT_ID>
<OBJECT_TYPE/>
</COLS>'); --uppercase!!!
insCtx DBMS_XMLSTORE.ctxType;
r NUMBER;
v_source_table xmltype;
v_cursor sys_refcursor;
begin
open v_cursor for 'select * from '||v_s_table||' where rownum <100 ';
v_source_table := xmltype(v_cursor);
close v_cursor;
-- Load source table into xmltype.
insCtx := DBMS_XMLSTORE.newContext(v_d_table); -- Get saved context
for rec in (
select tt.column_value from xmltable('
let $col := $anomyze/COLS
for $i in $doc/ROWSET/ROW
let $row := $i
return <ROWSET>
<ROW>
{
for $x in $row/*
return if(
exists($col/*[name() = $x/name()] )
) then element{$x/name()}{ $col/*[name() = $x/name()]/text() }
else element{$x/name()}{$x/text()}
}
</ROW>
</ROWSET>
'
passing v_source_table as "doc"
, v_mask_columns as "anomyze"
) tt) loop
null;
r := DBMS_XMLSTORE.insertXML(insCtx, rec.column_value);
end loop;
DBMS_XMLSTORE.closeContext(insCtx);
end;
How can i rewrite below pl/sql block in order to avoid hard code column names one by one ? Below data are from OE schema. For orders table there are 8 columns on this table. Is it possible to output the results without hard code column names ? Any help is appreciated.
create or replace PACKAGE show_details AS
TYPE rt_order IS REF CURSOR RETURN orders%ROWTYPE;
TYPE typ_cust_rec IS RECORD
(cust_id NUMBER(6), cust_name VARCHAR2(20),
custphone customers.phone_numbers%TYPE,
credit NUMBER(9,2), cust_email VARCHAR2(30));
TYPE rt_cust IS REF CURSOR RETURN typ_cust_rec;
--Get order detail
PROCEDURE get_order(p_orderid IN NUMBER, p_cv_order IN OUT rt_order);
--Get customer detail
PROCEDURE get_cust(p_custid IN NUMBER, p_cv_cust IN OUT rt_cust);
END show_details;
create or replace PACKAGE BODY show_details
AS
PROCEDURE get_order (p_orderid IN NUMBER, p_cv_order IN OUT rt_order)
IS
BEGIN
OPEN p_cv_order FOR
SELECT * FROM orders
WHERE order_id = p_orderid;
-- CLOSE p_cv_order
END get_order;
PROCEDURE get_cust (p_custid IN NUMBER, p_cv_cust IN OUT rt_cust)
IS
BEGIN
OPEN p_cv_cust FOR
SELECT customer_id, cust_first_name,phone_numbers,
credit_limit,cust_email FROM customers WHERE customer_id = p_custid;
-- CLOSE p_cv_cust
END get_cust;
END;
SET SERVEROUTPUT ON SIZE UNLIMITED;
declare
cur_orders show_details.rt_order;
v_ordertab cur_orders%ROWTYPE;
begin
show_details.get_order(p_orderid =>2397, p_cv_order =>cur_orders);
LOOP
FETCH cur_orders INTO v_ordertab;
EXIT WHEN cur_orders%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ORDER_ID: ' || v_ordertab.ORDER_ID || ' ORDER_DATE: ' || v_ordertab.ORDER_DATE || ' ORDER_MODE: ' || v_ordertab.ORDER_MODE || ' CUSTOMER_ID: ' || v_ordertab.CUSTOMER_ID);
END LOOP;
exception
when others then
DBMS_OUTPUT.put_line ('Error Code : ' || SQLCODE);
end;
/
Since you're executing this from SQL Developer, you can use the variable and print commands, which are carried over from SQL*Plus:
variable cur_orders refcursor;
exec show_details.get_order(p_orderid => 2397, p_cv_order => :cur_orders);
print cur_orders
CUR_ORDERS
-------------------------------------------------------------------------------------------------------------------------------------------------
ORDER_ID CUSTOMER_ID ORDER_MODE ORDER_DATE
--------------------------------------- --------------------------------------- --------------------------------------- -------------------------
2397 42 0 03-JAN-15
Notice that you're passing the ref cursor variable as a bind variable, so there is a colon before the name in the call (:cur_order). And you need to run script rather than run statement.
exec is just a shorthand for an anonymous block, so you could do it explicitly if you prefer, but the effect is the same:
var cur_orders refcursor;
begin
show_details.get_order(p_orderid => 2397, p_cv_order => :cur_orders);
end;
/
print cur_orders
You can also get the output in the result grid if you prefer, as shown here, but print is a bit closer to your dbms_output version. Or you can have a wrapper function so you could call query the procedure from a plain SQL call; depends what your end goal is, and if you're just manually executing the procedure to check the output then print may also be good enough.
I need to open a cursor while table name, columns and where clause are varying. The table name etc will be passed as parameter. For example
CURSOR batch_cur
IS
SELECT a.col_1, b.col_1
FROM table_1 a inner join table_2 b
ON a.col_2 = b.col_2
WHERE a.col_3 = 123
Here, projected columns, table names, join criteria and where clause will be passed as parameters. Once opened, i need to loop through and process each fetched record.
You need to use dynamic SQL something like this:
procedure dynamic_proc
( p_table_1 varchar2
, p_table_2 varchar2
, p_value number
)
is
batch_cur sys_refcursor;
begin
open batch_cur for
'select a.col_1, b.col_1
from ' || p_table_1 || ' a inner join || ' p_table_2 || ' b
on a.col_2 = b.col_2
where a.col_3 = :bind_value1';
using p_value;
-- Now fetch data from batch_cur...
end;
Note the use of a bind variable for the data value - very important if you will re-use this many times with different values.
From your question i guess you need a dynamic cursor. Oracle provides REFCURSOR for dynamic sql statements. Since your query will be built dynamically you need a refcursor to do that.
create procedure SP_REF_CHECK(v_col1 number,v_col2 date,v_tab1 number,v_var1 char,v_var2 varchar2)
is
Ref_cur is REF CURSOR;
My_cur Ref_cur;
My_type Table_name%rowtype;
stmt varchar2(500);
begin
stmt:='select :1,:2 from :3 where :4=:5';
open My_cur for stmt using v_col1,v_col2,v_tab1,v_var1,v_var2;
loop
fetch My_cur into My_type;
//do some processing //
exit when My_cur%notfound;
end loop;
close My_cur;
end;
Check this link for more http://docs.oracle.com/cd/B10500_01/appdev.920/a96624/11_dynam.htm