PL/SQL & T-SQL (Oracle and SQL Server) Module 15 – Production PL/SQL: staging, concurrency, JSON and tests
PL/SQL code review and interview drills
Read a PL/SQL package against a checklist of errors, loops, dynamic SQL, rights, state and speed, then answer interview drills failure first.
What you will learn
- Review a PL/SQL package against a checklist of errors, bulk processing, security and performance
- Explain the 19c and 26ai differences that change an answer
- Practise interview drills with model answers
- Avoid unreliable or leaked interview-question sources
Before you start
What you’ll learn · Before you start
What you will learn
- Review a PL/SQL package against a checklist of errors, bulk processing, security and performance
- Explain the 19c and 26ai differences that change an answer
- Practise interview drills with model answers
- Avoid unreliable or leaked interview-question sources
Before you start
On this page
A posting package passes its author’s test: a generated batch of 200 feed lines goes in and 200 ledger rows come out. A PL/SQL code review of the same package still finds eight problems, and one of them quietly drops the last 50 lines of a 250-line feed.
A code review: Reading a change to code before it is merged, to find defects, risks and unclear parts. For database code it covers errors and transactions, set-based logic, concurrency, permissions, performance and tests. More in “T-SQL code review and interview drills” is a second reader taking a change apart before it ships. You will read one package with a checklist and run a checker that points at its eight problems. Interview drills then argue each finding the way a good review comment does: the failure, the fix, the test.
A PL/SQL code review reads in the order failures cost
A single successful call proves little about three things: the state a failure leaves partway, the effect of a
caller’s text inside a statement, and the count a second call in one session reports. A PL/SQL code review asks about
each of them in order, costliest failures first. Step down the order on the package ledger_post:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- First, what a failure leaves. Find every handler and every COMMIT. Ask what is kept when line 180 of a 250-line feed fails. In ledger_post a handler that does nothing and a COMMIT inside the loop decide the answer.
- Then the loops. A loop that fetches with LIMIT must still use its last, short batch, and a loop that sends one UPDATE per line makes one call to SQL per line. Ask whether one SQL statement could do the work.
- Then caller text and rights. Look for parameters joined into a statement's text, and for the package's rights model. A value belongs in a bind; a column name belongs in a list of allowed names; AUTHID belongs in the specification.
- Then what outlives the call. Read the variables declared at the top of the body. Each session keeps its own copy until it ends, so a counter that a call adds to keeps growing over every call in that session.
- Then the conditions. Check that each condition leaves its column bare. A function wrapped around posted_at means a plain index on posted_at cannot narrow the search; read the plan with DBMS_XPLAN to be sure.
- Last, the tests. Ask which test fails if each finding comes back. The author's one test posts 200 lines that all succeed, so it walks past every stage above.
Each stage is a row of the checklist, linked to the lesson that teaches it:
| Area | What to check |
|---|---|
| Errors and transactions | no WHEN OTHERS THEN NULL, no COMMIT in a reusable unit, error codes in the specification |
| Loops and bulk SQL | SQL before loops; LIMIT loops that keep the last batch |
| Dynamic SQL and rights | binds for values, an allowed list for names, an explicit AUTHID |
| Package state | no variable that outlives a call without a reason |
| Performance | bare columns in conditions; the plan read, not guessed |
| Tests | a utPLSQL test per rule, failure paths too |
Eight planted problems, and what the checker flags
Here is ledger_post as it arrived, with the author’s test at the end of the script:
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- The package ledger_post as it arrives for a code review. post_batch moves one feed batch from feed_lines into the
-- ledger and adds each line to its account's balance; lines_posted reports how many lines were posted; open_report
-- opens a batch's ledger entries in the order a caller asks for; day_total adds up one day's entries. It compiles,
-- and the author's test at the end succeeds. Run schema_oracle.sql of practice-schema-and-dialect-basics first.
-- post_batch commits on its own, so the cleanup at the end deletes what the test posted and drops what the script made.
SET SERVEROUTPUT ON
DROP TABLE IF EXISTS feed_lines PURGE;
DROP INDEX IF EXISTS ledger_posted_ix;
CREATE TABLE feed_lines (
batch_id VARCHAR2(20) NOT NULL,
line_no NUMBER(5) NOT NULL,
account_id NUMBER(10) NOT NULL,
amount NUMBER(14, 2) NOT NULL,
posted_at DATE NOT NULL,
memo VARCHAR2(60),
CONSTRAINT feed_lines_pk PRIMARY KEY (batch_id, line_no)
);
CREATE INDEX ledger_posted_ix ON ledger (posted_at);
CREATE OR REPLACE PACKAGE ledger_post IS
PROCEDURE post_batch (p_batch IN VARCHAR2);
FUNCTION lines_posted RETURN PLS_INTEGER;
PROCEDURE open_report (p_batch IN VARCHAR2, p_sort IN VARCHAR2, p_rows OUT SYS_REFCURSOR);
FUNCTION day_total (p_day IN DATE) RETURN NUMBER;
END ledger_post;
/
CREATE OR REPLACE PACKAGE BODY ledger_post IS
g_posted PLS_INTEGER := 0;
PROCEDURE post_batch (p_batch IN VARCHAR2) IS
CURSOR c_lines IS
SELECT account_id, amount, posted_at, memo
FROM feed_lines
WHERE batch_id = p_batch
ORDER BY line_no;
TYPE t_lines IS TABLE OF c_lines%ROWTYPE INDEX BY PLS_INTEGER;
v_lines t_lines;
BEGIN
OPEN c_lines;
LOOP
FETCH c_lines BULK COLLECT INTO v_lines LIMIT 100;
EXIT WHEN c_lines%NOTFOUND;
FORALL i IN 1 .. v_lines.COUNT
INSERT INTO ledger (account_id, amount, posted_at, memo)
VALUES (v_lines(i).account_id, v_lines(i).amount, v_lines(i).posted_at, v_lines(i).memo);
FOR i IN 1 .. v_lines.COUNT LOOP
UPDATE accounts
SET balance = balance + v_lines(i).amount
WHERE account_id = v_lines(i).account_id;
END LOOP;
g_posted := g_posted + v_lines.COUNT;
COMMIT;
END LOOP;
CLOSE c_lines;
EXCEPTION
WHEN OTHERS THEN NULL;
END post_batch;
FUNCTION lines_posted RETURN PLS_INTEGER IS
BEGIN
RETURN g_posted;
END lines_posted;
PROCEDURE open_report (p_batch IN VARCHAR2, p_sort IN VARCHAR2, p_rows OUT SYS_REFCURSOR) IS
BEGIN
OPEN p_rows FOR
'SELECT entry_id, account_id, amount, memo FROM ledger WHERE memo LIKE ''' || p_batch
|| ' line %'' ORDER BY ' || p_sort;
END open_report;
FUNCTION day_total (p_day IN DATE) RETURN NUMBER IS
v_total NUMBER;
BEGIN
SELECT NVL(SUM(amount), 0)
INTO v_total
FROM ledger
WHERE TRUNC(posted_at) = TRUNC(p_day);
RETURN v_total;
END day_total;
END ledger_post;
/
-- The author's test: a generated batch of 200 lines, 100 each on accounts 1 and 2, then the count it checks.
INSERT INTO feed_lines (batch_id, line_no, account_id, amount, posted_at, memo)
SELECT 'b-0412', LEVEL, MOD(LEVEL, 2) + 1, 10.00, DATE '2031-04-12', 'b-0412 line ' || LEVEL
FROM dual
CONNECT BY LEVEL <= 200;
COMMIT;
BEGIN
ledger_post.post_batch('b-0412');
DBMS_OUTPUT.PUT_LINE('lines_posted = ' || ledger_post.lines_posted);
END;
/
-- Expected: lines_posted = 200
SELECT COUNT(*) AS ledger_rows FROM ledger WHERE memo LIKE 'b-0412 line %';
-- Expected: 200, the number the test compares, so the test succeeds.
-- Cleanup: post_batch committed, so its rows are removed by hand and the balances put back.
DELETE FROM ledger WHERE memo LIKE 'b-0412 line %';
UPDATE accounts SET balance = balance - 1000.00 WHERE account_id IN (1, 2);
COMMIT;
DROP PACKAGE ledger_post;
DROP INDEX ledger_posted_ix;
DROP TABLE feed_lines PURGE; """review_before.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""
from oracle_session import Lab
with Lab() as lab:
lab.sqlplus("review/review_before.sql") Output
$ sqlplus -s lab@//localhost/FREEPDB1 @review/review_before.sql
Table dropped.
Index dropped.
Table created.
Index created.
Package created.
Package body created.
200 rows created.
Commit complete.
lines_posted = 200
PL/SQL procedure successfully completed.
LEDGER_ROWS
-----------
200
200 rows deleted.
2 rows updated.
Commit complete.
Package dropped.
Index dropped.
Table dropped.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 review_before_session.py
The checker applies eight rules, one per planted problem, to this script and the revised one. It matches patterns rather than parsing PL/SQL, so each flagged line is a place to read closely:
"""Eight rules of the PL/SQL review checklist, run over review_before.sql and review_after.sql.
The checker reads each script beside it and prints the lines that its eight rules flag, in the checklist's order. It
is a teaching aid built on regular expressions, not a PL/SQL parser: it does not follow comments that span lines,
text inside quotes or code it was not written for. A flagged line is a place to read closely, and a script with no
findings has passed these eight rules only, which is not the same as being correct.
"""
import re
from pathlib import Path
HERE = Path(__file__).resolve().parent
FILES = ["review_before.sql", "review_after.sql"]
SPEC = re.compile(r"^\s*CREATE\s+(OR\s+REPLACE\s+)?PACKAGE\s+(?!BODY\b)(\w+)", re.I)
BODY = re.compile(r"^\s*CREATE\s+(OR\s+REPLACE\s+)?PACKAGE\s+BODY\s+(\w+)", re.I)
SUBPROGRAM = re.compile(r"^\s*(PROCEDURE|FUNCTION)\s+\w+", re.I)
DECLARATION = re.compile(r"^\s*([a-z]\w*)\s+[\w.%()]+[^;]*;", re.I)
NOT_VARIABLES = {"cursor", "type", "subtype", "pragma", "procedure", "function", "end"}
FOR_LOOP = re.compile(r"^\s*FOR\s+\w+\s+IN\b.*\bLOOP\b", re.I)
END_LOOP = re.compile(r"^\s*END\s+LOOP\b", re.I)
DML = re.compile(r"^\s*(INSERT|UPDATE|DELETE|MERGE)\b", re.I)
BULK_FETCH = re.compile(r"\bFETCH\b.*\bBULK\s+COLLECT\b.*\bLIMIT\b", re.I)
EXIT_NOTFOUND = re.compile(r"^\s*EXIT\s+WHEN\s+\w+%NOTFOUND\b", re.I)
DYNAMIC_START = re.compile(r"\b(OPEN\s+\w+\s+FOR|EXECUTE\s+IMMEDIATE)\b(.*)", re.I)
COLUMN_IN_FUNCTION = re.compile(
r"\b(WHERE|AND|OR)\b.*?\b(TRUNC|UPPER|LOWER|TO_CHAR|SUBSTR|NVL)\s*\(\s*([a-z][\w.]*)\s*[,)]", re.I
)
PARAMETER_OR_VARIABLE = re.compile(r"^(p|v|l|g)_", re.I)
RULES = [
("errors", "a WHEN OTHERS handler that does nothing hides every failure from the caller"),
("transactions", "a COMMIT or ROLLBACK inside the package ends the caller's transaction"),
("bulk loop", "EXIT WHEN %NOTFOUND right after a LIMIT fetch drops the last, partial batch"),
("set-based", "one DML statement per row inside a FOR loop: one call to SQL per row"),
("dynamic SQL", "caller text joined into a statement with || instead of a bind or an allowed value"),
("rights", "a package specification without AUTHID: definer's rights by default, never decided"),
("state", "a package-level variable that a call changes: its value outlives the call"),
("performance", "a function around a column in a condition: an index on that column cannot serve it"),
]
def code(line):
"""The line without a trailing -- comment (enough for these two scripts)."""
return line.split("--", 1)[0].rstrip()
def statement_from(lines, start):
"""The text of the statement that starts on line `start` (0-based), up to its semicolon."""
parts = []
for text in lines[start:]:
parts.append(code(text))
if ";" in code(text):
break
return " ".join(parts)
def findings(lines):
"""(rule number, line number) pairs for one script."""
found = []
body_start, body_end, body_name = None, None, None
for n, text in enumerate(lines):
m = BODY.match(text)
if m:
body_start, body_name = n, m.group(2)
elif body_start is not None and body_end is None and re.match(rf"^\s*END\s+{body_name}\s*;", text, re.I):
body_end = n
in_body = lambda n: body_start is not None and body_start < n < (body_end or len(lines))
# Rule 6: every package specification states its rights model.
for n, text in enumerate(lines):
if SPEC.match(text):
header = statement_from(lines, n).split(" IS", 1)[0].split(" AS", 1)[0]
if not re.search(r"\bAUTHID\b", header, re.I):
found.append((6, n + 1))
# Rule 7: variables declared in the body before its first subprogram, then assigned by a subprogram.
if body_start is not None:
first_sub = next((n for n in range(body_start + 1, len(lines)) if SUBPROGRAM.match(lines[n])), len(lines))
for n in range(body_start + 1, first_sub):
m = DECLARATION.match(code(lines[n]))
if m and m.group(1).lower() not in NOT_VARIABLES:
name = m.group(1)
if any(re.search(rf"\b{name}\s*:=", code(t), re.I) for t in lines[first_sub:body_end]):
found.append((7, n + 1))
depth = 0
for n, text in enumerate(lines):
line = code(text)
if re.search(r"\bWHEN\s+OTHERS\s+THEN\s+NULL\s*;", line, re.I):
found.append((1, n + 1))
if in_body(n) and re.match(r"^\s*(COMMIT|ROLLBACK)\b", line, re.I):
found.append((2, n + 1))
if BULK_FETCH.search(line):
nxt = next((k for k in range(n + 1, len(lines)) if code(lines[k]).strip()), None)
if nxt is not None and EXIT_NOTFOUND.match(code(lines[nxt])):
found.append((3, nxt + 1))
if FOR_LOOP.match(line):
depth += 1
elif END_LOOP.match(line) and depth:
depth -= 1
elif depth and DML.match(line):
found.append((4, n + 1))
if DYNAMIC_START.search(line):
rest = DYNAMIC_START.search(statement_from(lines, n)).group(2).strip()
if not rest.upper().startswith(("SELECT", "WITH")) and "||" in rest:
found.append((5, n + 1))
m = COLUMN_IN_FUNCTION.search(line)
if m and not PARAMETER_OR_VARIABLE.match(m.group(3)):
found.append((8, n + 1))
return sorted(found)
for name in FILES:
lines = (HERE / name).read_text(encoding="utf-8").splitlines()
result = findings(lines)
print(f"{name}: {len(result)} finding{'s' if len(result) != 1 else ''}")
for rule, line_no in result:
area, why = RULES[rule - 1]
print(f" line {line_no:>3} {area:<13} {why}")
print() Output
review_before.sql: 8 findings line 61 errors a WHEN OTHERS handler that does nothing hides every failure from the caller line 57 transactions a COMMIT or ROLLBACK inside the package ends the caller's transaction line 47 bulk loop EXIT WHEN %NOTFOUND right after a LIMIT fetch drops the last, partial batch line 52 set-based one DML statement per row inside a FOR loop: one call to SQL per row line 71 dynamic SQL caller text joined into a statement with || instead of a bind or an allowed value line 24 rights a package specification without AUTHID: definer's rights by default, never decided line 33 state a package-level variable that a call changes: its value outlives the call line 82 performance a function around a column in a condition: an index on that column cannot serve it review_after.sql: 0 findings
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 review_check.py
It flags eight lines of the submitted package and none of the revised one: a WHEN OTHERS handler that
swallowed exception: An exception that a handler catches and then drops, as WHEN OTHERS THEN NULL does, so the caller sees a normal return although the work failed., a COMMIT inside the loop, an exit straight after the LIMIT fetch, one UPDATE per line, caller text joined into a
statement, no AUTHID, the counter g_posted, and TRUNC around the indexed posted_at. The author’s test takes
only the happy path: The run of a program in which every input is valid and nothing fails. A test of the happy path alone cannot show what a failure, an unusual input or a second call does.: its 200 lines fill the LIMIT exactly and none of them fails.
Zero findings is where the reading starts
Wrong: close the review at 0 findings. Right: walk every stage yourself; the checker only makes sure that eight known patterns are never missed.
Checkpoint
Check: Reading a package in the order its failures cost · The planted problems and what a pattern checker shows
Name the release your answer depends on
Many PL/SQL answers hold on one release only, and a team still on 19c will ask which. Oracle’s 21c reference added
iteration controls such as stepped ranges; its 26ai reference lists the SQL BOOLEAN type and IF [NOT] EXISTS on DDL
among that release’s new features, and the 19c reference says IF [NOT] EXISTS reached 19c in Release Update 19.28:
| Feature | On 26ai | On 19c |
|---|---|---|
FOR i IN 1 .. 9 BY 2 |
yes, since 21c | no: a counter loop, as in control structures |
A BOOLEAN column |
yes | no: NUMBER(1) with a check of 0 or 1 |
CREATE TABLE IF NOT EXISTS |
yes | from 19.28; before it, a block that ignores ORA-00955 only |
So lead with the release, and on 19c with the release update.
Checkpoint
Check: Naming the release an answer depends on
A drill answer shows the failure, the fix and the test
Good interview drills come from code you can run or from behaviour a manual documents, and they name no company and repeat no remembered question. A strong answer gives the deciding sizes first (rows per run, callers at once, what must survive a failure), then the failure, the fix and the test that proves it.
Where you practise matters too:
| Source | Use it? |
|---|---|
| Oracle’s reference pages, checked in your own lab | Yes: the rule, then your proof |
| An answer written for 11g or 12c | Only after checking the current page |
| Lists of “real interview questions” from companies | No: no source, often leaked or copied |
Checkpoint
Check: Drill answers from trustworthy sources, failure first
Each planted problem fixed, with the test that would have caught it Pro: opens with a Pro, Premium or Ultimate pass
The revised package keeps what ledger_post is for and rewrites its contract wherever the original left the caller
guessing. Each fix below is paired with a test that fails on the submitted package and passes on the revised one.
In this part:
- Fixing the hidden error and the stray commit
- Fixing the loop that drops a batch
- Fixing the pasted SQL and the unstated rights
- Fixing the shared counter and the date condition
- Tests that fail on the submitted package
3 runnable examples (Python, SQL) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Four interview drills answered, with a rubric for each level Pro: opens with a Pro, Premium or Ultimate pass
Four drills follow, each with a model answer that uses only behaviour this track shows and Oracle documents. Answer each one aloud or on paper first; the rubric at the end scores your answer against each level.
In this part:
- How a strong answer is put together
- Drill 1: predict what the submitted package posts
- Drill 2: a handler the author wants to keep
- Drill 3: one change for 19c and 26ai sites
- Drill 4: pasted SQL that only an administrator calls
- A rubric to score your own answers
about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: name the area and the risk
Each question quotes a line of a package or a claim from a code review: name its checklist area and what it puts at risk.
Practice that adapts to you
Practise this lesson
12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): why is WHEN OTHERS THEN NULL a finding even when the code seems to work?
It turns every error into success. The handler catches anything the block raises, a missing account, a broken constraint, a full tablespace, and the call returns normally, so the caller commits part of the work with no message. Oracle’s compiler flags it as warning PLW-06009, an OTHERS handler that does not end in RAISE or RAISE_APPLICATION_ERROR. Handle the errors you can by name, and let the rest reach the caller.
Next come a load that commits after every 1,000 lines and fails partway, five findings on the eve of a release, and a utility that deletes from any table it is told:
-
Mid to senior
A package procedure loads a 60,000-line feed and commits after each 1,000. Line 43,512 names an unknown account: the call fails with ORA-02291 and the job rolls back. After the fix, a rerun succeeds, but 43,000 lines appear twice in the ledger. Explain why, and rework the load so a failed run can run again.
-
Senior to staff
The evening before a release, a code review of a payments package finds WHEN OTHERS THEN NULL in the refund procedure, a COMMIT inside an API procedure, a customer name joined into EXECUTE IMMEDIATE, TRUNC around an indexed date in a monthly report, and BULK COLLECT without LIMIT over a table of 5,000 rows. Which block the release?
-
Mid to senior
A housekeeping procedure purge_rows(p_table, p_days) runs EXECUTE IMMEDIATE with 'DELETE FROM ' || p_table || ' WHERE created_at < SYSDATE - ' || p_days. It has no AUTHID clause, its owner holds DELETE ANY TABLE, and an operations role can execute it. What do you raise in code review, and how do you rework it?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
In which order does a PL/SQL code review read a package?
Costliest failures first: errors and transactions, loops and bulk SQL, dynamic SQL and rights, package state, performance, then tests.
-
What does one successful call prove nothing about?
The state a failure leaves partway, the effect of a caller's text inside a statement, and the count a second call in the same session reports.
-
What does the review's errors and transactions row look for?
No WHEN OTHERS THEN NULL, no COMMIT in a reusable unit, and the error codes documented in the specification.
-
What do the loops and the dynamic SQL rows of the checklist ask for?
SQL before loops, and LIMIT loops whose exit keeps the last batch; then binds for values, an allowed list for names, and an explicit AUTHID.
-
What do the package state and performance rows check?
That no variable outlives a call without a reason, and that conditions keep bare columns, with the plan read rather than guessed: TRUNC around an indexed column is a finding.
-
Why did the author's test miss the bug that drops the last 50 lines of a 250-line feed?
Its 200 lines filled the LIMIT exactly and none of them failed, so the exit placed straight after the fetch never had a short last batch to drop.
-
Which three answers change with the release?
A stepped FOR loop, which exists from 21c; a BOOLEAN column, which 26ai has and 19c replaces with NUMBER(1) and a check of 0 or 1; and CREATE TABLE IF NOT EXISTS, which reached 19c in Release Update 19.28 and before that needed a block that ignores ORA-00955 only.
-
What shape does a strong drill answer have, and which sources are worth practising from?
The deciding sizes first, rows per run, callers at once and what must survive a failure, then the failure, the fix and the test that proves it. Practise from the vendor's reference checked in your own lab; an answer written for an old release needs rechecking, and lists of real interview questions from companies have no source.
Key takeaways
- A PL/SQL code review reads a package in the order its failures cost: errors and transactions, loops, dynamic SQL and rights, state, performance, then tests.
- The planted problems hide on the happy path and show only when a line fails, a feed is not a round size, a caller sends odd text or a session calls twice.
- A pattern checker catches only the patterns it knows, so the reader still walks every stage.
- A strong drill answer names its release, then shows the failure, the fix and the test.
Terms in this lesson
- happy path
- The run of a program in which every input is valid and nothing fails. A test of the happy path alone cannot show what a failure, an unusual input or a second call does.
- swallowed exception
- An exception that a handler catches and then drops, as WHEN OTHERS THEN NULL does, so the caller sees a normal return although the work failed.
References
- PL/SQL Language Reference 26ai: Package Writing Guidelines (Oracle)
- PL/SQL Language Reference 26ai: PL/SQL Error Handling (Oracle)
- PL/SQL Language Reference 26ai: Raising Exceptions Explicitly (Oracle)
- Database Error Messages: PLW-06009 (Oracle)
- PL/SQL Language Reference 26ai: Bulk SQL and Bulk Binding (Oracle)
- PL/SQL Language Reference 26ai: SQL Injection (Oracle)
- PL/SQL Packages and Types Reference 26ai: DBMS_ASSERT (Oracle)
- PL/SQL Language Reference 26ai: Invoker's Rights and Definer's Rights (AUTHID Property) (Oracle)
- Database Reference 26ai: ALL_PROCEDURES (Oracle)
- PL/SQL Language Reference 26ai: Package State (Oracle)
- Database Concepts 26ai: Indexes and Index-Organized Tables (Oracle)
- PL/SQL Packages and Types Reference 26ai: DBMS_XPLAN (Oracle)
- SQL Language Reference 26ai: COMMIT (Oracle)
- PL/SQL Language Reference 26ai: Changes in This Release (Oracle)
- PL/SQL Language Reference 21c: Changes in This Release (Oracle)
- PL/SQL Language Reference 19c: Changes in This Release (Oracle)
- PL/SQL Packages and Types Reference 26ai: DBMS_DB_VERSION (Oracle)
- Database Error Messages: ORA-00955 (Oracle)
- utPLSQL documentation: Annotations (utPLSQL project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress