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:

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:

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:

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);

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:

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:

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 CS on 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. JDBC READ_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_READ would hold all read locks until transaction end, causing unnecessary deadlocks. READ_COMMITTED is 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:

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:

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.