Oracle: How to wrap a Cursor inside A Function? - sql

I am working on an assignment and it requires me to place a cursor inside a function.
Now my cursor works fine, but I am having a hard time placing it inside a a function, how can I do this?
The function is supposed to have the same argument as the cursor.
Code:
DECLARE
CURSOR c_emp_salary (v_job_title VARCHAR2)
IS
SELECT employee_id, first_name, salary, min_salary FROM employees e JOIN jobs j ON
e.job_id = j.job_id
AND j.job_title = v_job_title;
v_emp_id employees.employee_id%TYPE;
v_emp_first_name employees.first_name%TYPE;
v_emp_salary employees.salary%TYPE;
v_min_salary jobs.min_salary%TYPE;
BEGIN
OPEN c_emp_salary('Shipping Clerk'); --I WANT THIS VALUE TO BE FROM A FUNCTION.
LOOP
FETCH c_emp_salary INTO v_emp_id, v_emp_first_name, v_emp_salary, v_min_salary;
EXIT WHEN c_emp_salary%NOTFOUND;
dbms_output.put_line('Employee ID: ' || v_emp_id);
dbms_output.put_line('Employee First Name: ' || v_emp_first_name);
dbms_output.put_line('Employee Salary: ' || v_emp_salary);
dbms_output.put_line('Job Min Salary: ' || v_min_salary);
dbms_output.put('Result? ');
IF v_emp_salary = v_min_salary THEN dbms_output.put('Yes');
ELSE dbms_output.put('No');
END IF;
dbms_output.put_line(NULL);
dbms_output.put_line('++++++++++++++++++++++++');
END LOOP;
END;
The assignment description: Write a function that takes job_title and checks all employees in
that job, if an employee has salary equal to min_salary print
“yes” else print “no”. Use a cursor

"Use a cursor" is pointless because every DML or SELECT statement is a cursor.
Anyway, I assume you are looking for this:
CREATE OR REPLACE FUNCTION WRITE_SALARY(v_job_title IN VARCHAR2) AS
CURSOR c_emp_salary IS
SELECT employee_id, first_name, salary, min_salary
FROM employees e
JOIN jobs j ON e.job_id = j.job_id
WHERE j.job_title = v_job_title;
v_emp_id employees.employee_id%TYPE;
v_emp_first_name employees.first_name%TYPE;
v_emp_salary employees.salary%TYPE;
v_min_salary jobs.min_salary%TYPE;
BEGIN
OPEN c_emp_salary;
...

I assume you need something as:
(It needed substitute SCHEMA_NAME and FUNCTION_NAME)
(If return does not needed procedure can be used, and return statements should be removed)
(I don't run this example)
CREATE OR REPLACE function SCHEMA_NAME.FUNCTION_NAME( v_job_title VARCHAR2) return number is
CURSOR c_emp_salary
IS
SELECT employee_id, first_name, salary, min_salary FROM employees e JOIN jobs j ON
e.job_id = j.job_id
AND j.job_title = v_job_title;
v_emp_id employees.employee_id%TYPE;
v_emp_first_name employees.first_name%TYPE;
v_emp_salary employees.salary%TYPE;
v_min_salary jobs.min_salary%TYPE;
result number;
BEGIN
OPEN c_emp_salary('Shipping Clerk'); --I WANT THIS VALUE TO BE FROM A FUNCTION.
LOOP
FETCH c_emp_salary INTO v_emp_id, v_emp_first_name, v_emp_salary, v_min_salary;
EXIT WHEN c_emp_salary%NOTFOUND;
dbms_output.put_line('Employee ID: ' || v_emp_id);
dbms_output.put_line('Employee First Name: ' || v_emp_first_name);
dbms_output.put_line('Employee Salary: ' || v_emp_salary);
dbms_output.put_line('Job Min Salary: ' || v_min_salary);
dbms_output.put('Result? ');
IF v_emp_salary = v_min_salary THEN
dbms_output.put('Yes');
result := 1;
ELSE dbms_output.put('No');
result := 0;
END IF;
dbms_output.put_line(NULL);
dbms_output.put_line('++++++++++++++++++++++++');
return result;
END LOOP;
END;

The OP originally asked:
I am having a hard time placing [the cursor] inside a a function, how can I do this?
and
OPEN c_emp_salary('Shipping Clerk'); --I WANT THIS VALUE TO BE FROM A FUNCTION.
The code below solves the problem originally asked in the question; it is not intended to solve the assignment question that was edited in later as they are not one-and-the-same (that is left as an exercise to the OP).
Oracle Setup:
CREATE TABLE employees (
employee_id NUMBER,
first_name VARCHAR2(50),
salary NUMBER(10,2),
job_id NUMBER
);
CREATE TABLE jobs (
job_id NUMBER,
job_title VARCHAR2(50),
min_salary NUMBER(10,2)
);
INSERT INTO employees VALUES ( 1, 'Alice', 300, 1 );
INSERT INTO jobs VALUES ( 1, 'Shipping Clerk', 200 );
PL/SQL Block:
DECLARE
v_emp_id employees.employee_id%TYPE;
v_emp_first_name employees.first_name%TYPE;
v_emp_salary employees.salary%TYPE;
v_min_salary jobs.min_salary%TYPE;
c_emp_cursor SYS_REFCURSOR;
FUNCTION get_cursor(
v_job_title IN JOBS.JOB_TITLE%TYPE
) RETURN SYS_REFCURSOR
IS
c_emp_salary SYS_REFCURSOR;
BEGIN
OPEN c_emp_salary FOR
SELECT employee_id,
first_name,
salary,
min_salary
FROM employees e
JOIN jobs j
ON ( e.job_id = j.job_id
AND j.job_title = v_job_title );
RETURN c_emp_salary;
END;
BEGIN
c_emp_cursor := get_cursor('Shipping Clerk');
LOOP
FETCH c_emp_cursor INTO v_emp_id, v_emp_first_name, v_emp_salary, v_min_salary;
EXIT WHEN c_emp_cursor%NOTFOUND;
dbms_output.put_line('Employee ID: ' || v_emp_id);
dbms_output.put_line('Employee First Name: ' || v_emp_first_name);
dbms_output.put_line('Employee Salary: ' || v_emp_salary);
dbms_output.put_line('Job Min Salary: ' || v_min_salary);
dbms_output.put('Result? ');
IF v_emp_salary = v_min_salary THEN dbms_output.put('Yes');
ELSE dbms_output.put('No');
END IF;
dbms_output.put_line(NULL);
dbms_output.put_line('++++++++++++++++++++++++');
END LOOP;
END;
/
Output:
Employee ID: 1
Employee First Name: Alice
Employee Salary: 300
Job Min Salary: 200
Result? No
++++++++++++++++++++++++
db<>fiddle here

Related

PL/SQL - The function deosn't return multiple rows

I wanted to select 10 highly paid employees from the "employees" table, but the function returnes only 1 row. How to get multiple rows in this case? My subquery for selecting employees works well, but when I call function it returns 1 row.
This is my code:
CREATE OR REPLACE FUNCTION f_sal
RETURN Varchar2
IS cursor c_emp is (select first_name, last_name from (select first_name, last_name, row_number()
over(order by salary desc) as ranking from employees) where ranking <= 10);
v_first employees.first_name%type;
v_last employees.last_name%type;
begin
open c_emp;
fetch c_emp into v_first, v_last;
close c_emp;
return v_first || ' ' || v_last;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
dbms_output.put_line('Error');
when others then dbms_output.put_line('Other Error');
END;
select f_sal from dual;
Option 1: Use a collection
CREATE FUNCTION f_sal
RETURN SYS.ODCIVARCHAR2LIST
IS
v_names SYS.ODCIVARCHAR2LIST;
BEGIN
SELECT first_name || ' ' || last_name
BULK COLLECT INTO v_names
FROM employees
ORDER BY salary DESC
FETCH FIRST 10 ROWS ONLY;
return v_names;
END;
/
Then:
SELECT * FROM TABLE(f_sal);
Option 2: Use a pipelined function and iterate over a cursor into a collection
CREATE OR REPLACE FUNCTION f_sal
RETURN SYS.ODCIVARCHAR2LIST PIPELINED
IS
BEGIN
FOR n IN (
SELECT first_name || ' ' || last_name AS name
FROM employees
ORDER BY salary DESC
FETCH FIRST 10 ROWS ONLY
)
LOOP
PIPE ROW (n.name);
END LOOP;
END;
/
Then:
SELECT * FROM TABLE(f_sal);
Option 3: Return a cursor
CREATE FUNCTION f_sal
RETURN SYS_REFCURSOR
IS
v_names SYS_REFCURSOR;
BEGIN
OPEN v_names FOR
SELECT first_name || ' ' || last_name AS name
FROM employees
ORDER BY salary DESC
FETCH FIRST 10 ROWS ONLY;
return v_names;
END;
/
Then:
DECLARE
v_names SYS_REFCURSOR := f_sal();
v_name VARCHAR2(100);
BEGIN
LOOP
FETCH v_names INTO v_name;
EXIT WHEN v_names%NOTFOUND;
DBMS_OUTPUT.PUT_LINE( v_name );
END LOOP;
END;
/
db<>fiddle here

How to resolve the missing or invalid option in oracle database?

SET SERVEROUTPUT on
declare
type emp_record is record
(v_empid emp1.empno % type;
v_ename emp1.ename % type; )
emp_rec emp_record;
v_sal emp1.sal%type;
begin
v_sal:=:v_sal;
select empno,ename
into emp_rec
from emp1
where sal:=v_sal;
dbms_output.put_line('The employee whose salary is ' || v_sal || 'has Employee Id ' || emp_rec.v_empid || 'and his name is ' || emp_rec.v_ename);
end;
The error is :
ORA-00922: missing or invalid option
There are several issues within the code. Convert to the one as below:
declare
type emp_record is record
(v_empid emp1.empno % type,
v_ename emp1.ename % type );
emp_rec emp_record;
v_sal emp1.sal%type:=&i_sal;
begin
select empno,ename
into emp_rec
from emp1
where sal = v_sal;
dbms_output.put_line('The employee whose salary is ' || v_sal || 'has Employee Id '
|| emp_rec.v_empid || 'and his name is ' || emp_rec.v_ename);
end;
/
Whenever it prompts for the i_sal, you'll input the desired salary parameter value.
Demo
If you have multiple records to be manipulated as #APC mentioned, such as more than one people has the same salary,you'll need such a loop statement :
declare
type emp_record is record
(v_empid emp1.empno % type,
v_ename emp1.ename % type,
v_sal emp1.sal % type);
emp_rec emp_record;
v_sal emp1.sal%type:=&i_sal;
cursor c_emp is
select empno,ename,sal
into emp_rec
from emp1
where sal = v_sal;
begin
open c_emp;
loop
fetch c_emp into emp_rec;
exit when c_emp%notfound;
dbms_output.put_line('The employee whose salary is ' || emp_rec.v_sal || ' has Employee Id '
|| emp_rec.v_empid || ' and his name is ' || emp_rec.v_ename);
end loop;
close c_emp;
end;
/
Demo
The simplest method to fix that particular problem is to use aggregation:
select max(empno), max(ename)
into emp_rec
from emp1
where sal = v_sal;
An aggregation query with no group by always returns exactly one row.
That said, the more correct method is to handle the exception that is generated by not having a matching value. You will also need to handle exceptions for duplicate values as well.

Issue when creating a stored procedure

I'm trying to create a stored procedure as follows:
CREATE OR REPLACE PROCEDURE storedprocedure(emp number) AS
BEGIN
DECLARE
-- create the cursor based on a query
cursor emp_cursor is
select e.employeeid, firstname, lastname, e.departmentid, e.title,
salary, d.departmentname, billrate
from employees e
full join departments d on e.departmentID = d.departmentID
full join employeeproject p on e.employeeID = p.employeeID where e.employeeID = emp;
BEGIN
open emp_cursor;
-- first display information about the employee
dbms_output.put_line('- -');
dbms_output.put_line('- -');
dbms_output.put_line('Employee#' || e.employeeid
|| ' Name:' || TRIM(e.firstname) || ' ' || TRIM(e.lastname)
|| ' Dept: ');
dbms_output.put_line('_________________________________________________________');
dbms_output.put_line('- -');
dbms_output.put_line('- - Title: ' || e.title
|| ' Salary: ' || to_char(e.salary,'$999,999,999.99'));
dbms_output.put_line('- - Billing Rate: ' || to_char(billrate,'$999,999.99'));
-- next call the stored procedure to show department information
END;
END;
/
But it compiles with errors. When I show errors it tells me e.employeeID, e.title, and billrate must all be declared, but they are the original query. What am I doing wrong here? Am I misunderstand what it means to have them declared?
These are all columns that exist within the tables being queried and running the query as SQL gets results.
You are opening the cursor, but you are not fetching it into anything - either a series of scalar variables or a record type - which would normally be done in a loop. Then when you are in the loop you refer to the variables/record, not the table used in the cursor query - that is out of scope outside the cursor declaration.
There's a slightly simpler implicit cursor loop that you might find a bit easier in this case:
CREATE OR REPLACE PROCEDURE storedprocedure(emp number) AS
BEGIN
FOR rec IN (
select e.employeeid, firstname, lastname, e.departmentid, e.title,
salary, d.departmentname, billrate
from employees e
full join departments d on e.departmentID = d.departmentID
full join employeeproject p on e.employeeID = p.employeeID where e.employeeID = emp
)
LOOP
-- first display information about the employee
dbms_output.put_line('- -');
dbms_output.put_line('- -');
dbms_output.put_line('Employee#' || rec.employeeid
|| ' Name:' || TRIM(rec.firstname) || ' ' || TRIM(rec.lastname)
|| ' Dept: ');
dbms_output.put_line('_________________________________________________________');
dbms_output.put_line('- -');
dbms_output.put_line('- - Title: ' || rec.title
|| ' Salary: ' || to_char(rec.salary,'$999,999,999.99'));
dbms_output.put_line('- - Billing Rate: ' || to_char(rec.billrate,'$999,999.99'));
-- next call the stored procedure to show department information
END LOOP;
END;
/
Notice that all the references to columns inside the loop are of the form rec.<field> - they are taken from the record for this time round the loop, not directly from the underlying tables.
You also had a nested block which isn't doing any harm but isn't useful, so I took that out.
If you particularly wanted to use the explicit open-fetch-close cursor handling it would look something like this:
CREATE OR REPLACE PROCEDURE storedprocedure(emp number) AS
-- create the cursor based on a query
cursor emp_cursor is
select e.employeeid, firstname, lastname, e.departmentid, e.title,
salary, d.departmentname, billrate
from employees e
full join departments d on e.departmentID = d.departmentID
full join employeeproject p on e.employeeID = p.employeeID where e.employeeID = emp;
-- record variable based on cursor definition
rec emp_cursor%ROWTYPE;
BEGIN
OPEN emp_cursor;
LOOP
-- fetch the next row result from the cursor into the record vairable
FETCH emp_cursor INTO rec;
-- break out of the loop if there are no more results to fetch
EXIT WHEN emp_cursor%NOTFOUND;
-- first display information about the employee
dbms_output.put_line('- -');
dbms_output.put_line('- -');
dbms_output.put_line('Employee#' || rec.employeeid
|| ' Name:' || TRIM(rec.firstname) || ' ' || TRIM(rec.lastname)
|| ' Dept: ');
dbms_output.put_line('_________________________________________________________');
dbms_output.put_line('- -');
dbms_output.put_line('- - Title: ' || rec.title
|| ' Salary: ' || to_char(rec.salary,'$999,999,999.99'));
dbms_output.put_line('- - Billing Rate: ' || to_char(rec.billrate,'$999,999.99'));
-- next call the stored procedure to show department information
END LOOP;
CLOSE emp_cursor;
END;
/

PL/SQL - Exact fetch returns more than requested number of rows

I am trying to create function that will return message with maximum salary for each job inside department and order by Maximum salary.
Message need to be:
Department: Department name,
Job/Position: Name of the job, Maximum salary: salary amount,
create or replace PACKAGE BODY Salary AS
FUNCTION max_sal(DEPTNO_F NUMBER)
RETURN VARCHAR2 IS
dept_name VARCHAR2(25);
job_possition VARCHAR(25);
maximum_salary NUMBER;
message VARCHAR2(255);
BEGIN
SELECT DNAME, JOB, MAX(SAL) AS "SAL"
INTO job_possition, maximum_salary
FROM EMP
WHERE DEPTNO = DEPTNO_F
GROUP BY JOB, DNAME
ORDER BY SAL DESC;
message := 'Department name: '||dept_name|| 'Job positin: ' ||job_possitin||, 'Maximum Salary: ' ||maximum_salary;
return message;
END max_sal;
END Salary;
The problem is your query is returning one than one row and it can't select into your 2 variables. Have you tried running the SELECT SQL statement outside of the package and checked the rows you are getting back?
For any given DEPTNO_F value you should not have more than one permutation of DNAME and JOB
To process several rows in query result you have to use loops:
create or replace PACKAGE BODY Salary AS
FUNCTION max_sal(DEPTNO_F NUMBER)
RETURN VARCHAR2 IS
dept_name VARCHAR2(25);
job_possition VARCHAR(25);
maximum_salary NUMBER;
message VARCHAR2(255);
BEGIN
for i in (SELECT DNAME, JOB, MAX(SAL) SAL
FROM EMP
WHERE DEPTNO = DEPTNO_F
GROUP BY JOB, DNAME
ORDER BY SAL DESC) loop
message := message || 'Department name: ' || i.DNAME || ', Job position: ' ||
i.JOB || ', Maximum Salary: ' || i.SAL || chr(10);
end loop;
return message;
END max_sal;
END Salary;

PL SQL Query for grouping Department names

I am new to PL/SQL and I am stuck at this one point where I am unable to group data by department names. my query is as follows
DECLARE
CURSOR dept_cur IS
select department_name , NVL(employee_name , 'N/A') , NVL(employee.JOB , 'N/A') , NVL(to_char(hire_date, 'DD-MON-YYYY') , 'N/A')
FROM department FULL OUTER JOIN employee ON department.department_id = employee.department_id
order by department_name , employee_name;
v_department_name department.department_name%TYPE;
v_employee_name employee.employee_name%type;
v_employee_job employee.job%type;
v_hire_date VARCHAR(20);
BEGIN
OPEN dept_cur;
fetch dept_cur into v_department_name , v_employee_name , v_employee_job , v_hire_date ;
IF dept_cur%FOUND THEN
DBMS_OUTPUT.PUT_LINE('DEPARTMENT_NAME EMPLOYEE NAME , EMPLOYEE JOB , HIRE DATE');
DBMS_OUTPUT.PUT_LINE('-----------------------------------------------------');
WHILE dept_cur%FOUND LOOP
DBMS_OUTPUT.PUT_LINE(lpad(v_department_name, 11) || lpad(v_employee_name, 16) || lpad(v_employee_job, 22)|| lpad(v_hire_date,15));
fetch dept_cur into v_department_name , v_employee_name , v_employee_job , v_hire_date ;
END LOOP;
ELSE
DBMS_OUTPUT.PUT_LINE('No output.');
END IF;
CLOSE dept_cur;
END;
I want the output to be something like this , but my query returns the entire list .
The number of employees in ACCOUNTING is 2
EMPLOYEE_NAME JOB HIRE DATE
--------------------------------------------------------------
JONES PUBLIC ACCOUNTANT 02-APR-81
STEEL PUBLIC ACCOUNTANT 02-MAR-83
and the same for other departments as well. I understand its a minor change in the PL SQL block logic , but I am unable to figure it out.
DECLARE
CURSOR dept_cur IS
SELECT department_name,
COUNT(employee.employee_id) OVER (PARTITION BY department_name),
NVL(employee_name, 'N/A'),
NVL(employee.JOB, 'N/A'),
NVL(to_char(hire_date, 'DD-MON-YYYY'), 'N/A')
FROM department
FULL OUTER JOIN employee ON department.department_id = employee.department_id
ORDER BY department_name, employee_name;
v_department_name department.department_name%TYPE;
last_department_name department.department_name%TYPE;
v_department_count NUMBER;
v_employee_name employee.employee_name%TYPE;
v_employee_job employee.job%TYPE;
v_hire_date VARCHAR(20);
BEGIN
OPEN dept_cur;
FETCH dept_cur INTO v_department_name, v_department_count, v_employee_name, v_employee_job, v_hire_date;
last_department_name := NULL;
IF dept_cur%FOUND THEN
WHILE dept_cur%FOUND LOOP
IF last_department_name IS NULL OR last_department_name <> v_department_name THEN
DBMS_OUTPUT.PUT_LINE('The number of employees in ' || v_department_name || ' is ' || to_char(v_department_count));
DBMS_OUTPUT.PUT_LINE('EMPLOYEE NAME EMPLOYEE JOB HIRE DATE');
DBMS_OUTPUT.PUT_LINE('--------------------------------------------------------------');
last_department_name := v_department_name;
END IF;
DBMS_OUTPUT.PUT_LINE(rpad(v_employee_name, 24) || rpad(v_employee_job, 24)|| lpad(v_hire_date,14));
FETCH dept_cur INTO v_department_name, v_department_count, v_employee_name, v_employee_job, v_hire_date;
END LOOP;
ELSE
DBMS_OUTPUT.PUT_LINE('No output.');
END IF;
CLOSE dept_cur;
END;
It uses an analytic function COUNT() OVER () to count the number of employees in the result for each department, and inserts that on every row for that department.
Since the query is already ordered by department, it just checks for when that changes to another value, and then prints the headers.