Sequential Files

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 ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot), in which sequential files come from the original language and LINE SEQUENTIAL organization was standardized in 2023; against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026) on z/OS 3.2, where a sequential file is normally a QSAM data set and LINE SEQUENTIAL is available only for z/OS UNIX files; and against GnuCOBOL 3.2 (28 July 2023, the latest release), with which every program on it was compiled with cobc -x -Wall and run on 2026-10-20 (and again with -std=ibm, which printed the same results unless the text says otherwise). z/OS examples are labelled "z/OS; not run here" and are checked against the Language Reference and the Programming Guide.

A sequential file is a file whose records are processed in the order in which they were written. This page covers the statements that work on it (OPEN, READ, WRITE, REWRITE, CLOSE), the difference between a record sequential file and a LINE SEQUENTIAL file, fixed-length, variable-length and multi-record files, and the algorithm that almost every batch system is built around: the balance-line update of an old master file with a file of sorted transactions. The general SELECT and FD rules and the mapping of an assignment name to a file are on File Concepts, SELECT and FD; the other organizations are on Indexed Files and VSAM and Relative Files; every status code mentioned here is in the table on File Status Codes; putting the transactions in key order is the job of Sort and Merge. The examples use the Circulation public-library scenario: the book master BOOKMAST is updated with the day’s loan transactions LOANTRAN.

Where the book is outdated: Taulli’s Modern Mainframe Development (2022, chapter 5) introduces SELECT, FD, sequential READ and WRITE, FILE STATUS and a master-file update, and the ideas are still current. Four details need correcting. A file is opened with OPEN INPUT, not "OPEN READ" (the compiler rejects the second form, see below). The FILE STATUS item is a separate two-character item in WORKING-STORAGE, not a field declared inside the record group, where it would become part of the data read from the file. A PC-style ASSIGN TO "Customers.dat" together with LINE SEQUENTIAL organization mixes two worlds: on z/OS batch the assignment name is a DD name and the file is a QSAM data set, and Enterprise COBOL supports LINE SEQUENTIAL only for z/OS UNIX files. And the book’s master-file update is described as a narrative; the complete, running balance-line version is below. Also note that the book’s examples run on GnuCOBOL through OpenCobolIDE, an unmaintained IDE (see Getting Started).

The sequential file model

A sequential file is a stream of records. There is no key and no slot number: the only operations that find a record are "give me the next one" and, after reading it, "replace this one". Three consequences follow, and they shape every program that uses such a file:

  • A file is read from the first record to the last, and a program that needs a record a second time closes and reopens the file, or keeps the record in storage.

  • A record cannot be inserted, deleted or lengthened in place. To change the set of records, a program writes a new file: copy what stays, change what changes, leave out what goes. The balance-line update below is that pattern.

  • REWRITE can replace the record just read, but only with a record of the same length.

The organization is chosen in the SELECT entry. ORGANIZATION IS SEQUENTIAL (the default when the clause is omitted) is a record sequential file: the records are binary-safe, any byte value is allowed, and the record length comes from the FD. ORGANIZATION IS LINE SEQUENTIAL is a text file: each record is one line ended by a line terminator, so only printable data belongs in it.

SEQUENTIAL (record sequential) LINE SEQUENTIAL

Record end

Known from the FD (fixed) or from a length prefix or a record descriptor word (variable)

A line terminator: a line feed on Unix-like systems, carriage return plus line feed on Windows

Data allowed

Any byte value, including packed and binary items

Text; the terminator must not occur in a record

Short record on WRITE

Fixed: the full record area is written, trailing spaces included

Trailing spaces are removed, so the line is as short as its data

Short line on READ

Not applicable

The record area is filled with spaces on the right

Long line on READ

Not applicable

The excess is returned as the next record, with status 06 in GnuCOBOL 3.2

Standard

COBOL 85 and earlier

GnuCOBOL extension that COBOL 2023 standardized

Enterprise COBOL 6.5

QSAM, VSAM ESDS, and z/OS UNIX files

z/OS UNIX files only

The Circulation LOANTRAN file is a LINE SEQUENTIAL file on the PC, which makes it easy to inspect with any editor. On z/OS the same data would be a fixed-block QSAM data set USERID.CIRC.LOANTRAN with 30-byte records and the same FD. The program text does not change when the file moves between the two, only the environment (the DD statement, the RECFM and LRECL) does.

OPEN, CLOSE and the four modes

OPEN names the mode, then the file names; one statement can open several files, each with its own mode:

OPEN {INPUT | OUTPUT | I-O | EXTEND} file-name-1 [file-name-2] ...
Mode What it does Statements allowed

INPUT

The file must exist. Reading starts at the first record.

READ

OUTPUT

Creates the file, or empties an existing one. Writing starts at the first record. Reading the old content afterwards is impossible: the content is gone.

WRITE

EXTEND

Positions after the last record; the old records stay.

WRITE

I-O

The file must exist. Records are read, and the one just read can be replaced.

READ, REWRITE

CLOSE ends the processing of a file and is the point where buffered output is written. A file that was opened must be closed before it is opened again, and a program that ends with files open has them closed by the run time, which GnuCOBOL reports as a warning (libcob: warning: implicit CLOSE of REJECT-FILE) and Enterprise COBOL handles through Language Environment. Code the CLOSE yourself.

The next program shows all four modes on one file. A fixed-length record sequential file LOANLOG is created with two records, extended with a third, opened I-O to replace the date of the second record, and read back; a second file LOANTXT shows the LINE SEQUENTIAL form of the first two records. WRITE ... FROM moves the working-storage record into the file’s record area and writes it in one statement, and READ ... INTO does the reverse.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SEQDEMO.
      * OPEN OUTPUT, EXTEND, INPUT and I-O on one fixed-length file.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT LOAN-LOG ASSIGN TO "LOANLOG"
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-STATUS.
           SELECT LOAN-TEXT ASSIGN TO "LOANTXT"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS WS-STATUS.
       DATA DIVISION.
       FILE SECTION.
       FD  LOAN-LOG.
       01  LOG-RECORD          PIC X(30).
       FD  LOAN-TEXT.
       01  TEXT-RECORD         PIC X(30).
       WORKING-STORAGE SECTION.
       COPY LOANREC.
       01  WS-STATUS           PIC XX.
       01  WS-COUNT            PIC 9(3) VALUE ZERO.
       01  WS-EOF              PIC X VALUE "N".
           88  END-OF-LOG      VALUE "Y".
       PROCEDURE DIVISION.
       0000-MAIN-LINE.
      * 1. OUTPUT creates the file (or empties an existing one).
           OPEN OUTPUT LOAN-LOG LOAN-TEXT
           MOVE "O" TO LOAN-TYPE
           MOVE 100 TO LOAN-MEMBER-ID
           MOVE 1 TO LOAN-BOOK-ID
           MOVE 20261020 TO LOAN-DATE
           WRITE LOG-RECORD FROM LOAN-RECORD
           WRITE TEXT-RECORD FROM LOAN-RECORD
           MOVE "R" TO LOAN-TYPE
           WRITE LOG-RECORD FROM LOAN-RECORD
           WRITE TEXT-RECORD FROM LOAN-RECORD
           CLOSE LOAN-LOG LOAN-TEXT
      * 2. EXTEND adds after the last record; the old ones stay.
           OPEN EXTEND LOAN-LOG
           MOVE "O" TO LOAN-TYPE
           MOVE 102 TO LOAN-MEMBER-ID
           MOVE 3 TO LOAN-BOOK-ID
           WRITE LOG-RECORD FROM LOAN-RECORD
           CLOSE LOAN-LOG
      * 3. I-O: READ a record, then REWRITE that same record.
           OPEN I-O LOAN-LOG
           READ LOAN-LOG INTO LOAN-RECORD
           READ LOAN-LOG INTO LOAN-RECORD
           MOVE 20261021 TO LOAN-DATE
           REWRITE LOG-RECORD FROM LOAN-RECORD
           DISPLAY "REWRITE status " WS-STATUS
           CLOSE LOAN-LOG
      * 4. INPUT: read everything back.
           OPEN INPUT LOAN-LOG
           PERFORM UNTIL END-OF-LOG
               READ LOAN-LOG INTO LOAN-RECORD
                   AT END SET END-OF-LOG TO TRUE
                   NOT AT END
                       ADD 1 TO WS-COUNT
                       DISPLAY WS-COUNT " " LOAN-TYPE " "
                           LOAN-MEMBER-ID " " LOAN-BOOK-ID " "
                           LOAN-DATE
               END-READ
           END-PERFORM
           CLOSE LOAN-LOG
           DISPLAY "records read: " WS-COUNT
           STOP RUN.

Compiled with cobc -x -Wall -I. seqdemo.cob (and with -std=ibm), no warnings, it prints:

REWRITE status 00
001 O 000100 000001 20261020
002 R 000100 000001 20261021
003 O 000102 000003 20261020
records read: 003

The files differ in size. LOANLOG is 90 bytes, three records of exactly 30 bytes with the trailing spaces of the FILLER written out; LOANTXT is 44 bytes, two lines of 21 data characters plus the line feed, because a LINE SEQUENTIAL WRITE drops trailing spaces:

$ ls -l LOANLOG LOANTXT
-rw-r--r--  1 user  group  90 Oct 20 18:26 LOANLOG
-rw-r--r--  1 user  group  44 Oct 20 18:26 LOANTXT

The OPEN READ mistake and its diagnostic

The book’s "OPEN READ" is not a COBOL statement. GnuCOBOL 3.2.0 rejects it at compile time and names the four valid modes:

openread.cob:13: error: syntax error, unexpected READ, expecting EXTEND or INPUT or I-O or OUTPUT

READ

A sequential READ returns the next record:

READ file-name [NEXT] RECORD [INTO identifier]
    [AT END imperative-statement-1]
    [NOT AT END imperative-statement-2]
[END-READ]
  • The NEXT word is optional for sequential files (it matters on indexed and relative files with dynamic access).

  • AT END runs when there is no next record, and the file status is 10. A READ that has neither an AT END phrase nor a FILE STATUS item nor an error declarative ends the run unit at the end of the file, so every sequential read loop needs one of them; the FILE STATUS item is the one you test in any case. NOT AT END runs only after a successful read, which is the right place for the work that uses the record.

  • INTO moves the record from the record area to a working-storage item after a successful read. It is a MOVE governed by the usual rules. For a variable-length file it moves only as many bytes as the record has, with the usual padding of the receiving item.

  • After AT END the content of the record area is undefined. A program that processes "one more record" after the loop ended works on garbage or on the last record, depending on the system. The balance-line program below solves this by storing a high key value when the file ends.

  • If the file has no records, the first READ gives status 10.

  • After a read that fails with status 30 or any other error the position is undefined; do not read on.

The usual loop is a priming read followed by a loop that processes and then reads again, or an inline loop with NOT AT END. The first form has one READ statement and needs no flag tests inside the loop body:

PERFORM 8000-READ-LOAN
PERFORM UNTIL END-OF-LOANS
    PERFORM 2000-PROCESS-LOAN
    PERFORM 8000-READ-LOAN
END-PERFORM

WRITE

WRITE record-name [FROM identifier]
    [{BEFORE | AFTER} ADVANCING {integer | identifier} {LINE | LINES}
                               | {mnemonic-name | PAGE}]
    [AT {END-OF-PAGE | EOP} imperative-statement-1]
    [NOT AT {END-OF-PAGE | EOP} imperative-statement-2]
[END-WRITE]
  • WRITE names the record, not the file. It puts the record area on the file; with FROM, the source item is moved into the record area first and stays unchanged. WRITE rec FROM item is therefore the same as MOVE item TO rec followed by WRITE rec, except that the program does not touch the record area.

  • After a successful WRITE the content of the record area is not guaranteed to be available to the program on every system (the standard leaves it undefined). Do not rely on it holding the data; use the FROM item.

  • The ADVANCING phrase controls the line spacing of a print file and is covered with the rest of report printing on Reports and Report Writer.

  • A file opened OUTPUT or EXTEND accepts WRITE; a WRITE on a file opened INPUT raises status 48. A WRITE on a full device or a data set without space raises a permanent error (status 34 on Enterprise COBOL when the extent is exhausted).

  • INVALID KEY is not allowed on a sequential WRITE; the error conditions are reported through the file status.

REWRITE

REWRITE record-name [FROM identifier]
  • The file must be open I-O, and the last statement on the file must have been a successful READ. The record replaced is the one just read.

  • On z/OS the new record must have the same length as the old one for a sequential file. Rewriting a different length is an error: the record is not replaced and the status is 44 (boundary violation).

  • In GnuCOBOL 3.2 REWRITE works on LINE SEQUENTIAL files as well (a 3.2 addition); the replacement has to keep the line length. The next program read and rewrote the first line of LOANTRAN with status 00.

  • REWRITE does not move the file position; the next READ returns the record after the one replaced.

  • In-place update is cheap and has a cost: after an abend in the middle of the pass, the file is half updated and the original is lost. Update by creating a new file (next section) when that matters, which in batch work is nearly always.

Fixed-length and variable-length records

A file whose FD has a single 01 item, or several 01 items of the same size, has fixed-length records. Variable-length records have two forms in the program:

  • Several 01 items of different lengths under one FD, or RECORD CONTAINS 10 TO 80 CHARACTERS: the record length is the length of the item written, and on input the length of the record read is the length that was stored.

  • RECORD IS VARYING IN SIZE FROM min TO max CHARACTERS DEPENDING ON data-name: the length is explicit. Before a WRITE the program moves the length to the DEPENDING ON item; after a READ the item holds the length of the record read. The item must be an integer in WORKING-STORAGE and must not be a field of the record.

On z/OS the record format is a property of the data set (RECFM=V, VB, F, FB, U, plus A or M for carriage control), set by the DCB parameters of the DD statement or by the program’s RECORDING MODE and BLOCK CONTAINS clauses. A variable-length record starts with a four-byte record descriptor word (RDW), and a block with a block descriptor word (BDW). Enterprise COBOL takes the record format from the FD when the RECORDING MODE clause is given and otherwise infers it from the record descriptions. BLOCK CONTAINS 0 RECORDS lets the system choose the best block size from the data set, which is the practice on z/OS. GnuCOBOL stores a variable-length record sequential record with its own four-byte length prefix, not an RDW.

The next program writes the Circulation titles as variable-length records of 6 + title length bytes and reads them back. The key technique is the COMPUTE: the length of the trimmed title plus the six-digit book number.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. VARDEMO.
      * Variable-length records: book id plus the title as long as it is.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT BOOK-IN ASSIGN TO "BOOKMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT TITLE-FILE ASSIGN TO "TITLES"
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-STATUS.
       DATA DIVISION.
       FILE SECTION.
       FD  BOOK-IN.
       01  BOOK-IN-LINE        PIC X(60).
       FD  TITLE-FILE
           RECORD IS VARYING IN SIZE FROM 7 TO 36 CHARACTERS
               DEPENDING ON WS-LENGTH.
       01  TITLE-RECORD.
           05  TITLE-ID        PIC 9(6).
           05  TITLE-TEXT      PIC X(30).
       WORKING-STORAGE SECTION.
       COPY BOOKREC.
       01  WS-STATUS           PIC XX.
       01  WS-LENGTH           PIC 9(3).
       01  WS-EOF              PIC X VALUE "N".
           88  END-OF-FILE     VALUE "Y".
       PROCEDURE DIVISION.
       0000-MAIN-LINE.
           OPEN INPUT BOOK-IN
           OPEN OUTPUT TITLE-FILE
           PERFORM UNTIL END-OF-FILE
               READ BOOK-IN INTO BOOK-RECORD
                   AT END SET END-OF-FILE TO TRUE
                   NOT AT END
                       MOVE BOOK-ID TO TITLE-ID
                       MOVE BOOK-TITLE TO TITLE-TEXT
                       COMPUTE WS-LENGTH = 6 + FUNCTION LENGTH(
                           FUNCTION TRIM(BOOK-TITLE TRAILING))
                       WRITE TITLE-RECORD
                       DISPLAY "wrote " TITLE-ID " length " WS-LENGTH
               END-READ
           END-PERFORM
           CLOSE BOOK-IN TITLE-FILE
           MOVE "N" TO WS-EOF
           OPEN INPUT TITLE-FILE
           PERFORM UNTIL END-OF-FILE
               READ TITLE-FILE
                   AT END SET END-OF-FILE TO TRUE
                   NOT AT END
                       DISPLAY "read  " TITLE-ID " length " WS-LENGTH
                           " [" TITLE-TEXT(1:WS-LENGTH - 6) "]"
               END-READ
           END-PERFORM
           CLOSE TITLE-FILE
           STOP RUN.

Compiled with cobc -x -Wall -I. vardemo.cob (and with -std=ibm), no warnings, it prints:

wrote 000001 length 021
wrote 000002 length 018
wrote 000003 length 022
wrote 000004 length 017
read  000001 length 021 [DATA STRUCTURES]
read  000002 length 018 [COBOL BASICS]
read  000003 length 022 [BATCH PROCESSING]
read  000004 length 017 [OLD LEDGERS]

The file TITLES is 94 bytes: the four records add up to 78 bytes, and the other 16 bytes are the four-byte length prefix that GnuCOBOL puts in front of each record. A fixed-length file of the same four titles would have taken 4 x 36 = 144 bytes. On z/OS the same program would produce RECFM=V or VB records, each with its four-byte RDW.

Files with several record types

A file can mix record layouts, typically a header, many details and a trailer with a count, and the first byte or two says which layout a record has. The FD then has one 01 item per layout; all of them share the same storage, the record area, so a program decides which one to look at after each READ. The next program copies the loan transactions to LOANTRL and adds a trailer record T with the number of details, then reads the file back and reacts to the first byte.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. MULTIREC.
      * One file, two record layouts: details and a trailer.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT TRAN-IN ASSIGN TO "LOANTRAN"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT TRAN-OUT ASSIGN TO "LOANTRL"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  TRAN-IN.
       01  TRAN-IN-LINE        PIC X(30).
       FD  TRAN-OUT.
       01  DETAIL-RECORD       PIC X(30).
       01  TRAILER-RECORD.
           05  TRAILER-TYPE    PIC X.
           05  TRAILER-COUNT   PIC 9(6).
           05  FILLER          PIC X(23).
       WORKING-STORAGE SECTION.
       COPY LOANREC.
       01  WS-EOF              PIC X VALUE "N".
           88  END-OF-FILE     VALUE "Y".
       01  WS-COUNT            PIC 9(6) VALUE ZERO.
       PROCEDURE DIVISION.
       0000-MAIN-LINE.
           OPEN INPUT TRAN-IN
           OPEN OUTPUT TRAN-OUT
           PERFORM UNTIL END-OF-FILE
               READ TRAN-IN
                   AT END SET END-OF-FILE TO TRUE
                   NOT AT END
                       WRITE DETAIL-RECORD FROM TRAN-IN-LINE
                       ADD 1 TO WS-COUNT
               END-READ
           END-PERFORM
           MOVE SPACES TO TRAILER-RECORD
           MOVE "T" TO TRAILER-TYPE
           MOVE WS-COUNT TO TRAILER-COUNT
           WRITE TRAILER-RECORD
           CLOSE TRAN-IN TRAN-OUT
      * Read it back: the first byte says which layout the line has.
           MOVE "N" TO WS-EOF
           OPEN INPUT TRAN-OUT
           PERFORM UNTIL END-OF-FILE
               READ TRAN-OUT
                   AT END SET END-OF-FILE TO TRUE
                   NOT AT END
                       IF TRAILER-TYPE = "T"
                           DISPLAY "trailer: " TRAILER-COUNT
                               " detail records"
                       ELSE
                           MOVE DETAIL-RECORD TO LOAN-RECORD
                           DISPLAY "detail:  " LOAN-TYPE " "
                               LOAN-MEMBER-ID " " LOAN-BOOK-ID
                       END-IF
               END-READ
           END-PERFORM
           CLOSE TRAN-OUT
           STOP RUN.

Compiled with cobc -x -Wall -I. multirec.cob (and with -std=ibm), no warnings, it prints:

detail:  O 000100 000001
detail:  O 000101 000002
detail:  R 000100 000001
detail:  O 000102 000003
trailer: 000004 detail records

Distinguishing the layouts by the first byte is a convention of the data, not something the compiler checks. A trailer count is how the next job in a batch stream verifies that it received everything the previous one wrote.

A line longer than the record

Reading a LINE SEQUENTIAL file whose lines are longer than the FD does not truncate silently in GnuCOBOL 3.2: the extra characters come back as the next record, and the first of the two reads has status 06. A file with the lines AB, ABCDEFGHIJKLMNOP and XY, read with PIC X(8), gave:

status 00 [AB      ]
status 06 [ABCDEFGH]
status 00 [IJKLMNOP]

The short line was padded with spaces. Any program that reads text files tests for status 06, or sizes the record for the longest line expected.

What goes wrong: status codes of sequential statements

The next program provokes the common mistakes and prints the status after each:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SEQERR.
      * Status codes that sequential statements really produce.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT LOAN-FILE ASSIGN TO "LOANTRAN"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS WS-STATUS.
           SELECT MISSING-FILE ASSIGN TO "NOSUCH"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS WS-STATUS.
       DATA DIVISION.
       FILE SECTION.
       FD  LOAN-FILE.
       01  LOAN-LINE           PIC X(30).
       FD  MISSING-FILE.
       01  MISSING-LINE        PIC X(30).
       WORKING-STORAGE SECTION.
       01  WS-STATUS           PIC XX.
       PROCEDURE DIVISION.
       0000-MAIN-LINE.
           OPEN INPUT MISSING-FILE
           DISPLAY "open of a missing file:     " WS-STATUS
           READ LOAN-FILE
           DISPLAY "read before open:           " WS-STATUS
           OPEN INPUT LOAN-FILE
           OPEN INPUT LOAN-FILE
           DISPLAY "second open:                " WS-STATUS
           MOVE "X" TO LOAN-LINE
           WRITE LOAN-LINE
           DISPLAY "write to an INPUT file:     " WS-STATUS
           REWRITE LOAN-LINE
           DISPLAY "rewrite on an INPUT file:   " WS-STATUS
           READ LOAN-FILE
           READ LOAN-FILE
           READ LOAN-FILE
           READ LOAN-FILE
           DISPLAY "fourth read:                " WS-STATUS
           READ LOAN-FILE
           DISPLAY "read past the end:          " WS-STATUS
           CLOSE LOAN-FILE
           CLOSE LOAN-FILE
           DISPLAY "close of a closed file:     " WS-STATUS
           STOP RUN.

Compiled with cobc -x -Wall -I. seqerr.cob (and with -std=ibm), no warnings, it prints the same eight lines both ways:

open of a missing file:     35
read before open:           47
second open:                41
write to an INPUT file:     48
rewrite on an INPUT file:   49
fourth read:                00
read past the end:          10
close of a closed file:     42

They are the codes the standard defines: 35 file not found on OPEN INPUT, 41 file already open, 42 close of a file that is not open, 47 READ on a file that is not open for input, 48 WRITE on a file that is not open for output, 49 REWRITE or DELETE on a file that is not open I-O, and 10 at end. All of them appear in the table on File Status Codes. The program has a FILE STATUS item for every file, so none of these errors ended the run; without the item, GnuCOBOL prints a libcob message and stops (see Error Handling and Declaratives).

The balance-line update

A sequential master file cannot be changed in place, and a transaction file is a list of changes in arrival order. The classic batch solution is the balance-line algorithm, also called the sequential master-file update or the match-merge: put the transactions in the same key order as the master, then read both files in step. The old master is never modified. Every master record ends up in the new master, changed or not, and the transactions that cannot be applied go to a reject report.

The conditions for it to work:

  • Both files are sorted ascending on the same key, here the book number. The loan transactions arrive in the order in which they happened, so a SORT step orders them first (see Sort and Merge). Transactions for the same key must stay in arrival order.

  • The key of a file that has ended is set to a value higher than any real key, 999999 here (HIGH-VALUES for an alphanumeric key). This sentinel makes the end of one file simply "that file has nothing left that matches", and the loop ends when both keys are the sentinel. No special case is needed for the end of either file.

  • One transaction is applied to the master record in storage; the master is written only when the next transaction belongs to a higher key. So any number of transactions for the same book, in any combination, fall out of the same code.

flowchart TD A["Open old master, sorted transactions, new master, rejects"] --> B["Read first master record and first transaction"] B --> C{"Both keys = 999999?"} C -- "Yes" --> Z["Close the files, show the counts"] C -- "No" --> D{"Compare master key with transaction key"} D -- "master lower" --> E["Write master to the new master, read next master"] D -- "equal" --> F["Apply the transaction to the master record in storage, read next transaction"] D -- "master higher" --> G["Write transaction to the reject report, read next transaction"] E --> C F --> C G --> C

The figure shows why the loop needs only three branches. "Master lower" means that no transaction is left for this book, so the book is final. "Equal" applies the change and reads the next transaction, but not the next master, so a second transaction for the same book finds the same master record. "Master higher" means a transaction for a book that is not on the master (an unknown book number), which is a reject.

The program

The master BOOKMAST and the sorted transactions LOANSRT are line sequential files. The transactions were sorted by book number with a stable sort before the run (the shell command is sort -s -k1.8,1.13 LOANTRAN > LOANSRT; the portable COBOL version is on Sort and Merge), so the transactions of LOANTRAN are applied in this order: checkout of book 1, return of book 1, checkout of book 2, checkout of book 3. Records are read into the copybook items BOOK-RECORD and LOAN-RECORD with READ ... INTO, and written with WRITE ... FROM, so the FD items are plain PIC X areas and the working-storage copy is the one the logic uses. A checkout lowers the number of copies, a return raises it, and a checkout of a book with no copies left is rejected with a reason. Anything that is neither checkout nor return is rejected too.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BOOKUPD.
      * Balance-line update: old book master + sorted loan
      * transactions -> new book master + reject report.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT OLD-MASTER ASSIGN TO "BOOKMAST"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS WS-OLD-STATUS.
           SELECT TRANS-FILE ASSIGN TO "LOANSRT"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS WS-TRN-STATUS.
           SELECT NEW-MASTER ASSIGN TO "BOOKNEW"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS WS-NEW-STATUS.
           SELECT REJECT-FILE ASSIGN TO "BOOKREJ"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS WS-REJ-STATUS.
       DATA DIVISION.
       FILE SECTION.
       FD  OLD-MASTER.
       01  OLD-MASTER-LINE     PIC X(60).
       FD  TRANS-FILE.
       01  TRANS-LINE          PIC X(30).
       FD  NEW-MASTER.
       01  NEW-MASTER-LINE     PIC X(60).
       FD  REJECT-FILE.
       01  REJECT-LINE         PIC X(60).
       WORKING-STORAGE SECTION.
       COPY BOOKREC.
       COPY LOANREC.
       01  WS-OLD-STATUS       PIC XX.
       01  WS-TRN-STATUS       PIC XX.
       01  WS-NEW-STATUS       PIC XX.
       01  WS-REJ-STATUS       PIC XX.
       01  WS-MASTER-KEY       PIC 9(6) VALUE ZERO.
       01  WS-TRANS-KEY        PIC 9(6) VALUE ZERO.
       01  WS-COUNTS.
           05  WS-MASTERS-COPIED   PIC 9(3) VALUE ZERO.
           05  WS-APPLIED          PIC 9(3) VALUE ZERO.
           05  WS-REJECTED         PIC 9(3) VALUE ZERO.
       01  WS-REASON           PIC X(24).
       PROCEDURE DIVISION.
       0000-MAIN-LINE.
           PERFORM 1000-OPEN-FILES
           PERFORM 8100-READ-MASTER
           PERFORM 8200-READ-TRANS
           PERFORM 2000-MATCH-RECORDS
               UNTIL WS-MASTER-KEY = 999999
                 AND WS-TRANS-KEY = 999999
           PERFORM 9000-CLOSE-FILES
           DISPLAY "masters written  " WS-MASTERS-COPIED
           DISPLAY "applied          " WS-APPLIED
           DISPLAY "rejected         " WS-REJECTED
           STOP RUN.
       1000-OPEN-FILES.
           OPEN INPUT OLD-MASTER TRANS-FILE
           OPEN OUTPUT NEW-MASTER REJECT-FILE
           IF WS-OLD-STATUS NOT = "00" OR WS-TRN-STATUS NOT = "00"
              OR WS-NEW-STATUS NOT = "00" OR WS-REJ-STATUS NOT = "00"
               DISPLAY "open failed " WS-OLD-STATUS WS-TRN-STATUS
                   WS-NEW-STATUS WS-REJ-STATUS
               MOVE 12 TO RETURN-CODE
               STOP RUN
           END-IF.
       2000-MATCH-RECORDS.
           EVALUATE TRUE
               WHEN WS-MASTER-KEY < WS-TRANS-KEY
                   PERFORM 2100-COPY-MASTER
               WHEN WS-MASTER-KEY = WS-TRANS-KEY
                   PERFORM 2200-APPLY-TRANS
               WHEN OTHER
                   MOVE "NO SUCH BOOK" TO WS-REASON
                   PERFORM 2300-REJECT-TRANS
           END-EVALUATE.
       2100-COPY-MASTER.
           WRITE NEW-MASTER-LINE FROM BOOK-RECORD
           ADD 1 TO WS-MASTERS-COPIED
           PERFORM 8100-READ-MASTER.
       2200-APPLY-TRANS.
           EVALUATE TRUE
               WHEN LOAN-CHECKOUT AND BOOK-COPIES = ZERO
                   MOVE "NO COPIES AVAILABLE" TO WS-REASON
                   PERFORM 2300-REJECT-TRANS
               WHEN LOAN-CHECKOUT
                   SUBTRACT 1 FROM BOOK-COPIES
                   ADD 1 TO WS-APPLIED
               WHEN LOAN-RETURN
                   ADD 1 TO BOOK-COPIES
                   ADD 1 TO WS-APPLIED
               WHEN OTHER
                   MOVE "BAD TRANSACTION TYPE" TO WS-REASON
                   PERFORM 2300-REJECT-TRANS
           END-EVALUATE
           PERFORM 8200-READ-TRANS.
       2300-REJECT-TRANS.
           MOVE SPACES TO REJECT-LINE
           STRING LOAN-TYPE " " LOAN-MEMBER-ID " " LOAN-BOOK-ID " "
               WS-REASON DELIMITED BY SIZE INTO REJECT-LINE
           WRITE REJECT-LINE
           ADD 1 TO WS-REJECTED
           IF WS-MASTER-KEY > WS-TRANS-KEY
               PERFORM 8200-READ-TRANS
           END-IF.
       8100-READ-MASTER.
           READ OLD-MASTER INTO BOOK-RECORD
               AT END MOVE 999999 TO WS-MASTER-KEY
               NOT AT END MOVE BOOK-ID TO WS-MASTER-KEY
           END-READ.
       8200-READ-TRANS.
           READ TRANS-FILE INTO LOAN-RECORD
               AT END MOVE 999999 TO WS-TRANS-KEY
               NOT AT END MOVE LOAN-BOOK-ID TO WS-TRANS-KEY
           END-READ.
       9000-CLOSE-FILES.
           CLOSE OLD-MASTER TRANS-FILE NEW-MASTER REJECT-FILE.

Compiled with cobc -x -Wall -I. bookupd.cob (and with -std=ibm), no warnings, it prints:

masters written  004
applied          004
rejected         000

The new master BOOKNEW holds the updated copy counts (each record shown as stored, without the trailing space that a LINE SEQUENTIAL write removes):

0000019780000000011DATA STRUCTURES               003002450A
0000029780000000028COBOL BASICS                  001001999A
0000039780000000035BATCH PROCESSING              000001500A
0000049780000000042OLD LEDGERS                   001000975W

Book 1 is checked out and returned (3 copies, unchanged), book 2 is checked out (2 to 1) and book 3 is checked out (1 to 0). The reject file BOOKREJ is empty because every transaction was valid. A second night, with BOOKNEW copied to BOOKMAST and four different sorted transactions, shows the rejects: a checkout of book 3 that no longer has a copy, a checkout of book 4 that works, a transaction of type X for book 4, and a checkout of book 9, which does not exist:

masters written  004
applied          001
rejected         003

and BOOKREJ contains:

O 000100 000003 NO COPIES AVAILABLE
X 000102 000004 BAD TRANSACTION TYPE
O 000102 000009 NO SUCH BOOK

Book 4 went from one copy to none, the new master holds 000000975W for it, and the other three books are unchanged. The last transaction, for book 9, has a higher key than the last master, so it is matched against the sentinel 999999 and handled by the "master higher" branch, exactly like an unknown number in the middle of the file.

If an input file is missing the program stops at once, before it reads anything. With LOANSRT removed, the status line shows which file failed and the return code 12 tells the job step that it did not work:

open failed 00350000
libcob: warning: implicit CLOSE of REJECT-FILE ('BOOKREJ')
libcob: warning: implicit CLOSE of NEW-MASTER ('BOOKNEW')
libcob: warning: implicit CLOSE of OLD-MASTER ('BOOKMAST')

The first line is the four statuses in the order master, transactions, new master and rejects: 00, 35, 00, 00. The STOP RUN with files still open is what produced the libcob warnings; Enterprise COBOL closes them through Language Environment without a message.

Points worth keeping in mind when the program is changed:

  • The program must keep working when the transaction file is empty (the master is simply copied) and when the master is empty (every transaction is a reject). Both follow from the sentinel keys.

  • A transaction file that is not in key order silently produces wrong results: an out-of-order transaction compares as "master higher" and becomes a reject, or lets the master move on past a book that still has transactions. Production versions check the sequence (IF WS-TRANS-KEY < previous key) and stop with a sequence error.

  • The control totals (masters written, applied and rejected) should reconcile: masters read equal masters written, and transactions read equal applied plus rejected. They are what operators compare with the previous step’s trailer count.

  • Adding a book (a transaction that creates a master record) and deleting one (a master that is dropped from the new file) are the same loop with two more branches: "master higher" with an add type creates a record and writes it, and an equal key with a delete type reads the next master without writing the current one. The master order is preserved because the new record is written at the place where its key falls.

  • The same algorithm with an indexed master (see Indexed Files and VSAM) needs no sorted input and no new file: a transaction reads the master by key and rewrites it. The balance-line form wins when the transactions touch a large share of the master, because both files are read once, in order.

On z/OS: the same program with DD statements

The program above does not change on z/OS except for the ASSIGN names, which become DD names. The job step supplies each file, and the new master and the reject report are created in the step (z/OS; not run here):

//BOOKUPD  EXEC PGM=BOOKUPD
//STEPLIB  DD DISP=SHR,DSN=USERID.CIRC.LOADLIB
//BOOKMAST DD DISP=SHR,DSN=USERID.CIRC.BOOKMAST
//LOANSRT  DD DISP=SHR,DSN=USERID.CIRC.LOANSRT
//BOOKNEW  DD DSN=USERID.CIRC.BOOKNEW,DISP=(NEW,CATLG,DELETE),
//            RECFM=FB,LRECL=60,SPACE=(TRK,(5,1))
//BOOKREJ  DD SYSOUT=*
//SYSOUT   DD SYSOUT=*

Each assignment name in the SELECT entries (BOOKMAST, LOANSRT, BOOKNEW, BOOKREJ) is the DD name of one of those statements, and the ORGANIZATION IS LINE SEQUENTIAL clauses would be removed or replaced by ORGANIZATION IS SEQUENTIAL because the data sets are QSAM. Note the new-master disposition (NEW,CATLG,DELETE): a failed step leaves no half-written master in the catalog, which is what makes the write-a-new-file approach safer than updating in place. The step’s return code 12 from the open check is what a following COND or IF test in the job uses to skip the rest of the job.

Common mistakes

  • Processing the record area after AT END. Its content is undefined; the balance-line loop uses a sentinel key instead.

  • OPEN OUTPUT on a file that holds data you need. It empties the file. Use EXTEND to add, and a new data set (or file name) for a new version.

  • No FILE STATUS. An unexpected status then ends the run unit with a run-time message; with the item, the program sees 35, 41 or 47 and can report which file was the problem.

  • A FILE STATUS item inside the record. It is part of the data read; declare it in WORKING-STORAGE.

  • Unsorted input to a match-merge. The result is wrong without any error message; sort the transactions first, with a stable sort that keeps arrival order within a key.

  • REWRITE with a different length. The length of a sequential record cannot change in place.

  • Using LINE SEQUENTIAL for data with packed or binary items. A byte equal to the line terminator splits the record; keep binary data in record sequential files.

  • Assuming the record area survives a WRITE. Use WRITE ... FROM and keep the data in working storage.

  • Reading a text file with a record shorter than its longest line. GnuCOBOL 3.2 splits the line and sets status 06.

  • Mixing a PC file name with a z/OS DD name. ASSIGN TO "Customers.dat" suits GnuCOBOL; on z/OS the assignment name is a DD name (see File Concepts, SELECT and FD).