Data Model Extraction
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/architecture/references/discovery/data-model-extraction.md |
| Description | Not specified |
Source Content
Data Model Extraction
Turning legacy record layouts — DDS, copybooks, DB2, VSAM, flat files — into a data model a modern engineer can read: a logical ERD plus a field dictionary. This is phase 2 of the discovery playbook. Extract the data before the logic; the record layout tells you what the program is about, and the code tells you what it does to that data.
Contents
- Where the model lives, per platform
- The decode-gotcha catalog
- Inferring relationships without foreign keys
- Building the ERD and field dictionary
- Verify before you trust the model
- Validating against live data
Jack got the export dump on a Monday and read PIC S9(7)V99 COMP-3 as a 12-character string. He mapped it straight into a VARCHAR, shipped, and the finance team found benefit amounts off by orders of magnitude — a $1,450.00 payment stored as raw packed bytes.
Jill, handed the same copybook, saw COMP-3 and stopped: five bytes of packed decimal, two implied decimals, a sign nibble at the end. She decoded it to a scaled integer, sampled ten real rows against the source, and her numbers matched to the cent. The difference was not skill with SQL — it was reading the storage clause before trusting the picture.
This file is the discipline that keeps you on Jill’s path. Every gotcha below is a place where the bytes on disk do not mean what the field name suggests.
Where the model lives, per platform
Legacy systems rarely wrote their schema as SQL DDL. The schema lives in the layout language of each platform — and you read it there. Decode syntax lives in the platform decoders; this section only says where to look.
IBM i — DDS physical and logical files
A PF (physical file) is the table: a record format, fields with type and length, and key fields. An LF (logical file) is a view, index, join, or row filter over PFs — treat each one as a query the business cared about enough to persist. Field-line syntax (A, P, S, B, L) and the PF/LF split are in ibm-i-as400.md. A join logical file is a declared relationship — harvest it directly for the ERD.
IBM i — DB2 for i DDL and the QSYS2 catalog
Modern IBM i code uses SQL CREATE TABLE / CREATE INDEX / CREATE VIEW in .sql members, which can carry long names and constraints. When those exist, they are authoritative — no reverse-engineering needed. The live catalog in QSYS2 (SYSTABLES, SYSCOLUMNS, SYSKEYS) confirms the model against the running box; see Validating against live data. Watch the DDS-file vs SQL-DDL coexistence noted in ibm-i-as400.md.
Mainframe — COBOL copybooks
A copybook is the single best source of the mainframe data model — the schema the system never wrote as DDL. One copybook is usually one file’s record; inventory the copybooks first and you have enumerated the entities. The COPY … REPLACING trap (field-name prefixing at copy time) means the effective layout can differ from the raw copybook — expand it before trusting field names. Level numbers, PIC, and USAGE are decoded in mainframe-cobol.md.
Mainframe — DB2 embedded SQL and DCLGEN
When a COBOL program contains EXEC SQL … END-EXEC, the data model for those tables is real SQL. Get the DDL or the catalog (SYSIBM.SYSTABLES, SYSCOLUMNS) and you have an authoritative schema. DCLGEN generates a copybook-like host-variable declaration from a table — a reliable column list straight from the source. Details in mainframe-cobol.md.
Mainframe — VSAM record keys
A KSDS is a keyed store — treat it as a single keyed table. Its record layout is the copybook; its primary key is in the COBOL SELECT … RECORD KEY IS … clause. An alternate index (AIX) is a secondary access path — note it as a query the business needed. ESDS (insertion order) and RRDS (by record number) map to append-log and array shapes; see mainframe-cobol.md.
Flat-file record layouts
Sequential datasets (PS), IBM i flat files, and fixed-width interchange feeds have no declared keys at all — the layout is only in the copybook or DDS that reads them, and the “key” is a convention. Read the layout from whatever program OPENs the file, and confirm field boundaries by offset arithmetic against sample bytes. A flat file shared between two systems is an integration point — flag it for the dependency map.
The decode-gotcha catalog
Each gotcha is a place where the raw bytes betray the field name. For each: how to detect it in the layout, and how to decode it. The platform decoders own the deeper syntax — this catalog is the extraction checklist.
Packed decimal (COMP-3)
Two decimal digits per byte, with a sign nibble in the low half of the last byte. A PIC S9(7)V99 COMP-3 is 5 bytes, not 12 characters.
- Detect: COBOL
USAGE COMP-3orPACKED-DECIMAL; DDS field typeP. Byte length isceil((digits + 1) / 2). - Decode: read two digits per byte; the final nibble is the sign (
C/Fpositive,Dnegative). Then apply the implied decimal below. This is the single most common extraction error — Jack’s error.
Zoned decimal
Numbers stored one digit per byte as text, with the sign folded into the zone nibble of the last byte.
- Detect: DDS field type
S; COBOL numericDISPLAYwith a leadingSin thePIC. - Decode: each byte is one digit; the last byte carries the sign in its high nibble (see signed overpunch). Length in bytes equals the digit count.
Signed overpunch
The sign of a zoned-decimal number is encoded into the last digit’s byte, so the final character prints as a letter or symbol.
- Detect: a numeric field whose last position shows
{,},A–R, or a stray letter in a data sample. - Decode:
{=+0,A–I=+1–+9;}=-0,J–R=-1–-9. So12{is+120and12}is-120. Glossary entry: signed overpunch.
EBCDIC vs ASCII
Mainframe and IBM i text is EBCDIC, not ASCII — the byte values differ for every letter and digit.
- Detect: raw bytes where
Ais0xC1(not0x41) and0is0xF0(not0x30); tools showing garbage for known text. - Decode: transcode with a named code page (US is CP037; some shops use CP1140 for the euro sign). Pick the code page deliberately — the wrong one corrupts
[,],@, and currency symbols silently.
Implied decimal (V)
The decimal point is defined but never stored. S9(7)V99 holds 145000 on disk for the value 1450.00.
- Detect: a
Vin a COBOLPIC; the decimal-position digit in a DDS packed/zoned field (the2in9 2P). - Decode: scale by
10^(-decimals)after reading the raw integer. Keep the scale in the field dictionary — a missing scale is a silent hundred-fold error.
Binary and endianness (COMP)
COMP / COMP-4 / BINARY fields are true binary integers (2, 4, or 8 bytes), big-endian on these platforms.
- Detect: COBOL
USAGE COMP/BINARY; DDS field typeB. - Decode: read as a big-endian signed integer of the declared byte width. Reading it little-endian (the x86 default) swaps the bytes and produces nonsense. Note the width —
PIC S9(4) COMPis 2 bytes,S9(9) COMPis 4.
REDEFINES (one field, several meanings)
REDEFINES overlays a second layout on the same bytes, so 20 bytes can be a date in one branch and a code in another.
- Detect: the
REDEFINESkeyword in COBOL; on IBM i, the same bytes read through different field definitions or aREFFLD-driven overlay. - Decode: find the condition that selects which view — that selector is a business rule, not just a layout note. Record both interpretations in the dictionary and cite the branch that picks each. See
mainframe-cobol.md.
OCCURS and OCCURS DEPENDING ON
OCCURS makes an array; OCCURS DEPENDING ON makes the record variable-length, so offsets shift at runtime.
- Detect: the
OCCURSclause; a trailingDEPENDING ON counter-field. - Decode: a fixed
OCCURSbecomes a child table or a repeating group (12 monthly amounts → 12 rows keyed by month).DEPENDING ONmeans no fixed offset past that point — flag it; any flat extract must read the counter first. Full trap inmainframe-cobol.md.
Date storage forms
Dates are the richest source of silent bugs because the legacy world stored them a dozen ways, rarely as a real date type.
- Detect: a numeric field named
*DTE/*DATE/DT; a 6- or 7-digit packed field near an event. - Decode by form:
- Packed CYYMMDD — the IBM i “century-year” form: a leading
0means 19xx,1means 20xx.1240704is 2024-07-04. - Julian (YYDDD or CYYDDD) — year plus day-of-year (
24186= 2024 day 186). - Separate y/m/d fields — three numeric fields to reassemble.
- 2-digit years — a windowing rule decides the century (often
<50→ 20xx). Find the pivot in the code; do not assume it.
- Packed CYYMMDD — the IBM i “century-year” form: a leading
Reference fields (DDS REFFLD)
A DDS field can inherit its full definition — type, length, decimals, text — from another field’s dictionary entry via REFFLD.
- Detect: the
REF/REFFLDkeyword on a DDS field; a field with no inline type. - Decode: resolve the reference to the source field’s definition before you record the type. The referenced field may live in another file, so the true type is not in the file you are reading. Glossary: reference field.
Coded and flag fields (single-char status codes)
A one-character field often carries a whole state machine: 'A' approved, 'P' pending, 'D' denied.
- Detect: a
PIC Xor 1-char DDS field tested against literals; COBOL88-level condition names bound to it. - Decode: harvest every value and its meaning. The
88-levels are gold — they name the states in plain English (88 CLAIM-APPROVED VALUE 'A'). On IBM i, the meanings hide inIF/SELECTcomparisons and message files. Put the full value list in the field dictionary’s “Coded values” column.
FILLER
FILLER is unnamed space in a record — but it is not always dead.
- Detect: the
FILLERkeyword (COBOL) or an unnamed gap in a DDS/flat layout. - Decode: confirm it is truly reserved.
FILLERis a favorite hiding place for a field someone repurposed without renaming, or bytes later reclaimed by aREDEFINES. Note its offset and length so a re-add of meaning is caught.
Inferring relationships without foreign keys
Legacy files almost never declare foreign keys — the database engine of 1990 did not enforce them, so the relationships live in naming, access code, and convention. You infer them, and you mark how confident you are.
Signals that a relationship exists
- Shared key field names.
CUSTNOin bothCUSTMASTandORDHDRis a near-certain link. Watch for renamed twins —CUST-NOhere,CUSTOMER-NUMBERthere — same value, different label. - Naming conventions. A shop-wide prefix (
CLfor claims,EMfor employers) groups a file family; a*MAST/*TRANS/*HISTsuffix pattern signals master-detail-archive triples. - DDS join logical files. A join LF declares the relationship in the layout — the join fields are the foreign key, stated outright. Harvest these first; they are the only relationships the legacy system wrote down.
- RPG access patterns. A
CHAINto file B using a field read from file A is a lookup — a many-to-one from A to B. ASETLL+READEloop on B keyed by an A value is a one-to-many (read all children of one parent). These key patterns are defined inibm-i-as400.md. - Copybook reuse across files. The same copybook
COPY’d into several programs means those programs share a record and likely a responsibility — a structural relationship even without a key. Seemainframe-cobol.md. - EVALUATE / lookup on code fields. An
EVALUATEor table lookup that maps a code to a description implies a lookup/reference entity — the code field points at a (often implicit) code table.
Inferring cardinality, and marking confidence
Cardinality comes from the access pattern, not the layout. A single-record CHAIN reads one parent per child (child → parent is many-to-one). A READE loop reads many children per parent (parent → child is one-to-many). A unique key on the child’s foreign field would make it one-to-one — confirm against the data.
Never present an inferred relationship as fact. Tag each edge with a confidence level and the evidence:
| Confidence | Basis | Example |
|---|---|---|
| Declared | A join LF, a DB2 constraint, or DDL | CLAIMLF joins CLAIM to CLAIMANT on CLMID |
| Strong | Shared key name plus matching access code | CHAIN CUSTNO from ORDHDR into CUSTMAST |
| Inferred | Naming/convention only, no access code seen yet | EM-prefixed files assumed one employer family |
| Guess | A hunch worth checking against data | FILLER bytes may hold a region code |
What to notice: the confidence column is the load-bearing one — it converts each relationship into a testable claim (“confirm CHAIN CUSTNO resolves 1:1 against CUSTMAST”), so a reviewer knows exactly what still needs proving against live data.
Building the ERD and field dictionary
The output of phase 2 is two artifacts: a logical ERD (entities, keys, relationships) and a field dictionary per entity (the decoded columns). Together they replace the schema the legacy system never wrote.
The logical ERD
Draw entities as boxes, primary keys and foreign keys marked, and relationships as lines carrying the inferred cardinality. Use the erDiagram pattern in mermaid-legacy-patterns.md — follow the technical-writing skill’s diagram rules before emitting the block, and put a “what to notice” line beneath it. Keep it logical, not physical: model what the data means (Claim, Claimant, Employer), collapsing the master/trans/hist file split into the entity it represents. Note the confidence of each inferred edge on or beside the line.
The field dictionary
One table per entity. This is where the decode-gotcha work becomes durable — every packed field, implied decimal, and coded value is written down once so no downstream engineer repeats Jack’s mistake.
| Field | Type / decoded | Meaning | Coded values | Notes |
|---|---|---|---|---|
CLMID | Packed 9,0 → int64 | Claim identifier (PK) | - | Join key to CLAIMANT (strong) |
CLMAMT | S9(7)V99 COMP-3 → decimal(9,2) | Weekly benefit amount | - | Implied 2 decimals; sign nibble last byte |
CLMSTS | X(1) → enum | Claim status | A approved, P pending, D denied | From 88-levels; see rule catalog |
CLMDTE | Packed CYYMMDD → date | Claim filed date | - | Century digit: 0=19xx, 1=20xx |
FILLER1 | X(4) reserved | Unused (verify) | - | Confirm not repurposed |
What to notice: the “Type / decoded” column always shows both the raw legacy type and the modern target type with an arrow between them — that arrow is the extraction work, and its absence means a field was transcribed rather than decoded.
Verify before you trust the model
The data model is where a reverse-engineering pass is most often subtly wrong — a foreign key stated backwards, a byte-width off by two, a constraint quoted from a commented-out line. These errors pass every readability check and then break a literal reimplementation. Treat the first extraction as a draft, and re-open the source before the ERD ships.
For every key, type, and foreign key in the ERD, go back to the source line and confirm three things:
- Direction. Re-read the access code that establishes the FK. A
CHAINfromORDHDRintoCUSTMASTmakesORDHDRthe child, not the parent — confirm the arrow points child → parent, not the reverse. - Width. Re-read the
PIC/USAGEor DDS type and recompute the byte length yourself. APIC S9(7)V99 COMP-3is 5 bytes; a 2-byte length prefix is not 4. Do not carry a width forward from the first read. - Live vs. commented-out. State whether a constraint or key is active in the source or sitting in a comment / dead
---guarded DDL. A commentedON DELETE CASCADEis not a live foreign key — say so explicitly rather than promoting it to the ERD.
This is a source re-read, distinct from validating against live data below: this confirms you read the source correctly; that confirms the source matches the running box. Do both when a live connection exists; do at least this one always.
Validating against live data
A reverse-engineered model is a hypothesis until you check it. If a live DB2 or database connection or a data dictionary exists, confirm the model two ways — against the catalog and against sample rows.
Confirm structure against the catalog
Compare your entities and columns to the system catalog: QSYS2.SYSTABLES / SYSCOLUMNS / SYSKEYS on IBM i, SYSIBM.SYSTABLES / SYSCOLUMNS on DB2 for z/OS. The catalog gives real column names, types, lengths, and declared keys — reconcile every field, and treat any mismatch as a decode error to run down, not a catalog error. DDS-defined files and copybook-only files may have no catalog row; those stay reverse-engineered and marked as such.
Confirm values against sample rows
Pull a small sample and test each gotcha against reality:
- Packed and zoned fields decode to plausible business numbers, not astronomical or negative-where-impossible values.
- Implied-decimal scaling produces money that matches a known report total.
- Coded fields contain only the values your dictionary lists — an unlisted code is an undocumented state.
- Date fields fall in a sane range once decoded; a
1900or2099row usually means the wrong date form.
flowchart TD RE[Reverse-engineered model]:::a --> Q{Live connection<br/>or dictionary?}:::b Q -->|no| MARK[Ship as inferred,<br/>mark confidence]:::c Q -->|yes| CAT[Reconcile vs catalog<br/>SYSTABLES / SYSCOLUMNS]:::d CAT --> SMP[Sample rows,<br/>test each gotcha]:::d SMP --> OK{Matches?}:::b OK -->|yes| CONF[Mark confirmed]:::e OK -->|no| FIX[Fix decode,<br/>re-sample]:::c FIX --> SMP classDef a fill:#e8eef7,stroke:#5b7aa8,color:#1a2a3a classDef b fill:#f3ecda,stroke:#b08a3a,color:#3a2e14 classDef c fill:#f2e3e0,stroke:#a8685b,color:#3a1e1a classDef d fill:#e5efe6,stroke:#5a8a5f,color:#1e2e1f classDef e fill:#e8e2ef,stroke:#7a5ba8,color:#241a3aWhat to notice: the model is never “done” — it is either confirmed against live data or shipped as inferred with its confidence marked, and the loop back from a value mismatch to fix-and-re-sample is where most silent decode errors get caught before a rebuild inherits them.