Advanced Cursors


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 CURSORs 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 CURSORs 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 CURSORs 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 CURSORs 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 CURSORs 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 CURSORs 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."