Fix It in the Model or Fix It in the Source?

Fix It in the Model or Fix It in the Source?

# ca2e# ibmi# rpgle# sql
Fix It in the Model or Fix It in the Source?Jay

Fix It in the Model or Fix It in the Source? The first compile error in a...

Fix It in the Model or Fix It in the Source?

The first compile error in a converted CA 2E program forces a decision most teams never make on purpose: which copy is the master now


Getting a CA 2E-generated program to compile is mostly mechanical. Article 7 covers the four causes that account for almost all of the errors. But every fix you make raises a question the compile listing never asks: where did you make it?

There are two places a fix can go. You can change the model, meaning the action diagram, the access path or the function's settings, and regenerate. Or you can change the generated source: open the member the generator wrote, edit the line and compile.

Both produce a working object. Only one of them is still true the next time someone presses "generate."

The previous post was about deciding which programs to convert. This one is about what happens to the first one you do. When we converted our first programs, every fix went into the generated source. Whether or not that was the right call, it was a decision, and it should have been written down as one, because it changes what the model is for those programs from that moment on.


What each choice actually means

Fixing in the model keeps the model as the source of truth. A change to an access path or an action diagram survives every future generation, and the model stays an accurate description of the program. The costs are real, though. You need someone who can work in the model's editors. And a model change can reach further than the program you're fixing: an access path is shared by every function that uses it, so adding fields to one to fix one program changes what it gives everything else.

Fixing in the generated source is fast. It's ordinary RPG editing in an ordinary source member, and anyone who reads RPG can do it. For the generator, though, the change doesn't exist. The next time that function is generated, the member is overwritten and the fix disappears without a word. Worse, the model now describes a program that isn't the one running. Anyone who trusts the model to explain the program is reading fiction.

Neither of those is wrong. What's wrong is not knowing which one you're in.


The kinds of fixes, and where each belongs

Most of the fixes from our conversion fell into a few kinds. Each one has a natural home.

The library list at compile time. Not a source change at all. It belongs in the build procedure, either the model's settings or the compile command. Fixing it by editing source is the wrong layer entirely.

A stale or duplicate generated member. Also not a code change. Delete it and regenerate, in either world.

A read-by-key that names something the compiler can't see. In one of our programs, the generated CHAIN pointed at the generator's own internal name for an access path, and the compiler reported it as undefined. Swapping in the record format name was the obvious first try, and it hit the same error. That's typical of this kind of fix: in the source it looks like a one-word edit, but the real cause is how the access path is defined in the model. If the program is staying in the model, fix it there, or the same line comes back broken on every generation.

Fields the program references that aren't on the logical file it reads. In source, you can drop the reference, but only once you've confirmed the values come from another file the program already reads. That's what we did, and in our case it was correct. In the model, the equivalent decision is either to add the fields to that access path (which affects every function sharing it) or to change which access path the function reads through. A deleted reference in generated source is a judgment call that's invisible to the model. If it was the right call, the model should say so too.

Here's the shape of that fix in generated fixed-format RPG IV, with names replaced by placeholders. Before:

     C     KEYLST1       CHAIN     IDXFMT                             90
     C                   IF        *IN90 = *OFF
     C                   EVAL      WFIELD1 = FIELD1
     C                   EVAL      WFIELD2 = FIELD2
     C                   ENDIF
Enter fullscreen mode Exit fullscreen mode

The compiler reads the index's record format, which doesn't carry FIELD1 or FIELD2, so both lines fail as undefined. After the fix:

     C     KEYLST1       CHAIN     IDXFMT                             90
     C                   IF        *IN90 = *OFF
     C* CHG003 FIELD1/FIELD2 are on the physical file but not on
     C*        the index format. Already loaded from OTHERFILE
     C*        (seq 0612) and OTHERFILE2 (seq 0655). See ledger.
     C*                  EVAL      WFIELD1 = FIELD1
     C*                  EVAL      WFIELD2 = FIELD2
     C                   ENDIF
Enter fullscreen mode Exit fullscreen mode

Two habits are worth copying from this. The lines are commented out, not deleted, so anyone comparing against a fresh generation sees exactly what changed. And the comment carries a ledger ID (CHG003) plus where the values really come from, which is the part someone will need to check in a year. The comment states the reason; the ledger (below) records who confirmed it and how.

The pattern: build and environment problems belong in the build. Logic and data-access problems belong wherever the program will be maintained from now on.


Decide per program, up front

The question that settles it isn't "which is easier for this error?" It's "after this conversion, where will this program be maintained?"

  • It stays in the model. The conversion's purpose was to change the target language, and the model is still the place people make changes. Then every logic fix goes into the model and you regenerate. Editing generated source is only for experiments, and the edits are thrown away afterwards.
  • It leaves the model. The conversion's purpose was to get off the generator. Then fix the generated source, and formally retire the function from the model at the same moment. Move the source out of the generation library into your normal source control and source library. Mark the function in the model so nobody regenerates it. From this point the RPG is the program, and the model is history.

Both are valid. What causes trouble is the third state, which is where most conversions end up by accident: fixes made in the source, while the model is still treated as the master. Everything works until someone regenerates the function for an unrelated reason, months later. The fixes quietly disappear, the program compiles again (or doesn't), and nobody connects the new bug to a conversion that "finished" last year.


Generation libraries are not source control

One detail makes the accidental state more likely than it should be.

The generated source lands in a generation library the model maintains alongside itself. That library is created and filled by generating. In our model copy, it only appeared once source was first generated. It's a work area, owned by the generator, and nothing in the generator protects your edits in it.

So if you fix code there, the fixed member has to go somewhere the generator doesn't own before anyone regenerates. That's the step that most often gets skipped, because the compiled object works fine either way.

You can find the members at risk with one query. QSYS2.SYSPARTITIONSTAT has a row per source member, including when its source was last changed. List every member that exists only in the generation library, with no copy in your managed source library, and flag the ones edited after their program was last compiled:

-- Members that exist only in the generator's work area
SELECT g.system_table_member          AS member,
       g.last_source_update_timestamp AS source_changed,
       o.objcreated                   AS program_compiled,
       CASE WHEN o.objname IS NULL
                 THEN 'NO PROGRAM'
            WHEN g.last_source_update_timestamp > o.objcreated
                 THEN 'EDITED, NOT RECOMPILED'
            ELSE 'COMPILED FROM THIS'
       END                            AS state
  FROM qsys2.syspartitionstat g
  LEFT JOIN qsys2.syspartitionstat m
         ON m.system_table_schema = 'SRCLIB'
        AND m.system_table_name   = 'QRPGLESRC'
        AND m.system_table_member = g.system_table_member
  LEFT JOIN TABLE (QSYS2.OBJECT_STATISTICS('PGMLIB', '*PGM')) o
         ON o.objname = g.system_table_member
 WHERE g.system_table_schema = 'GENLIB'
   AND g.system_table_name   = 'QRPGLESRC'
   AND m.system_table_member IS NULL          -- no managed copy anywhere
 ORDER BY g.last_source_update_timestamp DESC;
Enter fullscreen mode Exit fullscreen mode

Timestamps alone can't tell a hand edit from a fresh generation, since both change the member. So read the result as "everything a regeneration could overwrite", then cross it with the change ledger (below) to see which of those members actually carry hand fixes. Run both before anyone generates anything in that model, and the accidental state stops being invisible.

When a program has left the model, the move is a few commands. A small CL program makes it repeatable and impossible to half-do:

PGM        PARM(&PGM)
  DCL      VAR(&PGM) TYPE(*CHAR) LEN(10)

  /* 1. Copy the fixed source out of the generator's work area */
  CPYSRCF  FROMFILE(GENLIB/QRPGLESRC) TOFILE(SRCLIB/QRPGLESRC) +
             FROMMBR(&PGM) TOMBR(*FROMMBR) MBROPT(*REPLACE)

  /* 2. Say so on the member itself                             */
  CHGPFM   FILE(SRCLIB/QRPGLESRC) MBR(&PGM) +
             TEXT('Left CA 2E model - maintain here, never regenerate')

  /* 3. Rebuild from the managed copy, so the object and source  */
  /*    you keep are provably the same                           */
  CRTBNDRPG PGM(PGMLIB/&PGM) SRCFILE(SRCLIB/QRPGLESRC) +
             SRCMBR(&PGM) REPLACE(*YES)

  /* 4. Remove the generated member so a stale copy can't be     */
  /*    edited or compiled by mistake                            */
  RMVM     FILE(GENLIB/QRPGLESRC) MBR(&PGM)
ENDPGM
Enter fullscreen mode Exit fullscreen mode

Step 3 matters more than it looks. Compiling from the managed copy proves the source you're keeping is the source that's running. Step 4 is deliberately last, after the rebuild has succeeded. The one thing this can't do is mark the function in the model itself. That's a step in the model's own tooling, and it belongs on the same checklist.


Keep a change ledger, whichever way you go

The one practice from our conversion I'd keep no matter what: every fix to a generated program was written up as it was made, in a document listing the member, the change and the reason, for future reference. At the time it looked like extra paperwork. It's the only thing that would let someone else answer any of these questions:

  • If this function is regenerated, what will break?
  • Which fixes were environment problems, and which were real logic or data-access decisions?
  • Which removed field references were confirmed redundant, and how was that confirmed?

If the program stays in the model, the ledger is the list of model changes still to make. If it leaves the model, the ledger is the first page of its new history. Either way it should live with the source, not in someone's inbox.

Ours was a document. If you're converting more than a handful of programs, make it a table instead, so you can query it:

CREATE TABLE convlib.conv_ledger (
  chg_id        CHAR(6)       NOT NULL,   -- CHG003, matches the source comment
  program       CHAR(10)      NOT NULL,
  member        CHAR(10)      NOT NULL,
  source_seq    DECIMAL(6, 2),            -- generated line, e.g. 0612.00
  fix_kind      CHAR(8)       NOT NULL,   -- BUILD / STALE / KEYNAME / FIELDREF / LOGIC
  fix_home      CHAR(6)       NOT NULL,   -- BUILD / MODEL / SOURCE
  applied_in    CHAR(6)       NOT NULL,   -- where it has actually been made so far
  reason        VARCHAR(500)  NOT NULL,
  verified_how  VARCHAR(500),             -- e.g. "values traced to OTHERFILE read at 0612"
  changed_by    CHAR(10)      NOT NULL,
  changed_on    DATE          NOT NULL DEFAULT CURRENT DATE,
  PRIMARY KEY (chg_id)
);

INSERT INTO convlib.conv_ledger
  (chg_id, program, member, source_seq, fix_kind, fix_home, applied_in,
   reason, verified_how, changed_by)
VALUES
  ('CHG003', 'PGMNAME', 'PGMNAME', 0533.00, 'FIELDREF', 'MODEL', 'SOURCE',
   'FIELD1/FIELD2 not on index format; references commented out',
   'Both values already loaded from OTHERFILE (0612) and OTHERFILE2 (0655)',
   'DEVUSER');
Enter fullscreen mode Exit fullscreen mode

Two columns do most of the work. fix_home is where the fix should live. applied_in is where it actually lives today. Whenever they differ, the ledger is telling you about a decision that hasn't been finished.

Then the question nobody can answer from a document becomes one query:

-- "If we regenerate these programs today, what do we lose?"
SELECT program, chg_id, fix_kind, source_seq, reason
  FROM convlib.conv_ledger
 WHERE applied_in = 'SOURCE'          -- only exists in generated source
   AND fix_home  <> 'SOURCE'          -- ...but is supposed to live elsewhere
 ORDER BY program, source_seq;
Enter fullscreen mode Exit fullscreen mode

An empty result means every fix is where it belongs, and regenerating is safe. Anything else is a list of model changes to make first, or a program that should be formally retired from the model with the CL above.


Regeneration is an undo button, until it isn't

There's one upside to fixing in source that's worth naming. While a program is still being converted, regenerating is a clean reset. Our first pilot's first round of fixes went the wrong way. The fix was to regenerate and start again, and it cost a few hours instead of an untangling exercise.

That undo button works only until you've decided the program has left the model. After that, regenerating isn't undo. It's deleting the program's real source and replacing it with an older idea of it. Knowing which side of that line each program is on is the whole point of deciding up front.

The next post is about the line no conversion should cross without checking: proving the converted program does the same thing as the one it replaces.


Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.