Embedded SQL and Db2

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026) on z/OS 3.2 and Db2 13 for z/OS. Embedded SQL is not part of ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot): the standard defines no EXEC SQL, and the SQL statements are defined by the database product and the Db2 SQL Reference, not by the COBOL standard. GnuCOBOL 3.2 (28 July 2023, the latest release) does not compile EXEC SQL either; the last section shows its real diagnostic and the community tools that fill the gap. Nothing on this page except that diagnostic could be run here: all EXEC SQL code, JCL and Db2 commands are labelled "z/OS; not run here" and were checked statement by statement against the Db2 13 for z/OS documentation and the Enterprise COBOL 6.5 Programming Guide. Only the COBOL skeleton of each program was compiled (with the SQL text removed) with cobc -x -Wall and -std=ibm, without warnings, to check the COBOL syntax around the SQL.

Embedded SQL puts SQL statements in the middle of a COBOL program, so a batch program can read and change rows of a Db2 table with the same tools it uses for files. The Circulation back office of this reference keeps its loans in a Db2 table named LOAN on z/OS, next to the LOANTRAN file of Sequential Files. This page covers how a COBOL program talks to Db2 (host variables, indicators, the communication area and the error statements), the statements a batch program uses most (singleton SELECT, cursors, positioned updates, multi-row FETCH, COMMIT and ROLLBACK), and how such a program is prepared and run (coprocessor or precompiler, DBRM, BIND, packages and plans). SQL itself is on SQL Reference and Transaction Control; the COBOL file alternatives are on Indexed Files and VSAM; the numeric storage formats behind the data type mapping are on Usage and Numeric Representation; copy members in general are on Copybooks and REPLACE; the compile and link steps are on Compiling Linking and Language Environment and the JCL basics on JCL for COBOL Programmers.

Where the book is outdated: Taulli’s Modern Mainframe Development (2022, chapter 8) introduces Db2 with the Db2 12 era in mind and explains EXEC SQL INCLUDE as bringing in "the table from the Db2 database". It does not: the statement copies a source member, normally a DCLGEN output, into the program while the program is being prepared (by the Db2 coprocessor, or by the precompiler on older setups), and nothing is read from the database at that point. The book also hand-codes a communication area instead of using EXEC SQL INCLUDE SQLCA, with a mistyped field name and an item nested under an elementary item; the standard include gives the correct layout. Its cursor sample declares one cursor name and then opens and fetches a different name, which cannot work, because OPEN, FETCH and CLOSE must name the cursor that DECLARE CURSOR defined. And it does not describe program preparation (precompiler or coprocessor, the DBRM, BIND, packages and plans), without which none of its programs can run. Those points, and the Db2 13 versions of the statements, come from the official manuals below.

The Circulation LOAN table

The examples use one table. Each row is one checkout: who borrowed which book, when it is due, when it came back (null while the book is still out) and the fine assessed (null until one is). The table is the relational twin of the LOANREC record of the file examples; the nullable columns are there on purpose, to show indicator variables. The names and sizes are an original design for this reference.

CREATE TABLE USERID.LOAN
  (MEMBER_ID      INTEGER      NOT NULL,
   BOOK_ID        INTEGER      NOT NULL,
   CHECKOUT_DATE  DATE         NOT NULL,
   DUE_DATE       DATE         NOT NULL,
   RETURN_DATE    DATE,
   FINE_AMOUNT    DECIMAL(7,2),
   PRIMARY KEY (MEMBER_ID, BOOK_ID, CHECKOUT_DATE));

CREATE UNIQUE INDEX USERID.LOANIX
  ON USERID.LOAN (MEMBER_ID, BOOK_ID, CHECKOUT_DATE);

On Db2 for z/OS a table with a primary key is marked unavailable until its primary index is created (unless the table space is created implicitly, in which case Db2 creates the index), which is why the second statement is there; table space and database clauses are left out (the installation’s defaults apply), and USERID stands for the schema, as USERID.CIRC.BOOKMAST does for data sets elsewhere in this section. The table is created once, outside the COBOL programs, usually by a DBA or a deployment job.

How embedded SQL is written

An SQL statement in a COBOL program is a block that starts with EXEC SQL and ends with END-EXEC, placed where a COBOL statement or a data description entry is allowed. The Programming Guide gives the rules that matter in practice:

  • The EXEC SQL and END-EXEC delimiters must each fit on one line and cannot be continued.

  • COBOL statements cannot be coded inside an EXEC SQL block; the SQL text can only refer to COBOL data names, as host variables.

  • With the Db2 coprocessor, every line of the SQL block must lie in columns 12 through 72 (the precompiler lets continuation lines start at column 8).

  • An EXEC SQL block is not ended by a period; it is not a COBOL sentence by itself. A period after END-EXEC ends the enclosing sentence like any other. Inside IF and PERFORM the blocks are written without a period, as in the examples below.

  • SQL statements cannot be used in object-oriented COBOL classes or methods; keep them in procedural programs (see Object-Oriented COBOL).

  • Db2 sets a return code in the communication area after every SQL statement (with one exception), and the program must test it. Skipping the test is the commonest cause of programs that "run" and corrupt data.

The statements fall in three groups. Declarative statements such as DECLARE CURSOR, DECLARE TABLE, INCLUDE and WHENEVER produce no call; they only inform the compiler or the preparation step. Executable statements (SELECT INTO, OPEN, FETCH, CLOSE, INSERT, UPDATE, DELETE, COMMIT, ROLLBACK, GET DIAGNOSTICS) become calls to Db2. And dynamic SQL (PREPARE, EXECUTE IMMEDIATE, built at run time) builds the statement text in a character host variable; it is outside the scope of this page. The static form used here is checked and optimised at bind time, which is why a batch program runs the same access path every night.

Host variables and indicator variables

A host variable is a COBOL data item that an SQL statement reads or sets. Inside the SQL text its name carries a colon prefix, :WS-WANTED-BOOK, so that Db2 can tell it from a column; outside the SQL text it is an ordinary COBOL name and has no colon. The rules, from the Db2 13 documentation:

  • Declare every host variable in WORKING-STORAGE, LOCAL-STORAGE or LINKAGE before the SQL statement that uses it. A BEGIN DECLARE SECTION and END DECLARE SECTION pair around the declarations is optional, unless the STDSQL(YES) option is in effect.

  • OCCURS is not allowed on an ordinary host variable. It is allowed only on host-variable arrays (used for multi-row operations below) and on indicator arrays.

  • A group item can be a host structure: a named set of host variables used as one list.

  • With the separate precompiler a host variable name must be unique in the program, or qualified with its structure name. The coprocessor also requires unique references and honours the QUALIFY option.

  • The data type of the host variable must be equivalent to the column’s. Db2 converts compatible types in assignments and comparisons, but a mismatch can cost performance and, for numbers, precision.

An indicator variable is a 2-byte binary integer that travels with a host variable and carries what a COBOL item cannot: whether the column value is null. It follows the host variable in the SQL text, with or without a colon before it: :FINE-AMOUNT :WS-FINE-IND. For a value coming from Db2, a negative indicator means the column is null and the host variable is left unchanged; zero or more means the host variable holds a real value; and -2 means the value was null because of an arithmetic or conversion error that Db2 turned into a null. A positive indicator after a string FETCH or SELECT INTO means the string was truncated and holds the original length, with a warning in the communication area. For a value going to Db2, the program sets the indicator: -1 stores a null (the host variable is ignored) and 0 stores the host variable.

Reading a nullable column without an indicator is an error, SQLCODE -305 (SQLSTATE 22002): the statement fails and nothing is retrieved. This also applies to aggregates such as MAX and SUM, which are null when no row qualified. The practical rule is to give an indicator to every host variable that can receive a column that is not NOT NULL, and every aggregate.

Equivalent data types

The table is the subset of the Db2 13 mapping that the Circulation programs need, plus the usual character and floating-point types.

Db2 column type COBOL host variable Notes

SMALLINT

PIC S9(4) with COMP, COMP-4, COMP-5 or BINARY

Two bytes.

INTEGER

PIC S9(9) with COMP, COMP-4, COMP-5 or BINARY

Four bytes. MEMBER_ID and BOOK_ID use this.

BIGINT

PIC S9(18) with COMP, COMP-4, COMP-5 or BINARY

Eight bytes.

DECIMAL(p,s) or NUMERIC(p,s)

PIC S9(p-s)V9(s) with COMP-3 or PACKED-DECIMAL

0 <= s <= p <= 31. For s = 0 write S9(p) or S9(p)V; for s = p write SV9(s). FINE_AMOUNT DECIMAL(7,2) becomes PIC S9(5)V9(2) COMP-3.

REAL, FLOAT(n) with n up to 21

COMP-1

Single precision.

DOUBLE, FLOAT(n) with n from 22 to 53

COMP-2

Double precision. FLOAT is inexact; do not use it for money.

CHAR(n)

PIC X(n)

n from 1 to 255.

VARCHAR(n)

a group with 49 len PIC S9(4) USAGE BINARY and 49 text PIC X(n)

The inner items must be level 49. A DCLGEN member declares them this way.

DATE

PIC X(n)

n is at least 10 (2026-10-20) unless a date exit routine decides otherwise. CHECKOUT_DATE uses PIC X(10).

TIME

PIC X(n)

n is at least 6, or 8 to keep the seconds.

TIMESTAMP

PIC X(n)

n is at least 19; 26 to keep the microseconds.

Two rules of this mapping deserve emphasis. First, the exact decimal correspondence is packed decimal, COMP-3: the digits of a DECIMAL(7,2) fit in a S9(5)V9(2) item, held in 4 bytes of packed nibbles (see Usage and Numeric Representation). DISPLAY and NATIONAL items with a leading separate sign are also accepted for decimals, but they have no exact Db2 equivalent and cost a conversion. If the compiler or the target does not support 31-digit packed items, COMP-2 is the only substitute, and it is not exact. Second, a COMP item such as PIC S9(9) is subject to the TRUNC compiler option, which can cut values to the digits of the picture; the DCLGEN documentation suggests editing the generated member to use COMP-5 when columns may hold values beyond the picture’s digit count (see the TRUNC section of the usage page).

DCLGEN and EXEC SQL INCLUDE

Writing the host variables by hand for a table of thirty columns is a good way to introduce mismatches. DCLGEN (declarations generator) reads the Db2 catalog and writes a source member with two parts: an EXEC SQL DECLARE ... TABLE statement that lists the columns, and a COBOL record description with one level-10 item per column, with a name made from the column name with underscores turned into hyphens. The coprocessor checks table and column references against the DECLARE TABLE text at compile time and against the catalog at bind time, so generating the member keeps the program and the table in step.

DCLGEN is a DSN subcommand, run from DB2I or in a batch step; its options choose the table, the target library member and the language, the string delimiter (APOST or QUOTE), a prefix for the field names (NAMES) and the structure name, whether to append the column names (COLSUFFIX), and whether to generate an indicator array (INDVAR(YES)) with one PIC S9(4) USAGE COMP element per column. The output for LOAN looks like the following. The banner comments and the exact picture clauses depend on the options and the Db2 level, so treat the listing as the shape of the output, not as a byte-for-byte copy (z/OS; not run here).

      ******************************************************************
      * DCLGEN TABLE(USERID.LOAN)                                      *
      *        LIBRARY(USERID.CIRC.DCLGEN(LOANDCL))                    *
      *        LANGUAGE(IBMCOB)                                        *
      ******************************************************************
           EXEC SQL DECLARE USERID.LOAN TABLE
           ( MEMBER_ID                      INTEGER NOT NULL,
             BOOK_ID                        INTEGER NOT NULL,
             CHECKOUT_DATE                  DATE NOT NULL,
             DUE_DATE                       DATE NOT NULL,
             RETURN_DATE                    DATE,
             FINE_AMOUNT                    DECIMAL(7, 2)
           ) END-EXEC.
      ******************************************************************
      * COBOL DECLARATION FOR TABLE USERID.LOAN                        *
      ******************************************************************
       01  DCLLOAN.
           10 MEMBER-ID            PIC S9(9) USAGE COMP.
           10 BOOK-ID              PIC S9(9) USAGE COMP.
           10 CHECKOUT-DATE        PIC X(10).
           10 DUE-DATE             PIC X(10).
           10 RETURN-DATE          PIC X(10).
           10 FINE-AMOUNT          PIC S9(5)V9(2) USAGE COMP-3.
      ******************************************************************
      * THE NUMBER OF COLUMNS DESCRIBED BY THIS DECLARATION IS 6       *
      ******************************************************************

The program brings the member in with EXEC SQL INCLUDE LOANDCL END-EXEC.. INCLUDE member-name copies a member of a partitioned data set into the source while the program is prepared; it is not an executable statement, the member cannot contain another INCLUDE, and the name must be a valid SQL identifier (up to eight characters here, because it is a PDS member). With the coprocessor the include is handled like a COPY statement: the member is searched in the SYSLIB concatenation of the compile step, and the period after END-EXEC is required. The separate precompiler makes that period optional. EXEC SQL INCLUDE SQLCA END-EXEC. is the other form: it adds the standard communication area, once per program, in WORKING-STORAGE or LINKAGE.

SQLCA, SQLCODE and SQLSTATE

After every SQL statement Db2 reports the outcome in the SQL communication area (SQLCA). EXEC SQL INCLUDE SQLCA END-EXEC. defines it. The fields a program uses are:

  • SQLCODE, a fullword binary integer. Zero means success, a positive number a warning or the not-found condition, and a negative number an error. SQLCODE is Db2 specific; the same condition on another product has another number.

  • SQLSTATE, a five-character code that is standard across SQL products and grouped by class (for example class 02 is "no data", 21 "cardinality violation", 22 "data exception", 23 "integrity constraint violation", 40 "transaction rollback").

  • SQLERRD, a table of six fullwords. SQLERRD(3) holds the number of rows affected by an INSERT, UPDATE or DELETE and, after a multi-row FETCH, the number of rows returned.

  • SQLWARN0 to SQLWARNA, one-byte flags; SQLWARN0 is W when any of the others is set (for example SQLWARN3, set when there are fewer host variables than columns).

  • SQLERRM, the message tokens that go with the code.

A program that does not want the whole area can declare SQLCODE as PIC S9(9) COMP-5 and SQLSTATE as PIC X(5) itself, inside a declare section, and compile with STDSQL(YES); then no SQLCA may be included. Both forms are legal, but the INCLUDE SQLCA form is the common one and is used here. SQLSTATE cannot be an element of a structure.

The codes that a batch program meets most often are in the table. Every code in it comes from the Db2 13 for z/OS codes reference.

SQLCODE SQLSTATE Meaning and usual action

0

00000

Success.

+100

02000

No row found (SELECT INTO), or no more rows (FETCH). A normal end of data, not an error.

-811

21000

SELECT INTO produced more than one row. Add a key condition or FETCH FIRST 1 ROW ONLY, or use a cursor.

-305

22002

A null reached a host variable without an indicator. Add the indicator.

-803

23505

INSERT or UPDATE broke a uniqueness rule; the same loan row exists already.

-501

24501

FETCH or CLOSE on a cursor that is not open (never opened, already closed, or closed by COMMIT or ROLLBACK).

-508

24504

Positioned UPDATE or DELETE while the cursor is not on a row (for example after the FETCH that returned +100).

-805

51002

Db2 cannot find the package of the program in the plan’s package list. See the bind section.

-818

51003

The load module and the DBRM bound for it come from different compiles (timestamp mismatch). Rebind with the matching DBRM.

-911

40001

The unit of work was rolled back as a deadlock or timeout victim; the changes since the last commit are lost.

WHENEVER

WHENEVER is a non-executable statement that tells the compiler what code to generate after each following SQL statement. There are three conditions: NOT FOUND (SQLCODE +100, SQLSTATE 02000), SQLERROR (any negative SQLCODE) and SQLWARNING (SQLWARN0 is W, or a positive SQLCODE other than +100). There are two actions: CONTINUE, and GO TO a label, which in COBOL can be a section name or an unqualified paragraph name (GOTO is accepted too).

           EXEC SQL WHENEVER SQLERROR GO TO SQL-ERROR END-EXEC
           EXEC SQL WHENEVER NOT FOUND CONTINUE END-EXEC

The scope is the order of the statements in the source listing, not the order of execution. Each SQL statement is governed by the nearest WHENEVER of each condition above it in the source; with none above it, CONTINUE applies. That makes WHENEVER easy to misread in a program with many paragraphs, and it hides an unstructured jump (the generated code is a GO TO) behind SQL text, which is why modern standards prefer an explicit test of SQLCODE after each statement, as the examples on this page do. If WHENEVER SQLERROR GO TO is used, the error paragraph must begin with WHENEVER SQLERROR CONTINUE; otherwise a failing statement inside the error paragraph jumps to the paragraph again and the program loops. The error paragraph of the cursor example below shows that line.

GET DIAGNOSTICS

SQLCODE and SQLSTATE say what happened; GET DIAGNOSTICS says more. It returns information about the previous SQL statement (not counting itself) from the diagnostics area in three groups: statement items (ROW_COUNT, the rows affected, and NUMBER, the number of conditions), condition items for each warning or error (MESSAGE_TEXT, RETURNED_SQLSTATE, DB2_RETURNED_SQLCODE, DB2_ROW_NUMBER, CURSOR_NAME), and connection items after a CONNECT. A condition is selected by number, GET DIAGNOSTICS CONDITION 1 .... The statement is most useful after a multi-row INSERT written NOT ATOMIC CONTINUE ON SQLEXCEPTION, where the SQLCODE can be zero while some rows failed, and for MESSAGE_TEXT, which turns the code into readable text. Retrieve NUMBER first when you loop over conditions; it is at least one. The host variable for MESSAGE_TEXT is a VARCHAR-style group with level-49 items, as in the table of types above; the older alternative is to call the DSNTIAR routine, which formats the communication area into lines of 72 to 240 characters, but the Programming Guide for Db2 says programs that need long message tokens should use GET DIAGNOSTICS. The cursor example below calls it in its error paragraph.

Singleton SELECT INTO

A singleton SELECT returns at most one row and puts the columns straight into host variables with INTO. It is the statement for a lookup by a unique key. If no row qualifies, the host variables are left unchanged and the result is SQLCODE +100; if more than one qualifies, the statement fails with -811 (unless a FETCH FIRST 1 ROW ONLY or an offset clause limits the result). A null column needs an indicator, as described above.

The program below looks up one loan by its key and shows the three outcomes (z/OS; not run here). DUE-DATE, RETURN-DATE and FINE-AMOUNT come from the DCLGEN member; RETURN_DATE and FINE_AMOUNT are nullable, so each has an indicator.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANONE.
      * Read one loan row with a singleton SELECT INTO.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           EXEC SQL INCLUDE SQLCA END-EXEC.
           EXEC SQL INCLUDE LOANDCL END-EXEC.
       01  WS-WANTED-MEMBER      PIC S9(9) COMP VALUE 100.
       01  WS-WANTED-BOOK        PIC S9(9) COMP VALUE 1.
       01  WS-WANTED-DATE        PIC X(10) VALUE "2026-10-20".
       01  WS-RETURN-IND         PIC S9(4) COMP.
       01  WS-FINE-IND           PIC S9(4) COMP.
       01  WS-FINE-EDIT          PIC -ZZZZ9.99.
       01  WS-CODE-EDIT          PIC -(9)9.
       PROCEDURE DIVISION.
       MAIN-PARA.
           EXEC SQL
               SELECT DUE_DATE, RETURN_DATE, FINE_AMOUNT
                 INTO :DUE-DATE,
                      :RETURN-DATE :WS-RETURN-IND,
                      :FINE-AMOUNT :WS-FINE-IND
                 FROM LOAN
                WHERE MEMBER_ID = :WS-WANTED-MEMBER
                  AND BOOK_ID = :WS-WANTED-BOOK
                  AND CHECKOUT_DATE = :WS-WANTED-DATE
           END-EXEC
           EVALUATE SQLCODE
               WHEN 0
                   DISPLAY "DUE      " DUE-DATE
                   IF WS-RETURN-IND < 0
                       DISPLAY "RETURNED (still out)"
                   ELSE
                       DISPLAY "RETURNED " RETURN-DATE
                   END-IF
                   IF WS-FINE-IND < 0
                       DISPLAY "FINE     none assessed"
                   ELSE
                       MOVE FINE-AMOUNT TO WS-FINE-EDIT
                       DISPLAY "FINE    " WS-FINE-EDIT
                   END-IF
               WHEN 100
                   DISPLAY "NO SUCH LOAN"
               WHEN OTHER
                   MOVE SQLCODE TO WS-CODE-EDIT
                   DISPLAY "SQLCODE " WS-CODE-EDIT
                           " SQLSTATE " SQLSTATE
                   MOVE 12 TO RETURN-CODE
           END-EVALUATE
           GOBACK.

The WHERE clause names the whole primary key, so at most one row can qualify and -811 cannot occur; leaving CHECKOUT_DATE out would make it possible, because a member can borrow the same book more than once. DUE-DATE has no indicator because the column is NOT NULL; if it were nullable and null, the statement would end with -305 and the host variable would stay unchanged. The program shows no output here, because it was not run; against a table that holds the loan of the Circulation scenario (member 100, book 1, checked out on 2026-10-20 and still out) it would show the due date, RETURNED (still out) and FINE none assessed.

Cursors

A query that can return many rows needs a cursor: a named pointer that walks the result table one row (or one rowset) at a time. The lifecycle has four statements, and the diagram shows how they interact with the program.

sequenceDiagram participant P as COBOL program participant D as Db2 Note over P: DECLARE CURSOR is a declaration and sends nothing P->>D: OPEN OVERDUE_CSR D-->>P: SQLCODE 0, cursor before the first row loop for each qualifying row P->>D: FETCH OVERDUE_CSR INTO host variables D-->>P: SQLCODE 0 and one row P->>D: UPDATE ... WHERE CURRENT OF OVERDUE_CSR D-->>P: SQLCODE 0 end P->>D: FETCH OVERDUE_CSR D-->>P: SQLCODE +100, SQLSTATE 02000 P->>D: CLOSE OVERDUE_CSR D-->>P: SQLCODE 0 P->>D: COMMIT D-->>P: SQLCODE 0, locks released

The figure follows LOANFINE, the program below. DECLARE CURSOR defines the cursor and its SELECT; it must appear in the source before the first statement that uses the cursor. OPEN evaluates the SELECT, with the current special registers and the values the host variables have at that moment, and positions the cursor before the first row; Db2 does not report an empty result at OPEN, only at the first FETCH. FETCH moves to the next row and copies it into the INTO host variables; SQLCODE +100 means no more rows. CLOSE ends the cursor and frees its resources; opening a cursor that is already open is an error, so a cursor used several times is closed in between.

Cursor names are SQL identifiers, so they cannot contain a hyphen (an SQL name with a hyphen is a subtraction). The COBOL habit of OVERDUE-CSR does not carry over: write OVERDUE_CSR. Host variable names in the SQL text, in contrast, may use hyphens.

WITH HOLD and COMMIT

A COMMIT ends the unit of recovery, makes its changes permanent, releases the locks and, by default, closes every open cursor that was declared without WITH HOLD. A cursor declared WITH HOLD stays open across the commit, so a long batch program can commit every few hundred rows (to keep locks short and the log small) and carry on from where it was. Two details matter. A non-scrollable held cursor needs a FETCH after the COMMIT before it is positioned on a row again, so a positioned UPDATE or DELETE directly after the commit ends with -508. And a ROLLBACK closes all cursors, held or not, in a batch program (in IMS and CICS a held cursor can survive a rollback only in the narrow case the SQL Reference describes). The default is WITHOUT HOLD.

FOR UPDATE OF and positioned changes

A cursor whose rows will be changed with UPDATE ... WHERE CURRENT OF cursor-name or DELETE ... WHERE CURRENT OF cursor-name must have a result table that is not read-only. FOR UPDATE OF column in the cursor’s SELECT lists the columns that may change; the manual recommends it whenever a fetched column will be updated later, because it keeps Db2 from choosing an index on that column, which could make the same row appear twice. The opposite is FOR READ ONLY, which is the right choice when the cursor is not used for positioned changes, because Db2 may then block-fetch. The cursor must be open and positioned on a row when the positioned statement runs; the target table must be the one in the FROM clause of the cursor’s SELECT. After a positioned DELETE the cursor sits just before the next row (or after the last row), so the next statement on it is a FETCH. A cursor over a join, an ORDER BY or a view with an INSTEAD OF trigger is read-only for positioned changes; write the update with a key WHERE instead.

The program assesses fines on loans that are overdue and not returned (z/OS; not run here). It uses the fine rule of the Circulation scenario, 0.35 per day with a cap of 5.00, evaluated by Db2 in the UPDATE, and shows packed-decimal host variables as parameters. It commits every 100 rows with a held cursor, tests SQLCODE after each statement, and uses GET DIAGNOSTICS and WHENEVER in its error paragraph.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANFINE.
      * Assess fines on overdue loans through an updatable cursor.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           EXEC SQL INCLUDE SQLCA END-EXEC.
           EXEC SQL INCLUDE LOANDCL END-EXEC.
       01  WS-RATE               PIC S9(1)V9(2) COMP-3 VALUE 0.35.
       01  WS-CAP                PIC S9(3)V9(2) COMP-3 VALUE 5.00.
       01  WS-FINE-IND           PIC S9(4) COMP.
       01  WS-ASSESSED           PIC S9(9) COMP VALUE ZERO.
       01  WS-SINCE-COMMIT       PIC S9(4) COMP VALUE ZERO.
       01  WS-COMMIT-EVERY       PIC S9(4) COMP VALUE 100.
       01  WS-END-FLAG           PIC X VALUE "N".
           88  NO-MORE-ROWS      VALUE "Y".
       01  WS-CODE-EDIT          PIC -(9)9.
       01  WS-MSG.
           49  WS-MSG-LEN        PIC S9(4) COMP.
           49  WS-MSG-TEXT       PIC X(240).
           EXEC SQL
               DECLARE OVERDUE_CSR CURSOR WITH HOLD FOR
                 SELECT MEMBER_ID, BOOK_ID, DUE_DATE, FINE_AMOUNT
                   FROM LOAN
                  WHERE RETURN_DATE IS NULL
                    AND DUE_DATE < CURRENT DATE
                  FOR UPDATE OF FINE_AMOUNT
           END-EXEC.
       PROCEDURE DIVISION.
       MAIN-PARA.
           EXEC SQL OPEN OVERDUE_CSR END-EXEC
           IF SQLCODE NOT = 0
               PERFORM SQL-ERROR
           END-IF
           PERFORM UNTIL NO-MORE-ROWS
               EXEC SQL
                   FETCH OVERDUE_CSR
                    INTO :MEMBER-ID, :BOOK-ID, :DUE-DATE,
                         :FINE-AMOUNT :WS-FINE-IND
               END-EXEC
               EVALUATE SQLCODE
                   WHEN 0
                       PERFORM ASSESS-FINE
                   WHEN 100
                       SET NO-MORE-ROWS TO TRUE
                   WHEN OTHER
                       PERFORM SQL-ERROR
               END-EVALUATE
           END-PERFORM
           EXEC SQL CLOSE OVERDUE_CSR END-EXEC
           EXEC SQL COMMIT END-EXEC
           DISPLAY "FINES ASSESSED " WS-ASSESSED
           GOBACK.
       ASSESS-FINE.
           EXEC SQL
               UPDATE LOAN
                  SET FINE_AMOUNT =
                      CASE
                        WHEN :WS-RATE * (DAYS(CURRENT DATE)
                                         - DAYS(DUE_DATE)) > :WS-CAP
                          THEN :WS-CAP
                        ELSE :WS-RATE * (DAYS(CURRENT DATE)
                                         - DAYS(DUE_DATE))
                      END
                WHERE CURRENT OF OVERDUE_CSR
           END-EXEC
           IF SQLCODE NOT = 0
               PERFORM SQL-ERROR
           END-IF
           ADD 1 TO WS-ASSESSED
           ADD 1 TO WS-SINCE-COMMIT
           IF WS-SINCE-COMMIT >= WS-COMMIT-EVERY
               EXEC SQL COMMIT END-EXEC
               MOVE ZERO TO WS-SINCE-COMMIT
           END-IF.
       SQL-ERROR.
           EXEC SQL WHENEVER SQLERROR CONTINUE END-EXEC
           MOVE SQLCODE TO WS-CODE-EDIT
           DISPLAY "SQLCODE " WS-CODE-EDIT " SQLSTATE " SQLSTATE
           EXEC SQL
               GET DIAGNOSTICS CONDITION 1
                   :WS-MSG = MESSAGE_TEXT
           END-EXEC
           IF SQLCODE = 0
               DISPLAY WS-MSG-TEXT(1:WS-MSG-LEN)
           END-IF
           EXEC SQL ROLLBACK END-EXEC
           MOVE 12 TO RETURN-CODE
           GOBACK.

Points to notice in the program:

  • The commit sits right after the positioned UPDATE and before the next FETCH, which is the only place where a held cursor is safe: the following FETCH repositions it. Putting a positioned UPDATE after the commit, on the same row, would fail with -508.

  • WS-RATE and WS-CAP are COMP-3 because the Db2 columns and expressions they meet are DECIMAL; moving through COMP-2 would make 0.35 an approximation. The CASE expression evaluates the fine twice instead of using a scalar function, so that it does not depend on a function whose arguments must match in type.

  • The error paragraph ends with ROLLBACK and GOBACK, and sets RETURN-CODE 12 for the job step’s condition code (see Error Handling and Declaratives). The rollback undoes only the changes since the last commit; the fines committed earlier stay, which is the price of a commit interval and the reason a restartable batch program records its progress.

  • The final COMMIT after CLOSE ends the run. Without it, whether the last group of fines is kept or backed out depends on what the environment does when the program ends; commit explicitly.

COMMIT and ROLLBACK in a batch program

COMMIT makes the unit of recovery permanent, starts a new one and releases the locks it holds, except the ones needed by cursors that stay open and the table-space locks when the bind option RELEASE is not COMMIT. ROLLBACK backs out all changes of the current unit, closes the cursors, destroys prepared statements and releases the locks; it does not affect connections. ROLLBACK TO SAVEPOINT undoes only the changes after a named savepoint and ends no unit of recovery. The commit interval is a trade-off: committing after each row is safe and slow, committing once at the end holds locks for the whole run and risks -911 for other users and a long rollback after a failure. The usual design is a commit every few hundred rows, plus a checkpoint record so a restart continues after the last committed key. Under CICS and IMS the transaction manager’s own syncpoint statements replace EXEC SQL COMMIT (see CICS Online Programming).

Multi-row FETCH

A cursor declared WITH ROWSET POSITIONING can return a rowset with one FETCH: the columns of up to 32,767 rows go into host-variable arrays, which are ordinary level-01 items with OCCURS. One call to Db2 replaces many, which is the main reason to use it in a batch program that reads thousands of rows. The clause is FETCH NEXT ROWSET FROM cursor FOR n ROWS INTO :array, ...; the first fetch can be FETCH FIRST ROWSET. Each result column needs its own array, with one element per row of the rowset; a nullable column needs an indicator array. If the FOR n ROWS clause is left out of a later fetch, the size of the previous rowset fetch is used. SQLERRD(3) holds the number of rows actually returned: when the result table runs out inside a rowset, Db2 returns the rows that remain with SQLCODE +100 (and SQLSTATE 02000), and the program must process SQLERRD(3) rows before it stops. Copy SQLERRD(3) to a work item at once, because the next SQL statement overwrites it.

The program counts the loans still out, 100 rows at a time (z/OS; not run here):

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANROWS.
      * Count the loans still out, 100 rows per FETCH.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           EXEC SQL INCLUDE SQLCA END-EXEC.
       01  HV-MEMBER-ID          PIC S9(9) COMP OCCURS 100 TIMES.
       01  HV-BOOK-ID            PIC S9(9) COMP OCCURS 100 TIMES.
       01  HV-DUE-DATE           PIC X(10) OCCURS 100 TIMES.
       01  WS-GOT                PIC S9(9) COMP.
       01  WS-TOTAL              PIC S9(9) COMP VALUE ZERO.
       01  WS-IX                 PIC S9(9) COMP.
       01  WS-END-FLAG           PIC X VALUE "N".
           88  NO-MORE-ROWS      VALUE "Y".
           EXEC SQL
               DECLARE OUT_CSR CURSOR WITH ROWSET POSITIONING FOR
                 SELECT MEMBER_ID, BOOK_ID, DUE_DATE
                   FROM LOAN
                  WHERE RETURN_DATE IS NULL
                  ORDER BY MEMBER_ID, BOOK_ID
           END-EXEC.
       PROCEDURE DIVISION.
       MAIN-PARA.
           EXEC SQL OPEN OUT_CSR END-EXEC
           PERFORM UNTIL NO-MORE-ROWS
               EXEC SQL
                   FETCH NEXT ROWSET FROM OUT_CSR
                     FOR 100 ROWS
                     INTO :HV-MEMBER-ID, :HV-BOOK-ID, :HV-DUE-DATE
               END-EXEC
               MOVE SQLERRD(3) TO WS-GOT
               EVALUATE TRUE
                   WHEN SQLCODE < 0
                       DISPLAY "FETCH FAILED " SQLCODE
                       MOVE ZERO TO WS-GOT
                       MOVE 12 TO RETURN-CODE
                       SET NO-MORE-ROWS TO TRUE
                   WHEN SQLCODE = 100
                       SET NO-MORE-ROWS TO TRUE
               END-EVALUATE
               PERFORM VARYING WS-IX FROM 1 BY 1
                       UNTIL WS-IX > WS-GOT
                   ADD 1 TO WS-TOTAL
                   DISPLAY HV-MEMBER-ID(WS-IX) " "
                           HV-BOOK-ID(WS-IX) " " HV-DUE-DATE(WS-IX)
               END-PERFORM
           END-PERFORM
           EXEC SQL CLOSE OUT_CSR END-EXEC
           DISPLAY "LOANS OUT " WS-TOTAL
           GOBACK.

The cursor has an ORDER BY, so it is read-only, which is fine for a report. After a failed fetch the contents of SQLERRD(3) mean nothing, which is why the error branch sets WS-GOT to zero before the rows are processed. Positioned UPDATE and DELETE are also available for rowsets (FOR ROW n OF ROWSET selects one row of the current rowset), with an updatable cursor.

Program preparation

A COBOL program with EXEC SQL is not ready after a compile. Db2 must receive the SQL statements and the host variable descriptions, check them against the catalog, choose access paths and keep the result. The path from the source to a running program is:

flowchart TD SRC["COBOL source with EXEC SQL"] --> CMP["COBOL compiler with the SQL option (Db2 coprocessor)"] DCL["DCLGEN member, found through SYSLIB"] --> CMP CMP --> OBJ["Object module"] CMP --> DBRM["DBRM, written to DBRMLIB"] OBJ --> LNK["Link-edit with the Db2 language interface module"] LNK --> LOAD["Load module"] DBRM --> BPK["BIND PACKAGE into collection CIRC"] BPK --> PKG["Package in the Db2 catalog"] PKG --> BPL["BIND PLAN with PKLIST CIRC.*"] BPL --> PLAN["Application plan"] LOAD --> RUN["RUN PROGRAM under DSN, with PLAN"] PLAN --> RUN

The compile turns each SQL block into COBOL calls to the Db2 language interface and produces a database request module (DBRM) that holds the SQL text and the host variable information. The link step produces the load module, which includes the language interface module of the environment (DSNELI for a program run under the DSN command processor; other modules exist for CICS, RRSAF and CAF). The DBRM goes through BIND PACKAGE, which validates the tables, views and columns against the catalog, checks authorisation, selects access paths and stores the result as a package in a collection. A plan lists the packages, or whole collections, that an application may use. At run time Db2 matches the program to its package by name, collection and a consistency token stamped into both the load module and the DBRM.

Precompiler or coprocessor

There are two ways to turn the SQL blocks into calls. The Db2 coprocessor is built into the compiler: the SQL compiler option makes the compiler pass everything between EXEC SQL and END-EXEC to Db2 while it compiles, so there is no separate step, and the compile produces the object, the DBRM and the listing together. The separate precompiler is a step before the compile that writes a modified COBOL source with the SQL replaced by calls, and a DBRM. IBM recommends the coprocessor, and says that the separate precompiler is no longer enhanced; it also cannot be used with LP(64), whereas a program with embedded SQL under LP(64) needs SQL. With NOSQL, the default, SQL statements in the source are diagnosed and discarded.

Point Precompiler Coprocessor

When the SQL is processed

Separate step before the compile.

During the compile, with the SQL option.

Period after EXEC SQL INCLUDE ... END-EXEC

Optional.

Required; the include is handled like COPY.

COPY ... REPLACING and REPLACE

Apply to the expanded text produced from EXEC SQL.

Apply to the original source, EXEC SQL text included.

Code after END-EXEC on the same line

Ignored.

Processed.

Duplicate host variable names

The first definition that maps to a valid Db2 type is used.

References must be unique or qualified (or use the QUALIFY option).

Columns of the EXEC SQL lines

Opening line in columns 12 to 72; continuation lines may start at column 8.

Every line in columns 12 to 72.

FOR BIT DATA host variables

No declaration needed.

Needs an EXEC SQL DECLARE ... VARIABLE FOR BIT DATA, or the NOSQLCCSID option.

FILLER items inside a host structure

Not flagged.

Error IGYPS0294-S; the statement is discarded.

SQL in nested programs and copybooks, REPLACE

Limited.

Supported; the compiler listing also shows the SQL and the Db2 diagnostics.

Compiling with the SQL option (z/OS; not run here)

The compile step names the SQL option in PARM and adds the data sets the coprocessor needs: the Db2 load library in STEPLIB (unless it is in the link list), the DCLGEN library in SYSLIB for the INCLUDE members, and DBRMLIB for the DBRM. A suboption string for Db2 goes in quotes, SQL("..."), and only the Db2 coprocessor reads it. The option applies to the first program of a batch compile sequence only; naming it for a later program gives a compiler diagnostic. Library names below are placeholders for the installation’s names (COBOL.SIGYCOMP, DB2.SDSNLOAD); the work data sets of the compile step are left out, as on the JCL page.

//LOANFINE JOB (ACCT),'CIRC BUILD',CLASS=A,MSGCLASS=X
//* Compile with the Db2 coprocessor: object module and DBRM
//COMPILE  EXEC PGM=IGYCRCTL,REGION=0M,PARM='SQL'
//STEPLIB  DD DISP=SHR,DSN=COBOL.SIGYCOMP
//         DD DISP=SHR,DSN=DB2.SDSNLOAD
//SYSIN    DD DISP=SHR,DSN=USERID.CIRC.COBOL(LOANFINE)
//SYSLIB   DD DISP=SHR,DSN=USERID.CIRC.DCLGEN
//DBRMLIB  DD DISP=SHR,DSN=USERID.CIRC.DBRMLIB(LOANFINE)
//SYSLIN   DD DISP=(MOD,PASS),DSN=&&OBJ,UNIT=SYSALLDA,
//             SPACE=(TRK,(5,5))
//SYSPRINT DD SYSOUT=*

After the compile, the object module is link-edited as in Compiling Linking and Language Environment, with the Db2 load library on the linkage editor’s SYSLIB so that DSNELI is included, and the load module goes to USERID.CIRC.LOADLIB. If a program is also compiled as a dynamically called subprogram, the DYNAM option and its restrictions with embedded SQL matter; see Subprograms: CALL and Linkage.

BIND PACKAGE and BIND PLAN (z/OS; not run here)

BIND PACKAGE takes one DBRM (the MEMBER and LIBRARY options name it) and puts a package into the collection named in PACKAGE(collection-id); ACTION(REPLACE) replaces the existing version, and ACTION(ADD) creates a new package or version. BIND PLAN lists the packages the plan may use in PKLIST, where an entry is collection.package and an asterisk stands for every package of a collection. A plan is what a program runs under: RUN PROGRAM(...) PLAN(...) names it, and a package cannot be run on its own. The MEMBER option of BIND PLAN, which binds DBRMs straight into a plan, is deprecated: bind packages.

//BINDRUN  EXEC PGM=IKJEFT01,DYNAMNBR=20
//STEPLIB  DD DISP=SHR,DSN=DB2.SDSNLOAD
//SYSTSPRT DD SYSOUT=*
//SYSPRINT DD SYSOUT=*
//SYSOUT   DD SYSOUT=*
//SYSTSIN  DD *
  DSN SYSTEM(DB2X)
  BIND PACKAGE(CIRC) LIBRARY('USERID.CIRC.DBRMLIB') MEMBER(LOANFINE) -
       ACTION(REPLACE) ISOLATION(CS) VALIDATE(BIND) QUALIFIER(USERID)
  BIND PLAN(CIRCPLAN) PKLIST(CIRC.*) ACTION(REPLACE) ISOLATION(CS)
  RUN PROGRAM(LOANFINE) PLAN(CIRCPLAN) LIBRARY('USERID.CIRC.LOADLIB')
  END
/*

DB2X stands for the Db2 subsystem name, which is installation-specific. The options used are the ones the subcommand reference lists:

  • Collection. A collection is a named group of packages, here CIRC, that lets one set of programs be bound, authorised and rebound together; the plan’s PKLIST names CIRC.*. A program can be bound into several collections (for example a test and a production one) and the run picks the one in the PKLIST or in the CURRENT PACKAGESET special register.

  • Isolation. ISOLATION says how far the program is protected from other users: UR (uncommitted read: no locks on data, can read uncommitted changes; applies only to read-only operations, any other operation uses CS), CS (cursor stability: a row changed by another process is not read until that process releases it, and rows already read can change), RS (read stability: rows matching the search condition cannot be changed by others until you commit) and RR (repeatable read: protects every row read until commit). The default for BIND PLAN is CS; for a local BIND PACKAGE it is the plan’s value. The Circulation batch programs use CS.

  • Validate. VALIDATE(BIND) reports missing objects and authority errors at bind time and fails the bind; VALIDATE(RUN) postpones them to run time.

  • Qualifier and owner. QUALIFIER gives the schema for unqualified table names, which is why the programs above say FROM LOAN and not FROM USERID.LOAN; OWNER sets the package owner.

  • Release. RELEASE(COMMIT) or RELEASE(DEALLOCATE) decides when resources such as table-space locks are released.

A rebind is needed when the access path should change (after statistics change), a recompile is not; a recompile and a new bind are needed when the SQL text changes. Mixing them up produces the two classic errors: -805 (reason code 03 usually means the DBRM for the running program version was never bound into the collection of the plan) and -818 (the load module and the bound DBRM come from different compiles).

Off the mainframe: GnuCOBOL and PostgreSQL

GnuCOBOL has no embedded SQL. Compiling a program with an EXEC SQL block, such as the next one, gives an error because the compiler sees EXEC as an unknown statement:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. NOSQL.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COUNT              PIC S9(9) COMP VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           EXEC SQL
               SELECT COUNT(*) INTO :WS-COUNT FROM LOAN
           END-EXEC
           DISPLAY WS-COUNT
           GOBACK.

Compiled with cobc -x -Wall nosql.cob it prints (a real GnuCOBOL 3.2 run on 2026-10-20):

nosql.cob: in paragraph 'MAIN-PARA':
nosql.cob:8: error: syntax error, unexpected Identifier, expecting SECTION or .
    6 |        PROCEDURE DIVISION.
    7 |        MAIN-PARA.
    8 >            EXEC SQL
    9 |                SELECT COUNT(*) INTO :WS-COUNT FROM LOAN
   10 |            END-EXEC
nosql.cob:8: note: unknown statement 'EXEC'

Embedded SQL under GnuCOBOL needs an ESQL preprocessor that translates the EXEC SQL blocks into plain CALL statements to a runtime library before cobc runs. Two open-source tools do this. Both are community projects that are not part of GnuCOBOL and not supported by IBM or the GnuCOBOL maintainers; they were looked at on 2026-10-20 only through their repository pages, and nothing here was installed or run:

  • GixSQL (https://codeberg.org/GnuCOBOL/gixsql) is an ESQL preprocessor plus runtime libraries for GnuCOBOL and compatible compilers. Its repository lists support for PostgreSQL, ODBC, MySQL, Oracle and SQLite, with the preprocessor under GPLv3 or later and the runtime under LGPLv3 or later. Its README shows the preprocessing step with the gixpp tool (the -e option enables ESQL processing), an SQLCA copy member supplied with the tool, EXEC SQL CONNECT TO ... USER ... and cursors including UPDATE ... WHERE CURRENT OF. The repository shows recent development.

  • Open COBOL ESQL, ocesql (https://github.com/opensourcecobol/Open-COBOL-ESQL) is an embedded SQL precompiler and runtime library that supports PostgreSQL only and needs the PostgreSQL client library libpq. Its README shows ocesql SOURCE DEST to precompile and cobc -x -locesql DEST to build, and lists CONNECT, DECLARE CURSOR, SELECT INTO and FETCH among the supported statements.

What is different compared with Db2 on z/OS, whatever the tool: there is no DCLGEN (host variables are declared by hand, or generated by a script), no DBRM, no BIND, no package or plan and no collection, because the preprocessor emits calls that send the SQL text to the database at run time; connections are explicit CONNECT statements; and the SQL dialect is the database’s own, so CURRENT DATE, DAYS() and FETCH FIRST ... ROWS ONLY in the programs above would be CURRENT_DATE, date subtraction and LIMIT for PostgreSQL. The data type mapping follows each tool’s documentation, not the Db2 table above. The statement set also differs: multi-row FETCH into arrays, GET DIAGNOSTICS and WITH HOLD should be checked against the tool before relying on them. Programs written for one environment are therefore ported by editing, not by recompiling.

Common mistakes

  • Not testing SQLCODE after a statement. Db2 does not stop the program on an error; execution continues with the next statement, with unchanged host variables, unless WHENEVER or an explicit test intervenes.

  • Treating +100 as an error. It is the normal end of data of a FETCH and the normal "not there" of a SELECT INTO; handle it as a branch, not as a failure.

  • A hyphen in a cursor name or a column alias. OVERDUE-CSR is a subtraction in SQL; use OVERDUE_CSR. Host variable names are the exception.

  • Declaring a different name than the one opened. OPEN, FETCH and CLOSE must use the name from DECLARE CURSOR exactly; a mismatch is a compile or bind error at best.

  • FETCH or CLOSE on a cursor that COMMIT closed. A cursor without WITH HOLD is closed by the commit, and the next FETCH ends with -501.

  • A positioned UPDATE after COMMIT without a FETCH. A held cursor must be repositioned by a FETCH first; otherwise -508.

  • No indicator for a nullable column. A null reaches a plain host variable and the statement fails with -305. Aggregates over an empty set are null too.

  • SELECT INTO over a non-unique condition. More than one row ends with -811; use a cursor or limit the result deliberately.

  • FLOAT host variables for money. Use DECIMAL columns and COMP-3 items so the arithmetic stays exact.

  • Mixing the precompiler and the coprocessor rules. The missing period after EXEC SQL INCLUDE, FILLER items in a host structure and EXEC SQL text outside columns 12 to 72 are accepted by one and rejected by the other.

  • Recompiling without rebinding. The load module and the DBRM carry matching timestamps; a changed program needs a new DBRM bound into the package, or the run ends with -818 or -805.

  • Hand-coding the communication area. EXEC SQL INCLUDE SQLCA END-EXEC. is the only layout guaranteed to match the Db2 release.

  • Never committing in a long batch loop. The locks of the whole run are held until the end and other users meet -911; commit at an interval and use WITH HOLD if a cursor must survive it.

  • Treating EXEC SQL INCLUDE as a database read. It copies a source member at preparation time and reads nothing from the table; the DCLGEN member must be regenerated when the table changes.

References