PL/SQL Support in HeliosDB
PL/SQL Support in HeliosDB
Comprehensive documentation for PL/SQL support in HeliosDB, including supported constructs, conversion patterns for unsupported features, and working code examples.
Table of Contents
- Overview
- Supported PL/SQL Constructs
- Partially Supported Features
- Unsupported Features
- Conversion Patterns
- Working Code Examples
- Best Practices
- Troubleshooting
Overview
PL/SQL Compatibility Level: 40%
HeliosDB provides foundational PL/SQL support for executing procedural code within the database. The interpreter handles common PL/SQL constructs used in typical business applications while some advanced Oracle-specific features require conversion.
Key Capabilities
| Capability | Status | Notes |
|---|---|---|
| Anonymous blocks | Supported | DECLARE/BEGIN/END |
| Stored procedures | Supported | CREATE PROCEDURE |
| Functions | Supported | CREATE FUNCTION |
| Packages | Partial | Basic spec/body |
| Triggers | Partial | Row-level DML |
| Cursors | Supported | Explicit and implicit |
| Exception handling | Supported | WHEN/RAISE |
| Control flow | Supported | IF, CASE, LOOP |
| Collections | Partial | Basic nested tables |
| BULK operations | Partial | Simple BULK COLLECT |
Supported PL/SQL Constructs
Variables and Constants
DECLARE -- Scalar variables v_number NUMBER := 100; v_string VARCHAR2(100) := 'Hello'; v_date DATE := SYSDATE; v_boolean BOOLEAN := TRUE; v_timestamp TIMESTAMP := SYSTIMESTAMP;
-- Constants c_tax_rate CONSTANT NUMBER := 0.0825; c_max_value CONSTANT NUMBER := 1000000;
-- Anchored types v_emp_id employees.employee_id%TYPE; v_emp_row employees%ROWTYPE;
-- NULL initialization v_nullable VARCHAR2(50) := NULL;BEGIN -- Variable assignment v_number := v_number + 50; v_string := v_string || ' World';
-- Using constants v_number := v_number * (1 + c_tax_rate);
DBMS_OUTPUT.PUT_LINE('Result: ' || v_number);END;/Control Flow Statements
DECLARE v_grade VARCHAR2(1) := 'B'; v_score NUMBER := 85; v_message VARCHAR2(100);BEGIN -- IF-THEN-ELSE IF v_score >= 90 THEN v_message := 'Excellent'; ELSIF v_score >= 80 THEN v_message := 'Good'; ELSIF v_score >= 70 THEN v_message := 'Average'; ELSE v_message := 'Needs Improvement'; END IF;
DBMS_OUTPUT.PUT_LINE('Score message: ' || v_message);
-- Simple CASE v_message := CASE v_grade WHEN 'A' THEN 'Outstanding' WHEN 'B' THEN 'Above Average' WHEN 'C' THEN 'Satisfactory' WHEN 'D' THEN 'Below Average' ELSE 'Failing' END;
DBMS_OUTPUT.PUT_LINE('Grade message: ' || v_message);
-- Searched CASE v_message := CASE WHEN v_score >= 90 THEN 'A Grade' WHEN v_score >= 80 THEN 'B Grade' WHEN v_score >= 70 THEN 'C Grade' ELSE 'Below C' END;
DBMS_OUTPUT.PUT_LINE('Final: ' || v_message);END;/Loops
DECLARE v_counter NUMBER := 1; v_sum NUMBER := 0;BEGIN -- Simple LOOP LOOP v_sum := v_sum + v_counter; v_counter := v_counter + 1; EXIT WHEN v_counter > 10; END LOOP; DBMS_OUTPUT.PUT_LINE('Simple loop sum: ' || v_sum);
-- WHILE loop v_counter := 1; v_sum := 0; WHILE v_counter <= 10 LOOP v_sum := v_sum + v_counter; v_counter := v_counter + 1; END LOOP; DBMS_OUTPUT.PUT_LINE('While loop sum: ' || v_sum);
-- FOR loop (numeric) v_sum := 0; FOR i IN 1..10 LOOP v_sum := v_sum + i; END LOOP; DBMS_OUTPUT.PUT_LINE('For loop sum: ' || v_sum);
-- FOR loop REVERSE FOR i IN REVERSE 1..5 LOOP DBMS_OUTPUT.PUT_LINE('Countdown: ' || i); END LOOP;
-- Cursor FOR loop FOR emp IN (SELECT employee_id, first_name FROM employees WHERE ROWNUM <= 5) LOOP DBMS_OUTPUT.PUT_LINE('Employee: ' || emp.employee_id || ' - ' || emp.first_name); END LOOP;
-- CONTINUE statement FOR i IN 1..10 LOOP IF MOD(i, 2) = 0 THEN CONTINUE; -- Skip even numbers END IF; DBMS_OUTPUT.PUT_LINE('Odd: ' || i); END LOOP;END;/Cursors
DECLARE -- Explicit cursor declaration CURSOR emp_cursor IS SELECT employee_id, first_name, last_name, salary FROM employees WHERE department_id = 10 ORDER BY salary DESC;
-- Cursor with parameters CURSOR dept_cursor(p_dept_id NUMBER) IS SELECT employee_id, first_name, salary FROM employees WHERE department_id = p_dept_id;
-- Variables v_emp emp_cursor%ROWTYPE; v_total_salary NUMBER := 0; v_count NUMBER := 0;BEGIN -- Explicit cursor operations OPEN emp_cursor; LOOP FETCH emp_cursor INTO v_emp; EXIT WHEN emp_cursor%NOTFOUND;
v_total_salary := v_total_salary + v_emp.salary; v_count := v_count + 1;
DBMS_OUTPUT.PUT_LINE(v_emp.first_name || ' ' || v_emp.last_name || ': $' || v_emp.salary); END LOOP;
DBMS_OUTPUT.PUT_LINE('Total: $' || v_total_salary); DBMS_OUTPUT.PUT_LINE('Count: ' || v_count); DBMS_OUTPUT.PUT_LINE('Found: ' || emp_cursor%ROWCOUNT);
CLOSE emp_cursor;
-- Cursor with parameter OPEN dept_cursor(20); LOOP FETCH dept_cursor INTO v_emp.employee_id, v_emp.first_name, v_emp.salary; EXIT WHEN dept_cursor%NOTFOUND; DBMS_OUTPUT.PUT_LINE('Dept 20: ' || v_emp.first_name); END LOOP; CLOSE dept_cursor;
-- Implicit cursor attributes UPDATE employees SET salary = salary * 1.01 WHERE department_id = 10; DBMS_OUTPUT.PUT_LINE('Updated rows: ' || SQL%ROWCOUNT);
IF SQL%FOUND THEN DBMS_OUTPUT.PUT_LINE('Update was successful'); END IF;END;/Exception Handling
DECLARE v_salary NUMBER; v_name VARCHAR2(100);
-- User-defined exceptions e_salary_too_high EXCEPTION; e_employee_not_found EXCEPTION; PRAGMA EXCEPTION_INIT(e_employee_not_found, -20001);BEGIN -- Try to get employee data BEGIN SELECT first_name || ' ' || last_name, salary INTO v_name, v_salary FROM employees WHERE employee_id = 9999; -- May not exist
EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('Employee not found in inner block'); v_name := 'Unknown'; v_salary := 0; END;
-- Check salary limit IF v_salary > 500000 THEN RAISE e_salary_too_high; END IF;
DBMS_OUTPUT.PUT_LINE('Employee: ' || v_name); DBMS_OUTPUT.PUT_LINE('Salary: $' || v_salary);
EXCEPTION WHEN e_salary_too_high THEN DBMS_OUTPUT.PUT_LINE('Salary exceeds maximum allowed');
WHEN DUP_VAL_ON_INDEX THEN DBMS_OUTPUT.PUT_LINE('Duplicate value error');
WHEN VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE('Value conversion error');
WHEN ZERO_DIVIDE THEN DBMS_OUTPUT.PUT_LINE('Division by zero');
WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Error code: ' || SQLCODE); DBMS_OUTPUT.PUT_LINE('Error message: ' || SQLERRM); DBMS_OUTPUT.PUT_LINE('Backtrace: ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE); RAISE; -- Re-raise the exceptionEND;/Procedures and Functions
-- Procedure with IN, OUT, and IN OUT parametersCREATE OR REPLACE PROCEDURE calculate_employee_stats( p_dept_id IN NUMBER, p_total_sal OUT NUMBER, p_avg_sal OUT NUMBER, p_emp_count IN OUT NUMBER) ASBEGIN SELECT SUM(salary), AVG(salary), COUNT(*) INTO p_total_sal, p_avg_sal, p_emp_count FROM employees WHERE department_id = p_dept_id;
IF p_emp_count = 0 THEN p_total_sal := 0; p_avg_sal := 0; END IF;EXCEPTION WHEN OTHERS THEN p_total_sal := NULL; p_avg_sal := NULL; p_emp_count := 0; RAISE;END calculate_employee_stats;/
-- Function with default parameterCREATE OR REPLACE FUNCTION format_salary( p_salary IN NUMBER, p_currency IN VARCHAR2 DEFAULT '$', p_decimals IN NUMBER DEFAULT 2) RETURN VARCHAR2 AS v_format VARCHAR2(50);BEGIN IF p_salary IS NULL THEN RETURN p_currency || '0.00'; END IF;
-- Build format string v_format := p_currency || TO_CHAR(ROUND(p_salary, p_decimals), 'FM999,999,999,990.' || RPAD('0', p_decimals, '0'));
RETURN v_format;END format_salary;/
-- Using the procedure and functionDECLARE v_total NUMBER; v_avg NUMBER; v_count NUMBER := 0;BEGIN calculate_employee_stats(10, v_total, v_avg, v_count);
DBMS_OUTPUT.PUT_LINE('Department 10 Statistics:'); DBMS_OUTPUT.PUT_LINE(' Total Salary: ' || format_salary(v_total)); DBMS_OUTPUT.PUT_LINE(' Average Salary: ' || format_salary(v_avg)); DBMS_OUTPUT.PUT_LINE(' Employee Count: ' || v_count);END;/Basic Packages
-- Package SpecificationCREATE OR REPLACE PACKAGE hr_utils AS -- Constants c_min_salary CONSTANT NUMBER := 30000; c_max_salary CONSTANT NUMBER := 500000;
-- Type declarations TYPE salary_record IS RECORD ( employee_id NUMBER, salary NUMBER, department_id NUMBER );
-- Function declarations FUNCTION validate_salary(p_salary NUMBER) RETURN BOOLEAN; FUNCTION get_tax_rate(p_salary NUMBER) RETURN NUMBER;
-- Procedure declarations PROCEDURE give_raise( p_emp_id IN NUMBER, p_percent IN NUMBER, p_new_salary OUT NUMBER );
END hr_utils;/
-- Package BodyCREATE OR REPLACE PACKAGE BODY hr_utils AS
-- Private variable g_last_updated DATE := SYSDATE;
-- Implement functions FUNCTION validate_salary(p_salary NUMBER) RETURN BOOLEAN IS BEGIN RETURN p_salary >= c_min_salary AND p_salary <= c_max_salary; END validate_salary;
FUNCTION get_tax_rate(p_salary NUMBER) RETURN NUMBER IS BEGIN RETURN CASE WHEN p_salary <= 50000 THEN 0.15 WHEN p_salary <= 100000 THEN 0.25 WHEN p_salary <= 200000 THEN 0.30 ELSE 0.35 END; END get_tax_rate;
-- Implement procedure PROCEDURE give_raise( p_emp_id IN NUMBER, p_percent IN NUMBER, p_new_salary OUT NUMBER ) AS v_current_salary NUMBER; BEGIN SELECT salary INTO v_current_salary FROM employees WHERE employee_id = p_emp_id;
p_new_salary := v_current_salary * (1 + p_percent / 100);
IF NOT validate_salary(p_new_salary) THEN RAISE_APPLICATION_ERROR(-20001, 'Salary out of range'); END IF;
UPDATE employees SET salary = p_new_salary WHERE employee_id = p_emp_id;
g_last_updated := SYSDATE; COMMIT; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20002, 'Employee not found'); END give_raise;
END hr_utils;/Row-Level Triggers
-- Before insert triggerCREATE OR REPLACE TRIGGER employees_before_insertBEFORE INSERT ON employeesFOR EACH ROWBEGIN -- Auto-generate ID if not provided IF :NEW.employee_id IS NULL THEN SELECT employee_seq.NEXTVAL INTO :NEW.employee_id FROM DUAL; END IF;
-- Normalize email :NEW.email := LOWER(:NEW.email);
-- Set audit fields :NEW.created_at := SYSDATE; :NEW.created_by := USER;END;/
-- After update trigger for auditingCREATE OR REPLACE TRIGGER employees_auditAFTER UPDATE ON employeesFOR EACH ROWWHEN (OLD.salary != NEW.salary)BEGIN INSERT INTO salary_audit ( employee_id, old_salary, new_salary, change_date, changed_by ) VALUES ( :NEW.employee_id, :OLD.salary, :NEW.salary, SYSDATE, USER );END;/
-- Using INSERTING, UPDATING, DELETINGCREATE OR REPLACE TRIGGER employees_dmlAFTER INSERT OR UPDATE OR DELETE ON employeesFOR EACH ROWBEGIN IF INSERTING THEN INSERT INTO audit_log (action, table_name, record_id, action_date) VALUES ('INSERT', 'EMPLOYEES', :NEW.employee_id, SYSDATE); ELSIF UPDATING THEN INSERT INTO audit_log (action, table_name, record_id, action_date) VALUES ('UPDATE', 'EMPLOYEES', :NEW.employee_id, SYSDATE); ELSIF DELETING THEN INSERT INTO audit_log (action, table_name, record_id, action_date) VALUES ('DELETE', 'EMPLOYEES', :OLD.employee_id, SYSDATE); END IF;END;/Partially Supported Features
BULK COLLECT (Simple Cases)
DECLARE TYPE emp_id_tab IS TABLE OF employees.employee_id%TYPE; TYPE salary_tab IS TABLE OF employees.salary%TYPE;
v_emp_ids emp_id_tab; v_salaries salary_tab;BEGIN -- Simple BULK COLLECT SELECT employee_id, salary BULK COLLECT INTO v_emp_ids, v_salaries FROM employees WHERE department_id = 10;
FOR i IN 1..v_emp_ids.COUNT LOOP DBMS_OUTPUT.PUT_LINE('Emp ' || v_emp_ids(i) || ': $' || v_salaries(i)); END LOOP;END;/FORALL (Basic Operations)
DECLARE TYPE emp_id_tab IS TABLE OF NUMBER; v_emp_ids emp_id_tab := emp_id_tab(101, 102, 103, 104, 105);BEGIN -- Basic FORALL update FORALL i IN 1..v_emp_ids.COUNT UPDATE employees SET salary = salary * 1.05 WHERE employee_id = v_emp_ids(i);
DBMS_OUTPUT.PUT_LINE('Updated ' || SQL%ROWCOUNT || ' rows'); COMMIT;END;/REF CURSOR (Weak Cursors)
CREATE OR REPLACE PROCEDURE get_employees( p_dept_id IN NUMBER, p_cursor OUT SYS_REFCURSOR) ASBEGIN OPEN p_cursor FOR SELECT employee_id, first_name, last_name, salary FROM employees WHERE department_id = p_dept_id ORDER BY salary DESC;END;/
-- Using the REF CURSORDECLARE v_cursor SYS_REFCURSOR; v_emp_id NUMBER; v_name VARCHAR2(100); v_salary NUMBER;BEGIN get_employees(10, v_cursor);
LOOP FETCH v_cursor INTO v_emp_id, v_name, v_salary; EXIT WHEN v_cursor%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_emp_id || ': ' || v_name || ' - $' || v_salary); END LOOP;
CLOSE v_cursor;END;/Collections (Nested Tables)
DECLARE -- Nested table type TYPE number_table IS TABLE OF NUMBER; TYPE string_table IS TABLE OF VARCHAR2(100);
v_numbers number_table := number_table(10, 20, 30, 40, 50); v_names string_table := string_table();BEGIN -- Collection methods DBMS_OUTPUT.PUT_LINE('Count: ' || v_numbers.COUNT); DBMS_OUTPUT.PUT_LINE('First: ' || v_numbers.FIRST); DBMS_OUTPUT.PUT_LINE('Last: ' || v_numbers.LAST);
-- Check existence IF v_numbers.EXISTS(3) THEN DBMS_OUTPUT.PUT_LINE('Element 3: ' || v_numbers(3)); END IF;
-- EXTEND and add elements v_names.EXTEND(3); v_names(1) := 'John'; v_names(2) := 'Jane'; v_names(3) := 'Bob';
-- Iterate FOR i IN v_names.FIRST..v_names.LAST LOOP DBMS_OUTPUT.PUT_LINE('Name ' || i || ': ' || v_names(i)); END LOOP;
-- DELETE element v_numbers.DELETE(3); -- Delete element at index 3
-- Iterate remaining DBMS_OUTPUT.PUT_LINE('After delete:'); FOR i IN v_numbers.FIRST..v_numbers.LAST LOOP IF v_numbers.EXISTS(i) THEN DBMS_OUTPUT.PUT_LINE(' ' || i || ': ' || v_numbers(i)); END IF; END LOOP;END;/Unsupported Features
Autonomous Transactions
Not Supported: PRAGMA AUTONOMOUS_TRANSACTION
Workaround: Use separate connections or application-level transaction management.
-- NOT SUPPORTED:-- CREATE PROCEDURE log_error(p_msg VARCHAR2) AS-- PRAGMA AUTONOMOUS_TRANSACTION;-- BEGIN-- INSERT INTO error_log VALUES (SYSDATE, p_msg);-- COMMIT; -- Independent commit-- END;
-- WORKAROUND: Log to a separate table after main transactionCREATE OR REPLACE PROCEDURE do_work_with_logging AS v_error_msg VARCHAR2(4000);BEGIN -- Main work BEGIN UPDATE employees SET salary = salary * 1.1; COMMIT; EXCEPTION WHEN OTHERS THEN v_error_msg := SQLERRM; ROLLBACK; -- Log after rollback completes INSERT INTO error_log (log_date, message) VALUES (SYSDATE, v_error_msg); COMMIT; RAISE; END;END;/Pipelined Functions
Not Supported: PIPELINED keyword
Workaround: Return collection or use regular cursor.
-- NOT SUPPORTED:-- CREATE FUNCTION get_numbers RETURN number_tab PIPELINED AS-- BEGIN-- FOR i IN 1..10 LOOP-- PIPE ROW(i);-- END LOOP;-- END;
-- WORKAROUND: Return collectionCREATE OR REPLACE FUNCTION get_numbers RETURN number_tab AS v_result number_tab := number_tab();BEGIN FOR i IN 1..10 LOOP v_result.EXTEND; v_result(v_result.COUNT) := i; END LOOP; RETURN v_result;END;/Compound Triggers
Not Supported: COMPOUND TRIGGER syntax
Workaround: Use separate triggers for each timing point.
-- NOT SUPPORTED:-- CREATE TRIGGER employees_compound-- FOR INSERT OR UPDATE ON employees-- COMPOUND TRIGGER-- BEFORE STATEMENT IS ...-- BEFORE EACH ROW IS ...-- AFTER EACH ROW IS ...-- AFTER STATEMENT IS ...-- END;
-- WORKAROUND: Multiple separate triggersCREATE OR REPLACE TRIGGER employees_before_stmtBEFORE INSERT OR UPDATE ON employeesBEGIN -- Statement-level before logic NULL;END;/
CREATE OR REPLACE TRIGGER employees_before_rowBEFORE INSERT OR UPDATE ON employeesFOR EACH ROWBEGIN -- Row-level before logic NULL;END;/
CREATE OR REPLACE TRIGGER employees_after_rowAFTER INSERT OR UPDATE ON employeesFOR EACH ROWBEGIN -- Row-level after logic NULL;END;/FORALL SAVE EXCEPTIONS
Partially Supported: Basic FORALL works, but SAVE EXCEPTIONS may not fully capture all errors.
-- WORKAROUND: Use loop with individual error handlingDECLARE TYPE emp_id_tab IS TABLE OF NUMBER; v_emp_ids emp_id_tab := emp_id_tab(101, 999, 102, 998, 103); v_error_count NUMBER := 0;BEGIN FOR i IN 1..v_emp_ids.COUNT LOOP BEGIN UPDATE employees SET salary = salary * 1.05 WHERE employee_id = v_emp_ids(i);
IF SQL%ROWCOUNT = 0 THEN DBMS_OUTPUT.PUT_LINE('No employee found: ' || v_emp_ids(i)); v_error_count := v_error_count + 1; END IF; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Error for ' || v_emp_ids(i) || ': ' || SQLERRM); v_error_count := v_error_count + 1; END; END LOOP;
DBMS_OUTPUT.PUT_LINE('Total errors: ' || v_error_count); COMMIT;END;/Conversion Patterns
Pattern 1: DECODE to CASE
Oracle’s DECODE is supported, but CASE is more portable:
-- Oracle DECODESELECT DECODE(status, 'A', 'Active', 'I', 'Inactive', 'Unknown') FROM table;
-- ANSI CASE (recommended)SELECT CASE status WHEN 'A' THEN 'Active' WHEN 'I' THEN 'Inactive' ELSE 'Unknown'END FROM table;Pattern 2: ROWNUM Pagination to FETCH FIRST
-- Oracle ROWNUM (supported but limited)SELECT * FROM ( SELECT t.*, ROWNUM rn FROM table t WHERE ROWNUM <= 20) WHERE rn > 10;
-- ANSI Standard (recommended)SELECT * FROM tableORDER BY columnOFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;Pattern 3: (+) Joins to ANSI Joins
-- Oracle proprietary join (partially supported)SELECT a.*, b.*FROM table_a a, table_b bWHERE a.id = b.id(+);
-- ANSI Standard (recommended)SELECT a.*, b.*FROM table_a aLEFT OUTER JOIN table_b b ON a.id = b.id;Pattern 4: Complex Package State
-- Oracle package with session state (limited support)-- Package variables persist across calls in Oracle
-- WORKAROUND: Use context or session tablesCREATE TABLE session_state ( session_id VARCHAR2(100), key VARCHAR2(100), value VARCHAR2(4000), PRIMARY KEY (session_id, key));
CREATE OR REPLACE PACKAGE session_utils AS PROCEDURE set_value(p_key VARCHAR2, p_value VARCHAR2); FUNCTION get_value(p_key VARCHAR2) RETURN VARCHAR2;END;/
CREATE OR REPLACE PACKAGE BODY session_utils AS FUNCTION get_session_id RETURN VARCHAR2 IS BEGIN RETURN SYS_CONTEXT('USERENV', 'SESSIONID'); END;
PROCEDURE set_value(p_key VARCHAR2, p_value VARCHAR2) IS BEGIN MERGE INTO session_state s USING (SELECT get_session_id() AS sid, p_key AS k, p_value AS v FROM DUAL) src ON (s.session_id = src.sid AND s.key = src.k) WHEN MATCHED THEN UPDATE SET s.value = src.v WHEN NOT MATCHED THEN INSERT VALUES (src.sid, src.k, src.v); COMMIT; END;
FUNCTION get_value(p_key VARCHAR2) RETURN VARCHAR2 IS v_value VARCHAR2(4000); BEGIN SELECT value INTO v_value FROM session_state WHERE session_id = get_session_id() AND key = p_key; RETURN v_value; EXCEPTION WHEN NO_DATA_FOUND THEN RETURN NULL; END;END;/Pattern 5: DBMS_PIPE Alternative
-- Oracle DBMS_PIPE (not supported)-- Used for inter-session communication
-- WORKAROUND: Use queue tableCREATE TABLE message_queue ( msg_id NUMBER PRIMARY KEY, channel VARCHAR2(100), message CLOB, created_at TIMESTAMP DEFAULT SYSTIMESTAMP, processed NUMBER DEFAULT 0);
CREATE SEQUENCE msg_seq;
-- Send messageCREATE OR REPLACE PROCEDURE send_message( p_channel VARCHAR2, p_message CLOB) ASBEGIN INSERT INTO message_queue (msg_id, channel, message) VALUES (msg_seq.NEXTVAL, p_channel, p_message); COMMIT;END;/
-- Receive messageCREATE OR REPLACE FUNCTION receive_message( p_channel VARCHAR2, p_timeout NUMBER DEFAULT 10) RETURN CLOB AS v_message CLOB; v_msg_id NUMBER;BEGIN SELECT msg_id, message INTO v_msg_id, v_message FROM message_queue WHERE channel = p_channel AND processed = 0 ORDER BY created_at FETCH FIRST 1 ROW ONLY FOR UPDATE SKIP LOCKED;
UPDATE message_queue SET processed = 1 WHERE msg_id = v_msg_id; COMMIT; RETURN v_message;EXCEPTION WHEN NO_DATA_FOUND THEN RETURN NULL;END;/Working Code Examples
Complete Employee Management Package
-- Package for comprehensive employee managementCREATE OR REPLACE PACKAGE emp_management AS -- Types TYPE emp_summary_rec IS RECORD ( department_id NUMBER, dept_name VARCHAR2(100), emp_count NUMBER, total_salary NUMBER, avg_salary NUMBER );
TYPE emp_summary_tab IS TABLE OF emp_summary_rec;
-- Procedures PROCEDURE hire_employee( p_first_name IN VARCHAR2, p_last_name IN VARCHAR2, p_email IN VARCHAR2, p_salary IN NUMBER, p_dept_id IN NUMBER, p_emp_id OUT NUMBER );
PROCEDURE transfer_employee( p_emp_id IN NUMBER, p_new_dept_id IN NUMBER );
PROCEDURE give_raise( p_emp_id IN NUMBER, p_percent IN NUMBER );
-- Functions FUNCTION get_employee_count(p_dept_id NUMBER DEFAULT NULL) RETURN NUMBER; FUNCTION calculate_annual_compensation(p_emp_id NUMBER) RETURN NUMBER; FUNCTION get_department_summary RETURN emp_summary_tab;
END emp_management;/
CREATE OR REPLACE PACKAGE BODY emp_management AS
PROCEDURE hire_employee( p_first_name IN VARCHAR2, p_last_name IN VARCHAR2, p_email IN VARCHAR2, p_salary IN NUMBER, p_dept_id IN NUMBER, p_emp_id OUT NUMBER ) AS BEGIN -- Validate salary IF p_salary < 30000 OR p_salary > 500000 THEN RAISE_APPLICATION_ERROR(-20001, 'Salary must be between 30000 and 500000'); END IF;
-- Generate new ID SELECT emp_seq.NEXTVAL INTO p_emp_id FROM DUAL;
-- Insert employee INSERT INTO employees ( employee_id, first_name, last_name, email, hire_date, salary, department_id ) VALUES ( p_emp_id, p_first_name, p_last_name, LOWER(p_email), SYSDATE, p_salary, p_dept_id );
COMMIT; DBMS_OUTPUT.PUT_LINE('Hired employee ' || p_emp_id || ': ' || p_first_name); EXCEPTION WHEN DUP_VAL_ON_INDEX THEN RAISE_APPLICATION_ERROR(-20002, 'Email already exists: ' || p_email); WHEN OTHERS THEN ROLLBACK; RAISE; END hire_employee;
PROCEDURE transfer_employee( p_emp_id IN NUMBER, p_new_dept_id IN NUMBER ) AS v_old_dept NUMBER; BEGIN SELECT department_id INTO v_old_dept FROM employees WHERE employee_id = p_emp_id;
UPDATE employees SET department_id = p_new_dept_id WHERE employee_id = p_emp_id;
-- Log transfer INSERT INTO emp_transfers (employee_id, from_dept, to_dept, transfer_date) VALUES (p_emp_id, v_old_dept, p_new_dept_id, SYSDATE);
COMMIT; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20003, 'Employee not found: ' || p_emp_id); END transfer_employee;
PROCEDURE give_raise( p_emp_id IN NUMBER, p_percent IN NUMBER ) AS v_current_salary NUMBER; v_new_salary NUMBER; BEGIN SELECT salary INTO v_current_salary FROM employees WHERE employee_id = p_emp_id FOR UPDATE;
v_new_salary := v_current_salary * (1 + p_percent / 100);
IF v_new_salary > 500000 THEN RAISE_APPLICATION_ERROR(-20004, 'New salary exceeds maximum'); END IF;
UPDATE employees SET salary = v_new_salary WHERE employee_id = p_emp_id;
COMMIT; DBMS_OUTPUT.PUT_LINE('Updated salary from ' || v_current_salary || ' to ' || v_new_salary); EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20003, 'Employee not found: ' || p_emp_id); END give_raise;
FUNCTION get_employee_count(p_dept_id NUMBER DEFAULT NULL) RETURN NUMBER AS v_count NUMBER; BEGIN IF p_dept_id IS NULL THEN SELECT COUNT(*) INTO v_count FROM employees; ELSE SELECT COUNT(*) INTO v_count FROM employees WHERE department_id = p_dept_id; END IF; RETURN v_count; END get_employee_count;
FUNCTION calculate_annual_compensation(p_emp_id NUMBER) RETURN NUMBER AS v_salary NUMBER; v_bonus NUMBER; BEGIN SELECT salary, NVL(bonus, 0) INTO v_salary, v_bonus FROM employees WHERE employee_id = p_emp_id;
RETURN (v_salary * 12) + v_bonus; EXCEPTION WHEN NO_DATA_FOUND THEN RETURN NULL; END calculate_annual_compensation;
FUNCTION get_department_summary RETURN emp_summary_tab AS v_result emp_summary_tab := emp_summary_tab(); BEGIN FOR rec IN ( SELECT d.department_id, d.department_name, COUNT(e.employee_id) AS emp_count, NVL(SUM(e.salary), 0) AS total_salary, NVL(AVG(e.salary), 0) AS avg_salary FROM departments d LEFT JOIN employees e ON d.department_id = e.department_id GROUP BY d.department_id, d.department_name ORDER BY d.department_id ) LOOP v_result.EXTEND; v_result(v_result.COUNT).department_id := rec.department_id; v_result(v_result.COUNT).dept_name := rec.department_name; v_result(v_result.COUNT).emp_count := rec.emp_count; v_result(v_result.COUNT).total_salary := rec.total_salary; v_result(v_result.COUNT).avg_salary := rec.avg_salary; END LOOP;
RETURN v_result; END get_department_summary;
END emp_management;/Best Practices
1. Keep PL/SQL Simple
HeliosDB handles simple, straightforward PL/SQL best. Avoid deeply nested constructs.
-- Good: Simple, clear logicCREATE OR REPLACE PROCEDURE update_salary(p_emp_id NUMBER, p_new_sal NUMBER) ASBEGIN UPDATE employees SET salary = p_new_sal WHERE employee_id = p_emp_id; COMMIT;END;/
-- Avoid: Overly complex nested logic-- CREATE OR REPLACE PROCEDURE complex_procedure AS-- -- Multiple levels of nesting, complex cursor operations, etc.-- BEGIN ...2. Use Explicit Cursors Over Implicit
-- Preferred: Explicit cursorDECLARE CURSOR emp_cur IS SELECT * FROM employees; v_emp emp_cur%ROWTYPE;BEGIN OPEN emp_cur; LOOP FETCH emp_cur INTO v_emp; EXIT WHEN emp_cur%NOTFOUND; -- Process v_emp END LOOP; CLOSE emp_cur;END;/3. Handle Exceptions Explicitly
BEGIN -- Your codeEXCEPTION WHEN NO_DATA_FOUND THEN -- Handle specifically WHEN TOO_MANY_ROWS THEN -- Handle specifically WHEN OTHERS THEN -- Log and re-raise DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM); RAISE;END;/4. Avoid Package State
Minimize reliance on package-level variables that persist across calls.
5. Test Incrementally
Test PL/SQL code in small blocks before combining into larger procedures.
Troubleshooting
Common Error Messages
| Error | Cause | Solution |
|---|---|---|
| PLS-00201: identifier not declared | Unknown package/function | Check DBMS package support |
| PLS-00103: Encountered symbol | Syntax error | Verify construct is supported |
| PLS-00306: wrong number of arguments | Parameter mismatch | Check procedure signature |
| ORA-06550: line N, column N | PL/SQL compilation error | Review error details |
| PRA-00000: autonomous transaction | Autonomous transaction used | Use workaround pattern |
Debugging Tips
- Use DBMS_OUTPUT.PUT_LINE liberally for debugging
- Test anonymous blocks before creating stored procedures
- Break complex logic into smaller procedures
- Check compatibility matrix for unsupported features
Related Documentation
- README.md - Protocol overview
- CONFIGURATION.md - PL/SQL configuration settings
- COMPATIBILITY.md - Full compatibility matrix
- EXAMPLES.md - More PL/SQL examples
Last Updated: January 2026 PL/SQL Engine Version: HeliosDB 7.0+