Checking if a collection element exists in Oracle - oracle

I create a simple type:
create or replace TYPE SIMPLE_TYPE AS OBJECT (ID NUMBER(38), NAME VARCHAR2(20));
Simple test:
DECLARE
TYPE ObjectList IS TABLE OF SIMPLE_TYPE;
tmp SIMPLE_TYPE := SIMPLE_TYPE(1, 'a');
o ObjectList := new ObjectList(SIMPLE_TYPE(2, 'a'), SIMPLE_TYPE(3, 'a'));
BEGIN
IF tmp.EXISTS(tmp) THEN
dbms_output.put_line('OK, exists.');
END IF;
END;
I get an exception: PLS-00302: component 'EXISTS' must be declared
But this example work:
DECLARE
TYPE NumList IS TABLE OF INTEGER;
n NumList := NumList(1,3,5,7);
BEGIN
n.DELETE(2);
IF n.EXISTS(1) THEN
dbms_output.put_line('OK, element #1 exists.');
END IF;
IF n.EXISTS(3) = FALSE THEN
dbms_output.put_line('OK, element #2 has been deleted.');
END IF;
IF n.EXISTS(99) = FALSE THEN
dbms_output.put_line('OK, element #99 does not exist at all.');
END IF;
END;
Is it possible to implement EXISTS method in SIMPLE_TYPE type?

As the documentation states, EXISTS() tests for the existence of a numbered entry in a collection. That is, array.exists(3) asserts that the third element of array is populated.
What you are trying to do in your first example is test whether the instance tmp matches an element in ObjectList. From 10g onwards we can do this using the MEMBER OF syntax. Unfortunately, in order to make that work we have to declare a MAP method, which is rather clunky and would get rather annoying if the object has a lot of attributes.
SQL> create or replace type simple_type as object
2 ( id number
3 , name varchar2(30)
4 , map member function compare return varchar2);
5 /
Type created.
SQL>
SQL> create or replace type body simple_type as
2 map member function compare return varchar2
3 is
4 return_value integer;
5 begin
6 return to_char(id, '0000000')||name;
7 end compare;
8 end;
9 /
Type body created.
SQL>
Running the example...
SQL> set serveroutput on size unlimited
SQL>
SQL> declare
2 type objectlist is table of simple_type;
3 tmp simple_type := simple_type(1, 'a');
4 o objectlist := new objectlist(simple_type(2, 'a'), simple_type(3, 'a'));
5 begin
6 if tmp MEMBER OF o then
7 dbms_output.put_line('ok, exists.');
8 else
9 dbms_output.put_line('search me');
10 end if;
11 end;
12 /
search me
PL/SQL procedure successfully completed.
SQL>

tmp SIMPLE_TYPEE := SIMPLE_TYPE(1, 'a');
…
IF tmp.EXISTS(tmp) THEN
You declare tmp as SIMPLE_TYPE, not ObjectList.
SIMPLE_TYPE is scalar type, not a collection.
Probably you wanted to check o.EXISTS instead (which is an ObjectList)?
Update:
EXISTS when applied to a collection takes an integer index as an argument and checks if the element with this index exists (not its value).
To check that SIMPLE_TYPE(1, 'a') exists in your table, you should so the following:
Create ObjectList in a dictionary:
CREATE TYPE ObjectList IS TABLE OF SIMPLE_TYPE;
Issue the SELECT query:
DECLARE
tmp SIMPLE_TYPE := SIMPLE_TYPE(1, 'a');
o ObjectList := new ObjectList(SIMPLE_TYPE(2, 'a'), SIMPLE_TYPE(3, 'a'));
myid INT;
BEGIN
SELECT 1
INTO myid
FROM TABLE(o) q
WHERE SIMPLE_TYPE(q.id, q.name) = tmp
AND rownum = 1;
IF (myid = 1) THEN
dbms_output.put_line('OK, exists.');
END IF;
END;

Related

PL/SQL Table Function - How to return an empty result set?

I'm trying to create a PL/SQL table function. I don't mind if it is PIPELINED or not. I just want it to return a query-able result set.
And I want to start with an empty result set. (Because it is possible that the result set I intend to construct will be empty.)
Is it possible to create a PL/SQL table function that returns zero rows?
In the books I have, and tutorials I can find, I only see examples that must return at least one record.
Example of the problem:
CREATE OR REPLACE PACKAGE z_util AS
TYPE t_row
IS RECORD (
CATEGORY VARCHAR2( 128 CHAR )
, MEASURE NUMBER
);
TYPE t_tab
IS TABLE OF t_row;
FUNCTION f_test
RETURN t_tab;
END z_util;
/
CREATE OR REPLACE PACKAGE BODY z_util AS
FUNCTION f_test
RETURN t_tab IS
retval t_tab;
BEGIN
RETURN retval;
END;
END z_util;
/
SELECT test.*
FROM TABLE ( z_util.f_test ) test;
Output:
Error starting at line : 24 in command -
SELECT test.*
FROM TABLE ( z_util.f_test ) test
Error at Command Line : 25 Column : 14
Error report -
SQL Error: ORA-00902: invalid datatype
00902. 00000 - "invalid datatype"
*Cause:
*Action:
Something like this?
SQL> CREATE TYPE t_row AS OBJECT (id NUMBER, name VARCHAR2 (50));
2 /
Type created.
SQL> CREATE TYPE t_tab IS TABLE OF t_row;
2 /
Type created.
SQL> CREATE OR REPLACE FUNCTION f_test
2 RETURN t_tab
3 AS
4 retval t_tab;
5 BEGIN
6 RETURN retval;
7 END;
8 /
Function created.
SQL> SELECT f_test FROM DUAL;
F_TEST(ID, NAME)
--------------------------------------------------------------------
SQL>
Saying that this is too simple and that it doesn't work while in package:
SQL> CREATE OR REPLACE PACKAGE pkg_test
2 AS
3 FUNCTION f_test
4 RETURN t_tab;
5 END;
6 /
Package created.
SQL> CREATE OR REPLACE PACKAGE BODY pkg_test
2 AS
3 FUNCTION f_test
4 RETURN t_tab
5 AS
6 retval t_tab;
7 BEGIN
8 RETURN retval;
9 END f_test;
10 END pkg_test;
11 /
Package body created.
SQL> select pkg_test.f_test from dual;
F_TEST(ID, NAME)
-------------------------------------------------------
SQL>
Works OK as well. Did you, by any chance, declare type within the package? If so, try to create it at SQL level.
See 6.4.6 Querying a Collection:
Note: In SQL contexts, you cannot use a function whose return type was declared in a package specification.
You cannot unnest the result of function, but the variable with the same data type:
create or replace package utils as
type t_row is record (category varchar2 (8), measure number);
type t_tab is table of t_row;
function passon (t t_tab:=null) return t_tab;
end utils;
/
create or replace package body utils as
function passon (t t_tab:=null) return t_tab is
begin
return t;
end;
end utils;
/
Usage and outcomes:
var rc refcursor
declare
tab1 utils.t_tab := utils.passon (); -- empty
tab2 utils.t_tab := utils.passon (utils.t_tab (utils.t_row ('category', 50)));
begin
open :rc for
select * from table (tab1) union all
select * from table (tab2);
end;
/
CATEGORY MEASURE
-------- ----------
category 50
jsut use a return statment without any parameters.
I have already one table function that splits a string based in token.
I modified the code for you as an example answet for your quetion.
If you pass the first string null, the function will return no rows, otherwise will return each token in separate row based on the second parameter offcurse.
// create needed types
// row object
CREATE OR REPLACE TYPE T_TOKEN_ROW AS OBJECT (
id NUMBER,
token_text VARCHAR2(50)
);
// table object
CREATE OR REPLACE TYPE T_TOKEN_TAB IS TABLE OF t_token_row;
// create table function to toknize a string
// input : P_string : the string to be toknized
// P_separator : a character to separate tokens
// Outputs : each token in separate record with id field
CREATE OR REPLACE FUNCTION PIPE_tokens (P_string varchar2,P_separator char) RETURN t_token_tab PIPELINED
AS
sLine VARCHAR2(2000);
nPos INTEGER;
nPosOld INTEGER;
nIndex INTEGER;
nLength INTEGER;
nCnt INTEGER;
sToken VARCHAR2(200);
BEGIN
if (P_string is null ) then
return ;
else
sLine := P_string;
IF (SUBSTR(sLine, LENGTH(sLine), 1) <> '|') THEN
sLine := sLine || '|';
END IF;
nPos := 0;
sToken := '';
nLength := LENGTH(sLine);
nCnt := 0;
FOR nIndex IN 1..nLength LOOP
IF ((SUBSTR(sLine, nIndex, 1) = P_separator) OR (nIndex = nLength)) THEN
nPosOld := nPos;
nPos := nIndex;
nCnt := nCnt + 1;
sToken := SUBSTR(sLine, nPosOld + 1, nPos - nPosOld - 1);
PIPE ROW(t_token_row(nCnt,sToken));
--tTokenTab(nCnt) := sToken;
END IF;
END LOOP;
RETURN;
end if;
END;
// 2 Test query
select * from table(PIPE_tokens(null,';')) ;
select * from table(PIPE_tokens('5;2;3',';')) ;

PL/SQL Record populate

I have a record as following and I want to populate this with a for loop.
declare
type ch_type is table of record(id number, name varchar2(50));
type ch_type_tab is table of ch_type;
rec_typr ch_type_tab;
begin
for i in (select * from emp) loop
rec_typr.id := i.emp_id;
rec_typr.name := i.first_name;
end loop;
for i in rec_typr.first..rec_typr.last
loop
dbms_output.put_line(rec_typr(i).id);
end loop;
end;
but I get the error:
PLS:0302 component first must be declared.
Can you help me with this?
Two things are problematic in your code.
1) type ch_type is table of record is syntactically incorrect. You must first declare a record and then define its collection type.
2) Using implicit cursor loop is not an efficient method to load a collection and definitely can't be done the way you're trying to do. Use much simpler BULK COLLECT method instead.
declare
type ch_type is record(id number, name varchar2(50));
type ch_type_tab is table of ch_type;
rec_typr ch_type_tab;
begin
select emp_id,first_name bulk collect into
rec_typr from emp;
for i in rec_typr.first..rec_typr.last
loop
dbms_output.put_line(rec_typr(i).id);
end loop;
end;
/
Output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
PL/SQL procedure successfully completed.
EDIT
I need to populate the record through a loop not through bulk collect.
Is it any way?
Yes, there is. But, it is less efficient than the method described above.
declare
type ch_type is record(id number, name varchar2(50));
type ch_type_tab is table of ch_type;
rec_typr ch_type_tab := ch_type_tab();
i INTEGER := 1;
begin
rec_typr.extend;
for rec in
(
select emp_id,first_name bulk collect into
rec_typr from emp
)
loop
rec_typr(i).id := rec.emp_id;
rec_typr(i).name := rec.first_name;
rec_typr.extend;
i := i + 1;
end loop;
for i in rec_typr.first..rec_typr.last
loop
dbms_output.put_line(rec_typr(i).id);
end loop;
end;
/

Oracle Stored Procedure to select from multiple procedures, Array as IN parameter and 1 dynamic condition based on the length of the array element

I have 3 procedures which will return different columns values to insert in same table.
All procedures take Array as parameter and values of array can be of two type. if type a then where name = "tin" and if type 2 then name = "tan".
EX:
Array a = {a,b,b,a};
if(a){
name="tin"
}else{
name="tan"
}
how I will write a procedure to get all the values passing the array and 1 more condition based the length of array element.
set serveroutput on
CREATE OR REPLACE TYPE arrtype AS VARRAY(200) OF VARCHAR2(50);
/
created
CREATE OR REPLACE PROCEDURE arrtest (arr_in arrtype) IS
BEGIN
FOR i IN 1..arr_in.count LOOP
if(arr_in(i)='a') then
dbms_output.put_line('TIN');
ELSIF(arr_in(i)='b') then
dbms_output.put_line('TAN');
else
dbms_output.put_line('neither TIN nor TAN');
end if;
END LOOP;
END;
/
Procedure created
SQL> DECLARE
2 array1 arrtype;
3 BEGIN
4 array1 := arrtype();
5 array1.EXTEND(3);
6 array1(1) := 'a';
7 array1(2) := 'b';
8 array1(3) := 'c';
9 arrtest(array1);
10 END;
11 /
TIN
TAN
neither TIN nor TAN

Find specific varchar in Oracle Nested Table

I'm new to PL-SQL, and struggling to find clear documentation of operations are nested tables. Please correct any misused terminology etc.
I have a nested table type that I use as a parameters for a stored procedure.
CREATE OR REPLACE TYPE "STRARRAY" AS TABLE OF VARCHAR2 (255)
In my stored procedure, the table is initialized and populated. Say I have a VARCHAR2 variable, and I want to know true or false if that varchar exists in the nested table.
I tried
strarray.exists('somevarchar')
but I get an ORA-6502
Is there an easier way to do that other than iterating?
FOR i IN strarray.FIRST..strarray.LAST
LOOP
IF strarray(i) = value THEN
return 1;--found
END IF;
END LOOP;
For single value check I prefer the "member" operator.
zep#dev> declare
2 enames strarray;
3 wordToFind varchar2(255) := 'King';
4 begin
5 select emp.last_name bulk collect
6 into enames
7 from employees emp;
8 if wordToFind member of enames then
9 dbms_output.put_line('Found King');
10 end if;
11 end;
12 /
Found King
PL/SQL procedure successfully completed
zep#dev>
You can use the MULTISET INTERSECT operator to determine whether the string you're interested in exists in the collection. For example
declare
l_enames strarray;
l_interesting_enames strarray := new strarray( 'KING' );
begin
select ename
bulk collect into l_enames
from emp;
if( l_interesting_enames = l_interesting_enames MULTISET INTERSECT l_enames )
then
dbms_output.put_line( 'Found King' );
end if;
end;
will print out "Found King" if the string "KING" is an element of the l_enames collection.
You should pass an array index, not an array value to an exists in case you'd like to determine whether this element exists in collection. Nested tables are indexed by integers, so there's no way to reference them by strings.
However, you might want to look at associative arrays instead of collections in case you wish to reference your array element by string index. This will look like this:
DECLARE
TYPE assocArray IS TABLE OF VARCHAR2(100) INDEX BY VARCHAR2(100);
myArray assocArray;
BEGIN
myArray('foo') := 'bar';
IF myArray.exists('baz') THEN
dbms_output.put_line(myArray('baz'));
ELSIF myArray.exists('foo') THEN
dbms_output.put_line(myArray('foo'));
END IF;
END;
Basically, if your array values are distinct, you can create paired arrays referencing each other, like,
arr('b') := 'a'; arr('a') := 'b';
This technique might help you to easily look up any element and its index.
When a nested table is declared as a schema-level type, as you have done, it can be used in any SQL query as a table. So you can write a simple function like so:
CREATE OR REPLACE FUNCTION exists_in( str VARCHAR2, tab stararray)
RETURN BOOLEAN
AS
c INTEGER;
BEGIN
SELECT COUNT(*)
INTO c
FROM TABLE(CAST(tab AS strarray))
WHERE column_value = str;
RETURN (c > 0);
END exists_in;

Selecting Values from Oracle Table Variable / Array?

Following on from my last question (Table Variables in Oracle PL/SQL?)...
Once you have values in an array/table, how do you get them back out again? Preferably using a select statement or something of the like?
Here's what I've got so far:
declare
type array is table of number index by binary_integer;
pidms array;
begin
for i in (
select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
)
loop
pidms(pidms.count+1) := i.sgbstdn_pidm;
end loop;
select *
from pidms; --ORACLE DOESN'T LIKE THIS BIT!!!
end;
I know I can output them using dbms_output.putline(), but I'm hoping to get a result set like I would from selecting from any other table.
Thanks in advance,
Matt
You might need a GLOBAL TEMPORARY TABLE.
In Oracle these are created once and then when invoked the data is private to your session.
Oracle Documentation Link
Try something like this...
CREATE GLOBAL TEMPORARY TABLE temp_number
( number_column NUMBER( 10, 0 )
)
ON COMMIT DELETE ROWS;
BEGIN
INSERT INTO temp_number
( number_column )
( select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
);
FOR pidms_rec IN ( SELECT number_column FROM temp_number )
LOOP
-- Do something here
NULL;
END LOOP;
END;
/
In Oracle, the PL/SQL and SQL engines maintain some separation. When you execute a SQL statement within PL/SQL, it is handed off to the SQL engine, which has no knowledge of PL/SQL-specific structures like INDEX BY tables.
So, instead of declaring the type in the PL/SQL block, you need to create an equivalent collection type within the database schema:
CREATE OR REPLACE TYPE array is table of number;
/
Then you can use it as in these two examples within PL/SQL:
SQL> l
1 declare
2 p array := array();
3 begin
4 for i in (select level from dual connect by level < 10) loop
5 p.extend;
6 p(p.count) := i.level;
7 end loop;
8 for x in (select column_value from table(cast(p as array))) loop
9 dbms_output.put_line(x.column_value);
10 end loop;
11* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
SQL> l
1 declare
2 p array := array();
3 begin
4 select level bulk collect into p from dual connect by level < 10;
5 for x in (select column_value from table(cast(p as array))) loop
6 dbms_output.put_line(x.column_value);
7 end loop;
8* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
Additional example based on comments
Based on your comment on my answer and on the question itself, I think this is how I would implement it. Use a package so the records can be fetched from the actual table once and stored in a private package global; and have a function that returns an open ref cursor.
CREATE OR REPLACE PACKAGE p_cache AS
FUNCTION get_p_cursor RETURN sys_refcursor;
END p_cache;
/
CREATE OR REPLACE PACKAGE BODY p_cache AS
cache_array array;
FUNCTION get_p_cursor RETURN sys_refcursor IS
pCursor sys_refcursor;
BEGIN
OPEN pCursor FOR SELECT * from TABLE(CAST(cache_array AS array));
RETURN pCursor;
END get_p_cursor;
-- Package initialization runs once in each session that references the package
BEGIN
SELECT level BULK COLLECT INTO cache_array FROM dual CONNECT BY LEVEL < 10;
END p_cache;
/
The sql array type is not neccessary. Not if the element type is a primitive one. (Varchar, number, date,...)
Very basic sample:
declare
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
pidms TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into pidms
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH';
-- do something with pidms
open :someCursor for
select value(t) pidm
from table(pidms) t;
end;
When you want to reuse it, then it might be interesting to know how that would look like.
If you issue several commands than those could be grouped in a package.
The private package variable trick from above has its downsides.
When you add variables to a package, you give it state and now it doesn't act as a stateless bunch of functions but as some weird sort of singleton object instance instead.
e.g. When you recompile the body, it will raise exceptions in sessions that already used it before. (because the variable values got invalided)
However, you could declare the type in a package (or globally in sql), and use it as a paramter in methods that should use it.
create package Abc as
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList;
function Test1(list in TPidmList) return PLS_Integer;
-- "in" to make it immutable so that PL/SQL can pass a pointer instead of a copy
procedure Test2(list in TPidmList);
end;
create package body Abc as
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList is
result TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into result
from sgbstdn
where sgbstdn_majr_code_1 = majorCode
and sgbstdn_program_1 = program;
return result;
end;
function Test1(list in TPidmList) return PLS_Integer is
result PLS_Integer := 0;
begin
if list is null or list.Count = 0 then
return result;
end if;
for i in list.First .. list.Last loop
if ... then
result := result + list(i);
end if;
end loop;
end;
procedure Test2(list in TPidmList) as
begin
...
end;
return result;
end;
How to call it:
declare
pidms constant Abc.TPidmList := Abc.CreateList('HS04', 'HSCOMPH');
xyz PLS_Integer;
begin
Abc.Test2(pidms);
xyz := Abc.Test1(pidms);
...
open :someCursor for
select value(t) as Pidm,
xyz as SomeValue
from table(pidms) t;
end;

Resources