Fixed-format RPG programs can declare externally-described keyed DISK files using a fixed-format F-spec:

     FINVMAST   IF   E           K DISK

rpgc supports externally-described keyed DISK files declared with an F-spec. The file's field structure can be resolved from either a local DDS physical-file source file (.pf) or a live Db2 for i catalog via JDBC (verify mode).

In free-format, the equivalent declaration is DCL-F. The device keyword is optional for an externally-described file - when omitted it defaults to DISK, so all three of these declare the same keyed DISK file:

DCL-F INVMAST DISK KEYED USAGE(*INPUT);
DCL-F INVMAST KEYED USAGE(*INPUT);       // device omitted - defaults to DISK
DCL-F INVMAST;                            // device and usage omitted

PRINTER and WORKSTN files must still name their device explicitly; only the DISK default is implicit.

A program-described DISK file - one whose record layout is defined by the program rather than by an external file object - is declared by giving its record length as the device keyword's argument:

DCL-F ARRTBL DISK(10) USAGE(*INPUT);   // program-described, 10-byte records

A device keyword with no argument (DISK) is externally-described - the default; a device keyword with a record-length argument (DISK(10)) is program-described. This is the free-format spelling of the fixed-format F-spec record-length columns, so the same file declares identically in either format. PRINTER(132) carries a print-line width and WORKSTN(200) a program-described record length the same way.

Program-described record I/O

READ file ds, WRITE file ds and UPDATE file ds move the record as a byte image through the named data structure. The image is a plain concatenation of each column's storage image, in column order, with no padding or alignment - a record of CHAR(4), NUMERIC(5,0) and DECIMAL(5,2) occupies 4 + 5 + 3 = 12 bytes, with the character column held as characters, the zoned column as its numeric characters, and the packed column as BCD:

DCL-F MCOL DISK(12) USAGE(*UPDATE);
DCL-DS rec;
  sc1 CHAR(4) POS(1);
  sn1 ZONED(5:0) POS(5);
  sp1 PACKED(5:2) POS(10);
END-DS;
READ MCOL rec;
sc1 = 'WXYZ';
UPDATE MCOL rec;

Declare the structure's subfields at whatever positions or lengths match the file's column layout; the compiler reads that layout from the file's external description without making the column names visible to the program.

A negative zoned or packed value is carried the way IBM i carries it - the sign is overpunched into the existing image rather than occupying a byte of its own, so the field's width is the same either way. A ZONED(5:0) holding -12345 is x'F1F2F3F4D5' (the zone of the last byte becomes D), and a PACKED(5:2) holding -3.14 is x'00314D' (the low nibble becomes D).

Keyed access in fixed format

A fixed-format F-spec declares keyed access in a different column depending on how the file is described, and rpgc honours both spellings:

Description Column Entry
Externally described 34 (record address type) K
Program described 35 (file organization) I - with the key's length in columns 29-33
     FKLPF      IF   F   20     5AIDISK    KEYLOC(6)

Here column 22 F marks the file program-described, columns 23-27 give the record length, columns 29-33 the key length, column 34 A the key's type, and column 35 I the indexed organization. A file declared this way reads in key sequence; one without the I reads in arrival sequence.

KEYLOC(n) is accepted and does not affect which record an operation retrieves. Search arguments come from factor 1, and an operation that takes its key from the current record - READE with no factor 1 - uses the key the file was positioned on. This matches IBM i, which also does not require KEYLOC to agree with the file's actual key.

Supported column types

Character, zoned, packed, DATE, TIME and TIMESTAMP columns are supported.

The three date/time types occupy their *ISO text form in the record image, at the widths below - the form is *ISO regardless of the job's date format, and the separators are those *ISO uses:

Column type Bytes Image
DATE 10 yyyy-mm-dd
TIME 8 hh.mm.ss (dots)
TIMESTAMP 26 yyyy-mm-dd-hh.mm.ss.ffffff (six fractional places)

Declare the receiving subfield as D, T or Z at the matching position, or as a character subfield of that width if the program wants the text.

A column of any other type - binary and float among them - has no byte-image representation here and is rejected with TRN1060, as is an operation that names no data structure at all. That rejection is deliberate: a column whose image is not defined would otherwise reach the program as blank bytes, which reads as a wrong value rather than as an error. Read such a file as externally described instead, which supports the full type set. A program-described PRINTER file is a separate case: writing a data structure to one renders it as a print line.

The stated record length

DISK(n) states how much data each operation transfers, and it must be at least the summed storage widths of the columns. A longer length is accepted - the surplus bytes simply go unused. A shorter one is rejected with TRN1062: it is not a smaller read, it leaves the trailing subfields never receiving data at all, which on IBM i surfaces as a decimal-data error at run time rather than at compile time.

DDS Physical File Source

Place the DDS physical-file source alongside the RPG source or in the directory specified by --dds-path. The file must be named <FILENAME>.pf (case-insensitive). Example:

     A          R INVMASTR
     A            WHSE           4A         TEXT('Warehouse code')
     A            PARTNO        10A         TEXT('Part number')
     A            QTYOH          7P 0       TEXT('Quantity on hand')
     A          K WHSE
     A          K PARTNO

At compile time, the compiler reads the .pf file and automatically injects the field declarations (WHSE, PARTNO, QTYOH, etc.) as program variables. No explicit D-spec declarations are needed for DDS fields.

A DDS A (character) field is fixed-length: it holds exactly its declared length, blank-padded, so %LEN returns the column width and bracketed concatenation includes the trailing blanks - matching IBM i. Add the VARLEN keyword to a character field to make it variable-length instead.

SQL DDL (CREATE TABLE) Source

When a file's layout is defined in SQL DDL rather than DDS - a CREATE TABLE statement, as produced by qsqlsrc/*.table members - the compiler can resolve the external description directly from that source, with no hand-written mirror .pf. Place the DDL alongside the RPG source or in the --dds-path directory, named <FILENAME>.table (case-insensitive). Example:

CREATE OR REPLACE TABLE EMPLOYEE
      (EMPNO       CHAR(6)         NOT NULL,
       FIRSTNME    VARCHAR(12)     NOT NULL,
       EDLEVEL     SMALLINT        NOT NULL,
       SALARY      DECIMAL(9,2)            ,
       PRIMARY KEY (EMPNO));

Both a fixed-format E DS/F-spec and a free-format DCL-DS ... EXTNAME('EMPLOYEE') / DCL-F resolve their fields from this table exactly as they would from a .pf. -- line comments and /* */ block comments, CREATE OR REPLACE, a schema-qualified name, the PRIMARY KEY clause, and trailing ALTER TABLE / constraint statements are all handled; the PRIMARY KEY columns become the file's key fields.

Each SQL column maps to the RPG field type that native ILE RPG externally-describes it as:

SQL column type RPG field
CHAR(n) fixed character, length n
VARCHAR(n) / CHAR VARYING(n) varying character, length n
SMALLINT binary, 4 digits (4B 0)
INTEGER / INT binary, 9 digits (9B 0)
BIGINT integer, 20 digits (20I 0)
DECIMAL(p,s) packed decimal
NUMERIC(p,s) zoned decimal
DATE / TIME / TIMESTAMP date / time / timestamp
REAL 4-byte float
DOUBLE / FLOAT 8-byte float
GRAPHIC(n) / VARGRAPHIC(n) graphic (UCS-2 when the column CCSID is 13488 or 1200)

SMALLINT and INTEGER map to binary fields, not integer fields - this is how those columns are externally described. A SQL column with a long name (over 10 characters) is imported under its generated system name, with the long name available as its ALIAS; a FOR COLUMN clause supplies the system name explicitly. Any other SQL column type (DECFLOAT, BLOB/CLOB/DBCLOB, XML, ROWID, BINARY/VARBINARY, BOOLEAN) has no faithful RPG external field type and is rejected with TRN4003 rather than silently mistyped.

Resolution order is .pf DDS first, then .table SQL DDL, then verify mode - a .pf of the same name takes precedence over a .table.

Verify Mode (Catalog Resolution)

When no local .pf file is available, the compiler can resolve externally-described file layouts by querying the Db2 for i system catalog at compile time. This is called verify mode and is enabled by passing a JDBC URL:

JDBC_USER=MYUSER JDBC_PASSWORD=MYPASS \
rpgc --verify-jdbc-url "jdbc:as400://myhost/MYLIB" \
     --verify-jdbc-driver com.ibm.as400.access.AS400JDBCDriver \
     --verify-library APPLIB \
     --verify-library QGPL \
     MYPROG.rpgle

The catalog user and password are read from the environment (by default JDBC_USER / JDBC_PASSWORD), never passed on the command line where they would be visible in the OS process table.

Verify mode reads the system catalog for field definitions and key columns, so the build user needs read authority to it. Libraries specified with --verify-library are searched in order until the table is found.

Verify mode only runs for externally-described DISK files that could not be resolved from a local .pf file. If a .pf file exists, it takes precedence.

When no --verify-library is given, the table is resolved by name alone. If the same table name exists in more than one schema, the reference is ambiguous and the compile is rejected with an error naming the conflicting schemas - qualify the intended one with --verify-library rather than letting the compiler guess. With a single matching schema, name-only resolution succeeds.

Flag Description
--verify-jdbc-url <url> JDBC URL for catalog queries (enables verify mode)
--verify-jdbc-user-env <name> Env var holding the catalog username (default: JDBC_USER)
--verify-jdbc-password-env <name> Env var holding the catalog password (default: JDBC_PASSWORD)
--verify-jdbc-driver <class> JDBC driver class name
--verify-library <lib> Library to search (repeatable; searched in order)

Note: These are compile-time settings only. They are not related to the runtime JDBC settings (--jdbc-url-env, etc.) which are baked into the generated program for EXEC SQL and file I/O at runtime.

Supported Operations

Opcode Behaviour
SETLL Positions the file cursor at the first record with key >= Factor1. The EQ resulting indicator is set on when an exact-match record exists.
READE Reads the next record whose first key field equals Factor1. The EQ resulting indicator is set on at end-of-file (no more matching records). After a successful read, all DDS field variables are updated from the current row.
CHAIN Random-by-key lookup against the file's first key field. The HI resulting indicator (fixed-format cols 71-72) is set on when no matching record is found, off when a record is fetched. After a successful CHAIN, all DDS field variables are updated from the current row; after a miss the accessors return type defaults. An error indicator in the LO position (fixed-format cols 73-74) traps an I/O error (e.g. a CHAIN to a closed file, status 1211): the indicator is set on, %STATUS is set, execution continues, and the file's INFSR is not run - the same precedence as the (E) extender. A successful or not-found CHAIN sets the error indicator off.
UPDATE Writes the current field values back to the exact record last read by CHAIN/READ/READE, identified by its relative record number - not by key value. On a duplicate-key file (a keyed file with no UNIQUE constraint) only the one record the cursor is positioned on is updated, never every row that shares the key. UPDATE with no record read for update raises file status 1221 (see below).
DELETE With a search argument (Factor 1, or the free-form DELETE key file / DELETE (k1:k2) file / DELETE %KDS(ds) file form), deletes the record whose key equals it - no prior read required - and sets %FOUND (off when no record matches); on a duplicate-key file only the first matching record is removed. With no search argument, removes the exact record last read by its relative record number (as with UPDATE, only the one positioned record on a duplicate-key file). A keyless DELETE with no record read for update raises file status 1221 (see below).
OPEN Explicitly opens a file for processing. Required when the file is declared with USROPN. Opening an already-open file raises status 1215. Supports the (E) extender.
CLOSE Closes a file and releases its cursor resources. CLOSE on an already-closed file is a silent no-op. Factor 2 may be a file name or *ALL to close all files. Supports the (E) extender.
FEOD Forces logical end-of-data on a file without closing it. Sets %EOF ON and invalidates current cursors; subsequent sequential READ returns EOF. The file remains open and can be repositioned (SETLL/SETGT) or closed/reopened. FEOD on a closed file raises status 1211. Supports the (E) extender.

Record Locking

An input operation against an update-capable file - one declared USAGE(*UPDATE), or fixed-format file type U or C - reads the record for update and locks it, as on IBM i. CHAIN, READ, READE, READP and READPE all take the lock. An input-only file never locks.

While a record is locked, another job's read for update of the same record waits for the file's WAITRCD and then fails with file status 1218. The record is not returned. Trap it with an error indicator, the (E) extender, or a MONITOR block:

CHAIN(E) custKey CUSTFILE;
IF %ERROR AND %STATUS = 1218;
  // another job is holding this record
ENDIF;

The lock is a database row lock, so it is honoured by - and honours - other programs reading the same file, whether or not they were compiled by Triton RPG. It holds from the read until the record is given back, and it does so whether or not the file is under commitment control: the CHAIN-then-UPDATE idiom is protected against a concurrent updater without any transaction management on your part.

Holding a record therefore opens a database transaction that stays open until the record is released. Without commitment control that transaction is committed the moment the record is released - by the UPDATE or DELETE, by UNLOCK, by the next read, or by closing the file - so each operation is permanent as it completes, and a record is never held for longer than the program holds it. Under commitment control the lock joins the unit of work already in progress, and COMMIT or ROLBK ends it.

Reading without a lock - the (N) extender. Appending (N) to CHAIN, READ, READE, READP or READPE reads the record for input instead of for update: no lock is taken, and an existing lock held by another job does not block the read. The extender applies to the one operation it is written on. (N) combines with (E) in either order, as one run of letters:

CHAIN(N) custKey CUSTFILE;    // read for input, take no lock
CHAIN(EN) custKey CUSTFILE;   // ...and trap errors through %ERROR/%STATUS
     C     custKey       CHAIN(N)  CUSTFILE

Releasing the lock. A held lock is released by UPDATE, DELETE, the next input operation on the same file, SETLL, SETGT, UNLOCK, or closing the file. UNLOCK releases it without writing anything and without repositioning the file; afterwards no record is held for update, so an UPDATE or DELETE issued after an UNLOCK raises status 1221 exactly as one issued with no prior read (see below).

When the file is under commitment control (the COMMIT keyword), the connection is not in autocommit mode and locks are held until COMMIT or ROLBK.

UPDATE / DELETE Without a Prior Read

UPDATE and a keyless DELETE operate on the record last read for update. Issuing either against an open file with no such record - no preceding CHAIN/READ/READE/READP/READPE that read a record, or one whose record has since been given back by UNLOCK, SETLL or SETGT - raises file status 1221 ("update operation attempted without a prior read"). This is the same whether the file is keyed or keyless: a keyless file is updated by relative record number, so the absence of key fields does not change the behaviour.

A keyed DELETE (one with a search argument) is exempt: it locates the record by key and needs no prior read, so it never raises 1221 - instead it sets %FOUND to report whether a matching record was deleted.

Trap it like any other file I/O error - with an error indicator in the LO position (fixed-format cols 73-74), the (E) extender, or a MONITOR block:

     C                   UPDATE(E) MYFILER
     C                   IF        %ERROR
     C                   EVAL      st = %STATUS
     C                   ENDIF

Left untrapped, the error propagates and ends the program, rather than silently reporting success.

Relative Record Number - RECNO

RECNO(field) on a file declaration names a field the file operations write to: after a record is retrieved, it holds that record's relative record number.

     FCUSTMAST  IF   E             DISK    RECNO(Rrn)
     D Rrn             S              5P 0
     C                   READ      CUSTMASTR
      * Rrn now holds the relative record number of the record just read

and in free-format:

**FREE
DCL-F CustMast DISK USAGE(*INPUT) RECNO(Rrn);
DCL-S Rrn PACKED(5:0);
READ CustMast;

The field must be a numeric field with no decimal places - packed, zoned, or an integer type. Each retrieval replaces it, so it always describes the record currently in the input fields; before the first successful read it is zero.

RECNO does not make CHAIN by relative record number work - that is driven by the number you supply, and works whether or not the file declares RECNO:

     C                   EVAL      Rrn = 2
     C     Rrn           CHAIN     CUSTMASTR

RECNO's purpose is the other direction: capturing the number of a record you reached some other way, so you can return to it, store it, or update it later.

Null-Capable Fields - ALWNULL and %NULLIND

Database columns that allow SQL NULL (DDS ALWNULL fields, or nullable SQL columns) are handled according to the ALWNULL keyword on CTL-OPT (or the H-spec), exactly as on IBM i:

Value Behaviour
*NO (default) Reading a record that contains a NULL value fails with the data-mapping I/O error, %STATUS 01299; no record data is made available. Records without NULLs read normally.
*INPUTONLY / *YES NULL columns read as their type default (blanks / zero) with no error. %NULLIND is not maintained, and writes always store concrete values.
*USRCTL Full null support. On every read, %NULLIND(field) is set on for each column that was SQL NULL (the field itself receives its type default). On WRITE and UPDATE, a field whose %NULLIND is on is stored as SQL NULL - the field's own value is not written and is left unchanged in the program.
**FREE
CTL-OPT ALWNULL(*USRCTL);
DCL-F CUSTFILE DISK KEYED USAGE(*INPUT:*UPDATE);

CHAIN custKey CUSTFILE;
IF %FOUND(CUSTFILE) AND %NULLIND(CREDLIM);
  // CREDLIM was SQL NULL in the record just read
ENDIF;

%NULLIND(CREDLIM) = *ON;     // store NULL on the next update
UPDATE CUSTREC;

Under the default ALWNULL(*NO), trap the 01299 error with the (E) extender, an error indicator, or a MONITOR block - the same as any other file I/O error.

Runtime Requirement

Disk file I/O uses the same JDBC connection as EXEC SQL statements. Set the same environment variables (JDBC_URL, JDBC_DRIVER, etc.) as for SQL programs. The schema/library is determined by the JDBC connection's default schema.

File Control - OPEN / CLOSE / FEOD

The OPEN opcode explicitly opens a file for processing, and CLOSE closes it. When a file is declared with the USROPN keyword, it is not automatically opened at program initialization - an explicit OPEN must be issued before any I/O operation.

     FMYFILE    UF   E           K DISK    USROPN
     C                   OPEN      MYFILE
     C                   READ      MYFILER
     C                   CLOSE     MYFILE

In free-format:

**FREE
dcl-f MYFILE disk keyed usropn;
open MYFILE;
read MYFILE;
close MYFILE;

The FEOD opcode forces a logical end-of-data on a file without closing it. Unlike CLOSE, the file remains open after FEOD and can be repositioned or closed later.

     C                   FEOD      MYFILE

In free-format:

**FREE
feod MYFILE;

Extension Specification (E-spec) - Compile-Time Arrays

The E-spec (column 6 = E) defines compile-time arrays whose data is loaded from ** or **CTDATA records at the end of the source. It declares the same array the free-format DCL-S ... DIM(...) CTDATA form declares.

E-spec column layout (RPG/400):

Columns Field
7-26 From/to filenames (blank for compile-time)
27-32 Array name (6 chars)
33-35 Entries per record (PERRCD)
36-39 Total entries (DIM)
40-42 Entry length
43 Format (P = packed, blank = character)
44 Decimal positions (0-9)
45 Sequence (A = ascending, D = descending)

Example:

     E                    NAMES   1   5 10
**CTDATA NAMES
ALICE
BOB
CHARLIE
DAVE
EVE

Open Access - HANDLER Keyword

The HANDLER keyword on a DCL-F routes I/O operations to a user-written handler procedure instead of the database. The handler receives a QrnOpenAccess communication area (modeled after IBM's QrnOpenAccess_T) with the requested operation code, key data, and record buffers.

dcl-f CURRATE DISK keyed usage(*input) handler('CURRTHND');

The handler procedure takes a single LIKEDS parameter matching the communication-area template:

dcl-proc CURRTHND;
  dcl-pi CURRTHND;
    info likeds(qrnOpenAccess_t);
  end-pi;
  // info.rpgOperation contains the operation code (CHAIN=9, READ=4, etc.)
  // info.key contains the key bytes for keyed operations
  // info.inputBuffer is where the handler writes record data
  // info.found / info.eof are set by the handler as feedback
end-proc;

Operations are dispatched with the rpgOperation codes IBM defines for QrnOperation_*:

Operation rpgOperation Operation rpgOperation
OPEN 1 SETLL (keyed) 12
SETLL *START 2 UNLOCK 13
SETLL *END 3 UPDATE 14
READ 4 WRITE 15
READC 5 DELETE (keyed) 16
READE (keyed) 6 FEOD 17
READP 7 CLOSE 18
READPE (keyed) 8 DELETE (no key) 19
CHAIN 9 READE (no key) 20
EXFMT 10 READPE (no key) 21
SETGT 11

The code depends on the opcode and its operand, matching IBM i. SETLL, READE, READPE, and DELETE each dispatch a different code depending on whether a search argument (or *START/*END) is present:

A handler must switch on the exact code, since 16-vs-19 and 20/21-vs-6/8 are different record targets.

A plain sequential READ dispatches rpgOperation=4 (QrnOperation_READ); the handler advances to the next record and either provides input data or sets the eof feedback flag, which the runtime surfaces through %EOF and the READ's EOF resulting indicator. %FOUND, %EOF, and %EQUAL read feedback from the handler's communication area.

EXFMT (10) and READC (5) dispatch when the handler is declared on a WORKSTN file. READC reads the next changed subfile record: the handler sets its eof feedback flag when there are no more changed records, which the runtime surfaces through %EOF(file) exactly as for a database read.

When the handler is told the file is closing

A handler usually owns the resource behind the file - a socket, an HTTP session, a buffered writer - and CLOSE (rpgOperation=18) is its only signal to flush and release it. A compiled program dispatches CLOSE:

A program that returns with *INLR off stays activated and its files stay open, so no CLOSE is dispatched - the handler keeps its resource for the next call. A file that was already closed explicitly is not closed a second time when the program later ends, so the handler always receives exactly one CLOSE per open. Reopening a closed file with OPEN dispatches rpgOperation=1 again and puts it back under the implicit close.

In the default (buffer) mode, the record buffers use the same byte layout as an externally-described data structure (*INPUT for info.inputBuffer, *OUTPUT for info.outputBuffer): each field appears in its native record form - packed decimal as packed BCD, zoned as zoned, binary/integer as two's-complement, date/time/timestamp in the field's format - not as character text. A handler decodes info.inputBuffer and populates info.outputBuffer using that layout.

The handler can set useNamesValues = *ON during the OPEN call to opt into the name/value interface. In this mode, WRITE and UPDATE operations populate a QrnNamesValues array with per-field metadata (name, data type, length) and values instead of raw byte buffers. For input operations (CHAIN, READE, etc.), the handler writes field values into the array and the runtime reads them back.

namesValues is a pointer, exactly as IBM declares it. A handler reaches the array by basing a QrnNamesValues_T data structure over that pointer - the same declaration it uses on IBM i, so a handler written against IBM's QRNOPENACC copy member compiles and runs here unchanged:

dcl-ds nv likeds(QrnNamesValues_T) based(info.namesValues);
dcl-s valPtr pointer;
dcl-s valTxt char(64) based(valPtr);

for i = 1 to nv.num;
  if %trim(nv.field(i).externalName) = 'AMOUNT';
    valPtr = nv.field(i).value;
    valTxt = '77.77';
    nv.field(i).valueLenBytes = 5;
  endif;
endfor;

The array is shared with the program, not copied to the handler, so what the handler writes is what the program reads.

Each field's value is likewise a pointer, addressing storage the entry owns. Its capacity is in valueMaxLenBytes; do not write past it.

valueLenBytes is how much of that storage is meaningful, and both sides must set it.

Each field's value is conveyed as text - the same string the %CHAR built-in produces for that field's type (a decimal as 123.45, an integer as -5, a float in its 23-character exponential form, a date in its external format). A numeric field is therefore written to and read from the array as that %CHAR string, not as raw bytes; the runtime converts it back to the field's declared type on input.

For a keyed file, the search argument reaches the handler the same way through keynamesValues, a second array of the same shape holding one entry per key column in key order, with the key's value and valueLenBytes filled in for every keyed operation:

dcl-ds kv likeds(QrnNamesValues_T) based(info.keynamesValues);

An unkeyed file leaves keynamesValues set to *NULL.

Each field's dataType uses the QrnDatatype_* codes IBM assigns: character = 1, packed/zoned/binary = 8 (Decimal), integer = 9, unsigned = 10, float = 11, indicator = 7, date = 12, time = 13, timestamp = 14. A binary (DDS type B) field is reported as Decimal (8), the same as packed and zoned - not as character.

Program-Described WORKSTN Files

Program-described WORKSTN files (file format F instead of E in the F-spec) are accepted without requiring a .dspf DDS source file. The record length is extracted from the F-spec. The file declaration compiles, but screen I/O operations on a program-described WORKSTN file are not supported.

Not supported

Open Access HANDLER is supported on WORKSTN and PRINTER files. On a WORKSTN file it dispatches EXFMT (rpgOperation=10), READC (5), WRITE (15) and READ (4) to the handler, passing the record's field data through info.outputBuffer and info.inputBuffer.