COBOL 103: The Cassette Tape Pattern (Deleting and Editing Data on the Metal)


Modern software has made developers incredibly lazy. When a JavaScript developer wants to remove an item from a database, they just call an ORM method like Task.findByIdAndDelete(1). The framework abstracts away the database driver, which abstracts away the SQL engine, which abstracts away the filesystem, which abstracts away the disk sectors.

You press a button, and the magic happens. But when you work on the bare metal—or in our case, with COBOL Sequential Files—there is no magic. You have to understand exactly how the data sits on the disk.

Today, we are going to finish our COBOL To-Do list by adding the ability to edit and delete tasks, bringing our app full circle into a complete CRUD (Create, Read, Update, Delete) system.

The Problem: You Cannot Snip a Cassette Tape In the last tutorial, we saved our tasks to todos.dat using LINE SEQUENTIAL organization. The records are written back-to-back on the hard drive.

Here is the problem: You cannot just delete a line from the middle of a sequential file. If you have three tasks of 58 bytes each, and you want to delete the second one, you can't just leave an empty 58-byte hole on the disk. The filesystem doesn't work like that. If you try to write a shorter string over an existing string, you leave garbage trailing bytes.

To solve this, COBOL developers use what I call the Cassette Tape Pattern (officially known as the Master File Update pattern). Imagine an audio cassette. You can't magically erase the second song and have the third song slide perfectly into its place. Instead, you get a second, blank cassette. You play the first cassette, copy song one to the new tape, skip song two, and copy song three. Then, you throw away the old cassette and keep the new one.

Let's write the code to do exactly that.

1. Expanding the Data Division

To use our two-cassette system, we need to define a second file in our ENVIRONMENT and DATA divisions. We will call it TEMP-FILE. We also need a way to track which task the user wants to interact with, so we will add a WS-COUNTER and a WS-TARGET-NUM.

       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT OPTIONAL TODO-FILE ASSIGN TO "todos.dat"
           ORGANIZATION IS LINE SEQUENTIAL.
      * Our second "blank cassette"
           SELECT OPTIONAL TEMP-FILE ASSIGN TO "temp.dat"
           ORGANIZATION IS LINE SEQUENTIAL.

       DATA DIVISION.
       FILE SECTION.
       FD  TODO-FILE.
       01  TODO-RECORD.
           05 TODO-DUE-DATE   PIC 9(8).
           05 TODO-TEXT       PIC X(50).
           
       FD  TEMP-FILE.
       01  TEMP-RECORD.
           05 TEMP-DUE-DATE   PIC 9(8).
           05 TEMP-TEXT       PIC X(50).

       WORKING-STORAGE SECTION.
       01  WS-CHOICE          PIC 9 VALUE 0.
       01  WS-EOF             PIC X VALUE 'N'.
       01  WS-TARGET-NUM      PIC 9(3).
       01  WS-COUNTER         PIC 9(3).
       01  WS-CURRENT-DATE    PIC 9(8).
       01  WS-TEMP-DATE-TIME  PIC X(21).

2. The Delete Logic

When the user selects "Delete", we ask them for the task number. Then, we open the original file for reading (INPUT) and the temp file for writing (OUTPUT).

We loop through the original file, keeping a running counter. If the counter does not match the target number, we copy the record to the temp file. If it does match, we simply skip the write command.

       DELETE-TASK.
           DISPLAY "ENTER TASK NUMBER TO DELETE: " WITH NO ADVANCING.
           ACCEPT WS-TARGET-NUM.
           
           OPEN INPUT TODO-FILE.
           OPEN OUTPUT TEMP-FILE.
           MOVE 'N' TO WS-EOF.
           MOVE 1 TO WS-COUNTER.
           
           PERFORM UNTIL WS-EOF = 'Y'
               READ TODO-FILE
                   AT END 
                       MOVE 'Y' TO WS-EOF
                   NOT AT END
                       IF WS-COUNTER NOT = WS-TARGET-NUM
      * Copy the song to the new cassette
                           MOVE TODO-RECORD TO TEMP-RECORD
                           WRITE TEMP-RECORD
                       ELSE
      * Skip the song
                           DISPLAY "[+] TASK DELETED."
                       END-IF
                       ADD 1 TO WS-COUNTER
               END-READ
           END-PERFORM.
           
           CLOSE TODO-FILE TEMP-FILE.
           PERFORM COPY-TEMP-TO-MAIN.

3. The Edit Logic

Editing follows the exact same pattern, but instead of skipping the record when we find a match, we halt the system, ask the user for new input, and write the new data to the temp file instead of the old data.

       EDIT-TASK.
           DISPLAY "ENTER TASK NUMBER TO EDIT: " WITH NO ADVANCING.
           ACCEPT WS-TARGET-NUM.
           
           OPEN INPUT TODO-FILE.
           OPEN OUTPUT TEMP-FILE.
           MOVE 'N' TO WS-EOF.
           MOVE 1 TO WS-COUNTER.
           
           PERFORM UNTIL WS-EOF = 'Y'
               READ TODO-FILE
                   AT END 
                       MOVE 'Y' TO WS-EOF
                   NOT AT END
                       IF WS-COUNTER = WS-TARGET-NUM
      * We found the target. Ask for new data.
                           DISPLAY "NEW DUE DATE (YYYYMMDD): " WITH NO ADVANCING
                           ACCEPT TEMP-DUE-DATE
                           DISPLAY "NEW TASK DESCRIPTION: " WITH NO ADVANCING
                           ACCEPT TEMP-TEXT
                           WRITE TEMP-RECORD
                           DISPLAY "[+] TASK UPDATED."
                       ELSE
      * Not the target. Copy exactly as it is.
                           MOVE TODO-RECORD TO TEMP-RECORD
                           WRITE TEMP-RECORD
                       END-IF
                       ADD 1 TO WS-COUNTER
               END-READ
           END-PERFORM.
           
           CLOSE TODO-FILE TEMP-FILE.
           PERFORM COPY-TEMP-TO-MAIN.

4. Swapping the Cassettes

At the end of both the Delete and Edit paragraphs, you will notice a call to PERFORM COPY-TEMP-TO-MAIN.

Because our updated list now lives entirely inside temp.dat, we need to overwrite the old todos.dat file so the user sees their changes next time they run the app. We achieve this by opening TEMP-FILE for input, and TODO-FILE for output (which instantly wipes the original file clean), and copying the records back.

       COPY-TEMP-TO-MAIN.
           OPEN INPUT TEMP-FILE.
           OPEN OUTPUT TODO-FILE.
           MOVE 'N' TO WS-EOF.
           
           PERFORM UNTIL WS-EOF = 'Y'
               READ TEMP-FILE
                   AT END
                       MOVE 'Y' TO WS-EOF
                   NOT AT END
                       MOVE TEMP-RECORD TO TODO-RECORD
                       WRITE TODO-RECORD
               END-READ
           END-PERFORM.
           
           CLOSE TEMP-FILE TODO-FILE.

(Note: In a real IBM Mainframe environment, you wouldn't write a COBOL routine to copy the data back. You would just use the JCL script to safely delete the old dataset and rename the temporary dataset upon successful completion. But for our standalone GnuCOBOL executable, this internal copy routine works perfectly).

The Big Picture

If you followed along and compile this (cobc -x -o todo todo.cbl), you now have a lightning-fast, zero-dependency, bare-metal application that manages state entirely through raw byte manipulation on the disk.

There is no SQLite engine running in the background. There is no garbage collector cleaning up your mess. It is just you, the compiler, and the physical disk sectors.

Understanding how to manipulate sequential files like this teaches you exactly what databases are actually doing under the hood when they reorganize indexes or vacuum deleted rows. When you strip away the bloat, you are left with pure, predictable engineering.


← Back to COBOL & Z Server