TAFC to TAFJ
Why a Select That Worked in TAFC Returned Nothing in TAFJ
The routine compiled. It ran. It produced an output file with a header and a footer. There were no detail lines. This is that story.
There is a particular category of migration defect that is more annoying than the kind that throws an error. It is the kind that succeeds — quietly, politely, and completely wrong.
The batch export routine ran. It created the output file. It wrote the header. It wrote the footer. It exited cleanly. The only problem was that there were no detail lines in the file, because the select had returned nothing, because TAFJ had silently declined to resolve a field in exactly the same way TAFC had been resolving it for years.
No error. No warning. Just an empty export and a very quiet audit trail.
The select
The original logic looked straightforward. Field names below are deliberately obfuscated, but the pattern is real:
SELECT F.SOURCE.FILE WITH PRIMARY.KEY NE "" AND DERIVED.FLAG EQ "Y" BY PRIMARY.KEYUnder TAFJ, that returned no records. No runtime error. No indication anything had gone wrong. The select simply found nothing to select.
The first clue
The field being filtered on — DERIVED.FLAG — was visible in the TAFC DICT list. It could not be found physically on the raw source record.
That is an important distinction in T24 work, and one that is easy to miss. Seeing a field name in DICT does not prove it is stored on the file. It may be derived, calculated, or backed by subroutine logic. The DICT entry is the description. It is not necessarily the data.
The root cause
Once the dictionary definition was checked, the problem became clear:
SUBR("ENQ.TRANS","STATUS.ENQUIRY",@1,"DERIVED.FLAG")The field was an I-type derived item. The value was being resolved through enquiry logic, not stored directly on the source file. The select was filtering on something that only existed at runtime through a subroutine call — and TAFJ, quite reasonably, was not prepared to run that call at the database selection layer.
What was actually happening
Once the dependency chain was traced, the real data flow was much simpler than the DICT field made it look:
F.SOURCE.FILE<1> -> STATUS.CODE STATUS.CODE -> F.STATUS.LOOKUP.@ID F.STATUS.LOOKUP<2> -> DERIVED.FLAG
The select was relying on hidden cross-file logic. The real value did not live on the file being selected. It lived two reads away from it, through a lookup table, via a field that TAFC had been quietly resolving for years without anyone realising that was what it was doing.
Why TAFC and TAFJ diverged
TAFC often tolerated selection patterns built on derived dictionary items well enough that teams came to rely on them. The behaviour was not documented as a feature. It was just something that worked, until it did not.
TAFJ is less forgiving. The issue was not that ENQ.TRANS itself was unsupported. The issue was expecting the database selection layer to behave like the enquiry and runtime layer. In TAFJ, those are different things with a clear boundary between them. TAFC was more relaxed about that boundary. The migration made it visible.
There is a wider rule behind this. TAFJ does not support IDESC or I-descriptor behaviour in the same way older environments did. That matters because teams may still see the DICT entry in metadata and assume the field should behave normally at runtime. The dictionary entry can exist while the old descriptor-style behaviour is no longer something you can safely build batch logic around.
This is the migration trap. A routine can appear technically fine — it compiles, it runs, it produces output — and still return the wrong result because a hidden assumption no longer holds. TAFJ does not always tell you which assumption that was.
The fix
Stop filtering on the derived DICT field in the select. Replace the derived dependency with explicit reads.
The old logic:
SELECT F.SOURCE.FILE WITH PRIMARY.KEY NE "" AND DERIVED.FLAG EQ "Y" BY PRIMARY.KEYThe corrected TAFJ-safe logic:
SELECT F.SOURCE.FILE WITH PRIMARY.KEY NE "" BY PRIMARY.KEYThen inside the processing loop:
- Read the source record.
- Get the status code from the source file.
- Read the status lookup file using that code.
- Check the real stored flag on the lookup record.
- Only continue when the value matches the required business rule.
The minimal replacement
READ SOURCE.REC FROM F.SOURCE.FILE, REC.ID ELSE CONTINUE
STATUS.CODE = SOURCE.REC<1>
IF STATUS.CODE EQ "" THEN CONTINUE
READ LOOKUP.REC FROM F.STATUS.LOOKUP, STATUS.CODE ELSE CONTINUE
IF LOOKUP.REC<2> NE "Y" THEN CONTINUEThis makes the dependency explicit. Anyone reading the routine can see exactly what data is being checked and where it lives. There is no subroutine call hidden inside a DICT entry pretending to be a normal field.
Why this is the better pattern anyway
It is clearer
The export eligibility logic is now readable. Nobody needs to open the DICT to understand what the select is actually doing.
It is safer
Batch logic that depends on stored fields is more predictable across environments, migrations, and future upgrades.
It is easier to support
When the result set is wrong, you can test the stored fields directly. You are not reverse engineering a DICT chain at 11pm.
The wider pattern to check for
This is not just one export story. It is a migration pattern worth actively hunting for in older routines before they surprise you in production:
- I-type DICT fields used in
SELECTstatements - Old
IDESC-style assumptions carried forward from TAFC SUBR(...)-backed dictionary itemsENQ.TRANS-driven logic hidden behind a normal-looking field name- Cross-file business rules only visible once the DICT is opened
The test to apply to any select that involves a DICT field: open the definition and check what type it is. If it is derived, enquiry-driven, or backed by a subroutine, assume you may need to replace the selection logic with explicit reads. Do this before migration testing, not during it.
A routine that compiles and runs is not necessarily correct. This is true in every environment. In a TAFJ migration, it is especially true for anything that relied on TAFC being more lenient than it had any right to be.
Related reading
TAFJ DBTools: The SELECT Replacement Nobody Tells You About
In TAFC you ran a SELECT in the command line. In TAFJ that is gone. The replacement is DBTools — a separate console with its own users, its own modes, and a 200-row default limit nobody mentions.
TAFC to TAFJTAFJ Descriptor Limitations: Why Old TAFC Logic Breaks in TAFJ
The DICT item still exists. The routine compiles. But the runtime behaviour is different. A practical guide to descriptor-driven logic in TAFJ and why it is not what it looks like.
TAFC to TAFJTAFJ Compilation: Why Your Changes Aren't Doing Anything
In TAFJ, editing source code changes nothing until you compile, package, and deploy. The full pipeline: tCompile, tIntegrate, tComponentSplitter, tMerge — and what each one does.
