For the first several decades of mainframe computing, COBOL programs stored and retrieved data exclusively through file systems -- sequential files, indexed files (VSAM KSDS), and relative files (VSAM RRDS). Every program that needed access to...
In This Chapter
- Introduction: COBOL Meets the Relational Database
- 22.1 DB2 Overview
- 22.2 EXEC SQL...END-EXEC Syntax
- 22.3 Host Variables
- 22.4 The SQLCA: SQL Communication Area
- 22.5 VARCHAR Host Variables
- 22.6 Single-Row Operations: SELECT INTO
- 22.7 INSERT, UPDATE, and DELETE
- 22.8 Cursors: Multi-Row Processing
- 22.9 Indicator Variables for NULL Handling
- 22.10 Indicator Variable Arrays and Group Host Variables
- 22.11 Transaction Control: COMMIT and ROLLBACK
- 22.12 The WHENEVER Statement
- 22.11 The DB2 Precompile and Bind Process
- 22.12 Common DB2 Programming Errors and Solutions
- 22.13 Best Practices for COBOL-DB2 Programs
- 22.14 GnuCOBOL ODBC Alternative
- 22.13 Complete COBOL-DB2 Program Example
- Summary
- Advanced Topic: Multi-Table Queries
- Exercises
Chapter 22: Embedded SQL and DB2 Fundamentals
IBM Enterprise COBOL
Introduction: COBOL Meets the Relational Database
For the first several decades of mainframe computing, COBOL programs stored and retrieved data exclusively through file systems -- sequential files, indexed files (VSAM KSDS), and relative files (VSAM RRDS). Every program that needed access to customer records, account balances, or transaction histories had to manage its own file I/O, its own record locking, its own index structures, and its own data integrity. If two programs needed to access the same data simultaneously, the application programmers had to coordinate that access themselves, a task that was error-prone and often led to data corruption.
IBM's DB2 relational database management system, first released in 1983, fundamentally changed this model. DB2 manages data storage, indexing, concurrent access, recovery, and integrity centrally. COBOL programs access DB2 data not through READ, WRITE, and REWRITE statements, but through SQL statements embedded directly in the COBOL source code. The DB2 precompiler translates these embedded SQL statements into CALL statements that invoke DB2's runtime services, making the COBOL-DB2 integration seamless.
Today, the majority of COBOL programs on IBM mainframes access DB2 databases. Banking systems, insurance policy administration, government benefits processing, and airline reservation systems all use COBOL-DB2 programs as their primary data access layer. Understanding embedded SQL in COBOL is not optional for enterprise COBOL programmers -- it is a core professional skill.
This chapter covers the fundamentals of embedded SQL in COBOL: the syntax for embedding SQL statements, host variables for data exchange, the SQLCA for error detection, single-row operations, cursor-based multi-row processing, indicator variables for NULL handling, and the precompile/bind process that prepares a COBOL-DB2 program for execution.
22.1 DB2 Overview
DB2 for z/OS is IBM's relational database management system for the mainframe platform. It stores data in tables (rows and columns), enforces data integrity through constraints and triggers, provides concurrent access through locking and isolation levels, and supports recovery through logging and backup facilities.
Key DB2 Concepts for COBOL Programmers
| Concept | Description |
|---|---|
| Table | A collection of rows with a defined column structure, analogous to a file with a record layout |
| Row | A single record in a table |
| Column | A field within a row, with a defined data type |
| Primary Key | A unique identifier for each row, analogous to a KSDS record key |
| Index | An ordered structure that provides fast access to rows by key values |
| SQL | Structured Query Language, the language used to query and manipulate DB2 data |
| SQLCA | SQL Communication Area, a data structure that DB2 uses to report the status of each SQL operation |
| Cursor | A mechanism for processing multiple rows, one at a time, analogous to sequential file reading |
| Commit | A checkpoint that makes all changes since the last commit permanent |
| Rollback | Undoes all changes since the last commit |
DB2 Data Types and COBOL Equivalents
| DB2 Data Type | COBOL Equivalent | Example |
|---|---|---|
| INTEGER | PIC S9(9) COMP | 01 HV-CUST-ID PIC S9(9) COMP. |
| SMALLINT | PIC S9(4) COMP | 01 HV-STATUS PIC S9(4) COMP. |
| DECIMAL(p,s) | PIC S9(p-s)V9(s) COMP-3 | 01 HV-AMOUNT PIC S9(5)V99 COMP-3. |
| CHAR(n) | PIC X(n) | 01 HV-NAME PIC X(30). |
| VARCHAR(n) | 49-level structure (see 22.5) | Special structure |
| DATE | PIC X(10) | 01 HV-DATE PIC X(10). |
| TIME | PIC X(8) | 01 HV-TIME PIC X(8). |
| TIMESTAMP | PIC X(26) | 01 HV-TSTAMP PIC X(26). |
22.2 EXEC SQL...END-EXEC Syntax
All SQL statements embedded in COBOL are delimited by the EXEC SQL and END-EXEC keywords:
EXEC SQL
SELECT CUST_NAME, CUST_BALANCE
INTO :HV-CUST-NAME, :HV-CUST-BALANCE
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
Rules for Embedded SQL
- EXEC SQL begins the statement: It tells the precompiler that SQL follows.
- END-EXEC terminates the statement: It tells the precompiler that the SQL statement is complete. Note: no period after END-EXEC unless it ends a COBOL sentence.
- Host variables are prefixed with colon (:): When you reference a COBOL variable inside SQL, you prefix it with a colon. Outside SQL, you use the variable name without the colon.
- SQL statements can span multiple lines: SQL does not need to fit on one line. Continue on subsequent lines within the EXEC SQL...END-EXEC delimiters.
- SQL is not case-sensitive: SELECT, Select, and select are all equivalent in SQL (though uppercase is conventional).
- COBOL comments are allowed between EXEC SQL and END-EXEC in most implementations.
Where SQL Statements Can Appear
Embedded SQL statements can appear in:
- WORKING-STORAGE SECTION: For INCLUDE statements (SQLCA, DCLGEN)
- PROCEDURE DIVISION: For all executable SQL statements (SELECT, INSERT, UPDATE, DELETE, OPEN, FETCH, CLOSE, COMMIT, ROLLBACK)
WORKING-STORAGE SECTION.
* Include the SQLCA
EXEC SQL
INCLUDE SQLCA
END-EXEC
* Include the DCLGEN for the CUSTOMER table
EXEC SQL
INCLUDE DCLCUST
END-EXEC
PROCEDURE DIVISION.
2000-PROCESS.
* Executable SQL in the PROCEDURE DIVISION
EXEC SQL
SELECT CUST_NAME
INTO :HV-CUST-NAME
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
.
22.3 Host Variables
Host variables are COBOL data items that exchange data between the COBOL program and DB2. They serve two purposes: they provide input values to SQL statements (parameters for WHERE clauses, values for INSERT and UPDATE), and they receive output values from SQL statements (columns from SELECT).
Declaring Host Variables
Host variables are declared in the WORKING-STORAGE SECTION, optionally within a DECLARE SECTION:
WORKING-STORAGE SECTION.
EXEC SQL BEGIN DECLARE SECTION END-EXEC.
01 HV-CUST-ID PIC S9(9) COMP.
01 HV-CUST-NAME PIC X(30).
01 HV-CUST-BALANCE PIC S9(7)V99 COMP-3.
01 HV-CUST-STATUS PIC X(01).
01 HV-TRANS-DATE PIC X(10).
01 HV-TRANS-AMOUNT PIC S9(9)V99 COMP-3.
EXEC SQL END DECLARE SECTION END-EXEC.
The BEGIN DECLARE SECTION and END DECLARE SECTION delimiters are optional in DB2 for z/OS but recommended for clarity and portability. They explicitly identify which variables are intended for use as host variables.
Naming Conventions
Host variables are commonly prefixed with HV- or H- to distinguish them from regular WORKING-STORAGE variables:
* GOOD: Clear prefix identifies host variables
01 HV-CUSTOMER-ID PIC S9(9) COMP.
01 HV-CUSTOMER-NAME PIC X(30).
* ALSO ACCEPTABLE: DCLGEN-generated names
01 DCUST-ID PIC S9(9) COMP.
01 DCUST-NAME PIC X(30).
The Colon Prefix in SQL
Inside an EXEC SQL block, host variables are identified by a colon (:) prefix. Outside SQL, the colon is not used:
* Set the host variable (COBOL -- no colon)
MOVE 12345 TO HV-CUST-ID
* Use it in SQL (colon prefix required)
EXEC SQL
SELECT CUST_NAME, CUST_BALANCE
INTO :HV-CUST-NAME, :HV-CUST-BALANCE
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
* Use the results (COBOL -- no colon)
DISPLAY 'CUSTOMER: ' HV-CUST-NAME
DISPLAY 'BALANCE: ' HV-CUST-BALANCE
Data Type Mapping Rules
When declaring host variables, the COBOL data type must be compatible with the DB2 column data type:
* DB2 INTEGER -> COBOL COMP (fullword binary)
01 HV-CUST-ID PIC S9(9) COMP.
* DB2 SMALLINT -> COBOL COMP (halfword binary)
01 HV-DEPT-CODE PIC S9(4) COMP.
* DB2 DECIMAL(9,2) -> COBOL COMP-3 (packed decimal)
01 HV-SALARY PIC S9(7)V99 COMP-3.
* DB2 CHAR(30) -> COBOL PIC X
01 HV-LAST-NAME PIC X(30).
* DB2 DATE -> COBOL PIC X(10) (format: YYYY-MM-DD)
01 HV-HIRE-DATE PIC X(10).
* DB2 TIMESTAMP -> COBOL PIC X(26)
* Format: YYYY-MM-DD-HH.MM.SS.NNNNNN
01 HV-LAST-UPDATE PIC X(26).
DCLGEN: Automatic Host Variable Generation
IBM provides the DCLGEN (Declaration Generator) utility, which examines a DB2 table definition and generates the corresponding COBOL host variable declarations and an SQL DECLARE TABLE statement. This ensures that your host variables exactly match the table definition:
* Generated by DCLGEN utility:
EXEC SQL DECLARE CUSTOMER TABLE
( CUST_ID INTEGER NOT NULL,
CUST_NAME CHAR(30) NOT NULL,
CUST_ADDRESS VARCHAR(100),
CUST_BALANCE DECIMAL(9,2) NOT NULL,
CUST_STATUS CHAR(1) NOT NULL,
LAST_UPDATE TIMESTAMP NOT NULL
) END-EXEC.
* COBOL declaration for CUSTOMER
01 DCLCUSTOMER.
10 DCUST-ID PIC S9(9) COMP.
10 DCUST-NAME PIC X(30).
10 DCUST-ADDRESS.
49 DCUST-ADDRESS-LEN PIC S9(4) COMP.
49 DCUST-ADDRESS-TEXT PIC X(100).
10 DCUST-BALANCE PIC S9(7)V99 COMP-3.
10 DCUST-STATUS PIC X(1).
10 DCUST-LAST-UPD PIC X(26).
22.4 The SQLCA: SQL Communication Area
The SQLCA (SQL Communication Area) is a data structure that DB2 populates after every SQL statement execution. It contains the return code, error messages, and diagnostic information that your program uses to determine whether the SQL statement succeeded or failed.
Including the SQLCA
WORKING-STORAGE SECTION.
EXEC SQL
INCLUDE SQLCA
END-EXEC
Key SQLCA Fields
| Field | Type | Description |
|---|---|---|
| SQLCODE | PIC S9(9) COMP | Return code. 0 = success, +100 = not found, negative = error |
| SQLSTATE | PIC X(5) | SQL state code (ANSI standard) |
| SQLERRD(3) | PIC S9(9) COMP | Number of rows affected by INSERT, UPDATE, DELETE |
| SQLWARN0 | PIC X | 'W' if any warning indicator is set |
| SQLWARN1 | PIC X | 'W' if a column value was truncated |
| SQLWARN2 | PIC X | 'W' if NULL values were eliminated from a function |
| SQLERRMC | PIC X(70) | Error message tokens |
| SQLERRML | PIC S9(4) COMP | Length of SQLERRMC |
SQLCODE Values
The SQLCODE field is the primary mechanism for determining the outcome of an SQL statement:
| SQLCODE | Meaning |
|---|---|
| 0 | Successful execution |
| +100 | Row not found (SELECT INTO returned no rows, FETCH reached end of cursor) |
| -180 | String-to-date conversion error |
| -181 | Date or time value out of range |
| -204 | Object not defined in DB2 |
| -205 | Column not in specified table |
| -206 | Column not found |
| -305 | Indicator variable required (NULL value) |
| -501 | Cursor not open |
| -803 | Duplicate key on INSERT or UPDATE |
| -811 | SELECT INTO returned more than one row |
| -818 | Timestamp mismatch (plan and DBRM) |
| -904 | Unavailable resource |
| -911 | Deadlock or timeout, rollback performed |
| -913 | Deadlock or timeout, no rollback |
Checking SQLCODE After Every SQL Statement
2200-READ-CUSTOMER.
EXEC SQL
SELECT CUST_NAME, CUST_BALANCE
INTO :HV-CUST-NAME, :HV-CUST-BALANCE
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
EVALUATE SQLCODE
WHEN 0
SET WS-CUSTOMER-FOUND TO TRUE
WHEN +100
SET WS-CUSTOMER-NOT-FOUND TO TRUE
WHEN -811
DISPLAY 'DUPLICATE CUSTOMER IDS: '
HV-CUST-ID
SET WS-DATA-ERROR TO TRUE
WHEN OTHER
DISPLAY 'SQL ERROR: ' SQLCODE
DISPLAY 'SQLSTATE: ' SQLSTATE
PERFORM 9100-SQL-ERROR
END-EVALUATE
.
Centralized SQL Error Handler
9100-SQL-ERROR.
DISPLAY '*** SQL ERROR ***'
DISPLAY 'SQLCODE: ' SQLCODE
DISPLAY 'SQLSTATE: ' SQLSTATE
DISPLAY 'SQLERRMC: ' SQLERRMC(1:SQLERRML)
DISPLAY 'PARAGRAPH: ' WS-CURRENT-PARAGRAPH
DISPLAY 'ROWS: ' SQLERRD(3)
MOVE 16 TO RETURN-CODE
PERFORM 9900-ABORT
.
22.5 VARCHAR Host Variables
DB2 VARCHAR columns store variable-length character data. In COBOL, VARCHAR host variables are represented using a special two-part structure with a 49-level layout:
01 HV-CUSTOMER-ADDRESS.
49 HV-CUST-ADDR-LEN PIC S9(4) COMP.
49 HV-CUST-ADDR-TEXT PIC X(100).
The 49-level structure tells the DB2 precompiler that this is a VARCHAR host variable. The first field (LEN) contains the actual length of the data. The second field (TEXT) contains the data itself, padded with spaces to its declared length.
Using VARCHAR Host Variables
* Sending a VARCHAR value to DB2
MOVE 42 TO HV-CUST-ADDR-LEN
MOVE '123 Main Street, Springfield, IL 62701'
TO HV-CUST-ADDR-TEXT
EXEC SQL
UPDATE CUSTOMER
SET CUST_ADDRESS = :HV-CUSTOMER-ADDRESS
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
* Receiving a VARCHAR value from DB2
EXEC SQL
SELECT CUST_ADDRESS
INTO :HV-CUSTOMER-ADDRESS
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
DISPLAY 'ADDRESS LENGTH: ' HV-CUST-ADDR-LEN
DISPLAY 'ADDRESS: '
HV-CUST-ADDR-TEXT(1:HV-CUST-ADDR-LEN)
22.6 Single-Row Operations: SELECT INTO
The SELECT INTO statement retrieves a single row from a DB2 table into host variables. It is used when you know that the query will return exactly zero or one row (typically when querying by primary key).
Basic SELECT INTO
2200-GET-CUSTOMER.
MOVE WS-INPUT-CUST-ID TO HV-CUST-ID
EXEC SQL
SELECT CUST_NAME,
CUST_BALANCE,
CUST_STATUS,
LAST_UPDATE
INTO :HV-CUST-NAME,
:HV-CUST-BALANCE,
:HV-CUST-STATUS,
:HV-LAST-UPDATE
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
EVALUATE SQLCODE
WHEN 0
PERFORM 2210-CUSTOMER-FOUND
WHEN +100
PERFORM 2220-CUSTOMER-NOT-FOUND
WHEN -811
DISPLAY 'MULTIPLE ROWS FOR KEY: '
HV-CUST-ID
PERFORM 9100-SQL-ERROR
WHEN OTHER
PERFORM 9100-SQL-ERROR
END-EVALUATE
.
SELECT INTO with Expressions and Functions
* Using SQL functions
EXEC SQL
SELECT COUNT(*),
SUM(TRANS_AMOUNT),
MAX(TRANS_DATE)
INTO :HV-TRANS-COUNT,
:HV-TOTAL-AMOUNT,
:HV-LAST-TRANS-DATE
FROM TRANSACTION
WHERE ACCT_ID = :HV-ACCT-ID
AND TRANS_DATE >= :HV-START-DATE
END-EXEC
22.7 INSERT, UPDATE, and DELETE
INSERT
2300-ADD-CUSTOMER.
MOVE WS-NEW-CUST-ID TO HV-CUST-ID
MOVE WS-NEW-CUST-NAME TO HV-CUST-NAME
MOVE 0 TO HV-CUST-BALANCE
MOVE 'A' TO HV-CUST-STATUS
EXEC SQL
INSERT INTO CUSTOMER
(CUST_ID, CUST_NAME, CUST_BALANCE,
CUST_STATUS, LAST_UPDATE)
VALUES
(:HV-CUST-ID, :HV-CUST-NAME,
:HV-CUST-BALANCE, :HV-CUST-STATUS,
CURRENT TIMESTAMP)
END-EXEC
EVALUATE SQLCODE
WHEN 0
ADD 1 TO CT-INSERTS
WHEN -803
DISPLAY 'DUPLICATE CUSTOMER: '
HV-CUST-ID
ADD 1 TO CT-DUPLICATES
WHEN OTHER
PERFORM 9100-SQL-ERROR
END-EVALUATE
.
UPDATE
2400-UPDATE-BALANCE.
EXEC SQL
UPDATE CUSTOMER
SET CUST_BALANCE = CUST_BALANCE
+ :HV-TRANS-AMOUNT,
LAST_UPDATE = CURRENT TIMESTAMP
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
EVALUATE SQLCODE
WHEN 0
IF SQLERRD(3) = 0
DISPLAY 'NO ROW UPDATED FOR: '
HV-CUST-ID
ELSE
ADD 1 TO CT-UPDATES
END-IF
WHEN OTHER
PERFORM 9100-SQL-ERROR
END-EVALUATE
.
DELETE
2500-DELETE-CUSTOMER.
EXEC SQL
DELETE FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
AND CUST_STATUS = 'I'
END-EXEC
EVALUATE SQLCODE
WHEN 0
MOVE SQLERRD(3) TO WS-ROWS-DELETED
IF WS-ROWS-DELETED = 0
DISPLAY 'NO INACTIVE CUSTOMER: '
HV-CUST-ID
ELSE
ADD WS-ROWS-DELETED TO CT-DELETES
END-IF
WHEN OTHER
PERFORM 9100-SQL-ERROR
END-EVALUATE
.
22.8 Cursors: Multi-Row Processing
When an SQL query returns multiple rows, you cannot use SELECT INTO (which expects exactly one row). Instead, you use a cursor -- a mechanism that lets you process the result set one row at a time, similar to reading a sequential file.
Cursor Lifecycle
The cursor lifecycle parallels file processing:
| File I/O | Cursor Operation | Purpose |
|---|---|---|
| SELECT/FD (declaration) | DECLARE CURSOR | Define the query |
| OPEN file | OPEN cursor | Execute the query and prepare the result set |
| READ file | FETCH cursor | Retrieve the next row |
| CLOSE file | CLOSE cursor | Release the result set |
DECLARE CURSOR
The DECLARE CURSOR statement defines the SQL query associated with the cursor. It is a non-executable statement and is typically placed in WORKING-STORAGE:
WORKING-STORAGE SECTION.
EXEC SQL
DECLARE CUST-CURSOR CURSOR FOR
SELECT CUST_ID, CUST_NAME, CUST_BALANCE
FROM CUSTOMER
WHERE CUST_STATUS = :HV-STATUS-FILTER
ORDER BY CUST_NAME
END-EXEC
OPEN, FETCH, and CLOSE
2000-PROCESS-CUSTOMERS.
MOVE 'A' TO HV-STATUS-FILTER
EXEC SQL
OPEN CUST-CURSOR
END-EXEC
IF SQLCODE NOT = 0
DISPLAY 'CURSOR OPEN ERROR: ' SQLCODE
PERFORM 9100-SQL-ERROR
END-IF
PERFORM 2100-FETCH-CUSTOMER
PERFORM UNTIL SQLCODE = +100
PERFORM 2200-PROCESS-CUSTOMER
PERFORM 2100-FETCH-CUSTOMER
END-PERFORM
EXEC SQL
CLOSE CUST-CURSOR
END-EXEC
IF SQLCODE NOT = 0
DISPLAY 'CURSOR CLOSE ERROR: ' SQLCODE
END-IF
.
2100-FETCH-CUSTOMER.
EXEC SQL
FETCH CUST-CURSOR
INTO :HV-CUST-ID,
:HV-CUST-NAME,
:HV-CUST-BALANCE
END-EXEC
EVALUATE SQLCODE
WHEN 0
ADD 1 TO CT-ROWS-FETCHED
WHEN +100
CONTINUE
WHEN OTHER
PERFORM 9100-SQL-ERROR
END-EVALUATE
.
2200-PROCESS-CUSTOMER.
DISPLAY HV-CUST-ID ' '
HV-CUST-NAME ' '
HV-CUST-BALANCE
.
WITH HOLD Cursor
A normal cursor is automatically closed when a COMMIT is issued. A WITH HOLD cursor remains open across COMMIT operations, which is essential for long-running batch programs that commit periodically:
EXEC SQL
DECLARE BATCH-CURSOR CURSOR WITH HOLD FOR
SELECT CUST_ID, CUST_NAME, CUST_BALANCE
FROM CUSTOMER
WHERE CUST_STATUS = 'A'
ORDER BY CUST_ID
END-EXEC
FOR UPDATE Cursor
If you intend to update or delete rows as you process them through a cursor, declare the cursor FOR UPDATE:
EXEC SQL
DECLARE UPD-CURSOR CURSOR FOR
SELECT CUST_ID, CUST_NAME, CUST_BALANCE
FROM CUSTOMER
WHERE CUST_STATUS = 'A'
FOR UPDATE OF CUST_BALANCE, LAST_UPDATE
END-EXEC
2300-UPDATE-CURRENT-ROW.
EXEC SQL
UPDATE CUSTOMER
SET CUST_BALANCE = CUST_BALANCE *
(1 + :HV-INTEREST-RATE),
LAST_UPDATE = CURRENT TIMESTAMP
WHERE CURRENT OF UPD-CURSOR
END-EXEC
IF SQLCODE NOT = 0
PERFORM 9100-SQL-ERROR
END-IF
.
22.9 Indicator Variables for NULL Handling
In DB2, a column can contain a NULL value, meaning "no data" or "unknown." COBOL has no concept of NULL; every variable always contains some value. Indicator variables bridge this gap by telling the COBOL program whether a host variable contains a real value or represents a NULL.
Declaring Indicator Variables
Indicator variables are declared as PIC S9(4) COMP:
01 HV-CUST-ADDRESS PIC X(100).
01 IND-CUST-ADDRESS PIC S9(4) COMP.
01 HV-PHONE-NUMBER PIC X(15).
01 IND-PHONE-NUMBER PIC S9(4) COMP.
Using Indicator Variables in SELECT
An indicator variable follows its host variable in the INTO clause, separated by no comma (some shops use a colon prefix on the indicator as well):
EXEC SQL
SELECT CUST_NAME,
CUST_ADDRESS,
PHONE_NUMBER
INTO :HV-CUST-NAME,
:HV-CUST-ADDRESS :IND-CUST-ADDRESS,
:HV-PHONE-NUMBER :IND-PHONE-NUMBER
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
IF SQLCODE = 0
IF IND-CUST-ADDRESS < 0
MOVE 'NO ADDRESS ON FILE'
TO WS-DISPLAY-ADDRESS
ELSE
MOVE HV-CUST-ADDRESS
TO WS-DISPLAY-ADDRESS
END-IF
IF IND-PHONE-NUMBER < 0
MOVE 'NO PHONE ON FILE'
TO WS-DISPLAY-PHONE
ELSE
MOVE HV-PHONE-NUMBER
TO WS-DISPLAY-PHONE
END-IF
END-IF
Indicator Variable Values
| Value | Meaning |
|---|---|
| 0 | The host variable contains a valid non-NULL value |
| -1 | The column value is NULL |
| -2 | The column value is NULL (result of numeric conversion error) |
| > 0 | The original column length before truncation |
Using Indicator Variables in INSERT/UPDATE
To insert or update a NULL value, set the indicator variable to -1:
* Insert with a NULL address
MOVE -1 TO IND-CUST-ADDRESS
EXEC SQL
INSERT INTO CUSTOMER
(CUST_ID, CUST_NAME, CUST_ADDRESS)
VALUES
(:HV-CUST-ID, :HV-CUST-NAME,
:HV-CUST-ADDRESS :IND-CUST-ADDRESS)
END-EXEC
* Update to set a column to NULL
MOVE -1 TO IND-PHONE-NUMBER
EXEC SQL
UPDATE CUSTOMER
SET PHONE_NUMBER =
:HV-PHONE-NUMBER :IND-PHONE-NUMBER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
SQLCODE -305: Missing Indicator Variable
If a SELECT returns a NULL value and you did not provide an indicator variable, DB2 returns SQLCODE -305. This is a common error:
* This will fail with -305 if CUST_ADDRESS is NULL:
EXEC SQL
SELECT CUST_ADDRESS
INTO :HV-CUST-ADDRESS
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
* SQLCODE = -305 if the value is NULL
* FIX: Always use an indicator variable for nullable columns
EXEC SQL
SELECT CUST_ADDRESS
INTO :HV-CUST-ADDRESS :IND-CUST-ADDRESS
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
22.10 Indicator Variable Arrays and Group Host Variables
Indicator Variable Arrays
When working with multiple nullable columns, you can define an indicator variable array instead of individual indicator variables:
01 HV-CUSTOMER-DATA.
05 HV-CUST-ID PIC S9(9) COMP.
05 HV-CUST-NAME PIC X(30).
05 HV-CUST-ADDRESS PIC X(100).
05 HV-CUST-PHONE PIC X(15).
05 HV-CUST-EMAIL PIC X(50).
01 IND-CUSTOMER-DATA.
05 IND-CUST-ID PIC S9(4) COMP.
05 IND-CUST-NAME PIC S9(4) COMP.
05 IND-CUST-ADDRESS PIC S9(4) COMP.
05 IND-CUST-PHONE PIC S9(4) COMP.
05 IND-CUST-EMAIL PIC S9(4) COMP.
You can then use the group indicator variable in the INTO clause:
EXEC SQL
SELECT CUST_ID, CUST_NAME, CUST_ADDRESS,
CUST_PHONE, CUST_EMAIL
INTO :HV-CUSTOMER-DATA :IND-CUSTOMER-DATA
FROM CUSTOMER
WHERE CUST_ID = :HV-SEARCH-ID
END-EXEC
When you specify a group indicator variable, DB2 maps the individual indicator fields to the corresponding host variable fields in order. The first indicator maps to the first host variable, the second to the second, and so on.
Working with Indicator Variables in Practice
A common pattern is to write a utility paragraph that checks all indicator variables and sets display values:
2250-FORMAT-CUSTOMER-DISPLAY.
IF IND-CUST-ADDRESS < 0
MOVE '(NO ADDRESS ON FILE)'
TO WS-DISPLAY-ADDRESS
ELSE
MOVE HV-CUST-ADDRESS TO WS-DISPLAY-ADDRESS
END-IF
IF IND-CUST-PHONE < 0
MOVE '(NO PHONE)'
TO WS-DISPLAY-PHONE
ELSE
MOVE HV-CUST-PHONE TO WS-DISPLAY-PHONE
END-IF
IF IND-CUST-EMAIL < 0
MOVE '(NO EMAIL)'
TO WS-DISPLAY-EMAIL
ELSE
MOVE HV-CUST-EMAIL TO WS-DISPLAY-EMAIL
END-IF
.
Truncation Detection with Indicator Variables
When a host variable is too small to hold the retrieved column value, DB2 truncates the data and sets the indicator variable to the original length of the data before truncation. This allows you to detect truncation:
01 HV-SHORT-NAME PIC X(10).
01 IND-SHORT-NAME PIC S9(4) COMP.
EXEC SQL
SELECT CUST_NAME
INTO :HV-SHORT-NAME :IND-SHORT-NAME
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
IF SQLCODE = 0
IF IND-SHORT-NAME > 10
DISPLAY 'NAME WAS TRUNCATED'
DISPLAY 'ORIGINAL LENGTH: '
IND-SHORT-NAME
DISPLAY 'TRUNCATED VALUE: '
HV-SHORT-NAME
ELSE
DISPLAY 'NAME: ' HV-SHORT-NAME
END-IF
END-IF
22.11 Transaction Control: COMMIT and ROLLBACK
DB2 operations are grouped into transactions (units of work). A transaction begins implicitly with the first SQL statement and ends with either a COMMIT (making all changes permanent) or a ROLLBACK (undoing all changes since the last commit).
COMMIT
EXEC SQL COMMIT END-EXEC
IF SQLCODE NOT = 0
DISPLAY 'COMMIT FAILED: ' SQLCODE
PERFORM 9100-SQL-ERROR
END-IF
ROLLBACK
EXEC SQL ROLLBACK END-EXEC
IF SQLCODE NOT = 0
DISPLAY 'ROLLBACK FAILED: ' SQLCODE
* A failed ROLLBACK is a serious problem
DISPLAY 'CONTACT DATABASE ADMINISTRATOR'
MOVE 16 TO RETURN-CODE
STOP RUN
END-IF
When to COMMIT in Batch Programs
Batch programs that process large volumes of data must commit periodically. Without periodic commits, the entire run is a single transaction, which means that a failure near the end would roll back all the work done at the beginning. Additionally, DB2's log would fill up, and locks would accumulate, blocking other programs.
A common pattern commits every N rows:
01 WS-COMMIT-INTERVAL PIC S9(5) COMP VALUE 500.
01 WS-UNCOMMITTED PIC S9(5) COMP VALUE 0.
2000-PROCESS-TRANSACTION.
* ... perform business logic and SQL updates ...
ADD 1 TO WS-UNCOMMITTED
IF WS-UNCOMMITTED >= WS-COMMIT-INTERVAL
EXEC SQL COMMIT END-EXEC
IF SQLCODE = 0
MOVE 0 TO WS-UNCOMMITTED
ADD 1 TO CT-COMMITS
ELSE
PERFORM 9100-SQL-ERROR
END-IF
END-IF
.
3000-TERMINATE.
* Final commit for remaining uncommitted work
IF WS-UNCOMMITTED > 0
EXEC SQL COMMIT END-EXEC
ADD 1 TO CT-COMMITS
END-IF
.
SAVEPOINT
DB2 supports savepoints, which allow partial rollback within a transaction. This is useful when a batch of operations contains some that can fail without requiring the entire unit of work to be rolled back:
EXEC SQL SAVEPOINT SP1 ON ROLLBACK
RETAIN CURSORS
END-EXEC
* Try an operation that might fail
EXEC SQL
INSERT INTO AUDIT_LOG ...
END-EXEC
IF SQLCODE < 0
* Roll back to the savepoint, not the entire
* transaction
EXEC SQL ROLLBACK TO SAVEPOINT SP1
END-EXEC
DISPLAY 'AUDIT INSERT ROLLED BACK'
END-IF
* Continue with the rest of the transaction
EXEC SQL COMMIT END-EXEC
22.12 The WHENEVER Statement
The WHENEVER statement provides a declarative way to handle SQL errors without explicit SQLCODE checking after every statement. It directs the precompiler to generate automatic error-checking code:
* On any SQL error, go to the error handler
EXEC SQL
WHENEVER SQLERROR
GO TO 9100-SQL-ERROR
END-EXEC
* When no rows found, continue (do not jump)
EXEC SQL
WHENEVER NOT FOUND
CONTINUE
END-EXEC
* On any SQL warning, continue
EXEC SQL
WHENEVER SQLWARNING
CONTINUE
END-EXEC
WHENEVER Options
| Condition | Triggers When |
|---|---|
| SQLERROR | SQLCODE < 0 (any error) |
| NOT FOUND | SQLCODE = +100 (no row found) |
| SQLWARNING | SQLWARN0 = 'W' (any warning) |
| Action | Effect |
|---|---|
| CONTINUE | Do nothing, continue with next statement |
| GO TO paragraph | Transfer control to the specified paragraph |
Caution with WHENEVER
WHENEVER is a precompiler directive, not a runtime statement. It applies to all subsequent SQL statements in the source code until another WHENEVER for the same condition overrides it. This can cause unexpected behavior:
* Set up error handling
EXEC SQL
WHENEVER SQLERROR GO TO 9100-SQL-ERROR
END-EXEC
* All SQL statements below here will GO TO 9100-SQL-ERROR
* on any SQL error -- even if you intend to handle specific
* errors differently
* To handle errors inline for a specific statement,
* temporarily disable WHENEVER:
EXEC SQL
WHENEVER SQLERROR CONTINUE
END-EXEC
EXEC SQL
INSERT INTO CUSTOMER ...
END-EXEC
* Handle -803 (duplicate) specifically
IF SQLCODE = -803
PERFORM 2310-HANDLE-DUPLICATE
ELSE IF SQLCODE < 0
PERFORM 9100-SQL-ERROR
END-IF
* Re-enable WHENEVER
EXEC SQL
WHENEVER SQLERROR GO TO 9100-SQL-ERROR
END-EXEC
Most experienced COBOL-DB2 programmers prefer explicit SQLCODE checking after every statement rather than relying on WHENEVER, because explicit checking gives more control and is easier to understand.
22.11 The DB2 Precompile and Bind Process
Unlike regular COBOL programs that go through compile and link-edit only, COBOL-DB2 programs require additional steps: precompile and bind.
The Process Flow
COBOL Source with SQL
|
v
DB2 PRECOMPILE (DSNHPC)
- Extracts SQL statements
- Replaces them with CALL statements
- Produces modified COBOL source
- Produces a DBRM (Database Request Module)
|
+-----> Modified COBOL source
| |
| v
| COBOL COMPILE (IGYCRCTL)
| |
| v
| LINK-EDIT (IEWL)
| |
| v
| Load Module
|
+-----> DBRM
|
v
DB2 BIND (DSNTIAD or IKJEFT01)
|
v
Package or Plan
(contains access paths)
JCL for DB2 Precompile, Compile, Link, and Bind
//*------------------------------------------------------------
//* STEP 1: DB2 PRECOMPILE
//*------------------------------------------------------------
//PC EXEC PGM=DSNHPC,
// PARM='HOST(COB2),APOST,APOSTSQL'
//STEPLIB DD DSN=DSN.V13R1.SDSNLOAD,DISP=SHR
//DBRMLIB DD DSN=USER.DBRMLIB(CUSTUPDT),DISP=SHR
//SYSCIN DD DSN=&&DSNHOUT,DISP=(NEW,PASS),
// UNIT=SYSDA,SPACE=(800,(500,500))
//SYSIN DD DSN=USER.SRCLIB(CUSTUPDT),DISP=SHR
//SYSPRINT DD SYSOUT=*
//SYSUT1 DD UNIT=SYSDA,SPACE=(800,(500,500))
//SYSUT2 DD UNIT=SYSDA,SPACE=(800,(500,500))
//*
//*------------------------------------------------------------
//* STEP 2: COBOL COMPILE
//*------------------------------------------------------------
//COB EXEC PGM=IGYCRCTL,
// PARM='LIB,RENT,LIST,MAP,XREF'
//STEPLIB DD DSN=IGY.V6R4M0.SIGYCOMP,DISP=SHR
//SYSIN DD DSN=&&DSNHOUT,DISP=(OLD,DELETE)
//SYSLIB DD DSN=USER.COPYLIB,DISP=SHR
//SYSPRINT DD SYSOUT=*
//SYSLIN DD DSN=&&LOADSET,DISP=(NEW,PASS),
// UNIT=SYSDA,SPACE=(800,(500,500))
//SYSUT1 DD UNIT=SYSDA,SPACE=(800,(500,500))
//SYSUT2 DD UNIT=SYSDA,SPACE=(800,(500,500))
//SYSUT3 DD UNIT=SYSDA,SPACE=(800,(500,500))
//SYSUT4 DD UNIT=SYSDA,SPACE=(800,(500,500))
//*
//*------------------------------------------------------------
//* STEP 3: LINK-EDIT
//*------------------------------------------------------------
//LKED EXEC PGM=IEWL,
// PARM='LIST,MAP,RENT,AMODE=31,RMODE=ANY'
//SYSLIB DD DSN=CEE.SCEELKED,DISP=SHR
// DD DSN=DSN.V13R1.SDSNLOAD,DISP=SHR
//SYSLMOD DD DSN=USER.LOADLIB(CUSTUPDT),DISP=SHR
//SYSLIN DD DSN=&&LOADSET,DISP=(OLD,DELETE)
// DD *
INCLUDE SYSLIB(DSNELI)
NAME CUSTUPDT(R)
/*
//SYSPRINT DD SYSOUT=*
//*
//*------------------------------------------------------------
//* STEP 4: DB2 BIND
//*------------------------------------------------------------
//BIND EXEC PGM=IKJEFT01
//STEPLIB DD DSN=DSN.V13R1.SDSNLOAD,DISP=SHR
//DBRMLIB DD DSN=USER.DBRMLIB,DISP=SHR
//SYSPRINT DD SYSOUT=*
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
DSN SYSTEM(DB2P)
BIND PACKAGE(CUSTCOLL) -
MEMBER(CUSTUPDT) -
ACT(REP) -
ISO(CS) -
VALIDATE(BIND) -
RELEASE(COMMIT)
END
/*
22.12 Common DB2 Programming Errors and Solutions
This section documents the most frequently encountered DB2 programming errors in COBOL and how to prevent or resolve them.
SQLCODE -818: Timestamp Mismatch
This is probably the most common DB2 error encountered by COBOL-DB2 developers. It means the load module was compiled with a DBRM that has a different timestamp than the one that was bound into the package.
Cause: The program was recompiled (which creates a new DBRM with a new timestamp) but the package was not rebound, or vice versa. This can happen when a developer recompiles but forgets to rebind, or when the bind job runs before the compile job completes.
Fix: Rebind the package using the DBRM from the most recent precompile, then ensure the load module from that same compile is deployed:
DSN SYSTEM(DB2P)
BIND PACKAGE(CUSTCOLL) MEMBER(CUSTUPDT) ACT(REP)
END
Prevention: Always compile and bind together in the same JCL job stream, with the compile step preceding the bind step. Never compile without binding, and never bind without compiling.
SQLCODE -811: SELECT INTO Returns Multiple Rows
This error occurs when a SELECT INTO query returns more than one row. SELECT INTO expects exactly zero or one row.
* This will produce -811 if multiple customers have
* the same last name:
EXEC SQL
SELECT CUST_ID, CUST_NAME
INTO :HV-CUST-ID, :HV-CUST-NAME
FROM CUSTOMER
WHERE CUST_LAST_NAME = :HV-SEARCH-NAME
END-EXEC
* FIX: Use a cursor when multiple rows are possible
* Or add FETCH FIRST 1 ROW ONLY if you only need one:
EXEC SQL
SELECT CUST_ID, CUST_NAME
INTO :HV-CUST-ID, :HV-CUST-NAME
FROM CUSTOMER
WHERE CUST_LAST_NAME = :HV-SEARCH-NAME
FETCH FIRST 1 ROW ONLY
END-EXEC
SQLCODE -803: Duplicate Key on INSERT
This error occurs when an INSERT statement tries to create a row with a primary key (or unique index key) that already exists.
* Handle duplicate key gracefully
EXEC SQL
INSERT INTO CUSTOMER
(CUST_ID, CUST_NAME, CUST_BALANCE)
VALUES
(:HV-CUST-ID, :HV-CUST-NAME, 0)
END-EXEC
EVALUATE SQLCODE
WHEN 0
ADD 1 TO CT-INSERTED
WHEN -803
DISPLAY 'CUSTOMER ALREADY EXISTS: '
HV-CUST-ID
ADD 1 TO CT-DUPLICATES
WHEN OTHER
PERFORM 9100-SQL-ERROR
END-EVALUATE
SQLCODE -305: NULL Value Without Indicator Variable
As discussed in Section 22.9, this error occurs when DB2 attempts to place a NULL value into a host variable that has no indicator variable. It is one of the most common "new programmer" errors.
Prevention: Always use indicator variables for columns defined as nullable in DB2. When in doubt, use an indicator variable -- having an unnecessary indicator variable causes no harm, but missing one causes a runtime error.
SQLCODE -501: Cursor Not Open
This error occurs when a FETCH is attempted on a cursor that is not open, or that has been closed:
* Common mistake: Forgetting to OPEN the cursor
EXEC SQL
FETCH CUST-CURSOR
INTO :HV-CUST-ID, :HV-CUST-NAME
END-EXEC
* SQLCODE = -501 because OPEN was never issued
* Another common mistake: FETCH after cursor was closed
* by a COMMIT (cursor was not declared WITH HOLD)
Prevention: Always pair DECLARE/OPEN/FETCH/CLOSE. Use WITH HOLD if the cursor must survive a COMMIT.
SQLCODE -530 and -532: Referential Integrity Violations
These errors occur when an INSERT or UPDATE would violate a foreign key constraint (-530) or when a DELETE would leave orphan rows in a child table (-532):
EVALUATE SQLCODE
WHEN -530
DISPLAY 'PARENT ROW DOES NOT EXIST'
DISPLAY 'CHECK FOREIGN KEY VALUE: '
HV-PARENT-KEY
WHEN -532
DISPLAY 'CHILD ROWS EXIST'
DISPLAY 'DELETE CHILD ROWS FIRST'
END-EVALUATE
Performance-Related SQLCODEs
| SQLCODE | Description | Action |
|---|---|---|
| +100 | No row found, or end of cursor | Normal condition, handle in logic |
| +802 | Data exception during arithmetic | Check for overflow in SQL expressions |
| -904 | Unavailable resource | Retry later, or contact DBA |
| -911 | Deadlock, rollback occurred | Retry the transaction |
| -913 | Timeout, no rollback | Rollback and retry |
22.13 Best Practices for COBOL-DB2 Programs
Program Structure for DB2 Batch
A well-structured DB2 batch program follows this pattern:
0000-MAIN.
PERFORM 1000-INITIALIZE
PERFORM 2000-PROCESS UNTIL FL-EOF
PERFORM 3000-TERMINATE
STOP RUN
.
1000-INITIALIZE.
* Open files
* Initialize counters
* Read first input record (if file-driven)
* Check for restart from checkpoint table
.
2000-PROCESS.
* Read/validate input
* Perform DB2 operations
* Check commit threshold
* Read next input record
.
3000-TERMINATE.
* Final COMMIT
* Close files
* Display summary statistics
* Set RETURN-CODE
.
Error Recovery Strategy
Every DB2 batch program should have a clear error recovery strategy documented in the program header:
*========================================================*
* ERROR RECOVERY STRATEGY: *
* - SQL errors: Log error, skip record, continue *
* - Deadlocks: Retry 3 times with 2-second delay *
* - Timeouts: Same as deadlock *
* - Max errors: 1000 (configurable) *
* - Restart: Via CHECKPOINT_TABLE, key-based restart *
* - File errors: Abort with RC=16 *
*========================================================*
Separation of SQL and Business Logic
Keep SQL operations in dedicated paragraphs, separate from business logic. This makes the program easier to test, debug, and modify:
* GOOD: SQL isolated in dedicated paragraphs
2200-PROCESS-TRANSACTION.
PERFORM 2210-READ-CUSTOMER
IF WS-CUSTOMER-FOUND
PERFORM 2220-CALCULATE-INTEREST
PERFORM 2230-UPDATE-CUSTOMER
END-IF
.
2210-READ-CUSTOMER.
EXEC SQL
SELECT CUST_NAME, CUST_BALANCE
INTO :HV-CUST-NAME, :HV-CUST-BALANCE
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
EVALUATE SQLCODE
WHEN 0 SET WS-CUSTOMER-FOUND TO TRUE
WHEN +100 SET WS-CUSTOMER-NOT-FOUND TO TRUE
WHEN OTHER PERFORM 9100-SQL-ERROR
END-EVALUATE
.
2220-CALCULATE-INTEREST.
* Pure COBOL business logic -- no SQL here
COMPUTE WS-INTEREST =
HV-CUST-BALANCE * WS-ANNUAL-RATE / 12
ON SIZE ERROR
DISPLAY 'INTEREST CALC OVERFLOW'
MOVE 0 TO WS-INTEREST
END-COMPUTE
.
2230-UPDATE-CUSTOMER.
COMPUTE HV-NEW-BALANCE =
HV-CUST-BALANCE + WS-INTEREST
EXEC SQL
UPDATE CUSTOMER
SET CUST_BALANCE = :HV-NEW-BALANCE,
LAST_UPDATE = CURRENT TIMESTAMP
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
EVALUATE SQLCODE
WHEN 0 ADD 1 TO CT-UPDATED
WHEN OTHER PERFORM 9100-SQL-ERROR
END-EVALUATE
.
22.14 GnuCOBOL ODBC Alternative
GnuCOBOL does not support embedded SQL with DB2 directly, but it can access relational databases through ODBC (Open Database Connectivity) using the CALL interface or through the ESQL preprocessor (ocesql) that supports a subset of embedded SQL syntax.
Using ocesql with GnuCOBOL
The ocesql preprocessor provides embedded SQL support for GnuCOBOL with ODBC-compliant databases (PostgreSQL, MySQL, SQLite, etc.):
IDENTIFICATION DIVISION.
PROGRAM-ID. GNUDB.
DATA DIVISION.
WORKING-STORAGE SECTION.
EXEC SQL BEGIN DECLARE SECTION END-EXEC.
01 HV-CUST-ID PIC 9(9).
01 HV-CUST-NAME PIC X(30).
01 HV-CUST-BALANCE PIC S9(7)V99.
01 DBNAME PIC X(30)
VALUE 'mydb'.
01 DBUSER PIC X(30)
VALUE 'appuser'.
01 DBPASSWD PIC X(30)
VALUE 'password'.
EXEC SQL END DECLARE SECTION END-EXEC.
EXEC SQL INCLUDE SQLCA END-EXEC.
PROCEDURE DIVISION.
0000-MAIN.
EXEC SQL
CONNECT :DBUSER
IDENTIFIED BY :DBPASSWD
USING :DBNAME
END-EXEC
IF SQLCODE NOT = 0
DISPLAY 'CONNECTION FAILED: ' SQLCODE
STOP RUN
END-IF
MOVE 1001 TO HV-CUST-ID
EXEC SQL
SELECT CUST_NAME, CUST_BALANCE
INTO :HV-CUST-NAME, :HV-CUST-BALANCE
FROM CUSTOMER
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
IF SQLCODE = 0
DISPLAY 'NAME: ' HV-CUST-NAME
DISPLAY 'BALANCE: ' HV-CUST-BALANCE
END-IF
EXEC SQL COMMIT WORK END-EXEC
EXEC SQL DISCONNECT ALL END-EXEC
STOP RUN
.
Compiling with ocesql:
# Preprocess the COBOL source
ocesql program.cob program.cbl
# Compile the preprocessed source
cobc -x program.cbl -lodbc
22.13 Complete COBOL-DB2 Program Example
The following program demonstrates a complete COBOL-DB2 batch program that reads a transaction file and updates customer balances in a DB2 table:
IDENTIFICATION DIVISION.
PROGRAM-ID. CUSTUPDT.
*========================================================*
* PROGRAM: CUSTUPDT *
* PURPOSE: Update customer balances from transaction file *
* INPUT: TRANFILE - Transaction records (sequential) *
* DB2: CUSTOMER table (read/update) *
* OUTPUT: ERRFILE - Rejected transactions *
*========================================================*
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT TRANSACTION-FILE
ASSIGN TO TRANFILE
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-TRAN-STATUS.
SELECT ERROR-FILE
ASSIGN TO ERRFILE
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-ERR-STATUS.
DATA DIVISION.
FILE SECTION.
FD TRANSACTION-FILE
RECORDING MODE IS F
RECORD CONTAINS 80 CHARACTERS.
01 TRAN-RECORD.
05 TRAN-CUST-ID PIC 9(9).
05 TRAN-AMOUNT PIC S9(7)V99.
05 TRAN-TYPE PIC X.
05 TRAN-DATE PIC X(10).
05 FILLER PIC X(51).
FD ERROR-FILE
RECORDING MODE IS F
RECORD CONTAINS 130 CHARACTERS.
01 ERROR-RECORD PIC X(130).
WORKING-STORAGE SECTION.
01 WS-FILE-STATUSES.
05 WS-TRAN-STATUS PIC XX.
05 WS-ERR-STATUS PIC XX.
01 FL-FLAGS.
05 FL-EOF-FLAG PIC 9 VALUE 0.
88 FL-NOT-EOF VALUE 0.
88 FL-EOF VALUE 1.
01 CT-COUNTERS.
05 CT-READ PIC 9(7) COMP VALUE 0.
05 CT-UPDATED PIC 9(7) COMP VALUE 0.
05 CT-NOT-FOUND PIC 9(7) COMP VALUE 0.
05 CT-ERRORS PIC 9(7) COMP VALUE 0.
05 CT-COMMITS PIC 9(7) COMP VALUE 0.
01 WS-COMMIT-FREQ PIC 9(5) COMP VALUE 500.
01 WS-SINCE-COMMIT PIC 9(5) COMP VALUE 0.
01 WS-CURRENT-PARAGRAPH PIC X(30).
01 WS-ERROR-RECORD.
05 WER-CUST-ID PIC 9(9).
05 FILLER PIC X VALUE ' '.
05 WER-AMOUNT PIC S9(7)V99.
05 FILLER PIC X VALUE ' '.
05 WER-ERROR-MSG PIC X(50).
05 FILLER PIC X(60).
EXEC SQL INCLUDE SQLCA END-EXEC.
EXEC SQL BEGIN DECLARE SECTION END-EXEC.
01 HV-CUST-ID PIC S9(9) COMP.
01 HV-CUST-NAME PIC X(30).
01 HV-CUST-BALANCE PIC S9(7)V99 COMP-3.
01 HV-TRANS-AMOUNT PIC S9(7)V99 COMP-3.
EXEC SQL END DECLARE SECTION END-EXEC.
PROCEDURE DIVISION.
0000-MAIN.
PERFORM 1000-INITIALIZE
PERFORM 2000-PROCESS
UNTIL FL-EOF
PERFORM 3000-TERMINATE
STOP RUN
.
1000-INITIALIZE.
MOVE '1000-INITIALIZE' TO WS-CURRENT-PARAGRAPH
OPEN INPUT TRANSACTION-FILE
IF WS-TRAN-STATUS NOT = '00'
DISPLAY 'CANNOT OPEN TRAN FILE: '
WS-TRAN-STATUS
MOVE 16 TO RETURN-CODE
STOP RUN
END-IF
OPEN OUTPUT ERROR-FILE
IF WS-ERR-STATUS NOT = '00'
DISPLAY 'CANNOT OPEN ERROR FILE: '
WS-ERR-STATUS
MOVE 16 TO RETURN-CODE
STOP RUN
END-IF
PERFORM 2100-READ-TRANSACTION
.
2000-PROCESS.
MOVE '2000-PROCESS' TO WS-CURRENT-PARAGRAPH
MOVE TRAN-CUST-ID TO HV-CUST-ID
MOVE TRAN-AMOUNT TO HV-TRANS-AMOUNT
EXEC SQL
UPDATE CUSTOMER
SET CUST_BALANCE = CUST_BALANCE
+ :HV-TRANS-AMOUNT,
LAST_UPDATE = CURRENT TIMESTAMP
WHERE CUST_ID = :HV-CUST-ID
END-EXEC
EVALUATE SQLCODE
WHEN 0
IF SQLERRD(3) > 0
ADD 1 TO CT-UPDATED
ADD 1 TO WS-SINCE-COMMIT
IF WS-SINCE-COMMIT >= WS-COMMIT-FREQ
PERFORM 2500-COMMIT
END-IF
ELSE
MOVE TRAN-CUST-ID TO WER-CUST-ID
MOVE TRAN-AMOUNT TO WER-AMOUNT
MOVE 'CUSTOMER NOT FOUND (NO ROWS)'
TO WER-ERROR-MSG
PERFORM 2900-WRITE-ERROR
ADD 1 TO CT-NOT-FOUND
END-IF
WHEN OTHER
MOVE TRAN-CUST-ID TO WER-CUST-ID
MOVE TRAN-AMOUNT TO WER-AMOUNT
STRING 'SQL ERROR: ' SQLCODE
DELIMITED BY SIZE
INTO WER-ERROR-MSG
END-STRING
PERFORM 2900-WRITE-ERROR
ADD 1 TO CT-ERRORS
END-EVALUATE
PERFORM 2100-READ-TRANSACTION
.
2100-READ-TRANSACTION.
READ TRANSACTION-FILE
AT END
SET FL-EOF TO TRUE
NOT AT END
ADD 1 TO CT-READ
END-READ
.
2500-COMMIT.
EXEC SQL COMMIT END-EXEC
IF SQLCODE NOT = 0
DISPLAY 'COMMIT ERROR: ' SQLCODE
ELSE
ADD 1 TO CT-COMMITS
MOVE 0 TO WS-SINCE-COMMIT
END-IF
.
2900-WRITE-ERROR.
WRITE ERROR-RECORD FROM WS-ERROR-RECORD
INITIALIZE WS-ERROR-RECORD
.
3000-TERMINATE.
PERFORM 2500-COMMIT
CLOSE TRANSACTION-FILE
CLOSE ERROR-FILE
DISPLAY '=============================='
DISPLAY 'CUSTUPDT PROCESSING SUMMARY'
DISPLAY '=============================='
DISPLAY 'TRANSACTIONS READ: ' CT-READ
DISPLAY 'CUSTOMERS UPDATED: ' CT-UPDATED
DISPLAY 'NOT FOUND: ' CT-NOT-FOUND
DISPLAY 'SQL ERRORS: ' CT-ERRORS
DISPLAY 'COMMITS: ' CT-COMMITS
DISPLAY '=============================='
EVALUATE TRUE
WHEN CT-ERRORS > 0
MOVE 8 TO RETURN-CODE
WHEN CT-NOT-FOUND > 0
MOVE 4 TO RETURN-CODE
WHEN OTHER
MOVE 0 TO RETURN-CODE
END-EVALUATE
.
9100-SQL-ERROR.
DISPLAY '*** SQL ERROR ***'
DISPLAY 'SQLCODE: ' SQLCODE
DISPLAY 'SQLSTATE: ' SQLSTATE
DISPLAY 'PARAGRAPH: ' WS-CURRENT-PARAGRAPH
MOVE 16 TO RETURN-CODE
EXEC SQL ROLLBACK END-EXEC
CLOSE TRANSACTION-FILE
CLOSE ERROR-FILE
STOP RUN
.
Summary
This chapter covered the fundamentals of COBOL-DB2 programming:
- DB2 is IBM's relational database for z/OS, providing centralized data management with SQL as the data access language.
- EXEC SQL...END-EXEC delimiters wrap all SQL statements embedded in COBOL source code.
- Host variables are COBOL data items that exchange data with DB2, prefixed with a colon (:) inside SQL statements and declared with compatible COBOL data types. DCLGEN automates host variable generation.
- The SQLCA (SQL Communication Area) provides the SQLCODE return code after every SQL operation: 0 for success, +100 for not found, and negative values for errors.
- SELECT INTO retrieves a single row into host variables and should always be followed by SQLCODE checking.
- INSERT, UPDATE, and DELETE modify DB2 data. Check SQLERRD(3) to determine how many rows were affected.
- Cursors (DECLARE, OPEN, FETCH, CLOSE) provide sequential access to multi-row result sets. WITH HOLD keeps cursors open across commits. FOR UPDATE enables positioned updates and deletes.
- Indicator variables (PIC S9(4) COMP) handle NULL values. A value of -1 indicates NULL; 0 indicates a valid value. Always use indicator variables for nullable columns.
- VARCHAR host variables use a special 49-level structure with a length field and a text field.
- WHENEVER provides declarative error handling but is less flexible than explicit SQLCODE checking.
- The precompile/bind process extracts SQL from COBOL, produces a DBRM, and binds it into a package that DB2 uses to determine access paths.
- GnuCOBOL can access databases through ODBC using the ocesql preprocessor.
Embedded SQL transforms COBOL from a file-processing language into a database application language. The combination of COBOL's data handling strengths with DB2's relational capabilities forms the backbone of enterprise data processing on IBM mainframes.
Advanced Topic: Multi-Table Queries
While the cursor examples in this chapter used single-table queries, production programs frequently join multiple tables. Here is how multi-table operations work with COBOL host variables:
Inner Join with Host Variables
EXEC SQL
DECLARE ORDER-CURSOR CURSOR FOR
SELECT C.CUST_ID,
C.CUST_NAME,
O.ORDER_ID,
O.ORDER_DATE,
O.ORDER_AMOUNT
FROM CUSTOMER C
INNER JOIN ORDERS O
ON C.CUST_ID = O.CUST_ID
WHERE O.ORDER_DATE >= :HV-START-DATE
AND O.ORDER_DATE <= :HV-END-DATE
AND C.CUST_STATUS = 'A'
ORDER BY C.CUST_NAME, O.ORDER_DATE
END-EXEC
The cursor is fetched in exactly the same way as a single-table cursor. The host variables in the INTO clause correspond to the columns in the SELECT clause regardless of which table they come from:
2100-FETCH-ORDER.
EXEC SQL
FETCH ORDER-CURSOR
INTO :HV-CUST-ID,
:HV-CUST-NAME,
:HV-ORDER-ID,
:HV-ORDER-DATE,
:HV-ORDER-AMOUNT
END-EXEC
.
Subqueries
Subqueries in WHERE clauses work transparently with host variables:
* Find customers whose balance exceeds the average
EXEC SQL
DECLARE HIGH-BAL-CURSOR CURSOR FOR
SELECT CUST_ID, CUST_NAME, CUST_BALANCE
FROM CUSTOMER
WHERE CUST_BALANCE >
(SELECT AVG(CUST_BALANCE)
FROM CUSTOMER
WHERE CUST_STATUS = 'A')
AND CUST_STATUS = 'A'
ORDER BY CUST_BALANCE DESC
END-EXEC
Correlated Subqueries
Correlated subqueries reference the outer query and execute once for each row of the outer query:
* Find customers with no orders in the last 30 days
EXEC SQL
DECLARE INACTIVE-CURSOR CURSOR FOR
SELECT CUST_ID, CUST_NAME
FROM CUSTOMER C
WHERE NOT EXISTS
(SELECT 1
FROM ORDERS O
WHERE O.CUST_ID = C.CUST_ID
AND O.ORDER_DATE >=
CURRENT DATE - 30 DAYS)
AND C.CUST_STATUS = 'A'
ORDER BY C.CUST_NAME
END-EXEC
UNION Queries
When you need to combine results from multiple queries, UNION ALL combines them into a single result set:
EXEC SQL
DECLARE ACTIVITY-CURSOR CURSOR FOR
SELECT CUST_ID,
'DEPOSIT' AS ACTIVITY_TYPE,
DEPOSIT_DATE AS ACTIVITY_DATE,
DEPOSIT_AMOUNT AS AMOUNT
FROM DEPOSITS
WHERE DEPOSIT_DATE >= :HV-START-DATE
UNION ALL
SELECT CUST_ID,
'WITHDRAWAL',
WITHDRAWAL_DATE,
WITHDRAWAL_AMOUNT
FROM WITHDRAWALS
WHERE WITHDRAWAL_DATE >= :HV-START-DATE
ORDER BY CUST_ID, ACTIVITY_DATE
END-EXEC
Exercises
-
Basic SELECT INTO: Write a COBOL-DB2 program that accepts a customer ID as input (from SYSIN or a parameter), retrieves the customer's name and balance from a CUSTOMER table, and displays the results. Handle SQLCODE 0, +100, and negative values.
-
Cursor Processing: Write a program that uses a cursor to read all customers with a balance greater than a specified threshold, displays each customer's information, and at the end displays the total number of customers and the sum of their balances.
-
INSERT with Duplicate Handling: Write a program that reads a sequential file of new customer records and inserts them into a CUSTOMER table. Handle SQLCODE -803 (duplicate key) by logging the duplicate and continuing with the next record.
-
NULL Handling: Write a program that reads customer records from DB2 where the PHONE_NUMBER column is nullable. Use indicator variables to detect NULLs and display "NO PHONE" for customers without phone numbers.
-
Cursor with UPDATE: Write a program that uses a FOR UPDATE cursor to read all customers with status 'I' (inactive) and updates their status to 'C' (closed) if their balance is zero. Commit every 100 updates using a WITH HOLD cursor.