Data stores anti-pattern
IMS hierarchical navigation
IMS DB programs navigate a segment hierarchy with DL/I GU, GN and GNP calls whose results depend on current database position, so the query is a sequence of calls, not a statement.
- FAMILY
- Data stores
- CUTOVER RISK
- Critical
- Changes runtime behavior in ways a line-by-line reading misses. Resolve before any cutover plan.
- MODERN MAPPING
- Relational aggregate with foreign keys
- GENERATED ARTIFACT
- CustomerAggregate.schema.json
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1 01 DLI-GU PIC X(04) VALUE 'GU '.2 01 DLI-GNP PIC X(04) VALUE 'GNP '.3 01 SSA-CUSTOMER.4 05 FILLER PIC X(09) VALUE 'CUSTOMER('.5 05 FILLER PIC X(10) VALUE 'CUSTNO ='.6 05 SSA-CUSTNO PIC X(08).7 05 FILLER PIC X(01) VALUE ')'.8 01 SSA-POLICY PIC X(09) VALUE 'POLICY '.9 ...10 MOVE IN-CUSTNO TO SSA-CUSTNO11 CALL 'CBLTDLI' USING DLI-GU, CUST-PCB,12 CUST-IO, SSA-CUSTOMER13 IF CUST-PCB-STATUS = 'GE'14 GO TO 4999-EXIT15 END-IF16 PERFORM UNTIL CUST-PCB-STATUS NOT = SPACES17 CALL 'CBLTDLI' USING DLI-GNP, CUST-PCB,18 POL-IO, SSA-POLICY19 IF CUST-PCB-STATUS = SPACES20 PERFORM 5000-RATE-POLICY21 END-IF22 END-PERFORM.
Modern: Relational aggregate with foreign keys
1CREATE TABLE customer (2 cust_no CHAR(8) PRIMARY KEY3);4CREATE TABLE policy (5 cust_no CHAR(8) NOT NULL REFERENCES customer,6 policy_no CHAR(10) NOT NULL, -- sequence field7 PRIMARY KEY (cust_no, policy_no)8);910-- path NP-03: GU CUSTOMER, then GNP POLICY11SELECT p.*12FROM policy p13WHERE p.cust_no = :custNo14ORDER BY p.policy_no; -- hierarchical order
Why it is dangerous in modernization
An IMS database is a tree of segment types: a customer root, policies under it, coverages under each policy. Programs read it through DL/I calls against a PCB: GU (get unique) positions on a segment that matches the segment search arguments, GN (get next) moves forward in hierarchical order, and GNP (get next within parent) moves forward but only under the current parent. What a GN returns depends on where the previous call left the position pointer, and on the status code it set, such as GE (not found), GB (end of database), GA and GK (crossed a level or segment type). The meaning of a program is a walk over the tree that the code never states as a query. Relational conversions that flatten segments into tables without reconstructing those walks change which children are processed, in which order, and under which parent.
How PYD ENTERPRISES detects it
PYD ENTERPRISES reads DBDs and PSBs to build the segment hierarchy, field definitions and the program's sensitive segments. Every CALL 'CBLTDLI' is decoded into function code, PCB, I/O area and SSA list, with qualified SSAs parsed into segment, field, operator and value. A position-tracking analysis then simulates database position across the control-flow graph, so each GN and GNP is resolved to the segment types it can return and the parent it is bounded by. Status-code tests are linked to the calls that set them. The result is a set of navigation paths, each equivalent to a parent-child query.
Maps to: Relational aggregate with foreign keys
Each segment type becomes a table keyed by its parent's key plus its own sequence field. Each navigation path becomes a join or an aggregate query with an explicit ORDER BY matching IMS hierarchical sequence, which is what the legacy program relied on. Where the target is a document store, the hierarchy maps directly to nested documents, and the navigation paths become projections.
Generated specification fragment
1{2 "$schema": "https://json-schema.org/draft/2020-12/schema",3 "title": "CustomerAggregate",4 "description": "From DBD CUSTDBD: CUSTOMER > POLICY > COVERAGE",5 "type": "object",6 "required": ["custNo", "policies"],7 "properties": {8 "custNo": { "type": "string", "maxLength": 8 },9 "policies": {10 "type": "array",11 "description": "IMS hierarchical sequence by POLICYNO",12 "items": { "$ref": "#/$defs/Policy" }13 }14 }15}
Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.
Position is state
After the GU in the example, database position sits on one CUSTOMER segment. Each GNP returns the next POLICY under that customer and sets a blank status; when there are no more, it returns GE and the loop ends. If 5000-RATE-POLICY issued its own call against the same PCB, for example a GU to read a reference segment, position would move, and the next GNP would continue from somewhere else entirely. This is why the engine tracks position through called paragraphs and through CALLs to other programs that receive the PCB, not only within the loop.
Status codes as control flow
- Blank: the call succeeded.
- GE: no segment satisfied the call (for GNP, no more children under the parent).
- GB: end of database reached during a GN.
- GA: an unqualified GN moved up to a higher level in the hierarchy.
- GK: an unqualified GN returned a different segment type at the same level.
Programs that use unqualified GN and branch on GA or GK are walking the whole tree and reacting to its shape. Those walks become the most complex queries in the migration, and PYD ENTERPRISES reports them separately so they are planned, not discovered in testing.
Logical relationships and secondary indexes
IMS logical relationships let a program see segments from another database as if they were children, and secondary indexes let it enter the hierarchy through a field other than the root key. Both are declared in the DBD, not in the program. The engine resolves them from the DBD and PSB, so a navigation path that crosses databases is shown with its real physical source and becomes a join between the right tables.
Related patterns
Usually found in the same codebase.
Find every IMS hierarchical navigation in your codebase.
Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.