The compiler ignores NOCOPY in these cases - oracle

I read about IN OUT and NOCOPY. Then I encountered NOCOPY use cases but I was not able to get it. Can anybody explain these with examples? Thanks in advance.
The actual parameter must be implicitly converted to the data type of the formal parameter.
The actual parameter is the element of a collection.
The actual parameter is a scalar variable with the NOT NULL constraint.
The actual parameter is a scalar numeric variable with a range, size, scale, or precision constraint.
The actual and formal parameters are records, one or both was declared with %ROWTYPE or %TYPE, and constraints on corresponding fields differ.
The actual and formal parameters are records, the actual parameter was declared (implicitly) as the index of a cursor FOR LOOP statement, and constraints on corresponding fields differ.
The subprogram is invoked through a database link or as an external subprogram.

The basic principle is that PL/SQL will honour the NOCOPY directive as long as the value we pass can be used as provided, without transformation, and is addressable by the called program. The scenarios you list are circumstances where this is not the cases. I must admit a couple of these examples made me think, so this is a worthwhile exercise.
The first four examples call this toy procedure.
create or replace procedure tst2 (p1 in out nocopy t34%rowtype) is
begin
p1.id := 42;
end;
/
Case 1: The actual parameter must be implicitly converted to the data type of the formal parameter.
declare
n varchar2(3) := '23';
begin
tst(n);
dbms_output.put_line(n);
end;
/
Case 2: The actual parameter is the element of a collection.
declare
nt sys.odcinumberlist := sys.odcinumberlist(17,23,69);
begin
tst(nt(2));
dbms_output.put_line(to_char(nt(2)));
end;
/
Case 3: The actual parameter is a scalar variable with the NOT NULL constraint.
declare
n number not null := 23;
begin
tst(n);
dbms_output.put_line(to_char(n));
end;
/
Case 4: The actual parameter is a scalar numeric variable with a range, size, scale, or precision constraint.
declare
n number(5,2) := 23;
begin
tst(n);
dbms_output.put_line(to_char(n));
end;
/
The next example uses this table ...
create table t34 (id number not null, col1 date not null)
/
...and toy procedure:
create or replace procedure tst2 (p1 in out nocopy t34%rowtype) is
begin
p1.id := 42;
end;
/
Case 5 : The actual and formal parameters are records, one or both was declared with %ROWTYPE or %TYPE, and constraints on corresponding fields differ.
declare
type r34 is record (id number, dt date);
r r34;
begin
r.id := 23;
r.dt := to_date(null); --trunc(sysdate);
tst2(r);
dbms_output.put_line(to_char(r.id));
end;
/
The next example uses this package spec...
create or replace package pkg is
type r34 is record (id number, dt date);
end;
/
...and toy procedure:
create or replace procedure tst3 (p1 in out nocopy pkg.r34) is
begin
p1.id := p1.id + 10;
end;
/
Case 6: The actual and formal parameters are records, the actual parameter was declared (implicitly) as the index of a cursor FOR LOOP statement, and constraints on corresponding fields differ.
begin
for j in ( select * from t34) loop
tst3(j);
dbms_output.put_line(to_char(j.id));
end loop;
end;
/
The last example uses a remote version of the first procedure.
Case 7: The subprogram is invoked through a database link or as an external subprogram.
declare
n number := 23;
begin
tst#remote_db(n);
dbms_output.put_line(to_char(n));
end;
/
There are working demos of the first six cases on db<>fiddle here.

Related

How can I test my package with one function?

I made a package that compiles fine but when I try to test it it gives me "invalid data type".
I've tried two different ways, first one like this
select pkg_contabilidad.f_totalizar_Detalle(100) FROM DUAL;
It gives me the ORA-00902 'invalid data type'
Also I've tried this
DECLARE
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
resultao r_registro;
numero NUMBER :=100;
BEGIN
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
END;
It gives me another error PLS-00382 'expression is of wrong type'
I don't know what am I doing wrong, cause my function receives just one parameter and is of type NUMBER, so I dont know where's my mistake. I'll leave the code of my package just in case
CREATE OR REPLACE PACKAGE pkg_contabilidad AS
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
TYPE t_detalle IS TABLE OF
r_registro INDEX BY BINARY_INTEGER;
FUNCTION f_totalizar_detalle(p_clave NUMBER)RETURN t_detalle;
END pkg_contabilidad;
/
CREATE OR REPLACE PACKAGE BODY pkg_contabilidad AS
B_detalle t_detalle;
i integer :=1;
FUNCTION f_totalizar_detalle(p_clave NUMBER) RETURN t_detalle IS
v_detalle t_detalle;
CURSOR c_facturado IS
SELECT c.cod_rubro, 'H', CASE WHEN SUM(d.gravada)=0 THEN SUM(d.iva) ELSE SUM(d.gravada) END
FROM fn_documentos_det d JOIN fn_conceptos c ON d.cod_concepto = c.cod_concepto
WHERE d.clave_doc=p_clave
GROUP BY c.cod_rubro;
CURSOR c_datos IS
SELECT SUM(d.total_doc), 'D',r.cod_rubro
FROM fn_documentos d JOIN fn_cajas_ctas r ON d.num_caja_cta = r.num_caja_cta
WHERE d.clave_doc = p_clave
GROUP BY r.cod_rubro;
BEGIN
open c_datos;
LOOP
FETCH c_datos INTO v_detalle(1);
END LOOP;
CLOSE c_datos;
FOR fila IN c_facturado LOOP
i := i + 1;
v_detalle(i) := fila;
END LOOP;
END;
END PKG_CONTABILIDAD;
The function returns a pkg_contabilidad.t_detalle, so the test needs to be:
declare
resultao pkg_contabilidad.t_detalle;
numero number := 100;
begin
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
end;
It doesn't work in SQL because pkg_contabilidad.t_detalle is a PL/SQL type, not a SQL type (create or replace type). The database can perform some automatic conversions, but there are still limitations.
By the way, this loop will never complete because it lacks an exit condition:
open c_datos;
loop
fetch c_datos into v_detalle(1);
end loop;
close c_datos;
Your function returns a PL/SQL table type, with a table of a PL/SQL record type, which is defined in your package, which plain SQL doesn't know about and can't display - hence your invalid datatype error. If you need to call the function and access the data from SQL you can create schema-level object and collection types instead.
In your anonymous block you are a declaring a new record type. That looks the same to you because the structure is the same, but Oracle expects the exact type the function returns. That makes your test code shorter and simpler though. But you are also trying to return the whole collection into a single record.
DECLARE
l_detalle pkg_contabilidad.t_detalle;
l_registro pkg_contabilidad.r_registro;
l_idx pls_integer;
numero NUMBER :=100;
BEGIN
l_detalle := pkg_contabilidad.f_totalizar_detalle(numero);
l_idx := l_detalle.FIRST;
WHILE l_idx is not null LOOP
l_registro := l_detalle(l_idx);
-- do something with this record
dbms_output.put_line(l_registro.tipo);
l_idx := l_detalle.NEXT(l_idx);
END LOOP;
END;
db<>fiddle with dummy cursors.
Your function is a bit strange and probably isn't doing quite what you want; but also has two fatal problems: it isn't returning anything, and it has an infinite loop. I've fixed those for the fiddle but not anything else, as this seems to be an exercise.

Input sanitization - Numeric values

I've been asked to do input validation in order to prevent sql injection. I've been using dbms assert package functions to do the sanitization. However, when I try to sanitize a number(I'm getting it in varchar2(12 byte)) error is thrown. It's the same case with alphanumeric characters starting with number.
I tried various functions of dbms assert. Nothing seems to work except noop. But, noop is of no use since it does not do any validation.
create or replace procedure employee
(
v_emp_id IN varchar2(12 byte)
)
AS
lv_query CLOB;
BEGIN
if v_emp_id is NOT NULL THEN
lv_query := 'select * from employee where emp_id=''' || dbms_assert.enquote_name(v_emp_id) || '''';
--I also tried below:
-- lv_query := 'select * from employee where emp_id=''' || dbms_assert.simple_sql_name(v_emp_id) || '''';
end if;
END
No source gives more detailed input on dbms_assert package. Please help me in
Whether dbms_assert package can be used to sanitize numeric values(stored in VARCHAR2 variables). If yes, how?
Other ways of sanitizing input. (other than using bind variables)
Thanks.
Oracle 12.2 and higher
If you are on Oracle 12.2 or higher, you can use the VALIDATE_CONVERSION function which would be the simplest solution. Your code could potentially look something like this:
CREATE OR REPLACE PROCEDURE employee (v_emp_id IN VARCHAR2)
AS
lv_query CLOB;
BEGIN
IF v_emp_id IS NOT NULL AND validate_conversion (v_emp_id AS NUMBER) = 1
THEN
lv_query := 'select * from employee where emp_id = ' || v_emp_id;
ELSE
--do something here with an invalid number
null;
END IF;
END;
/
Earlier than Oracle 12.2
If you are not on Oracle 12.2 or higher, you can write your own small function to validate that the value is a number. Using a method similar to what Belayer suggested, just attempt to convert the value to a number using the TO_NUMBER function and if it fails, then you know it's not a number. In my example, I have it as a small anonymous block within the code but you can also make it a standalone function if you wish.
CREATE OR REPLACE PROCEDURE employee (v_emp_id IN VARCHAR2)
AS
lv_query CLOB;
l_is_number BOOLEAN;
BEGIN
--Verify that the parameter is a number
DECLARE
l_test_num NUMBER;
BEGIN
l_test_num := TO_NUMBER (v_emp_id);
l_is_number := TRUE;
EXCEPTION
WHEN VALUE_ERROR
THEN
l_is_number := FALSE;
END;
--Finished verifying if the parameter is a number
IF v_emp_id IS NOT NULL AND l_is_number
THEN
lv_query := 'select * from employee where emp_id = ' || v_emp_id;
ELSE
--do something here with an invalid number
null;
END IF;
END;
/
Well if you cannot change the procedure it means you have no test as that procedure will not compile, so it cannot be executed. However that may be a moot point. You need to define exactly what you mean by "sanitize numeric values". Do you mean validate a string contains a numeric value. If so DBMS_ASSERT will not do that. (Note: The function chooses ENQUOTE_NAME will uppercase the string and put double quotes (") around it thus making it a valid object name.) Further your particular validation may require you define a valid numeric value, is it: an integer, a floating point, is scientific nation permitted, is there a required precision and scale that must be satisfied, etc. As a brute force validation you can simulate the assertion by just convert to number. The following will do that. Like dbms_assert if the assertion is successful it returns the input string. Unlike dbms_assert, however, when the assertion fails it just returns null instead of raising an exception. See fiddle.
create or replace
function assert_is_numeric(value_in varchar2)
return varchar2
is
not_numeric exception;
pragma exception_init (not_numeric,-06502);
l_numeric number;
begin
l_numeric := to_number(value_in);
return value_in;
exception
when not_numeric then
return null;
end assert_is_numeric;

PLS-00307 when subprograms differ only in type parameters (RAW vs VARCHAR2)

I have a package with two methods:
create or replace package demo
as
function overloaded(p_in varchar2)
return pls_integer;
function overloaded(p_in raw)
return pls_integer;
end;
/
create or replace package body demo
as
function overloaded(p_in raw)
return pls_integer
is
begin
return 1;
end;
function overloaded(p_in varchar2)
return pls_integer
is
begin
return 2;
end;
end;
/
It compiles without any errors, but I cannot call either method, as in both cases I get error PLS-00307: too many declarations match this call. Why does this happen with RAW and VARCHAR2? How can I work around this limitation? Is giving different names to my subprograms the only way out?
The documentation says:
PL/SQL lets you overload nested subprograms, package subprograms, and type methods. You can use the same name for several different subprograms if their formal parameters differ in name, number, order, or data type family.
And according to the the appendix that refers to, 'varchar2 and raw are both members of the char data type family.
You can give the subprograms different names, but you can also change the name and order of the formal parameters; in this case as there is only one parameter that means you can change only the name(s):
create or replace package demo
as
function overloaded(p_in_vc varchar2)
return pls_integer;
function overloaded(p_in_raw raw)
return pls_integer;
end;
/
(and the same change in the body of course); and then call with named notation for the actual parameters:
select demo.overloaded(p_in_vc=>'test') from dual;
DEMO.OVERLOADED(P_IN_VC=>'TEST')
--------------------------------
2
select demo.overloaded(p_in_raw=>'AABB') from dual;
DEMO.OVERLOADED(P_IN_RAW=>'AABB')
---------------------------------
1

Can I know the number of parameters passed in stored procedure in Oracle PL/SQL

I have a stored procedure in a Oracle Database that receives 3 parameters.
I know that I call it with 1 to 3 parameters but it's possible to know inside itself how many arguments are the defaults or are really passed?
For example:
dummy(1) some keyword say me "1"
dummy(1,2,3) say me "3"
I ask this because I worked with Informix 4GL and I could use "NARGS" to know the number of arguments that I receive.
The short answer is no.. there is not an equivalent to NARGS or "C"s argc.
if you are using null default values you could manually count the number of arguments that do no equal the default value.. but that wont tell you if you explicitly pass the default value as a parameter.
i can think of 2 solutions.
1. user overloaded procedures .. ie
procedure a (p_1 number);
procedure a (p_1 number, p_2 number);
procedure a (p_1 number, p_2 number, p_3 number);
then in the bodies you would "know" by which one you are in.
option 2. pass a varray/plsql table as a single argument but then actually passing the arguments becomes problematic.
create or replace package x
is
type an_arg is record ( n number, v varchar2(2000), d date);
type args is table of an_arg;
procedure a(argv args);
end;
/
create or replace package body x
is
procedure a(argv args)
is
begin
dbms_output.put_line('i was passed '||argv.count||' arguments');
end;
end;
/
Similar to ShoeLace's answer, I think counting the number of parameters that do not equal the default of each parameter would work.
The key to this is to give each parameter a nonsensical default value. Like '~#dummee_v#1u3#~' maybe. Anything you are confident will never actually be passed in. This way you don't have to worry about somebody passing in a parameter value that equals the default value.
So:
create procedure p1 (id1 varchar2 default '~#dummee_v#1u3#~', id2 varchar2 default '~#dummee_v#1u3#~') is
lParamCount number := 0;
lDummyParamValue varchar2 := '~#dummee_v#1u3#~';
begin
if id1 <> lDummyParamValue then lParamCount := lParamCount + 1;
if id2 <> lDummyParamValue then lParamCount := lParamCount + 1;
end p1;

Handle the result of procedure A in procedure B

I have procedure A, which has some in and out parameters. I can't change parameters or code of first procedure, but I need to get different result. I decided to create procedure B, which will pass additional parameter. I want to call procedure A from this procedure and handle result of procedure A in it. I know type of parameters, which procedure A returns.
I need somethink like this:
procedure A(id in number, names out names_array) is
begin
// procedure A body
// filling names var
end;
procedure B (id in number, myLimit in number, names out names_array) is
temp_names names_array;
i integer;
begin
A(id => id, names => temp_names);
while i < myLimit loop
names(i) := temp_names(i);
end loop;
end;
So the problem is in handling names (the result of procedure A). How I can do this correctly? Maybe with some select ... limit myLimit query?
Definition of names_array:
TYPE names_array IS TABLE OF VARCHAR2 (40)
INDEX BY BINARY_INTEGER;

Resources