Cursors are fundamental to PL/SQL for processing multiple rows returned by a SQL query. While explicit and implicit cursors handle many scenarios, advanced cursor concepts provide greater control, flexibility, and efficiency for complex data manipulation. Mastering these concepts is crucial for any "Oracle PL/SQL developer" looking to write robust and high-performing applications.
The FOR UPDATE Clause
The FOR UPDATE
clause is used with an explicit cursor to lock the rows retrieved by a SELECT
statement. This prevents other transactions from modifying or deleting these rows until your transaction either commits or rolls back. This is essential for maintaining data integrity in "concurrent PL/SQL applications" and avoiding "lost updates" when dealing with "shared data" in an "Oracle database."
Example 1: Basic FOR UPDATE
DECLARE
v_employee_id NUMBER := 100;
v_salary NUMBER;
CURSOR c_employee_salary IS
SELECT salary
FROM employees
WHERE employee_id = v_employee_id
FOR UPDATE OF salary; -- Locks the salary column for the selected row
BEGIN
OPEN c_employee_salary;
FETCH c_employee_salary INTO v_salary;
IF c_employee_salary%FOUND THEN
-- Simulate some processing that takes time, holding the lock
DBMS_LOCK.SLEEP(5);
-- Update the salary, which is safe because the row is locked
UPDATE employees
SET salary = v_salary * 1.10
WHERE employee_id = v_employee_id;
DBMS_OUTPUT.PUT_LINE('Salary updated for employee ' || v_employee_id);
ELSE
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id || ' not found.');
END IF;
CLOSE c_employee_salary;
COMMIT; -- Releases the lock
EXCEPTION
WHEN OTHERS THEN
ROLLBACK; -- Releases the lock in case of error
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/
Explanation
This example demonstrates a basic use of FOR UPDATE. We declare a cursor c_employee_salary that selects the salary for a specific employee_id and explicitly locks that row for updates using FOR UPDATE OF salary. This ensures that while our transaction is processing, no other session can modify this employee's salary. The DBMS_LOCK.SLEEP simulates work being done while the lock is held. The COMMIT statement is crucial as it releases the row lock, making the updated data available to other transactions. This is a common pattern for "row-level locking" in "PL/SQL transactions."
Example 2: FOR UPDATE with NOWAIT
DECLARE
v_employee_id NUMBER := 101;
v_salary NUMBER;
CURSOR c_employee_salary IS
SELECT salary
FROM employees
WHERE employee_id = v_employee_id
FOR UPDATE OF salary NOWAIT; -- Attempts to lock immediately, errors if locked
BEGIN
OPEN c_employee_salary;
FETCH c_employee_salary INTO v_salary;
IF c_employee_salary%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Successfully locked row for employee ' || v_employee_id);
-- Perform update
UPDATE employees
SET salary = v_salary * 1.05
WHERE employee_id = v_employee_id;
COMMIT;
END IF;
CLOSE c_employee_salary;
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -54 THEN -- ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
DBMS_OUTPUT.PUT_LINE('Row is currently locked by another session. Try again later.');
ELSE
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END IF;
ROLLBACK;
END;
/
Explanation
The NOWAIT clause is used when you don't want your transaction to wait if the requested rows are already locked by another session. Instead of waiting, the FOR UPDATE statement immediately raises an ORA-00054 error. This is particularly useful in "high-concurrency environments" where immediate feedback is preferred over blocking. This example shows how to handle this specific error.
Example 3: FOR UPDATE with WAIT
DECLARE
v_product_id NUMBER := 10;
v_quantity NUMBER;
CURSOR c_product_stock IS
SELECT quantity_on_hand
FROM products
WHERE product_id = v_product_id
FOR UPDATE OF quantity_on_hand WAIT 10; -- Waits up to 10 seconds for the lock
BEGIN
OPEN c_product_stock;
FETCH c_product_stock INTO v_quantity;
IF c_product_stock%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Current quantity: ' || v_quantity);
-- Simulate processing
DBMS_LOCK.SLEEP(2);
UPDATE products
SET quantity_on_hand = v_quantity - 1
WHERE product_id = v_product_id;
DBMS_OUTPUT.PUT_LINE('Quantity updated for product ' || v_product_id);
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('Product ' || v_product_id || ' not found.');
END IF;
CLOSE c_product_stock;
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -54 THEN
DBMS_OUTPUT.PUT_LINE('Lock could not be acquired within 10 seconds. Try again.');
ELSE
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END IF;
ROLLBACK;
END;
/
Explanation
The WAIT clause allows you to specify a timeout in seconds. If the FOR UPDATE statement cannot acquire the lock within the specified time, it will raise an ORA-00054 error. This offers a middle ground between immediately failing (NOWAIT) and waiting indefinitely (default behavior). It's helpful for "resource management" in "Oracle applications."
Example 4: FOR UPDATE in a FOR loop (Implicit Cursor)
-- This example demonstrates FOR UPDATE within an implicit cursor FOR loop,
-- which implicitly opens, fetches, and closes the cursor.
BEGIN
FOR emp_rec IN (SELECT employee_id, salary
FROM employees
WHERE department_id = 50
FOR UPDATE OF salary)
LOOP
DBMS_OUTPUT.PUT_LINE('Processing employee ' || emp_rec.employee_id || ' with salary ' || emp_rec.salary);
-- Simulate some work
DBMS_LOCK.SLEEP(1);
UPDATE employees
SET salary = emp_rec.salary * 1.03
WHERE employee_id = emp_rec.employee_id;
-- The lock on emp_rec.employee_id is held until the loop finishes
-- or an explicit COMMIT/ROLLBACK occurs.
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Salaries updated for department 50.');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Error during update: ' || SQLERRM);
END;
/
Explanation
While FOR UPDATE is most commonly seen with explicit cursors, it can also be used in an implicit cursor FOR loop. In this case, Oracle automatically manages the cursor lifecycle (opening, fetching, closing). Each row fetched within the loop is locked for the duration of that iteration, or until the loop completes, or until a COMMIT or ROLLBACK is issued. This is a concise way to "process and lock multiple rows" in "PL/SQL."
Example 5: Skipping Locked Rows (SKIP LOCKED)
-- This feature is available in Oracle Database 11g Release 2 and later.
DECLARE
v_order_id NUMBER;
v_status VARCHAR2(20);
CURSOR c_pending_orders IS
SELECT order_id, status
FROM orders
WHERE status = 'PENDING'
FOR UPDATE SKIP LOCKED; -- Skips rows currently locked by other sessions
BEGIN
OPEN c_pending_orders;
LOOP
FETCH c_pending_orders INTO v_order_id, v_status;
EXIT WHEN c_pending_orders%NOTFOUND;
-- Process the order
DBMS_OUTPUT.PUT_LINE('Processing order ' || v_order_id || ' with status ' || v_status);
-- Simulate work
DBMS_LOCK.SLEEP(0.5);
-- Update the status
UPDATE orders
SET status = 'PROCESSED'
WHERE order_id = v_order_id;
END LOOP;
CLOSE c_pending_orders;
COMMIT;
DBMS_OUTPUT.PUT_LINE('All available pending orders processed.');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/
Explanation
The SKIP LOCKED clause (introduced in Oracle 11gR2) is extremely useful in "high-volume processing" scenarios where multiple sessions might be trying to process the same set of records (e.g., a queue). Instead of waiting or erroring out when a row is locked, SKIP LOCKED simply bypasses those rows and fetches the next available unlocked row. This prevents contention and improves throughput, making it a key feature for "batch processing" and "queue processing" in "Oracle PL/SQL."
T
he WHERE CURRENT OF Clause
The WHERE CURRENT OF
clause is used with UPDATE
or DELETE
statements inside a cursor loop. It allows you to modify or delete the current row that was just fetched by the cursor, without needing to specify the primary key or unique identifier again in the WHERE
clause. This ensures that you are acting on precisely the row currently being pointed to by the cursor, preventing "race conditions" and ensuring "data consistency." This is often used in conjunction with FOR UPDATE
to guarantee the row is locked before modification.
Example 1: Basic WHERE CURRENT OF with UPDATE
DECLARE
v_job_id VARCHAR2(10) := 'IT_PROG';
v_min_salary NUMBER;
CURSOR c_job_salaries IS
SELECT job_id, min_salary
FROM jobs
WHERE job_id = v_job_id
FOR UPDATE OF min_salary; -- Lock the row
BEGIN
OPEN c_job_salaries;
FETCH c_job_salaries INTO v_job_id, v_min_salary;
IF c_job_salaries%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Updating minimum salary for ' || v_job_id || ' from ' || v_min_salary);
-- Update the current row in the cursor result set
UPDATE jobs
SET min_salary = v_min_salary + 500
WHERE CURRENT OF c_job_salaries;
DBMS_OUTPUT.PUT_LINE('Minimum salary updated.');
ELSE
DBMS_OUTPUT.PUT_LINE('Job ' || v_job_id || ' not found.');
END IF;
CLOSE c_job_salaries;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/
Explanation
This example demonstrates how WHERE CURRENT OF simplifies updating the current row processed by a cursor. After fetching a row using c_job_salaries, the UPDATE statement uses WHERE CURRENT OF c_job_salaries to refer to that specific row, avoiding the need to construct a WHERE clause based on the job_id. This is highly efficient and less error-prone when "updating cursor rows."
Example 2: WHERE CURRENT OF with DELETE
DECLARE
v_order_id NUMBER;
v_customer_id NUMBER;
CURSOR c_old_orders IS
SELECT order_id, customer_id
FROM orders
WHERE order_date < ADD_MONTHS(SYSDATE, -12) -- Orders older than 1 year
FOR UPDATE; -- Lock rows to be deleted
BEGIN
OPEN c_old_orders;
LOOP
FETCH c_old_orders INTO v_order_id, v_customer_id;
EXIT WHEN c_old_orders%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('Deleting order ' || v_order_id || ' for customer ' || v_customer_id);
-- Delete the current row in the cursor result set
DELETE FROM orders
WHERE CURRENT OF c_old_orders;
END LOOP;
CLOSE c_old_orders;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Old orders deleted.');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Error during deletion: ' || SQLERRM);
END;
/
Explanation
Similar to UPDATE, WHERE CURRENT OF can be used with DELETE to remove the specific row currently pointed to by the cursor. This is particularly useful in "data archiving" or "data purging" operations where you are iterating through a result set and deleting based on certain criteria. The FOR UPDATE ensures that the row is exclusively locked for deletion.
Example 3: Combining FOR UPDATE and WHERE CURRENT OF in a loop
DECLARE
v_employee_id NUMBER;
v_salary NUMBER;
CURSOR c_underpaid_employees IS
SELECT employee_id, salary
FROM employees
WHERE salary < 5000
FOR UPDATE OF salary; -- Locks the rows for update
BEGIN
OPEN c_underpaid_employees;
LOOP
FETCH c_underpaid_employees INTO v_employee_id, v_salary;
EXIT WHEN c_underpaid_employees%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('Adjusting salary for employee ' || v_employee_id || ' from ' || v_salary);
-- Update the current row in the cursor result set
UPDATE employees
SET salary = v_salary * 1.15 -- Increase by 15%
WHERE CURRENT OF c_underpaid_employees;
END LOOP;
CLOSE c_underpaid_employees;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Salaries adjusted for underpaid employees.');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Error during salary adjustment: ' || SQLERRM);
END;
/
Explanation
This example showcases the powerful combination of FOR UPDATE and WHERE CURRENT OF within a loop. The FOR UPDATE clause ensures that each row fetched is locked, preventing other sessions from modifying it. Then, WHERE CURRENT OF precisely targets that locked row for the UPDATE operation. This is a robust pattern for "batch updates" that require strict "transactional integrity."
Example 4: Using WHERE CURRENT OF with ROWID (less common but possible)
-- While not typical, WHERE CURRENT OF conceptually works with ROWID,
-- though direct ROWID updates are often less maintainable.
DECLARE
v_department_id NUMBER := 10;
v_department_name VARCHAR2(30);
v_rowid UROWID; -- Universal ROWID
CURSOR c_dept IS
SELECT ROWID, department_id, department_name
FROM departments
WHERE department_id = v_department_id
FOR UPDATE;
BEGIN
OPEN c_dept;
FETCH c_dept INTO v_rowid, v_department_id, v_department_name;
IF c_dept%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Old department name: ' || v_department_name);
UPDATE departments
SET department_name = v_department_name || ' (UPDATED)'
WHERE CURRENT OF c_dept;
DBMS_OUTPUT.PUT_LINE('New department name: ' || v_department_name || ' (UPDATED)');
ELSE
DBMS_OUTPUT.PUT_LINE('Department ' || v_department_id || ' not found.');
END IF;
CLOSE c_dept;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/
Explanation
This example demonstrates that WHERE CURRENT OF operates on the "current row" identified by the cursor, regardless of whether you explicitly fetch the ROWID. While you could fetch the ROWID and use it in a separate UPDATE statement, WHERE CURRENT OF is specifically designed for this purpose and is generally preferred for its directness and safety within cursor operations.
Example 5: Error Handling with WHERE CURRENT OF (Ensuring Cursor Open)
DECLARE
v_location_id NUMBER := 1700;
v_city VARCHAR2(30);
CURSOR c_locations IS
SELECT city
FROM locations
WHERE location_id = v_location_id
FOR UPDATE;
BEGIN
-- Attempt to update before opening the cursor - this would raise an error
-- UPDATE locations SET city = 'New City' WHERE CURRENT OF c_locations; -- PLS-00306: wrong number or types of arguments in call to 'CURRENT_OF'
OPEN c_locations;
FETCH c_locations INTO v_city;
IF c_locations%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Old city name: ' || v_city);
UPDATE locations
SET city = 'Shelton, WA'
WHERE CURRENT OF c_locations;
DBMS_OUTPUT.PUT_LINE('City updated to Shelton, WA.');
ELSE
DBMS_OUTPUT.PUT_LINE('Location ' || v_location_id || ' not found.');
END IF;
CLOSE c_locations;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/
Explanation
This example highlights a common mistake: attempting to use WHERE CURRENT OF before the cursor has been opened and a row fetched. The WHERE CURRENT OF clause is context-dependent and relies on the cursor being in an active state and positioned on a row. The commented-out line shows the compile-time error (PLS-00306) you would receive. It reinforces the correct "cursor lifecycle" for "PL/SQL update operations."
Cursor Variables (REF CURSOR)
A cursor variable, also known as a REF CURSOR
, is a PL/SQL data type that holds a pointer to a result set, rather than the result set itself. Unlike explicit cursors, which are static and bound to a specific query at compile time, REF CURSOR
s are dynamic and can be opened with different queries at runtime. This flexibility makes them ideal for "dynamic SQL," "passing result sets between subprograms," and "returning multiple rows from functions." They are a powerful tool for building "flexible PL/SQL applications."
Example 1: Declaring and Opening a REF CURSOR
DECLARE
TYPE EmpCurTyp IS REF CURSOR; -- Declare a REF CURSOR type
emp_cv EmpCurTyp; -- Declare a cursor variable of that type
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
v_salary NUMBER;
v_department_id NUMBER := 90;
BEGIN
-- Open the REF CURSOR with a specific query
OPEN emp_cv FOR
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = v_department_id;
DBMS_OUTPUT.PUT_LINE('Employees in Department ' || v_department_id || ':');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
-- Fetch rows from the REF CURSOR
LOOP
FETCH emp_cv INTO v_employee_id, v_first_name, v_last_name, v_salary;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_employee_id || ' - ' || v_first_name || ' ' || v_last_name || ' ($' || v_salary || ')');
END LOOP;
CLOSE emp_cv; -- Close the REF CURSOR
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/
Explanation
This example demonstrates the basic declaration, opening, and fetching from a REF CURSOR. We first define a REF CURSOR type (EmpCurTyp) and then declare a variable (emp_cv) of that type. The key is that OPEN emp_cv FOR allows you to associate a SELECT statement with the REF CURSOR at runtime. This makes REF CURSORs much more dynamic than explicit cursors for "retrieving data in PL/SQL."
Example 2: Passing REF CURSOR as an OUT parameter
CREATE OR REPLACE PACKAGE emp_data_pkg AS
TYPE EmpCurTyp IS REF CURSOR;
PROCEDURE get_employees (p_dept_id IN NUMBER, p_emp_cur OUT EmpCurTyp);
END emp_data_pkg;
/
CREATE OR REPLACE PACKAGE BODY emp_data_pkg AS
PROCEDURE get_employees (p_dept_id IN NUMBER, p_emp_cur OUT EmpCurTyp) IS
BEGIN
OPEN p_emp_cur FOR
SELECT employee_id, first_name, last_name, email, phone_number
FROM employees
WHERE department_id = p_dept_id;
END get_employees;
END emp_data_pkg;
/
-- Anonymous block to test the package
DECLARE
TYPE EmpCurTyp IS REF CURSOR; -- Redeclare type in calling block or use package type
emp_cv EmpCurTyp;
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
v_email VARCHAR2(25);
v_phone_number VARCHAR2(20);
BEGIN
emp_data_pkg.get_employees(p_dept_id => 60, p_emp_cur => emp_cv); -- Call the procedure
DBMS_OUTPUT.PUT_LINE('Employees in Department 60 (from package):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH emp_cv INTO v_employee_id, v_first_name, v_last_name, v_email, v_phone_number;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_employee_id || ' - ' || v_first_name || ' ' || v_last_name || ' - ' || v_email);
END LOOP;
CLOSE emp_cv;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/
Explanation
One of the most powerful uses of REF CURSORs is to pass them as parameters to "PL/SQL subprograms" (procedures or functions). In this example, the get_employees procedure in emp_data_pkg opens a REF CURSOR and returns it to the calling block. This allows the calling block to fetch and process the result set, decoupling the query logic from the data consumption logic. This is fundamental for "modular PL/SQL development" and "API design."
Example 3: Dynamic SQL with REF CURSOR
DECLARE
TYPE EmpCurTyp IS REF CURSOR;
emp_cv EmpCurTyp;
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
v_table_name VARCHAR2(30) := 'employees'; -- Dynamic table name
v_column_name VARCHAR2(30) := 'first_name'; -- Dynamic column name
v_where_clause VARCHAR2(100) := ' WHERE salary > 10000 ORDER BY employee_id'; -- Dynamic WHERE clause
BEGIN
-- Construct dynamic SQL query
OPEN emp_cv FOR 'SELECT employee_id, ' || v_column_name || ', last_name FROM ' || v_table_name || v_where_clause;
DBMS_OUTPUT.PUT_LINE('Dynamic Query Results:');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH emp_cv INTO v_employee_id, v_first_name, v_last_name;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_employee_id || ' - ' || v_first_name || ' ' || v_last_name);
END LOOP;
CLOSE emp_cv;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error with dynamic query: ' || SQLERRM);
END;
/
Explanation
This example showcases how REF CURSORs facilitate "dynamic SQL" in PL/SQL. The SELECT statement passed to OPEN emp_cv FOR is constructed as a string at runtime, allowing for flexible queries where table names, column names, or WHERE clauses can change based on application logic or user input. This is invaluable for building highly configurable "data retrieval routines."
Example 4: REF CURSOR as a return type from a Function
CREATE OR REPLACE FUNCTION get_active_users_fc RETURN SYS_REFCURSOR AS
l_rc SYS_REFCURSOR;
BEGIN
OPEN l_rc FOR
SELECT user_id, username, created_date
FROM users
WHERE status = 'ACTIVE';
RETURN l_rc;
END;
/
-- Anonymous block to call the function and process the result
DECLARE
v_user_id NUMBER;
v_username VARCHAR2(30);
v_created_date DATE;
user_cur SYS_REFCURSOR; -- Using the predefined SYS_REFCURSOR
BEGIN
user_cur := get_active_users_fc; -- Call the function that returns a REF CURSOR
DBMS_OUTPUT.PUT_LINE('Active Users:');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH user_cur INTO v_user_id, v_username, v_created_date;
EXIT WHEN user_cur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_user_id || ' - ' || v_username || ' (Created: ' || TO_CHAR(v_created_date, 'YYYY-MM-DD') || ')');
END LOOP;
CLOSE user_cur;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/
Explanation
REF CURSORs can also be returned directly from "PL/SQL functions." This is a common pattern for exposing complex query results to external applications or other PL/SQL units. This example uses SYS_REFCURSOR, a predefined REF CURSOR type that is always weak (explained in the next section). This approach is popular for "reporting functions" and "data access layers."
Example 5: Error Handling with REF CURSORs
DECLARE
TYPE EmpCurTyp IS REF CURSOR;
emp_cv EmpCurTyp;
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
v_invalid_column VARCHAR2(10); -- To cause an error
BEGIN
-- Attempt to open with a non-existent table (will raise ORA-00942)
-- OPEN emp_cv FOR 'SELECT employee_id FROM non_existent_table';
-- Attempt to open with incorrect number of columns (will raise PLS-00394)
-- OPEN emp_cv FOR 'SELECT employee_id, first_name FROM employees';
-- FETCH emp_cv INTO v_employee_id; -- Will cause PLS-00394 after fetch
-- Correct usage for fetching
OPEN emp_cv FOR 'SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 10';
LOOP
-- Attempt to fetch into wrong number of variables (will raise PLS-00394 on fetch)
-- FETCH emp_cv INTO v_employee_id, v_first_name;
FETCH emp_cv INTO v_employee_id, v_first_name, v_last_name;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_employee_id || ' - ' || v_first_name || ' ' || v_last_name);
END LOOP;
CLOSE emp_cv;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error handling REF CURSOR: ' || SQLERRM);
IF emp_cv%ISOPEN THEN -- Check if cursor is open before attempting to close
CLOSE emp_cv;
END IF;
END;
/
Explanation
This example demonstrates common errors and best practices for error handling with REF CURSORs. Errors can occur during the OPEN phase (e.g., invalid SQL, non-existent table) or during the FETCH phase (e.g., mismatch in the number or data types of columns being fetched into variables). It's crucial to include exception handling and to always check emp_cv%ISOPEN before attempting to CLOSE a REF CURSOR within an exception handler to prevent further errors. This improves the "robustness of PL/SQL code."
Strong vs. Weak REF CURSORs
REF CURSOR
s can be classified as either "strong" or "weak," depending on whether their return type (the structure of the result set) is defined at compile time. This distinction is crucial for "type safety" and "flexibility" in "PL/SQL applications."
Strong REF CURSORs: These REF CURSOR
s have their return type explicitly defined in the TYPE
declaration, usually by referencing a ROWTYPE
or a specific list of columns and their data types. This provides compile-time type checking, meaning if the query opened with the strong REF CURSOR
does not match the defined type, a PLS-00394
error (or similar) will be raised during compilation or at OPEN
time. This offers greater "type safety" and helps catch errors early in the development cycle.
Weak REF CURSORs: These REF CURSOR
s do not have a predefined return type. They can be opened with any SELECT
statement, regardless of the number or data types of the columns returned. The type checking is performed only at runtime during the FETCH
operation. If the fetched columns don't match the variables provided, a runtime error (e.g., PLS-00394
) will occur. SYS_REFCURSOR
is a predefined weak REF CURSOR
type. Weak REF CURSOR
s offer maximum "flexibility" but at the cost of less "compile-time type checking."
Example 1: Strong REF CURSOR
DECLARE
-- Define a strong REF CURSOR type based on employees%ROWTYPE
TYPE EmpStrongCurTyp IS REF CURSOR RETURN employees%ROWTYPE;
emp_cv EmpStrongCurTyp;
emp_rec employees%ROWTYPE; -- Record variable to match the strong type
BEGIN
-- This OPEN statement matches the strong type (employees%ROWTYPE)
OPEN emp_cv FOR
SELECT *
FROM employees
WHERE department_id = 80;
DBMS_OUTPUT.PUT_LINE('Employees in Department 80 (Strong REF CURSOR):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH emp_cv INTO emp_rec;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(emp_rec.employee_id || ' - ' || emp_rec.first_name || ' ' || emp_rec.last_name);
END LOOP;
CLOSE emp_cv;
-- This would cause a compile-time error (PLS-00394) because the return types don't match
-- OPEN emp_cv FOR SELECT employee_id, first_name FROM jobs;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error with strong REF CURSOR: ' || SQLERRM);
END;
/
Explanation
This example defines EmpStrongCurTyp as a strong REF CURSOR because its RETURN clause specifies the employees%ROWTYPE. This means any query opened with emp_cv must return columns compatible with the employees table structure. The commented-out OPEN statement demonstrates how attempting to open it with an incompatible query would result in a PLS-00394 error during compilation, providing "compile-time type safety."
Example 2: Weak REF CURSOR (using TYPE REF CURSOR)
DECLARE
-- Define a weak REF CURSOR type (no RETURN clause)
TYPE WeakCurTyp IS REF CURSOR;
weak_cv WeakCurTyp;
v_col1 VARCHAR2(50);
v_col2 NUMBER;
BEGIN
-- Open with one query (2 columns, different types)
OPEN weak_cv FOR
SELECT first_name, salary
FROM employees
WHERE salary > 15000;
DBMS_OUTPUT.PUT_LINE('High Salary Employees (Weak REF CURSOR 1):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH weak_cv INTO v_col1, v_col2; -- Matches types
EXIT WHEN weak_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_col1 || ' - $' || v_col2);
END LOOP;
CLOSE weak_cv;
-- Re-open with a different query (2 columns, different types)
OPEN weak_cv FOR
SELECT department_name, location_id
FROM departments
WHERE location_id = 1700;
DBMS_OUTPUT.PUT_LINE(CHR(10) || 'Departments in Location 1700 (Weak REF CURSOR 2):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH weak_cv INTO v_col1, v_col2; -- Still matches types
EXIT WHEN weak_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_col1 || ' - ' || v_col2);
END LOOP;
CLOSE weak_cv;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error with weak REF CURSOR: ' || SQLERRM);
END;
/
Explanation
This example demonstrates the flexibility of a weak REF CURSOR. The WeakCurTyp is defined without a RETURN clause. This allows weak_cv to be opened with completely different SELECT statements, returning different numbers and types of columns. The FETCH operations succeed as long as the variables provided match the columns returned by the current query. This flexibility makes weak REF CURSORs suitable for highly "generic data access."
Example 3: Weak REF CURSOR using SYS_REFCURSOR
DECLARE
-- SYS_REFCURSOR is a predefined weak REF CURSOR type
sys_rc SYS_REFCURSOR;
v_job_title VARCHAR2(35);
v_min_salary NUMBER;
v_max_salary NUMBER;
BEGIN
OPEN sys_rc FOR
SELECT job_title, min_salary, max_salary
FROM jobs
WHERE min_salary > 5000;
DBMS_OUTPUT.PUT_LINE('Job Titles (SYS_REFCURSOR):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH sys_rc INTO v_job_title, v_min_salary, v_max_salary;
EXIT WHEN sys_rc%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_job_title || ' (Min: $' || v_min_salary || ', Max: $' || v_max_salary || ')');
END LOOP;
CLOSE sys_rc;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error with SYS_REFCURSOR: ' || SQLERRM);
END;
/
Explanation
SYS_REFCURSOR is a globally available predefined weak REF CURSOR type. It behaves identically to a user-defined weak REF CURSOR type. Its primary advantage is convenience, as you don't need to declare your own TYPE ... IS REF CURSOR; when a weak REF CURSOR is sufficient. It's frequently used when passing REF CURSORs between "PL/SQL and Java" or other "external programming languages" through JDBC/ODBC.
Example 4: Runtime Errors with Weak REF CURSOR Mismatches
DECLARE
TYPE WeakCurTyp IS REF CURSOR;
weak_cv WeakCurTyp;
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
BEGIN
OPEN weak_cv FOR
SELECT employee_id, first_name, last_name, salary -- Query returns 4 columns
FROM employees
WHERE ROWNUM <= 2;
DBMS_OUTPUT.PUT_LINE('Attempting to fetch 4 columns into 2 variables:');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
-- This FETCH will cause a PLS-00394 runtime error
FETCH weak_cv INTO v_employee_id, v_first_name;
EXIT WHEN weak_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_employee_id || ' - ' || v_first_name);
END LOOP;
CLOSE weak_cv;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Caught error: ' || SQLERRM);
IF weak_cv%ISOPEN THEN
CLOSE weak_cv;
END IF;
END;
/
Explanation
This crucial example illustrates the primary drawback of weak REF CURSORs: lack of compile-time type checking. The OPEN statement succeeds, but the FETCH statement attempts to fetch four columns into only two variables, leading to a PLS-00394 error at runtime. This highlights the importance of careful programming and robust error handling when using "weakly typed cursors."
Example 5: Choosing Between Strong and Weak REF CURSORs
-- Scenario: A procedure that fetches employee data for different departments,
-- but the exact columns returned might vary based on a parameter.
CREATE OR REPLACE PROCEDURE get_flexible_employee_data (
p_department_id IN NUMBER,
p_include_email IN BOOLEAN,
p_employee_data OUT SYS_REFCURSOR -- Use weak REF CURSOR for flexibility
) AS
l_sql_stmt VARCHAR2(500);
BEGIN
l_sql_stmt := 'SELECT employee_id, first_name, last_name';
IF p_include_email THEN
l_sql_stmt := l_sql_stmt || ', email';
END IF;
l_sql_stmt := l_sql_stmt || ' FROM employees WHERE department_id = :1';
OPEN p_employee_data FOR l_sql_stmt USING p_department_id;
END;
/
-- Test Block 1: Get employees including email
DECLARE
employee_cur SYS_REFCURSOR;
v_emp_id NUMBER;
v_fname VARCHAR2(20);
v_lname VARCHAR2(25);
v_email VARCHAR2(25); -- Variable for email
BEGIN
DBMS_OUTPUT.PUT_LINE('Employees with Email (Dept 50):');
DBMS_OUTPUT.PUT_LINE('-------------------------------');
get_flexible_employee_data(50, TRUE, employee_cur);
LOOP
FETCH employee_cur INTO v_emp_id, v_fname, v_lname, v_email;
EXIT WHEN employee_cur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_emp_id || ' - ' || v_fname || ' ' || v_lname || ' (' || v_email || ')');
END LOOP;
CLOSE employee_cur;
DBMS_OUTPUT.PUT_LINE(CHR(10) || 'Employees without Email (Dept 20):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
-- Test Block 2: Get employees without email
get_flexible_employee_data(20, FALSE, employee_cur);
LOOP
-- Note: We must fetch into the correct number of variables for this call
FETCH employee_cur INTO v_emp_id, v_fname, v_lname;
EXIT WHEN employee_cur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_emp_id || ' - ' || v_fname || ' ' || v_lname);
END LOOP;
CLOSE employee_cur;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error in main block: ' || SQLERRM);
END;
/
Explanation
This example provides a practical scenario for choosing between strong and weak REF CURSORs. If the exact structure of the result set can vary (e.g., conditionally including or excluding columns), a weak REF CURSOR (like SYS_REFCURSOR) is necessary due to its flexibility. The calling code then needs to be aware of the exact columns being returned in each invocation to fetch correctly. If the result set structure is always fixed, a strong REF CURSOR is generally preferred for its "compile-time type safety."
Using REF CURSORs as Subprogram Parameters
As demonstrated in previous sections, REF CURSOR
s are commonly used to pass result sets between PL/SQL subprograms (procedures and functions) or between PL/SQL and client applications (like Java or .NET). This enables efficient data transfer without having to store the entire result set in memory on the server side before sending it to the client.
Example 1: REF CURSOR as an IN parameter (for internal processing)
DECLARE
TYPE EmpCurTyp IS REF CURSOR;
emp_cv EmpCurTyp;
PROCEDURE process_employees (p_employee_cursor IN EmpCurTyp) IS
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
BEGIN
DBMS_OUTPUT.PUT_LINE('Processing employees from IN cursor:');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH p_employee_cursor INTO v_employee_id, v_first_name, v_last_name;
EXIT WHEN p_employee_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ID: ' || v_employee_id || ', Name: ' || v_first_name || ' ' || v_last_name);
END LOOP;
-- Note: The cursor is still open in the calling block after this procedure
-- You should not close an IN REF CURSOR unless specified by design.
END process_employees;
BEGIN
OPEN emp_cv FOR
SELECT employee_id, first_name, last_name
FROM employees
WHERE ROWNUM <= 5; -- Just get first 5 for example
process_employees(emp_cv); -- Pass the opened REF CURSOR
-- You can continue fetching from the cursor here if needed
DBMS_OUTPUT.PUT_LINE(CHR(10) || 'Continuing from main block:');
LOOP
FETCH emp_cv INTO v_employee_id, v_first_name, v_last_name;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('Main: ' || v_employee_id || ' - ' || v_first_name);
END LOOP;
CLOSE emp_cv;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
IF emp_cv%ISOPEN THEN
CLOSE emp_cv;
END IF;
END;
/
Explanation
When a REF CURSOR is passed as an IN parameter, the subprogram receives a copy of the pointer to the result set. It can then fetch rows from this cursor. Crucially, the original REF CURSOR in the calling environment remains open and can continue to be used after the subprogram finishes. This is useful for "chained processing" or when a subprogram needs to perform an initial read without closing the cursor for the caller.
Example 2: REF CURSOR as an OUT parameter (most common for returning result sets)
CREATE OR REPLACE PROCEDURE get_employees_by_job (
p_job_id IN VARCHAR2,
p_emp_data OUT SYS_REFCURSOR -- OUT parameter to return the cursor
) AS
BEGIN
OPEN p_emp_data FOR
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE job_id = p_job_id;
END;
/
-- Anonymous block to call the procedure
DECLARE
emp_rc SYS_REFCURSOR;
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
v_salary NUMBER;
BEGIN
DBMS_OUTPUT.PUT_LINE('Employees with Job ID AD_VP:');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
get_employees_by_job('AD_VP', emp_rc); -- Call procedure, emp_rc receives the cursor
LOOP
FETCH emp_rc INTO v_employee_id, v_first_name, v_last_name, v_salary;
EXIT WHEN emp_rc%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_employee_id || ' - ' || v_first_name || ' ' || v_last_name || ' ($' || v_salary || ')');
END LOOP;
CLOSE emp_rc;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
IF emp_rc%ISOPEN THEN
CLOSE emp_rc;
END IF;
END;
/
Explanation
Passing a REF CURSOR as an OUT parameter is the most common and powerful way to return a result set from a "PL/SQL procedure" or function. The subprogram opens the REF CURSOR, populates it with a query, and then passes this opened cursor back to the calling environment. The calling block then takes responsibility for fetching rows and closing the cursor. This is the cornerstone of "returning dynamic result sets" from stored procedures.
Example 3: REF CURSOR as an IN OUT parameter (less common, for filtering/modifying)
DECLARE
TYPE EmpCurTyp IS REF CURSOR;
emp_cv EmpCurTyp;
PROCEDURE filter_employees (p_employee_cursor IN OUT EmpCurTyp, p_min_salary IN NUMBER) IS
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
v_salary NUMBER;
l_temp_cur EmpCurTyp; -- Temporary cursor for re-opening
BEGIN
-- First, process some rows from the IN cursor if needed
DBMS_OUTPUT.PUT_LINE('Filtering employees (original cursor fetches 2):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
FOR i IN 1..2 LOOP -- Fetch first two to show current position
FETCH p_employee_cursor INTO v_employee_id, v_first_name, v_last_name, v_salary;
EXIT WHEN p_employee_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('Fetched (initial): ' || v_first_name || ' (Salary: ' || v_salary || ')');
END LOOP;
-- Now, open the IN OUT cursor with a new query, potentially based on previous fetches
-- Or, just use it to re-initialize with a different filter
OPEN l_temp_cur FOR
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE salary >= p_min_salary
ORDER BY salary DESC;
-- Assign the new cursor to the IN OUT parameter
p_employee_cursor := l_temp_cur;
DBMS_OUTPUT.PUT_LINE(CHR(10) || 'Cursor re-opened with new filter (min salary: ' || p_min_salary || '):');
END filter_employees;
v_employee_id NUMBER;
v_first_name VARCHAR2(20);
v_last_name VARCHAR2(25);
v_salary NUMBER;
BEGIN
-- Initial open of the cursor
OPEN emp_cv FOR
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = 90
ORDER BY employee_id;
filter_employees(emp_cv, 10000); -- Pass IN OUT
-- Now fetch from the re-opened cursor in the main block
LOOP
FETCH emp_cv INTO v_employee_id, v_first_name, v_last_name, v_salary;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('Final fetch: ' || v_employee_id || ' - ' || v_first_name || ' (' || v_salary || ')');
END LOOP;
CLOSE emp_cv;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
IF emp_cv%ISOPEN THEN
CLOSE emp_cv;
END IF;
END;
/
Explanation
An IN OUT REF CURSOR allows a subprogram to both consume rows from an existing cursor and/or re-open it with a new query, thereby changing the result set that the calling environment will subsequently access. While less common than OUT parameters for simple data retrieval, IN OUT can be useful in complex scenarios where a cursor needs to be "transformed" or "refined" by multiple "PL/SQL components."
Example 4: Returning Multiple REF CURSORs from a Procedure
CREATE OR REPLACE PROCEDURE get_dept_and_emp_data (
p_department_id IN NUMBER,
p_department_cur OUT SYS_REFCURSOR,
p_employee_cur OUT SYS_REFCURSOR
) AS
BEGIN
-- Open cursor for department data
OPEN p_department_cur FOR
SELECT department_id, department_name, location_id
FROM departments
WHERE department_id = p_department_id;
-- Open cursor for employee data in that department
OPEN p_employee_cur FOR
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = p_department_id;
END;
/
-- Anonymous block to call the procedure and process both cursors
DECLARE
dept_rc SYS_REFCURSOR;
emp_rc SYS_REFCURSOR;
v_dept_id NUMBER;
v_dept_name VARCHAR2(30);
v_location_id NUMBER;
v_emp_id NUMBER;
v_fname VARCHAR2(20);
v_lname VARCHAR2(25);
v_salary NUMBER;
BEGIN
get_dept_and_emp_data(90, dept_rc, emp_rc);
DBMS_OUTPUT.PUT_LINE('Department Data (Dept 90):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH dept_rc INTO v_dept_id, v_dept_name, v_location_id;
EXIT WHEN dept_rc%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ID: ' || v_dept_id || ', Name: ' || v_dept_name || ', Location: ' || v_location_id);
END LOOP;
CLOSE dept_rc;
DBMS_OUTPUT.PUT_LINE(CHR(10) || 'Employee Data (Dept 90):');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
LOOP
FETCH emp_rc INTO v_emp_id, v_fname, v_lname, v_salary;
EXIT WHEN emp_rc%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ID: ' || v_emp_id || ', Name: ' || v_fname || ' ' || v_lname || ', Salary: $' || v_salary);
END LOOP;
CLOSE emp_rc;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
IF dept_rc%ISOPEN THEN CLOSE dept_rc; END IF;
IF emp_rc%ISOPEN THEN CLOSE emp_rc; END IF;
END;
/
Explanation
A single PL/SQL procedure can return multiple REF CURSORs as OUT parameters, allowing it to provide several related result sets in a single call. This is particularly useful when retrieving "master-detail data" or "related datasets" from the database in a single round trip, improving "application performance" and reducing "network overhead."
Example 5: Using REF CURSOR parameters with Polymorphic Table Functions (Advanced)
-- This is an advanced concept requiring Oracle 12c or later and specific privileges.
-- It's used when you want a function to return a table whose structure depends on input.
-- This example provides a conceptual outline as full implementation is complex.
-- 1. Create a package for the PTF (Polymorphic Table Function)
/*
CREATE OR REPLACE PACKAGE my_data_transform_pkg AS
FUNCTION transform_data (p_input_cur IN SYS_REFCURSOR)
RETURN SYS.ANYDATASET PIPELINED;
END my_data_transform_pkg;
/
CREATE OR REPLACE PACKAGE BODY my_data_transform_pkg AS
FUNCTION transform_data (p_input_cur IN SYS_REFCURSOR)
RETURN SYS.ANYDATASET PIPELINED
IS
-- Define dynamic types based on input cursor
TYPE RecTyp IS RECORD (col1 VARCHAR2(100), col2 NUMBER); -- Example structure
rec RecTyp;
-- Variables to hold input cursor data
v_col1_in VARCHAR2(200);
v_col2_in NUMBER;
-- Describe call variables
desctab DBMS_SQL.DESC_TAB;
colcnt NUMBER;
-- Cursor operations
l_curid INTEGER := DBMS_SQL.TO_CURSOR_NUMBER(p_input_cur);
l_row_count NUMBER := 0;
BEGIN
-- Describe the input cursor
DBMS_SQL.DESCRIBE_COLUMNS(l_curid, colcnt, desctab);
-- Loop through the input cursor, transform data, and pipe out
LOOP
FETCH p_input_cur INTO v_col1_in, v_col2_in; -- Fetch based on input cursor's actual structure
EXIT WHEN p_input_cur%NOTFOUND;
-- Apply transformation logic
rec.col1 := 'Processed: ' || v_col1_in;
rec.col2 := v_col2_in * 10;
PIPE ROW (rec);
l_row_count := l_row_count + 1;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(l_curid); -- Close the internal cursor number for the REF CURSOR
RETURN;
END;
END my_data_transform_pkg;
/
*/
-- 2. How it would be used (conceptual)
/*
SELECT * FROM TABLE(my_data_transform_pkg.transform_data(
CURSOR(SELECT employee_id, salary FROM employees WHERE department_id = 50)
));
*/
-- Explanation in simple terms:
-- This advanced usage isn't a direct "REF CURSOR as parameter" in the traditional sense
-- but showcases how REF CURSORs can be the source for powerful PTFs.
-- A PTF is a function that takes a cursor (or an implicit query, which becomes a cursor)
-- as input and returns a table, where the output table's structure can adapt
-- based on the input cursor's structure.
-- This allows for highly generic data processing functions that operate on
-- arbitrary result sets.
Explanation
This example delves into a very advanced concept: Polymorphic Table Functions (PTFs). While not a straightforward REF CURSOR parameter example, it illustrates how REF CURSORs (or more accurately, the underlying cursor number of a query) can be passed to special functions that dynamically determine their output table structure based on the input cursor. This allows for extremely flexible "data transformation pipelines" and "generic data processing functions" in Oracle. This is beyond typical REF CURSOR usage but highlights their foundational role in advanced "Oracle database features."