Programs that use EXEC SQL statements require a JDBC database connection at runtime. When the compiled program runs standalone (i.e., not in --lib mode), the generated main() method automatically bootstraps a JDBC connection from environment variables before program execution begins.
Statement Syntax - Free-Format and Fixed-Format
Embedded SQL is supported in both source formats, and both behave identically (same host-variable handling, same generated SQL).
Free-format uses EXEC SQL … ; - the statement may span multiple lines and ends at the semicolon:
EXEC SQL SELECT NAME INTO :custName
FROM CUSTOMER WHERE ID = :custId;
Fixed-format uses the classic SQLRPGLE precompiler block: C/EXEC SQL in columns 6 - 7 opens the block, one or more C+ continuation lines carry the SQL text (from column 9 onward), and C/END-EXEC closes it:
C/EXEC SQL
C+ SELECT NAME INTO :custName
C+ FROM CUSTOMER WHERE ID = :custId
C/END-EXEC
Continuation lines are joined with a single blank, so line breaks may fall anywhere a blank is allowed. A * in column 7 within the block is a comment line and is ignored.
Required Environment Variables
| Variable | Default Name | Required | Description |
|---|---|---|---|
| JDBC URL | JDBC_URL |
Yes | JDBC connection URL (e.g. jdbc:db2://host:port/database) |
| JDBC Driver | JDBC_DRIVER |
Yes | Fully-qualified JDBC driver class name (e.g. com.ibm.db2.jcc.DB2Driver) |
| JDBC User | JDBC_USER |
No | Database username |
| JDBC Password | JDBC_PASSWORD |
No | Database password |
If JDBC_USER and JDBC_PASSWORD are both unset, the connection is opened without credentials. Both JDBC_URL and JDBC_DRIVER must be set or the program will fail at startup with a clear error message.
The driver class is loaded explicitly via Class.forName() to ensure it registers with the JDBC DriverManager, regardless of whether the driver JAR includes automatic service provider metadata.
Database Backends
The SQL a compiled program issues is tailored to the database it is actually connected to. The backend is chosen at run time, from the connection - never fixed at compile time - so one compiled artifact runs unchanged against Db2 for i in production and a different database elsewhere.
Selection reads the JDBC driver's reported product name once per connection:
| Reported product | Backend | Notes |
|---|---|---|
Db2 for i (DB2 UDB for AS/400, DB2 for IBM i, …) |
Db2 for i | The production target. Statements are passed to the driver unchanged. |
| Db2 for Linux/UNIX/Windows | Db2 LUW | Row identity is RID_BIT, which IBM documents as possibly differing after a REORG. |
| Microsoft SQL Server | SQL Server | Exposes no supported physical row identifier; rows are re-found by key. |
| H2 | H2 | |
| anything else | generic | Best-effort. See below. |
The generic backend is best-effort and says so. On an unrecognised database the program reports, once, that no backend exists for that product and that fidelity to IBM i semantics is not guaranteed - decimal scale, collation, blank padding, date arithmetic and error codes follow the driver rather than IBM i. A supported backend reports nothing.
If a driver misreports its product, set RPGC_SQL_DIALECT to force a backend:
| Value | Backend |
|---|---|
DB2I |
Db2 for i |
DB2LUW |
Db2 LUW |
SQLSERVER |
SQL Server |
H2 |
H2 |
GENERIC |
generic |
The value is case-insensitive. A value naming no backend is ignored and the reported product name is used instead.
What Happens to a Statement Triton RPG Cannot Translate
On a non-Db2-for-i backend, a statement reaches the database in one of three ways, and it is worth knowing which one you are getting:
| Outcome | What reaches the database | What you see |
|---|---|---|
| Issued in the backend's own form | the backend's own form | nothing |
| Refused - it names something the backend cannot express | nothing - the statement is refused | an error naming the construct and the backend |
| Issued exactly as you wrote it | your statement, byte for byte | one warning, the first time it runs |
Treat the third outcome as unverified. Such a statement is handed to the driver as your program wrote it - same bytes, same whitespace, same comments, same case - and whatever that database makes of it is what happens. It may behave exactly as it does on Db2 for i. It may also differ in NULL ordering, string comparison and padding, decimal result scale, or date arithmetic, and it will not tell you which: the statement can succeed and still return a different answer than it would on IBM i. Issuing it unaltered is better than reshaping it into a guess, but it is not a guarantee of fidelity. A program that depends on such a statement should be verified against the backend you deploy on. The warning is raised once per statement, not once per execution, so a statement in a loop does not flood your log.
A construct with no equivalent is refused, never approximated. RRN is the clearest case: it is Db2 for i's physical row identifier, and SQL Server has no supported equivalent. Substituting a key or a row number would leave your program addressing a different row while reporting success, so the statement fails instead, naming RRN and the backend. Re-find the row by key if you need this on such a backend.
The SQLCA is not used to report any of this. Db2 for i leaves SQLWN0 and SQLWN1 - SQLWNA blank after an ordinary successful statement, so raising a flag here would make a program that branches on SQLWN0 behave differently than it does on IBM i. The warning is reported separately and the SQLCA you read is exactly what the driver reported.
On Db2 for i none of this applies. Statements go to the driver unchanged, as they always have, and no warning is ever raised.
A statement inside a loop raises its warning once, not once per iteration. Your statement text is never retained, and the statement issued is always built from the text your program supplied on that call. Db2 for i does not participate in this at all.
Overriding Environment Variable Names
If the default environment variable names conflict with your deployment environment, you can override them at compile time:
rpgc --jdbc-url-env MY_DB_URL \
--jdbc-user-env MY_DB_USER \
--jdbc-password-env MY_DB_PASS \
--jdbc-driver-env MY_DB_DRIVER \
MYPROGRAM.rpgle
The overridden names are baked into the compiled class as constants. At runtime, the program reads from the specified environment variables instead of the defaults. This applies on every run path - the standalone main() entry point and the Headless Bridge alike.
Implicit SQLCA Variables
When a program contains any EXEC SQL statement, the compiler automatically defines the SQL Communication Area (SQLCA) variables that are set after each SQL operation, matching IBM i SQL precompiler behavior:
| Variable | Type | Description |
|---|---|---|
SQLCAID |
CHAR(8) | Eye-catcher, always 'SQLCA' blank-padded to 8 |
SQLCABC |
INT(10) | Byte length of the SQLCA structure: 136 |
SQLCODE |
INT(10) | Integer completion code: 0 = success, 100 = no data found, negative = error |
SQLSTATE |
CHAR(5) | 5-character state code: '00000' = success, '02000' = not found |
SQLER1 - SQLER6 |
INT(10) | SQLERRD array elements. SQLER3 holds the row count after INSERT, UPDATE, DELETE |
SQLWN0 - SQLWNA |
CHAR(1) | SQLWARN flags - 'W' when set, blank when not. See Warning Flags below |
SQLERRML |
INT(5) | Meaningful length of SQLERRMC (0 while SQLERRMC is blank) |
SQLERRMC |
CHAR(70) | Diagnostic message text of the last statement, blank-padded to 70; blank after a statement that succeeds |
SQLERRP |
CHAR(8) | Identifier of the runtime that reports the condition - always 'TRITON', set after any statement |
These variables do not need to be declared with DCL-S; they are available implicitly. Referencing them in a program that does not contain EXEC SQL will produce an "Undefined variable" error.
Every character field above is fixed-length, not varying. %LEN therefore reports the declared length (%LEN(SQLERRP) is always 8, %LEN(SQLERRMC) always 70), the value is blank-padded to that width, and a field that has not been set compares equal to blanks.
The numeric fields carry the widths IBM declares them with, so %LEN(SQLCODE) is 10 and SQLER3 describes a row count well past 99,999. SQLCABC and SQLCAID are constants describing the SQLCA itself; no SQL statement changes them.
IBM ILE RPG accepts both long and short subfield names for the SQLCA. The short names are aliases - SQLCOD references the same storage as SQLCODE:
| Short name | Long name equivalent |
|---|---|
SQLAID |
SQLCAID |
SQLABC |
SQLCABC |
SQLCOD |
SQLCODE |
SQLSTT |
SQLSTATE |
SQLERM |
SQLERRMC |
SQLERL |
SQLERRML |
SQLERP |
SQLERRP |
SQLERRMC and SQLERRP differ from IBM i in what they carry, because the underlying values are not available off-platform. On IBM i, SQLERRMC holds the message replacement tokens (delimited by X'FF') and SQLERRP holds the name of the internal Db2 module that detected the condition (for example QSQINS after a duplicate-key insert). Those are IBM-internal values a JDBC driver never exposes, so the compiler reports the truthful equivalents it does have: SQLERRMC receives the driver's formatted first-level message text (truncated to 70 characters, with SQLERRML giving its length), and SQLERRP reports the fixed identifier 'TRITON' rather than imitating a QSQ… module name. The canonical error-logging idiom errMsg = 'SQL error: ' + %trim(SQLERRMC) therefore yields a usable message; code that parses SQLERRMC as X'FF'-delimited tokens, or that keys off a specific QSQ… value in SQLERRP, will not see those exact bytes.
Warning Flags: String Truncation
When a SELECT ... INTO or a FETCH assigns a value to a character host variable
that is too small to hold it, the value is truncated and SQLCODE stays 0. The
only signal is the SQLCA:
SQLSTATEbecomes'01004',SQLWN1becomes'W'(string truncation), andSQLWN0becomes'W'- the summary flag, set whenever any other flag is set.
So a program that wants to notice truncation must check the flags, not SQLCODE:
exec sql SELECT name INTO :shortName FROM cust WHERE id = :id;
if SQLWN1 = 'W';
// the name did not fit and was cut short
endif;
The flags are rebuilt by every SQL statement, so a clean statement after a warning clears them - the SQLCA always describes the most recent operation only.
A value is reported as truncated when what the database returned is longer than the
host variable can hold. Note this compares lengths, not content: as on IBM i, a
CHAR(10) column holding 'AB' fetched into a CHAR(5) host variable does raise
the flag, even though the characters dropped were only the blanks padding the column
out to its declared width.
Warning Flags: Null Elimination and Missing WHERE
Two further conditions raise flags, again with SQLCODE left at 0:
- A null value eliminated from an aggregate. When a column function such as
AVG,SUM,MIN, orMAXskips one or more null values,SQLSTATEbecomes'01003'andSQLWN2(with the summarySQLWN0) becomes'W'. The aggregate result is still returned. This flag reflects the completion warning the database reports, so it is raised when running against Db2 for i; a database that does not report the condition simply leaves the flag blank. - A searched UPDATE or DELETE with no WHERE clause. Changing every row is
flagged:
SQLSTATEbecomes'01504'andSQLWN4(withSQLWN0) becomes'W'. AWHEREclause - even one that matches every row - suppresses it; aWHEREthat appears only inside a subquery does not, because the statement itself still has no search condition.
exec sql UPDATE cust SET active = 'N'; // no WHERE - every row changed
if SQLWN4 = 'W';
// guard against an accidental whole-table update
endif;
The remaining flags - SQLWN3 and SQLWN5 through SQLWNA - are declared and
always blank: they report conditions that cannot arise here (for example SQLWN3, a mismatch between the number of result columns and
host variables, cannot arise because the two are matched when the statement is
compiled).
Supported Host Variable Types
SQL host variables (prefixed with : in EXEC SQL statements) support the following RPG data types:
| RPG Type | SQL Column Types | Notes |
|---|---|---|
| PACKED / ZONED | DECIMAL, NUMERIC, INTEGER, SMALLINT, BIGINT | Automatic BigDecimal conversion |
| BINDEC | DECIMAL, NUMERIC, INTEGER, SMALLINT, BIGINT | Automatic BigDecimal conversion (binary-decimal representation) |
| FLOAT(4) / FLOAT(8) | FLOAT, REAL, DOUBLE, and any numeric column | Bound as input (INSERT/UPDATE VALUES, WHERE predicates, CALL arguments) and populated on FETCH INTO / SELECT INTO via Number.floatValue() / doubleValue() |
| INT | INTEGER, SMALLINT | Via Number.intValue() |
| INT(10) / INT(20) | BIGINT | Via Number.longValue() |
| VARCHAR / CHAR | CHAR, VARCHAR, CLOB | Direct String mapping |
| DATE | DATE | Bound as input and populated on FETCH INTO / SELECT INTO |
| TIME | TIME | Bound as input and populated on FETCH INTO / SELECT INTO |
| TIMESTAMP | TIMESTAMP | Bound as input and populated on FETCH INTO / SELECT INTO |
| SQLTYPE(RESULT_SET_LOCATOR) | - | Declared as INT(20); names a result set a stored procedure returned (see Result sets from a stored procedure) |
| SQLTYPE(CLOB:n) / (BLOB:n) / (DBCLOB:n) | CLOB, BLOB, DBCLOB | Declared as a varying-length character host variable of the declared capacity. The length is required and must be 1 to 16,773,100 - see LOB host variables |
A single-row SELECT ... INTO / FETCH ... INTO populates a packed, zoned,
binary-decimal, or float host variable with the fetched value, coercing whatever
numeric type the driver returns to the target's type and scale. A SQL NULL leaves
the host variable unchanged.
DATE, TIME, and TIMESTAMP host variables are bound correctly as input
values wherever a host variable supplies data - INSERT/UPDATE VALUES, WHERE
predicates, and stored-procedure CALL arguments. They are translated to the
JDBC date/time/timestamp types the driver expects (a raw temporal value would
otherwise be rejected by Db2 for i with SQLSTATE 07006, "Data type mismatch").
FETCH INTO / SELECT INTO output: a DATE column populates its RpgDate host
variable, a TIME column its RpgTime host variable, and a TIMESTAMP column its
RpgTimestamp host variable. A SQL NULL leaves the host variable unchanged.
Null indicator variables
When a column may contain NULL, a null indicator variable can follow the host variable
(separated by a space, no comma). The indicator must be declared as INT(5):
DCL-S empSal PACKED(9:2);
DCL-S empInd INT(5);
EXEC SQL SELECT SALARY INTO :empSal :empInd FROM EMP WHERE EMPNO = :empNo;
IF empInd = -1;
// SALARY was NULL
ENDIF;
After the fetch, the indicator is set to 0 (non-null) or -1 (null). When the column is NULL, the host variable's value is not meaningful - the indicator must be checked first. Without an indicator, a NULL column produces SQLCODE -305 (SQLSTATE 22002) and no host variables are assigned.
The optional INDICATOR keyword may appear between the host variable and its indicator:
:empSal INDICATOR :empInd.
Host variables can be declared at any scope: module-level (DCL-S), procedure-local variables, or procedure parameters. For by-reference parameters, the caller sees the updated value after the SQL statement executes, matching IBM i behavior.
Data structures as host structures
A data structure may be used as a single host variable - a host structure. rpgc
expands it to one host variable per subfield, in declaration order, so a single
:myDs reference binds (or receives) every subfield:
DCL-DS employee QUALIFIED;
empno CHAR(6);
name VARCHAR(30);
salary PACKED(9:2);
END-DS;
// Equivalent to VALUES(:employee.empno, :employee.name, :employee.salary)
EXEC SQL INSERT INTO EMPLOYEE VALUES(:employee);
The structure may be declared at any scope - module-level or procedure-local - and may
be defined directly, with LIKEDS(template), or with EXTNAME(...); the subfields
resolved from the template or external file drive the expansion. The number of subfields
must match the number of columns referenced.
Qualified subfields as host variables
A single subfield may also be named directly, as :structure.subfield:
DCL-DS employee QUALIFIED;
empno CHAR(6);
name VARCHAR(30);
END-DS;
EXEC SQL SELECT NAME INTO :employee.name FROM EMPLOYEE
WHERE EMPNO = :employee.empno;
This works in every host-variable position - the INTO list, a WHERE predicate,
an INSERT VALUES list, a SET clause, FETCH ... INTO, OPEN ... USING, and
EXECUTE ... USING - and the subfield may carry a null indicator, which may itself
be qualified:
EXEC SQL SELECT NAME INTO :employee.name :flags.nameNull FROM EMPLOYEE;
Exactly one level of qualification is allowed, matching IBM's SQL precompiler.
A subfield of a subfield cannot be named - if outer has a subfield inner defined
with LIKEDS, :outer.inner.field is not a valid host variable. Reference the inner
structure through a separate LIKEDS variable, or use the whole structure as a host
structure instead.
Common patterns:
**FREE
DCL-S CustName VARCHAR(40);
EXEC SQL DECLARE C1 CURSOR FOR SELECT NAME FROM CUSTOMER;
EXEC SQL OPEN C1;
EXEC SQL FETCH NEXT FROM C1 INTO :CustName;
// Standard cursor loop using SQLCODE
DOW SQLCODE = 0;
DSPLY CustName;
EXEC SQL FETCH NEXT FROM C1 INTO :CustName;
ENDDO;
EXEC SQL CLOSE C1;
CALL (Stored Procedure)
Invoke a stored procedure with host variables as parameters:
DCL-S inVal PACKED(9:2);
DCL-S outVal PACKED(9:2);
inVal = 42.5;
EXEC SQL CALL MYLIB.MYPROC(:inVal, :outVal);
The CALL executes as a CallableStatement on the JDBC connection. As on IBM i,
the statement itself does not mark parameter direction - each parameter's
direction (IN, OUT, or INOUT) is taken from the procedure's own declaration in
the catalog. IN and INOUT parameters are bound from their host variables; after
the call, OUT and INOUT parameters are read back into their host variables (an
OUT parameter that comes back null leaves its host variable unchanged, the same
as a null column on a FETCH without an indicator). If the procedure's catalog
entry cannot be read, every parameter is treated as IN.
Multi-Row FETCH
Retrieve multiple rows at once into a DIM'd data structure array:
**FREE
DCL-DS rowDs EXTNAME('MYTABLE') QUALIFIED DIM(10);
END-DS;
DCL-S nRows UNS(5) INZ(%ELEM(rowDs));
DCL-S fetched UNS(5);
EXEC SQL DECLARE C1 CURSOR FOR SELECT * FROM MYTABLE;
EXEC SQL OPEN C1;
EXEC SQL FETCH C1 FOR :nRows ROWS INTO :rowDs;
EXEC SQL GET DIAGNOSTICS :fetched = ROW_COUNT;
FOR i = 1 TO fetched;
DSPLY rowDs(i).NAME;
ENDFOR;
EXEC SQL CLOSE C1;
The FOR :n ROWS clause fetches up to n rows in a single operation. Use
GET DIAGNOSTICS :var = ROW_COUNT to determine how many rows were actually
fetched (may be less than requested at end of result set). Each column is
decoded into its declared subfield type - packed, zoned, integer, unsigned,
float, date/time/timestamp, and character subfields are all populated from the
fetched row, not just character fields.
The host-structure array may be declared in either free-format
(DCL-DS … DIM(n)) or fixed-format (a D-spec data-structure header carrying the
DIM(n) keyword). Both are accepted as multi-row FETCH targets, and in both
%ELEM returns the declared dimension and arr(i).subfield reads an element's
subfield. The equivalent fixed-format declaration is:
Drows DS QUALIFIED DIM(10)
D id 5P 0
D name 6A
D amt 7P 2
DnRows S 5I 0 INZ(%ELEM(rows))
SQLCODE values follow IBM i conventions:
- 0: The SQL statement completed successfully.
- 100: No row was found (SELECT INTO with no match, or FETCH past end of cursor).
- Negative: An error occurred (e.g., -803 for duplicate key constraint violation). The exact negative value corresponds to the database vendor's error code.
LOB host variables
SQLTYPE(CLOB:n), SQLTYPE(BLOB:n) and SQLTYPE(DBCLOB:n) declare a large-object
host variable whose capacity is the declared length:
**FREE
DCL-S doc SQLTYPE(CLOB:1048576);
DCL-S image SQLTYPE(BLOB:100000);
- The length is required.
SQLTYPE(CLOB)with no length is rejected (SQL0104), so rpgc does too - see TRN1059. It is never defaulted. - The length must be 1 to 16,773,100 bytes, the range Db2 for i accepts. A larger value is rejected there (SQL0604) and here.
- The declared length is honored exactly, at any size in range - a 1 MB CLOB holds 1 MB.
- Write a plain byte count, not a multiplier.
SQLTYPE(CLOB:1M)is valid in SQL DDL but not in the RPGSQLTYPEkeyword; the IBM i precompiler rejects it. WriteSQLTYPE(CLOB:1048576).
Scrollable cursors (SCROLL)
A cursor declared SCROLL can be walked in either direction and repositioned
relative to its current row:
**FREE
EXEC SQL DECLARE C1 SCROLL CURSOR FOR
SELECT ID, NAME FROM CUSTOMER ORDER BY ID;
EXEC SQL OPEN C1;
EXEC SQL FETCH LAST FROM C1 INTO :id, :name;
EXEC SQL FETCH PRIOR FROM C1 INTO :id, :name;
EXEC SQL FETCH RELATIVE -2 FROM C1 INTO :id, :name;
EXEC SQL CLOSE C1;
Supported orientations:
| Orientation | Moves to |
|---|---|
NEXT (the default) |
the following row |
PRIOR |
the preceding row |
FIRST |
the first row |
LAST |
the last row |
CURRENT |
nowhere - re-reads the row the cursor is on |
RELATIVE n |
n rows from the current one; n may be negative, and RELATIVE 0 is the same as CURRENT |
Behavior, matching IBM i:
-
Running off either end sets SQLCODE 100 (SQLSTATE
02000) - the same end-of-data code a forward-onlyFETCHgives, not a distinct error. -
After running off an end the cursor parks at that end rather than sticking: a
NEXTafter a before-first hit returns the first row, and aPRIORafter an after-last hit returns the last.RELATIVEbehaves the same way when it overshoots. -
CURRENT(andRELATIVE 0) with the cursor on no row - straight afterOPEN, or after running off an end - reports SQLCODE 100. -
A plain
SCROLLdeclaration is insensitive: the result set is a stable snapshot.INSENSITIVE SCROLL,SENSITIVE SCROLLandASENSITIVE SCROLLare accepted and behave the same way; a sensitivity keyword may also appear withoutSCROLL, leaving the cursor forward-only. -
WITH HOLDandWITHOUT HOLDare accepted on the declaration. -
A cursor declared without
SCROLLis forward-only, exactly as before. -
BEFOREandAFTERposition the cursor without returning a row:FETCH BEFOREparks ahead of the first row andFETCH AFTERpast the last. Both report SQLCODE 0 - not 100, since nothing went wrong - and assign no host variable, leaving every one exactly as the program set it. TheFETCHthat follows resumes from the adjacent row, so aNEXTafterBEFOREreturns the first row and aPRIORafterAFTERreturns the last. Neither takes anINTOclause; one written there is rejected with TRN3033.
exec sql FETCH BEFORE FROM C1; // SQLCODE 0, nothing assigned
exec sql FETCH NEXT FROM C1 INTO :id, :name; // the first row
FETCH ABSOLUTE is rejected, because Db2 for i has no such orientation - its
own precompiler reports "Token ABSOLUTE was not valid. Valid tokens: RELATIVE".
It produces TRN3033. To reach a specific row
number, use FETCH FIRST followed by FETCH RELATIVE n.
Both source formats are supported.
Result sets from a stored procedure
A procedure declared DYNAMIC RESULT SETS hands its rows back as open cursors
rather than through OUT parameters. The caller declares a result set locator,
CALLs the procedure, associates the locator with a returned result set, allocates
a cursor over it, and FETCHes:
**FREE
DCL-S rs SQLTYPE(RESULT_SET_LOCATOR);
DCL-S rid INT(10);
DCL-S rname CHAR(10);
EXEC SQL CALL MYLIB.MYPROC();
EXEC SQL ASSOCIATE RESULT SET LOCATOR (:rs) WITH PROCEDURE MYLIB.MYPROC;
EXEC SQL ALLOCATE C1 CURSOR FOR RESULT SET :rs;
DOU SQLCODE <> 0;
EXEC SQL FETCH C1 INTO :rid, :rname;
...
ENDDO;
EXEC SQL CLOSE C1;
Behavior, matching IBM i:
SQLTYPE(RESULT_SET_LOCATOR)declares anINT(20)host variable. Its value is opaque - onlyALLOCATEinterprets it.- A
CALLto a procedure that returned result sets sets SQLCODE +466 (SQLSTATE0100C), a warning, not an error. A program that testsSQLCODE = 0after such a CALL sees a non-zero code. A CALL that returns no result sets reports 0 as before. ASSOCIATE [RESULT SET] LOCATOR(S) (:rs1, :rs2) WITH PROCEDURE namefills the locators in the order the procedure returned the result sets. When there are none to claim - including when the procedure was never called - it reports SQLCODE +482 (SQLSTATE02503), again a warning, and leaves the locator variables alone.ALLOCATE cursor CURSOR FOR RESULT SET :rsregisters an ordinary cursor, soFETCHandCLOSEwork on it exactly as on aDECLAREd one, and end of data sets SQLCODE 100. A locator naming no result set is an error: SQLCODE -423 (SQLSTATE0F001).- The rows come from the procedure's own open cursor; nothing is re-executed on the caller's side.
- The procedure name may be written qualified or unqualified, in any case, and may be supplied by a host variable.
Both source formats are supported; the statements are written in a fixed-format
C/EXEC SQL block exactly as any other.
Multiple result sets and the JDBC driver. Claiming more than one result set
from a single CALL requires a driver that can leave an earlier result set open
while moving to the next. The IBM Toolbox for Java driver (jt400) does not
support that call and reports SQLSTATE IM001; against such a driver the first
result set is retained and any further ones go unclaimed. A procedure returning
one result set - the common case - is unaffected.
Isolation clause (WITH NC / UR / CS / RS / RR)
A statement may carry a trailing isolation clause. The requested level is applied
to the JDBC connection for the statement, so the same source runs against any JDBC
backend, not only Db2 for i. WITH NC ("no commit") and WITH UR map to
read-uncommitted, CS to read-committed, RS to repeatable-read, and RR to
serializable.
**FREE
EXEC SQL INSERT INTO EMPLOYEE VALUES (:newEmp) WITH NC;
EXEC SQL SELECT NAME INTO :n FROM EMPLOYEE WHERE ID = :id WITH UR;
Lock-release timing note for
WITH CSon non-Db2 backends. On Db2 for i, Cursor Stability holds a shared lock on the current cursor row until the cursor moves to the next row. JDBCREAD_COMMITTED(the mapped level) releases shared locks as soon as the read completes. No standard JDBC isolation level exactly reproduces CS's "hold until cursor moves" semantics - >REPEATABLE_READwould hold all read locks until transaction end, causing unnecessary deadlocks.READ_COMMITTEDis the pragmatic choice: it prevents dirty reads (the core guarantee of CS) while avoiding over-locking. Programs that rely on CS to prevent concurrent modification of the current cursor row may see weaker guarantees on non-Db2 backends.
SET OPTION
The SET OPTION statement specifies SQL precompile options (naming convention, date format, commit level, etc.). Most options are governed by JDBC connection properties and are accepted without further effect, with one exception: COMMIT = *NONE opts the module out of SQL commitment control, so autocommit is left on even when a file is declared with the COMMIT keyword (the SET OPTION value governs the module's commit level).
**FREE
EXEC SQL SET OPTION DATFMT = *ISO, CLOSQLCSR = *ENDMOD, COMMIT = *NONE;
GET DIAGNOSTICS
The GET DIAGNOSTICS statement retrieves diagnostic information about the most recently executed SQL statement into one or more host variables:
**FREE
DCL-S Rows INT(10);
EXEC SQL DELETE FROM ORDERS WHERE STATUS = 'CLOSED';
EXEC SQL GET DIAGNOSTICS :Rows = ROW_COUNT;
DSPLY Rows; // displays the number of deleted rows
Two forms are accepted: the statement-information form shown above, and the
condition-information form with a leading CONDITION n clause. Because a single
diagnostic area is surfaced, the condition number is accepted and ignored - use
CONDITION 1. Several items may be assigned in one statement:
**FREE
DCL-S Code INT(10);
DCL-S State CHAR(5);
DCL-S Text VARCHAR(200);
EXEC SQL INSERT INTO ORDERS VALUES('A0001');
EXEC SQL GET DIAGNOSTICS CONDITION 1
:Code = DB2_RETURNED_SQLCODE,
:State = RETURNED_SQLSTATE,
:Text = MESSAGE_TEXT;
Supported diagnostic items:
ROW_COUNT- the number of rows affected by the preceding INSERT, UPDATE, DELETE, or FETCH statement.DB2_RETURNED_SQLCODE- the SQLCODE of the most recently executed statement.RETURNED_SQLSTATE- the five-character SQLSTATE of the most recently executed statement.MESSAGE_TEXT- the database engine's diagnostic message for that statement (blank when it succeeded).MESSAGE_LENGTH- the length ofMESSAGE_TEXT.DB2_NUMBER_ROWS- the number of rows in the result set of the preceding statement.DB2_RETURN_STATUS- the return status of the precedingCALL.NUMBER,MORE,CONDITION_NUMBER,CONNECTION_NAME,DB2_SQL_NESTING_LEVEL- describe the single diagnostic area a compiled program surfaces.
Any other diagnostic item is rejected at compile time with TRN3019 rather than silently ignored, since leaving the target host variable unchanged would misrepresent a failed statement as having succeeded.
Library Mode
Programs compiled with --lib do not generate a main() method and therefore do not include any JDBC bootstrap logic. Library consumers are responsible for calling _initSql(java.sql.Connection) on the program instance before invoking run().
Constructing a program (new Foo()) fully initializes it - there is no separate init call. Each instance is one activation with its own program state. The JDBC connection (and its commit boundary) and the named data areas are owned by an activation group rather than the individual program, so that a unit of work and data areas can be shared across cooperating programs:
- Single program (the common case): just
new Foo(); a private activation group is created automatically and_initSqlwires the connection into it. - Multiple programs sharing one transaction / data areas: create one
RpgActivationGroup, callsetActivationGroup(group)on each program before use (and before_initSql), and aCOMMIT/ROLBKin one program then applies to the shared connection used by the others. - Named activation groups:
RpgRuntime.getNamedActivationGroup("NAME")returns a persistent per-thread activation group. Programs compiled withACTGRP('NAME')share one AG instance per name per thread, matching IBM i semantics. - Server environments: call
RpgRuntime.endJob()at request boundaries (servlet filter, Spring interceptor) to prevent per-thread state from leaking across pooled-thread reuse. This resets all request-scoped runtime state on the current thread, including named activation groups.
Example
# Compile
rpgc -o out INVOICES.rpgle
# Run with Db2 on IBM i
export JDBC_URL="jdbc:as400://myhost/MYLIB"
export JDBC_DRIVER="com.ibm.as400.access.AS400JDBCDriver"
export JDBC_USER="MYUSER"
export JDBC_PASSWORD="MYPASS"
java -cp out:triton-rpg.jar:jt400.jar INVOICES
Schema conformance
When you run compiled RPG against a database other than Db2 for i, the shape of
the target schema decides whether your program reads the right rows. A CHAR(30)
column under a binary collation compares and sorts the way Db2 for i does. The
same column as VARCHAR(30) under a case-insensitive collation does not - the
same WHERE selects different rows and the same ORDER BY sequences them
differently, with no error raised anywhere.
--schema-map lets you state what your migration produced, so that a mismatch
stops the program instead of quietly changing its answers.
Turning it on
Checking is off until you supply a map:
rpgc --schema-map schema-map.yaml INVCALC.rpgle
With no map, nothing is verified and the compiled program runs against any supported database. With one, the first open of each file compares its columns against the contract and raises a fatal error if they disagree:
Schema conformance: INVMAST.CUSTNAME
expected CHAR(30) COLLATE Latin1_General_BIN2
found VARCHAR(30)
A variable-length column does not store trailing blanks, so the
value read back is shorter than the fixed-length field it came
from: LENGTH and concatenation give different results, and any
expression built on the stored length follows them.
Equality and ORDER BY are not affected - Db2 pads both operands
to compare them - so this changes computed values, not which
rows match.
The check runs once per file, not once per record, so a keyed read loop pays a single catalog lookup.
The map file
One file per project, written for one target database:
# Applies to every character column.
character:
collation: Latin1_General_BIN2
fixed: CHAR
nullable: false
# Applies to every column of one RPG type.
types:
packed: DECIMAL
zoned: DECIMAL
timestamp: DATETIME2(6)
# Applies to one column of one file.
columns:
INVMAST:
ORDDATE: { type: DATETIME2(3) }
CUSTNAME: { collation: SQL_Latin1_General_CP1_CI_AS }
The three sections narrow in scope, and the most specific statement wins: a
columns entry beats a types entry, which beats character. Each of type,
collation and nullable resolves independently, so naming only a collation for
a column leaves its type as the broader rules set it.
The map does not replace the built-in mapping for your target, it layers over it. That is why supplying a map checks every column of every file your program declares, not only the columns you named - you name the places your migration differs, and the rest is held to the default.
What a declaration means
A declaration says this is what my schema is, not accept whatever is there.
Declaring CUSTNAME as case-insensitive makes the contract require a
case-insensitive column; a database that is anything else still fails. There is
no "ignore this column" setting, and rpgc never generates DDL or reports on
schema drift - your schema is yours.
Db2 for i is never checked
On Db2 for i the schema is the source of truth your file layouts were resolved from, so there is nothing to verify and no check runs - even when the artifact carries a map written for another database.
That is what makes a staged migration possible. Compile once with the map for your future target, keep running against Db2 for i, and switch when you are ready; the same artifact works on both, so you can run them side by side and compare output. Nothing needs recompiling at cutover.