PL/SQL Macro, like in C Programming - oracle

So i've been studying C programming about Macros and using them, but in my job i'm using PL/SQL a lot and I was wondering if there is some way to do the same type of thing in PL/SQL. Right now I have it calling a function with 3 different values and then returning a value but the function is so simple that I think I could do it from inside of the original stored procedure. In C a Macro is a line (or lines) of code that just get completely replaced by the call upon compilation, but it is way more efficient than calling a function over and over.
Example from C:
#define query(fieldValue, Attribute, Table) (select fieldValue from Table where record = Attribute)
and when called in the body of the code, query(value, value, value) would get completely replaced by the select statement.
just a rough example, of how it might appear in C because i'm really not sure how it would be in PL/SQL.
Is this possible in SQL? It would have to be for 2-3 lines of code, thats it.
Thanks a lot,
SMKS

There is a way to use macros in most any language, but it does require an extra step.
The m4 macroprocessor can be used as a pre-processor.
There are quite a few resources available for m4, just google for m4 macro processor.
Here is a simple PL/SQL example
Following is the macro file.
The m4_discard and m4_undiscard code is not strictly necessary, it serves to reduce whitespace in the output.
'define(m4_discard', define(m4_undiscard', `divert'(divnum))divert(-1)')dnl"
m4_discard
define(`pl_sleep',`dbms_lock.sleep($1);')
define(`noop',`null;')
define(`useless_loop',
`for i in 1..$1
loop
$2($3)
end loop;')
m4_undiscard
Following is the PL/SQL file test.m4sql
include(./macros)
useless_loop(`10',`pl_sleep',`.1')
useless_loop(`10',`noop',`.1')
Now for some PL/SQL pre-processing
> m4 test.m4sql
for i in 1..10
loop
dbms_lock.sleep(.1);
end loop;
for i in 1..10
loop
null;
end loop;
There is still more white space than I would like, but you can see how m4 might be used.
Whether or not you use this depends on how badly you want to use macros.
As stated in other answers, dynamic SQL may be the answer.
At times dynamic SQL may work well, but for some uses, such as dynamic block of PL/SQL, it is a coding nightmare.
My intent is to use m4 on a current PL/SQL project where I need some logging code at the beginning and end of each procedure. This use is not really a good fit for dynamic SQL in my opinion, and m4 only needs to be used once to retrofit the code with the calls as they need to be.

Just following on from OldProgrammer's comments, I think you would want a single function that uses dynamic SQL to return single values from very similar SQL statements.
Below is an example of how this can be acheived:
declare
function get_field_val(
p_field varchar2,
p_table varchar2,
p_where_clause varchar2
) return varchar2 is
v_query clob;
v_result varchar2(4000);
begin
v_query := 'select to_char(' || p_field ||')' ||
'from ' || p_table || ' ' || p_where_clause;
execute immediate v_query into v_result;
return v_result;
end;
begin
dbms_output.put_line(
get_field_val(
p_field => 'COLUMN_NAME',
p_table => 'ALL_TAB_COLUMNS',
p_where_clause => 'where owner = ''SYS'' and table_name = ''ACCESS$''
and column_id = 1'));
dbms_output.put_line(
get_field_val(
p_field => 'max(table_name)',
p_table => 'all_tables',
p_where_clause => 'where owner = ''SYS'''));
end;
A few things to note about this:
This function can only return a single varchar value. If you want different types or arrays of values you'll need to approach this using built-in or user defined plsql collections.
It's probably a bad idea to make this kind of function public as it means anyone could run any query with the same privledges as the package definer (unless you create it with AUTHID CURRENT_USER)

Related

Referencing Schema Name as Variable in Oracle Procedure

Is there a way to set schema name as variable in oracle procedure?
create or replace procedure test is
v_schema varchar2(30);
begin
insert into v_schema.tab_a ( a, b)
select (a, b) from xyz;
end;
/
Thanks
You'd need to resort to dynamic SQL
create or replace procedure test
is
v_schema varchar2(30);
v_sql varchar2(1000);
begin
v_sql := 'insert into ' || v_schema || '.tab_a( a, b ) ' ||
'select a, b from xyz';
dbms_output.put_line( 'About to execute the statement ' || v_sql );
execute immediate v_sql;
end;
A couple of points
You almost certainly want to build the SQL statement in a local variable that you can print out and/or log before executing it. Otherwise, when there are syntax errors, you're going to have a much harder time debugging.
You almost never want to resort to dynamic SQL in the first place. The fact that you have a procedure where you know you want to insert all the rows from xyz into a table named tab_a but you don't know which schema that table is in is a red flag. That's unusual and often indicates a problem with your design. Very, very occasionally dynamic SQL is a wonderful tool when you need extra flexibility. But more often than not when you're thinking about a problem and dynamic SQL is the answer you want to reconsider the problem.

A syntax for custom lazy-evaluation/short-circuiting of function parameters

Oracle defines several structures that make use of what looks like lazy evaluation but what's actually short-circuiting.
For example:
x := case when 1 = 2 then count_all_prime_numbers_below(100000000)
else 2*2
end;
The function count_all(...) will never be called.
However, what I'm more interested in is the syntax that looks like regular function call:
x := coalesce(null, 42, hundreth_digit_of_pi());
Hundreth_digit_of_pi() will not be called since coalesce is not a regular function, but a syntax sugar that looks like one - for regular ones parameters get evaluated when the function is called.
The question is:
is it possible do define in plsql a custom procedure/function that would behave in the same way?
If you're not convinced I'll give an example when that could be useful:
We use ''framework'' for logging.
To trace something you call a procedure:
trace_something('A text to be saved somewhere');
Trace_something is 'pragma autonomous transaction' procedure that does the following steps:
1. See, if any tracing methods (for example file / db table) are enabled
2. For every enabled method use that method to save parameter somewhere.
3. If it was saved to DB, commit.
A problem occurs when building the actual string to be traced might take noticable amount of time, and we wouldn't want to have to spend it, if tracing isn't even enabled in the first place.
The objective would be, in pseudocode:
procedure lazily_trace_something(some_text lazily_eval_type) {
if do_i_have_to_trace() = TRUE then
trace_something(evaluate(some_text));
else
NULL; -- in which case, some_text doesn't get evaluated
end if;
}
/*
*/
lazily_trace_something(first_50_paragraphs_of_lorem_ipsum(a_rowtype_variable));
Is it possible to be done in plsql?
Lazy evaluation can be (partially) implemented using ref cursors, conditional compilation, or execute immediate. The ANYDATA type can be used to pass generic data.
Ref Cursor
Ref cursors can be opened with a static SQL statement, passed as arguments, and will not execute until needed.
While this literally answers your question about lazy evaluation I'm not sure if it's truly practical. This isn't the intended use of ref cursors. And it may not be convenient to have to add SQL to everything.
First, to prove that the slow function is running, create a function that simply sleeps for a few seconds:
grant execute on sys.dbms_lock to <your_user>;
create or replace function sleep(seconds number) return number is
begin
dbms_lock.sleep(seconds);
return 1;
end;
/
Create a function to determine whether evaltuation is necessary:
create or replace function do_i_have_to_trace return boolean is
begin
return true;
end;
/
This function may perform the work by executing the SQL statement. The SQL statement must return something, even though you may not want a return value.
create or replace procedure trace_something(p_cursor sys_refcursor) is
v_dummy varchar2(1);
begin
if do_i_have_to_trace then
fetch p_cursor into v_dummy;
end if;
end;
/
Now create the procedure that will always call trace but will not necessarily spend time evaluating the arguments.
create or replace procedure lazily_trace_something(some_number in number) is
v_cursor sys_refcursor;
begin
open v_cursor for select sleep(some_number) from dual;
trace_something(v_cursor);
end;
/
By default it's doing the work and is slow:
--Takes 2 seconds to run:
begin
lazily_trace_something(2);
end;
/
But when you change DO_I_HAVE_TO_TRACE to return false the procedure is fast, even though it's passing a slow argument.
create or replace function do_i_have_to_trace return boolean is
begin
return false;
end;
/
--Runs in 0 seconds.
begin
lazily_trace_something(2);
end;
/
Other Options
Conditional compilation is more traditionally used to enable or disable instrumentation. For example:
create or replace package constants is
c_is_trace_enabled constant boolean := false;
end;
/
declare
v_dummy number;
begin
$if constants.c_is_trace_enabled $then
v_dummy := sleep(1);
This line of code does not even need to be valid!
(Until you change the constant anyway)
$else
null;
$end
end;
/
You may also want to re-consider dynamic SQL. Programming style and some syntactic sugar can make a big difference here. In short, the alternative quote syntax and simple templates can make dynamic SQL much more readable. For more details see my post here.
Passing Generic Data
The ANY types can be use to store and pass any imaginable data type. Unfortunately there's no native data type for each row type. You'll need to create a TYPE for each table. Those custom types are very simple so that step can be automated if necessary.
create table some_table(a number, b number);
create or replace type some_table_type is object(a number, b number);
declare
a_rowtype_variable some_table_type;
v_anydata anydata;
v_cursor sys_refcursor;
begin
a_rowtype_variable := some_table_type(1,2);
v_anydata := anydata.ConvertObject(a_rowtype_variable);
open v_cursor for select v_anydata from dual;
trace_something(v_cursor);
end;
/

how to declare %ROWTYPE of a variable that is a weakly typed SYS_REFCURSOR?

W.r.t code below I can not declare the type of fetch-into-variable as the underlying table's %ROWTYPE because the SYS_REFCURSOR is on a select that joins two tables and also selects a few functions called on the attributes of the underlying two tables; i.e I can't declare as L_RECORD T%ROWTYPE
---
DECLARE
P_RS SYS_REFCURSOR;
L_RECORD P_RS%ROWTYPE;
BEGIN
CAPITALEXTRACT(
P_RS => P_RS
);
OPEN P_RS;
LOOP
BEGIN
FETCH P_RS INTO L_RECORD;
EXIT WHEN P_RS%NOTFOUND;
...
EXCEPTION
WHEN OTHERS THEN
...
END;
END LOOP;
CLOSE P_RS;
END;
--------
CREATE or REPLACE PROCEDURE CAPITALEXTRACT
(
p_rs OUT SYS_REFCURSOR
) AS
BEGIN
OPEN p_rs for
select t.*,tminusone.*, f(t.cash), g(t.cash) FROM T t, TMINUSONE tminusone
where t.ticket=tminusone.ticket;
END CAPITALEXTRACT;
Of course I don't want to define a static table R with columns as returned in the SYS_REFCURSOR and then declare as L_RECORD R%ROWTYPE.
And hence the question:
how to declare %ROWTYPE of a variable that is a weakly typed SYS_REFCURSOR ?
The short answer is, you can't. You'd need to define a variable for each column that wil be returned.
DECLARE
P_RS SYS_REFCURSOR;
L_T_COL1 T.COL1%TYPE;
L_T_COL1 T.COL2%TYPE;
...
And then fetch into the list of columns:
FETCH P_RS INTO L_T_COL1, L_T_COL2, ... ;
This is painful but manageable as long as you know what you're expecting in the ref cursor. Using T.* in your procedure makes this fragile though, as adding a column to the table would break the code that thinks it knows what columns there are and what order they're in. (You can also break it between environments if the tables aren't built consistently - I've seen places where column ordering is different in different environments). You'll probably want to make sure you're only selecting the columns you really care about anyway, to avoid having to define variables for things you'll never read.
From 11g you can use the DBMS_SQL package to convert your sys_refcursor into a DBMS_SQL cursor, and you can interrogate that to determine the columns. Just as an example of what you can do, this will print out the value of every column in every row, with the column name:
DECLARE
P_RS SYS_REFCURSOR;
L_COLS NUMBER;
L_DESC DBMS_SQL.DESC_TAB;
L_CURS INTEGER;
L_VARCHAR VARCHAR2(4000);
BEGIN
CAPITALEXTRACT(P_RS => P_RS);
L_CURS := DBMS_SQL.TO_CURSOR_NUMBER(P_RS);
DBMS_SQL.DESCRIBE_COLUMNS(C => L_CURS, COL_CNT => L_COLS,
DESC_T => L_DESC);
FOR i IN 1..L_COLS LOOP
DBMS_SQL.DEFINE_COLUMN(L_CURS, i, L_VARCHAR, 4000);
END LOOP;
WHILE DBMS_SQL.FETCH_ROWS(L_CURS) > 0 LOOP
FOR i IN 1..L_COLS LOOP
DBMS_SQL.COLUMN_VALUE(L_CURS, i, L_VARCHAR);
DBMS_OUTPUT.PUT_LINE('Row ' || DBMS_SQL.LAST_ROW_COUNT
|| ': ' || l_desc(i).col_name
|| ' = ' || L_VARCHAR);
END LOOP;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(L_CURS);
END;
/
That's not of much practical use, and for brevity I'm treating every value as a string since I just want to print it anyway. Look at the docs and search for examples for more practical applications.
If you only want a few columns from your ref cursor you could, I suppose, loop around l_desc and record the position where column_name is whatever you're interested in, as a numeric variable; you could then refer to the column by that variable later where you would normally use the name in a cursor loop. Depends what you're doing with the data.
But unless you're expecting to not know the column order you're getting back, which is unlikely since you seem to control the procedure - and assuming you get rid of the .*s - you're probably much better off reducing the returned columns to the minimum you need and just declaring them all individually.

Nested if statement is returning a statement handle not executed error

I have a stored procedure with two input, two output params and 5 sys_refcursors.
I had a succesful IF/ELSE where I opened these cursors by calling different stored procs but now need a third option which is another stored proc call. The third option is virtually identical to the second with one difference.
I was pretty sure I got the nested if statement correct but I keep getting ora-24338 Statement handle not executed when it tries to get the cursors from the this new call.
The problem stored procedure call is the middle one.
create or replace Procedure procedure_name (
OutVar out varachar2,
Outvar2 out number,
inParam1 date,
REf-Cur1 in out sys_refcursor,
REf-Cur2 in out sys_refcursor,
REf-Cur3 in out sys_refcursor,
REf-Cur4 in out sys_refcursor,
REf-Cur5 in out sys_refcursor
)
is
tIsBindVar1 varchar2(100);
tIsBindVar2 varchar2(100);
tOutVar1 varchar2(100);
TOutVar2 varchar2(100);
Begin
Select Max(T.Var1)
into tIsBindVar1
From table1
where T.aField = inParam1;
Select Function_Name (inParam1)
into tIsBindVar2
from Dual;
IF tIsBindVar1 is NOT NULL
THEN
Select P.Field_A P.Field_B
INTO tOutVar1, tOutVar2
FROM table1
WHERE P.Field_A = inParam1;
Stored_Proc_One (tInParam => tOutVar1,
inParam1 => inParam1,
5 cursors => 5 cursors);
ELSE
IF tIsBindVar2 = 'Y'
THEN
Stored_Proc_Two (inParam1 => inParam1,
5 cursors => 5 cursors);
ELSE
Stored_Proc_Three ();
Stored_Proc_Two ( inParam1 => inParam1, 5 cursors => 5 cursors);
END IF;
END IF;
SELECT tOutVar1, tOutVar2
INTO OutVar1, OutVar2
FROM DUAL;
Some quick extra notes.
Stored_procs one and two are straight up data grabs, nothing fancy, stored proc 3 generates some data based on some input params (not listed) and stored proc 2 is called to collect.
I can change these stored proc calls around and I always wind up with the same error from the middle one. That includes changing the conditions in any order.
I tried to simplify the code as I'm not looking for anyone to do the work for me but to try and get understanding what the problem is.
Hopefully I did not leave anything important out but I think the issue is something with how I'm doing the nested if. I certainly don;t think the issue is in the the stored procs themselves as I say they work when I change the order.
So if long winded and very hard to read code. Going to try and find where they keep the editing info and clean it up.
Thanks in advance.
What are the values of tIsBindVar1 and tIsBindVar2?
Do you really want the IF statement structured the way I formatted it? Or do you really want
IF tIsBindVar1 is NOT NULL
THEN
<<do something>>
ELSIF tIsBindVar2 = 'Y'
THEN
<<do something else>>
ELSE
<<do one more thing>>
END IF;
If you want to ensure that at least one path is followed in all cases, you'd want an IF ELSIF ELSE.
Incidentally, there is no need for all those SELECT FROM dual statements. You can simply assign variables in PL/SQL
tIsBindVar2 := Function_Name (inParam1);
and
tOutVar1 := OutVar1;
tOutVar2 := OutVar2;
are more conventional.

TO_CHAR of an Oracle PL/SQL TABLE type

For debugging purposes, I'd like to be able to "TO_CHAR" an Oracle PL/SQL in-memory table. Here's a simplified example, of what I'd like to do:
DECLARE
TYPE T IS TABLE OF MY_TABLE%ROWTYPE INDEX BY PLS_INTEGER;
V T;
BEGIN
-- ..
-- Here, I'd like to dbms_output V's contents, which of course doesn't compile
FOR i IN V.FIRST .. V.LAST LOOP
dbms_output.put_line(V(i));
END LOOP;
-- I want to omit doing this:
FOR i IN V.FIRST .. V.LAST LOOP
dbms_output.put_line(V(i).ID || ',' || V(i).AMOUNT ...);
END LOOP;
END;
Can this be achieved, simply? The reason I ask is because I'm too lazy to write this debugging code again and again, and I'd like to use it with any table type.
ok, sorry this isn't complete, but to followup with #Lukas, here's what I have so far:
First, instead of trying to create anydata/anytype types, I tried using XML extracted from a cursor...weird, but its generic:
CREATE OR REPLACE procedure printCur(in_cursor IN sys_refcursor) IS
begin
FOR c IN (SELECT ROWNUM rn,
t2.COLUMN_VALUE.getrootelement () NAME,
EXTRACTVALUE (t2.COLUMN_VALUE, 'node()') VALUE
FROM TABLE (XMLSEQUENCE (in_cursor)) t,
TABLE (XMLSEQUENCE (EXTRACT (COLUMN_VALUE, '/ROW/node()'))) t2
order by 1)
LOOP
DBMS_OUTPUT.put_line (c.NAME || ': ' || c.VALUE);
END LOOP;
exception
when others then raise;
end;
/
Now, to call it, you need a cursor, so I tried casting to cursor in pl/sql, something like:
open v_cur for select * from table(cast(v_tab as tab_type));
But depending on how v_tab is defined, this may or may not cause issues in pl/sql cast (using %rowtype in nested table def seems to give issues).
Anyway, you can build on this or refine it as you like. (and possibly use xmltable...)
Hope that helps

Resources