IMS DL/I
Databases, segments, and the PCB list.
Part of the BankTS language reference.
IMS DL/I
record AccountSegment {
acctId: string<10>;
balance: decimal<9, 2>;
}
database accountDb pcb segment "ACCTSEG" key "ACCTID"
record AccountSegment status dbStatus;
getUnique accountDb into segment key "0000000001";
getNext accountDb into segment;
getHoldUnique accountDb into segment key "0000000001";
getHoldNext accountDb into segment;
insertSegment accountDb from segment;
replaceSegment accountDb from segment;
deleteSegment accountDb;
An IMS program does not open a database or read it with file control. The region
hands it a PCB, and every operation is CALL "CBLTDLI" with a function code,
that PCB, a segment area, and — for a qualified read — a search argument:
PROCEDURE DIVISION USING IO-PCB ACCOUNT-DB-PCB.
MOVE "0000000001" TO ACCOUNT-DB-SSA-VALUE
CALL "CBLTDLI" USING DLI-GU, ACCOUNT-DB-PCB, ACCOUNT-SEGMENT,
ACCOUNT-DB-SSA
MOVE ACCOUNT-DB-PCB-STATUS TO DB-STATUS
The I/O PCB comes first, always. A batch program needs it to make system
service calls, so CMPAT=YES is what IBM says to specify — and with it the
region passes the I/O PCB ahead of every database PCB. Leaving it out does not
fail to compile: it shifts every database PCB by one, so the program reads the
I/O PCB as its first database and works on whatever that storage holds.
The segment and key names live on the declaration, because the search argument
is built from them once and each is eight bytes — what DL/I carries, and a
longer one is truncated into a name matching nothing in the DBD
(BANK-DLI-001).
Each call takes the search argument it needs, and they are not the same:
| Call | Argument |
|---|---|
getUnique, getHoldUnique |
qualified — segment, field, value |
getNext, getHoldNext |
unqualified — nine bytes of segment name |
insertSegment |
unqualified |
replaceSegment, deleteSegment |
none |
An unqualified argument still matters. A GN without one returns the next
segment of any type in hierarchical order, not the one the database
declares, and an ISRT without one has nothing telling DL/I what to insert.
REPL and DLET take none because they act on the segment the get-hold held.
Holding before updating
DL/I will not update a segment the program has not held. A replaceSegment
or deleteSegment after a plain getUnique comes back with status DJ and the
update does not happen — which the program only discovers if it tests the
status.
So read it with getHoldUnique or getHoldNext first, and the compiler
insists (BANK-DLI-002):
getHoldUnique accountDb into segment key accountId;
if dbStatus == " " {
segment.balance = segment.balance + amount;
replaceSegment accountDb from segment;
}
A hold earlier in an enclosing block covers a branch inside it, because every path through the branch has passed it. A hold inside a branch does not travel back out: the path that skipped the branch reaches the update unheld.
The status field is required. The two characters DL/I leaves in the PCB are
the entire error model — spaces worked, GE found nothing, GB reached the end
— so without somewhere to read them a getUnique that found nothing is
indistinguishable from one that worked, and the program uses whatever the
segment area held last. It reads like a file status:
getUnique accountDb into segment key accountId;
if dbStatus == " " {
...
}
What the local run establishes
The tests execute against runtime/CBLTDLI.cbl, which
is not IMS: it evaluates no database, holds no segments, and maintains no
position. It puts a scripted status in the PCB so the branches can be reached.
That proves the program issues its calls in order with the right function codes,
and takes the branch its status test selects. It proves nothing about what IMS
would return. Same grade of evidence as Db2 and CICS already have here — see
runtime/README.md.