How do I push items into arrays and iterate through them in PL/SQL? - arrays

I'm trying to do something very basic in PL/SQL, but I keep getting owned... how do I push items into an array and iterate through them?
Googling it seems to suggest using owa_text.multi_line;
owa_text.multi_line is a record of this type:
/* A multi_line is just an abstract datatype which can hold */
/* large amounts of text data as one piece. */
type multi_line is record
(
rows vc_arr,
num_rows integer,
partial_row boolean
);
To iterate through vc_arr, we have to use l_array.first.. l_array.last. But that gives an error while trying to access it.
Here's a simple sample to find load distinct values into an array:
declare
l_persons owa_text.multi_line := owa_text.new_multi();
/* Documentation of owa_text.new_multi(): Standard "make element" routines. */
--function new_multi return multi_line;
l_value_exists boolean := false;
cursor c_get_orders is
select person,
choice
from my_orders;
begin
for i in c_get_orders loop
l_value_exists := false;
for j in l_persons.rows.first.. l_persons.rows.last loop --Fails here,
--PL/SQL: numeric or value error
if l_persons.rows(j) = i.person then
l_value_exists := true;
exit;
end if;
end loop;
if not l_value_exists then
owa_text.add2multi(i.person, l_persons);
end if;
end loop;
for i in l_persons.rows.first.. l_persons.rows.last loop
write_to_log(l_persons.rows(i));
end loop;
end;
What am I missing? How do I do this?
EDIT: Here's a script to get set up, if it helps follow the example:
create table my_orders
(
person varchar2(4000 byte),
choice varchar2(4000 byte)
);
insert into my_orders
(person, choice)
values
('Tom', 'Juice');
insert into my_orders
(person, choice)
values
('Jane', 'Apple');
insert into my_orders
(person, choice)
values
('Tom', 'Cake');
insert into my_orders
(person, choice)
values
('Jane', 'Chocolate');
insert into my_orders
(person, choice)
values
('Tom', 'Coffee');
commit;

Presumable, the new_multi() method initializes an empty collection.
One of the more esoteric features of Oracle collections is that using FIRST / LAST to iterate over empty collections doesn't work - you have to either check whether the collection is empty, or use 1 .. <collection>.COUNT instead:
declare
type t_number_nt is table of number;
l_numbers t_number_nt := t_number_nt();
begin
-- raises ORA-006502
/* for i in l_numbers.first .. l_numbers.last
loop
dbms_output.put_line(l_numbers(i));
end loop;
*/
-- doesn't raise an error
for i in 1 .. l_numbers.count loop
dbms_output.put_line(l_numbers(i));
end loop;
end;
UPDATE
For a more thorough explanation of techniques for iterating over PL/SQL collections, see this OTN article by Steven Feuerstein

It has to be like this:
declare
l_persons owa_text.multi_line;
begin
OWA_TEXT.new_multi (l_persons);
FOR i IN 1 .. l_persons.num_rows
loop
null;
end loop;
end;

Related

Oracle: Insert only unique records into custom record in compound trigger

I am having a compound trigger in the following format. I need to ensure that my custom declaration ints_rows takes only "unique rows". Meaning if the ints_rows already has a record similar to what is being inserted, it should ignore it. More like a SET data structure. How do I do it in oracle? (I am pretty new to oracle, hence I am not great at syntax) I think I have to either change the BULK COLLECT statement or the declaration of int_records but I might be wrong. Any help/hints are much appreciated.
This is my compound trigger code.
CREATE OR REPLACE
TRIGGER MY_COMPOUND_TRIGGER
FOR UPDATE OF some_random_column ON some_random_table
COMPOUND TRIGGER
TYPE int_records IS RECORD (
column_one another_table.column_one%TYPE,
column_two another_table.column_two%TYPE
);
TYPE row_list IS TABLE OF int_records INDEX BY simple_integer;
ints_rows row_list;
BEFORE STATEMENT IS
BEGIN
ints_rows.delete;
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
SELECT column_one, column_two BULK COLLECT INTO ints_rows
FROM some_table_x WHERE some_col_id=:OLD.some_col_id;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN 1 .. ints_rows.COUNT LOOP
-- DO SOMETHING
END LOOP;
auctions_rows.delete;
END AFTER STATEMENT;
END;
This statement should not be getting duplicates at all.
SELECT column_one, column_two BULK COLLECT INTO ints_rows
FROM some_table_x WHERE some_col_id=:OLD.some_col_id;
In the AFTER EACH ROW part you overwrite entire ints_rows. In principle it could be this one:
CREATE TABLE SOME_RANDOM_TABLE (some_random_column NUMBER, some_col_id NUMBER);
CREATE TABLE ANOTHER_TABLE (column_one NUMBER, column_two NUMBER);
CREATE TABLE SOME_TABLE_X (some_col_id NUMBER, column_one NUMBER, column_two NUMBER);
CREATE OR REPLACE TYPE int_records AS OBJECT (
column_one NUMBER,
column_two NUMBER,
MAP MEMBER FUNCTION getId RETURN VARCHAR2
);
CREATE OR REPLACE TYPE BODY int_records AS
MAP MEMBER FUNCTION getId RETURN VARCHAR2 IS
BEGIN
RETURN column_one ||','|| column_two;
END getId;
END;
/
CREATE OR REPLACE TYPE row_list IS TABLE OF int_records;
CREATE OR REPLACE TRIGGER MY_COMPOUND_TRIGGER
FOR UPDATE OF some_random_column ON SOME_RANDOM_TABLE
COMPOUND TRIGGER
ints_rows row_list;
BEFORE STATEMENT IS
BEGIN
ints_rows := row_list();
END BEFORE STATEMENT;
AFTER EACH ROW IS
int_row row_list;
BEGIN
SELECT int_records(column_one, column_two)
BULK COLLECT INTO int_row
FROM SOME_TABLE_X x
WHERE x.some_col_id = :OLD.some_col_id;
ints_rows := ints_rows MULTISET UNION DISTINCT int_row;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN 1 .. ints_rows.COUNT LOOP
NULL;
END LOOP;
END AFTER STATEMENT;
END;
Please test and let us know if it does not work. You may make the records distinct manually with a loop or you need to implement a MAP MEMBER FUNCTION for the RECORD.

Get value from from a json_array in oracle

i need the values of a json_array. I tried this:
DECLARE
l_stuff json_array_t;
BEGIN
l_stuff := json_array_t ('["Stirfry", "Yogurt", "Apple"] ');
FOR indx IN 0 .. l_stuff.get_size - 1
LOOP
INSERT INTO t_taböe (name, type)
VALUES(l_stuff.get(i), 'TEXT');
END LOOP;
END;
You are passing the position as i instead of indx; but you need a string so use get_string(indx) as #Sayan said.
But if you try to use that directly in an insert you'll get "ORA-40573: Invalid use of PL/SQL JSON object type" because of a still-outstanding (as far as I know) bug.
To work around that you can assign the string to a variable first:
l_name := l_stuff.get_string(indx);
INSERT INTO t_taböe (name, type)
VALUES(l_name, 'TEXT');
db<>fiddle
You do not need PL/SQL and can do it in a single SQL statement:
INSERT INTO t_taböe (name, type)
SELECT value,
'TEXT'
FROM JSON_TABLE(
'["Stirfry","Yogurt","Apple"]',
'$[*]'
COLUMNS (
value VARCHAR2(50) PATH '$'
)
);
db<>fiddle here
First convert the JSON array into an ordinary PL/SQL array, then use a bulk insert.
Here is a reproducible example:
create table tab (name varchar2 (8), type varchar2 (8))
/
declare
type namelist is table of varchar2(8) index by pls_integer;
names namelist;
arr json_array_t := json_array_t ('["Stirfry", "Yogurt", "Apple"]');
begin
for idx in 1..arr.get_size loop
names(idx) := arr.get_string(idx-1);
end loop;
forall idx in indices of names
insert into tab (name, type) values (names(idx), 'TEXT');
end;
/
The query and outcomes:
select * from tab
/
NAME TYPE
-------- --------
Stirfry TEXT
Yogurt TEXT
Apple TEXT
Just use get_string:
DECLARE
l_stuff json_array_t;
BEGIN
l_stuff := json_array_t ('["Stirfry", "Yogurt", "Apple"] ');
FOR indx IN 0 .. l_stuff.get_size - 1
LOOP
--INSERT INTO t_taböe (name, type)
-- VALUES(l_stuff.get_string(indx), 'TEXT');
dbms_output.put_line(l_stuff.get_string(indx));
END LOOP;
END;

PLSQL: IF EXISTS in stored procedure while using loop

I am new to PLSQL. I am trying to create a procedure which iterates through an array.
My requirement is if one of the value is not found in table, it should add into FAILARRAY, otherwise it should add into PASSARRAY.
I was getting no data found exception even if it is handled, it goes out of the loop and next value in the loop is not getting iterated again.
Is there any way we can use if exists command here. Please help.
CREATE OR REPLACE PROCEDURE SCHEMA.PR_VALIDATE
(
FILEARRAY IN STRARRAY,
PASSARRAY OUT STRARRAY,
FAILARRAY OUT STRARRAY,
)
IS
--DECLARE
fileName VARCHAR2 (50);
fileId NUMBER;
BEGIN
for i in 1 .. FILEARRAY.count
loop
fileName := FILEARRAY(i);
DBMS_OUTPUT.put_line (FILEARRAY (i));
SELECT FILEID into fileId FROM TABLE_NAME WHERE FILENAME=fileName;
end loop
END;
I suspect you haven't realised that you can have a PL/SQL BEGIN ... END block, including an exception handler, within a loop. In fact, anywhere you can have PL/SQL statements you can have a block.
You mention an exception handler, although your code doesn't contain one. As you say your code goes 'out of the loop', I can only assume it's, well, outside of the for loop. But you can easily add a block, with an exception handler, inside the for loop, for example:
BEGIN
for i in 1 .. FILEARRAY.count
loop
fileName := FILEARRAY(i);
DBMS_OUTPUT.put_line (FILEARRAY (i));
-- Inner block starts at the line below:
BEGIN
SELECT FILEID into fileId FROM TABLE_NAME WHERE FILENAME=fileName;
-- TODO add to PASSARRAY
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- TODO add to FAILARRAY
END;
end loop
END;
This way, if there are 8 values in FILEARRAY and no data is found in the table for the third value, the NO_DATA_FOUND exception gets caught without exiting the loop and the loop then progresses to the fourth value in FILEARRAY.
You are handling the exception but you need to avoid the exception. Try:
SELECT NVL(FILEID, "<Put Something here or leave it empty") FROM TABLE_NAME WHERE FILENAME=fileName;
That way if it finds a null value in the select it will just pull "" instead. Then you can check to see if your SELECT returns "" and if so populate your FAILARRAY, otherwise populate PASSARRAY.
CREATE OR REPLACE PROCEDURE SCHEMA.PR_VALIDATE(
FILEARRAY IN STRARRAY,
PASSARRAY OUT STRARRAY,
FAILARRAY OUT STRARRAY )
IS
fileName VARCHAR2 (50);
l_n_count NUMBER;
l_n_file_id NUMBER;
BEGIN
FOR i IN 1 .. FILEARRAY.count
LOOP
fileName := FILEARRAY(i);
DBMS_OUTPUT.put_line (FILEARRAY(i));
SELECT COUNT(FILEID) INTO l_n_count FROM TABLE_NAME WHERE FILENAME=fileName;
IF l_n_count =0 THEN
failarray(i):='No Value Found';
elsif l_n_count=1 THEN
SELECT FILEID INTO l_n_file_id FROM TABLE_NAME WHERE FILENAME=fileName;
Passarray(i):=l_n_file_id;
END IF;
END LOOP;
END;
/

Nested table loops to format data, PL/SQL

I am trying to format data returned from a cursor to JSON by looping through the records and columns without having to explicitly call on each column name. From what I've researched this vary well may not be a simple task or at least as simple as I'm trying to make it. I'm wondering if anyone else has tried a similar approach and if they had any luck.
declare
type type_cur_tab is table of employees%rowtype
index by PLS_integer;
type type_col_tab is table of varchar2(1000)
index by binary_integer;
tbl_rec type_cur_tab;
tbl_col type_col_tab;
begin
select * BULK COLLECT INTO tbl_rec
from employees;
select column_name BULK COLLECT INTO tbl_col
from all_tab_columns
where UPPER(table_name) = 'EMPLOYEES';
for i IN 1..tbl_rec.COUNT Loop
for j IN 1..tbl_col.count Loop
dbms_output.put_line(tbl_rec(i).tbl_col(j));
end loop;
end loop;
end;
It throws an error saying 'tbl_col' must be declared. I'm sure this is bc it's looking for 'tbl_col' listed inside 'tbl_rec'. Any help is greatly appreciated.
NOTE: I'm aware of the built in JSON conversion but I haven't been able to get it to as fast as I'd like so I'm trying to loop through and add the appropriate formatting along the way.
It is impossible to specify field of tbl_rec(i) in this manner.
Try this:
declare
v_cur sys_refcursor;
col_cnt number;
desc_t dbms_sql.desc_tab;
c number;
vVarchar varchar2(32000);
vNumber number;
vDate date;
v_result clob:='';
rn number:=0;
begin
--Any sql query or pass v_cur as input parameter in function on procedure
open v_cur for
select * from dual;
--------
c:=dbms_sql.to_cursor_number(v_cur);
dbms_sql.describe_columns(c => c, col_cnt => col_cnt, desc_t => desc_t);
for i in 1 .. col_cnt
loop
case desc_t(i).col_type
when dbms_types.TYPECODE_DATE then
dbms_sql.define_column(c, i ,vDate);
when dbms_types.TYPECODE_NUMBER then
dbms_sql.define_column(c, i ,vNumber);
else
dbms_sql.define_column(c, i ,vVarchar,32000);
end case;
end loop;
v_result:='{"rows":{ "row": [';
while (dbms_sql.fetch_rows(c)>0)
loop
if rn > 1 then v_result:=v_result||','; end if;
v_result:=v_result||'{';
for i in 1 .. col_cnt
loop
if (i>1) then v_result:=v_result||','; end if;
case desc_t(i).col_type
--Date
when dbms_types.typecode_date then
dbms_sql.column_value(c,i,vDate);
v_result:=v_result||' "'||desc_t(i).col_name||'" :"'||to_char(vDate,'dd.mm.yyyy hh24:mi')||'"';
--Number
when dbms_types.typecode_number then
dbms_sql.column_value(c,i,vNumber);
v_result:=v_result||' "'||desc_t(i).col_name||'" :"'||to_char(vNumber)||'"';
--Varchar - default
else
dbms_sql.column_value(c,i,vVarchar);
v_result:=v_result||' "'||desc_t(i).col_name||'" :"'||vVarchar||'"';
end case;
end loop;
v_result:=v_result||'}';
end loop;
v_result:=v_result||']}}';
dbms_output.put_line (v_result);
end;
Also you can generate XML from ref cursor with DBMS_XMLGEN package and then translate xml into json with xslt transformation.

PL/SQL cursor for loop and record not working

I have the following problem. I'm trying to check a number (bsn), if it's in the database or not. If it's not in the database it should give me an error, however now I'm getting always an error even if the number exists in the database. It worked fine with only one number in the database, but with more... That's the problem. Oh and I'm working with APEX, so I use this as a process.
create or replace PROCEDURE CONTROLE_BSN IS
CURSOR c_klanten
IS
SELECT bsn
FROM klant;
v_bsn VARCHAR2(10) := V('P7_BSN');
e_geen_bsn EXCEPTION;
BEGIN
FOR r_record IN c_klanten
LOOP
IF r_record.bsn != v_bsn THEN
RAISE e_geen_bsn;
END IF;
END LOOP;
EXCEPTION
WHEN e_geen_bsn THEN
raise_application_error(-20001, 'This bsn-number does not exists.');
END CONTROLE_BSN;
Your logic is flowed. As soon as you have two different bsn in your table, your test will be true for at least one of them:
FOR r_record IN c_klanten
LOOP
IF r_record.bsn != v_bsn THEN --< when N different records,
-- this is true for at least N-1 of them
RAISE e_geen_bsn;
END IF;
END LOOP;
Maybe you should go for something a little bit simpler than that. Why not write your cursor like this instead:
CURSOR c_klanten
IS
SELECT count(*) n
FROM klant
WHERE nbc = v_bsn;
That way, you will easily get the number of matching bsn. Either 0, 1 or more. And then perform the appropriate action.
Perhaps the following would help:
create or replace PROCEDURE CONTROLE_BSN IS
CURSOR c_klanten(p_bsn) IS
SELECT count(*) as bsn_count
FROM klant
where bsn = p_bsn;
v_bsn VARCHAR2(10) := V('P7_BSN');
e_geen_bsn EXCEPTION;
BEGIN
FOR r_record IN c_klanten(v_bsn)
LOOP
IF r_record.bsn_count = 0 THEN
RAISE e_geen_bsn;
END IF;
END LOOP;
EXCEPTION
WHEN e_geen_bsn THEN
raise_application_error(-20001, 'This bsn-number does not exists.');
END CONTROLE_BSN;
Best of luck.

Resources