Oracle: Creating an array of object type in package - oracle

I want to implement a stack data structure in 10gR2.
However, when I try to do:
CREATE or replace TYPE ContainerArray AS VARRAY(25) OF XDB.DBMS_XMLDOM.DOMNODE ;
I get
"schema-level type has illegal reference to XDB.DBMS_XMLDOM."
So I've figured out that's because Oracle will not let you create a schema-level type which references a package-level type.
I can fix that problem by creating the type inside of the package:
TYPE ContainerArray AS VARRAY(25) OF XDB.DBMS_XMLDOM.DOMNODE;
However, then when I try to create my object, I can't reference the package-level type:
CREATE TYPE DOMNode_Stack AS OBJECT (
max_size INTEGER,
top INTEGER,
position ContainerArray,
MEMBER PROCEDURE initialize,
MEMBER FUNCTION full RETURN BOOLEAN,
MEMBER FUNCTION empty RETURN BOOLEAN,
MEMBER PROCEDURE push (n IN INTEGER),
MEMBER PROCEDURE pop (n OUT INTEGER)
);
and I can't create the object type inside of the package because I get
"Object not supported in this context."
Any thoughts on how I can solve this issue?

sql level objects like an OBJECT can only reference SQL types in their attributes.
if you see the package spec:
TYPE DOMNode IS RECORD (id RAW(13));
so you could create an sql array of
SQL> CREATE or replace TYPE ContainerArray AS VARRAY(25) OF raw(13);
2 /
Type created.
then in your object put
position ContainerArray
which will work. when you reference it, just assign a local variable
v_dom xml_dom.domnode;
begin
v_dom.id := your array index value that you're dealing with at the time;
be advised that on any Oracle upgrade you need to verify that the structure of DOMNODE hasn't changed.
edit: here's a sample:
SQL> set serverout on
SQL> create or replace type containerarray as varray(25) of raw(13);
2 /
Type created.
SQL> create type domnode_stack as object (
2 position containerarray,
3 member procedure initialize,
4 member procedure print_node_names
5 );
6 /
Type created.
SQL> CREATE TYPE body DOMNode_Stack
2 as
3 member procedure initialize
4 is
5 v_dom dbms_xmldom.DOMNode;
6 v_doc dbms_xmldom.domdocument;
7 v_nl dbms_xmldom.DOMNodeList;
8 begin
9 position := containerarray();
10 v_doc := dbms_xmldom.newDOMDocument(xmltype('<root><foo>test</foo><foo2>test2</foo2></root>')); -- just some summy data.
11 v_nl := dbms_xmldom.getElementsByTagName(v_doc, '*');
12 for idx in 0..dbms_xmldom.getLength(v_nl)-1 loop
13 v_dom := DBMS_XMLDOM.item(v_nl, idx);
14 position.extend;
15 position(position.last) := v_dom.id;
16 end loop;
17 end;
18
19 member procedure print_node_names
20 is
21 v_dom dbms_xmldom.DOMNode;
22 begin
23 for idx in 1..position.count
24 loop
25 v_dom.id := position(idx);
26 dbms_output.put_line(dbms_xmldom.getnodename(v_dom)||'='||dbms_xmldom.getnodevalue(dbms_xmldom.getfirstchild(v_dom)));
27 end loop;
28 end;
29 end;
30 /
Type body created.
SQL> show errors type body DOMNode_Stack
No errors.
SQL> declare
2 o_domnode DOMNode_Stack := DOMNode_Stack(null);
3 begin
4 o_domnode.initialize();
5 o_domnode.print_node_names();
6 end;
7 /
root=
foo=test
foo2=test2
PL/SQL procedure successfully completed.
the important bit being:
to assign from DBMS_XMLDOM to the sql type we take v_dom.id.
13 v_dom := DBMS_XMLDOM.item(v_nl, idx);
14 position.extend;
15 position(position.last) := v_dom.id;
and when reversing:
23 for idx in 1..position.count
24 loop
25 v_dom.id := position(idx);
ie assign the ID part (not the record itself..that would error out) back.

Related

Struggling with Oracle Function with parameter (Reference to uninitialized collection)

I can't find what is wrong here, finally I decided to ask here for help.
This is how I am invoking:
DECLARE
RESSULT VARCHAR2(1000 CHAR);
V_TEST1 CARS_TABLE:=CARS_TABLE();
idx pls_integer :=0;
BEGIN
V_TEST1.EXTEND(2);
IDX := V_TEST1.COUNT; -- here there is NO problem
RESSULT:=MYFUNCTION(1, V_TEST1);
END;
This is what I am invoking..
CREATE OR REPLACE
FUNCTION MYFUNCTION(
CAR_ID IN INTEGER,
MYOUTTABLE OUT CARS_TABLE)
RETURN VARCHAR2
IS
RESSULT VARCHAR2(1000 CHAR);
idx pls_integer :=0;
BEGIN
IDX := MYOUTTABLE.COUNT; - HERE IS PROBLEM.. but why - I pass initialized collection
RETURN 'return param';
END MYFUNCTION;
CREATE OR REPLACE TYPE carElement
IS
OBJECT
(
kind VARCHAR2(60),
created DATE );
CREATE OR REPLACE TYPE CARS_TABLE
AS
TABLE OF carElement;
00000 - "Reference to uninitialized collection"
*Cause: An element or member function of a nested table or varray
was referenced (where an initialized collection is needed)
without the collection having been initialized.
*Action: Initialize the collection with an appropriate constructor
or whole-object assignment.
"IDX := MYOUTTABLE.COUNT; - HERE IS PROBLEM.. but why - I pass initialized collection"
you're resetting the collection to NULL on the input to the function call as you've defined the parameter as OUT which automatically nulls out the parameter as it calls the function (documentation link).
MYOUTTABLE OUT CARS_TABLE
if you want to preserve the input, then you need to pass it as IN or if you want to preserve teh input and also manipulate the object in the function then use IN OUT.
eg note with the first call it shows its null if i use out:
SQL> declare
2 v_test1 cars_table:=cars_table();
3
4 function myfunction(
5 car_id in integer,
6 myouttable &mode cars_table)
7 return varchar2
8 is
9 ressult varchar2(1000 char);
10 idx pls_integer :=0;
11 begin
12 if (myouttable is null)
13 then
14 dbms_output.put_line('ARRAY IS NULL');
15 else
16 idx := myouttable.count;
17 end if;
18 return 'return param';
19 end myfunction;
20 begin
21
22 v_test1.extend(2);
23 dbms_output.put_line(myfunction(1, v_test1));
24
25 end;
26 /
Enter value for mode: OUT
old 6: myouttable &mode cars_table)
new 6: myouttable OUT cars_table)
ARRAY IS NULL
return param
PL/SQL procedure successfully completed.
SQL> /
Enter value for mode: IN OUT
old 6: myouttable &mode cars_table)
new 6: myouttable IN OUT cars_table)
return param
PL/SQL procedure successfully completed.

Why is this check for null associative array in PL/SQL failing?

I have an associative array created by a type of rowtype of a table column.
To give an example, this is how it is(the table names are different, but the structure is the same):
This is the DDL of the table
CREATE TABLE employees
(
id NUMBER,
name VARCHAR2(240),
salary NUMBER
);
Here's what my procedure is doing:
DECLARE
TYPE table_of_emp
IS TABLE OF employees%ROWTYPE INDEX BY BINARY_INTEGER;
emp TABLE_OF_EMP;
BEGIN
IF emp IS NULL THEN
dbms_output.Put_line('Null associative array');
ELSE
dbms_output.Put_line('Not null');
END IF;
END;
I assume this should result in "Null associative array" being printed. However, the if condition fails and the execution jumps to the else part.
Now if I put in a for loop to print the collection values
DECLARE
TYPE table_of_emp
IS TABLE OF employees%ROWTYPE INDEX BY BINARY_INTEGER;
emp TABLE_OF_EMP;
BEGIN
IF emp IS NULL THEN
dbms_output.Put_line('Null associative array');
ELSE
dbms_output.Put_line('Not null');
FOR i IN emp.first..emp.last LOOP
dbms_output.Put_line('Emp name: '
|| Emp(i).name);
END LOOP;
END IF;
END;
then the program unit raises an exception, referencing the for loop line
ORA-06502: PL/SQL: Numeric or value error
which I presume is because of the null associative array. Is the error being raised because of null associative array?
So why is the first check failing then? What am I doing wrong?
The database server is Oracle 11g EE (version 11.2.0.3.0 64 bit)
I assume this should result in "Null associative array" being printed. That assumption is wrong for associative arrays. They exist when declared, but are empty. It would be correct for other types of PL/SQL collections:
Until you initialize it, a nested table or varray is atomically null;
the collection itself is null, not its elements. To initialize a
nested table or varray, you use a constructor, a system-defined
function with the same name as the collection type. This function
constructs collections from the elements passed to it.
You must explicitly call a constructor for each varray and nested
table variable. Associative arrays, the third kind of collection, do
not use constructors. Constructor calls are allowed wherever function
calls are allowed. Initializing and Referencing Collections
Compare:
SQL> declare
2 type varchar2_100_aa is table of varchar2(100) index by binary_integer;
3 test varchar2_100_aa;
4 begin
5 test(1) := 'Hello';
6 dbms_output.put_line(test(1));
7 end;
8 /
Hello
PL/SQL procedure successfully completed.
SQL> declare
2 type varchar2_100_va is varray(100) of varchar2(100);
3 test varchar2_100_va;
4 begin
5 test(1) := 'Hello';
6 dbms_output.put_line(test(1));
7 end;
8 /
declare
*
ERROR at line 1:
ORA-06531: Reference to uninitialized collection
ORA-06512: at line 5
Variable array done correctly:
SQL> declare
2 type varchar2_100_va is varray(10) of varchar2(100);
3 test varchar2_100_va;
4 begin
5 test := varchar2_100_va(); -- not needed on associative array
6 test.extend; -- not needed on associative array
7 test(1) := 'Hello';
8 dbms_output.put_line(test(1));
9 end;
10 /
Hello
PL/SQL procedure successfully completed.
Because the associative array is empty first and last are null, which is why your second example results in ORA-06502: PL/SQL: Numeric or value error:
SQL> declare
2 type varchar2_100_aa is table of varchar2(100) index by binary_integer;
3 test varchar2_100_aa;
4 begin
5 dbms_output.put_line(test.count);
6 dbms_output.put_line(coalesce(to_char(test.first), 'NULL'));
7 dbms_output.put_line(coalesce(to_char(test.last), 'NULL'));
8 test(1) := 'Hello';
9 dbms_output.new_line;
10 dbms_output.put_line(test.count);
11 dbms_output.put_line(coalesce(to_char(test.first), 'NULL'));
12 dbms_output.put_line(coalesce(to_char(test.last), 'NULL'));
13 end;
14 /
0
NULL
NULL
1
1
1
PL/SQL procedure successfully completed.
EDIT Also note that associative arrays can be sparse. Looping over the numbers between first and last will raise an exception for any collection that is sparse. Instead use first and next like so: (Last and prev to loop the other direction.)
SQL> declare
2 type varchar2_100_aa is table of varchar2(100) index by binary_integer;
3 test varchar2_100_aa;
4 i binary_integer;
5 begin
6 test(1) := 'Hello';
7 test(100) := 'Good bye';
8 dbms_output.put_line(test.count);
9 dbms_output.put_line(coalesce(to_char(test.first), 'NULL'));
10 dbms_output.put_line(coalesce(to_char(test.last), 'NULL'));
11 dbms_output.new_line;
12 --
13 i := test.first;
14 while (i is not null) loop
15 dbms_output.put_line(to_char(i, '999') || ' - ' || test(i));
16 i := test.next(i);
17 end loop;
18 end;
19 /
2
1
100
1 - Hello
100 - Good bye
PL/SQL procedure successfully completed.
I'm not going to answer why the first check is failing. I've never thought of doing anything like that and am quite surprised that it doesn't raise an error.
The reason why you're getting an exception raised on the loop is, as you've noted, that the index emp.first does not exist.
Rather than checking for nulls, you should really be checking for the existence of this index. Which you can do be using the .exists(i) syntax:
if not emp.exists(emp.first) then
dbms_output.put_line('Nothing in here.');
end if;

How can we define output parameter size in stored procedure?

How can we define output parameter size in stored procedure?
You can't. Of course, you are in control of how much data you put into the OUT parameter in the stored procedure. If you want you can create a sized local variable to hold the data and then assign the value of that variable to the OUT parameter.
The calling program determines the size of the variable that receives the OUT parameter.
Here is a simple package which declares and uses a subtype:
SQL> create or replace package my_pkg as
2 subtype limited_string is varchar2(10);
3 procedure pad_string (p_in_str varchar
4 , p_length number
5 , p_out_str out limited_string);
6 end my_pkg;
7 /
Package created.
SQL> create or replace package body my_pkg as
2 procedure pad_string
3 (p_in_str varchar
4 , p_length number
5 , p_out_str out limited_string)
6 as
7 begin
8 p_out_str := rpad(p_in_str, p_length, 'A');
9 end pad_string;
10 end my_pkg;
11 /
Package body created.
SQL>
However, if we call PAD_STRING() in such a way that the output string exceeds the subtype's precision it still completes successfully. Bother!
SQL> var out_str varchar2(128)
SQL>
SQL> exec my_pkg.pad_string('PAD THIS!', 12, :out_str)
PL/SQL procedure successfully completed.
SQL>
SQL> select length(:out_str) from dual
2 /
LENGTH(:OUT_STR)
----------------
12
SQL>
This is annoying but it's the way PL/SQL works so we have to live with it.
The way to resolve the situaton is basically to apply DBC principles and validate our parameters. So, we can assert business rules against the inputs like this:
SQL> create or replace package body my_pkg as
2 procedure pad_string
3 (p_in_str varchar
4 , p_length number
5 , p_out_str out limited_string)
6 as
7 begin
8 if length(p_in_str) + p_length > 10 then
9 raise_application_error(
10 -20000
11 , 'Returned string cannot be longer than 10 characters!');
12 end if;
13 p_out_str := rpad(p_in_str, p_length, 'A');
14 end pad_string;
15 end my_pkg;
16 /
Package body created.
SQL>
SQL> exec my_pkg.pad_string('PAD THIS!', 12, :out_str)
BEGIN my_pkg.pad_string('PAD THIS!', 12, :out_str); END;
*
ERROR at line 1:
ORA-20000: Returned string cannot be longer than 10 characters!
ORA-06512: at "APC.MY_PKG", line 9
ORA-06512: at line 1
SQL>
Or we can assert business rules against the output like this:
SQL> create or replace package body my_pkg as
2 procedure pad_string
3 (p_in_str varchar
4 , p_length number
5 , p_out_str out limited_string)
6 as
7 l_str limited_string;
8 begin
9 l_str := rpad(p_in_str, p_length, 'A');
10 p_out_str := l_str;
11 end pad_string;
12 end my_pkg;
13 /
Package body created.
SQL>
SQL> exec my_pkg.pad_string('PAD THIS!', 12, :out_str)
BEGIN my_pkg.pad_string('PAD THIS!', 12, :out_str); END;
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at "APC.MY_PKG", line 9
ORA-06512: at line 1
SQL>
In most scenarios we should do both. This is the polite way to build interfaces, because it means other routines can call our procedures with the confidence that they will return the values they say they will.
You could use a subtype in a package header and type check that in the body...
CREATE OR REPLACE PACKAGE my_test
AS
SUBTYPE my_out IS VARCHAR2( 10 );
PROCEDURE do_something( pv_variable IN OUT my_out );
END;
/
CREATE OR REPLACE PACKAGE BODY my_test
AS
PROCEDURE do_something( pv_variable IN OUT my_out )
IS
lv_variable my_out;
BEGIN
-- Work on a local copy of the variable in question
lv_variable := 'abcdefghijklmnopqrstuvwxyz';
pv_variable := lv_variable;
END do_something;
END;
/
Then when you run this
DECLARE
lv_variable VARCHAR2(30);
BEGIN
my_test.do_something( lv_variable );
DBMS_OUTPUT.PUT_LINE( '['||lv_variable||']');
END;
/
You would get the error
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
Seems to go against the spirit of using an out parameter, but after Tony's comment this was the only thing I could think of to control data within the called code.

Passing an array of data as an input parameter to an Oracle procedure

I'm trying to pass an array of (varchar) data into an Oracle procedure. The Oracle procedure would be either called from SQL*Plus or from another PL/SQL procedure like so:
BEGIN
pr_perform_task('1','2','3','4');
END;
pr_perform_task will read each of the input parameters and perform the tasks.
I'm not sure as to how I can achieve this. My first thought was to use an input parameter of type varray but I'm getting Error: PLS-00201: identifier 'VARRAY' must be declared error, when the procedure definiton looks like this:
CREATE OR REPLACE PROCEDURE PR_DELETE_RECORD_VARRAY(P_ID VARRAY) IS
To summarize, how can I pass the data as an array, let the SP loop through each of the parameters and perform the task ?
I'm using Oracle 10gR2 as my database.
This is one way to do it:
SQL> set serveroutput on
SQL> CREATE OR REPLACE TYPE MyType AS VARRAY(200) OF VARCHAR2(50);
2 /
Type created
SQL> CREATE OR REPLACE PROCEDURE testing (t_in MyType) IS
2 BEGIN
3 FOR i IN 1..t_in.count LOOP
4 dbms_output.put_line(t_in(i));
5 END LOOP;
6 END;
7 /
Procedure created
SQL> DECLARE
2 v_t MyType;
3 BEGIN
4 v_t := MyType();
5 v_t.EXTEND(10);
6 v_t(1) := 'this is a test';
7 v_t(2) := 'A second test line';
8 testing(v_t);
9 END;
10 /
this is a test
A second test line
To expand on my comment to #dcp's answer, here's how you could implement the solution proposed there if you wanted to use an associative array:
SQL> CREATE OR REPLACE PACKAGE p IS
2 TYPE p_type IS TABLE OF VARCHAR2(50) INDEX BY BINARY_INTEGER;
3
4 PROCEDURE pp (inp p_type);
5 END p;
6 /
Package created
SQL> CREATE OR REPLACE PACKAGE BODY p IS
2 PROCEDURE pp (inp p_type) IS
3 BEGIN
4 FOR i IN 1..inp.count LOOP
5 dbms_output.put_line(inp(i));
6 END LOOP;
7 END pp;
8 END p;
9 /
Package body created
SQL> DECLARE
2 v_t p.p_type;
3 BEGIN
4 v_t(1) := 'this is a test of p';
5 v_t(2) := 'A second test line for p';
6 p.pp(v_t);
7 END;
8 /
this is a test of p
A second test line for p
PL/SQL procedure successfully completed
SQL>
This trades creating a standalone Oracle TYPE (which cannot be an associative array) with requiring the definition of a package that can be seen by all in order that the TYPE it defines there can be used by all.
If the types of the parameters are all the same (varchar2 for example), you can have a package like this which will do the following:
CREATE OR REPLACE PACKAGE testuser.test_pkg IS
TYPE assoc_array_varchar2_t IS TABLE OF VARCHAR2(4000) INDEX BY BINARY_INTEGER;
PROCEDURE your_proc(p_parm IN assoc_array_varchar2_t);
END test_pkg;
CREATE OR REPLACE PACKAGE BODY testuser.test_pkg IS
PROCEDURE your_proc(p_parm IN assoc_array_varchar2_t) AS
BEGIN
FOR i IN p_parm.first .. p_parm.last
LOOP
dbms_output.put_line(p_parm(i));
END LOOP;
END;
END test_pkg;
Then, to call it you'd need to set up the array and pass it:
DECLARE
l_array testuser.test_pkg.assoc_array_varchar2_t;
BEGIN
l_array(0) := 'hello';
l_array(1) := 'there';
testuser.test_pkg.your_proc(l_array);
END;
/

pl/sql object types "ORA-06530: Reference to uninitialized composite" error

i have a type as follows:
CREATE OR REPLACE TYPE tbusiness_inter_item_bag AS OBJECT (
item_id NUMBER,
system_event_cd VARCHAR2 (20),
CONSTRUCTOR FUNCTION tbusiness_inter_item_bag RETURN SELF AS RESULT
);
CREATE OR REPLACE TYPE BODY tbusiness_inter_item_bag
AS
CONSTRUCTOR FUNCTION tbusiness_inter_item_bag RETURN SELF AS RESULT
AS
BEGIN
RETURN;
END;
END;
when i execute the following script, i got a "Reference to uninitialized composite" error, which is imho quite suitable.
DECLARE
item tbusiness_inter_item_bag;
BEGIN
item.system_event_cd := 'ABC';
END;
This also raises the same error:
item.item_id := 3;
But if i change my object type into:
CREATE OR REPLACE TYPE tbusiness_inter_item_bag AS OBJECT (
item_id NUMBER(1),
system_event_cd VARCHAR2 (20),
CONSTRUCTOR FUNCTION tbusiness_inter_item_bag RETURN SELF AS RESULT
);
then the last statement raises no more error (where my "item" is still uninitialized):
item.item_id := 3;
Shouldn't i get the same ORA-06530 error?
ps: Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - 64bi
I reproduced the same behaviour in Oracle 11gR1. I would agree with you, this seems like a bug to me too, albeit a trivial one.
SQL> CREATE OR REPLACE TYPE tbusiness_inter_item_bag AS OBJECT (
2 item_id NUMBER(1),
3 system_event_cd VARCHAR2 (20),
4 CONSTRUCTOR FUNCTION tbusiness_inter_item_bag RETURN SELF AS RESULT
5 );
6 /
Type created.
SQL> DECLARE
2 item tbusiness_inter_item_bag;
3 BEGIN
4 item.item_id := 1;
5 END;
6 /
PL/SQL procedure successfully completed.
SQL>
Note that this still fails:
SQL> DECLARE
2 item tbusiness_inter_item_bag;
3 BEGIN
4 item.item_id := 1;
5 item.system_event_cd := 'ABC';
6 END;
7 /
DECLARE
*
ERROR at line 1:
ORA-06530: Reference to uninitialized composite
ORA-06512: at line 5
SQL>
Obviously, the correct practice is always initialize objects before referencing them.
SQL> DECLARE
2 item tbusiness_inter_item_bag := tbusiness_inter_item_bag();
3 BEGIN
4 item.system_event_cd := 'ABC';
5 END;
6 /
PL/SQL procedure successfully completed.
SQL>
You need to call the constructor you defined:
SQL> DECLARE
2 item tbusiness_inter_item_bag := tbusiness_inter_item_bag();
3 /* ^^ call the constructor */
4 BEGIN
5 item.system_event_cd := 'ABC';
6 END;
7 /
PL/SQL procedure successfully completed
I observe the behaviour you described on a 10.2.0.3 database. I wouldn't rely on it though, it looks like a bug.
I dont think its bug. As per documentation, you are supposed to initialize the composite type. This will always work :
SQL> DECLARE
2 item tbusiness_inter_item_bag := tbusiness_inter_item_bag();
3 BEGIN
4 item.system_event_cd := 'ABC';
5 END;
6 /
PL/SQL procedure successfully completed.
SQL>

Resources