How to return a set of results from one Oracle subprogram to another? - sql

This is a simplified version of what I'm trying to do, but let's say I want to select all employees whose manager is in a given department. I could have a SQL query like the following:
SELECT employees.id
FROM employees
WHERE employees.manager IN (SELECT managers.id
FROM managers
WHERE managers.dept = 12)
;
But let's say I want to abstract the manager subquery into a PL/SQL subprogram. How do I do that?
The stored procedures I've worked with (which are mostly written by other developers) tend to have out parameters that get mapped by PHP calling code into a PHP array. I don't really have any experience of calling one stored procedure from another.
What I'd like to do is to have something like this:
SELECT employees.id
FROM employees
WHERE employees.manager IN my_stored_procedure(12)
;
and then my_stored_procedure would output the set of manager IDs for the input parameter (which is 12 in this example).

It is not possible to do exactly as you have posted, but if the selection of managers are not straightforward, you could abstract it through a view or make use of a function that returns a table, like this:
SELECT employees.id
FROM employees
WHERE employees.manager IN (SELECT * from TABLE(get_managers_from_dept(12)));
In this link there is an example of that approach:
Function or Procedure for an IN clause

To Call A stored Proc from another Stored Proc you just need to call it from the Main Proc as mentioned below. This Main Proc can be called /initiated by a PHP Code.
PROCEDURE some_sp
AS
BEGIN
some_other_sp('parm1');
END;

Although less straight-forward, You can do accomplish it by using dynamic sql. This is the structure of your stored procedure. It returns a comma separated list of manager_ids for a given department.
CREATE OR REPLACE FUNCTION my_stored_procedure(
p_dept NUMBER)
RETURN VARCHAR2
IS
v_manager_list VARCHAR2(1000);
BEGIN
SELECT m.id INTO v_manager_list FROM managers m WHERE m.dept = p_dept;
RETURN '('||v_manager_list||')';
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN '(NULL)';
END;
/
Now you cannot use this to directly query as ...IN my_stored_procedure(12),
rather you must use a dynamic fetch into a collection.
SET SERVEROUTPUT ON;
DECLARE
TYPE v_emp_type
IS
TABLE OF employees.id%TYPE;
v_emp v_emp_type;
BEGIN
EXECUTE IMMEDIATE 'SELECT employees.id
FROM employees
WHERE employees.id IN '|| my_stored_procedure(100) BULK COLLECT INTO v_emp ;
FOR i IN v_emp.FIRST..v_emp.LAST
LOOP
DBMS_OUTPUT.PUT_LINE(v_emp(i));
END LOOP;
END;
/

Related

Wrong number or types of arguments

Create or replace procedure sp_create_tables as
Lv_str varchar2(1000);
Begin
For I in (select distinct(deptno) from emp) loop
Lv_str:='create table deptno'||I||' select * from emp where
1=2';
Execute immediate lv_str;
End loop;
End;
From what I can understand, your question is probably "why the procedure throws this compilation error"
PLS-00306: wrong number or types of arguments in call to '||'
The reason is that the implicit cursor loop variable I refers to the set of records from the query and not the deptno itself. In order to refer to the deptno, you should use <loop_variable>.deptno. Also 2 other things you should change: The as keyword is missing and DISTINCT is a keyword and you are using it as a function, which works because of default parentheses, but is not the right way to use it.
Create or replace procedure sp_create_tables as
Lv_str varchar2(1000);
Begin
For I in (select distinct dept from emp) loop
Lv_str:='create table deptno'||I.deptno||' as select * from emp where
1=2';
Execute immediate lv_str;
End loop;
End;
In this line of code I is an implicit rowtype variable, with a data structure defined by the projection of the driving select statement:
For I in (select distinct(deptno) from emp) loop
But you are attempting to reference it in your dynamic SQL as though it were an attribute. You need to use a column name instead.
Create or replace procedure sp_create_tables as
Lv_str varchar2(1000);
Begin
For I in (select distinct (deptno) from emp) loop
Lv_str:='create table deptno'|| I.deptno ||
' as select * from emp where 1=2';
Execute immediate lv_str;
End loop;
End;
Incidentally there is a bug in your dynamic SQL statement. The correct syntax is CREATE TABLE ... AS SELECT .... Dynamic SQL is hard because the compiler can't validate the bits of code in strings. Consequently what should be compilation errors manifest themselves as runtime errors. You will find it helpful to instrument your code with some logging (or dbms_output.put_line()) to record the assembled statement before it runs. It makes debugging a lot easier.
" i have got a error saying -01031 insufficient priviliges"
So what this means is your authorisation to create a table was granted through a role. The Oracle security model does not allow us to build PL/SQL programs - or views - using privileges granted through a role. This includes PL/SQL executing DDL through dynamic SQL. You need a DBA user to grant CREATE TABLE to your user directly.

Can an Oracle Procedure produce output records?

I am new to Oracle 10g and know that in MS SQL Server I can create a procedure such as the one below to generate an output record. Is this possible with a standard Oracle Procedure or do I need to use a package/function?
CREATE PROCEDURE SAMPLE_STORED_PROCEDURE
#USERNAME varchar(10)
AS
BEGIN
SELECT NAME as Output from Employee where Username = #USERNAME
END
GO
Thank you
Assuming that username is unique in employee (which seems like a reasonable guess to me), you probably want a function
CREATE OR REPLACE FUNCTION function_name( p_username IN employee.username%type )
RETURN employee.name%type
IS
l_name employee.name%type;
BEGIN
SELECT name
INTO l_name
FROM employee
WHERE username = p_username;
RETURN l_name;
END function_name;
You could also use a procedure with an OUT parameter
CREATE OR REPLACE PROCEDURE procedure_name( p_username IN employee.username%type,
p_name OUT employee.name%type )
AS
BEGIN
SELECT name
INTO p_name
FROM employee
WHERE username = p_username;
END procedure_name;
Generally, in PL/SQL you'd want to use a function whenever you want to write some sort of "getter" that reads data from the database and a procedure whenever you want to write some sort of "setter" that writes data to the database. So a function would make more sense here. Both functions and procedures can and should be organized into packages that allow you to group together related functionality. It would probably make sense, for example, to have a package that groups together all the functions and procedures related to adding, modifying, deleting, and reading information about employees.
If my original guess that username is not unique in employee is incorrect and you expect your select statement to return multiple rows, there are a number of different choices depending on exactly what you're going to be doing with that data. You can write a function that returns a sys_refcursor. You can write a function that returns a collection. You can write a pipelined table function. Without knowing more, however, it's impossible to know which of these options would make more sense in your case.

Dynamic Cursors

I use a cursor for the statement:
SELECT NAME FROM STUDENT WHERE ROLL = 1;
I used:
CURSOR C IS SELECT NAME FROM STUDENT WHERE ROLL = roll;
--roll is a variable I receive via a procedure, and the procedure works fine for the received parameter.
Upon executing this, I am able to retrieve all records with roll = 1.
Now, I need to retrieve the records of a group (possibly via a cursor), just like:
SELECT NAME FROM STUDENT WHERE ROLL IN (2, 4, 6);
But the values in the IN clause are known only at run time. How should I do this? That is, is there any way I could assign parameters to the WHERE clause of the cursor?
I tried using an array in the declaration of the cursor, but an error pops up telling something like: standard types cannot be used.
I used:
CURSOR C IS SELECT NAME FROM STUDENT WHERE ROLL IN (rolls);
--rolls is an array initialized with the required roll numbers.
First, I assume that the parameter to your procedure doesn't actually match the name of a column in the STUDENT table. If you actually coded the statement you posted, roll would be resolved as the name of the column, not the parameter or local variable so this statement would return every row in the STUDENT table where the ROLL column was NOT NULL.
CURSOR C
IS SELECT NAME
FROM STUDENT
WHERE ROLL = roll;
Second, while it is possible to use dynamic SQL as #Gaurav Soni suggests, doing so generates a bunch of non-sharable SQL statements. That's going to flood the shared pool, probably aging other statements out of cache, and use a lot of CPU hard-parsing the statement every time. Oracle is built on the premise that you are going to parse a SQL statement once, generally using bind variables, and then execute the statement many times with different values for the bind variables. Oracle can go through the process of parsing the query, generating the query plan, placing the query in the shared pool, etc. only once and then reuse all that when you execute the query again. If you generate a bunch of SQL statements that will never be used again because you're using dynamic SQL without bind variables, Oracle is going to end up spending a lot of time caching SQL statements that will never be executed again, pushing useful cached statements that will be used again out of the shared pool meaning that you're going to have to re-parse those queries the next time they're encountered.
Additionally, you've opened yourself up to SQL injection attacks. An attacker can exploit the procedure to read any data from any table or execute any function that the owner of the stored procedure has access to. That is going to be a major security hole even if your application isn't particularly security conscious.
You would be better off using a collection. That prevents SQL injection attacks and it generates a single sharable SQL statement so you don't have to do constant hard parses.
SQL> create type empno_tbl is table of number;
2 /
Type created.
SQL> create or replace procedure get_emps( p_empno_arr in empno_tbl )
2 is
3 begin
4 for e in (select *
5 from emp
6 where empno in (select column_value
7 from table( p_empno_arr )))
8 loop
9 dbms_output.put_line( e.ename );
10 end loop;
11 end;
12 /
Procedure created.
SQL> set serveroutput on;
SQL> begin
2 get_emps( empno_tbl( 7369,7499,7934 ));
3 end;
4 /
SMITH
ALLEN
MILLER
PL/SQL procedure successfully completed.
create or replace procedure dynamic_cur(p_empno VARCHAR2) IS
cur sys_refcursor;
v_ename emp.ename%type;
begin
open cur for 'select ename from emp where empno in (' || p_empno || ')';
loop
fetch cur into v_ename;
exit when cur%notfound;
dbms_output.put_line(v_ename);
end loop;
close cur;
end dynamic_cur;
Procedure created
Run the procedure dynamic_cur
declare
v_empno varchar2(200) := '7499,7521,7566';
begin
dynamic_cur(v_empno);
end;
Output
ALLEN
WARD
JONES
Note:As mentioned by XQbert,dynamic cursor leads to SQL injection ,but if you're not working on any critical requirement ,where security is not involved then you can use this .
Maybe you can pass rolls as a set of quoted comma separated values.
e.g. '1', '2' etc
If this value is passes into the procedure in a varchar input variable, the it can be used to get multiple rows as per the table match.
Hence the cursor
SELECT NAME FROM STUDENT WHERE ROLL IN (rolls);
will be evaluated as
SELECT NAME FROM STUDENT WHERE ROLL IN ('1','2');
Hope it helps

Possible to extend static SQL statements with dynamic parts?

I'd like to create a Oracle package where I have a procedure that executes some dynamic SQL. This is no problem if I'm doing it all dynamic with EXECUTE IMMEDIATE but it would be better if the static parts of the query could be coded static (to have compile time checking).
Example of fully dynamic query:
-- v_stmt is built dynamically.
v_stmt := 'SELECT count(*) FROM <here some joins> WHERE <here some conditions>';
EXECUTE IMMEDIATE v_stmt
USING v_param1, v_param2
RETURNING INTO v_count;
Example of what I tried to make the FROM-part static:
-- v_stmt is built dynamically.
v_stmt := 'SELECT count(*) FROM my_package.my_function(:param1, :param2) WHERE <here some conditions>';
EXECUTE IMMEDIATE v_stmt
USING v_param1, v_param2
RETURNING INTO v_count;
FUNCTION my_function(
i_param1 IN VARCHAR2,
i_param2 IN NUMBER
)
RETURN SYS_REFCURSOR
AS
v_cursor SYS_REFCURSOR;
BEGIN
-- Open a cursor for different queries depending on params.
IF i_param2 = 1 THEN
OPEN v_cursor FOR <some static query>;
ELSE
OPEN v_cursor FOR <some other static query>;
END IF;
RETURN v_cursor;
END;
This doesn't work because it's not possible to select from a SYS_REFCURSOR (at least that's what I found with Google).
Is there any way to reach this goal?
edit: As requested, here are some examples:
Static queries:
SELECT a.*, ca.CUS_ID FROM adresses a INNER JOIN customer_adresses ca ON (ca.adr_id = a.adr_id);
SELECT p.*, cp.CUS_ID FROM persons p INNER JOIN customer_persons cp ON (cp.per_id = p.per_id);
Then they are extended dynamically like the following examples:
-- Checks if there is an adress in the customer where the zip is null.
SELECT count(*) FROM <static adresses query> q WHERE q.cus_id = :param1 AND a.zip IS NULL;
-- Checks if there is at least one person in the customer.
SELECT count(*) FROM <static persons query> q WHERE q.cus_id = :param1;
Sorry, but why the need to do this? Seems you're over complicating things by introducing a function that will return different types of data/tables depending on the parameter list. Very confusing imo. Besides, you have to do the work somewhere, you're just trying to hide it in this function (inside if param1=this then x if param1=that then y...)
Besides, even if you did implement a cursor function (even pipelined), it would be a bad idea in this case because you'd be forcing Oracle into doing work that it wouldn't necessarily need to do (ignore all the context switching for now). To just get a count, you'd have Oracle grab each an every row result and then count. Many times Oracle can just do a fast full index scan to get the count (depending on the query of course). And often same query run multiple times will not need to do all the work each time if blocks are found in buffer cache. I'd challenge you to run the count multiple times using straight SQL vs using a function returning a cursor. You might be surprised. And to my knowledge (check me on this) the new 11g function result cache won't work on a pipelined functions or a function returning a ref cursor (along with other issues like invalidations due to relies on tables).
So, what I'm saying is why not just do: select count(1) into v_variable from ...;
If you want to hide and modularize, then just know what you're potentially losing.
You may want to open a query in function1 and then pipeline the results of it as a table to function2 which then will add a where clause to this "table"
In this case you'll want to rewrite your function1 as a pipelined table function
v_stmt := 'SELECT count(*) FROM table(my_package.my_function(:param1, :param2)) WHERE <here some conditions>';
EXECUTE IMMEDIATE v_stmt
USING v_param1, v_param2
RETURNING INTO v_count;
CREATE TYPE object_row_type AS OBJECT (
OWNER VARCHAR2(30),
OBJECT_TYPE VARCHAR2(18),
OBJECT_NAME VARCHAR2(30),
STATUS VARCHAR2(7)
);
CREATE TYPE object_table_type AS TABLE OF object_row_type;
FUNCTION my_function(
i_param1 IN VARCHAR2,
i_param2 IN NUMBER
)
RETURN object_table_type PIPELINED AS
BEGIN
You can have compile time checking of expressions with Oracle expression filter.
It's probably more complicated than the other solutions, but if you really need to verify your conditions it can be helpful.

Querying multiple rows from Oracle table using package

I wrote a package to query rows from a table. This select query will call other functions and returns all the rows from table. But when i write a package with all functions and sprocs , my sproc with select statement gives me an error saying i cannot execute without into statement. But if i use into then it will return only one row. How can i retrieve all rows using oracle sp?
Procedure GetData As
BEGIN
Select Jobid, JobName, JobLocation, JobCompany, X(jobid) FROM jobsTable; END GetData;
END;
I had to change it to following make the error go away:
Procedure GetData As
r_Jobid jobsTable.jobid%type;
r_JobName jobsTable.jobName%type;
r_JobLocation jobsTable.jobLocation%type;
r_temp varhar2(10);
BEGIN
Select Jobid, JobName, JobLocation, JobCompany, X(jobid)
INTO r_jobid, r_jobName, r_jobLocation, r_temp
FROM jobsTable;
END GetData;
END;
This is a better approach to returning multiple rows from a function:
FUNCTION GET_DATA()
RETURN SYS_REFCURSOR IS
results_cursor SYS_REFCURSOR;
BEGIN
OPEN results_cursor FOR
SELECT t.jobid,
t.jobName,
t.joblocation,
t.jobcompany,
X(t.jobid)
FROM JOBSTABLE t;
RETURN results_cursor;
END;
I agree with afk though that this doesn't appear to be what you really need to be using. Here's my recommendation for using a cursor:
CURSOR jobs IS
SELECT t.jobid,
t.jobName,
t.joblocation,
t.jobcompany,
X(t.jobid)
FROM JOBSTABLE t;
v_row jobs%ROWTYPE; --has to be declared AFTER the cursor to be able to reference the row type
BEGIN
OPEN jobs;
FETCH jobs INTO v_row;
IF jobs%FOUND THEN
--do stuff here, per row basis
--access columns in the row using: v_row.jobid/etc
END IF;
CLOSE jobs;
END;
Are you aware that this:
Procedure GetData As
r_Jobid jobsTable.jobid%type;
r_JobName jobsTable.jobName%type;
r_JobLocation jobsTable.jobLocation%type;
r_temp varhar2(10);
...means you defined local variables? You won't be able to get information out of the procedure. If you do, you'd need parameters, like this:
Procedure GetData(IO_R_JOBID IN OUT JOBSTABLE.JOBID%TYPE,
IO_R_JOBNAME IN OUT JOBSTABLE.JOBNAME%TYPE,
IO_R_JOBLOCATION IN OUT JOBSTABLE.JOBLOCATION%TYPE,
IO_R_TEMP IN OUT VARCHAR2(10)) AS
I use the IO_ to note which parameters are IN/OUT. I'd use IN_ or OUT_ where applicable. But the key here is to define OUT if you want to get a parameter back out.
Also - packages are just logical grouping of procedures & functions, with the ability to define constants scoped to the package. The package itself doesn't execute any SQL - it's still a function or procedure that is executing. God how I wish SQL Server had packages...
You could use a pipelined function. For this example I'm only fetching the ID columns, but you just need to add the others.
CREATE PACKAGE jobsPkg AS
TYPE jobsDataRec IS RECORD ( jobid jobsTable.jobid%type );
TYPE jobsDataTab IS TABLE OF jobsDataRec;
FUNCTION getJobsData RETURN jobsDataTab PIPELINED;
END jobsPkg;
/
CREATE PACKAGE BODY jobsPkg AS
FUNCTION getJobsData RETURN jobsDataTab PIPELINED
IS
output_record jobsDataRec;
BEGIN
FOR input_record IN ( SELECT jobid FROM jobsTable ) LOOP
output_record.jobid := input_record.jobid;
PIPE ROW (output_record);
END LOOP;
END getJobsData;
END jobsPkg;
/
This function can then be used as a row source:
SELECT * FROM TABLE( jobsPkg.getJobsData );
You need to use a Cursor for multiple return results. Take a look at this article on using Cursors for some details.
You could do something like:
DECLARE
CURSOR myCur IS
SELECT Jobid, JobName, JobLocation, JobCompany, X(jobid)
FROM jobsTable;
BEGIN
FOR resultRow in myCur
LOOP
--do your stuff here
END LOOP;
END;