Multi cursor in oracle function - oracle

I want to create an oracle function that get the user_id as param and return varchar2 that contain the e-mail text for the books that the user take from the library and didn't return them back, my email text that I want is:
"hello "
You should return this books:
1 <BOOK_NAME> that taken at
2. ....
This is what I wrote so far
create or replace function get_un_recived_books(param_client_id in number) return varchar2 is
Result varchar2(2000);
cursor cur_book is
select * from hashala natural join client natural join all_books natural join book
where
recived =0
and recived_date is null
and clientid= param_client_id
and taken_date < add_months(trunc(sysdate, 'month'), -3);
begin
FOR b IN cur_book LOOP
Result:= 'book ' || b.book_name;
END LOOP;
return(Result);
end;
I have 3 problems
how can i return all the books and not the last one (like += in c)
for the client name do I need to add another cursor, can I do it?
how to pass the value from the original query
select get_un_recived_books(!!!ADD HERE THE CLIENID!!!), clientid from hashala natural join client natural join all_books natural join
book
where
recived =0
and recived_date is null
and taken_date < add_months(trunc(sysdate, 'month'), -3);

I don't have your tables so I'll use Scott's sample schema to illustrate how you might do that. Read comments within code.
Function:
SQL> create or replace function f_test (par_deptno in number)
2 return varchar2
3 is
4 l_dname dept.dname%type;
5 retval varchar2(4000);
6 begin
7 -- department name (in your case, client name)
8 select dname
9 into l_dname
10 from dept
11 where deptno = par_deptno;
12
13 -- loop through employees in PAR_DEPTNO (in your case,
14 -- books client borrowed)
15 for cur_r in (select ename, hiredate
16 from emp
17 where deptno = par_deptno
18 )
19 loop
20 -- this is what you're missing: "retval := retval || ..."
21 retval := retval || 'Name: ' || cur_r.ename ||
22 ', borrowed on ' || to_char(cur_r.hiredate, 'dd.mm.yyyy') || chr(10);
23 end loop;
24
25 retval := 'Hello, ' || l_dname ||chr(10) ||
26 'you borrowed the following books and didn''t return them yet.' || chr(10) ||
27 retval;
28 return retval;
29 end;
30 /
Function created.
Testing:
SQL> select f_test(10) from dual;
F_TEST(10)
--------------------------------------------------------------------------------
Hello, ACCOUNTING
you borrowed the following books and didn't return them yet.
Name: CLARK, borrowed on 09.06.1981
Name: KING, borrowed on 17.11.1981
Name: MILLER, borrowed on 23.01.1982
SQL>
If you - as you said - want to pass the ID "dynamically", just include it into the function. Something like this (I'm retrieving data for departments 10 and 30, for everyone who works as a clerk):
SQL> select f_test(d.deptno)
2 from dept d join emp e on e.deptno = d.deptno
3 where d.deptno in (10, 30)
4 and e.job = 'CLERK';
F_TEST(D.DEPTNO)
------------------------------------------------------------------------
Hello, SALES
you borrowed the following books and didn't return them yet.
Name: ALLEN, borrowed on 20.02.1981
Name: WARD, borrowed on 22.02.1981
Name: MARTIN, borrowed on 28.09.1981
Name: BLAKE, borrowed on 01.05.1981
Name: TURNER, borrowed on 08.09.1981
Name: JAMES, borrowed on 03.12.1981
Hello, ACCOUNTING
you borrowed the following books and didn't return them yet.
Name: CLARK, borrowed on 09.06.1981
Name: KING, borrowed on 17.11.1981
Name: MILLER, borrowed on 23.01.1982
SQL>

You may use LISTAGG function to achieve this. This task can be solved in pure SQL via view, which will not produce child cursors compared to function invocation. You just need to filter it if you wish to select individual clients.
However you can also put the same text in a function if you wish, which also doesn't require any loops. Because from Oracle 12C and above you may extend varchar2 up to 32767 bytes with MAX_STRING_SIZE = EXTENDED parameter, which removes the boundary of varchar2 in SQL compared to PL/SQL.
Both the examples are below:
create table client
as
select
level as id
, 'Client ' || level as name
from dual
connect by level < 10
create table book
as
select
level as id
, 'Book ' || level as name
from dual
connect by level < 100
create table borrowed_book
as
select
trunc(level / 4) + 1 as client_id
, level as book_id
, add_months(sysdate, -mod(level, 11)) as dt
, case mod(level, 4) when 1 then 1 else 0 end as received
from dual
connect by level < 60
create view v_reminder
as
select
bb.client_id,
c.name as client_name,
'Hello, ' || max(c.name) || '!'
|| chr(10) || chr(10)
|| 'You should return this books:' || chr(10)
|| listagg(b.name || ' borrowed on ' || to_char(bb.dt, 'dd-Mon-yyyy'), chr(10))
within group(order by bb.dt desc, bb.book_id) as rem
from borrowed_book bb
join client c
on bb.client_id = c.id
join book b
on bb.book_id = b.id
where bb.received = 0
and bb.dt < add_months(sysdate, -3)
group by bb.client_id, c.name
create or replace function f_get_reminder(p_client_id in int, p_dt_offset in int default 3)
return varchar2
as
res varchar2(32000);
begin
select
'Hello, ' || max(c.name) || '!'
|| chr(10) || chr(10)
|| 'You should return this books:' || chr(10)
|| listagg(b.name || ' borrowed on ' || to_char(bb.dt, 'dd-Mon-yyyy'), chr(10))
within group(order by bb.dt desc, bb.book_id)
into res
from borrowed_book bb
join client c
on bb.client_id = c.id
join book b
on bb.book_id = b.id
where bb.received = 0
and bb.dt < add_months(sysdate, -p_dt_offset)
and bb.client_id = p_client_id
/*Added this line to get NULL as output, because aggregate function
without group by always returns a row even for empty input dataset*/
group by bb.client_id
;
return res;
end;
/
select
c.id
, c.name
, f_get_reminder(c.id) as rem
from client c
ID
NAME
REM
1
Client 1
null
2
Client 2
Hello, Client 2!You should return this books:Book 4 borrowed on 27-Feb-2021Book 6 borrowed on 27-Dec-2020Book 7 borrowed on 27-Nov-2020
3
Client 3
Hello, Client 3!You should return this books:Book 8 borrowed on 27-Oct-2020Book 10 borrowed on 27-Aug-2020
4
Client 4
Hello, Client 4!You should return this books:Book 15 borrowed on 27-Feb-2021
5
Client 5
Hello, Client 5!You should return this books:Book 16 borrowed on 27-Jan-2021Book 18 borrowed on 27-Nov-2020Book 19 borrowed on 27-Oct-2020
6
Client 6
Hello, Client 6!You should return this books:Book 20 borrowed on 27-Sep-2020
7
Client 7
Hello, Client 7!You should return this books:Book 26 borrowed on 27-Feb-2021Book 27 borrowed on 27-Jan-2021
8
Client 8
Hello, Client 8!You should return this books:Book 28 borrowed on 27-Dec-2020Book 30 borrowed on 27-Oct-2020Book 31 borrowed on 27-Sep-2020
9
Client 9
Hello, Client 9!You should return this books:Book 32 borrowed on 27-Aug-2020
db<>fiddle here

Related

Is this possible to apply a function to every fields of table (groupy by type)

I would like to replace every string are null by 'n' and every number by 0.
Is there a way to do that?
With a polymorphic table function I can select all the columns of a certain type but I can't modify the value of the columns.
Yes, it is possible with PTF also. You may modify columns in case you've set pass_through to false for the column in the describe method (to drop it) and copy it into new_columns parameter of the describe.
Below is the code:
create package pkg_nvl as
/*Package to implement PTF*/
function describe(
tab in out dbms_tf.table_t
) return dbms_tf.describe_t
;
procedure fetch_rows;
end pkg_nvl;
/
create package body pkg_nvl as
function describe(
tab in out dbms_tf.table_t
) return dbms_tf.describe_t
as
modif_cols dbms_tf.columns_new_t;
new_col_cnt pls_integer := 0;
begin
/*Mark input columns as used and as modifiable for subsequent row processing*/
for i in 1..tab.column.count loop
if tab.column(i).description.type in (
dbms_tf.type_number,
dbms_tf.type_varchar2
) then
/*Modifiable*/
tab.column(i).pass_through := FALSE;
/*Used in the PTF context*/
tab.column(i).for_read := TRUE;
/* Propagate column to the modified*/
modif_cols(new_col_cnt) := tab.column(i).description;
new_col_cnt := new_col_cnt + 1;
end if;
end loop;
/*Return the list of modified cols*/
return dbms_tf.describe_t(
new_columns => modif_cols
);
end;
procedure fetch_rows
/*Process rowset and replace nulls*/
as
rowset dbms_tf.row_set_t;
num_rows pls_integer;
in_col_vc2 dbms_tf.tab_varchar2_t;
in_col_num dbms_tf.tab_number_t;
new_col_vc2 dbms_tf.tab_varchar2_t;
new_col_num dbms_tf.tab_number_t;
begin
/*Get rows*/
dbms_tf.get_row_set(
rowset => rowset,
row_count => num_rows
);
for col_num in 1..rowset.count() loop
/*Loop through the columns*/
for rn in 1..num_rows loop
/*Calculate new values in the same row*/
/*Get column by index and nvl the value for return column*/
if rowset(col_num).description.type = dbms_tf.type_number then
dbms_tf.get_col(
columnid => col_num,
collection => in_col_num
);
new_col_num(rn) := nvl(in_col_num(rn), 0);
elsif rowset(col_num).description.type = dbms_tf.type_varchar2 then
dbms_tf.get_col(
columnid => col_num,
collection => in_col_vc2
);
new_col_vc2(rn) := nvl(in_col_vc2(rn), 'n');
end if;
end loop;
/*Put the modified column to the result*/
if rowset(col_num).description.type = dbms_tf.type_number then
dbms_tf.put_col(
columnid => col_num,
collection => new_col_num
);
elsif rowset(col_num).description.type = dbms_tf.type_varchar2 then
dbms_tf.put_col(
columnid => col_num,
collection => new_col_vc2
);
end if;
end loop;
end;
end pkg_nvl;
/
create function f_replace_nulls(tab in table)
/*Function to replace nulls using PTF*/
return table pipelined
row polymorphic using pkg_nvl;
/
with a as (
select
1 as id, 'q' as val_vc2, 1 as val_num
from dual
union all
select
2 as id, '' as val_vc2, null as val_num
from dual
union all
select
3 as id, ' ' as val_vc2, 0 as val_num
from dual
)
select
id
, a.val_num
, a.val_vc2
, n.val_num as val_num_repl
, n.val_vc2 as val_vc2_repl
from a
join f_replace_nulls(a) n
using(id)
ID | VAL_NUM | VAL_VC2 | VAL_NUM_REPL | VAL_VC2_REPL
-: | ------: | :------ | -----------: | :-----------
3 | 0 | | 0 |
2 | null | null | 0 | n
1 | 1 | q | 1 | q
db<>fiddle here
Use COALESCE or NVL and list the columns you want to apply them to:
SELECT COALESCE(col1, 'n') AS col1,
COALESCE(col2, 0) AS col2,
COALESCE(col3, 'n') AS col3
FROM table_name;
The way I understood the question, you actually want to modify table's contents. If that's so, you'll need dynamic SQL.
Here's an example; sample data first, with some numeric and character columns having NULL values:
SQL> desc test
Name Null? Type
----------------------------------------- -------- ----------------------------
ID NUMBER
NAME VARCHAR2(20)
SALARY NUMBER
SQL> select * from test order by id;
ID NAME SALARY
---------- ---------- ----------
1 Little 100
2 200
3 Foot 0
4 0
Procedure reads USER_TAB_COLUMNS, checking desired data types (you can add some more, if you want), composes the update statement and executes it:
SQL> declare
2 l_str varchar2(1000);
3 begin
4 for cur_r in (select column_name, data_type
5 from user_tab_columns
6 where table_name = 'TEST'
7 and data_type in ('CHAR', 'VARCHAR2')
8 )
9 loop
10 l_str := 'update test set ' ||
11 cur_r.column_name || ' = nvl(' || cur_r.column_name ||', ''n'')';
12 execute immediate l_str;
13 end loop;
14
15 --
16
17 for cur_r in (select column_name, data_type
18 from user_tab_columns
19 where table_name = 'TEST'
20 and data_type in ('NUMBER')
21 )
22 loop
23 l_str := 'update test set ' ||
24 cur_r.column_name || ' = nvl(' || cur_r.column_name ||', 0)';
25 execute immediate l_str;
26 end loop;
27 end;
28 /
PL/SQL procedure successfully completed.
Result:
SQL> select * from test order by id;
ID NAME SALARY
---------- ---------- ----------
1 Little 100
2 n 200
3 Foot 0
4 n 0
SQL>
You can create a table macro to intercept the columns and replace them with nvl if they're a number or varchar2:
create or replace function replace_nulls (
tab dbms_tf.table_t
)
return clob sql_macro as
stmt clob := 'select ';
begin
for i in 1..tab.column.count loop
if tab.column(i).description.type = dbms_tf.type_number
then
stmt := stmt || ' nvl ( ' ||
tab.column(i).description.name || ', 0 ) ' ||
tab.column(i).description.name || ',';
elsif tab.column(i).description.type = dbms_tf.type_varchar2
then
stmt := stmt || ' nvl ( ' ||
tab.column(i).description.name || ', ''n'' ) ' ||
tab.column(i).description.name || ',';
else
stmt := stmt || tab.column(i).description.name || ',';
end if;
end loop;
stmt := rtrim ( stmt, ',' ) || ' from tab';
return stmt;
end;
/
with a (
aa1,aa2,aa3
) as (
select 1, '2hhhh', sysdate from dual
union all
select null, null, null from dual
)
select * from replace_nulls(a);
AA1 AA2 AA3
---------- ----- -----------------
1 2hhhh 27-JUN-2022 13:17
0 n <null>

Cursor loop - COUNT from two tables - PLSQL Oracle

Need some advice here. I have two tables: 1) Car owners; 2) Cars. With cursor loop I need to count how many cars owns each person. I don't understand how do you write the select code when two tables are involved.
CREATE TABLE car_owner(
pnr VARCHAR2(13) PRIMARY KEY,
fname VARCHAR2(20);
CREATE TABLE car(
regnr VARCHAR2(6) PRIMARY KEY,
pnr REFERENCES car_owner(pnr);
INSERT INTO car_owner VALUES('19490321','anna');
INSERT INTO car_owner VALUES('19540201','tomas');
INSERT INTO car_owner VALUES('19650823','roger');
INSERT INTO car VALUES('ase456','19490321');
INSERT INTO car VALUES('ptg889','19490321');
INSERT INTO car VALUES('bon666','19650823');
declare
cursor c_cars is select pnr, fnamn, count(amount_of_cars(pnr))
from car_owner, car;
v_pnr car_owner.pnr%type;
v_fnamn car_owner.fnamn%type;
begin
if not c_cars %isopen then
open c_cars;
end if;
loop
fetch c_cars
into v_pnr, v_fnamn;
exit when c_cars %notfound;
dbms_output.put_line(v_pnr || ', ' || v_fnamn || ', ' ||
', owns: ' || amount_of_cars || ' cars');
end loop;
close c_cars;
end;
So the answer should look like this:
19490321, anna, owns: 2 cars
19540201, tomas, owns: 1 car
19650823, roger, owns: 0 cars
Join those tables. As you're aggregating something, all non-aggregated columns should be part of the group by clause.
Also, consider using a cursor FOR loop as it is way simpler (Oracle does all the dirty job for you - you don't have to declare variable(s), open the cursor, worry about exiting the loop, close the cursor).
SQL> DECLARE
2 CURSOR c_cars IS
3 SELECT o.pnr, o.fname, COUNT (*) cnt
4 FROM car_owner o JOIN car c ON c.pnr = o.pnr
5 GROUP BY o.pnr, o.fname;
6 BEGIN
7 FOR cur_r IN ( SELECT o.pnr, o.fname, COUNT (*) cnt
8 FROM car_owner o JOIN car c ON c.pnr = o.pnr
9 GROUP BY o.pnr, o.fname)
10 LOOP
11 DBMS_OUTPUT.put_line (
12 cur_r.pnr
13 || ', '
14 || cur_r.fname
15 || ', '
16 || ', owns: '
17 || cur_r.cnt
18 || ' car(s)');
19 END LOOP;
20 END;
21 /
19650823, roger, , owns: 1 car(s)
19490321, anna, , owns: 2 car(s)
PL/SQL procedure successfully completed.
SQL>

Getting rid of comma at the end of PL/SQL Function [Oracle PL/SQL]

Am working on a stored PL/SQL function that find the names of dependents of an employee by the given employee's number. So far I am able to get the desired output but seem to have an issue with the commas during the output. Any help is appreciated in regards to how I can remove the last bracket during the output.
[Current Code]
SQL> create or replace function FINDDEPENDENTS(empid in Employee.E#%TYPE)
2 RETURN VARCHAR IS
3 EID Employee.E#%TYPE;
4 Ename Employee.Name%TYPE;
5
6 DEPNAME Dependent.DName%TYPE;
7 finalRow VARCHAR(2000);
8 CURSOR q IS
9 --
10 select DName from Dependent WHERE E#=empid;
11 begin
12 select E#, Name INTO EID, Ename FROM Employee WHERE E#= empid;
13 finalRow:= EID || ' ' || Ename || ': ';
14 open q;
15 loop
16 fetch q into depname;
17 if q%notfound then exit;
18 end if;
19 finalRow:= finalRow || DEPNAME || ', ';
20 end loop;
21 close q;
22 return(finalRow);
23 end FINDDEPENDENTS;
24 /
Function created.
SQL> show errors
No errors.
SQL> SELECT FINDDEPENDENTS(E#) FROM Employee;
FINDDEPENDENTS(E#)
----------------------------------------------------------------------------------------------------
00100 Albert: Bolt, Edee, Judy,
00101 Peter:
00103 Ami:
00105 Robert:
00107 Wendy:
00109 Michael:
00110 Alvin:
00120 Alice: Blues, Edee, Kadi,
00125 Angela:
00136 Aban:
00150 Bob:
00187 Eadger:
00200 Carl: Eva,
00250 Douglass:
14 rows selected.
How can I get rid of the last comma so that I can get an output like:
00100 Albert: Bolt, Edee, Judy
00110 Alvin:
00120 Alice: Blures, Edee, Kadi
TRIM it:
return(rtrim(finalRow, ','));
You can use RTRIM as mentioned in one of the answers, But you can also use the following logic which will prevent the creation of such data.
Use
finalRow:= finalRow || CASE WHEN SUBSTR(finalRow ,-2) <> ': ' THEN ', ' END || DEPNAME;
Instead of
finalRow:= finalRow || DEPNAME || ', ';
Use TRIM function:
create or replace function foo (s varchar2) return varchar2 is
begin
return trim (trailing ',' from s);
end;
/
var ret varchar2
exec :ret := foo ('foo,')
RET
---
foo

E-mail result of 'select' statement having multiple rows as result set in ORACLE

I am new to ORACLE PL/SQL world. I am trying to figure out a way to calculate something as below.
Let's say you have a MASTER_TABLE as below :
SELECT * FROM MASTER_TABLE;
+----------+----------+------------------+-----------------------+
| SCHEMA | TABLE_NM | REQUIRED_COLUMNS | TABLE_FILTER |
+----------+----------+------------------+-----------------------+
| USER_SCH | A | A1,A2,A3 | EXAM_DT > SYSDATE - 1 |
| USER_SCH | B | B1,B2 | TRUNC(SYSDATE) |
+----------+----------+------------------+-----------------------+
I would like to generate SELECT query from above table such as below:
SELECT 'SELECT SCHEMA || '.' || TABLE_NM ||' WHERE '|| TABLE_FILTER FROM MASTER_TABLE;
Obviously, the result of above query would generate multiple select statements.
Now, I want to execute all such SELECT statements and send out the resultset via e-mail.
The tricky part is, the columns mentioned in the MASTER_TABLE varies (i.e. For table 'A' there can be 3 REQUIRED_COLUMNS to be selected, For table 'B' there can be 2 REQUIRED_COLUMNS to be selected - As shown in the MASTER_TABLE)
I have the e-mail utility ready which basically takes an argument as your_message and sends it out via e-mail.
Here is what I have tried :
Created CURSOR to generate such select statements.
Tried inserting the resultset (LIST OF SELECT QUERIES) to another temp table by concatenating the columns.
(i.e.
SELECT
'SELECT '
|| replace(required_columns, ',', '||'',''||')
|| ' AS MSG_BDY'
|| ' FROM '
|| schema
|| '.'
|| table_nm
|| ' WHERE '
|| table_filter
as my_select_stmt
FROM
master_table;
I am stuck after this.
Can you please help me out ? or is there any approach to achieve this ?.
Note : Tables mentioned in MASTER_TABLE can have 1 or more rows.
I don't have your tables so I used Scott's.
Master table:
SQL> select * From master_table;
SCHEM TABL REQUIRED_COLUMN TABLE_FILTER
----- ---- --------------- ----------------------
scott emp ename, job, sal hiredate < sysdate - 1
scott dept dname, loc deptno = 20
SQL>
Procedure which simulates your mailing procedure; I'll just display those values.
SQL> CREATE OR REPLACE PROCEDURE p_mail (par_result IN SYS.odcivarchar2list)
2 AS
3 BEGIN
4 FOR i IN par_result.FIRST .. par_result.LAST
5 LOOP
6 DBMS_OUTPUT.put_line (par_result (i));
7 END LOOP;
8 END;
9 /
Procedure created.
SQL>
Procedure you actually need; as you composed the select statement(s), now you only have to run them. In order to do so, use dynamic SQL (e.g. execute immediate):
SQL> SET SERVEROUTPUT ON
SQL>
SQL> DECLARE
2 retval SYS.odcivarchar2list;
3 BEGIN
4 FOR cur_r
5 IN (SELECT 'SELECT '
6 || REPLACE (required_columns, ',', '||'',''||')
7 || ' AS MSG_BDY'
8 || ' FROM '
9 || schema
10 || '.'
11 || table_nm
12 || ' WHERE '
13 || table_filter
14 AS my_select_stmt
15 FROM master_table)
16 LOOP
17 EXECUTE IMMEDIATE cur_r.my_select_stmt BULK COLLECT INTO retval;
18
19 -- you'd call your mailing procedure here
20 p_mail (retval);
21 END LOOP;
22 END;
23 /
SMITH,CLERK,920
ALLEN,SALESMAN,1600
WARD,SALESMAN,1250
JONES,MANAGER,2975
MARTIN,SALESMAN,1250
BLAKE,MANAGER,2850
CLARK,MANAGER,2450
SCOTT,ANALYST,3000
KING,PRESIDENT,10000
TURNER,SALESMAN,1500
ADAMS,CLERK,1300
JAMES,CLERK,950
FORD,ANALYST,3000
MILLER,CLERK,1300
RESEARCH,DALLAS
PL/SQL procedure successfully completed.
SQL>
[EDIT: what if you wanted to display 'null' for missing values?]
Well, that's a new moment - probably not very simple. See if this helps.
In order to help myself, I modified master_table and added ID column to uniquely identify every row. It'll be used to split required columns' list to rows, apply NVL to them, apply CAST to columns (because NVL complains if datatypes don't match), aggregate them back using listagg. As this is quite a lot to do, I'm going to create a view and use it instead of the table itself.
SQL> CREATE OR REPLACE VIEW v_master_table
2 AS
3 SELECT id,
4 schema,
5 table_nm,
6 LISTAGG ('NVL(cast(' || col || ' as varchar2(20)), ''null'')', '||'',''||')
7 WITHIN GROUP (ORDER BY lvl)
8 required_columns,
9 table_filter
10 FROM (SELECT id,
11 schema,
12 table_nm,
13 table_filter,
14 COLUMN_VALUE lvl,
15 TRIM (REGEXP_SUBSTR (required_columns,
16 '[^,]+',
17 1,
18 COLUMN_VALUE))
19 col
20 FROM master_table
21 CROSS JOIN
22 TABLE (
23 CAST (
24 MULTISET (
25 SELECT LEVEL
26 FROM DUAL
27 CONNECT BY LEVEL <=
28 REGEXP_COUNT (required_columns,
29 ',')
30 + 1) AS SYS.odcinumberlist)))
31 GROUP BY id,
32 schema,
33 table_nm,
34 table_filter;
View created.
For example, it now looks like this:
SQL> select * from v_master_table where id = 2;
ID SCHEM TABL REQUIRED_COLUMNS TABLE_FILTER
--- ----- ---- ------------------------------------------------------------------------------------- ------------
2 scott dept NVL(cast(dname as varchar2(20)), 'null')||','||NVL(cast(loc as varchar2(20)), 'null') deptno = 20
SQL>
The mailing procedure remains the same, no change.
Anonymous PL/SQL block is slightly changed - I removed REPLACE you previously used as view does it now; also, source is the view, not the table.
SQL> alter session set nls_date_format = 'dd.mm.yyyy';
Session altered.
SQL> DECLARE
2 retval SYS.odcivarchar2list;
3 BEGIN
4 FOR cur_r
5 IN (SELECT 'SELECT '
6 || required_columns
7 || ' AS MSG_BDY'
8 || ' FROM '
9 || schema
10 || '.'
11 || table_nm
12 || ' WHERE '
13 || table_filter
14 AS my_select_stmt
15 FROM v_master_table)
16 LOOP
17 EXECUTE IMMEDIATE cur_r.my_select_stmt BULK COLLECT INTO retval;
18
19 -- you'd call your mailing procedure here
20 p_mail (retval);
21 END LOOP;
22 END;
23 /
CLARK,09.06.1981,null
KING,17.11.1981,null
MILLER,23.01.1982,null
RESEARCH,DALLAS
PL/SQL procedure successfully completed.
SQL>

pl/sql nested loop

How would I display all records in the database using the items table? My current query displays the information for item 894. I attempted using a loop, but no luck.
I have two tables, inventory and itemid. Where itemid has the item number and the description, and the inventory table has the items' information, such as size, color, price, and quantity on hand.
set serveroutput on
DECLARE
current_item number(8);
totalvalue number(8,2);
description varchar2(50);
item_id number(3);
CURSOR Inventory_Info IS
SELECT
itemsize
,color
,curr_price
,qoh
,curr_price*qoh as Total
FROM inventory
WHERE itemid=Current_item;
BEGIN
current_item:=894;
totalvAlue:=0;
SELECT
itemdesc, itemid
INTO description, item_id
FROM item
WHERE itemid=current_item;
DBMS_OUTPUT.PUT_LINE('Item ID: ' || TO_CHAR(item_id) || ' Item Description: ' || description);
OPEN Inventory_Info;
LOOP
Fetch Inventory_Info INTO Inventory_rocord;
EXIT WHEN Inventory_Info%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('Size: ' || Inventory_record.itemsize);
DBMS_OUTPUT.PUT_LINE('Color: ' || Inventory_record.color);
DBMS_OUTPUT.PUT_LINE('Price: ' || Inventory_record.curr_price);
DBMS_OUTPUT.PUT_LINE('QOH: ' || Inventory_record.qoh);
DBMS_OUTPUT.PUT_LINE('Value: ' || Inventory_record.total);
TotalValue:=TotalValue + Inventory_record.total;
End Loop;
DBMS_OUTPUT.PUT_LINE('TOTAL VALUE: ' || TotalValue);
Close Inventory_Info;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No inventory for Item No. '|| current_item);
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error Message: '|| SQLERRM);
END;
If we for a moment forget about the formatting this could be done much simpler with a cursor for loop.
set serveroutput ON
DECLARE
BEGIN
FOR item_rec IN (SELECT itemdesc, itemid
FROM item
) LOOP
DBMS_OUTPUT.PUT_LINE('Item ID: ' || TO_CHAR(item_rec.itemid)
|| ' Item Description: ' || item_rec.itemdesc);
FOR Inventory_record IN (SELECT itemsize
, color
, curr_price
, qoh
, curr_price*qoh AS Total
FROM inventory
WHERE itemid = item_rec.itemid
) LOOP
DBMS_OUTPUT.PUT_LINE('Size: ' || Inventory_record.itemsize);
DBMS_OUTPUT.PUT_LINE('Color: ' || Inventory_record.color);
DBMS_OUTPUT.PUT_LINE('Price: ' || Inventory_record.curr_price);
DBMS_OUTPUT.PUT_LINE('QOH: ' || Inventory_record.qoh);
DBMS_OUTPUT.PUT_LINE('Value: ' || Inventory_record.total);
TotalValue:= TotalValue + Inventory_record.total;
END LOOP;
END LOOP;
END;
You're using a separate SELECT..INTO to fetch the itemid, but you assign the itemid to a single value and don't change that.
Looking at your queries, you can move the itemid fetch into the cursor & join the 2 tables. You fetch the items into inventory_record, but I don't see the definition/declaration of that anywhere.
Here I declare a record type variable consisting of what you're fetching, open the cursor, fetch the details into that cursor. Since there's no explicit where condition, it will perform an inner join between item & inventory tables & fetch all rows matching the join condition.
set serveroutput ON
DECLARE
TYPE inventory_rec IS RECORD (
itemid item.itemid%TYPE,
itemdesc item.itemdesc%TYPE,
itemsize inventory.itemsize%TYPE,
color inventory.color%TYPE,
curr_price inventory.curr_price%TYPE,
qoh inventory.qoh %TYPE,
total inventory.curr_price%TYPE); -- record type to hold what cursor will be fetching
inventory_record INVENTORY_REC;
current_item NUMBER(8);
totalvalue NUMBER(8, 2);
description VARCHAR2(50);
item_id NUMBER(3);
CURSOR inventory_info IS
SELECT itemid,
itemdesc,
itemsize,
color,
curr_price,
qoh,
curr_price * qoh AS Total
FROM inventory
join item USING (itemid);
-- join item & inventory, so that it shows listings for all items present in inventory
BEGIN
OPEN inventory_info;
LOOP
FETCH inventory_info INTO inventory_record;
EXIT WHEN inventory_info%NOTFOUND;
dbms_output.Put_line('Current item: '
|| inventory_record.itemdesc);
dbms_output.Put_line('Size: '
|| inventory_record.itemsize);
dbms_output.Put_line('Color: '
|| inventory_record.color);
dbms_output.Put_line('Price: '
|| inventory_record.curr_price);
dbms_output.Put_line('QOH: '
|| inventory_record.qoh);
dbms_output.Put_line('Value: '
|| inventory_record.total);
totalvalue := totalvalue + inventory_record.total;
END LOOP;
dbms_output.Put_line('TOTAL VALUE: '
|| totalvalue);
CLOSE inventory_info;
EXCEPTION
WHEN no_data_found THEN
dbms_output.Put_line('No inventory for Item No. '
|| current_item);
WHEN OTHERS THEN
dbms_output.Put_line('Error Message: '
|| SQLERRM);
END;
Don't use nested look-ups, use joins. Databases are really good at joins, and the performance of set operations is a lot a better than row-by-row processing.
Also, you don't need to declare cursors and variables in most situations. Use a cursor loop and let Oracle do the heavy lifting for you.
set serveroutput on
DECLARE
totalvalue number(8,2);
BEGIN
totalvAlue:=0;
FOR inv_itm_rec IN (
SELECT itm.itemid
, itm.itemdesc
, inv.itemsize
, inv.color
,inv.curr_price
,inv.qoh
,inv.curr_price*inv.qoh as Total
FROM inventory inv
JOIN item itm
ON itm.itemid=inv.itemid
)
LOOP
DBMS_OUTPUT.PUT_LINE('ItemId: ' || inv_itm_rec.itemid);
DBMS_OUTPUT.PUT_LINE('Description: ' || inv_itm_rec.itemdesc);
DBMS_OUTPUT.PUT_LINE('Size: ' || inv_itm_rec.itemsize);
DBMS_OUTPUT.PUT_LINE('Color: ' || inv_itm_rec.color);
DBMS_OUTPUT.PUT_LINE('Price: ' || inv_itm_rec.curr_price);
DBMS_OUTPUT.PUT_LINE('QOH: ' || inv_itm_rec.qoh);
DBMS_OUTPUT.PUT_LINE('Value: ' || inv_itm_rec.total);
TotalValue:=TotalValue + inv_itm_rec.total;
END LOOP;
DBMS_OUTPUT.PUT_LINE('TOTAL VALUE: ' || TotalValue);
END;
Note, this solution assumes that evry ITEM does have a matching INVENTORY record. It would be a rum old warehouse application if the data model allowed anything else to be the case.
If it's just a report(it seems to be). Consider using sql*plus report formatting commands to display the output of a query in a way you want it to be displayed.
Here is an example:
SQL> create table Items(
2 i_num number,
3 i_descr varchar2(101),
4 i_size varchar2(3),
5 i_price number,
6 i_qoh number
7 );
Table created
SQL> create table Inventories(
2 id number,
3 i_num number
4 );
Table created
SQL> insert into items(i_num,i_size,i_price,i_qoh,i_descr)
2 select 1, 'S', 123, 25, 'Item_1' from dual union all
3 select 2, 'L', 424, 12, 'Item_1' from dual union all
4 select 4, 'S', 45, 54, 'Item_4' from dual union all
5 select 5, 'S', 78, 54, 'Item_4' from dual union all
6 select 6, 'S', 123, 22, 'Item_5' from dual union all
7 select 7, 'S', 127, 65, 'Item_5' from dual
8 ;
6 rows inserted
SQL> commit;
Commit complete
SQL> insert into inventories
2 select i_num, i_num
3 from items;
6 rows inserted
SQL> commit;
Commit complete
And now our report:
SQL> column i_descr format a10
SQL> column i_size format a3
SQL> column i_price format 99999
SQL> column i_qoh format 99999
SQL> column value format 99999
SQL> break on i_descr skip 2
SQL> compute sum label 'Total: ' of value on i_descr;
SQL> select itm.i_descr
2 , itm.i_size
3 , itm.i_price
4 , itm.i_qoh
5 , sum(i_price*i_qoh) Value
6 from inventories inv
7 join items itm on (inv.i_num = itm.i_num)
8 group by itm.i_num
9 , itm.i_descr
10 , itm.i_size
11 , itm.i_price
12 , itm.i_qoh
13 order by i_descr, i_price;
I_DESCR I_S I_PRICE I_QOH VALUE
---------- --- ---------- ------ ------
Item_1 S 123 25 3075
L 424 12 5088
********** ------
Total: 8163
Item_4 L 45 54 2430
S 78 54 4212
********** ------
Total: 6642
Item_5 S 123 22 2706
L 127 65 8255
********** ------
Total: 10961
6 rows selected.

Resources