Files
File declarations, organisation, keys, and file status.
Part of the BankTS language reference.
File declarations
file accountInput sequential input record AccountRecord status accountInputStatus;
file postingOutput sequential output record PostingRecord status postingOutputStatus;
Rules:
- file status must be checked
- record type must map to a copybook-compatible layout
- generated COBOL must contain file-control and FD sections
The status clause names the field that receives the COBOL FILE STATUS
value. It is optional at parse time so that a missing status is reported as
BANK-FILE-001 with a remediation hint rather than as a syntax error.
A file is declared input, output, or update. update opens I-O, which is
what a master file update needs: the same OPEN serves the read that finds a
record and the rewrite that puts it back.
An indexed file is declared ACCESS MODE IS DYNAMIC, not RANDOM, because it
is both read by key and browsed, and RANDOM allows only the first.
Text files
file paymentFeed lineSequential input record PaymentLine status paymentFeedStatus;
lineSequential is a text file: records end at a newline rather than at a fixed
width. It is what an import from anything that is not a mainframe looks like — a
payment feed, a reconciliation extract, a file a counterparty sent — and
Enterprise COBOL 6.4 has it as ORGANIZATION IS LINE SEQUENTIAL for files in
the z/OS UNIX file system.
It carries three restrictions the other organisations do not, and all three are the target's rather than this compiler's.
Every field has to be printable. Enterprise COBOL requires that records "contain only USAGE DISPLAY and DISPLAY-1 items", and this is the one that catches people, because BankTS's default is exactly what is forbidden:
record PaymentLine {
payAccount: string<16>;
payAmount: decimal<11, 2>; // BANK-FILE-014
}
decimal is packed — two digits to a byte with a sign nibble — and written into
a text file it produces bytes that are neither the number nor readable text. The
WRITE succeeds and nothing says so until somebody opens the file. Declare the
number zoned if it can be negative, which emits the SIGN IS TRAILING SEPARATE the target asks for, or unsigned if it cannot:
record PaymentLine {
payAccount: string<16>;
payAmount: zoned<11, 2>;
payValueDate: date;
}
It cannot be opened for update. input and output only. A record's length
is fixed once written, so there is nothing to rewrite in place —
BANK-FILE-013. A text file is amended by reading it and writing a new one,
which is what a job that rebuilds an extract already does.
There is no delete and no browse. Both need a record the file can address,
and this organisation has neither an index nor a relative number.
Reading pads: a line shorter than the record is space-filled to the declared width, and writing strips the trailing blanks again. So a record round-trips through a text file unchanged only if its declared width matches what is in the file — worth remembering when a feed's last field is variable.
The generated JCL allocates a z/OS UNIX path rather than a dataset, because that is where these files live:
//PAYMENTF DD PATH='/u/banklang/paymentf',
// PATHOPTS=(ORDONLY)
Two differences between GnuCOBOL and the target are recorded in divergences — D23 on a final record with no delimiter, and D24 on what a blank numeric field does.
Records that vary in length
file feed sequential output record FeedLine
varying 10 to 80 length feedLength status feedStatus;
RECORD IS VARYING IN SIZE. A fixed-length file pads every record to the
longest one it might hold; for a feed whose records differ by hundreds of bytes
that is most of the dataset, and on tape it is most of the tape.
length names the field that says how much of the record is in use — set it
before a write, and a read fills it:
line.payload = "SHORTER ONE";
feedLength = textLength(line.payload);
write feed from line;
textLength pairs with it: it is what the field holds rather than how wide it
was declared, which is exactly the number a varying write needs.
A record written shorter than the declared minimum is not written — COBOL
rejects it and the file status says so, which is what the status field is for.
The bounds have to be a range, and the file has to be sequential: an indexed or
relative dataset addresses a record by key or by position, which a varying length
would move (BANK-FILE-009).
File operations
open accountFeed;
read accountFeed into account;
write adviceOutput from advice;
close accountFeed;
The record variable's type must match the file's declared record type, or the
bytes would not line up; a mismatch is BANK-FILE-002. Reading an output file
or writing an input file is BANK-FILE-001.
A file's name folds to a DD name of eight characters, and the generated SELECT
reads ASSIGN TO <DD>. If a record or a field has that same COBOL name, both
Enterprise COBOL 6 and GnuCOBOL take the file name from its contents instead —
the program compiles and the OPEN fails with status 35. BANK-FILE-016.
A read sets the status field to "10" at end of file, so a batch loop can
test it:
while accountFeedStatus == "00" limit 100000 {
read accountFeed into account;
}
Several record layouts on one file
record BillHeading { headingText: string<98>; }
record BillDetail { billCustomer: string<5>; billTotal: edited<zoned<9,2>, "plain">; }
file bills lineSequential output record BillHeading, BillDetail status billsStatus;
COBOL's several 01 entries under one FD. They share a record area as long as
the longest of them, and each write names the layout it is writing:
write bills from heading;
write bills from detail;
The type chooses the variant, so a heading cannot reach a detail's fields and a
short layout writes its own length rather than the area's. It is what a report
file is: a heading line and detail lines are different shapes and always were.
2,812 of the 6,451 file descriptions in the X-COBOL corpus declare more than one
record, and 2,663 of those are opened OUTPUT.
Output only (BANK-FILE-015). A write names a layout; a read does not.
Which one arrived is decided by the data, and BankTS will not hand back a value
whose type is a guess. A file that is read declares one record, and a feed
carrying several kinds is read as one layout with a field saying which kind it
is. The same rule covers a record key and a varying length, each of which
describes one layout rather than a choice between several.
The read side is measured rather than assumed. 143 of the corpus's file
descriptions carry several records and are opened INPUT, and those 143 are 51
distinct file contents: 21 parser, grammar, language-server and compiler-test
fixtures, 16 copies of the NIST CCVS85 conformance suite, and 14 textbook and
course programs. No application in 5,195 files reads a file this way. Eleven of
the fourteen are the same shape — a record code in the leading field, named by
88 levels — so if that changes, what it would need is a typed variant with
narrowing the compiler checks, not the redefines above.
The outcome of an operation has to be looked at
read accountFeed into account;
if accountFeedStatus == "00" {
post(account);
}
End of file (10), no such record (23) and a duplicate key (22) are not
failures: they are answers, and the generated check lets them through for the
program to decide about. A program that does not decide carries on with the
record area still holding the record before it — a read past the end of a feed
posts the last transaction twice, with a return code of zero.
So an operation that can end with one of those statuses leaves an outcome the
program owes an answer to, and BANK-FILE-017 is raised when it uses the record
that operation filled, operates on the file again, or reaches the end of the
routine without comparing the status. A close counts as operating on the file:
it sets the status too, so a test written after one reads the close's answer.
The comparison counts wherever it is written — in an if, in a loop condition,
into a local — so the drain loop above stays exactly as it was. A log of the
status does not count: printing the answer is not reading it.
Using the record covers every way a program can read it, not only reading a
field out of it. COBOL hands whole records to things by naming them, and
write trail from line, release line, putMessage feedQueue from line,
call "BANKSUB" using line and json out from line are each the stale record
going somewhere. A statement that fills it — a second read into it, a queue
getMessage into it — is not a use: replacing the bytes is the fix.
The rule reaches into every block a statement runs, the on page block of a
write and a sort procedure's body included. A transaction's on failure
handler and a file's on error handler are each checked as a routine of their
own, because control reaches them from anywhere: nothing the body owed is known
there, and an operation the handler itself performs owes the same answer.
What the compiler checks for you
Every I/O statement is followed by a generated test of the file status key —
the first character, since class 0 is successful completion and includes 02,
04, 05 and 07, not only 00. A status outside class 0 names the operation,
the file and the status in the job log, sets a return code of 12, and stops.
The statuses a statement is written to produce are left to the program: end of file on a read, a key that was not there on a keyed read or a browse, a duplicate key on a write to a KSDS. Those say the request found nothing, not that the file failed, so the loop above still ends the way it always did.
This matters most where nothing else would notice. A write that cannot happen
— the volume full, a varying record outside its declared length — leaves the
loop running and the output file short, and the job ends with a return code of
zero. Inside a sort's input or output procedure the same failure sets
SORT-RETURN to 16 instead, because control may not leave a sort procedure
while the sort is running.
Read and write map the record field by field rather than moving it as a
group, so the correspondence between the file record and working storage is
explicit in the generated COBOL and does not depend on the two layouts being
byte-identical. An array field is copied element by element, because COBOL
rejects a move of an OCCURS item without a subscript.
The FD record is emitted as an unstructured buffer sized from the copybook
layout, and the structured record is declared once in working storage. Emitting
the record inside each FD as well would duplicate field names and make every
unqualified reference ambiguous.
Alternate keys
file accountMaster indexed input record Account
key accountId alternate customerId, branchId status masterStatus;
A KSDS is read by its primary key and browsed by any of its alternates. A program that can only name the primary cannot open a file whose alternate index is the whole reason it exists — an account file read by customer, say.
Each alternate is declared WITH DUPLICATES, because many accounts per customer
is nearly always why one exists. Only an indexed file has them
(BANK-FILE-004).
When an operation fails anyway
on error accountInput {
log "FILE ERROR ", inStatus;
returnCode = 12;
}
A file status check covers the statement that thought to look. This covers the
ones that did not: COBOL runs a USE AFTER STANDARD ERROR procedure when any
operation on that file fails, wherever it was written. That is what makes
DECLARATIVES the standard error path rather than a convenience.
The handler is declared at the top level, not inside a transaction, because it is not reached from one — it runs when the failure happens. It sees the file statuses and nothing else: there is no record in scope and no ledger to post to.
A file may have one handler (BANK-FILE-005), which is what COBOL allows. When
any handler exists, the program's own paragraphs move into a BANK-BODY SECTION,
because everything after DECLARATIVES has to be in a section.
Browsing an indexed file
start accountMaster key master.accountId; // position at or after the key
readNext accountMaster into master; // walk from there
START uses KEY IS NOT LESS THAN, which begins at the first record at or
after the key — what a range walk wants. An exact match would make a browse from
a partial key impossible.
readNext reports end of data through the file status, the way a sequential
read does, because a browse runs out rather than failing.
Updating in place
read accountMaster into master key master.accountId;
master.balance = master.balance + amount;
rewrite accountMaster from master;
delete accountMaster key master.accountId;
rewrite and delete need the file open for update, because updating a
record in place means finding it first (BANK-FILE-005). start and readNext
need an indexed file, because there is no index to walk otherwise. A write
or rewrite to an indexed file captures INVALID KEY in the file status: a
duplicate key is the failure a KSDS write actually has, and it is silent
otherwise.