Pass an array to bind variables - oracle

I have a table that contains queries, for example:
select text from queries;
TEXT
1 select item from items where item_no between :low_item_no and :high_item_no and description <> :irellevant
The queries already contains the place holders for the bind variables.
The values themselves exists in variables table:
select * from vars;
ID NAME VALUE
1 1 low_item_no 100
2 2 high_item_no 300
3 3 irellevant_desc old
I have a package that takes the query and execute it with
execute immediate statement
but how do I bind those variables?
I don't know how much variables I have in such query, it's not static.
I wish to have a way to do something like that:
Execute immedaite my_query_str using v_array_of_vars;
Until now I don't know of a way to do something like that, only with list of variables for example:
Execute immedaite my_query_str using v_1, v_2, v_3;
Thanks!

I don't think you can do this with execute immediate as too much is unknown at compile time, so you'll have to use the dbms_sql package instead.
Here's a quick demo that gets the query and variables based on a common query ID. This assumes that the values in vars.name actually match the bind variable names in queries.text, and I haven't included any checks or error handling for that or other potential issues, or dealt with multiple select-list items or data types - just the basics:
declare
my_query_str queries.text%type;
my_cursor pls_integer;
my_result pls_integer;
my_col_descs dbms_sql.desc_tab2;
my_num_cols pls_integer;
my_item items.item%type;
begin
select text into my_query_str from queries where query_id = 42;
dbms_output.put_line(my_query_str);
-- open cursor
my_cursor := dbms_sql.open_cursor;
-- parse this query
dbms_sql.parse(my_cursor, my_query_str, dbms_sql.native);
-- bind all variables by name; assumes bind variables match vars.name
for r in (select name, value from vars where query_id = 42) loop
dbms_output.put_line('Binding ' || r.name || ' || with <' || r.value ||'>');
dbms_sql.bind_variable(my_cursor, r.name, r.value);
end loop;
my_result := dbms_sql.execute(my_cursor);
dbms_output.put_line('execute got: ' || my_result);
dbms_sql.describe_columns2(my_cursor, my_num_cols, my_col_descs);
dbms_sql.define_column(my_cursor, 1, my_item, 30); -- whatever size matches 'item'
-- fetch and do something with the results
while true loop
my_result := dbms_sql.fetch_rows(my_cursor);
if my_result <= 0 then
exit;
end if;
dbms_sql.column_value(my_cursor, 1, my_item);
dbms_output.put_line('Got item: ' || my_item);
end loop;
dbms_sql.close_cursor(my_cursor);
end;
/
You don't seem to really need an array; but if you wanted to you could create and populate an associative array as name/value pairs and then use that fir the binds.
This is just a starting point; you may have to deal with an unknown number and/or types of columns being returned, though if that's the case processing them meaningfully will be a challenge. Perhaps you need to return the result of the query as a ref cursor, which is even simpler; demo using the SQL*Plus variable and print commands:
var rc refcursor;
declare
my_query_str queries.text%type;
my_cursor pls_integer;
my_result pls_integer;
begin
select text into my_query_str from queries where query_id = 42;
dbms_output.put_line(my_query_str);
-- open cursor
my_cursor := dbms_sql.open_cursor;
-- parse this query
dbms_sql.parse(my_cursor, my_query_str, dbms_sql.native);
-- bind all variables by name; assumes bind variables match vars.name
for r in (select name, value from vars where query_id = 42) loop
dbms_output.put_line('Binding ' || r.name || ' || with <' || r.value ||'>');
dbms_sql.bind_variable(my_cursor, r.name, r.value);
end loop;
my_result := dbms_sql.execute(my_cursor);
dbms_output.put_line('execute got: ' || my_result);
:rc := dbms_sql.to_refcursor(my_cursor);
end;
/
print rc
Notice you don't close the cursor inside the PL/SQL block in this scenario.
You could also convert to a ref cursor and then fetch from that within your procedure - there's a bulk-collect example in the docs - but again you'd need to know the number and types of the select-list items to do that.

Related

ORA-06511: PL/SQL: cursor already open. I am closing my cursor but no luck

I am getting the ORA-06511: PL/SQL: cursor already open ERROR.
Not sure why I am getting this error since I am closing my cursor.
Please see code below.
BEGIN
OPEN findem; ---OPENING HERE!
FOR crfindem IN findem LOOP
FETCH findem into other1, other2, other3;
l_CollectionOfRows(Counter).tmps_key := other1;
l_CollectionOfRows(Counter).tmps_cfb_rate := other2;
l_CollectionOfRows(Counter).tmps_engagement_pay_rate := other3;
Counter := Counter + 1;
END LOOP;
CLOSE findem;---CLOSING HERE!
FORALL i IN l_CollectionOfRows.FIRST .. l_CollectionOfRows.LAST
UPDATE Base.Table
SET MARGIN = :PAGE56_MARGIN,
PERCENT = :PAGE56_MARGIN + l_CollectionOfRows(i).rate,
PAY_RATE = (l_CollectionOfRows(i).pay_rate * (:PAGE56_MARGIN + l_CollectionOfRows(i).rate)) + l_CollectionOfRows(i).pay_rate
WHERE tmps_key = l_CollectionOfRows(i).tmps_key;
END;
I read from some online threads that for every Insert/Update statement, Oracle will create an implicit cursor. If this is the case how do you treat those implicit cursors that Oracle creates?
You are getting that error because you are opening the same cursor twice: the FOR construct already does all these things for you:
FOR opens the cursor
FOR implicitly declares a record variable (the one named crfindem, in your code) that will receive the values for each row read from the cursor
FOR loops on every row and assigns the values of the current row to the crfindem variable
FOR automatically closes the cursor at the end of the loop
so you don't need any OPEN/CLOSE/FETCH .. INTO commands if you are using a FOR loop:
see this simple example: itjust works.
declare
cursor cur is select * from user_tab_comments;
begin
for c in cur loop
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
end;
but if i try to open the cursor before using the for loop, I will get your same error because the cursor is already open and the for construct is trying to open it again:
declare
cursor cur is select * from user_tab_comments;
begin
open cur; -- this is not needed and will cause problems
for c in cur loop --! ERROR: here I am trying to open AGAIN the same cursor
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
end;
so, you either must choose if you want to write this code:
declare
cursor cur is select * from user_tab_comments;
begin
for c in cur loop
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
end;
or avoid using the FOR construct and do all the open/fetch/close operations by yourself, by writing this:
declare
-- I cant' use "select *" here:
-- if I use "fetch into" to a precise list of variables,
-- I have to extract exactly the fields I want to assign:
cursor cur is
select table_name,comments
from user_tab_comments;
tabname varchar(100);
tabcomment varchar2(4000);
begin
open cur;
loop
fetch cur into tabname,tabcomment;
exit when cur%notfound;
DBMS_OUTPUT.PUT_LINE( tabname || ' - ' || tabcomment);
end loop;
close cur;
end;
Your error is that your code is trying to do both these things at the same time.
you should have written this:
-- OPEN findem; NO NEED TO OPEN THE CURSOR (when using FOR)
FOR crfindem IN findem LOOP
--- FETCH findem into other1, other2, other3; FOR ALREADY DOES THIS: the values are in crfindem
l_CollectionOfRows(Counter).tmps_key := crfindem.name_of_the_first_field;
l_CollectionOfRows(Counter).tmps_cfb_rate := crfindem.name_of_the_second_field;
l_CollectionOfRows(Counter).tmps_engagement_pay_rate := crfindem.name_of_the_third_field;
Counter := Counter + 1;
END LOOP;
--- CLOSE findem; NO NEED TO CLOSE THE CURSOR (when using FOR)
Now let me add some considerations about your code (and about this example):
I don't see where you initialize your Counter variable: you MUST initialize it to 0 before entering the loop, because otherwise its initial value will be NULL and will stay null for the whole operation because (NULL + 1) evaluates again to NULL.
I don't see how your cursor is declared, so I don't know the names of the fields it extracts. in the code above I used the "fake" names name_of_the_first_field, name_of_the_second_field, name_of_the_third_field... but you must use the correct field names returned by your query
if your cursor returns some calculated value (like "select 1+2, sysdate, null from dual") you must assign a name to the calculated column to make it accessible by giving an alias to each calculated column you extract ("select 1+2 AS first_name, sysdate AS second_name, null as third_name from dual")
Edit... another info: you don't really need to declare a variable for each field even when you are explicitly using open/fetch/close: you can declare a RECORD variable (that will contain all column values with the same column names, exactly like the for loop does) by using the %ROWTYPE syntax. my example becomes like this, using %rowtype:
declare
cursor cur is select * from user_tab_comments;
-- here I am declaring a variable named c that is a RECORD variable:
-- it can contain a whole row returned by cursor cur
c cur%rowtype;
begin
open cur;
loop
fetch cur into c;
exit when cur%notfound;
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
close cur;
end;

Oracle single row into variable

I need to get all result in one row... it is working but when i want to see it in dbms there is nothink.. why ?
CREATE OR REPLACE PROCEDURE NXMESEP.SP_IN_CHECK_AND_SEND_SMS
( RC_TABLE0 OUT SYS_REFCURSOR,
RS_CODE OUT VARCHAR2, -- RETURN 코드
RS_MSG OUT VARCHAR2
) IS ERROR_EXCEPTION EXCEPTION;
BEGIN
begin
DECLARE
promena varchar2(32767);
BEGIN
OPEN RC_TABLE0 FOR
SELECT listagg(ITEM_ID,', ') within group(order by ITEM_ID)
INTO promena
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U';
LOOP
FETCH rc_table0 INTO promena;
EXIT WHEN rc_table0%NOTFOUND;
dbms_output.put_line(promena);
END LOOP;
CLOSE rc_table0;
end;
EXCEPTION
.... END;
RS_CODE := 'S'; RS_MSG := 'Complete successfully!';
RETURN; END SP_CHECK_AND_SEND_SMS; /
This should be promena that i expected..
" 12993NXUA, 13595NXUA, 14495NXUA, 16589NX, 16589NX, 16590NX, 16590NX, 16622NX, 16622NX "
Now it is working but im getting unknown error ORA-65535 every time when i execute. But after this I can see dbms result is ok.
Assuming your real code has RC_TABLE0 declared, as a ref cursor, then your variable ends up null because opening the cursor into something doesn't really do anything. You can't open a cursor and select something from the cursor query into a separate variable at the same time, whichever way round you try to do it. You need either a cursor, or a simple select ... into:
DECLARE
promena varchar2(32767);
BEGIN
SELECT listagg(ITEM_ID,', ') within group (order by ITEM_ID)
INTO promena
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U';
dbms_output.put_line('test: '||promena);
END;
/
test: 12993NXUA, 13595NXUA, 14495NXUA ...
PL/SQL procedure successfully completed.
You also have to set serveroutput on or equivalent to actually see the results, of course.
I've also removed the redundant distinct, the unnecessary select .. from dual - which seemed to be part of the odd cursor construct - and the extra level of begin/end.
Incidentally, your code implies that wo_date is a string, which seems unlikely, or at least not ideal. If it is actually a real date then you should not be using a string for the comparison as you're forcing implicit conversions; use an actual date instead, maybe as an ANSI date literal:
AND WO_DATE = DATE '2018-10-12'
If you did really want to use an explicit cursor approach you would need to use a loop to populate the string variable:
DECLARE
promena varchar2(32767);
rc_table0 sys_refcursor;
BEGIN
OPEN rc_table0 FOR
SELECT DISTINCT listagg(ITEM_ID,', ') within group (order by ITEM_ID)
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U';
LOOP
FETCH rc_table0 INTO promena;
EXIT WHEN rc_table0%NOTFOUND;
dbms_output.put_line('test: '||promena);
END LOOP;
CLOSE rc_table0;
END;
/
As you're only expecting a single row back there isn't much point doing that; and if you expected multiple rows (from a modified query, e.g. getting several days data and grouping by day) then an implicit cursor would be simpler anyway:
BEGIN
FOR r IN (
SELECT DISTINCT listagg(ITEM_ID,', ') within group (order by ITEM_ID) AS promena
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U'
)
LOOP
dbms_output.put_line('test: '||r.promena);
END LOOP;
END;
/
If this is really part of a procedure and the rc_table0 is an OUT parameter then you just can't do this. In code you posted as an answer you tried:
OPEN RC_TABLE0 FOR
SELECT listagg(ITEM_ID,', ') within group(order by ITEM_ID)
INTO promena
FROM TB_PL_M_WRKORD WRKOD
...
In that construct the into is still ignored, because the open doesn't fetch anything. And if you loop and fetch inside your procedure to display the results as I did above then you are consuming the result set, so the caller will get no results (or "ORA-01001: invalid cursor" if you close it inside the procedure).
You just can't do both, unless you re-open the cursor, which seems like overhead you probably don't want...

Oracle PL/SQL dynamic if statement global vars

I'm having trouble with dynamic sql, Issue is (I think) reading and setting global variable. Here's what I have and any help at all is greatly appreciated. Please let me know if you need table data too although I have included the data in comments.
CREATE OR REPLACE PACKAGE data_load
IS
curr_rec NUMBER;
curr_rule VARCHAR2(200);
curr_sql VARCHAR2(4000);
curr_sql_two VARCHAR2(4000);
curr_data_element VARCHAR2 (200);
curr_rule_text VARCHAR2(200);
curr_error_code VARCHAR2(10);
curr_error_flag VARCHAR2(10);
curr_flag_val NUMBER;
v_check NUMBER;
v_ID NUMBER;
cur_hdl INT ;
rows_processed NUMBER;
PROCEDURE check_rules;
END data_load;
The package body:
create or replace PACKAGE BODY data_load IS
PROCEDURE check_rules IS
CURSOR c1
IS
SELECT * FROM STAGING_TABLE where rownum < 3;
CURSOR c2
IS
SELECT * FROM ERROR_CODES WHERE rule_text IS NOT NULL AND status =1;
BEGIN
FOR rec1 IN c1
LOOP
FOR rec2 IN c2
LOOP
curr_data_element := 'rec1.'||rec2.data_element; --- this results in value "rec1.SHIP_FROM_ACCOUNT_ORG_CODE" without quotes
curr_rule_text := rec2.rule_text; --- this value is "is not null" without quotes
curr_error_flag := rec2.error_flag; --this value is "FLAG_03" without quotes
curr_flag_val := to_number(rec2.error_code); --- this value is 31
curr_sql :='begin if :curr_data_element '||curr_rule_text||' then update table_with_column_FLAG_03 set '||curr_error_flag ||' = 0; else update table_with_column_FLAG_03 set '||curr_error_flag ||' = '||curr_flag_val||'; end if; end;';
dbms_output.put_line(curr_sql); -- results in "begin if :curr_data_element is null then update table_with_column_FLAG_03 set FLAG_03 = 0; else update table_with_column_FLAG_03 set FLAG_03 = 31; end if; end;"
EXECUTE IMMEDIATE curr_sql USING curr_data_element ; -- this always updates the column with 31 even when curr_data_element/ rec1.SHIP_FROM_ACCOUNT_ORG_CODE is null and that's the problem
COMMIT;
END LOOP;
curr_rec := curr_rec+1;
END LOOP;
dbms_output.put_line(curr_rec);
END check_rules;
END data_load;
You've already highlighted the problem really:
curr_data_element := 'rec1.'||rec2.data_element; --- this results in value "rec1.SHIP_FROM_ACCOUNT_ORG_CODE" without quotes
You can't refer to cursor columns dynamically. You are creating a string with value 'rec1.SHIP_FROM_ACCOUNT_ORG_CODE'; there is no mechanism to evaluate what that represents. You can't, for instance, try to dynamically select that from dual because the rec1 is not in scope for a SQL call, even dynamically.
When you bind that string value it is never going to be null. You are using that string, not the value in the outer cursor that it represents, and essentially you cannot do that.
The simplest way to deal with this, if you have a reasonably small number of columns in your staging table that might appear as the rec2.data_element value, is to use a case expression to assign the appropriate actual rec1 column value to the curr_data_element variable, based on the rec2.data_element value:
...
BEGIN
FOR rec1 IN c1
LOOP
FOR rec2 IN c2
LOOP
curr_data_element :=
case rec2.data_element
when 'SHIP_FROM_ACCOUNT_ORG_CODE' then rec1.SHIP_FROM_ACCOUNT_ORG_CODE
when 'ANOTHER_COLUMN' then rec1.ANOTHER_COLUMN
-- when ... -- repeat for all possible columns
end;
curr_rule_text := rec2.rule_text;
...
If you have a lot of columns you could potentially do that via a collection but it may not be worth the extra effort.
The curr_sql string stays the same, all that's changing is that you're binding the actual value from the relevant rec1 column, rather than never-null string you were forming.

ROWID in casted pl-sql collection

Is there any rowid like facility in a pl-sql collection? In my case, while I am using this collection in an sql query, I also need the sequence number as they are put in. I know modification is data struecture is a way, but I want to use the index of the collection. so what I am looking for is something like this:
TYPE t_List IS TABLE OF VARCHAR2(200);
and
declare
v_Data t_List := t_List('data 1'
,'data_2'
,'data3');
......
FOR Rec IN (SELECT Column_Value v
,ROWID r
FROM TABLE(CAST(v_data t_list)))
LOOP
Dbms_Output.Put_Line('at ' || Rec.r || ':' || Rec.v);
-- .... and other codes here
END LOOP;
The loop is not expected to be executed in sequence, but I want something built-in like ROWID that is like the index of the collection.
Only schema-level types can be used in SQl statement, even within a PL/SQL block. As you seem to suggest you already know, you can create your own object type that includes the 'sequence' ID:
CREATE TYPE t_object AS OBJECT (
id NUMBER,
data VARCHAR2(200)
)
/
And a collection of that type:
CREATE TYPE t_List IS TABLE OF t_object;
/
And then populate the ID as you build the list:
DECLARE
l_List t_List := t_List(t_object(1, 'data 1')
,t_object(2, 'data_2')
,t_object(3, 'data3'));
BEGIN
FOR Rec IN (SELECT id, data
FROM TABLE(l_list))
LOOP
Dbms_Output.Put_Line('at ' || Rec.id || ':' || Rec.data);
-- .... and other codes here
END LOOP;
END;
/
Without an object type you can use the ROWNUM pseudocolumn:
CREATE TYPE t_List IS TABLE OF VARCHAR2(200);
/
DECLARE
v_Data t_List := t_List('data 1'
,'data_2'
,'data3');
BEGIN
FOR Rec IN (SELECT Column_Value v
,ROWNUM r
FROM TABLE(v_data))
LOOP
Dbms_Output.Put_Line('at ' || Rec.r || ':' || Rec.v);
-- .... and other codes here
END LOOP;
END;
/
anonymous block completed
at 1:data 1
at 2:data_2
at 3:data3
As far as I'm aware that isn't guaranteed to preserve the original creation sequence. I think it almost certainly will at the moment, but perhaps isn't something you should rely on as always being true. (There is no order without an order by, but here you don't have anything you can order by without destroying your initial order...).
If you query a subset of the table - I'm not sure what you mean by "the loop is not expected to be executed in sequence" - you'd need to generate the ROWNUM in a subquery before filtering or it won't be consistent. You'd also need to generate the ROWNUM in a subquery if you're joining this to other, real, tables - I imagine you are, otherwise you could use a PL/SQL collection.
If indexing is important, use Associative Arrays(Also known as Index-by tables)
Refer his.

Fetch variable from cursor error in oracle pl sql

I'm fetching value using cursor:
CURSOR Colcond
IS
SELECT CONDITION
FROM CONDITION_TAB
WHERE PROCEDURE_NAME = 'CALL_VOL';
In first iteration it would fetch "SUM(CASE WHEN CALL_REF=0 THEN 1 ELSE 0 END)".
In my program:
OPEN Colcond;
FETCH Colcond INTO cond_val;
SELECT Appnum, customer_num,'"cond_val"'
INTO iappnum, icustnum,icond_val
FROM CALL_DETAILS WHERE APPNUM = val_appl
AND customer_num = val_cust
Group by APPLICATION_NUM,CUST_SGMT_NUM,DNIS;
INSERT INTO S_CALL_VOLUME VALUES (iappnum, icustnum, SYSDATE, icond_val);
The record thRough the variable "icond_val" inserted is SUM(CASE WHEN CALL_REF=0 THEN 1 ELSE 0 END) instead of the value (10 or 20 or 50).
How to get the value instead of that Sum case statement?
You need to use dynamic SQL to incorporate the value you selected from the condition_tab table into the next query. Here's an example in an anonymous block rather than a procedure:
declare
val_appl number; -- procedure argument in your version?
val_cust number; -- procedure argument in your version?
query_string varchar2(2000);
cond_val condition_tab.condition%type;
iappnum call_details.appnum%type;
icustnum call_details.customer_num%type;
icond_val number;
cursordyn sys_refcursor;
cursor colcond is
select condition
from condition_tab
where procedure_name = 'CALL_VOL';
begin
open colcond;
fetch colcond into cond_val;
close colcond;
query_string:='select appnum, customer_num, ' || cond_val || ' from call_details '
|| 'where appnum = :val_appl and customer_num = :val_cust '
|| 'group by application_num,cust_sgmt_num,dnis';
open cursordyn for query_string using val_appl, val_cust;
fetch cursordyn into iappnum, icustnum, icond_val;
close cursordyn;
insert into s_call_volume values (iappnum, icustnum, sysdate, icond_val);
end;
/
Your column names seem to be a bit inconsistent so it probably needs some tweaking.
For both cursors you're only selecting one row, so (a) they don't really need to be cursors, they can just be select into statements; and (b) the second one is selecting the two columns from the where clause which seems a bit pointless - when you use iappnum in the insert, you could just use val_app, etc. So I think you could simplify this to:
declare
val_appl number; -- procedure argument in your version?
val_cust number; -- procedure argument in your version?
query_string varchar2(2000);
cond_val condition_tab.condition%type;
icond_val number;
begin
select condition
into cond_val
from condition_tab
where procedure_name = 'CALL_VOL';
query_string:='select ' || cond_val || ' from call_details '
|| 'where appnum = :val_appl and customer_num = :val_cust '
|| 'group by application_num,cust_sgmt_num,dnis';
execute immediate query_string into icond_val using val_appl, val_cust;
insert into s_call_volume values (val_appl, val_cust, sysdate, icond_val);
end;
/
This will error if either query doesn't return exactly one row. Your cursor version will error if the condition_tab query finds no data, and will only use one 'condition' if there are multiples; and will only use the first result from the second query if there are multiples. If you're expecting multiples from either (not sure what your actual grouping is supposed to be, it looks inconsistent too) then you need to loop over the cursor, fetching repeatedly.
Hopefully this will get you started though.

Resources