Chapter 12 · Triggers, Dynamic SQL, Scheduler, Advanced PL/SQL, and Database APIs

Native Dynamic SQL, DBMS_SQL, Bind Variables, Identifier Validation, and Injection Safety

Choose native dynamic SQL for known runtime shapes and DBMS_SQL for genuinely unknown shapes; bind data, validate and allowlist identifiers, and make injection failures and privilege boundaries explicit.

Advanced120–140 minutesNative dynamic SQL + DBMS_SQL/injection labBind data · allowlist/DBMS_ASSERT identifiersFree-compatible · no management pack requiredLast reviewed: August 2026

Learning outcomes

ServiceHub needs an administrative report whose selected columns and sort key are chosen at runtime. A developer concatenates status text and a requested column name into SQL. One crafted status changes the predicate, while another input attempts to turn an identifier into SQL syntax. Dynamic SQL is sometimes necessary; unsafe text construction is not.

01

Choose EXECUTE IMMEDIATE or OPEN FOR when the runtime SQL shape and bind contract are known at compile time.

02

Use DBMS_SQL when the select list, bind list, or returned shape is genuinely unknown until runtime.

03

Bind every data value and explain why identifiers cannot be replaced with ordinary bind placeholders.

04

Validate and allowlist dynamic identifiers with DBMS_ASSERT as a syntax check rather than an authorization decision.

05

Reproduce a safe local injection demonstration, repair it, and verify that hostile-looking text remains data.

Version, tooling, scope, privilege, and licensing baseline

Mandatory work targets a disposable ServiceHub schema in Oracle AI Database Free 26ai and was reviewed against RU 23.26.3 (July 2026), SQL Developer 26.2, and SQLcl 26.2.1. Free is capped at 2 foreground CPU cores, 2 GB combined SGA/PGA memory, and 12 GB user data and receives no Oracle patches or Oracle Support service requests. The mandatory Chapter 12 path uses local database features only: no Diagnostics/Tuning Pack, RAC, Data Guard, GoldenGate, Exadata, OCI service, remote Scheduler agent, or COMPATIBLE change is required. Stored program/trigger creation requires CREATE PROCEDURE/CREATE TRIGGER as appropriate; Scheduler job creation requires CREATE JOB. Cross-schema API tests use the course identities SERVICEHUB_OWNER and SERVICEHUB_APP with narrow grants.

1. Dynamic SQL has two Oracle interfaces

Native Dynamic SQL (NDS) is PL/SQL syntax such as EXECUTE IMMEDIATE and OPEN ... FOR. It is simpler and is the default when you know the number and types of inputs/outputs at compile time. DBMS_SQL exposes cursor numbers plus parse/bind/describe/define/fetch APIs. Oracle documents DBMS_SQL as required when the select list or placeholder set is unknown until runtime, or when a stored program returns an implicit query result.

sql · known-shape NDS
DECLARE  l_count NUMBER;  l_status VARCHAR2(12) := 'OPEN';BEGIN  EXECUTE IMMEDIATE    'SELECT COUNT(*) FROM servicehub_dyn_case WHERE status_code=:b'    INTO l_count    USING l_status;  DBMS_OUTPUT.PUT_LINE('count='||l_count);END;/

2. Bind data; do not concatenate data

A bind placeholder preserves the SQL structure while the value is supplied separately. This improves cursor reuse and prevents a data value from changing the SQL grammar.

sql · deliberately vulnerable shape in the disposable lab
-- p_status is treated as trusted SQL text here. That is the bug.l_sql := 'SELECT COUNT(*) FROM servicehub_dyn_case ' ||         'WHERE status_code = ''' || p_status || '''';EXECUTE IMMEDIATE l_sql INTO l_count;-- If p_status is: OPEN'' OR ''1''=''1-- the predicate is changed and all rows can match.
sql · repair: preserve SQL text and bind the value
l_sql := 'SELECT COUNT(*) FROM servicehub_dyn_case ' ||         'WHERE status_code = :status';EXECUTE IMMEDIATE l_sql INTO l_count USING p_status;-- The exact text OPEN'' OR ''1''=''1 is now only a VARCHAR2 value.-- It cannot add an OR predicate.

3. Identifiers are SQL grammar, not bind data

You cannot bind a table name, column name, keyword, or sort direction in the same way you bind a value. If a runtime identifier is genuinely necessary, first restrict it to the business-approved set and then validate/quote it with an Oracle-supported mechanism.

sql · allowlist plus DBMS_ASSERT
CREATE OR REPLACE FUNCTION servicehub_safe_order_column(  p_name IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINERIS  l_name VARCHAR2(128) := UPPER(TRIM(p_name));BEGIN  IF l_name NOT IN ('CASE_ID','STATUS_CODE','AMOUNT') THEN    RAISE_APPLICATION_ERROR(-20210,'unsupported order column');  END IF;  RETURN DBMS_ASSERT.SIMPLE_SQL_NAME(l_name);END;/

DBMS_ASSERT.SIMPLE_SQL_NAME checks that text is a syntactically valid simple SQL name. That does not prove the object/column is authorized for this API, and it does not replace an allowlist. SQL_OBJECT_NAME can additionally validate a qualified object reference exists, but existence still is not application authorization.

sql · sort direction is also allowlisted
IF UPPER(p_direction) NOT IN ('ASC','DESC') THEN  RAISE_APPLICATION_ERROR(-20211,'unsupported sort direction');END IF;l_sql := 'SELECT case_id,status_code,amount ' ||         'FROM servicehub_dyn_case ORDER BY ' ||         servicehub_safe_order_column(p_column) || ' ' ||         UPPER(p_direction);

4. DBMS_SQL is for genuinely dynamic shapes

When the caller can choose an arbitrary projection, NDS cannot declare a fixed INTO record at compile time. DBMS_SQL can parse the statement, bind variables, describe the columns, define output buffers, execute, and fetch rows generically.

sql · DBMS_SQL shape discovery skeleton
DECLARE  l_cur      INTEGER := DBMS_SQL.OPEN_CURSOR;  l_cols     INTEGER;  l_desc     DBMS_SQL.DESC_TAB3;  l_rows     INTEGER;BEGIN  DBMS_SQL.PARSE(    l_cur,    'SELECT case_id,status_code,amount FROM servicehub_dyn_case ' ||    'WHERE amount >= :min_amount',    DBMS_SQL.NATIVE  );  DBMS_SQL.BIND_VARIABLE(l_cur, ':min_amount', 100);  DBMS_SQL.DESCRIBE_COLUMNS3(l_cur, l_cols, l_desc);  l_rows := DBMS_SQL.EXECUTE(l_cur);  FOR i IN 1..l_cols LOOP    DBMS_OUTPUT.PUT_LINE(i||':'||l_desc(i).col_name);  END LOOP;  DBMS_SQL.CLOSE_CURSOR(l_cur);EXCEPTION  WHEN OTHERS THEN    IF DBMS_SQL.IS_OPEN(l_cur) THEN DBMS_SQL.CLOSE_CURSOR(l_cur); END IF;    RAISE;END;/

Real generic fetch code must call DEFINE_COLUMN/COLUMN_VALUE using the described data types. Do not adopt DBMS_SQL for a query whose shape is already known merely because it looks more advanced.

5. AUTHID still governs dynamic SQL

Dynamic SQL does not bypass PL/SQL privilege semantics. A definer-rights unit (AUTHID DEFINER, the default) executes SQL under the definer's security context; needed cross-schema object privileges for definer-rights stored code must be granted directly rather than only through roles. An invoker-rights unit (AUTHID CURRENT_USER) resolves/checks relevant external references with the invoker and is additionally subject to the INHERIT PRIVILEGES security model.

Never accept arbitrary SQL text into a privileged definer-rights package. That turns the package into a privilege-escalation interface even if every input string passes a syntax validator.

6. Reproducible lab: demonstrate injection then repair it

sql · setup
BEGIN EXECUTE IMMEDIATE 'DROP TABLE servicehub_dyn_case PURGE';EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;/CREATE TABLE servicehub_dyn_case(  case_id NUMBER PRIMARY KEY,  status_code VARCHAR2(12) NOT NULL,  amount NUMBER(10,2) NOT NULL);INSERT INTO servicehub_dyn_case VALUES(1,'OPEN',100);INSERT INTO servicehub_dyn_case VALUES(2,'CLOSED',200);INSERT INTO servicehub_dyn_case VALUES(3,'HOLD',300);COMMIT;
sql · unsafe demo procedure — create only in disposable training schema
CREATE OR REPLACE PROCEDURE sh12_unsafe_count(  p_status IN VARCHAR2,  p_count OUT NUMBER) AUTHID DEFINER IS  l_sql VARCHAR2(4000);BEGIN  l_sql := 'SELECT COUNT(*) FROM servicehub_dyn_case '||           'WHERE status_code='''||p_status||'''';  EXECUTE IMMEDIATE l_sql INTO p_count;END;/
sql · safe procedure
CREATE OR REPLACE PROCEDURE sh12_safe_count(  p_status IN VARCHAR2,  p_count OUT NUMBER) AUTHID DEFINER ISBEGIN  EXECUTE IMMEDIATE    'SELECT COUNT(*) FROM servicehub_dyn_case WHERE status_code=:b'    INTO p_count    USING p_status;END;/
sql · compare behavior
VARIABLE n NUMBEREXEC sh12_unsafe_count(q'[OPEN' OR '1'='1]', :n);PRINT n-- Unsafe result: 3 rows matched because the SQL grammar changed.EXEC sh12_safe_count(q'[OPEN' OR '1'='1]', :n);PRINT n-- Safe result: 0, because that entire string is a data value.EXEC sh12_safe_count('OPEN', :n);PRINT n-- Safe result: 1.
sql · cleanup
DROP PROCEDURE sh12_unsafe_count;DROP PROCEDURE sh12_safe_count;BEGIN EXECUTE IMMEDIATE 'DROP FUNCTION servicehub_safe_order_column'; EXCEPTION WHEN OTHERS THEN NULL; END;/DROP TABLE servicehub_dyn_case PURGE;

7. Production judgment

Prefer static SQL when the statement is known. Use NDS for runtime text with a known bind/result shape and DBMS_SQL for true metadata-driven shapes. Bind values, allowlist identifiers/fragments, validate names with DBMS_ASSERT where appropriate, and keep privileged dynamic APIs narrowly scoped. Log statement templates/signatures—not secrets or raw sensitive bind values.

No option, pack, restart, or COMPATIBLE change is required. Lesson 3 moves from runtime-generated SQL to runtime-generated work: Oracle Scheduler executes database jobs independently of application requests and therefore needs explicit time, identity, overlap, retry, and observability contracts.

Check your understanding

  1. When is DBMS_SQL required instead of native dynamic SQL?
  2. Can a bind placeholder represent a table name?
  3. What does DBMS_ASSERT.SIMPLE_SQL_NAME prove?
  4. Why should an identifier still be allowlisted after DBMS_ASSERT validation?
  5. Why is accepting arbitrary SQL text into a definer-rights API dangerous?
Review the answers

When runtime code does not know the SELECT-list shape or bind structure at compile time, or for certain implicit-result use cases.

No. A table/column name is SQL grammar; ordinary bind variables represent data values.

That the string is a syntactically valid simple SQL identifier.

Syntactic validity does not establish that the caller is authorized to select/sort by that identifier or that the business API supports it.

The SQL can execute with the definer security domain, turning user-controlled text into a privilege-escalation path.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.