Table of Contents
- Overview
- The SQLCA: how your program learns the result
- Working with cursors
- How PL/I data maps to SQL columns
- Dynamic SQL: PREPARE, EXECUTE, and DESCRIBE
- Who owns the database connection
- Concurrency: each thread has its own SQL state
- A checklist for your programs
- Summary
- Related articles
Audience: developers and operations engineers running PL/I applications migrated with Heirloom that use embedded EXEC SQL.
Overview
Your migrated program keeps its embedded EXEC SQL statements unchanged. Underneath, the runtime turns them into standard JDBC calls against a DB2-compatible database, and gives your program back exactly the feedback it expects on the mainframe: an SQLCA to check, cursors to fetch from, and host variables filled with converted values.
This article explains how that execution works and what you interact with — the result/diagnostic block, cursors, data-type conversion, dynamic SQL, who owns the database connection, and how multiple concurrent transactions stay isolated. It sits between two companion articles: the configuration how-to (how to wire up a connection) and the supported-features reference (what statements are covered).
Note: The single most important habit carries over from the mainframe unchanged: check
SQLCODEafter every SQL statement. Everything below builds on that.
The SQLCA: how your program learns the result
After every SQL operation, the runtime populates the SQL Communication Area (SQLCA) — the same diagnostic block your program reads on the mainframe. Your code branches on it to decide success, end-of-data, or error.
The fields you will use most:
| Field | Meaning |
|---|---|
SQLCODE |
0 = success, 100 = no row found / end of data, a negative value = error. |
SQLSTATE |
Standardized 5-character status code (portable across databases). |
SQLERRD(3) |
Number of rows affected by an INSERT, UPDATE, DELETE, or FETCH. |
SQLWARN0 |
Set to 'W' when any warning flag is raised. |
SQLERRM |
Message text/tokens for the most recent error. |
Common SQLCODE values you should handle:
SQLCODE |
Situation | Typical action |
|---|---|---|
0 |
Statement succeeded. | Continue. |
100 |
No data / end of result set. | Exit your fetch loop. |
-501 |
Cursor not open. | Open the cursor before fetching. |
-803 |
Duplicate key on insert/update. | Handle the conflict. |
-911 |
Deadlock or lock timeout (rolled back). | Retry the unit of work. |
-913 |
Lock timeout (not rolled back). | Retry or back off. |
The runtime maps the underlying JDBC/DB2 error into these fields automatically — when the database reports DB2-specific diagnostics, they are extracted into the SQLCA; otherwise a generic SQLCODE is set. You do not change how you read the SQLCA.
Working with cursors
Multi-row result sets work through cursors, with the familiar lifecycle:
EXEC SQL DECLARE C1 CURSOR FOR
SELECT COL1, COL2 FROM T ORDER BY COL1;
EXEC SQL OPEN C1;
DO WHILE (SQLCODE = 0);
EXEC SQL FETCH C1 INTO :COL1, :COL2;
IF SQLCODE = 0 THEN
/* process the row */;
END;
EXEC SQL CLOSE C1;
Each FETCH sets SQLCODE = 0 when a row was returned and SQLCODE = 100 when there are no more rows — that is how your loop knows to stop.
The runtime supports the cursor features your programs rely on:
-
Positioned updates/deletes — declare the cursor
FOR UPDATE OF ...and issueUPDATE ... WHERE CURRENT OF C1inside the fetch loop. -
WITH HOLD— a held cursor survives a commit so you can keep fetching across syncpoints. - Re-open — a cursor can be closed and opened again to re-run the query.
-
Multi-row fetch — fetching into array host variables retrieves a block of rows at once;
SQLERRD(3)tells you how many rows actually came back.
How PL/I data maps to SQL columns
Host variables are converted between PL/I storage and SQL column types automatically. The common mappings:
| PL/I declaration | SQL column type |
|---|---|
CHAR(n) / VARCHAR
|
CHAR, VARCHAR
|
FIXED BIN(15) |
SMALLINT |
FIXED BIN(31) |
INTEGER |
FIXED DEC(p,s) |
DECIMAL, NUMERIC
|
FLOAT BIN(53) |
DOUBLE, FLOAT
|
BIT(n) |
BINARY, VARBINARY
|
A detail worth knowing: TIMESTAMP values are returned in the 26-character PL/I format yyyy-mm-dd-hh.mm.ss.nnnnnnnnnn. Programs that overlay a timestamp on a fixed-length field depend on this exact width, so it is preserved precisely.
You can also use the standard SQL special registers — CURRENT DATE, CURRENT TIME, and CURRENT TIMESTAMP — in your statements as usual.
Dynamic SQL: PREPARE, EXECUTE, and DESCRIBE
Statements built at run time are fully supported:
-
PREPAREa statement string, thenEXECUTEit (optionallyUSINGhost variables), or open a cursor over a preparedSELECT. -
EXECUTE IMMEDIATEfor one-off statements with no parameters. -
DESCRIBEpopulates anSQLDAdescriptor area so your program can inspect the number, names, and types of the result columns or parameter markers before binding them. -
Stored procedures via
CALL, withIN,OUT, andINOUTparameters;OUTvalues are read back after the call returns.
Who owns the database connection
There are two ways your program gets a connection, and the difference matters for commit behavior:
-
Transaction programs (the common case). The execution platform creates, pools, owns, and enlists the JDBC connection in the current unit of work. Your
COMMIT/ROLLBACKhappen at the transaction boundary (SYNCPOINT), not as standalone statements. Do not close the connection yourself — it is reused across the programs in a transaction, and closing it breaks that reuse. -
Standalone programs (no managed transaction). The runtime builds a JDBC connection from your
sql.<schema>.*configuration and manages autocommit/isolation directly. HereCOMMITandROLLBACKapply immediately.
Connections are selected by schema: the runtime uses sql.<schema>.* settings when present and falls back to the default configuration otherwise. See How to Configure DB2 / SQL Connectivity for the property keys, and How Transactions Are Managed for the commit/rollback boundary.
Concurrency: each thread has its own SQL state
A migrated application typically serves many transactions on pooled threads. The runtime keeps the SQL state fully isolated per thread: each thread has its own SQLCA, its own open cursors, and its own connection per schema. Two transactions can therefore declare cursors with the same name and run independent units of work at the same time without interfering with one another. You do not add any locking in your program for this — it is automatic.
(For the bigger picture of how the runtime serializes program logic while letting database I/O overlap, see How Heirloom Handles Concurrency.)
A checklist for your programs
-
Check
SQLCODEafter every statement — and treat100as end-of-data, not an error. -
Exit fetch loops on
SQLCODE = 100. -
Retry on
-911/-913(deadlock/timeout) rather than failing the unit of work outright. -
Let the platform commit for transaction programs — don't issue standalone
COMMITs or close the connection. - Expect the 26-character timestamp format if you overlay timestamps on fixed-length storage.
Summary
- Your
EXEC SQLruns on JDBC but behaves like DB2: you read anSQLCA, drive cursors, and get converted host variables. -
SQLCODE(0/100/ negative) plusSQLSTATEandSQLERRD(3)are how your program learns what happened. - Cursors support positioned updates,
WITH HOLD, re-open, and multi-row fetch. - Data types convert automatically; timestamps keep the 26-character PL/I format.
- Dynamic SQL (
PREPARE/EXECUTE/DESCRIBE/CALL) and special registers are supported. - The execution platform owns and pools the connection for transaction programs — commit at the transaction boundary and never close it yourself.
- SQL state is isolated per thread, so concurrent transactions are safe with no extra locking.
Related articles
- How to Configure DB2 / SQL Connectivity — connection setup and commit/rollback timing.
-
Supported SQL Features — the full list of supported
EXEC SQLfeatures. - How Transactions Are Managed (ETP + PL/I Runtime) — the transaction boundary and resource lifecycle.
- How Heirloom Handles Concurrency — why concurrent transactions stay isolated.
- JVM Options & Runtime Configuration — enabling trace for diagnostics.
0 Comments