Oracle CAST MULTISET ORA-00902: invalid datatype - oracle

I'm trying to sort an Oracle PL/SQL collection (an associative array) using the approach presented here. I modified the example from the linked page in order to use an associative array, but I guess I'm having some casting issues.
Here it is my code:
DECLARE
TYPE TABLE_TYPE IS TABLE OF NUMBER INDEX BY VARCHAR2(10);
table_in TABLE_TYPE;
table_out TABLE_TYPE;
BEGIN
-- 1. Populating the collection with random numbers between 1 and 50
FOR i IN 1..9 LOOP
SELECT ROUND(DBMS_RANDOM.VALUE(1,50))
INTO table_in('key-'||i)
FROM DUAL;
END LOOP;
-- 2. Trying to order the collection -> throws ORA-00902: invalid datatype
SELECT CAST (MULTISET(
SELECT * FROM TABLE(table_in)
ORDER BY 2
) AS TABLE_TYPE
)
INTO table_out
FROM DUAL;
END;
What am I doing wrong?

You can't use associative array as a table in select, because that time is working SQL Engine and SQL Engine doesn't know what is associative array, SQL Engine can only works with Table Type, and you don't need first select (select from dual), you can do it via pl/sql engine, change it like this
....................
TYPE NumbersTableType IS TABLE OF NUMBER; -- it must be in global types, not in your local package
....................
DECLARE
table_in NumbersTableType := NumbersTableType();
table_out NumbersTableType;
BEGIN
-- 1. Populating the collection with random numbers between 1 and 50
FOR i IN 1..9 LOOP
table_in.Extend;
table_in(i):=ROUND(DBMS_RANDOM.VALUE(1,50));
END LOOP;
SELECT CAST (MULTISET(
SELECT * FROM TABLE(table_in)
ORDER BY 1
) AS NumbersTableType
)
INTO table_out
FROM DUAL;
END;

For sake of completeness I'll report here the trivial workaround I used to order a collection (index by varchar) in Oracle version before 12c. The solution just replaces the associative array collection with a custom Table type (in order to make it visible to the SQL Engine).
CREATE OR REPLACE TYPE MY_TABLE_TYPE_OBJ AS OBJECT (
THE_KEY VARCHAR2(10),
THE_VALUE NUMBER
);
/
CREATE OR REPLACE TYPE MY_TABLE_TYPE IS TABLE OF MY_TABLE_TYPE_OBJ;
/
DECLARE
my_key VARCHAR2(10);
table_in MY_TABLE_TYPE := MY_TABLE_TYPE();
table_out MY_TABLE_TYPE := MY_TABLE_TYPE();
BEGIN
-- Creating an unsorted collection
FOR i IN 1..9 LOOP
table_in.EXTEND;
table_in(i) := MY_TABLE_TYPE_OBJ(
'key-'||i,
ROUND(DBMS_RANDOM.VALUE(1,50))
);
END LOOP;
-- Sorting the collection
SELECT CAST (MULTISET(
SELECT * FROM TABLE(table_in)
ORDER BY 2
) AS MY_TABLE_TYPE
)
INTO table_out
FROM DUAL;
END;
Hope this helps.

Related

Issue selecting from Associative Array using a Table Collection Expression

I seem to be having an issue when using a Table Collection Expression to select from an Associative Array (Note: I am using Oracle 12c, so this is allowed: Oracle Documentation)
Take the following "simple" example:
First, create a package which declares a record and the associative array:
CREATE OR REPLACE PACKAGE table_test
IS
TYPE pt_DateSpan IS RECORD
(
StartDate DATE,
EndDate DATE
);
TYPE pt_DateSpanTable IS TABLE OF pt_DateSpan INDEX BY PLS_INTEGER;
END;
/
Then, I wrote the following anonymous block to test the functionality:
DECLARE
l_tTest table_test.pt_DateSpanTable;
PROCEDURE lp_outputAArray (p_aaInput table_test.pt_DateSpanTable) IS
l_nTableSize INTEGER;
BEGIN
--I know I can use p_aaInput.COUNT, but I want to select using TABLE() to show that the functionality "works"
SELECT COUNT(*)
INTO l_nTableSize
FROM TABLE(p_aaInput);
dbms_output.put_line('Table Size: '||l_nTableSize);
FOR i IN 1..p_aaInput.COUNT LOOP
dbms_output.put_line(i||': '||to_char(p_aaInput(i).StartDate, 'MM/DD/YYYY')||' - '||to_char(p_aaInput(i).EndDate, 'MM/DD/YYYY'));
END LOOP;
END lp_outputAArray;
BEGIN
--ADD RECORD TO ASSOCIATIVE ARRAY
SELECT to_date('01/01/2000', 'MM/DD/YYYY'), to_date('01/01/2010', 'MM/DD/YYYY')
BULK COLLECT INTO l_tTest
FROM DUAL;
lp_outputAArray(l_tTest);
--SELECT THE ASSOCIATIVE ARRAY INTO ITSELF
SELECT t.StartDate, t.EndDate
BULK COLLECT INTO l_tTest
FROM TABLE(l_tTest) t;
lp_outputAArray(l_tTest);
END;
/
This block produces the following output:
Table Size: 1
1: 01/01/2000 - 01/01/2010
Table Size: 0
My question is why is the second output not identical to the first?
Also, I realize that I don't need to use BULK COLLECT in most of this example, it is a simplified version of my actual code which does SELECT from actual tables.
My final goal was to use UNION ALL to allow me to append values to my Associative Array instead of replacing it when performing a series of SELECT statements. Something like this:
SELECT *
BULK COLLECT INTO l_tTest
FROM (SELECT t.StartDate, t.EndDate
FROM TABLE(l_tTest) t
UNION ALL
SELECT to_date('01/01/2011', 'MM/DD/YYYY'), to_date('01/01/2019', 'MM/DD/YYYY')
FROM DUAL);
I would appreciate any help you could provide.
It appears that when you use:
SELECT ...
BULK COLLECT INTO array
FROM ...
Then the first thing that happens is that the array you BULK COLLECT INTO is re-initialised to an empty array.
Therefore, when you want to use it in the table collection expression it is already empty and no rows are generated.
Instead, you could use a non-associative array and use the MULTISET operators in PL/SQL:
CREATE OR REPLACE PACKAGE table_test
IS
TYPE range IS RECORD
(
StartDate DATE,
EndDate DATE
);
TYPE range_table IS TABLE OF range
--INDEX BY PLS_INTEGER
;
END;
/
DECLARE
l_ranges table_test.range_table := table_test.range_table();
l_ranges2 table_test.range_table := table_test.range_table();
PROCEDURE output_ranges(
range_array table_test.range_table
)
IS
idx PLS_INTEGER;
BEGIN
dbms_output.put_line('Table Size: '||range_array.COUNT);
idx := range_array.FIRST;
LOOP
EXIT WHEN idx IS NULL;
dbms_output.put_line(
idx||': '||range_array(idx).StartDate||' - '||range_array(idx).EndDate
);
idx := range_array.NEXT(idx);
END LOOP;
END output_ranges;
BEGIN
l_ranges.EXTEND(2);
l_ranges(1) := table_test.range(DATE '2000-01-01', DATE '2001-01-01');
l_ranges(2) := table_test.range(DATE '2001-01-01', DATE '2002-01-01');
l_ranges2.EXTEND(2);
l_ranges2(1) := table_test.range(DATE '2002-01-01', DATE '2003-01-01');
l_ranges2(2) := table_test.range(DATE '2003-01-01', DATE '2004-01-01');
output_ranges(l_ranges);
output_ranges(l_ranges2);
l_ranges := l_ranges MULTISET UNION ALL l_ranges2;
output_ranges(l_ranges);
END;
/
Which outputs:
Table Size: 2
1: 01-JAN-00 - 01-JAN-01
2: 01-JAN-01 - 01-JAN-02
Table Size: 2
1: 01-JAN-02 - 01-JAN-03
2: 01-JAN-03 - 01-JAN-04
Table Size: 4
1: 01-JAN-00 - 01-JAN-01
2: 01-JAN-01 - 01-JAN-02
3: 01-JAN-02 - 01-JAN-03
4: 01-JAN-03 - 01-JAN-04

looping through an array for the where condition pl/sql

Is it possible in pl/sql to loop through a number of id's that need to go in the WHERE clause of the pl/sql statement. The sql statement itself is pretty simple, but I need to iterate over a number of id's:
SELECT x_name
FROM table_x
WHERE x_id = {array of 90 id's};
How can I insert the 90 id's here so that sql iterates over them? I tried using the cursor For Loop, but I'm stuck. The code below is erroneous, but it might give an indication what Im trying to achieve here
DECLARE
TYPE x_id_array IS VARRAY(3) OF NUMBER;
CURSOR cur_x_id (x_ondz_id NUMBER) IS
SELECT x_name
FROM table_x
WHERE x_id = var_ondz_id;
loop_total integer;
x_id x_id_array;
name VARCHAR;
BEGIN
x_id_new := x_id_array(8779254, 8819930, 8819931); --3 for testing
loop_total := x_id_new.count;
FOR i in 1 .. loop_total LOOP
dbms_output.put_line('x_id: ' || x_id_new(i) || '= Name: ' || x_name );
END LOOP;
END;
/
The expected out put would be
x_id: 8779254= Name: Name_1
x_id: 8819930= Name: Name_2
x_id: 8819931= Name: Name_3
...
... etc for all 90 id's in the array
Any help is appreciated
We can use TABLE function on a collection to get a list of numbers / character.
SELECT *
FROM TABLE ( sys.odcinumberlist(8779254,8819930,8819931) );
8779254
8819930
8819931
Here I'm using Oracle's internal VARRAY with a limit of 32767. You may use your own NESTED TABLE type.
create OR REPLACE TYPE yourtype AS TABLE OF NUMBER;
and then select it.
SELECT *
FROM TABLE ( yourtype(8779254,8819930,8819931) );
So, your query can simply be written as
SELECT x_name
FROM table_x
WHERE x_id IN ( SELECT * FROM
TABLE ( yourtype(8779254,8819930,8819931) ) );
12.2 and above, you won't even need to specify TABLE.
SELECT * FROM yourtype(8779254,8819930,8819931) works.

How to access and query objects passed as parameter to a procedure while converting from Oracle to postgresql

I have a procedure in Oracle that I need to convert to Postgresql and need help on it. It paases a collection of objects in a procedure.The procedure then checks if each object is present in a database table or not and if present it gives a message that , that specific element is found/present. if some element that is paassed to the procedure is not present in the table, the procedure just doesnt do anything. I have to write equivalent of that in postgresql. I think the heart of the issue is this statement:
SELECT COUNT (*)
INTO v_cnt
FROM **TABLE (p_cust_tab_type_i)** pt
WHERE pt.ssn = cc.ssn;
In Oracle a collection can be treated as a table and one can query it but I dont know how to do that in postgresql. The code to create the table, add data, create the procedure, call the procedure by passing the collection (3 objects) and output of that is posted below. Can someone suggest how this can be done in postgresql?
Following the oracle related code and details:
--create table
create table temp_n_tab1
(ssn number,
fname varchar2(20),
lname varchar2(20),
items varchar2(100));
/
--add data
insert into temp_n_tab1 values (1,'f1','l1','i1');
--SKIP no. ssn no. 2 intentionally..
insert into temp_n_tab1 values (3,'f3','l3','i3');
insert into temp_n_tab1 values (4,'f4','l4','i4');
insert into temp_n_tab1 values (5,'f5','l5','i5');
insert into temp_n_tab1 values (6,'f6','l6','i6');
commit;
--create procedure
SET SERVEROUTPUT ON
CREATE OR REPLACE PROCEDURE temp_n_proc (
p_cust_tab_type_i IN temp_n_customer_tab_type)
IS
t_cust_tab_type_i temp_n_customer_tab_type;
v_cnt NUMBER;
v_ssn temp_n_tab1.ssn%TYPE;
CURSOR c
IS
SELECT ssn
FROM temp_n_tab1
ORDER BY 1;
BEGIN
--t_cust_tab_type_i := p_cust_tab_type_i();
FOR cc IN c
LOOP
SELECT COUNT (*)
INTO v_cnt
FROM TABLE (p_cust_tab_type_i) pt
WHERE pt.ssn = cc.ssn;
IF (v_cnt > 0)
THEN
DBMS_OUTPUT.put_line (
'The array element '
|| TO_CHAR (cc.ssn)
|| ' exists in the table.');
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE (SQLERRM);
END;
/
--caller proc
SET SERVEROUTPUT ON
declare
array temp_n_customer_tab_type := temp_n_customer_tab_type();
begin
for i in 1 .. 3
loop
array.extend;
array(i) := temp_n_cust_header_type( i, 'name ' || i, 'lname ' || i,i*i*i*i );
end loop;
temp_n_proc( array );
end;
/
caller proc output:
The array element 1 exists in the table.
The array element 3 exists in the table.
When you create a table in Postgres, a type with the same name is also created. So you can simply pass an array of the table's type as a parameter to the function.
Inside the function you can then use unnest() to treat the array like a table.
The following is the closest match to your original Oracle code:
create function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns void
as
$$
declare
l_rec record;
l_msg text;
l_count integer;
BEGIN
for l_rec in select t1.ssn
from temp_n_tab1 t1
loop
select count(*)
into l_count
from unnest(p_cust_tab_type_i) as t
where t.ssn = l_rec.ssn;
if l_count > 0 then
raise notice 'The array element % exist in the table', l_rec.ssn;
end if;
end loop;
END;
$$
language plpgsql;
The row-by-row processing is not a good idea to begin with (neither in Postgres, nor in Oracle). It would be a lot more efficient to get the existing elements in a single query:
create function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns void
as
$$
declare
l_rec record;
l_msg text;
BEGIN
for l_rec in select t1.ssn
from temp_n_tab1 t1
where t1.ssn in (select t.ssn
from unnest(p_cust_tab_type_i) as t)
loop
raise notice 'The array element % exist in the table', l_rec.ssn;
end loop;
return;
END;
$$
language plpgsql;
You can call the function like this:
select temp_n_proc(array[row(1,'f1','l1','i1'),
row(2,'f2','l2','i2'),
row(3,'f3','l3','i3')
]::temp_n_tab1[]);
However a more "Postgres" like and much more efficient way would be to not use PL/pgSQL for this, but create a simple SQL function that returns the messages as a result:
create or replace function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns table(message text)
as
$$
select format('The array element %s exist in the table', t1.ssn)
from temp_n_tab1 t1
where t1.ssn in (select t.ssn
from unnest(p_cust_tab_type_i) as t)
$$
language sql;
This returns the output of the function as a result rather than using the clumsy raise notice.
You can use it like this:
select *
from temp_n_proc(array[row(1,'f1','l1','i1'),
row(2,'f2','l2','i2'),
row(3,'f3','l3','i3')
]::temp_n_tab1[]);

PL SQL : Create oracle record dynamically

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.

Oracle table type to nested table cast error

I declared table type and set a value in it with using loop. I am having an error while I was casting this t_table
DECLARE
TYPE t_row IS RECORD
(
id NUMBER,
description VARCHAR2(50)
);
TYPE t_table IS TABLE OF t_row;
l_tab t_table := t_table();
BEGIN
FOR i IN 1 .. 10 LOOP
l_tab.extend();
l_tab(l_tab.last).id := i;
l_tab(l_tab.last).description := 'Description for ' || i;
END LOOP;
SELECT * from TABLE(CAST(l_tab AS t_table));
END
Best regards
Why do you want to do a select onto the the type? You would use the the TABLE() and the CAST rather if you have a collection in a column stored in a table.
You could just loop through the table in your code. Example:
for i in l_tab.first .. l_tab.last
loop
dbms_output.put_line(l_tab(i).id||' '||l_tab(i).description);
end loop;
Since l_tab is of type t_table, there's no need for the cast. But that's not your problem.
Your problem is that you're trying to reference a PL/SQL type in SQL, which you simply can't do. You can either remove the select as #hol suggested or make the type a database object (which will allow SQL to access it):
CREATE OR REPLACE TYPE t_row AS OBJECT
(
id NUMBER,
description VARCHAR2 (50)
);
CREATE OR REPLACE TYPE t_table AS TABLE OF t_row;
DECLARE
l_tab t_table := t_table ();
BEGIN
FOR i IN 1 .. 10 LOOP
l_tab.EXTEND ();
l_tab (l_tab.LAST) := t_row (i, 'Description for ' || i);
END LOOP;
FOR r IN (SELECT * FROM TABLE (l_tab)) LOOP
DBMS_OUTPUT.put_line (r.id);
END LOOP;
END;
There is a second problem with the initial code, in that you are running a select without telling the code what to do with it. Unlike some other procedural SQL extensions, PL/SQL does not allow you to implicitly return a handle to a resultset (prior to 12c). You must either handle it directly or explicitly return a ref_cursor that points to it. The code above has been update to primitively handle the result of the query.

Resources